349 lines
7.3 KiB
PHP
349 lines
7.3 KiB
PHP
|
<?php
|
||
|
namespace STS\Backoff;
|
||
|
|
||
|
use Exception;
|
||
|
use InvalidArgumentException;
|
||
|
use STS\Backoff\Strategies\ConstantStrategy;
|
||
|
use STS\Backoff\Strategies\ExponentialStrategy;
|
||
|
use STS\Backoff\Strategies\LinearStrategy;
|
||
|
use STS\Backoff\Strategies\PolynomialStrategy;
|
||
|
|
||
|
/**
|
||
|
* Class Retry
|
||
|
* @package STS\Backoff
|
||
|
*/
|
||
|
class Backoff
|
||
|
{
|
||
|
/**
|
||
|
* @var string
|
||
|
*/
|
||
|
public static $defaultStrategy = "polynomial";
|
||
|
|
||
|
/**
|
||
|
* @var int
|
||
|
*/
|
||
|
public static $defaultMaxAttempts = 5;
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
public static $defaultJitterEnabled = false;
|
||
|
|
||
|
/**
|
||
|
* This callable should take an 'attempt' integer, and return a wait time in milliseconds
|
||
|
*
|
||
|
* @var callable
|
||
|
*/
|
||
|
protected $strategy;
|
||
|
|
||
|
/**
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $strategies = [
|
||
|
'constant' => ConstantStrategy::class,
|
||
|
'linear' => LinearStrategy::class,
|
||
|
'polynomial' => PolynomialStrategy::class,
|
||
|
'exponential' => ExponentialStrategy::class
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* @var int
|
||
|
*/
|
||
|
protected $maxAttempts;
|
||
|
|
||
|
/**
|
||
|
* The max wait time you want to allow, regardless of what the strategy says
|
||
|
*
|
||
|
* @var int|null In milliseconds
|
||
|
*/
|
||
|
protected $waitCap;
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
protected $useJitter = false;
|
||
|
|
||
|
/**
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $exceptions = [];
|
||
|
|
||
|
/**
|
||
|
* This will decide whether to retry or not.
|
||
|
* @var callable
|
||
|
*/
|
||
|
protected $decider;
|
||
|
|
||
|
/**
|
||
|
* This receive any exceptions we encounter.
|
||
|
* @var callable
|
||
|
*/
|
||
|
protected $errorHandler;
|
||
|
|
||
|
/**
|
||
|
* @param int $maxAttempts
|
||
|
* @param mixed $strategy
|
||
|
* @param int $waitCap
|
||
|
* @param bool $useJitter
|
||
|
* @param callable $decider
|
||
|
*/
|
||
|
public function __construct(
|
||
|
$maxAttempts = null,
|
||
|
$strategy = null,
|
||
|
$waitCap = null,
|
||
|
$useJitter = null,
|
||
|
$decider = null
|
||
|
) {
|
||
|
$this->setMaxAttempts($maxAttempts ?: self::$defaultMaxAttempts);
|
||
|
$this->setStrategy($strategy ?: self::$defaultStrategy);
|
||
|
$this->setJitter($useJitter ?: self::$defaultJitterEnabled);
|
||
|
$this->setWaitCap($waitCap);
|
||
|
$this->setDecider($decider ?: $this->getDefaultDecider());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param integer $attempts
|
||
|
*/
|
||
|
public function setMaxAttempts($attempts)
|
||
|
{
|
||
|
$this->maxAttempts = $attempts;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return integer
|
||
|
*/
|
||
|
public function getMaxAttempts()
|
||
|
{
|
||
|
return $this->maxAttempts;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param int|null $cap
|
||
|
*
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setWaitCap($cap)
|
||
|
{
|
||
|
$this->waitCap = $cap;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return int|null
|
||
|
*/
|
||
|
public function getWaitCap()
|
||
|
{
|
||
|
return $this->waitCap;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $useJitter
|
||
|
*
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setJitter($useJitter)
|
||
|
{
|
||
|
$this->useJitter = $useJitter;
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
*/
|
||
|
public function enableJitter()
|
||
|
{
|
||
|
$this->setJitter(true);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
*/
|
||
|
public function disableJitter()
|
||
|
{
|
||
|
$this->setJitter(false);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
public function jitterEnabled()
|
||
|
{
|
||
|
return $this->useJitter;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return callable
|
||
|
*/
|
||
|
public function getStrategy()
|
||
|
{
|
||
|
return $this->strategy;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param mixed $strategy
|
||
|
*
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setStrategy($strategy)
|
||
|
{
|
||
|
$this->strategy = $this->buildStrategy($strategy);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Builds a callable strategy.
|
||
|
*
|
||
|
* @param mixed $strategy Can be a string that matches a key in $strategies, an instance of AbstractStrategy
|
||
|
* (or any other instance that has an __invoke method), a callback function, or
|
||
|
* an integer (which we interpret to mean you want a ConstantStrategy)
|
||
|
*
|
||
|
* @return callable
|
||
|
*/
|
||
|
protected function buildStrategy($strategy)
|
||
|
{
|
||
|
if (is_string($strategy) && array_key_exists($strategy, $this->strategies)) {
|
||
|
return new $this->strategies[$strategy];
|
||
|
}
|
||
|
|
||
|
if (is_callable($strategy)) {
|
||
|
return $strategy;
|
||
|
}
|
||
|
|
||
|
if (is_int($strategy)) {
|
||
|
return new ConstantStrategy($strategy);
|
||
|
}
|
||
|
|
||
|
throw new InvalidArgumentException("Invalid strategy: " . $strategy);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param callable $callback
|
||
|
*
|
||
|
* @return mixed
|
||
|
* @throws Exception
|
||
|
*/
|
||
|
public function run($callback)
|
||
|
{
|
||
|
$attempt = 0;
|
||
|
$try = true;
|
||
|
|
||
|
while ($try) {
|
||
|
|
||
|
$result = null;
|
||
|
$exception = null;
|
||
|
|
||
|
$this->wait($attempt);
|
||
|
try {
|
||
|
$result = call_user_func($callback);
|
||
|
} catch (\Throwable $e) {
|
||
|
if ($e instanceof \Error) {
|
||
|
$e = new Exception($e->getMessage(), $e->getCode(), $e);
|
||
|
}
|
||
|
$this->exceptions[] = $e;
|
||
|
$exception = $e;
|
||
|
} catch (Exception $e) {
|
||
|
$this->exceptions[] = $e;
|
||
|
$exception = $e;
|
||
|
}
|
||
|
$try = call_user_func($this->decider, ++$attempt, $this->getMaxAttempts(), $result, $exception);
|
||
|
|
||
|
if($try && isset($this->errorHandler)) {
|
||
|
call_user_func($this->errorHandler, $exception, $attempt, $this->getMaxAttempts());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the decider callback
|
||
|
* @param callable $callback
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setDecider($callback)
|
||
|
{
|
||
|
$this->decider = $callback;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the error handler callback
|
||
|
* @param callable $callback
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function setErrorHandler($callback)
|
||
|
{
|
||
|
$this->errorHandler = $callback;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets a default decider that simply check exceptions and maxattempts
|
||
|
* @return \Closure
|
||
|
*/
|
||
|
protected function getDefaultDecider()
|
||
|
{
|
||
|
return function ($retry, $maxAttempts, $result = null, $exception = null) {
|
||
|
if($retry >= $maxAttempts && ! is_null($exception)) {
|
||
|
throw $exception;
|
||
|
}
|
||
|
|
||
|
return $retry < $maxAttempts && !is_null($exception);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param int $attempt
|
||
|
*/
|
||
|
public function wait($attempt)
|
||
|
{
|
||
|
if ($attempt == 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
usleep($this->getWaitTime($attempt) * 1000);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param int $attempt
|
||
|
*
|
||
|
* @return int
|
||
|
*/
|
||
|
public function getWaitTime($attempt)
|
||
|
{
|
||
|
$waitTime = call_user_func($this->getStrategy(), $attempt);
|
||
|
|
||
|
return $this->jitter($this->cap($waitTime));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param int $waitTime
|
||
|
*
|
||
|
* @return mixed
|
||
|
*/
|
||
|
protected function cap($waitTime)
|
||
|
{
|
||
|
return is_int($this->getWaitCap())
|
||
|
? min($this->getWaitCap(), $waitTime)
|
||
|
: $waitTime;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param int $waitTime
|
||
|
*
|
||
|
* @return int
|
||
|
*/
|
||
|
protected function jitter($waitTime)
|
||
|
{
|
||
|
return $this->jitterEnabled()
|
||
|
? mt_rand(0, $waitTime)
|
||
|
: $waitTime;
|
||
|
}
|
||
|
}
|