Skip to content

Commit 21f8f8e

Browse files
committed
feature symfony#53328 [Messenger] Add jitter parameter to MultiplierRetryStrategy (rmikalkenas)
This PR was merged into the 7.1 branch. Discussion ---------- [Messenger] Add jitter parameter to MultiplierRetryStrategy | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT I'm seeing an interesting case, when multiple queue consumers causes failures in a downstream systems a.k.a. [thundering herd](https://en.wikipedia.org/wiki/Thundering_herd_problem) effect. Many jobs fail, they all get enqueued to try again in a static interval and bring down the downstream system yet again. This repeats until the retry limit is exhausted. Introduced delay randomness prevents thundering herd effect. The randomness can be controlled via `$jitter` parameter. ~~**Open question:** I added `$jitter` parameter with a default value of `0.1` (as in [http client's generic retry](https://github.com/symfony/symfony/blob/7.1/src/Symfony/Component/HttpClient/Retry/GenericRetryStrategy.php)), but maybe it should be set as `0` to keep code in a BC fashion?~~ Commits ------- 9949684 [Messenger] Add jitter parameter to MultiplierRetryStrategy
2 parents e3e518b + 9949684 commit 21f8f8e

File tree

6 files changed

+54
-3
lines changed

6 files changed

+54
-3
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,6 +1592,7 @@ function ($a) {
15921592
->integerNode('delay')->defaultValue(1000)->min(0)->info('Time in ms to delay (or the initial value when multiplier is used)')->end()
15931593
->floatNode('multiplier')->defaultValue(2)->min(1)->info('If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries))')->end()
15941594
->integerNode('max_delay')->defaultValue(0)->min(0)->info('Max time in ms that a retry should ever be delayed (0 = infinite)')->end()
1595+
->floatNode('jitter')->defaultValue(0.1)->min(0)->max(1)->info('Randomness to apply to the delay (between 0 and 1)')->end()
15951596
->end()
15961597
->end()
15971598
->scalarNode('rate_limiter')

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2195,7 +2195,8 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
21952195
->replaceArgument(0, $transport['retry_strategy']['max_retries'])
21962196
->replaceArgument(1, $transport['retry_strategy']['delay'])
21972197
->replaceArgument(2, $transport['retry_strategy']['multiplier'])
2198-
->replaceArgument(3, $transport['retry_strategy']['max_delay']);
2198+
->replaceArgument(3, $transport['retry_strategy']['max_delay'])
2199+
->replaceArgument(4, $transport['retry_strategy']['jitter']);
21992200
$container->setDefinition($retryServiceId, $retryDefinition);
22002201

22012202
$transportRetryReferences[$name] = new Reference($retryServiceId);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
abstract_arg('delay ms'),
161161
abstract_arg('multiplier'),
162162
abstract_arg('max delay ms'),
163+
abstract_arg('jitter'),
163164
])
164165

165166
// rate limiter

src/Symfony/Component/Messenger/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add option `redis_sentinel` as an alias for `sentinel_master`
88
* Add `--all` option to the `messenger:consume` command
99
* Make `#[AsMessageHandler]` final
10+
* Add parameter `$jitter` to `MultiplierRetryStrategy` in order to randomize delay and prevent the thundering herd effect
1011

1112
7.0
1213
---

src/Symfony/Component/Messenger/Retry/MultiplierRetryStrategy.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,16 @@ class MultiplierRetryStrategy implements RetryStrategyInterface
3636
private int $delayMilliseconds;
3737
private float $multiplier;
3838
private int $maxDelayMilliseconds;
39+
private float $jitter;
3940

