Skip to content

Commit 9ad799b

Browse files
[HttpClient] simplify retry mechanism around RetryStrategyInterface
1 parent d4d376c commit 9ad799b

10 files changed

+160
-219
lines changed

Response/AmpResponse.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
use Symfony\Component\HttpClient\Exception\TransportException;
2929
use Symfony\Component\HttpClient\HttpClientTrait;
3030
use Symfony\Component\HttpClient\Internal\AmpBody;
31-
use Symfony\Component\HttpClient\Internal\AmpCanary;
3231
use Symfony\Component\HttpClient\Internal\AmpClientState;
3332
use Symfony\Component\HttpClient\Internal\Canary;
3433
use Symfony\Component\HttpClient\Internal\ClientState;

Retry/ExponentialBackOff.php

Lines changed: 0 additions & 81 deletions
This file was deleted.

Retry/GenericRetryStrategy.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient\Retry;
13+
14+
use Symfony\Component\HttpClient\Response\AsyncContext;
15+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
16+
17+
/**
18+
* Decides to retry the request when HTTP status codes belong to the given list of codes.
19+
*
20+
* @author Jérémy Derussé <[email protected]>
21+
*/
22+
class GenericRetryStrategy implements RetryStrategyInterface
23+
{
24+
public const DEFAULT_RETRY_STATUS_CODES = [423, 425, 429, 500, 502, 503, 504, 507, 510];
25+
26+
private $statusCodes;
27+
private $delayMs;
28+
private $multiplier;
29+
private $maxDelayMs;
30+
private $jitter;
31+
32+
/**
33+
* @param array $statusCodes List of HTTP status codes that trigger a retry
34+
* @param int $delayMs Amount of time to delay (or the initial value when multiplier is used)
35+
* @param float $multiplier Multiplier to apply to the delay each time a retry occurs
36+
* @param int $maxDelayMs Maximum delay to allow (0 means no maximum)
37+
* @param float $jitter Probability of randomness int delay (0 = none, 1 = 100% random)
38+
*/
39+
public function __construct(array $statusCodes = self::DEFAULT_RETRY_STATUS_CODES, int $delayMs = 1000, float $multiplier = 2.0, int $maxDelayMs = 0, float $jitter = 0.1)
40+
{
41+
$this->statusCodes = $statusCodes;
42+
43+
if ($delayMs < 0) {
44+
throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMs));
45+
}
46+
$this->delayMs = $delayMs;
47+
48+
if ($multiplier < 1) {
49+
throw new InvalidArgumentException(sprintf('Multiplier must be greater than or equal to one: "%s" given.', $multiplier));
50+
}
51+
$this->multiplier = $multiplier;
52+
53+
if ($maxDelayMs < 0) {
54+
throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMs));
55+
}
56+
$this->maxDelayMs = $maxDelayMs;
57+
58+
if ($jitter < 0 || $jitter > 1) {
59+
throw new InvalidArgumentException(sprintf('Jitter must be between 0 and 1: "%s" given.', $jitter));
60+
}
61+
$this->jitter = $jitter;
62+
}
63+
64+
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
65+
{
66+
return \in_array($context->getStatusCode(), $this->statusCodes, true);
67+
}
68+
69+
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int
70+
{
71+
$delay = $this->delayMs * $this->multiplier ** $context->getInfo('retry_count');
72+
73+
if ($this->jitter > 0) {
74+
$randomness = $delay * $this->jitter;
75+
$delay = $delay + random_int(-$randomness, +$randomness);
76+
}
77+
78+
if ($delay > $this->maxDelayMs && 0 !== $this->maxDelayMs) {
79+
return $this->maxDelayMs;
80+
}
81+
82+
return (int) $delay;
83+
}
84+
}

Retry/HttpStatusCodeDecider.php

Lines changed: 0 additions & 35 deletions
This file was deleted.

Retry/RetryBackOffInterface.php

Lines changed: 0 additions & 25 deletions
This file was deleted.

Retry/RetryDeciderInterface.php renamed to Retry/RetryStrategyInterface.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@
1111

1212
namespace Symfony\Component\HttpClient\Retry;
1313

14+
use Symfony\Component\HttpClient\Response\AsyncContext;
15+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
16+
1417
/**
1518
* @author Jérémy Derussé <[email protected]>
19+
* @author Nicolas Grekas <[email protected]>
1620
*/
17-
interface RetryDeciderInterface
21+
interface RetryStrategyInterface
1822
{
1923
/**
2024
* Returns whether the request should be retried.
@@ -23,5 +27,10 @@ interface RetryDeciderInterface
2327
*
2428
* @return ?bool Returns null to signal that the body is required to take a decision
2529
*/
26-
public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, int $responseStatusCode, array $responseHeaders, ?string $responseContent): ?bool;
30+
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool;
31+
32+
/**
33+
* Returns the time to wait in milliseconds.
34+
*/
35+
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int;
2736
}

