diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 75a4221c10d59..0d4ee4f797528 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1641,19 +1641,15 @@ private function addHttpClientRetrySection() ->addDefaultsIfNotSet() ->beforeNormalization() ->always(function ($v) { - if (isset($v['backoff_service']) && (isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay']) || isset($v['jitter']))) { - throw new \InvalidArgumentException('The "backoff_service" option cannot be used along with the "delay", "multiplier", "max_delay" or "jitter" options.'); - } - if (isset($v['decider_service']) && (isset($v['http_codes']))) { - throw new \InvalidArgumentException('The "decider_service" option cannot be used along with the "http_codes" options.'); + if (isset($v['retry_strategy']) && (isset($v['http_codes']) || isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay']) || isset($v['jitter']))) { + throw new \InvalidArgumentException('The "retry_strategy" option cannot be used along with the "http_codes", "delay", "multiplier", "max_delay" or "jitter" options.'); } return $v; }) ->end() ->children() - ->scalarNode('backoff_service')->defaultNull()->info('service id to override the retry backoff')->end() - ->scalarNode('decider_service')->defaultNull()->info('service id to override the retry decider')->end() + ->scalarNode('retry_strategy')->defaultNull()->info('service id to override the retry strategy')->end() ->arrayNode('http_codes') ->performNoDeepMerging() ->beforeNormalization() @@ -1668,9 +1664,9 @@ private function addHttpClientRetrySection() ->end() ->integerNode('max_retries')->defaultValue(3)->min(0)->end() ->integerNode('delay')->defaultValue(1000)->min(0)->info('Time in ms to delay (or the initial value when multiplier is used)')->end() - ->floatNode('multiplier')->defaultValue(2)->min(1)->info('If greater than 1, delay will grow exponentially for each retry: (delay * (multiple ^ retries))')->end() + ->floatNode('multiplier')->defaultValue(2)->min(1)->info('If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries)')->end() ->integerNode('max_delay')->defaultValue(0)->min(0)->info('Max time in ms that a retry should ever be delayed (0 = infinite)')->end() - ->floatNode('jitter')->defaultValue(0.1)->min(0)->max(1)->info('Randomness in percent (between 0 and 1)) to apply to the delay')->end() + ->floatNode('jitter')->defaultValue(0.1)->min(0)->max(1)->info('Randomness in percent (between 0 and 1) to apply to the delay')->end() ->end() ; } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 44c15027cee45..eb6aeb2c323b4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2011,10 +2011,10 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder } if ($this->isConfigEnabled($container, $retryOptions)) { - $this->registerHttpClientRetry($retryOptions, 'http_client', $container); + $this->registerRetryableHttpClient($retryOptions, 'http_client', $container); } - $httpClientId = $retryOptions['enabled'] ?? false ? 'http_client.retry.inner' : ($this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client'); + $httpClientId = ($retryOptions['enabled'] ?? false) ? 'http_client.retryable.inner' : ($this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client'); foreach ($config['scoped_clients'] as $name => $scopeConfig) { if ('http_client' === $name) { throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name)); @@ -2042,7 +2042,7 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder } if ($this->isConfigEnabled($container, $retryOptions)) { - $this->registerHttpClientRetry($retryOptions, $name, $container); + $this->registerRetryableHttpClient($retryOptions, $name, $container); } $container->registerAliasForArgument($name, HttpClientInterface::class); @@ -2062,42 +2062,31 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder } } - private function registerHttpClientRetry(array $retryOptions, string $name, ContainerBuilder $container) + private function registerRetryableHttpClient(array $options, string $name, ContainerBuilder $container) { if (!class_exists(RetryableHttpClient::class)) { - throw new LogicException('Retry failed request support cannot be enabled as version 5.2+ of the HTTP Client component is required.'); + throw new LogicException('Support for retrying failed requests requires symfony/http-client 5.2 or higher, try upgrading.'); } - if (null !== $retryOptions['backoff_service']) { - $backoffReference = new Reference($retryOptions['backoff_service']); + if (null !== $options['retry_strategy']) { + $retryStrategy = new Reference($options['retry_strategy']); } else { - $retryServiceId = $name.'.retry.exponential_backoff'; - $retryDefinition = new ChildDefinition('http_client.retry.abstract_exponential_backoff'); - $retryDefinition - ->replaceArgument(0, $retryOptions['delay']) - ->replaceArgument(1, $retryOptions['multiplier']) - ->replaceArgument(2, $retryOptions['max_delay']) - ->replaceArgument(3, $retryOptions['jitter']); - $container->setDefinition($retryServiceId, $retryDefinition); - - $backoffReference = new Reference($retryServiceId); - } - if (null !== $retryOptions['decider_service']) { - $deciderReference = new Reference($retryOptions['decider_service']); - } else { - $retryServiceId = $name.'.retry.decider'; - $retryDefinition = new ChildDefinition('http_client.retry.abstract_httpstatuscode_decider'); - $retryDefinition - ->replaceArgument(0, $retryOptions['http_codes']); - $container->setDefinition($retryServiceId, $retryDefinition); + $retryStrategy = new ChildDefinition('http_client.abstract_retry_strategy'); + $retryStrategy + ->replaceArgument(0, $options['http_codes']) + ->replaceArgument(1, $options['delay']) + ->replaceArgument(2, $options['multiplier']) + ->replaceArgument(3, $options['max_delay']) + ->replaceArgument(4, $options['jitter']); + $container->setDefinition($name.'.retry_strategy', $retryStrategy); - $deciderReference = new Reference($retryServiceId); + $retryStrategy = new Reference($name.'.retry_strategy'); } $container - ->register($name.'.retry', RetryableHttpClient::class) + ->register($name.'.retryable', RetryableHttpClient::class) ->setDecoratedService($name, null, -10) // lower priority than TraceableHttpClient - ->setArguments([new Reference($name.'.retry.inner'), $deciderReference, $backoffReference, $retryOptions['max_retries'], new Reference('logger')]) + ->setArguments([new Reference($name.'.retryable.inner'), $retryStrategy, $options['max_retries'], new Reference('logger')]) ->addTag('monolog.logger', ['channel' => 'http_client']); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php index ddc5caf85a27d..ca9d8894a2150 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php @@ -17,8 +17,7 @@ use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpClient\HttplugClient; use Symfony\Component\HttpClient\Psr18Client; -use Symfony\Component\HttpClient\Retry\ExponentialBackOff; -use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider; +use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; use Symfony\Contracts\HttpClient\HttpClientInterface; return static function (ContainerConfigurator $container) { @@ -51,19 +50,14 @@ service(StreamFactoryInterface::class)->ignoreOnInvalid(), ]) - // retry - ->set('http_client.retry.abstract_exponential_backoff', ExponentialBackOff::class) + ->set('http_client.abstract_retry_strategy', GenericRetryStrategy::class) ->abstract() ->args([ + abstract_arg('http codes'), abstract_arg('delay ms'), abstract_arg('multiplier'), abstract_arg('max delay ms'), abstract_arg('jitter'), ]) - ->set('http_client.retry.abstract_httpstatuscode_decider', HttpStatusCodeDecider::class) - ->abstract() - ->args([ - abstract_arg('http codes'), - ]) ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 288006092d65c..93058d190de22 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -581,8 +581,7 @@ - - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php index 8fc9e6440d7a3..206db6b02af92 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php @@ -4,8 +4,7 @@ 'http_client' => [ 'default_options' => [ 'retry_failed' => [ - 'backoff_service' => null, - 'decider_service' => null, + 'retry_strategy' => null, 'http_codes' => [429, 500], 'max_retries' => 2, 'delay' => 100, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml index 4cade94440d94..0ef87bad38b49 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml @@ -2,8 +2,7 @@ framework: http_client: default_options: retry_failed: - backoff_service: null - decider_service: null + retry_strategy: null http_codes: [429, 500] max_retries: 2 delay: 100 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 643390fdb6ada..22a6027b7c99e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -1500,15 +1500,15 @@ public function testHttpClientRetry() } $container = $this->createContainerFromFile('http_client_retry'); - $this->assertSame([429, 500], $container->getDefinition('http_client.retry.decider')->getArgument(0)); - $this->assertSame(100, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(0)); - $this->assertSame(2, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(1)); - $this->assertSame(0, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(2)); - $this->assertSame(0.3, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(3)); - $this->assertSame(2, $container->getDefinition('http_client.retry')->getArgument(3)); - - $this->assertSame(RetryableHttpClient::class, $container->getDefinition('foo.retry')->getClass()); - $this->assertSame(4, $container->getDefinition('foo.retry.exponential_backoff')->getArgument(1)); + $this->assertSame([429, 500], $container->getDefinition('http_client.retry_strategy')->getArgument(0)); + $this->assertSame(100, $container->getDefinition('http_client.retry_strategy')->getArgument(1)); + $this->assertSame(2, $container->getDefinition('http_client.retry_strategy')->getArgument(2)); + $this->assertSame(0, $container->getDefinition('http_client.retry_strategy')->getArgument(3)); + $this->assertSame(0.3, $container->getDefinition('http_client.retry_strategy')->getArgument(4)); + $this->assertSame(2, $container->getDefinition('http_client.retryable')->getArgument(2)); + + $this->assertSame(RetryableHttpClient::class, $container->getDefinition('foo.retryable')->getClass()); + $this->assertSame(4, $container->getDefinition('foo.retry_strategy')->getArgument(2)); } public function testHttpClientWithQueryParameterKey() diff --git a/src/Symfony/Component/HttpClient/Response/AmpResponse.php b/src/Symfony/Component/HttpClient/Response/AmpResponse.php index a862530bef152..b813ad774a536 100644 --- a/src/Symfony/Component/HttpClient/Response/AmpResponse.php +++ b/src/Symfony/Component/HttpClient/Response/AmpResponse.php @@ -28,7 +28,6 @@ use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\HttpClientTrait; use Symfony\Component\HttpClient\Internal\AmpBody; -use Symfony\Component\HttpClient\Internal\AmpCanary; use Symfony\Component\HttpClient\Internal\AmpClientState; use Symfony\Component\HttpClient\Internal\Canary; use Symfony\Component\HttpClient\Internal\ClientState; diff --git a/src/Symfony/Component/HttpClient/Retry/ExponentialBackOff.php b/src/Symfony/Component/HttpClient/Retry/ExponentialBackOff.php deleted file mode 100644 index 205fb9c82a688..0000000000000 --- a/src/Symfony/Component/HttpClient/Retry/ExponentialBackOff.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpClient\Retry; - -use Symfony\Component\HttpClient\Exception\InvalidArgumentException; -use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; - -/** - * A retry backOff with a constant or exponential retry delay. - * - * For example, if $delayMilliseconds=10000 & $multiplier=1, - * each retry will wait exactly 10 seconds. - * - * But if $delayMilliseconds=10000 & $multiplier=2: - * * Retry 1: 10 second delay - * * Retry 2: 20 second delay (10000 * 2 = 20000) - * * Retry 3: 40 second delay (20000 * 2 = 40000) - * - * @author Ryan Weaver - * @author Jérémy Derussé - */ -final class ExponentialBackOff implements RetryBackOffInterface -{ - private $delayMilliseconds; - private $multiplier; - private $maxDelayMilliseconds; - private $jitter; - - /** - * @param int $delayMilliseconds Amount of time to delay (or the initial value when multiplier is used) - * @param float $multiplier Multiplier to apply to the delay each time a retry occurs - * @param int $maxDelayMilliseconds Maximum delay to allow (0 means no maximum) - * @param float $jitter Probability of randomness int delay (0 = none, 1 = 100% random) - */ - public function __construct(int $delayMilliseconds = 1000, float $multiplier = 2.0, int $maxDelayMilliseconds = 0, float $jitter = 0.1) - { - if ($delayMilliseconds < 0) { - 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 or equal to one: "%s" given.', $multiplier)); - } - $this->multiplier = $multiplier; - - if ($maxDelayMilliseconds < 0) { - throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMilliseconds)); - } - $this->maxDelayMilliseconds = $maxDelayMilliseconds; - - if ($jitter < 0 || $jitter > 1) { - throw new InvalidArgumentException(sprintf('Jitter must be between 0 and 1: "%s" given.', $jitter)); - } - $this->jitter = $jitter; - } - - public function getDelay(int $retryCount, string $requestMethod, string $requestUrl, array $requestOptions, int $responseStatusCode, array $responseHeaders, ?string $responseContent, ?TransportExceptionInterface $exception): int - { - $delay = $this->delayMilliseconds * $this->multiplier ** $retryCount; - if ($this->jitter > 0) { - $randomness = $delay * $this->jitter; - $delay = $delay + random_int(-$randomness, +$randomness); - } - - if ($delay > $this->maxDelayMilliseconds && 0 !== $this->maxDelayMilliseconds) { - return $this->maxDelayMilliseconds; - } - - return (int) $delay; - } -} diff --git a/src/Symfony/Component/HttpClient/Retry/GenericRetryStrategy.php b/src/Symfony/Component/HttpClient/Retry/GenericRetryStrategy.php new file mode 100644 index 0000000000000..14727f7584ad6 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Retry/GenericRetryStrategy.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpClient\Retry; + +use Symfony\Component\HttpClient\Response\AsyncContext; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; + +/** + * Decides to retry the request when HTTP status codes belong to the given list of codes. + * + * @author Jérémy Derussé + */ +class GenericRetryStrategy implements RetryStrategyInterface +{ + public const DEFAULT_RETRY_STATUS_CODES = [423, 425, 429, 500, 502, 503, 504, 507, 510]; + + private $statusCodes; + private $delayMs; + private $multiplier; + private $maxDelayMs; + private $jitter; + + /** + * @param array $statusCodes List of HTTP status codes that trigger a retry + * @param int $delayMs Amount of time to delay (or the initial value when multiplier is used) + * @param float $multiplier Multiplier to apply to the delay each time a retry occurs + * @param int $maxDelayMs Maximum delay to allow (0 means no maximum) + * @param float $jitter Probability of randomness int delay (0 = none, 1 = 100% random) + */ + public function __construct(array $statusCodes = self::DEFAULT_RETRY_STATUS_CODES, int $delayMs = 1000, float $multiplier = 2.0, int $maxDelayMs = 0, float $jitter = 0.1) + { + $this->statusCodes = $statusCodes; + + if ($delayMs < 0) { + throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMs)); + } + $this->delayMs = $delayMs; + + if ($multiplier < 1) { + throw new InvalidArgumentException(sprintf('Multiplier must be greater than or equal to one: "%s" given.', $multiplier)); + } + $this->multiplier = $multiplier; + + if ($maxDelayMs < 0) { + throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMs)); + } + $this->maxDelayMs = $maxDelayMs; + + if ($jitter < 0 || $jitter > 1) { + throw new InvalidArgumentException(sprintf('Jitter must be between 0 and 1: "%s" given.', $jitter)); + } + $this->jitter = $jitter; + } + + public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool + { + return \in_array($context->getStatusCode(), $this->statusCodes, true); + } + + public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int + { + $delay = $this->delayMs * $this->multiplier ** $context->getInfo('retry_count'); + + if ($this->jitter > 0) { + $randomness = $delay * $this->jitter; + $delay = $delay + random_int(-$randomness, +$randomness); + } + + if ($delay > $this->maxDelayMs && 0 !== $this->maxDelayMs) { + return $this->maxDelayMs; + } + + return (int) $delay; + } +} diff --git a/src/Symfony/Component/HttpClient/Retry/HttpStatusCodeDecider.php b/src/Symfony/Component/HttpClient/Retry/HttpStatusCodeDecider.php deleted file mode 100644 index 9415cd33dfeb8..0000000000000 --- a/src/Symfony/Component/HttpClient/Retry/HttpStatusCodeDecider.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpClient\Retry; - -/** - * Decides to retry the request when HTTP status codes belong to the given list of codes. - * - * @author Jérémy Derussé - */ -final class HttpStatusCodeDecider implements RetryDeciderInterface -{ - private $statusCodes; - - /** - * @param array $statusCodes List of HTTP status codes that trigger a retry - */ - public function __construct(array $statusCodes = [423, 425, 429, 500, 502, 503, 504, 507, 510]) - { - $this->statusCodes = $statusCodes; - } - - public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, int $responseStatusCode, array $responseHeaders, ?string $responseContent): ?bool - { - return \in_array($responseStatusCode, $this->statusCodes, true); - } -} diff --git a/src/Symfony/Component/HttpClient/Retry/RetryBackOffInterface.php b/src/Symfony/Component/HttpClient/Retry/RetryBackOffInterface.php deleted file mode 100644 index 2935119d21632..0000000000000 --- a/src/Symfony/Component/HttpClient/Retry/RetryBackOffInterface.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpClient\Retry; - -use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; - -/** - * @author Jérémy Derussé - */ -interface RetryBackOffInterface -{ - /** - * Returns the time to wait in milliseconds. - */ - public function getDelay(int $retryCount, string $requestMethod, string $requestUrl, array $requestOptions, int $responseStatusCode, array $responseHeaders, ?string $responseContent, ?TransportExceptionInterface $exception): int; -} diff --git a/src/Symfony/Component/HttpClient/Retry/RetryDeciderInterface.php b/src/Symfony/Component/HttpClient/Retry/RetryStrategyInterface.php similarity index 53% rename from src/Symfony/Component/HttpClient/Retry/RetryDeciderInterface.php rename to src/Symfony/Component/HttpClient/Retry/RetryStrategyInterface.php index fef606e697c87..4f6767f731b5f 100644 --- a/src/Symfony/Component/HttpClient/Retry/RetryDeciderInterface.php +++ b/src/Symfony/Component/HttpClient/Retry/RetryStrategyInterface.php @@ -11,10 +11,14 @@ namespace Symfony\Component\HttpClient\Retry; +use Symfony\Component\HttpClient\Response\AsyncContext; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; + /** * @author Jérémy Derussé + * @author Nicolas Grekas */ -interface RetryDeciderInterface +interface RetryStrategyInterface { /** * Returns whether the request should be retried. @@ -23,5 +27,10 @@ interface RetryDeciderInterface * * @return ?bool Returns null to signal that the body is required to take a decision */ - public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, int $responseStatusCode, array $responseHeaders, ?string $responseContent): ?bool; + public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool; + + /** + * Returns the time to wait in milliseconds. + */ + public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int; } diff --git a/src/Symfony/Component/HttpClient/RetryableHttpClient.php b/src/Symfony/Component/HttpClient/RetryableHttpClient.php index 1e78393d9d99f..6cf932e60a901 100644 --- a/src/Symfony/Component/HttpClient/RetryableHttpClient.php +++ b/src/Symfony/Component/HttpClient/RetryableHttpClient.php @@ -15,10 +15,8 @@ use Psr\Log\NullLogger; use Symfony\Component\HttpClient\Response\AsyncContext; use Symfony\Component\HttpClient\Response\AsyncResponse; -use Symfony\Component\HttpClient\Retry\ExponentialBackOff; -use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider; -use Symfony\Component\HttpClient\Retry\RetryBackOffInterface; -use Symfony\Component\HttpClient\Retry\RetryDeciderInterface; +use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; +use Symfony\Component\HttpClient\Retry\RetryStrategyInterface; use Symfony\Contracts\HttpClient\ChunkInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -33,7 +31,6 @@ class RetryableHttpClient implements HttpClientInterface { use AsyncDecoratorTrait; - private $decider; private $strategy; private $maxRetries; private $logger; @@ -41,11 +38,10 @@ class RetryableHttpClient implements HttpClientInterface /** * @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) + public function __construct(HttpClientInterface $client, RetryStrategyInterface $strategy = null, int $maxRetries = 3, LoggerInterface $logger = null) { $this->client = $client; - $this->decider = $decider ?? new HttpStatusCodeDecider(); - $this->strategy = $strategy ?? new ExponentialBackOff(); + $this->strategy = $strategy ?? new GenericRetryStrategy(); $this->maxRetries = $maxRetries; $this->logger = $logger ?: new NullLogger(); } @@ -69,23 +65,22 @@ public function request(string $method, string $url, array $options = []): Respo return; } } catch (TransportExceptionInterface $exception) { - // catch TransportExceptionInterface to send it to strategy. + // catch TransportExceptionInterface to send it to the strategy + $context->setInfo('retry_count', $retryCount); } - $statusCode = $context->getStatusCode(); - $headers = $context->getHeaders(); if (null === $exception) { if ($chunk->isFirst()) { - $shouldRetry = $this->decider->shouldRetry($method, $url, $options, $statusCode, $headers, null); + $context->setInfo('retry_count', $retryCount); - if (false === $shouldRetry) { + if (false === $shouldRetry = $this->strategy->shouldRetry($context, null, null)) { $context->passthru(); yield $chunk; return; } - // Decider need body to decide + // Body is needed to decide if (null === $shouldRetry) { $firstChunk = $chunk; $content = ''; @@ -94,12 +89,13 @@ public function request(string $method, string $url, array $options = []): Respo } } else { $content .= $chunk->getContent(); + if (!$chunk->isLast()) { return; } - $shouldRetry = $this->decider->shouldRetry($method, $url, $options, $statusCode, $headers, $content); - if (null === $shouldRetry) { - throw new \LogicException(sprintf('The "%s::shouldRetry" method must not return null when called with a body.', \get_class($this->decider))); + + if (null === $shouldRetry = $this->strategy->shouldRetry($context, $content, null)) { + throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with a body.', \get_class($this->strategy))); } if (false === $shouldRetry) { @@ -113,14 +109,13 @@ public function request(string $method, string $url, array $options = []): Respo } } - $context->setInfo('retry_count', $retryCount); $context->getResponse()->cancel(); - $delay = $this->getDelayFromHeader($headers) ?? $this->strategy->getDelay($retryCount, $method, $url, $options, $statusCode, $headers, $chunk instanceof LastChunk ? $content : null, $exception); + $delay = $this->getDelayFromHeader($context->getHeaders()) ?? $this->strategy->getDelay($context, $chunk instanceof LastChunk ? $content : null, $exception); ++$retryCount; - $this->logger->info('Error returned by the server. Retrying #{retryCount} using {delay} ms delay: '.($exception ? $exception->getMessage() : 'StatusCode: '.$statusCode), [ - 'retryCount' => $retryCount, + $this->logger->info('Try #{count} after {delay}ms'.($exception ? ': '.$exception->getMessage() : ', status code: '.$context->getStatusCode()), [ + 'count' => $retryCount, 'delay' => $delay, ]); @@ -139,6 +134,7 @@ private function getDelayFromHeader(array $headers): ?int if (is_numeric($after)) { return (int) $after * 1000; } + if (false !== $time = strtotime($after)) { return max(0, $time - time()) * 1000; } diff --git a/src/Symfony/Component/HttpClient/Tests/Retry/ExponentialBackOffTest.php b/src/Symfony/Component/HttpClient/Tests/Retry/GenericRetryStrategyTest.php similarity index 51% rename from src/Symfony/Component/HttpClient/Tests/Retry/ExponentialBackOffTest.php rename to src/Symfony/Component/HttpClient/Tests/Retry/GenericRetryStrategyTest.php index 70351df6cbcb9..ecb84cb482ec7 100644 --- a/src/Symfony/Component/HttpClient/Tests/Retry/ExponentialBackOffTest.php +++ b/src/Symfony/Component/HttpClient/Tests/Retry/GenericRetryStrategyTest.php @@ -12,18 +12,35 @@ namespace Symfony\Component\HttpClient\Tests\Retry; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpClient\Retry\ExponentialBackOff; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\AsyncContext; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; -class ExponentialBackOffTest extends TestCase +class GenericRetryStrategyTest extends TestCase { + public function testShouldRetryStatusCode() + { + $strategy = new GenericRetryStrategy([500]); + + self::assertTrue($strategy->shouldRetry($this->getContext(0, 'GET', 'http://example.com/', 500), null, null)); + } + + public function testIsNotRetryableOk() + { + $strategy = new GenericRetryStrategy([500]); + + self::assertFalse($strategy->shouldRetry($this->getContext(0, 'GET', 'http://example.com/', 200), null, null)); + } + /** * @dataProvider provideDelay */ public function testGetDelay(int $delay, int $multiplier, int $maxDelay, int $previousRetries, int $expectedDelay) { - $backOff = new ExponentialBackOff($delay, $multiplier, $maxDelay, 0); + $strategy = new GenericRetryStrategy([], $delay, $multiplier, $maxDelay, 0); - self::assertSame($expectedDelay, $backOff->getDelay($previousRetries, 'GET', 'http://example.com/', [], 200, [], null, null)); + self::assertSame($expectedDelay, $strategy->getDelay($this->getContext($previousRetries, 'GET', 'http://example.com/', 200), null, null)); } public function provideDelay(): iterable @@ -53,11 +70,11 @@ public function provideDelay(): iterable public function testJitter() { - $backOff = new ExponentialBackOff(1000, 1, 0, 1); + $strategy = new GenericRetryStrategy([], 1000, 1, 0, 1); $belowHalf = 0; $aboveHalf = 0; for ($i = 0; $i < 20; ++$i) { - $delay = $backOff->getDelay(0, 'GET', 'http://example.com/', [], 200, [], null, null); + $delay = $strategy->getDelay($this->getContext(0, 'GET', 'http://example.com/', 200), null, null); if ($delay < 500) { ++$belowHalf; } elseif ($delay > 1500) { @@ -68,4 +85,18 @@ public function testJitter() $this->assertGreaterThanOrEqual(1, $belowHalf); $this->assertGreaterThanOrEqual(1, $aboveHalf); } + + private function getContext($retryCount, $method, $url, $statusCode): AsyncContext + { + $passthru = null; + $info = [ + 'retry_count' => $retryCount, + 'http_method' => $method, + 'url' => $url, + 'http_code' => $statusCode, + ]; + $response = new MockResponse('', $info); + + return new AsyncContext($passthru, new MockHttpClient(), $response, $info, null, 0); + } } diff --git a/src/Symfony/Component/HttpClient/Tests/Retry/HttpStatusCodeDeciderTest.php b/src/Symfony/Component/HttpClient/Tests/Retry/HttpStatusCodeDeciderTest.php deleted file mode 100644 index d634c15ae6559..0000000000000 --- a/src/Symfony/Component/HttpClient/Tests/Retry/HttpStatusCodeDeciderTest.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * 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\Retry\HttpStatusCodeDecider; - -class HttpStatusCodeDeciderTest extends TestCase -{ - public function testShouldRetryStatusCode() - { - $decider = new HttpStatusCodeDecider([500]); - - self::assertTrue($decider->shouldRetry('GET', 'http://example.com/', [], 500, [], null)); - } - - public function testIsNotRetryableOk() - { - $decider = new HttpStatusCodeDecider([500]); - - self::assertFalse($decider->shouldRetry('GET', 'http://example.com/', [], 200, [], null)); - } -} diff --git a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php index 9e502407e8c45..d4649e5665bbd 100644 --- a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php @@ -5,11 +5,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpClient\Exception\ServerException; use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\AsyncContext; use Symfony\Component\HttpClient\Response\MockResponse; -use Symfony\Component\HttpClient\Retry\ExponentialBackOff; -use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider; -use Symfony\Component\HttpClient\Retry\RetryDeciderInterface; +use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; use Symfony\Component\HttpClient\RetryableHttpClient; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; class RetryableHttpClientTest extends TestCase { @@ -20,8 +20,7 @@ public function testRetryOnError() new MockResponse('', ['http_code' => 500]), new MockResponse('', ['http_code' => 200]), ]), - new HttpStatusCodeDecider([500]), - new ExponentialBackOff(0), + new GenericRetryStrategy([500], 0), 1 ); @@ -38,8 +37,7 @@ public function testRetryRespectStrategy() new MockResponse('', ['http_code' => 500]), new MockResponse('', ['http_code' => 200]), ]), - new HttpStatusCodeDecider([500]), - new ExponentialBackOff(0), + new GenericRetryStrategy([500], 0), 1 ); @@ -56,13 +54,12 @@ public function testRetryWithBody() new MockResponse('', ['http_code' => 500]), new MockResponse('', ['http_code' => 200]), ]), - new class() implements RetryDeciderInterface { - public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, int $responseCode, array $responseHeaders, ?string $responseContent): ?bool + new class(GenericRetryStrategy::DEFAULT_RETRY_STATUS_CODES, 0) extends GenericRetryStrategy { + public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool { - return null === $responseContent ? null : 200 !== $responseCode; + return null === $responseContent ? null : 200 !== $context->getStatusCode(); } }, - new ExponentialBackOff(0), 1 ); @@ -78,13 +75,12 @@ public function testRetryWithBodyInvalid() new MockResponse('', ['http_code' => 500]), new MockResponse('', ['http_code' => 200]), ]), - new class() implements RetryDeciderInterface { - public function shouldRetry(string $requestMethod, string $requestUrl, array $requestOptions, int $responseCode, array $responseHeaders, ?string $responseContent, \Throwable $throwable = null): ?bool + new class(GenericRetryStrategy::DEFAULT_RETRY_STATUS_CODES, 0) extends GenericRetryStrategy { + public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool { return null; } }, - new ExponentialBackOff(0), 1 ); @@ -100,8 +96,7 @@ public function testStreamNoRetry() new MockHttpClient([ new MockResponse('', ['http_code' => 500]), ]), - new HttpStatusCodeDecider([500]), - new ExponentialBackOff(0), + new GenericRetryStrategy([500], 0), 0 ); 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