8000 [HttpClient] add RetryStrategyInterface · symfony/symfony@42f9b9b · GitHub
[go: up one dir, main page]

Skip to content

Commit 42f9b9b

Browse files
[HttpClient] add RetryStrategyInterface
1 parent a324b22 commit 42f9b9b

8 files changed

+159
-42
lines changed

src/Symfony/Component/HttpClient/Retry/HttpStatusCodeDecider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function __construct(array $statusCodes = [423, 425, 429, 500, 502, 503,
2828
$this->statusCodes = $statusCodes;
2929
}
3030

31-
public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, int $responseStatusCode, array $responseHeaders, ?string $responseContent): ?bool
31+
public function shouldRetry(int $retryCount, string $requestMethod, string $requestUrl, array $requestOptions, int $responseStatusCode, array $responseHeaders, ?string $responseContent): ?bool
3232
{
3333
return \in_array($responseStatusCode, $this->statusCodes, true);
3434
}

src/Symfony/Component/HttpClient/Retry/RetryDeciderInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ interface RetryDeciderInterface
2323
*
2424
* @return ?bool Returns null to signal that the body is required to take a decision
2525
*/
26-
public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, int $responseStatusCode, array $responseHeaders, ?string $responseContent): ?bool;
26+
public function shouldRetry(int $retryCount, string $requestMethod, string $requestUrl, array $requestOptions, int $responseStatusCode, array $responseHeaders, ?string $responseContent): ?bool;
2727
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
/**
15+
* @author Nicolas Grekas <p@tchwork.com>
16+
*/
17+
interface RetryStrategyInterface
18+
{
19+
public function getToken(string $requestMethod, string $requestUrl, array $requestOptions): ?RetryToken;
20+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
/**
15+
* @author Nicolas Grekas <p@tchwork.com>
16+
*/
17+
class RetryToken
18+
{
19+
private $shouldRetry;
20+
private $getDelay;
21+
22+
public function __construct(\Closure $shouldRetry, \Closure $getDelay)
23+
{
24+
$this->shouldRetry = $shouldRetry;
25+
$this->getDelay = $getDelay;
26+
}
27+
28+
/**
29+
* Returns whether the request should be retried.
30+
*
31+
* @param ?string $responseContent Null is passed when the body did not arrive yet
32+
*
33+
* @return ?bool Returns null to signal that the body is required to take a decision
34+
*/
35+
public function shouldRetry(int $retryCount, int $responseStatusCode, array $responseHeaders, ?string $responseContent): ?bool
36+
{
37+
return ($this->shouldRetry)($retryCount, $responseStatusCode, $responseHeaders, $responseContent);
38+
}
39+
40+
/**
41+
* Returns the time to wait in milliseconds.
42+
*/
43+
public function getDelay(int $retryCount, int $responseStatusCode, array $responseHeaders, ?string $responseContent, ?TransportExceptionInterface $exception): int
44+
{
45+
return ($this->getDelay)($retryCount, $responseStatusCode, $responseHeaders, $responseContent, $exception);
46+
}
47+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
/**
15+
* @author Nicolas Grekas <p@tchwork.com>
16+
*/
17+
class StatelessStrategy implements RetryStrategyInterface
18+
{
19+
private $decider;
20+
private $backoff;
21+
22+
public function __construct(RetryDeciderInterface $decider = null, RetryBackOffInterface $backoff = null)
23+
{
24+
$this->decider = $decider ?? new HttpStatusCodeDecider();
25+
$this->backoff = $backoff ?? new ExponentialBackOff();
26+
}
27+
28+
public function getToken(string $requestMethod, string $requestUrl, array $requestOptions): ?RetryToken
29+
{
30+
$decider = function ($retryCount, $responseStatusCode, $responseHeaders, $responseContent) use ($requestMethod, $requestUrl, $requestOptions) {
31+
return $this->decider->shouldRetry($retryCount, $requestMethod, $requestUrl, $requestOptions, $responseStatusCode, $responseHeaders, $responseContent);
32+
};
33+
34+
$backoff = function ($retryCount, $responseStatusCode, $responseHeaders, $responseContent, $exception) use ($requestMethod, $requestUrl, $requestOptions) {
35+
return $this->backoff->getDelay($retryCount, $requestMethod, $requestUrl, $requestOptions, $responseStatusCode, $responseHeaders, $responseContent, $exception);
36+
};
37+
38+
return new RetryToken($decider, $backoff);
39+
}
40+
}

src/Symfony/Component/HttpClient/RetryableHttpClient.php

Lines changed: 17 additions & 18 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\RetryStrategyInterface;
19+
use Symfony\Component\HttpClient\Retry\StatelessStrategy;
2220
use Symfony\Contracts\HttpClient\ChunkInterface;
2321
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
2422
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -33,34 +31,32 @@ 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 StatelessStrategy();
4945
$this->maxRetries = $maxRetries;
5046
$this->logger = $logger ?: new NullLogger();
5147
}
5248

5349
public function request(string $method, string $url, array $options = []): ResponseInterface
5450
{
55-
if ($this->maxRetries <= 0) {
51+
$retryToken = $this->strategy->getToken($method, $url, $options);
52+
53+
if (null === $retryToken || $this->maxRetries <= 0) {
5654
return new AsyncResponse($this->client, $method, $url, $options);
5755
}
5856

5957
$retryCount = 0;
60-
$content = '';
61-
$firstChunk = null;
6258

63-
return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $url, $options, &$retryCount, &$content, &$firstChunk) {
59+
return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $url, $options, $retryToken, &$retryCount, &$content, &$firstChunk) {
6460
$exception = null;
6561
try {
6662
if ($chunk->isTimeout() || null !== $chunk->getInformationalStatus()) {
@@ -69,14 +65,14 @@ 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 RetryToken::getDelay().
7369
}
7470

7571
$statusCode = $context->getStatusCode();
7672
$headers = $context->getHeaders();
7773
if (null === $exception) {
7874
if ($chunk->isFirst()) {
79-
$shouldRetry = $this->decider->shouldRetry($method, $url, $options, $statusCode, $headers, null);
75+
$shouldRetry = $retryToken->shouldRetry($retryCount, $statusCode, $headers, null);
8076

8177
if (false === $shouldRetry) {
8278
$context->passthru();
@@ -85,7 +81,7 @@ public function request(string $method, string $url, array $options = []): Respo
8581
return;
8682
}
8783

88-
// Decider need body to decide
84+
// Body is needed to decide
8985
if (null === $shouldRetry) {
9086
$firstChunk = $chunk;
9187
$content = '';
@@ -94,12 +90,15 @@ public function request(string $method, string $url, array $options = []): Respo
9490
}
9591
} else {
9692
$content .= $chunk->getContent();
93+
9794
if (!$chunk->isLast()) {
9895
return;
9996
}
100-
$shouldRetry = $this->decider->shouldRetry($method, $url, $options, $statusCode, $headers, $content);
97+
98+
$shouldRetry = $retryToken->shouldRetry($retryCount, $statusCode, $headers, $content);
99+
101100
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)));
101+
throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with a body.', \get_debug_type($retryToken)));
103102
}
104103

105104
if (false === $shouldRetry) {
@@ -116,7 +115,7 @@ public function request(string $method, string $url, array $options = []): Respo
116115
$context->setInfo('retry_count', $retryCount);
117116
$context->getResponse()->cancel();
118117

119-
$delay = $this->getDelayFromHeader($headers) ?? $this->strategy->getDelay($retryCount, $method, $url, $options, $statusCode, $headers, $chunk instanceof LastChunk ? $content : null, $exception);
118+
$delay = $this->getDelayFromHeader($headers) ?? $retryToken->getDelay($retryCount, $statusCode, $headers, $chunk instanceof LastChunk ? $content : null, $exception);
120119
++$retryCount;
121120

122121
$this->logger->info('Error returned by the server. Retrying #{retryCount} using {delay} ms delay: '.($exception ? $exception->getMessage() : 'StatusCode: '.$statusCode), [

src/Symfony/Component/HttpClient/Tests/Retry/HttpStatusCodeDeciderTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ public function testShouldRetryStatusCode()
2020
{
2121
$decider = new HttpStatusCodeDecider([500]);
2222

23-
self::assertTrue($decider->shouldRetry('GET', 'http://example.com/', [], 500, [], null));
23+
self::assertTrue($decider->shouldRetry(1, 'GET', 'http://example.com/', [], 500, [], null));
2424
}
2525

2626
public function testIsNotRetryableOk()
2727
{
2828
$decider = new HttpStatusCodeDecider([500]);
2929

30-
self::assertFalse($decider->shouldRetry('GET', 'http://example.com/', [], 200, [], null));
30+
self::assertFalse($decider->shouldRetry(1, 'GET', 'http://example.com/', [], 200, [], null));
3131
}
3232
}

src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Symfony\Component\HttpClient\Retry\ExponentialBackOff;
1010
use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider;
1111
use Symfony\Component\HttpClient\Retry\RetryDeciderInterface;
12+
use Symfony\Component\HttpClient\Retry\StatelessStrategy;
1213
use Symfony\Component\HttpClient\RetryableHttpClient;
1314

1415
class RetryableHttpClientTest extends TestCase
@@ -20,8 +21,10 @@ public function testRetryOnError()
2021
new MockResponse('', ['http_code' => 500]),
2122
new MockResponse('', ['http_code' => 200]),
2223
]),
23-
new HttpStatusCodeDecider([500]),
24-
new ExponentialBackOff(0),
24+
new StatelessStrategy(
25+
new HttpStatusCodeDecider([500]),
26+
new ExponentialBackOff(0)
27+
),
2528
1
2629
);
2730

@@ -38,8 +41,10 @@ public function testRetryRespectStrategy()
3841
new MockResponse('', ['http_code' => 500]),
3942
new MockResponse('', ['http_code' => 200]),
4043
]),
41-
new HttpStatusCodeDecider([500]),
42-
new ExponentialBackOff(0),
44+
new StatelessStrategy(
45+
new HttpStatusCodeDecider([500]),
46+
new ExponentialBackOff(0)
47+
),
4348
1
4449
);
4550

