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