-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: new retries implementation #3269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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." | ||
| } | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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. | ||||||||||||||||||||||||||||
|
|
@@ -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. | ||||||||||||||||||||||||||||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||||||||||||||
| $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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||||||||||||||
| 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 */ | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| 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]); | ||
| } | ||
| } |
| 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'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be a bit more permissive with the value here. See |
||
| } | ||
|
|
||
| return self::$enabled; | ||
| } | ||
|
|
||
| /** | ||
| * Clears the memoized value. Test hook only. | ||
| * | ||
| * @internal | ||
| */ | ||
| public static function reset(): void | ||
| { | ||
| self::$enabled = null; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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
adaptivemode