Skip to content

Commit 8164de2

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

File tree

2 files changed

+70
-30
lines changed

2 files changed

+70
-30
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: 66 additions & 29 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,44 +59,41 @@ 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
{
58-
if (array_key_exists('decider', $config)) {
59-
if (array_key_exists('exception_decider', $config)) {
60-
throw new \InvalidArgumentException('Do not set both the old "decider" and new "exception_decider" options');
61-
}
62-
trigger_error('The "decider" option has been deprecated in favour of "exception_decider"', E_USER_DEPRECATED);
63-
$config['exception_decider'] = $config['decider'];
64-
unset($config['decider']);
65-
}
66-
if (array_key_exists('delay', $config)) {
67-
if (array_key_exists('exception_delay', $config)) {
68-
throw new \InvalidArgumentException('Do not set both the old "delay" and new "exception_delay" options');
69-
}
70-
trigger_error('The "delay" option has been deprecated in favour of "exception_delay"', E_USER_DEPRECATED);
71-
$config['exception_delay'] = $config['delay'];
72-
unset($config['delay']);
73-
}
74-
7572
$resolver = new OptionsResolver();
7673
$resolver->setDefaults([
7774
'retries' => 1,
75+
'error_response_decider' => function (RequestInterface $request, ResponseInterface $response) {
76+
// do not retry client errors
77+
return $response->getStatusCode() >= 500;
78+
},
7879
'exception_decider' => function (RequestInterface $request, Exception $e) {
7980
// do not retry client errors
8081
return !$e instanceof HttpException || $e->getCode() >= 500;
8182
},
82-
'exception_delay' => __CLASS__.'::defaultDelay',
83+
'error_response_delay' => __CLASS__.'::defaultResponseDelay',
84+
'exception_delay' => __CLASS__.'::defaultExceptionDelay',
8385
]);
86+
8487
$resolver->setAllowedTypes('retries', 'int');
88+
$resolver->setAllowedTypes('error_response_decider', 'callable');
8589
$resolver->setAllowedTypes('exception_decider', 'callable');
90+
$resolver->setAllowedTypes('error_response_delay', 'callable');
8691
$resolver->setAllowedTypes('exception_delay', 'callable');
8792
$options = $resolver->resolve($config);
8893

8994
$this->retry = $options['retries'];
95+
$this->errorResponseDecider = $options['error_response_decider'];
96+
$this->errorResponseDelay = $options['error_response_delay'];
9097
$this->exceptionDecider = $options['exception_decider'];
9198
$this->exceptionDelay = $options['exception_delay'];
9299
}
@@ -98,7 +105,22 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
98105
{
99106
$chainIdentifier = spl_object_hash((object) $first);
100107

101-
return $next($request)->then(function (ResponseInterface $response) use ($request, $chainIdentifier) {
108+
return $next($request)->then(function (ResponseInterface $response) use ($request, $next, $first, $chainIdentifier) {
109+
if (!array_key_exists($chainIdentifier, $this->retryStorage)) {
110+
$this->retryStorage[$chainIdentifier] = 0;
111+
}
112+
113+
if ($this->retryStorage[$chainIdentifier] >= $this->retry) {
114+
unset($this->retryStorage[$chainIdentifier]);
115+
116+
return $response;
117+
}
118+
119+
if (call_user_func($this->errorResponseDecider, $request, $response)) {
120+
$time = call_user_func($this->errorResponseDelay, $request, $response, $this->retryStorage[$chainIdentifier]);
121+
$response = $this->retry($request, $next, $first, $chainIdentifier, $time);
122+
}
123+
102124
if (array_key_exists($chainIdentifier, $this->retryStorage)) {
103125
unset($this->retryStorage[$chainIdentifier]);
104126
}
@@ -120,23 +142,38 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
120142
}
121143

122144
$time = call_user_func($this->exceptionDelay, $request, $exception, $this->retryStorage[$chainIdentifier]);
123-
usleep($time);
124145

125-
// Retry synchronously
126-
++$this->retryStorage[$chainIdentifier];
127-
$promise = $this->handleRequest($request, $next, $first);
128-
129-
return $promise->wait();
146+
return $this->retry($request, $next, $first, $chainIdentifier, $time);
130147
});
131148
}
132149

133150
/**
134151
* @param int $retries The number of retries we made before. First time this get called it will be 0.
135-
*
136-
* @return int
137152
*/
138-
public static function defaultDelay(RequestInterface $request, Exception $e, $retries)
153+
public static function defaultErrorResponseDelay(RequestInterface $request, ResponseInterface $response, $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, Exception $e, $retries): int
139162
{
140163
return pow(2, $retries) * 500000;
141164
}
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+
}
142179
}

0 commit comments

Comments
 (0)