Skip to content

Commit 6eccca5

Browse files
feat: new retries implementation
Adds an opt-in new retry behavior. Set AWS_NEW_RETRIES_2026=true to enable the new path. When the env var is unset (the default), retry behavior is unchanged from previous releases. With the flag enabled, the SDK switches the default retry mode from 'legacy' to 'standard', adopts a throttling-aware token-bucket retry quota (cost 14 for non-throttling, 5 for throttling), reduces the non-throttling base backoff to 50ms, checks max-attempts before quota, honors the x-amz-retry-after header, sleeps without retrying on long-polling operations (SQS, SFN, SWF) when the quota is exhausted, and lets custom deciders supplement (rather than replace) built-in retryability checks. DynamoDB defaults to 4 attempts with a 25ms base; STS treats IDPCommunicationError as transient; S3's existing custom decider keeps its socket carve-out. The flag is intended as an opt-in for early adopters and will become the default in a future release.
1 parent 261bfa4 commit 6eccca5

15 files changed

Lines changed: 3075 additions & 128 deletions

src/ClientResolver.php

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
use Aws\Exception\InvalidRegionException;
3232
use Aws\Retry\ConfigurationInterface as RetryConfigInterface;
3333
use Aws\Retry\ConfigurationProvider as RetryConfigProvider;
34+
use Aws\Retry\Standard\OptIn as NewRetriesOptIn;
35+
use Aws\Retry\Standard\RetryMiddleware as StandardRetryMiddleware;
3436
use Aws\Signature\SignatureProvider;
3537
use Aws\Token\Token;
3638
use Aws\Token\TokenInterface;
@@ -547,28 +549,42 @@ private function throwRequired(array $args)
547549
public static function _apply_retries($value, array &$args, HandlerList $list)
548550
{
549551
// A value of 0 for the config option disables retries
550-
if ($value) {
551-
$config = RetryConfigProvider::unwrap($value);
552+
if (!$value) {
553+
return;
554+
}
552555

553-
if ($config->getMode() === 'legacy') {
554-
// # of retries is 1 less than # of attempts
555-
$decider = RetryMiddleware::createDefaultDecider(
556-
$config->getMaxAttempts() - 1
557-
);
558-
$list->appendSign(
559-
Middleware::retry($decider, null, $args['stats']['retries']),
560-
'retry'
561-
);
562-
} else {
563-
$list->appendSign(
564-
RetryMiddlewareV2::wrap(
565-
$config,
566-
['collect_stats' => $args['stats']['retries']]
567-
),
568-
'retry'
569-
);
570-
}
556+
$config = RetryConfigProvider::unwrap($value);
557+
558+
if ($config->getMode() === 'legacy') {
559+
// # of retries is 1 less than # of attempts
560+
$decider = RetryMiddleware::createDefaultDecider(
561+
$config->getMaxAttempts() - 1
562+
);
563+
$list->appendSign(
564+
Middleware::retry($decider, null, $args['stats']['retries']),
565+
'retry'
566+
);
567+
return;
568+
}
569+
570+
if (NewRetriesOptIn::isEnabled()) {
571+
$list->appendSign(
572+
StandardRetryMiddleware::wrap($config, [
573+
'collect_stats' => $args['stats']['retries'],
574+
'service' => $args['service'],
575+
]),
576+
'retry'
577+
);
578+
return;
571579
}
580+
581+
$list->appendSign(
582+
RetryMiddlewareV2::wrap(
583+
$config,
584+
['collect_stats' => $args['stats']['retries']]
585+
),
586+
'retry'
587+
);
572588
}
573589

574590
public static function _apply_defaults($value, array &$args, HandlerList $list)

src/DynamoDb/DynamoDbClient.php

Lines changed: 133 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@
77
use Aws\Exception\AwsException;
88
use Aws\HandlerList;
99
use Aws\Middleware;
10+
use Aws\Retry\Configuration as RetryConfiguration;
11+
use Aws\Retry\ConfigurationInterface as RetryConfigurationInterface;
12+
use Aws\Retry\ConfigurationProvider as RetryConfigurationProvider;
13+
use Aws\Retry\Standard\OptIn as NewRetriesOptIn;
14+
use Aws\Retry\Standard\RetryMiddleware as StandardRetryMiddleware;
1015
use Aws\RetryMiddleware;
1116
use Aws\RetryMiddlewareV2;
17+
use GuzzleHttp\Promise\Create;
1218

1319
/**
1420
* This client is used to interact with the **Amazon DynamoDB** service.
@@ -130,16 +136,50 @@
130136
*/
131137
class DynamoDbClient extends AwsClient
132138
{
139+
/** @internal Default attempts for the AWS_NEW_RETRIES_2026 path. */
140+
private const DYNAMODB_MAX_ATTEMPTS = 4;
141+
/** @internal Base backoff in ms for the AWS_NEW_RETRIES_2026 path. */
142+
private const DEFAULT_BASE_DELAY_MS = 25;
143+
/**
144+
* @internal Legacy-mode fallback when an array config does not specify
145+
* max_attempts. Only consulted on the AWS_NEW_RETRIES_2026 path.
146+
*/
147+
public const DEFAULT_LEGACY_MAX_ATTEMPTS = 10;
148+
133149
public static function getArguments()
134150
{
135151
$args = parent::getArguments();
136-
$args['retries']['default'] = 10;
152+
$args['retries']['default'] = NewRetriesOptIn::isEnabled()
153+
? [__CLASS__, '_defaultRetries']
154+
: self::DEFAULT_LEGACY_MAX_ATTEMPTS;
137155
$args['retries']['fn'] = [__CLASS__, '_applyRetryConfig'];
138156
$args['api_provider']['fn'] = [__CLASS__, '_applyApiProvider'];
139157

140158
return $args;
141159
}
142160

161+
/**
162+
* @internal Default retry-config provider for the AWS_NEW_RETRIES_2026
163+
* path. Falls through to env/INI before applying the DynamoDB
164+
* default of {@see self::DYNAMODB_MAX_ATTEMPTS} attempts in
165+
* the specs standard mode.
166+
*/
167+
public static function _defaultRetries()
168+
{
169+
return RetryConfigurationProvider::chain(
170+
RetryConfigurationProvider::env(),
171+
RetryConfigurationProvider::ini(),
172+
function () {
173+
return Create::promiseFor(
174+
new RetryConfiguration(
175+
RetryConfigurationProvider::getDefaultMode(),
176+
self::DYNAMODB_MAX_ATTEMPTS
177+
)
178+
);
179+
}
180+
);
181+
}
182+
143183
/**
144184
* Convenience method for instantiating and registering the DynamoDB
145185
* Session handler with this DynamoDB client object.
@@ -159,40 +199,99 @@ public function registerSessionHandler(array $config = [])
159199
/** @internal */
160200
public static function _applyRetryConfig($value, array &$args, HandlerList $list)
161201
{
162-
if ($value) {
163-
$config = \Aws\Retry\ConfigurationProvider::unwrap($value);
164-
165-
if ($config->getMode() === 'legacy') {
166-
$list->appendSign(
167-
Middleware::retry(
168-
RetryMiddleware::createDefaultDecider(
169-
$config->getMaxAttempts() - 1,
170-
['error_codes' => ['TransactionInProgressException']]
171-
),
172-
function ($retries) {
173-
return $retries
174-
? RetryMiddleware::exponentialDelay($retries) / 2
175-
: 0;
176-
},
177-
isset($args['stats']['retries'])
178-
? (bool)$args['stats']['retries']
179-
: false
180-
),
181-
'retry'
182-
);
183-
} else {
184-
$list->appendSign(
185-
RetryMiddlewareV2::wrap(
186-
$config,
187-
[
188-
'collect_stats' => $args['stats']['retries'],
189-
'transient_error_codes' => ['TransactionInProgressException']
190-
]
191-
),
192-
'retry'
193-
);
194-
}
202+
if (!$value) {
203+
return;
204+
}
205+
206+
$config = RetryConfigurationProvider::unwrap($value);
207+
208+
if ($config->getMode() === 'legacy') {
209+
self::appendLegacyModeRetries($value, $config, $args, $list);
210+
return;
211+
}
212+
213+
if (NewRetriesOptIn::isEnabled()) {
214+
self::appendStandardModeRetriesNew($config, $args, $list);
215+
return;
195216
}
217+
218+
self::appendStandardModeRetries($config, $args, $list);
219+
}
220+
221+
private static function appendLegacyModeRetries(
222+
$value,
223+
RetryConfigurationInterface $config,
224+
array &$args,
225+
HandlerList $list
226+
): void {
227+
$maxRetries = self::resolveLegacyModeMaxRetries($value, $config);
228+
229+
$list->appendSign(
230+
Middleware::retry(
231+
RetryMiddleware::createDefaultDecider(
232+
$maxRetries,
233+
['error_codes' => ['TransactionInProgressException']]
234+
),
235+
function ($retries) {
236+
return $retries
237+
? RetryMiddleware::exponentialDelay($retries) / 2
238+
: 0;
239+
},
240+
isset($args['stats']['retries']) ? (bool) $args['stats']['retries'] : false
241+
),
242+
'retry'
243+
);
244+
}
245+
246+
private static function resolveLegacyModeMaxRetries(
247+
$value,
248+
RetryConfigurationInterface $config
249+
): int {
250+
if (
251+
NewRetriesOptIn::isEnabled()
252+
&& is_array($value)
253+
&& !isset($value['max_attempts'])
254+
) {
255+
return self::DEFAULT_LEGACY_MAX_ATTEMPTS;
256+
}
257+
258+
return $config->getMaxAttempts() - 1;
259+
}
260+
261+
private static function appendStandardModeRetries(
262+
RetryConfigurationInterface $config,
263+
array &$args,
264+
HandlerList $list
265+
): void {
266+
$list->appendSign(
267+
RetryMiddlewareV2::wrap(
268+
$config,
269+
[
270+
'collect_stats' => $args['stats']['retries'],
271+
'transient_error_codes' => ['TransactionInProgressException'],
272+
]
273+
),
274+
'retry'
275+
);
276+
}
277+
278+
private static function appendStandardModeRetriesNew(
279+
RetryConfigurationInterface $config,
280+
array &$args,
281+
HandlerList $list
282+
): void {
283+
$list->appendSign(
284+
StandardRetryMiddleware::wrap(
285+
$config,
286+
[
287+
'collect_stats' => $args['stats']['retries'],
288+
'service' => $args['service'],
289+
'base_delay' => self::DEFAULT_BASE_DELAY_MS,
290+
'transient_error_codes' => ['TransactionInProgressException'],
291+
]
292+
),
293+
'retry'
294+
);
196295
}
197296

198297
/** @internal */

src/Retry/ConfigurationProvider.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Aws\CacheInterface;
66
use Aws\ConfigurationProviderInterface;
77
use Aws\Retry\Exception\ConfigurationException;
8+
use Aws\Retry\Standard\OptIn;
89
use GuzzleHttp\Promise;
910
use GuzzleHttp\Promise\PromiseInterface;
1011

@@ -130,11 +131,20 @@ public static function fallback()
130131
{
131132
return function () {
132133
return Promise\Create::promiseFor(
133-
new Configuration(self::DEFAULT_MODE, self::DEFAULT_MAX_ATTEMPTS)
134+
new Configuration(self::getDefaultMode(), self::DEFAULT_MAX_ATTEMPTS)
134135
);
135136
};
136137
}
137138

139+
/**
140+
* Returns the default retry mode. Reflects the AWS_NEW_RETRIES_2026
141+
* opt-in: 'standard' when the env flag is set, 'legacy' otherwise.
142+
*/
143+
public static function getDefaultMode(): string
144+
{
145+
return OptIn::isEnabled() ? 'standard' : self::DEFAULT_MODE;
146+
}
147+
138148
/**
139149
* Config provider that creates config using a config file whose location
140150
* is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to

src/Retry/Standard/LongPolling.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
namespace Aws\Retry\Standard;
3+
4+
/**
5+
* Operations that use server-side long polling and should not be retried
6+
* when the retry quota is exhausted; instead the middleware sleeps for
7+
* the computed backoff and lets the next request proceed.
8+
*
9+
* @internal
10+
*/
11+
final class LongPolling
12+
{
13+
private const OPERATIONS = [
14+
'sqs' => ['ReceiveMessage' => true],
15+
'states' => ['GetActivityTask' => true],
16+
'swf' => [
17+
'PollForActivityTask' => true,
18+
'PollForDecisionTask' => true,
19+
],
20+
];
21+
22+
public static function isLongPolling(?string $service, string $operation): bool
23+
{
24+
return $service !== null
25+
&& isset(self::OPERATIONS[$service][$operation]);
26+
}
27+
}

src/Retry/Standard/OptIn.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
namespace Aws\Retry\Standard;
3+
4+
/**
5+
* Source of truth for the AWS_NEW_RETRIES_2026 opt-in flag. The env var
6+
* is read once per process and memoized so retry decisions do not pay a
7+
* getenv() cost on every attempt.
8+
*
9+
* @internal
10+
*/
11+
final class OptIn
12+
{
13+
public const ENV = 'AWS_NEW_RETRIES_2026';
14+
15+
private static ?bool $enabled = null;
16+
17+
public static function isEnabled(): bool
18+
{
19+
if (self::$enabled === null) {
20+
self::$enabled = getenv(self::ENV) === 'true';
21+
}
22+
23+
return self::$enabled;
24+
}
25+
26+
/**
27+
* Clears the memoized value. Test hook only.
28+
*
29+
* @internal
30+
*/
31+
public static function reset(): void
32+
{
33+
self::$enabled = null;
34+
}
35+
}

0 commit comments

Comments
 (0)