PluginProbe
DecaLog / 4.4.0
DecaLog v4.4.0
3.0.2 3.1.0 3.10.0 3.2.0 3.3.0 3.4.0 3.4.1 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.6.3 3.7.0 3.7.1 3.8.0 3.9.0 3.9.1 4.0.0 4.1.0 4.2.0 4.3.0 4.3.1 4.4.0 4.5.0 All 75 releases
decalog / includes / libraries / http / client-common / Plugin / RetryPlugin.php

RetryPlugin.php in DecaLog 4.4.0, at includes/libraries/http/client-common/Plugin/RetryPlugin.php

180 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Http\Client\Common\Plugin;
6
7 use Http\Client\Common\Plugin;
8 use Http\Client\Exception\HttpException;
9 use Http\Promise\Promise;
10 use Psr\Http\Client\ClientExceptionInterface;
11 use Psr\Http\Message\RequestInterface;
12 use Psr\Http\Message\ResponseInterface;
13 use Symfony\Component\OptionsResolver\OptionsResolver;
14
15 /**
16 * Retry the request if an exception is thrown.
17 *
18 * By default will retry only one time.
19 *
20 * @author Joel Wurtz <joel.wurtz@gmail.com>
21 */
22 final class RetryPlugin implements Plugin
23 {
24 /**
25 * Number of retry before sending an exception.
26 *
27 * @var int
28 */
29 private $retry;
30
31 /**
32 * @var callable
33 */
34 private $errorResponseDelay;
35
36 /**
37 * @var callable
38 */
39 private $errorResponseDecider;
40
41 /**
42 * @var callable
43 */
44 private $exceptionDecider;
45
46 /**
47 * @var callable
48 */
49 private $exceptionDelay;
50
51 /**
52 * Store the retry counter for each request.
53 *
54 * @var array
55 */
56 private $retryStorage = [];
57
58 /**
59 * @param array{'retries'?: int, 'error_response_decider'?: callable, 'exception_decider'?: callable, 'error_response_delay'?: callable, 'exception_delay'?: callable} $config
60 *
61 * Configuration options:
62 * - retries: Number of retries to attempt if an exception occurs before letting the exception bubble up
63 * - error_response_decider: A callback that gets a request and response to decide whether the request should be retried
64 * - exception_decider: A callback that gets a request and an exception to decide after a failure whether the request should be retried
65 * - error_response_delay: A callback that gets a request and response and the current number of retries and returns how many microseconds we should wait before trying again
66 * - exception_delay: A callback that gets a request, an exception and the current number of retries and returns how many microseconds we should wait before trying again
67 */
68 public function __construct(array $config = [])
69 {
70 $resolver = new OptionsResolver();
71 $resolver->setDefaults([
72 'retries' => 1,
73 'error_response_decider' => function (RequestInterface $request, ResponseInterface $response) {
74 // do not retry client errors
75 return $response->getStatusCode() >= 500 && $response->getStatusCode() < 600;
76 },
77 'exception_decider' => function (RequestInterface $request, ClientExceptionInterface $e) {
78 // do not retry client errors
79 return !$e instanceof HttpException || $e->getCode() >= 500 && $e->getCode() < 600;
80 },
81 'error_response_delay' => __CLASS__.'::defaultErrorResponseDelay',
82 'exception_delay' => __CLASS__.'::defaultExceptionDelay',
83 ]);
84
85 $resolver->setAllowedTypes('retries', 'int');
86 $resolver->setAllowedTypes('error_response_decider', 'callable');
87 $resolver->setAllowedTypes('exception_decider', 'callable');
88 $resolver->setAllowedTypes('error_response_delay', 'callable');
89 $resolver->setAllowedTypes('exception_delay', 'callable');
90 $options = $resolver->resolve($config);
91
92 $this->retry = $options['retries'];
93 $this->errorResponseDecider = $options['error_response_decider'];
94 $this->errorResponseDelay = $options['error_response_delay'];
95 $this->exceptionDecider = $options['exception_decider'];
96 $this->exceptionDelay = $options['exception_delay'];
97 }
98
99 /**
100 * {@inheritdoc}
101 */
102 public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
103 {
104 $chainIdentifier = spl_object_hash((object) $first);
105
106 return $next($request)->then(function (ResponseInterface $response) use ($request, $next, $first, $chainIdentifier) {
107 if (!array_key_exists($chainIdentifier, $this->retryStorage)) {
108 $this->retryStorage[$chainIdentifier] = 0;
109 }
110
111 if ($this->retryStorage[$chainIdentifier] >= $this->retry) {
112 unset($this->retryStorage[$chainIdentifier]);
113
114 return $response;
115 }
116
117 if (call_user_func($this->errorResponseDecider, $request, $response)) {
118 /** @var int $time */
119 $time = call_user_func($this->errorResponseDelay, $request, $response, $this->retryStorage[$chainIdentifier]);
120 $response = $this->retry($request, $next, $first, $chainIdentifier, $time);
121 }
122
123 if (array_key_exists($chainIdentifier, $this->retryStorage)) {
124 unset($this->retryStorage[$chainIdentifier]);
125 }
126
127 return $response;
128 }, function (ClientExceptionInterface $exception) use ($request, $next, $first, $chainIdentifier) {
129 if (!array_key_exists($chainIdentifier, $this->retryStorage)) {
130 $this->retryStorage[$chainIdentifier] = 0;
131 }
132
133 if ($this->retryStorage[$chainIdentifier] >= $this->retry) {
134 unset($this->retryStorage[$chainIdentifier]);
135
136 throw $exception;
137 }
138
139 if (!call_user_func($this->exceptionDecider, $request, $exception)) {
140 throw $exception;
141 }
142
143 /** @var int $time */
144 $time = call_user_func($this->exceptionDelay, $request, $exception, $this->retryStorage[$chainIdentifier]);
145
146 return $this->retry($request, $next, $first, $chainIdentifier, $time);
147 });
148 }
149
150 /**
151 * @param int $retries The number of retries we made before. First time this get called it will be 0.
152 */
153 public static function defaultErrorResponseDelay(RequestInterface $request, ResponseInterface $response, int $retries): int
154 {
155 return pow(2, $retries) * 500000;
156 }
157
158 /**
159 * @param int $retries The number of retries we made before. First time this get called it will be 0.
160 */
161 public static function defaultExceptionDelay(RequestInterface $request, ClientExceptionInterface $e, int $retries): int
162 {
163 return pow(2, $retries) * 500000;
164 }
165
166 /**
167 * @throws \Exception if retrying returns a failed promise
168 */
169 private function retry(RequestInterface $request, callable $next, callable $first, string $chainIdentifier, int $delay): ResponseInterface
170 {
171 usleep($delay);
172
173 // Retry synchronously
174 ++$this->retryStorage[$chainIdentifier];
175 $promise = $this->handleRequest($request, $next, $first);
176
177 return $promise->wait();
178 }
179 }
180