RetryMiddleware.php 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. <?php
  2. namespace GuzzleHttp;
  3. use GuzzleHttp\Promise\PromiseInterface;
  4. use Psr\Http\Message\RequestInterface;
  5. use Psr\Http\Message\ResponseInterface;
  6. /**
  7. * Middleware that retries requests based on the boolean result of
  8. * invoking the provided "decider" function.
  9. *
  10. * @final
  11. */
  12. class RetryMiddleware
  13. {
  14. /**
  15. * @var callable(RequestInterface, array): PromiseInterface
  16. */
  17. private $nextHandler;
  18. /**
  19. * @var callable
  20. */
  21. private $decider;
  22. /**
  23. * @var callable(int)
  24. */
  25. private $delay;
  26. /**
  27. * @param callable $decider Function that accepts the number of retries,
  28. * a request, [response], and [exception] and
  29. * returns true if the request is to be
  30. * retried.
  31. * @param callable(RequestInterface, array): PromiseInterface $nextHandler Next handler to invoke.
  32. * @param null|callable(int): int $delay Function that accepts the number of retries
  33. * and returns the number of
  34. * milliseconds to delay.
  35. */
  36. public function __construct(
  37. callable $decider,
  38. callable $nextHandler,
  39. callable $delay = null
  40. ) {
  41. $this->decider = $decider;
  42. $this->nextHandler = $nextHandler;
  43. $this->delay = $delay ?: __CLASS__ . '::exponentialDelay';
  44. }
  45. /**
  46. * Default exponential backoff delay function.
  47. *
  48. * @return int milliseconds.
  49. */
  50. public static function exponentialDelay(int $retries): int
  51. {
  52. return (int) \pow(2, $retries - 1) * 1000;
  53. }
  54. public function __invoke(RequestInterface $request, array $options): PromiseInterface
  55. {
  56. if (!isset($options['retries'])) {
  57. $options['retries'] = 0;
  58. }
  59. $fn = $this->nextHandler;
  60. return $fn($request, $options)
  61. ->then(
  62. $this->onFulfilled($request, $options),
  63. $this->onRejected($request, $options)
  64. );
  65. }
  66. /**
  67. * Execute fulfilled closure
  68. */
  69. private function onFulfilled(RequestInterface $request, array $options): callable
  70. {
  71. return function ($value) use ($request, $options) {
  72. if (!($this->decider)(
  73. $options['retries'],
  74. $request,
  75. $value,
  76. null
  77. )) {
  78. return $value;
  79. }
  80. return $this->doRetry($request, $options, $value);
  81. };
  82. }
  83. /**
  84. * Execute rejected closure
  85. */
  86. private function onRejected(RequestInterface $req, array $options): callable
  87. {
  88. return function ($reason) use ($req, $options) {
  89. if (!($this->decider)(
  90. $options['retries'],
  91. $req,
  92. null,
  93. $reason
  94. )) {
  95. return \GuzzleHttp\Promise\rejection_for($reason);
  96. }
  97. return $this->doRetry($req, $options);
  98. };
  99. }
  100. private function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null): PromiseInterface
  101. {
  102. $options['delay'] = ($this->delay)(++$options['retries'], $response);
  103. return $this($request, $options);
  104. }
  105. }