Skip to content

Commit b6e5b59

Browse files
committed
no exception should be thrown on server error, but retrying those would make sense
1 parent d375665 commit b6e5b59

File tree

2 files changed

+77
-19
lines changed

2 files changed

+77
-19
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
## 2.0 (unreleased)
44

55
### Changed
6-
- RetryPlugin will no longer retry requests when the response failed with a HTTP code < 500.
6+
- RetryPlugin will no longer retry exceptions when there is a response failed with a HTTP code < 500.
7+
- RetryPlugin also retries when no exception is thrown if the responses has HTTP code >= 500. The callbacks
8+
for exception handling have been renamed and callbacks for response handling have been added.
79
- Abstract method `HttpClientPool::chooseHttpClient()` has now an explicit return type (`Http\Client\Common\HttpClientPoolItem`)
810
- Interface method `Plugin::handleRequest(...)` has now an explicit return type (`Http\Promise\Promise`)
911
- Made classes final that are not intended to be extended.
@@ -13,6 +15,7 @@
1315

1416
### Removed
1517
- Deprecated option `debug_plugins` has been removed from `PluginClient`
18+
- Deprecated options `decider` and `delay` have been removed from `RetryPlugin`, use `exception_decider` and `exception_delay` instead.
1619

1720
## 1.9.0 - [unreleased]
1821

src/Plugin/RetryPlugin.php

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,23 @@ final class RetryPlugin implements Plugin
3131
/**
3232
* @var callable
3333
*/
34-
private $exceptionDelay;
34+
private $errorResponseDelay;
35+
36+
/**
37+
* @var callable
38+
*/
39+
private $errorResponseDecider;
3540

3641
/**
3742
* @var callable
3843
*/
3944
private $exceptionDecider;
4045

46+
/**
47+
* @var callable
48+
*/
49+
private $exceptionDelay;
50+
4151
/**
4252
* Store the retry counter for each request.
4353
*
@@ -49,12 +59,31 @@ final class RetryPlugin implements Plugin
4959
* @param array $config {
5060
*
5161
* @var int $retries Number of retries to attempt if an exception occurs before letting the exception bubble up.
62+
* @var callable $error_response_decider A callback that gets a request and response to decide whether the request should be retried.
5263
* @var callable $exception_decider A callback that gets a request and an exception to decide after a failure whether the request should be retried.
64+
* @var callable $error_response_delay A callback that gets a request and response and the number of retries and returns how many microseconds we should wait before trying again.
5365
* @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.
5466
* }
67+
*
68+
* @since exception_decider and error_response_decider have been added in version 2. In version 1.*, the exception_decider has been called decider.
5569
*/
5670
public function __construct(array $config = [])
5771
{
72+
$resolver = new OptionsResolver();
73+
$resolver->setDefaults([
74+
'retries' => 1,
75+
'error_response_decider' => function (RequestInterface $request, ResponseInterface $response) {
76+
// do not retry client errors
77+
return $response->getStatusCode() >= 500;
78+
},
79+
'exception_decider' => function (RequestInterface $request, Exception $e) {
80+
// do not retry client errors
81+
return !$e instanceof HttpException || $e->getCode() >= 500;
82+
},
83+
'error_response_delay' => __CLASS__.'::defaultResponseDelay',
84+
'exception_delay' => __CLASS__.'::defaultExceptionDelay',
85+
]);
86+
5887
if (array_key_exists('decider', $config)) {
5988
if (array_key_exists('exception_decider', $config)) {
6089
throw new \InvalidArgumentException('Do not set both the old "decider" and new "exception_decider" options');
@@ -72,21 +101,16 @@ public function __construct(array $config = [])
72101
unset($config['delay']);
73102
}
74103

75-
$resolver = new OptionsResolver();
76-
$resolver->setDefaults([
77-
'retries' => 1,
78-
'exception_decider' => function (RequestInterface $request, Exception $e) {
79-
// do not retry client errors
80-
return !$e instanceof HttpException || $e->getCode() >= 500;
81-
},
82-
'exception_delay' => __CLASS__.'::defaultDelay',
83-
]);
84104
$resolver->setAllowedTypes('retries', 'int');
105+
$resolver->setAllowedTypes('error_response_decider', 'callable');
85106
$resolver->setAllowedTypes('exception_decider', 'callable');
107+
$resolver->setAllowedTypes('error_response_delay', 'callable');
86108
$resolver->setAllowedTypes('exception_delay', 'callable');
87109
$options = $resolver->resolve($config);
88110

89111
$this->retry = $options['retries'];
112+
$this->errorResponseDecider = $options['error_response_decider'];
113+
$this->errorResponseDelay = $options['error_response_delay'];
90114
$this->exceptionDecider = $options['exception_decider'];
91115
$this->exceptionDelay = $options['exception_delay'];
92116
}
@@ -98,7 +122,22 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
98122
{
99123
$chainIdentifier = spl_object_hash((object) $first);
100124

101-
return $next($request)->then(function (ResponseInterface $response) use ($request, $chainIdentifier) {
125+
return $next($request)->then(function (ResponseInterface $response) use ($request, $next, $first, $chainIdentifier) {
126+
if (!array_key_exists($chainIdentifier, $this->retryStorage)) {
127+
$this->retryStorage[$chainIdentifier] = 0;
128+
}
129+
130+
if ($this->retryStorage[$chainIdentifier] >= $this->retry) {
131+
unset($this->retryStorage[$chainIdentifier]);
132+
133+
return $response;
134+
}
135+
136+
if (call_user_func($this->errorResponseDecider, $request, $response)) {
137+
$time = call_user_func($this->errorResponseDelay, $request, $response, $this->retryStorage[$chainIdentifier]);
138+
$response = $this->retry($request, $next, $first, $chainIdentifier, $time);
139+
}
140+
102141
if (array_key_exists($chainIdentifier, $this->retryStorage)) {
103142
unset($this->retryStorage[$chainIdentifier]);
104143
}
@@ -120,13 +159,8 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
120159
}
121160

122161
$time = call_user_func($this->exceptionDelay, $request, $exception, $this->retryStorage[$chainIdentifier]);
123-
usleep($time);
124-
125-
// Retry synchronously
126-
++$this->retryStorage[$chainIdentifier];
127-
$promise = $this->handleRequest($request, $next, $first);
128162

129-
return $promise->wait();
163+
return $this->retry($request, $next, $first, $chainIdentifier, $time);
130164
});
131165
}
132166

@@ -135,8 +169,29 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
135169
*
136170
* @return int
137171
*/
138-
public static function defaultDelay(RequestInterface $request, Exception $e, $retries)
172+
public static function defaultErrorResponseDelay(RequestInterface $request, ResponseInterface $response, $retries)
139173
{
140174
return pow(2, $retries) * 500000;
141175
}
176+
177+
/**
178+
* @param int $retries The number of retries we made before. First time this get called it will be 0.
179+
*
180+
* @return int
181+
*/
182+
public static function defaultExceptionDelay(RequestInterface $request, Exception $e, $retries)
183+
{
184+
return pow(2, $retries) * 500000;
185+
}
186+
187+
private function retry(RequestInterface $request, callable $next, callable $first, string $chainIdentifier, int $delay)
188+
{
189+
usleep($delay);
190+
191+
// Retry synchronously
192+
++$this->retryStorage[$chainIdentifier];
193+
$promise = $this->handleRequest($request, $next, $first);
194+
195+
return $promise->wait();
196+
}
142197
}

0 commit comments

Comments
 (0)