123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- <?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;
- }
- }
|