|
7 | 7 | * Computes delay using exponential backoff with full jitter. |
8 | 8 | * Implements __invoke so it can be used as a callable delayer. |
9 | 9 | * |
| 10 | + * Supports error-type-aware and service-aware base values per SEP v2.1: |
| 11 | + * - Throttling errors: base = 1000 ms |
| 12 | + * - Non-throttling, non-DynamoDB: base = 50 ms |
| 13 | + * - Non-throttling, DynamoDB/DynamoDB Streams: base = 25 ms |
| 14 | + * |
| 15 | + * Jitter is applied AFTER the MAX_BACKOFF cap: |
| 16 | + * delay = b * min(x * 2^i, MAX_BACKOFF) |
| 17 | + * |
10 | 18 | * @internal |
11 | 19 | */ |
12 | 20 | final class ExponentialBackoffDelay |
13 | 21 | { |
| 22 | + private const DYNAMODB_SERVICES = [ |
| 23 | + 'dynamodb' => true, |
| 24 | + 'dynamodb-streams' => true, |
| 25 | + ]; |
| 26 | + |
14 | 27 | public function __construct(private readonly int $maxBackoff = 20000) |
15 | 28 | {} |
16 | 29 |
|
17 | 30 | /** |
18 | 31 | * Computes delay in milliseconds for the given attempt number. |
19 | 32 | * |
20 | 33 | * @param int $attempts Current attempt number |
| 34 | + * @param bool $isThrottlingError Whether the error is a throttling error |
| 35 | + * @param string|null $service The service name (e.g. 'dynamodb') |
21 | 36 | * @return int Delay in milliseconds |
22 | 37 | */ |
23 | | - public function __invoke(int $attempts): int |
24 | | - { |
25 | | - return $this->computeDelay($attempts); |
| 38 | + public function __invoke( |
| 39 | + int $attempts, |
| 40 | + bool $isThrottlingError = false, |
| 41 | + ?string $service = null |
| 42 | + ): int { |
| 43 | + return $this->computeDelay($attempts, $isThrottlingError, $service); |
26 | 44 | } |
27 | 45 |
|
28 | 46 | /** |
29 | 47 | * Computes delay in milliseconds using exponential backoff with full jitter. |
30 | 48 | * |
| 49 | + * Formula: b * min(x * 2^i, MAX_BACKOFF) |
| 50 | + * where b is random jitter [0,1], x is error/service-aware base, i is attempt number. |
| 51 | + * |
31 | 52 | * @param int $attempts Current attempt number |
| 53 | + * @param bool $isThrottlingError Whether the error is a throttling error |
| 54 | + * @param string|null $service The service name |
32 | 55 | * @return int Delay in milliseconds |
33 | 56 | */ |
34 | | - public function computeDelay(int $attempts): int |
35 | | - { |
| 57 | + public function computeDelay( |
| 58 | + int $attempts, |
| 59 | + bool $isThrottlingError = false, |
| 60 | + ?string $service = null |
| 61 | + ): int { |
36 | 62 | $max = mt_getrandmax(); |
37 | 63 | try { |
38 | 64 | $rand = random_int(0, $max) / $max; |
39 | 65 | } catch (Exception $_) { |
40 | | - // fallback to prevent failing |
41 | 66 | $rand = mt_rand(0, $max) / $max; |
42 | 67 | } |
43 | 68 |
|
44 | | - return (int) min(1000 * $rand * pow(2, $attempts), $this->maxBackoff); |
| 69 | + // Determine base in milliseconds based on error type and service |
| 70 | + if ($isThrottlingError) { |
| 71 | + $baseMs = 1000; |
| 72 | + } elseif ($service !== null && isset(self::DYNAMODB_SERVICES[strtolower($service)])) { |
| 73 | + $baseMs = 25; |
| 74 | + } else { |
| 75 | + $baseMs = 50; |
| 76 | + } |
| 77 | + |
| 78 | + // Apply MAX_BACKOFF cap BEFORE jitter: b * min(x * 2^i, MAX_BACKOFF) |
| 79 | + return (int) ($rand * min($baseMs * pow(2, $attempts), $this->maxBackoff)); |
45 | 80 | } |
46 | 81 | } |
0 commit comments