@@ -56,13 +61,15 @@ public function testRetryWithBody()
5661
new MockResponse('', ['http_code' => 500]),
5762
new MockResponse('', ['http_code' => 200]),
5863
]),
59-
new class() implements RetryDeciderInterface {
60-
public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, int $responseCode, array $responseHeaders, ?string $responseContent): ?bool
61-
{
62-
return null === $responseContent ? null : 200 !== $responseCode;
63-
}
64-
},
65-
new ExponentialBackOff(0),
64+
new StatelessStrategy(
65+
new class() implements RetryDeciderInterface {
66+
public function shouldRetry(int $retryCount, string $requestMethod, string $requestUrl, array $requestOptions, int $responseCode, array $responseHeaders, ?string $responseContent): ?bool
67+
{
68+
return null === $responseContent ? null : 200 !== $responseCode;
69+
}
70+
},
71+
new ExponentialBackOff(0)
72+
),
6673
1
6774
);
6875

@@ -78,13 +85,15 @@ public function testRetryWithBodyInvalid()
7885
new MockResponse('', ['http_code' => 500]),
7986
new MockResponse('', ['http_code' => 200]),
8087
]),
81-
new class() implements RetryDeciderInterface {
82-
public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, int $responseCode, array $responseHeaders, ?string $responseContent, \Throwable $throwable = null): ?bool
83-
{
84-
return null;
85-
}
86-
},
87-
new ExponentialBackOff(0),
88+
new StatelessStrategy(
89+
new class() implements RetryDeciderInterface {
90+
public function shouldRetry(int $retryCount, string $requestMethod, string $requestUrl, array $requestOptions, int $responseCode, array $responseHeaders, ?string $responseContent, \Throwable $throwable = null): ?bool
91+
{
92+
return null;
93+
}
94+
},
95+
new ExponentialBackOff(0)
96+
),
8897
1
8998
);
9099

@@ -100,8 +109,10 @@ public function testStreamNoRetry()
100109
new MockHttpClient([
101110
new MockResponse('', ['http_code' => 500]),
102111
]),
103-
new HttpStatusCodeDecider([500]),
104-
new ExponentialBackOff(0),
112+
new StatelessStrategy(
113+
new HttpStatusCodeDecider([500]),
114+
new ExponentialBackOff(0)
115+
),
105116
0
106117
);
107118

0 commit comments

Comments
 (0)
0