+ */
+class RetryableHttpClient implements HttpClientInterface
+{
+ use AsyncDecoratorTrait;
+
+ private $decider;
+ private $strategy;
+ private $maxRetries;
+ private $logger;
+
+ /**
+ * @param int $maxRetries The maximum number of times to retry
+ */
+ public function __construct(HttpClientInterface $client, RetryDeciderInterface $decider = null, RetryBackOffInterface $strategy = null, int $maxRetries = 3, LoggerInterface $logger = null)
+ {
+ $this->client = $client;
+ $this->decider = $decider ?? new HttpStatusCodeDecider();
+ $this->strategy = $strategy ?? new ExponentialBackOff();
+ $this->maxRetries = $maxRetries;
+ $this->logger = $logger ?: new NullLogger();
+ }
+
+ public function request(string $method, string $url, array $options = []): ResponseInterface
+ {
+ $retryCount = 0;
+
+ return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $url, $options, &$retryCount) {
+ $exception = null;
+ try {
+ if ($chunk->isTimeout() || null !== $chunk->getInformationalStatus()) {
+ yield $chunk;
+
+ return;
+ }
+
+ // only retry first chunk
+ if (!$chunk->isFirst()) {
+ $context->passthru();
+ yield $chunk;
+
+ return;
+ }
+ } catch (TransportExceptionInterface $exception) {
+ // catch TransportExceptionInterface to send it to strategy.
+ }
+
+ $statusCode = $context->getStatusCode();
+ $headers = $context->getHeaders();
+ if ($retryCount >= $this->maxRetries || !$this->decider->shouldRetry($method, $url, $options, $partialResponse = new MockResponse($context->getContent(), ['http_code' => $statusCode, 'headers' => $headers]), $exception)) {
+ $context->passthru();
+ yield $chunk;
+
+ return;
+ }
+
+ $context->setInfo('retry_count', $retryCount);
+ $context->getResponse()->cancel();
+
+ $delay = $this->getDelayFromHeader($headers) ?? $this->strategy->getDelay($retryCount, $method, $url, $options, $partialResponse, $exception);
+ ++$retryCount;
+
+ $this->logger->info('Error returned by the server. Retrying #{retryCount} using {delay} ms delay: '.($exception ? $exception->getMessage() : 'StatusCode: '.$statusCode), [
+ 'retryCount' => $retryCount,
+ 'delay' => $delay,
+ ]);
+
+ $context->replaceRequest($method, $url, $options);
+ $context->pause($delay / 1000);
+ });
+ }
+
+ private function getDelayFromHeader(array $headers): ?int
+ {
+ if (null !== $after = $headers['retry-after'][0] ?? null) {
+ if (is_numeric($after)) {
+ return (int) $after * 1000;
+ }
+ if (false !== $time = strtotime($after)) {
+ return max(0, $time - time()) * 1000;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/Symfony/Component/HttpClient/Tests/Retry/ExponentialBackOffTest.php b/src/Symfony/Component/HttpClient/Tests/Retry/ExponentialBackOffTest.php
new file mode 100644
index 0000000000000..f97572ecc42fc
--- /dev/null
+++ b/src/Symfony/Component/HttpClient/Tests/Retry/ExponentialBackOffTest.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Tests\Retry;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpClient\Response\MockResponse;
+use Symfony\Component\HttpClient\Retry\ExponentialBackOff;
+
+class ExponentialBackOffTest extends TestCase
+{
+ /**
+ * @dataProvider provideDelay
+ */
+ public function testGetDelay(int $delay, int $multiplier, int $maxDelay, int $previousRetries, int $expectedDelay)
+ {
+ $backOff = new ExponentialBackOff($delay, $multiplier, $maxDelay);
+
+ self::assertSame($expectedDelay, $backOff->getDelay($previousRetries, 'GET', 'http://example.com/', [], new MockResponse(), null));
+ }
+
+ public function provideDelay(): iterable
+ {
+ // delay, multiplier, maxDelay, retries, expectedDelay
+ yield [1000, 1, 5000, 0, 1000];
+ yield [1000, 1, 5000, 1, 1000];
+ yield [1000, 1, 5000, 2, 1000];
+
+ yield [1000, 2, 10000, 0, 1000];
+ yield [1000, 2, 10000, 1, 2000];
+ yield [1000, 2, 10000, 2, 4000];
+ yield [1000, 2, 10000, 3, 8000];
+ yield [1000, 2, 10000, 4, 10000]; // max hit
+ yield [1000, 2, 0, 4, 16000]; // no max
+
+ yield [1000, 3, 10000, 0, 1000];
+ yield [1000, 3, 10000, 1, 3000];
+ yield [1000, 3, 10000, 2, 9000];
+
+ yield [1000, 1, 500, 0, 500]; // max hit immediately
+
+ // never a delay
+ yield [0, 2, 10000, 0, 0];
+ yield [0, 2, 10000, 1, 0];
+ }
+}
diff --git a/src/Symfony/Component/HttpClient/Tests/Retry/HttpStatusCodeDeciderTest.php b/src/Symfony/Component/HttpClient/Tests/Retry/HttpStatusCodeDeciderTest.php
new file mode 100644
index 0000000000000..3c9a882b02e82
--- /dev/null
+++ b/src/Symfony/Component/HttpClient/Tests/Retry/HttpStatusCodeDeciderTest.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Tests\Retry;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Component\HttpClient\Response\MockResponse;
+use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider;
+
+class HttpStatusCodeDeciderTest extends TestCase
+{
+ public function testShouldRetryException()
+ {
+ $decider = new HttpStatusCodeDecider([500]);
+
+ self::assertTrue($decider->shouldRetry('GET', 'http://example.com/', [], new MockResponse(), new TransportException()));
+ }
+
+ public function testShouldRetryStatusCode()
+ {
+ $decider = new HttpStatusCodeDecider([500]);
+
+ self::assertTrue($decider->shouldRetry('GET', 'http://example.com/', [], new MockResponse('', ['http_code' => 500]), null));
+ }
+
+ public function testIsNotRetryableOk()
+ {
+ $decider = new HttpStatusCodeDecider([500]);
+
+ self::assertFalse($decider->shouldRetry('GET', 'http://example.com/', [], new MockResponse(''), null));
+ }
+}
diff --git a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php
new file mode 100644
index 0000000000000..c7b67117288cd
--- /dev/null
+++ b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php
@@ -0,0 +1,50 @@
+ 500]),
+ new MockResponse('', ['http_code' => 200]),
+ ]),
+ new HttpStatusCodeDecider([500]),
+ new ExponentialBackOff(0),
+ 1
+ );
+
+ $response = $client->request('GET', 'http://example.com/foo-bar');
+
+ self::assertSame(200, $response->getStatusCode());
+ }
+
+ public function testRetryRespectStrategy(): void
+ {
+ $client = new RetryableHttpClient(
+ new MockHttpClient([
+ new MockResponse('', ['http_code' => 500]),
+ new MockResponse('', ['http_code' => 500]),
+ new MockResponse('', ['http_code' => 200]),
+ ]),
+ new HttpStatusCodeDecider([500]),
+ new ExponentialBackOff(0),
+ 1
+ );
+
+ $response = $client->request('GET', 'http://example.com/foo-bar');
+
+ $this->expectException(ServerException::class);
+ $response->getHeaders();
+ }
+}
diff --git a/src/Symfony/Component/Messenger/Retry/MultiplierRetryStrategy.php b/src/Symfony/Component/Messenger/Retry/MultiplierRetryStrategy.php
index 8f5c1cdc3f3de..70ae43e9ec92d 100644
--- a/src/Symfony/Component/Messenger/Retry/MultiplierRetryStrategy.php
+++ b/src/Symfony/Component/Messenger/Retry/MultiplierRetryStrategy.php
@@ -48,17 +48,17 @@ public function __construct(int $maxRetries = 3, int $delayMilliseconds = 1000,
$this->maxRetries = $maxRetries;
if ($delayMilliseconds < 0) {
- throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" passed.', $delayMilliseconds));
+ throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMilliseconds));
}
$this->delayMilliseconds = $delayMilliseconds;
if ($multiplier < 1) {
- throw new InvalidArgumentException(sprintf('Multiplier must be greater than zero: "%s" passed.', $multiplier));
+ throw new InvalidArgumentException(sprintf('Multiplier must be greater than zero: "%s" given.', $multiplier));
}
$this->multiplier = $multiplier;
if ($maxDelayMilliseconds < 0) {
- throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" passed.', $maxDelayMilliseconds));
+ throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMilliseconds));
}
$this->maxDelayMilliseconds = $maxDelayMilliseconds;
}
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy