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