RetryableHttpClient.php

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@
1515
use Psr\Log\NullLogger;
1616
use Symfony\Component\HttpClient\Response\AsyncContext;
1717
use Symfony\Component\HttpClient\Response\AsyncResponse;
18-
use Symfony\Component\HttpClient\Retry\ExponentialBackOff;
19-
use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider;
20-
use Symfony\Component\HttpClient\Retry\RetryBackOffInterface;
21-
use Symfony\Component\HttpClient\Retry\RetryDeciderInterface;
18+
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
19+
use Symfony\Component\HttpClient\Retry\RetryStrategyInterface;
2220
use Symfony\Contracts\HttpClient\ChunkInterface;
2321
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
2422
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -33,19 +31,17 @@ class RetryableHttpClient implements HttpClientInterface
3331
{
3432
use AsyncDecoratorTrait;
3533

36-
private $decider;
3734
private $strategy;
3835
private $maxRetries;
3936
private $logger;
4037

4138
/**
4239
* @param int $maxRetries The maximum number of times to retry
4340
*/
44-
public function __construct(HttpClientInterface $client, RetryDeciderInterface $decider = null, RetryBackOffInterface $strategy = null, int $maxRetries = 3, LoggerInterface $logger = null)
41+
public function __construct(HttpClientInterface $client, RetryStrategyInterface $strategy = null, int $maxRetries = 3, LoggerInterface $logger = null)
4542
{
4643
$this->client = $client;
47-
$this->decider = $decider ?? new HttpStatusCodeDecider();
48-
$this->strategy = $strategy ?? new ExponentialBackOff();
44+
$this->strategy = $strategy ?? new GenericRetryStrategy();
4945
$this->maxRetries = $maxRetries;
5046
$this->logger = $logger ?: new NullLogger();
5147
}
@@ -69,23 +65,22 @@ public function request(string $method, string $url, array $options = []): Respo
6965
return;
7066
}
7167
} catch (TransportExceptionInterface $exception) {
72-
// catch TransportExceptionInterface to send it to strategy.
68+
// catch TransportExceptionInterface to send it to the strategy
69+
$context->setInfo('retry_count', $retryCount);
7370
}
7471

75-
$statusCode = $context->getStatusCode();
76-
$headers = $context->getHeaders();
7772
if (null === $exception) {
7873
if ($chunk->isFirst()) {
79-
$shouldRetry = $this->decider->shouldRetry($method, $url, $options, $statusCode, $headers, null);
74+
$context->setInfo('retry_count', $retryCount);
8075

81-
if (false === $shouldRetry) {
76+
if (false === $shouldRetry = $this->strategy->shouldRetry($context, null, null)) {
8277
$context->passthru();
8378
yield $chunk;
8479

8580
return;
8681
}
8782

88-
// Decider need body to decide
83+
// Body is needed to decide
8984
if (null === $shouldRetry) {
9085
$firstChunk = $chunk;
9186
$content = '';
@@ -94,12 +89,13 @@ public function request(string $method, string $url, array $options = []): Respo
9489
}
9590
} else {
9691
$content .= $chunk->getContent();
92+
9793
if (!$chunk->isLast()) {
9894
return;
9995
}
100-
$shouldRetry = $this->decider->shouldRetry($method, $url, $options, $statusCode, $headers, $content);
101-
if (null === $shouldRetry) {
102-
throw new \LogicException(sprintf('The "%s::shouldRetry" method must not return null when called with a body.', \get_class($this->decider)));
96+
97+
if (null === $shouldRetry = $this->strategy->shouldRetry($context, $content, null)) {
98+
throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with a body.', \get_class($this->strategy)));
10399
}
104100

105101
if (false === $shouldRetry) {
@@ -113,14 +109,13 @@ public function request(string $method, string $url, array $options = []): Respo
113109
}
114110
}
115111

116-
$context->setInfo('retry_count', $retryCount);
117112
$context->getResponse()->cancel();
118113

119-
$delay = $this->getDelayFromHeader($headers) ?? $this->strategy->getDelay($retryCount, $method, $url, $options, $statusCode, $headers, $chunk instanceof LastChunk ? $content : null, $exception);
114+
$delay = $this->getDelayFromHeader($context->getHeaders()) ?? $this->strategy->getDelay($context, $chunk instanceof LastChunk ? $content : null, $exception);
120115
++$retryCount;
121116

122-
$this->logger->info('Error returned by the server. Retrying #{retryCount} using {delay} ms delay: '.($exception ? $exception->getMessage() : 'StatusCode: '.$statusCode), [
123-
'retryCount' => $retryCount,
117+
$this->logger->info('Try #{count} after {delay}ms'.($exception ? ': '.$exception->getMessage() : ', status code: '.$context->getStatusCode()), [
118+
'count' => $retryCount,
124119
'delay' => $delay,
125120
]);
126121

@@ -139,6 +134,7 @@ private function getDelayFromHeader(array $headers): ?int
139134
if (is_numeric($after)) {
140135
return (int) $after * 1000;
141136
}
137+
142138
if (false !== $time = strtotime($after)) {
143139
return max(0, $time - time()) * 1000;
144140
}

0 commit comments

Comments
 (0)