Skip to content

Commit 39d992d

Browse files
committed
no exception should be thrown on server error, but retrying those would make sense
1 parent 53bddb2 commit 39d992d

File tree

2 files changed

+95
-21
lines changed

2 files changed

+95
-21
lines changed

CHANGELOG.md

Lines changed: 3 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.

src/Plugin/RetryPlugin.php

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,22 @@ final class RetryPlugin implements Plugin
2929
/**
3030
* @var callable
3131
*/
32-
private $delay;
32+
private $errorResponseDecider;
3333

3434
/**
3535
* @var callable
3636
*/
37-
private $decider;
37+
private $exceptionDecider;
38+
39+
/**
40+
* @var callable
41+
*/
42+
private $errorResponseDelay;
43+
44+
/**
45+
* @var callable
46+
*/
47+
private $exceptionDelay;
3848

3949
/**
4050
* Store the retry counter for each request.
@@ -47,29 +57,60 @@ final class RetryPlugin implements Plugin
4757
* @param array $config {
4858
*
4959
* @var int $retries Number of retries to attempt if an exception occurs before letting the exception bubble up
50-
* @var callable $decider A callback that gets a request and an exception to decide after a failure whether the request should be retried
51-
* @var callable $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.
60+
* @var callable $error_response_decider A callback that gets a request and response to decide whether the request should be retried
61+
* @var callable $exception_decider A callback that gets a request and an exception to decide after a failure whether the request should be retried
62+
* @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
63+
* @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
5264
* }
65+
*
66+
* @since exception_decider and error_response_decider have been added in version 2. In version 1.*, the exception_decider has been called decider.
5367
*/
5468
public function __construct(array $config = [])
5569
{
5670
$resolver = new OptionsResolver();
5771
$resolver->setDefaults([
5872
'retries' => 1,
59-
'decider' => function (RequestInterface $request, Exception $e) {
73+
'error_response_decider' => function (RequestInterface $request, ResponseInterface $response) {
74+
// do not retry client errors
75+
return $response->getStatusCode() >= 500;
76+
},
77+
'exception_decider' => function (RequestInterface $request, Exception $e) {
6078
// do not retry client errors
6179
return !$e instanceof HttpException || $e->getCode() >= 500;
6280
},
63-
'delay' => __CLASS__.'::defaultDelay',
81+
'error_response_delay' => __CLASS__.'::defaultResponseDelay',
82+
'exception_delay' => __CLASS__.'::defaultExceptionDelay',
6483
]);
84+
85+
if (array_key_exists('decider', $config)) {
86+
if (array_key_exists('exception_decider', $config)) {
87+
throw new \InvalidArgumentException('Do not set both the old "decider" and new "exception_decider" options');
88+
}
89+
trigger_error('The "decider" option has been deprecated in favour of "exception_decider"', E_USER_DEPRECATED);
90+
$config['exception_decider'] = $config['decider'];
91+
unset($config['decider']);
92+
}
93+
if (array_key_exists('delay', $config)) {
94+
if (array_key_exists('exception_delay', $config)) {
95+
throw new \InvalidArgumentException('Do not set both the old "delay" and new "exception_delay" options');
96+
}
97+
trigger_error('The "delay" option has been deprecated in favour of "exception_delay"', E_USER_DEPRECATED);
98+
$config['exception_delay'] = $config['delay'];
99+
unset($config['delay']);
100+
}
101+
65102
$resolver->setAllowedTypes('retries', 'int');
66-
$resolver->setAllowedTypes('decider', 'callable');
67-
$resolver->setAllowedTypes('delay', 'callable');
103+
$resolver->setAllowedTypes('error_response_decider', 'callable');
104+
$resolver->setAllowedTypes('exception_decider', 'callable');
105+
$resolver->setAllowedTypes('error_response_delay', 'callable');
106+
$resolver->setAllowedTypes('exception_delay', 'callable');
68107
$options = $resolver->resolve($config);
69108

70109
$this->retry = $options['retries'];
71-
$this->decider = $options['decider'];
72-
$this->delay = $options['delay'];
110+
$this->exceptionDecider = $options['exception_decider'];
111+
$this->errorResponseDecider = $options['error_response_decider'];
112+
$this->errorResponseDelay = $options['error_response_delay'];
113+
$this->exceptionDelay = $options['exception_delay'];
73114
}
74115

75116
/**
@@ -79,7 +120,22 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
79120
{
80121
$chainIdentifier = spl_object_hash((object) $first);
81122

82-
return $next($request)->then(function (ResponseInterface $response) use ($request, $chainIdentifier) {
123+
return $next($request)->then(function (ResponseInterface $response) use ($request, $next, $first, $chainIdentifier) {
124+
if (!array_key_exists($chainIdentifier, $this->retryStorage)) {
125+
$this->retryStorage[$chainIdentifier] = 0;
126+
}
127+
128+
if ($this->retryStorage[$chainIdentifier] >= $this->retry) {
129+
unset($this->retryStorage[$chainIdentifier]);
130+
131+
return $response;
132+
}
133+
134+
if (call_user_func($this->errorResponseDecider, $request, $response)) {
135+
$time = call_user_func($this->errorResponseDelay, $request, $response, $this->retryStorage[$chainIdentifier]);
136+
$response = $this->retry($request, $next, $first, $chainIdentifier, $time);
137+
}
138+
83139
if (array_key_exists($chainIdentifier, $this->retryStorage)) {
84140
unset($this->retryStorage[$chainIdentifier]);
85141
}
@@ -96,18 +152,13 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
96152
throw $exception;
97153
}
98154

99-
if (!call_user_func($this->decider, $request, $exception)) {
155+
if (!call_user_func($this->exceptionDecider, $request, $exception)) {
100156
throw $exception;
101157
}
102158

103-
$time = call_user_func($this->delay, $request, $exception, $this->retryStorage[$chainIdentifier]);
104-
usleep($time);
159+
$time = call_user_func($this->exceptionDelay, $request, $exception, $this->retryStorage[$chainIdentifier]);
105160

106-
// Retry synchronously
107-
++$this->retryStorage[$chainIdentifier];
108-
$promise = $this->handleRequest($request, $next, $first);
109-
110-
return $promise->wait();
161+
return $this->retry($request, $next, $first, $chainIdentifier, $time);
111162
});
112163
}
113164

@@ -116,8 +167,29 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
116167
*
117168
* @return int
118169
*/
119-
public static function defaultDelay(RequestInterface $request, Exception $e, $retries)
170+
public static function defaultErrorResponseDelay(RequestInterface $request, ResponseInterface $response, $retries)
171+
{
172+
return pow(2, $retries) * 500000;
173+
}
174+
175+
/**
176+
* @param int $retries The number of retries we made before. First time this get called it will be 0.
177+
*
178+
* @return int
179+
*/
180+
public static function defaultExceptionDelay(RequestInterface $request, Exception $e, $retries)
120181
{
121182
return pow(2, $retries) * 500000;
122183
}
184+
185+
private function retry(RequestInterface $request, callable $next, callable $first, string $chainIdentifier, int $delay)
186+
{
187+
usleep($delay);
188+
189+
// Retry synchronously
190+
++$this->retryStorage[$chainIdentifier];
191+
$promise = $this->handleRequest($request, $next, $first);
192+
193+
return $promise->wait();
194+
}
123195
}

0 commit comments

Comments
 (0)