8000 [HttpClient] simplify retry mechanism around RetryStrategyInterface · symfony/symfony@544c3e8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 544c3e8

Browse files
[HttpClient] simplify retry mechanism around RetryStrategyInterface
1 parent ef1253b commit 544c3e8

17 files changed

+198
-281
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,19 +1641,15 @@ private function addHttpClientRetrySection()
16411641
->addDefaultsIfNotSet()
16421642
->beforeNormalization()
16431643
->always(function ($v) {
1644-
if (isset($v['backoff_service']) && (isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay']) || isset($v['jitter']))) {
1645-
throw new \InvalidArgumentException('The "backoff_service" option cannot be used along with the "delay", "multiplier", "max_delay" or "jitter" options.');
1646-
}
1647-
if (isset($v['decider_service']) && (isset($v['http_codes']))) {
1648-
throw new \InvalidArgumentException('The "decider_service" option cannot be used along with the "http_codes" options.');
1644+
if (isset($v['retry_strategy']) && (isset($v['http_codes']) || isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay']) || isset($v['jitter']))) {
1645+
throw new \InvalidArgumentException('The "retry_strategy" option cannot be used along with the "http_codes", "delay", "multiplier", "max_delay" or "jitter" options.');
16491646
}
16501647

16511648
return $v;
16521649
})
16531650
->end()
16541651
->children()
1655-
->scalarNode('backoff_service')->defaultNull()->info('service id to override the retry backoff')->end()
1656-
->scalarNode('decider_service')->defaultNull()->info('service id to override the retry decider')->end()
1652+
->scalarNode('retry_strategy')->defaultNull()->info('service id to override the retry strategy')->end()
16571653
->arrayNode('http_codes')
16581654
->performNoDeepMerging()
16591655
->beforeNormalization()
@@ -1668,9 +1664,9 @@ private function addHttpClientRetrySection()
16681664
->end()
16691665
->integerNode('max_retries')->defaultValue(3)->min(0)->end()
16701666
->integerNode('delay')->defaultValue(1000)->min(0)->info('Time in ms to delay (or the initial value when multiplier is used)')->end()
1671-
->floatNode('multiplier')->defaultValue(2)->min(1)->info('If greater than 1, delay will grow exponentially for each retry: (delay * (multiple ^ retries))')->end()
1667+
->floatNode('multiplier')->defaultValue(2)->min(1)->info('If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries)')->end()
16721668
->integerNode('max_delay')->defaultValue(0)->min(0)->info('Max time in ms that a retry should ever be delayed (0 = infinite)')->end()
1673-
->floatNode('jitter')->defaultValue(0.1)->min(0)->max(1)->info('Randomness in percent (between 0 and 1)) to apply to the delay')->end()
1669+
->floatNode('jitter')->defaultValue(0.1)->min(0)->max(1)->info('Randomness in percent (between 0 and 1) to apply to the delay')->end()
16741670
->end()
16751671
;
16761672
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2011,10 +2011,10 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
20112011
}
20122012

20132013
if ($this->isConfigEnabled($container, $retryOptions)) {
2014-
$this->registerHttpClientRetry($retryOptions, 'http_client', $container);
2014+
$this->registerRetryableHttpClient($retryOptions, 'http_client', $container);
20152015
}
20162016

2017-
$httpClientId = $retryOptions['enabled'] ?? false ? 'http_client.retry.inner' : ($this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client');
2017+
$httpClientId = ($retryOptions['enabled'] ?? false) ? 'http_client.retryable.inner' : ($this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client');
20182018
foreach ($config['scoped_clients'] as $name => $scopeConfig) {
20192019
if ('http_client' === $name) {
20202020
throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name));
@@ -2042,7 +2042,7 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
20422042
}
20432043

20442044
if ($this->isConfigEnabled($container, $retryOptions)) {
2045-
$this->registerHttpClientRetry($retryOptions, $name, $container);
2045+
$this->registerRetryableHttpClient($retryOptions, $name, $container);
20462046
}
20472047

20482048
$container->registerAliasForArgument($name, HttpClientInterface::class);
@@ -2062,42 +2062,31 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder
20622062
}
20632063
}
20642064

2065-
private function registerHttpClientRetry(array $retryOptions, string $name, ContainerBuilder $container)
2065+
private function registerRetryableHttpClient(array $options, string $name, ContainerBuilder $container)
20662066
{
20672067
if (!class_exists(RetryableHttpClient::class)) {
2068-
throw new LogicException('Retry failed request support cannot be enabled as version 5.2+ of the HTTP Client component is required.');
2068+
throw new LogicException('Support for retrying failed requests requires symfony/http-client 5.2 or higher, try upgrading.');
20692069
}
20702070

2071-
if (null !== $retryOptions['backoff_service']) {
2072-
$backoffReference = new Reference($retryOptions['backoff_service']);
2071+
if (null !== $options['retry_strategy']) {
2072+
$retryStrategy = new Reference($options['retry_strategy']);
20732073
} else {
2074-
$retryServiceId = $name.'.retry.exponential_backoff';
2075-
$retryDefinition = new ChildDefinition('http_client.retry.abstract_exponential_backoff');
2076-
$retryDefinition
2077-
->replaceArgument(0, $retryOptions['delay'])
2078-
->replaceArgument(1, $retryOptions['multiplier'])
2079-
->replaceArgument(2, $retryOptions['max_delay'])
2080-
->replaceArgument(3, $retryOptions['jitter']);
2081-
$container->setDefinition($retryServiceId, $retryDefinition);
2082-
2083-
$backoffReference = new Reference($retryServiceId);
2084-
}
2085-
if (null !== $retryOptions['decider_service']) {
2086-
$deciderReference = new Reference($retryOptions['decider_service']);
2087-
} else {
2088-
$retryServiceId = $name.'.retry.decider';
2089-
$retryDefinition = new ChildDefinition('http_client.retry.abstract_httpstatuscode_decider');
2090-
$retryDefinition
2091-
->replaceArgument(0, $retryOptions['http_codes']);
2092-
$container->setDefinition($retryServiceId, $retryDefinition);
2074+
$retryStrategy = new ChildDefinition('http_client.abstract_retry_strategy');
2075+
$retryStrategy
2076+
->replaceArgument(0, $options['http_codes'])
2077+
->replaceArgument(1, $options['delay'])
2078+
->replaceArgument(2, $options['multiplier'])
2079+
->replaceArgument(3, $options['max_delay'])
2080+
->replaceArgument(4, $options['jitter']);
2081+
$container->setDefinition($name.'.retry_strategy', $retryStrategy);
20932082

2094-
$deciderReference = new Reference($retryServiceId);
2083+
$retryStrategy = new Reference($name.'.retry_strategy');
20952084
}
20962085

20972086
$container
2098-
->register($name.'.retry', RetryableHttpClient::class)
2087+
->register($name.'.retryable', RetryableHttpClient::class)
20992088
->setDecoratedService($name, null, -10) // lower priority than TraceableHttpClient
2100-
->setArguments([new Reference($name.'.retry.inner'), $deciderReference, $backoffReference, $retryOptions['max_retries'], new Reference('logger')])
2089+
->setArguments([new Reference($name.'.retryable.inner'), $retryStrategy, $options['max_retries'], new Reference('logger')])
21012090
->addTag('monolog.logger', ['channel' => 'http_client']);
21022091
}
21032092

src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
use Symfony\Component\HttpClient\HttpClient;
1818
use Symfony\Component\HttpClient\HttplugClient;
1919
use Symfony\Component\HttpClient\Psr18Client;
20-
use Symfony\Component\HttpClient\Retry\ExponentialBackOff;
21-
use Symfony\Component\HttpClient\Retry\HttpStatusCodeDecider;
20+
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
2221
use Symfony\Contracts\HttpClient\HttpClientInterface;
2322

2423
return static function (ContainerConfigurator $container) {
@@ -51,19 +50,14 @@
5150
service(StreamFactoryInterface::class)->ignoreOnInvalid(),
5251
])
5352

54-
// retry
55-
->set('http_client.retry.abstract_exponential_backoff', ExponentialBackOff::class)
53+
->set('http_client.abstract_retry_strategy', GenericRetryStrategy::class)
5654
->abstract()
5755
->args([
56+
abstract_arg('http codes'),
5857
abstract_arg('delay ms'),
5958
abstract_arg('multiplier'),
6059
abstract_arg('max delay ms'),
6160
abstract_arg('jitter'),
6261
])
63-
->set('http_client.retry.abstract_httpstatuscode_decider', HttpStatusCodeDecider::class)
64-
->abstract()
65-
->args([
66-
abstract_arg('http codes'),
67-
])
6862
;
6963
};

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,8 +581,7 @@
581581
<xsd:element name="http-code" type="xsd:integer" minOccurs="0" maxOccurs="unbounded" />
582582
</xsd:sequence>
583583
<xsd:attribute name="enabled" type="xsd:boolean" />
584-
<xsd:attribute name="backoff-service" type="xsd:string" />
585-
<xsd:attribute name="decider-service" type="xsd:string" />
584+
<xsd:attribute name="retry-strategy" type="xsd:string" />
586585
<xsd:attribute name="max-retries" type="xsd:integer" />
587586
<xsd:attribute name="delay" type="xsd:integer" />
588587
<xsd:attribute name="multiplier" type="xsd:float" />

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
'http_client' => [
55
'default_options' => [
66
'retry_failed' => [
7-
'backoff_service' => null,
8-
'decider_service' => null,
7+
'retry_strategy' => null,
98
'http_codes' => [429, 500],
109
'max_retries' => 2,
1110
'delay' => 100,

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ framework:
22
http_client:
33
default_options:
44
retry_failed:
5-
backoff_service: null
6-
decider_service: null
5+
retry_strategy: null
76
http_codes: [429, 500]
87
max_retries: 2
98
delay: 100

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,15 +1500,15 @@ public function testHttpClientRetry()
15001500
}
15011501
$container = $this->createContainerFromFile('http_client_retry');
15021502

1503-
$this->assertSame([429, 500], $container->getDefinition('http_client.retry.decider')->getArgument(0));
1504-
$this->assertSame(100, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(0));
1505-
$this->assertSame(2, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(1));
1506-
$this->assertSame(0, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(2));
1507-
$this->assertSame(0.3, $container->getDefinition('http_client.retry.exponential_backoff')->getArgument(3));
1508-
$this->assertSame(2, $container->getDefinition('http_client.retry')->getArgument(3));
1509-
1510-
$this->assertSame(RetryableHttpClient::class, $container->getDefinition('foo.retry')->getClass());
1511-
$this->assertSame(4, $container->getDefini 10000 tion('foo.retry.exponential_backoff')->getArgument(1));
1503+
$this->assertSame([429, 500], $container->getDefinition('http_client.retry_strategy')->getArgument(0));
1504+
$this->assertSame(100, $container->getDefinition('http_client.retry_strategy')->getArgument(1));
1505+
$this->assertSame(2, $container->getDefinition('http_client.retry_strategy')->getArgument(2));
1506+
$this->assertSame(0, $container->getDefinition('http_client.retry_strategy')->getArgument(3));
1507+
$this->assertSame(0.3, $container->getDefinition('http_client.retry_strategy')->getArgument(4));
1508+
$this->assertSame(2, $container->getDefinition('http_client.retryable')->getArgument(2));
1509+
1510+
$this->assertSame(RetryableHttpClient::class, $container->getDefinition('foo.retryable')->getClass());
1511+
$this->assertSame(4, $container->getDefinition('foo.retry_strategy')->getArgument(2));
15121512
}
15131513

15141514
public function testHttpClientWithQueryParameterKey()

src/Symfony/Component/HttpClient/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;

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

Lines changed: 0 additions & 81 deletions
This file was deleted.
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 <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+
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é <jeremy@derusse.com>
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+
}

0 commit comments

Comments
 (0)
0