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