Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changes/nextrelease/feat-new-retries.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "feature",
"category": "Retries",
"description": "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."
}
]
56 changes: 36 additions & 20 deletions src/ClientResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
use Aws\Exception\InvalidRegionException;
use Aws\Retry\ConfigurationInterface as RetryConfigInterface;
use Aws\Retry\ConfigurationProvider as RetryConfigProvider;
use Aws\Retry\Standard\OptIn as NewRetriesOptIn;
use Aws\Retry\Standard\RetryMiddleware as StandardRetryMiddleware;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recommend a rename here since this middleware also handles adaptive mode

use Aws\Signature\SignatureProvider;
use Aws\Token\Token;
use Aws\Token\TokenInterface;
Expand Down Expand Up @@ -547,28 +549,42 @@ private function throwRequired(array $args)
public static function _apply_retries($value, array &$args, HandlerList $list)
{
// A value of 0 for the config option disables retries
if ($value) {
$config = RetryConfigProvider::unwrap($value);
if (!$value) {
return;
}

if ($config->getMode() === 'legacy') {
// # of retries is 1 less than # of attempts
$decider = RetryMiddleware::createDefaultDecider(
$config->getMaxAttempts() - 1
);
$list->appendSign(
Middleware::retry($decider, null, $args['stats']['retries']),
'retry'
);
} else {
$list->appendSign(
RetryMiddlewareV2::wrap(
$config,
['collect_stats' => $args['stats']['retries']]
),
'retry'
);
}
$config = RetryConfigProvider::unwrap($value);

if ($config->getMode() === 'legacy') {
// # of retries is 1 less than # of attempts
$decider = RetryMiddleware::createDefaultDecider(
$config->getMaxAttempts() - 1
);
$list->appendSign(
Middleware::retry($decider, null, $args['stats']['retries']),
'retry'
);
return;
}

if (NewRetriesOptIn::isEnabled()) {
$list->appendSign(
StandardRetryMiddleware::wrap($config, [
'collect_stats' => $args['stats']['retries'],
'service' => $args['service'],
]),
'retry'
);
return;
}

$list->appendSign(
RetryMiddlewareV2::wrap(
$config,
['collect_stats' => $args['stats']['retries']]
),
'retry'
);
}

public static function _apply_defaults($value, array &$args, HandlerList $list)
Expand Down
167 changes: 133 additions & 34 deletions src/DynamoDb/DynamoDbClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
use Aws\Exception\AwsException;
use Aws\HandlerList;
use Aws\Middleware;
use Aws\Retry\Configuration as RetryConfiguration;
use Aws\Retry\ConfigurationInterface as RetryConfigurationInterface;
use Aws\Retry\ConfigurationProvider as RetryConfigurationProvider;
use Aws\Retry\Standard\OptIn as NewRetriesOptIn;
use Aws\Retry\Standard\RetryMiddleware as StandardRetryMiddleware;
use Aws\RetryMiddleware;
use Aws\RetryMiddlewareV2;
use GuzzleHttp\Promise\Create;

/**
* This client is used to interact with the **Amazon DynamoDB** service.
Expand Down Expand Up @@ -130,16 +136,50 @@
*/
class DynamoDbClient extends AwsClient
{
/** @internal Default attempts for the AWS_NEW_RETRIES_2026 path. */
private const DYNAMODB_MAX_ATTEMPTS = 4;
/** @internal Base backoff in ms for the AWS_NEW_RETRIES_2026 path. */
private const DEFAULT_BASE_DELAY_MS = 25;
/**
* @internal Legacy-mode fallback when an array config does not specify
* max_attempts. Only consulted on the AWS_NEW_RETRIES_2026 path.
*/
public const DEFAULT_LEGACY_MAX_ATTEMPTS = 10;

public static function getArguments()
{
$args = parent::getArguments();
$args['retries']['default'] = 10;
$args['retries']['default'] = NewRetriesOptIn::isEnabled()
? [__CLASS__, '_defaultRetries']
: self::DEFAULT_LEGACY_MAX_ATTEMPTS;
$args['retries']['fn'] = [__CLASS__, '_applyRetryConfig'];
$args['api_provider']['fn'] = [__CLASS__, '_applyApiProvider'];

return $args;
}

/**
* @internal Default retry-config provider for the AWS_NEW_RETRIES_2026
* path. Falls through to env/INI before applying the DynamoDB
* default of {@see self::DYNAMODB_MAX_ATTEMPTS} attempts in
* the specs standard mode.
*/
public static function _defaultRetries()
{
return RetryConfigurationProvider::chain(
RetryConfigurationProvider::env(),
RetryConfigurationProvider::ini(),
function () {
return Create::promiseFor(
new RetryConfiguration(
RetryConfigurationProvider::getDefaultMode(),
self::DYNAMODB_MAX_ATTEMPTS
)
);
}
);
}

/**
* Convenience method for instantiating and registering the DynamoDB
* Session handler with this DynamoDB client object.
Expand All @@ -159,40 +199,99 @@ public function registerSessionHandler(array $config = [])
/** @internal */
public static function _applyRetryConfig($value, array &$args, HandlerList $list)
{
if ($value) {
$config = \Aws\Retry\ConfigurationProvider::unwrap($value);

if ($config->getMode() === 'legacy') {
$list->appendSign(
Middleware::retry(
RetryMiddleware::createDefaultDecider(
$config->getMaxAttempts() - 1,
['error_codes' => ['TransactionInProgressException']]
),
function ($retries) {
return $retries
? RetryMiddleware::exponentialDelay($retries) / 2
: 0;
},
isset($args['stats']['retries'])
? (bool)$args['stats']['retries']
: false
),
'retry'
);
} else {
$list->appendSign(
RetryMiddlewareV2::wrap(
$config,
[
'collect_stats' => $args['stats']['retries'],
'transient_error_codes' => ['TransactionInProgressException']
]
),
'retry'
);
}
if (!$value) {
return;
}

$config = RetryConfigurationProvider::unwrap($value);

if ($config->getMode() === 'legacy') {
self::appendLegacyModeRetries($value, $config, $args, $list);
return;
}

if (NewRetriesOptIn::isEnabled()) {
self::appendStandardModeRetriesNew($config, $args, $list);
return;
}

self::appendStandardModeRetries($config, $args, $list);
}

private static function appendLegacyModeRetries(
$value,
RetryConfigurationInterface $config,
array &$args,
HandlerList $list
): void {
Comment on lines +221 to +226
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing a few places in multi-line method signatures where the opening brace needs to be moved down:

Suggested change
private static function appendLegacyModeRetries(
$value,
RetryConfigurationInterface $config,
array &$args,
HandlerList $list
): void {
private static function appendLegacyModeRetries(
$value,
RetryConfigurationInterface $config,
array &$args,
HandlerList $list
): void
{

$maxRetries = self::resolveLegacyModeMaxRetries($value, $config);

$list->appendSign(
Middleware::retry(
RetryMiddleware::createDefaultDecider(
$maxRetries,
['error_codes' => ['TransactionInProgressException']]
),
function ($retries) {
return $retries
? RetryMiddleware::exponentialDelay($retries) / 2
: 0;
},
isset($args['stats']['retries']) ? (bool) $args['stats']['retries'] : false
),
'retry'
);
}

private static function resolveLegacyModeMaxRetries(
$value,
RetryConfigurationInterface $config
): int {
if (
NewRetriesOptIn::isEnabled()
&& is_array($value)
&& !isset($value['max_attempts'])
) {
Comment on lines +250 to +254
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the only one, but I'd check for others:

Suggested change
if (
NewRetriesOptIn::isEnabled()
&& is_array($value)
&& !isset($value['max_attempts'])
) {
if (NewRetriesOptIn::isEnabled()
&& is_array($value)
&& !isset($value['max_attempts'])
) {

return self::DEFAULT_LEGACY_MAX_ATTEMPTS;
}

return $config->getMaxAttempts() - 1;
}

private static function appendStandardModeRetries(
RetryConfigurationInterface $config,
array &$args,
HandlerList $list
): void {
$list->appendSign(
RetryMiddlewareV2::wrap(
$config,
[
'collect_stats' => $args['stats']['retries'],
'transient_error_codes' => ['TransactionInProgressException'],
]
),
'retry'
);
}

private static function appendStandardModeRetriesNew(
RetryConfigurationInterface $config,
array &$args,
HandlerList $list
): void {
$list->appendSign(
StandardRetryMiddleware::wrap(
$config,
[
'collect_stats' => $args['stats']['retries'],
'service' => $args['service'],
'base_delay' => self::DEFAULT_BASE_DELAY_MS,
'transient_error_codes' => ['TransactionInProgressException'],
]
),
'retry'
);
}

/** @internal */
Expand Down
12 changes: 11 additions & 1 deletion src/Retry/ConfigurationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Aws\CacheInterface;
use Aws\ConfigurationProviderInterface;
use Aws\Retry\Exception\ConfigurationException;
use Aws\Retry\Standard\OptIn;
use GuzzleHttp\Promise;
use GuzzleHttp\Promise\PromiseInterface;

Expand Down Expand Up @@ -130,11 +131,20 @@ public static function fallback()
{
return function () {
return Promise\Create::promiseFor(
new Configuration(self::DEFAULT_MODE, self::DEFAULT_MAX_ATTEMPTS)
new Configuration(self::getDefaultMode(), self::DEFAULT_MAX_ATTEMPTS)
);
};
}

/**
* Returns the default retry mode. Reflects the AWS_NEW_RETRIES_2026
* opt-in: 'standard' when the env flag is set, 'legacy' otherwise.
*/
public static function getDefaultMode(): string
{
return OptIn::isEnabled() ? 'standard' : self::DEFAULT_MODE;
}

/**
* Config provider that creates config using a config file whose location
* is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to
Expand Down
27 changes: 27 additions & 0 deletions src/Retry/Standard/LongPolling.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
namespace Aws\Retry\Standard;

/**
* Operations that use server-side long polling and should not be retried
* when the retry quota is exhausted; instead the middleware sleeps for
* the computed backoff and lets the next request proceed.
*
* @internal
*/
final class LongPolling
{
private const OPERATIONS = [
'sqs' => ['ReceiveMessage' => true],
'states' => ['GetActivityTask' => true],
'swf' => [
'PollForActivityTask' => true,
'PollForDecisionTask' => true,
],
];

public static function isLongPolling(?string $service, string $operation): bool
{
return $service !== null
&& isset(self::OPERATIONS[$service][$operation]);
}
}
35 changes: 35 additions & 0 deletions src/Retry/Standard/OptIn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
namespace Aws\Retry\Standard;

/**
* Source of truth for the AWS_NEW_RETRIES_2026 opt-in flag. The env var
* is read once per process and memoized so retry decisions do not pay a
* getenv() cost on every attempt.
*
* @internal
*/
final class OptIn
{
public const ENV = 'AWS_NEW_RETRIES_2026';

private static ?bool $enabled = null;

public static function isEnabled(): bool
{
if (self::$enabled === null) {
self::$enabled = getenv(self::ENV) === 'true';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be a bit more permissive with the value here. See \Aws\boolean_value()

}

return self::$enabled;
}

/**
* Clears the memoized value. Test hook only.
*
* @internal
*/
public static function reset(): void
{
self::$enabled = null;
}
}
Loading