Skip to content

Commit 7fa4366

Browse files
committed
Correct retry waiting behaviour
See #70
1 parent 62a945c commit 7fa4366

2 files changed

Lines changed: 70 additions & 13 deletions

File tree

src/Support/RetriesRequests.php

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ trait RetriesRequests
2424
* @param callable(Throwable, Request): (bool)|null $handleRetry
2525
* @throws FatalRequestException|RequestException
2626
*/
27-
protected function sendAsyncWithRetries(Request $request, ?MockClient $mockClient = null, ?callable $handleRetry = null, int $attempts = 1): PromiseInterface
27+
protected function sendAsyncWithRetries(Request $request, ?MockClient $mockClient = null, ?callable $handleRetry = null, int $attempt = 1): PromiseInterface
2828
{
2929
// Allow retries by default, unless `$handleRetry` is specified
3030
if (is_null($handleRetry)) {
3131
$handleRetry = static fn(Throwable $throwable, Request $request): bool => true;
3232
}
3333

3434
// Send off the request asynchronously and register a rejection handler
35-
return parent::sendAsync($request, $mockClient)->otherwise(function (FatalRequestException|RequestException $exception) use ($request, $mockClient, $handleRetry, $attempts) {
35+
return parent::sendAsync($request, $mockClient)->otherwise(function (FatalRequestException|RequestException $exception) use ($request, $mockClient, $handleRetry, $attempt) {
3636

3737
$maxTries = $request->tries ?? $this->tries;
3838
$retryInterval = $request->retryInterval ?? $this->retryInterval;
@@ -52,7 +52,7 @@ protected function sendAsyncWithRetries(Request $request, ?MockClient $mockClien
5252
}
5353

5454
// Handle max tries being exhausted
55-
if ($attempts === $maxTries) {
55+
if ($attempt === $maxTries) {
5656
return isset($exceptionResponse) && $throwOnMaxTries === false ? $exceptionResponse : throw $exception;
5757
}
5858

@@ -67,10 +67,10 @@ protected function sendAsyncWithRetries(Request $request, ?MockClient $mockClien
6767
}
6868

6969
// Wait for the appropriate amount of time based on the retry config
70-
$this->waitBeforeRetry($attempts, $retryInterval, $useExponentialBackoff);
70+
$this->waitBeforeRetry($attempt, $retryInterval, $useExponentialBackoff);
7171

7272
// Retry the request
73-
return $this->sendAsyncWithRetries($request, $mockClient, $handleRetry, $attempts + 1);
73+
return $this->sendAsyncWithRetries($request, $mockClient, $handleRetry, $attempt + 1);
7474
});
7575
}
7676

@@ -90,16 +90,17 @@ public function handleRetry(FatalRequestException|RequestException $exception, R
9090

9191
/**
9292
* Wait before the next retry, based on the retry config.
93+
*
94+
* @param int $attempt The index of the attempt that has just taken place
95+
* @param int $interval The number of miliseconds to use as the base wait interval
96+
* @param bool $useExponentialBackoff If enabled, the interval will be doubled for each subsequent attempt
9397
*/
94-
protected function waitBeforeRetry(int $attempts, int $interval, bool $useExponentialBackoff): void
98+
protected function waitBeforeRetry(int $attempt, int $interval, bool $useExponentialBackoff): void
9599
{
96-
// From the 2nd attempt onwards, wait before executing the attempt
97-
if ($attempts > 0) {
98-
$sleepTime = $useExponentialBackoff
99-
? $interval * (2 ** ($attempts - 2)) * 1000
100-
: $interval * 1000;
100+
$sleepTime = $useExponentialBackoff
101+
? $interval * (2 ** ($attempt - 1)) * 1000
102+
: $interval * 1000;
101103

102-
usleep($sleepTime);
103-
}
104+
usleep($sleepTime);
104105
}
105106
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace Dru1x\ExpoPush\Tests\Unit\Support;
4+
5+
use Dru1x\ExpoPush\Support\RetriesRequests;
6+
use PHPUnit\Framework\Attributes\Test;
7+
use PHPUnit\Framework\TestCase;
8+
9+
class RetriesRequestsTest extends TestCase
10+
{
11+
#[Test]
12+
public function exponential_backoff_wait_time_doubles_with_each_attempt(): void
13+
{
14+
$waiter = new RetryWaiter();
15+
$interval = 5;
16+
17+
foreach ([1, 2, 3, 4] as $attempt) {
18+
$expectedMicroseconds = $interval * (2 ** ($attempt - 1)) * 1000;
19+
20+
$start = hrtime(true);
21+
$waiter->wait($attempt, $interval, true);
22+
$elapsedMicroseconds = (hrtime(true) - $start) / 1000;
23+
24+
$this->assertGreaterThanOrEqual($expectedMicroseconds, $elapsedMicroseconds, "Attempt {$attempt} did not wait long enough.");
25+
$this->assertLessThan($expectedMicroseconds + 50_000, $elapsedMicroseconds, "Attempt {$attempt} waited too long.");
26+
}
27+
}
28+
29+
#[Test]
30+
public function wait_time_stays_constant_across_attempts_when_exponential_backoff_is_disabled(): void
31+
{
32+
$waiter = new RetryWaiter();
33+
$interval = 5;
34+
35+
$expectedMicroseconds = $interval * 1000;
36+
37+
foreach ([1, 2, 3] as $attempt) {
38+
$start = hrtime(true);
39+
$waiter->wait($attempt, $interval, false);
40+
$elapsedMicroseconds = (hrtime(true) - $start) / 1000;
41+
42+
$this->assertGreaterThanOrEqual($expectedMicroseconds, $elapsedMicroseconds, "Attempt {$attempt} did not wait long enough.");
43+
$this->assertLessThan($expectedMicroseconds + 50_000, $elapsedMicroseconds, "Attempt {$attempt} waited too long.");
44+
}
45+
}
46+
}
47+
48+
class RetryWaiter
49+
{
50+
use RetriesRequests;
51+
52+
public function wait(int $attempt, int $interval, bool $useExponentialBackoff): void
53+
{
54+
$this->waitBeforeRetry($attempt, $interval, $useExponentialBackoff);
55+
}
56+
}

0 commit comments

Comments
 (0)