Skip to content

Commit bafa59d

Browse files
feat: new retries updates
1 parent 5c33ed0 commit bafa59d

12 files changed

Lines changed: 342 additions & 131 deletions

src/DynamoDb/DynamoDbClient.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public function registerSessionHandler(array $config = [])
157157
}
158158

159159
/** @internal */
160-
public static function _applyRetryConfig($value, array &$args, HandlerList $list)
160+
public static function _applyRetryConfig($value, array &$args, HandlerList $list, string $service = 'dynamodb')
161161
{
162162
if ($value) {
163163
$config = \Aws\Retry\ConfigurationProvider::unwrap($value);
@@ -181,12 +181,21 @@ function ($retries) {
181181
'retry'
182182
);
183183
} else {
184+
// Default to 4 max attempts for DynamoDB/DynamoDB Streams
185+
// when no explicit max attempts is configured
186+
if ($config->getMaxAttempts() === \Aws\Retry\ConfigurationProvider::DEFAULT_MAX_ATTEMPTS) {
187+
$config = new \Aws\Retry\Configuration(
188+
$config->getMode(),
189+
4
190+
);
191+
}
184192
$list->appendSign(
185193
RetryMiddlewareV2::wrap(
186194
$config,
187195
[
188196
'collect_stats' => $args['stats']['retries'],
189-
'transient_error_codes' => ['TransactionInProgressException']
197+
'transient_error_codes' => ['TransactionInProgressException'],
198+
'service' => $service,
190199
]
191200
),
192201
'retry'

src/DynamoDbStreams/DynamoDbStreamsClient.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use Aws\AwsClient;
55
use Aws\DynamoDb\DynamoDbClient;
6+
use Aws\HandlerList;
67

78
/**
89
* This client is used to interact with the **Amazon DynamoDb Streams** service.
@@ -22,8 +23,14 @@ public static function getArguments()
2223
{
2324
$args = parent::getArguments();
2425
$args['retries']['default'] = 11;
25-
$args['retries']['fn'] = [DynamoDbClient::class, '_applyRetryConfig'];
26+
$args['retries']['fn'] = [__CLASS__, '_applyRetryConfig'];
2627

2728
return $args;
2829
}
29-
}
30+
31+
/** @internal */
32+
public static function _applyRetryConfig($value, array &$args, HandlerList $list)
33+
{
34+
DynamoDbClient::_applyRetryConfig($value, $args, $list, 'dynamodb-streams');
35+
}
36+
}

src/Retry/ConfigurationProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class ConfigurationProvider extends AbstractConfigurationProvider
4646
implements ConfigurationProviderInterface
4747
{
4848
const DEFAULT_MAX_ATTEMPTS = 3;
49-
const DEFAULT_MODE = 'legacy';
49+
const DEFAULT_MODE = 'standard';
5050
const ENV_MAX_ATTEMPTS = 'AWS_MAX_ATTEMPTS';
5151
const ENV_MODE = 'AWS_RETRY_MODE';
5252
const ENV_PROFILE = 'AWS_PROFILE';

src/Retry/ErrorClassifier.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ final class ErrorClassifier
3636
private const TRANSIENT_ERRORS = [
3737
'RequestTimeout' => true,
3838
'RequestTimeoutException' => true,
39+
'InternalError' => true,
3940
];
4041

4142
private const TRANSIENT_STATUS_CODES = [

src/Retry/ExponentialBackoffDelay.php

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,75 @@
77
* Computes delay using exponential backoff with full jitter.
88
* Implements __invoke so it can be used as a callable delayer.
99
*
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+
*
1018
* @internal
1119
*/
1220
final class ExponentialBackoffDelay
1321
{
22+
private const DYNAMODB_SERVICES = [
23+
'dynamodb' => true,
24+
'dynamodb-streams' => true,
25+
];
26+
1427
public function __construct(private readonly int $maxBackoff = 20000)
1528
{}
1629

1730
/**
1831
* Computes delay in milliseconds for the given attempt number.
1932
*
2033
* @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')
2136
* @return int Delay in milliseconds
2237
*/
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);
2644
}
2745

2846
/**
2947
* Computes delay in milliseconds using exponential backoff with full jitter.
3048
*
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+
*
3152
* @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
3255
* @return int Delay in milliseconds
3356
*/
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 {
3662
$max = mt_getrandmax();
3763
try {
3864
$rand = random_int(0, $max) / $max;
3965
} catch (Exception $_) {
40-
// fallback to prevent failing
4166
$rand = mt_rand(0, $max) / $max;
4267
}
4368

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));
4580
}
4681
}

src/Retry/QuotaManager.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class QuotaManager
1212
private readonly int $initialRetryTokens;
1313
private readonly int $noRetryIncrement;
1414
private readonly int $retryCost;
15-
private readonly int $timeoutRetryCost;
15+
private readonly int $throttlingRetryCost;
1616
private readonly int $maxCapacity;
1717
private int $availableCapacity;
1818
private ?int $capacityAmount = null;
@@ -21,16 +21,16 @@ public function __construct(array $config = [])
2121
{
2222
$this->initialRetryTokens = $config['initial_retry_tokens'] ?? 500;
2323
$this->noRetryIncrement = $config['no_retry_increment'] ?? 1;
24-
$this->retryCost = $config['retry_cost'] ?? 5;
25-
$this->timeoutRetryCost = $config['timeout_retry_cost'] ?? 10;
24+
$this->retryCost = $config['retry_cost'] ?? 14;
25+
$this->throttlingRetryCost = $config['throttling_retry_cost'] ?? 5;
2626
$this->maxCapacity = $this->initialRetryTokens;
2727
$this->availableCapacity = $this->initialRetryTokens;
2828
}
2929

30-
public function hasRetryQuota(mixed $result): bool
30+
public function hasRetryQuota(mixed $result, bool $isThrottlingError = false): bool
3131
{
32-
if ($result instanceof AwsException && $result->isConnectionError()) {
33-
$this->capacityAmount = $this->timeoutRetryCost;
32+
if ($isThrottlingError) {
33+
$this->capacityAmount = $this->throttlingRetryCost;
3434
} else {
3535
$this->capacityAmount = $this->retryCost;
3636
}

src/Retry/RetryDecider.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,19 @@ public function __invoke(
4545
$isRetryable = $this->classifier->isRetryable($result);
4646

4747
if ($isRetryable) {
48-
// Retrieve retry tokens and check if quota has been exceeded
49-
if (!$this->quotaManager->hasRetryQuota($result)) {
50-
return false;
51-
}
52-
48+
// Check max attempts BEFORE deducting quota tokens
5349
if ($attempts >= $maxAttempts) {
5450
if ($result instanceof AwsException) {
5551
$result->setMaxRetriesExceeded();
5652
}
5753
return false;
5854
}
55+
56+
// Retrieve retry tokens and check if quota has been exceeded
57+
$isThrottlingError = $this->classifier->isThrottlingError($result);
58+
if (!$this->quotaManager->hasRetryQuota($result, $isThrottlingError)) {
59+
return false;
60+
}
5961
}
6062

6163
return $isRetryable;

0 commit comments

Comments
 (0)