From b63ad4025ffbc4f1e513dd34650a34a50712afe4 Mon Sep 17 00:00:00 2001 From: Benjamin Zaslavsky Date: Fri, 24 Mar 2023 20:01:28 +0100 Subject: [PATCH] [HttpClient] Allow using multiple base_uri as array for retries --- src/Symfony/Component/HttpClient/CHANGELOG.md | 1 + .../HttpClient/RetryableHttpClient.php | 36 ++++++- .../Tests/RetryableHttpClientTest.php | 100 ++++++++++++++++++ 3 files changed, 135 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md index 51683c591745..104e4d324777 100644 --- a/src/Symfony/Component/HttpClient/CHANGELOG.md +++ b/src/Symfony/Component/HttpClient/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add `UriTemplateHttpClient` to use URI templates as specified in the RFC 6570 * Add `ServerSentEvent::getArrayData()` to get the Server-Sent Event's data decoded as an array when it's a JSON payload + * Allow array of urls as `base_uri` option value in `RetryableHttpClient` to retry on a new url each time 6.2 --- diff --git a/src/Symfony/Component/HttpClient/RetryableHttpClient.php b/src/Symfony/Component/HttpClient/RetryableHttpClient.php index 2fc429489c26..1a6ec7d35e63 100644 --- a/src/Symfony/Component/HttpClient/RetryableHttpClient.php +++ b/src/Symfony/Component/HttpClient/RetryableHttpClient.php @@ -35,6 +35,7 @@ class RetryableHttpClient implements HttpClientInterface, ResetInterface private RetryStrategyInterface $strategy; private int $maxRetries; private LoggerInterface $logger; + private array $baseUris = []; /** * @param int $maxRetries The maximum number of times to retry @@ -47,13 +48,34 @@ public function __construct(HttpClientInterface $client, RetryStrategyInterface $this->logger = $logger ?? new NullLogger(); } + public function withOptions(array $options): static + { + if (\array_key_exists('base_uri', $options)) { + if (\is_array($options['base_uri'])) { + $this->baseUris = $options['base_uri']; + unset($options['base_uri']); + } else { + $this->baseUris = []; + } + } + + $clone = clone $this; + $clone->client = $this->client->withOptions($options); + + return $clone; + } + public function request(string $method, string $url, array $options = []): ResponseInterface { + $baseUris = \array_key_exists('base_uri', $options) ? $options['base_uri'] : $this->baseUris; + $baseUris = \is_array($baseUris) ? $baseUris : []; + $options = self::shiftBaseUri($options, $baseUris); + if ($this->maxRetries <= 0) { return new AsyncResponse($this->client, $method, $url, $options); } - return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $url, $options) { + return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $url, $options, &$baseUris) { static $retryCount = 0; static $content = ''; static $firstChunk; @@ -127,7 +149,7 @@ public function request(string $method, string $url, array $options = []): Respo ]); $context->setInfo('retry_count', $retryCount); - $context->replaceRequest($method, $url, $options); + $context->replaceRequest($method, $url, self::shiftBaseUri($options, $baseUris)); $context->pause($delay / 1000); if ($retryCount >= $this->maxRetries) { @@ -168,4 +190,14 @@ private function passthru(AsyncContext $context, ?ChunkInterface $firstChunk, st yield $lastChunk; } + + private static function shiftBaseUri(array $options, array &$baseUris): array + { + if ($baseUris) { + $baseUri = 1 < \count($baseUris) ? array_shift($baseUris) : current($baseUris); + $options['base_uri'] = \is_array($baseUri) ? $baseUri[array_rand($baseUri)] : $baseUri; + } + + return $options; + } } diff --git a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php index cf2af1560c34..b32601aefc5a 100644 --- a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php @@ -244,4 +244,104 @@ public function testRetryOnErrorAssertContent() self::assertSame('Test out content', $response->getContent()); self::assertSame('Test out content', $response->getContent(), 'Content should be buffered'); } + + public function testRetryWithMultipleBaseUris() + { + $client = new RetryableHttpClient( + new MockHttpClient([ + new MockResponse('', ['http_code' => 500]), + new MockResponse('Hit on second uri', ['http_code' => 200]), + ]), + new GenericRetryStrategy([500], 0), + 1 + ); + + $response = $client->request('GET', 'foo-bar', [ + 'base_uri' => [ + 'http://example.com/a/', + 'http://example.com/b/', + ], + ]); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame('http://example.com/b/foo-bar', $response->getInfo('url')); + } + + public function testMultipleBaseUrisAsOptions() + { + $client = new RetryableHttpClient( + new MockHttpClient([ + new MockResponse('', ['http_code' => 500]), + new MockResponse('Hit on second uri', ['http_code' => 200]), + ]), + new GenericRetryStrategy([500], 0), + 1 + ); + + $client = $client->withOptions([ + 'base_uri' => [ + 'http://example.com/a/', + 'http://example.com/b/', + ], + ]); + + $response = $client->request('GET', 'foo-bar'); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame('http://example.com/b/foo-bar', $response->getInfo('url')); + } + + public function testRetryWithMultipleBaseUrisShufflesNestedArray() + { + $client = new RetryableHttpClient( + new MockHttpClient([ + new MockResponse('', ['http_code' => 500]), + new MockResponse('Hit on second uri', ['http_code' => 200]), + ]), + new GenericRetryStrategy([500], 0), + 1 + ); + + $response = $client->request('GET', 'foo-bar', [ + 'base_uri' => [ + 'http://example.com/a/', + [ + 'http://example.com/b/', + 'http://example.com/c/', + ], + 'http://example.com/d/', + ], + ]); + + self::assertSame(200, $response->getStatusCode()); + self::assertMatchesRegularExpression('#^http://example.com/(b|c)/foo-bar$#', $response->getInfo('url')); + } + + public function testRetryWithMultipleBaseUrisPreservesNonNestedOrder() + { + $client = new RetryableHttpClient( + new MockHttpClient([ + new MockResponse('', ['http_code' => 500]), + new MockResponse('', ['http_code' => 500]), + new MockResponse('', ['http_code' => 500]), + new MockResponse('Hit on second uri', ['http_code' => 200]), + ]), + new GenericRetryStrategy([500], 0), + 3 + ); + + $response = $client->request('GET', 'foo-bar', [ + 'base_uri' => [ + 'http://example.com/a/', + [ + 'http://example.com/b/', + 'http://example.com/c/', + ], + 'http://example.com/d/', + ], + ]); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame('http://example.com/d/foo-bar', $response->getInfo('url')); + } }