AddHostPlugin.php
3 years ago
AddPathPlugin.php
3 years ago
AuthenticationPlugin.php
3 years ago
BaseUriPlugin.php
3 years ago
ContentLengthPlugin.php
3 years ago
ContentTypePlugin.php
3 years ago
CookiePlugin.php
3 years ago
DecoderPlugin.php
3 years ago
ErrorPlugin.php
3 years ago
HeaderAppendPlugin.php
3 years ago
HeaderDefaultsPlugin.php
3 years ago
HeaderRemovePlugin.php
3 years ago
HeaderSetPlugin.php
3 years ago
HistoryPlugin.php
3 years ago
Journal.php
3 years ago
QueryDefaultsPlugin.php
3 years ago
RedirectPlugin.php
3 years ago
RequestMatcherPlugin.php
3 years ago
RetryPlugin.php
3 years ago
VersionBridgePlugin.php
3 years ago
RetryPlugin.php
140 lines
| 1 | <?php |
| 2 | |
| 3 | namespace AmeliaHttp\Client\Common\Plugin; |
| 4 | |
| 5 | use AmeliaHttp\Client\Common\Plugin; |
| 6 | use AmeliaHttp\Client\Exception; |
| 7 | use AmeliaPsr\Http\Message\RequestInterface; |
| 8 | use AmeliaPsr\Http\Message\ResponseInterface; |
| 9 | use Symfony\Component\OptionsResolver\OptionsResolver; |
| 10 | |
| 11 | /** |
| 12 | * Retry the request if an exception is thrown. |
| 13 | * |
| 14 | * By default will retry only one time. |
| 15 | * |
| 16 | * @author Joel Wurtz <joel.wurtz@gmail.com> |
| 17 | */ |
| 18 | final class RetryPlugin implements Plugin |
| 19 | { |
| 20 | /** |
| 21 | * Number of retry before sending an exception. |
| 22 | * |
| 23 | * @var int |
| 24 | */ |
| 25 | private $retry; |
| 26 | |
| 27 | /** |
| 28 | * @var callable |
| 29 | */ |
| 30 | private $exceptionDelay; |
| 31 | |
| 32 | /** |
| 33 | * @var callable |
| 34 | */ |
| 35 | private $exceptionDecider; |
| 36 | |
| 37 | /** |
| 38 | * Store the retry counter for each request. |
| 39 | * |
| 40 | * @var array |
| 41 | */ |
| 42 | private $retryStorage = []; |
| 43 | |
| 44 | /** |
| 45 | * @param array $config { |
| 46 | * |
| 47 | * @var int $retries Number of retries to attempt if an exception occurs before letting the exception bubble up. |
| 48 | * @var callable $exception_decider A callback that gets a request and an exception to decide after a failure whether the request should be retried. |
| 49 | * @var callable $exception_delay A callback that gets a request, an exception and the number of retries and returns how many microseconds we should wait before trying again. |
| 50 | * } |
| 51 | */ |
| 52 | public function __construct(array $config = []) |
| 53 | { |
| 54 | if (array_key_exists('decider', $config)) { |
| 55 | if (array_key_exists('exception_decider', $config)) { |
| 56 | throw new \InvalidArgumentException('Do not set both the old "decider" and new "exception_decider" options'); |
| 57 | } |
| 58 | trigger_error('The "decider" option has been deprecated in favour of "exception_decider"', E_USER_DEPRECATED); |
| 59 | $config['exception_decider'] = $config['decider']; |
| 60 | unset($config['decider']); |
| 61 | } |
| 62 | if (array_key_exists('delay', $config)) { |
| 63 | if (array_key_exists('exception_delay', $config)) { |
| 64 | throw new \InvalidArgumentException('Do not set both the old "delay" and new "exception_delay" options'); |
| 65 | } |
| 66 | trigger_error('The "delay" option has been deprecated in favour of "exception_delay"', E_USER_DEPRECATED); |
| 67 | $config['exception_delay'] = $config['delay']; |
| 68 | unset($config['delay']); |
| 69 | } |
| 70 | |
| 71 | $resolver = new OptionsResolver(); |
| 72 | $resolver->setDefaults([ |
| 73 | 'retries' => 1, |
| 74 | 'exception_decider' => function (RequestInterface $request, Exception $e) { |
| 75 | return true; |
| 76 | }, |
| 77 | 'exception_delay' => __CLASS__.'::defaultDelay', |
| 78 | ]); |
| 79 | $resolver->setAllowedTypes('retries', 'int'); |
| 80 | $resolver->setAllowedTypes('exception_decider', 'callable'); |
| 81 | $resolver->setAllowedTypes('exception_delay', 'callable'); |
| 82 | $options = $resolver->resolve($config); |
| 83 | |
| 84 | $this->retry = $options['retries']; |
| 85 | $this->exceptionDecider = $options['exception_decider']; |
| 86 | $this->exceptionDelay = $options['exception_delay']; |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * {@inheritdoc} |
| 91 | */ |
| 92 | public function handleRequest(RequestInterface $request, callable $next, callable $first) |
| 93 | { |
| 94 | $chainIdentifier = spl_object_hash((object) $first); |
| 95 | |
| 96 | return $next($request)->then(function (ResponseInterface $response) use ($request, $chainIdentifier) { |
| 97 | if (array_key_exists($chainIdentifier, $this->retryStorage)) { |
| 98 | unset($this->retryStorage[$chainIdentifier]); |
| 99 | } |
| 100 | |
| 101 | return $response; |
| 102 | }, function (Exception $exception) use ($request, $next, $first, $chainIdentifier) { |
| 103 | if (!array_key_exists($chainIdentifier, $this->retryStorage)) { |
| 104 | $this->retryStorage[$chainIdentifier] = 0; |
| 105 | } |
| 106 | |
| 107 | if ($this->retryStorage[$chainIdentifier] >= $this->retry) { |
| 108 | unset($this->retryStorage[$chainIdentifier]); |
| 109 | |
| 110 | throw $exception; |
| 111 | } |
| 112 | |
| 113 | if (!call_user_func($this->exceptionDecider, $request, $exception)) { |
| 114 | throw $exception; |
| 115 | } |
| 116 | |
| 117 | $time = call_user_func($this->exceptionDelay, $request, $exception, $this->retryStorage[$chainIdentifier]); |
| 118 | usleep($time); |
| 119 | |
| 120 | // Retry in synchrone |
| 121 | ++$this->retryStorage[$chainIdentifier]; |
| 122 | $promise = $this->handleRequest($request, $next, $first); |
| 123 | |
| 124 | return $promise->wait(); |
| 125 | }); |
| 126 | } |
| 127 | |
| 128 | /** |
| 129 | * @param RequestInterface $request |
| 130 | * @param Exception $e |
| 131 | * @param int $retries The number of retries we made before. First time this get called it will be 0. |
| 132 | * |
| 133 | * @return int |
| 134 | */ |
| 135 | public static function defaultDelay(RequestInterface $request, Exception $e, $retries) |
| 136 | { |
| 137 | return pow(2, $retries) * 500000; |
| 138 | } |
| 139 | } |
| 140 |