4041
/**
4142
* @param int $maxRetries The maximum number of times to retry
4243
* @param int $delayMilliseconds Amount of time to delay (or the initial value when multiplier is used)
4344
* @param float $multiplier Multiplier to apply to the delay each time a retry occurs
4445
* @param int $maxDelayMilliseconds Maximum delay to allow (0 means no maximum)
46+
* @param float $jitter Randomness to apply to the delay (between 0 and 1)
4547
*/
46-
public function __construct(int $maxRetries = 3, int $delayMilliseconds = 1000, float $multiplier = 1, int $maxDelayMilliseconds = 0)
48+
public function __construct(int $maxRetries = 3, int $delayMilliseconds = 1000, float $multiplier = 1, int $maxDelayMilliseconds = 0, float $jitter = 0.1)
4749
{
4850
$this->maxRetries = $maxRetries;
4951

@@ -61,6 +63,11 @@ public function __construct(int $maxRetries = 3, int $delayMilliseconds = 1000,
6163
throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMilliseconds));
6264
}
6365
$this->maxDelayMilliseconds = $maxDelayMilliseconds;
66+
67+
if ($jitter < 0 || $jitter > 1) {
68+
throw new InvalidArgumentException(sprintf('Jitter must be between 0 and 1: "%s" given.', $jitter));
69+
}
70+
$this->jitter = $jitter;
6471
}
6572

6673
/**
@@ -82,6 +89,11 @@ public function getWaitingTime(Envelope $message, \Throwable $throwable = null):
8289

8390
$delay = $this->delayMilliseconds * $this->multiplier ** $retries;
8491

92+
if ($this->jitter > 0) {
93+
$randomness = (int) ($delay * $this->jitter);
94+
$delay += random_int(-$randomness, +$randomness);
95+
}
96+
8597
if ($delay > $this->maxDelayMilliseconds && 0 !== $this->maxDelayMilliseconds) {
8698
return $this->maxDelayMilliseconds;
8799
}

src/Symfony/Component/Messenger/Tests/Retry/MultiplierRetryStrategyTest.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public function testIsRetryableWithNoStamp()
5454
*/
5555
public function testGetWaitTime(int $delay, float $multiplier, int $maxDelay, int $previousRetries, int $expectedDelay)
5656
{
57-
$strategy = new MultiplierRetryStrategy(10, $delay, $multiplier, $maxDelay);
57+
$strategy = new MultiplierRetryStrategy(10, $delay, $multiplier, $maxDelay, 0);
5858
$envelope = new Envelope(new \stdClass(), [new RedeliveryStamp($previousRetries)]);
5959

6060
$this->assertSame($expectedDelay, $strategy->getWaitingTime($envelope));
@@ -89,4 +89,39 @@ public static function getWaitTimeTests(): iterable
8989
yield [1000, 1.5555, 5000, 1, 1556];
9090
yield [1000, 1.5555, 5000, 2, 2420];
9191
}
92+
93+
/**
94+
* @dataProvider getJitterTest
95+
*/
96+
public function testJitter(float $jitter, int $maxMin, int $maxMax)
97+
{
98+
$strategy = new MultiplierRetryStrategy(3, 1000, 1, 0, $jitter);
99+
$envelope = new Envelope(new \stdClass());
100+
101+
$min = 1000;
102+
$max = 1000;
103+
for ($i = 0; $i < 100; ++$i) {
104+
$delay = $strategy->getWaitingTime($envelope);
105+
$min = min($min, $delay);
106+
$max = max($max, $delay);
107+
}
108+
109+
$this->assertGreaterThanOrEqual($maxMin, $min);
110+
$this->assertLessThanOrEqual($maxMax, $max);
111+
}
112+
113+
public static function getJitterTest(): iterable
114+
{
115+
yield [1.0, 0, 2000];
116+
yield [0.9, 100, 1900];
117+
yield [0.8, 200, 1800];
118+
yield [0.7, 300, 1700];
119+
yield [0.6, 400, 1600];
120+
yield [0.5, 500, 1500];
121+
yield [0.4, 600, 1400];
122+
yield [0.3, 700, 1300];
123+
yield [0.2, 800, 1200];
124+
yield [0.1, 900, 1100];
125+
yield [0.0, 1000, 1000];
126+
}
92127
}

0 commit comments

Comments
 (0)