Skip to content

Commit d46c1b6

Browse files
committed
Add jitter to RetryBackof
1 parent a261a25 commit d46c1b6

File tree

2 files changed

+32
-3
lines changed

2 files changed

+32
-3
lines changed

Retry/ExponentialBackOff.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
/**
1818
* A retry backOff with a constant or exponential retry delay.
1919
*
20-
* For example, if $delayMilliseconds=10000 & $multiplier=1 (default),
20+
* For example, if $delayMilliseconds=10000 & $multiplier=1,
2121
* each retry will wait exactly 10 seconds.
2222
*
2323
* But if $delayMilliseconds=10000 & $multiplier=2:
@@ -33,13 +33,15 @@ final class ExponentialBackOff implements RetryBackOffInterface
3333
private $delayMilliseconds;
3434
private $multiplier;
3535
private $maxDelayMilliseconds;
36+
private $jitter;
3637

3738
/**
3839
* @param int $delayMilliseconds Amount of time to delay (or the initial value when multiplier is used)
3940
* @param float $multiplier Multiplier to apply to the delay each time a retry occurs
4041
* @param int $maxDelayMilliseconds Maximum delay to allow (0 means no maximum)
42+
* @param float $jitter Probability of randomness int delay (0 = none, 1 = 100% random)
4143
*/
42-
public function __construct(int $delayMilliseconds = 1000, float $multiplier = 2.0, int $maxDelayMilliseconds = 0)
44+
public function __construct(int $delayMilliseconds = 1000, float $multiplier = 2.0, int $maxDelayMilliseconds = 0, float $jitter = 0.1)
4345
{
4446
if ($delayMilliseconds < 0) {
4547
throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMilliseconds));
@@ -55,11 +57,20 @@ public function __construct(int $delayMilliseconds = 1000, float $multiplier = 2
5557
throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMilliseconds));
5658
}
5759
$this->maxDelayMilliseconds = $maxDelayMilliseconds;
60+
61+
if ($jitter < 0 || $jitter > 1) {
62+
throw new InvalidArgumentException(sprintf('Jitter must be between 0 and 1: "%s" given.', $jitter));
63+
}
64+
$this->jitter = $jitter;
5865
}
5966

6067
public function getDelay(int $retryCount, string $requestMethod, string $requestUrl, array $requestOptions, int $responseStatusCode, array $responseHeaders, ?string $responseContent, ?TransportExceptionInterface $exception): int
6168
{
6269
$delay = $this->delayMilliseconds * $this->multiplier ** $retryCount;
70+
if ($this->jitter > 0) {
71+
$randomness = $delay * $this->jitter;
72+
$delay = $delay + random_int(-$randomness, +$randomness);
73+
}
6374

6475
if ($delay > $this->maxDelayMilliseconds && 0 !== $this->maxDelayMilliseconds) {
6576
return $this->maxDelayMilliseconds;

Tests/Retry/ExponentialBackOffTest.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ExponentialBackOffTest extends TestCase
2121
*/
2222
public function testGetDelay(int $delay, int $multiplier, int $maxDelay, int $previousRetries, int $expectedDelay)
2323
{
24-
$backOff = new ExponentialBackOff($delay, $multiplier, $maxDelay);
24+
$backOff = new ExponentialBackOff($delay, $multiplier, $maxDelay, 0);
2525

2626
self::assertSame($expectedDelay, $backOff->getDelay($previousRetries, 'GET', 'http://example.com/', [], 200, [], null, null));
2727
}
@@ -50,4 +50,22 @@ public function provideDelay(): iterable
5050
yield [0, 2, 10000, 0, 0];
5151
yield [0, 2, 10000, 1, 0];
5252
}
53+
54+
public function testJitter()
55+
{
56+
$backOff = new ExponentialBackOff(1000, 1, 0, 1);
57+
$belowHalf = 0;
58+
$aboveHalf = 0;
59+
for ($i = 0; $i < 20; ++$i) {
60+
$delay = $backOff->getDelay(0, 'GET', 'http://example.com/', [], 200, [], null, null);
61+
if ($delay < 500) {
62+
++$belowHalf;
63+
} elseif ($delay > 1500) {
64+
++$aboveHalf;
65+
}
66+
}
67+
68+
$this->assertGreaterThanOrEqual(1, $belowHalf);
69+
$this->assertGreaterThanOrEqual(1, $aboveHalf);
70+
}
5371
}

0 commit comments

Comments
 (0)