From 943a6fe57236e2aaf594aeecaf04868c601bd27e Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Wed, 7 Oct 2026 22:46:21 -0400 Subject: [PATCH 1/2] bugfix: DynamoDB retry defaults on the opt-in path --- .../bugfix-retries-dynamodb-defaults.json | 7 + src/DynamoDb/DynamoDbClient.php | 80 ++++++++-- src/DynamoDbStreams/DynamoDbStreamsClient.php | 5 +- tests/Retry/V3/RoutingTest.php | 143 ++++++++++++++++++ 4 files changed, 218 insertions(+), 17 deletions(-) create mode 100644 .changes/nextrelease/bugfix-retries-dynamodb-defaults.json diff --git a/.changes/nextrelease/bugfix-retries-dynamodb-defaults.json b/.changes/nextrelease/bugfix-retries-dynamodb-defaults.json new file mode 100644 index 0000000000..d406b8bef6 --- /dev/null +++ b/.changes/nextrelease/bugfix-retries-dynamodb-defaults.json @@ -0,0 +1,7 @@ +[ + { + "type": "bugfix", + "category": "Retries", + "description": "Fixed the opt-in retry path (AWS_NEW_RETRIES_2026) for DynamoDB and DynamoDB Streams. A retry mode selected without max_attempts (via AWS_RETRY_MODE, retry_mode, or 'retries' => ['mode' => ...]) now uses the DynamoDB attempt defaults (4 in standard/adaptive mode, 11 in legacy mode) instead of 3. DynamoDbStreamsClient now applies the same defaults as DynamoDbClient instead of always using 12 legacy attempts." + } +] diff --git a/src/DynamoDb/DynamoDbClient.php b/src/DynamoDb/DynamoDbClient.php index 22f1479de5..ed1e46b68c 100644 --- a/src/DynamoDb/DynamoDbClient.php +++ b/src/DynamoDb/DynamoDbClient.php @@ -4,6 +4,7 @@ use Aws\Api\Parser\Crc32ValidatingParser; use Aws\AwsClient; use Aws\ClientResolver; +use Aws\Configuration\ConfigurationResolver; use Aws\Exception\AwsException; use Aws\HandlerList; use Aws\Middleware; @@ -1452,24 +1453,49 @@ public static function getArguments() /** * @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. + * path. Resolves the mode from env/INI (falling back to the + * spec default). Unless max_attempts is set explicitly, the + * attempt count is the DynamoDB default for that mode: + * {@see self::DYNAMODB_MAX_ATTEMPTS} attempts in standard and + * adaptive mode, {@see self::DEFAULT_LEGACY_MAX_ATTEMPTS} + * retries in legacy mode. */ public static function _defaultRetries() { - return RetryConfigurationProvider::chain( - RetryConfigurationProvider::env(), - RetryConfigurationProvider::ini(), - function () { - return Create::promiseFor( - new RetryConfiguration( - RetryConfigurationProvider::getDefaultMode(), - self::DYNAMODB_MAX_ATTEMPTS - ) - ); - } - ); + return function () { + $provider = RetryConfigurationProvider::chain( + RetryConfigurationProvider::env(), + RetryConfigurationProvider::ini(), + function () { + return Create::promiseFor( + new RetryConfiguration( + RetryConfigurationProvider::getDefaultMode(), + self::DYNAMODB_MAX_ATTEMPTS + ) + ); + } + ); + + return $provider()->then( + function (RetryConfigurationInterface $config) { + $configuredMaxAttempts = ConfigurationResolver::resolve( + RetryConfigurationProvider::INI_MAX_ATTEMPTS, + null, + 'int' + ); + if ($configuredMaxAttempts !== null) { + return $config; + } + + return new RetryConfiguration( + $config->getMode(), + $config->getMode() === 'legacy' + ? self::DEFAULT_LEGACY_MAX_ATTEMPTS + 1 + : self::DYNAMODB_MAX_ATTEMPTS + ); + } + ); + }; } /** @@ -1503,7 +1529,7 @@ public static function _applyRetryConfig($value, array &$args, HandlerList $list } if (NewRetriesOptIn::isEnabled()) { - self::appendStandardModeRetriesNew($config, $args, $list); + self::appendStandardModeRetriesNew($value, $config, $args, $list); return; } @@ -1552,6 +1578,25 @@ private static function resolveLegacyModeMaxRetries( return $config->getMaxAttempts() - 1; } + private static function resolveStandardModeConfig( + $value, + RetryConfigurationInterface $config + ): RetryConfigurationInterface + { + if ( + NewRetriesOptIn::isEnabled() + && is_array($value) + && !isset($value['max_attempts']) + ) { + return new RetryConfiguration( + $config->getMode(), + self::DYNAMODB_MAX_ATTEMPTS + ); + } + + return $config; + } + private static function appendStandardModeRetries( RetryConfigurationInterface $config, array &$args, @@ -1571,11 +1616,14 @@ private static function appendStandardModeRetries( } private static function appendStandardModeRetriesNew( + $value, RetryConfigurationInterface $config, array &$args, HandlerList $list ): void { + $config = self::resolveStandardModeConfig($value, $config); + $list->appendSign( RetryV3Middleware::wrap( $config, diff --git a/src/DynamoDbStreams/DynamoDbStreamsClient.php b/src/DynamoDbStreams/DynamoDbStreamsClient.php index 797396aefd..08dc61db43 100644 --- a/src/DynamoDbStreams/DynamoDbStreamsClient.php +++ b/src/DynamoDbStreams/DynamoDbStreamsClient.php @@ -3,6 +3,7 @@ use Aws\AwsClient; use Aws\DynamoDb\DynamoDbClient; +use Aws\Retry\V3\OptIn as NewRetriesOptIn; /** * This client is used to interact with the **Amazon DynamoDb Streams** service. @@ -53,7 +54,9 @@ class DynamoDbStreamsClient extends AwsClient public static function getArguments() { $args = parent::getArguments(); - $args['retries']['default'] = 11; + $args['retries']['default'] = NewRetriesOptIn::isEnabled() + ? [DynamoDbClient::class, '_defaultRetries'] + : 11; $args['retries']['fn'] = [DynamoDbClient::class, '_applyRetryConfig']; return $args; diff --git a/tests/Retry/V3/RoutingTest.php b/tests/Retry/V3/RoutingTest.php index 1256c2c2bc..959bd38a70 100644 --- a/tests/Retry/V3/RoutingTest.php +++ b/tests/Retry/V3/RoutingTest.php @@ -2,6 +2,7 @@ namespace Aws\Test\Retry\V3; use Aws\DynamoDb\DynamoDbClient; +use Aws\DynamoDbStreams\DynamoDbStreamsClient; use Aws\Retry\Configuration; use Aws\Retry\ConfigurationProvider; use Aws\Retry\V3\OptIn; @@ -9,6 +10,8 @@ use Aws\RetryMiddlewareV2; use Aws\S3\S3Client; use Aws\Sts\StsClient; +use GuzzleHttp\Promise\Create; +use GuzzleHttp\Psr7\Response; use PHPUnit\Framework\TestCase; /** @@ -107,6 +110,121 @@ public function testDynamoDbUsesStandardMiddlewareWhenOptedIn(): void $this->assertSame(RetryV3Middleware::class, $entries[0]['middleware_class']); } + /** + * @dataProvider envOrIniModeProvider + */ + public function testDynamoDbDefaultAttemptsFromEnvOrIniWhenOptedIn( + array $env, + string $expectedMode, + int $expectedAttempts, + ?string $ini = null + ): void { + // When only the mode comes from the environment or ~/.aws/config, + // DynamoDB must apply its own max_attempts default (4 standard/adaptive, + // 11 legacy) rather than the generic 3 filled in by the env/ini providers. + $this->enableFlag(); + $saved = []; + foreach (['AWS_RETRY_MODE', 'AWS_MAX_ATTEMPTS', 'AWS_CONFIG_FILE', 'AWS_PROFILE', 'HOME'] as $k) { + $saved[$k] = getenv($k); + } + $home = sys_get_temp_dir() . '/ddb-retries-' . uniqid(); + mkdir($home . '/.aws', 0777, true); + try { + putenv('AWS_CONFIG_FILE'); + putenv('AWS_PROFILE'); + putenv('AWS_MAX_ATTEMPTS'); + putenv("HOME=$home"); + foreach ($env as $k => $v) { + putenv("$k=$v"); + } + if ($ini !== null) { + file_put_contents($home . '/.aws/config', $ini); + } + + $config = call_user_func(DynamoDbClient::_defaultRetries())->wait(); + $this->assertSame($expectedMode, $config->getMode()); + $this->assertSame($expectedAttempts, $config->getMaxAttempts()); + } finally { + foreach ($saved as $k => $v) { + putenv($v === false ? $k : "$k=$v"); + } + @unlink($home . '/.aws/config'); + rmdir($home . '/.aws'); + rmdir($home); + } + } + + public static function envOrIniModeProvider(): array + { + return [ + 'legacy only' => [['AWS_RETRY_MODE' => 'legacy'], 'legacy', 11], + 'standard only' => [['AWS_RETRY_MODE' => 'standard'], 'standard', 4], + 'adaptive only' => [['AWS_RETRY_MODE' => 'adaptive'], 'adaptive', 4], + 'legacy + explicit max_attempts' => [['AWS_RETRY_MODE' => 'legacy', 'AWS_MAX_ATTEMPTS' => '5'], 'legacy', 5], + 'standard + explicit max_attempts' => [['AWS_RETRY_MODE' => 'standard', 'AWS_MAX_ATTEMPTS' => '5'], 'standard', 5], + 'ini legacy only' => [[], 'legacy', 11, "[default]\nretry_mode = legacy\n"], + 'ini standard only' => [[], 'standard', 4, "[default]\nretry_mode = standard\n"], + 'ini standard + explicit max_attempts' => [[], 'standard', 5, "[default]\nretry_mode = standard\nmax_attempts = 5\n"], + ]; + } + + /** + * @dataProvider arrayModeProvider + */ + public function testDynamoDbDefaultAttemptsFromArrayWhenOptedIn( + array $retries, + int $expectedAttempts + ): void { + // 'retries' => ['mode' => ...] without max_attempts must use the + // DynamoDB attempt default (4 standard/adaptive, 11 legacy), not the + // generic 3 that ConfigurationProvider::unwrap() fills in. + $this->enableFlag(); + $this->assertSame($expectedAttempts, $this->countDynamoDbAttempts($retries)); + } + + public static function arrayModeProvider(): array + { + return [ + 'standard' => [['mode' => 'standard'], 4], + 'adaptive' => [['mode' => 'adaptive'], 4], + 'legacy' => [['mode' => 'legacy'], 11], + 'standard + max_attempts' => [['mode' => 'standard', 'max_attempts' => 2], 2], + ]; + } + + public function testDynamoDbArrayAttemptsUnchangedWhenOptedOut(): void + { + // Pre-opt-in behaviour: the generic 3 attempts. + $this->assertSame(3, $this->countDynamoDbAttempts(['mode' => 'standard'])); + } + + public function testDynamoDbStreamsDefaultRetriesIsElevenWhenOptedOut(): void + { + $args = DynamoDbStreamsClient::getArguments(); + $this->assertSame(11, $args['retries']['default']); + } + + public function testDynamoDbStreamsSharesDynamoDbDefaultsWhenOptedIn(): void + { + $this->enableFlag(); + $args = DynamoDbStreamsClient::getArguments(); + $this->assertSame([DynamoDbClient::class, '_defaultRetries'], $args['retries']['default']); + + $attempts = 0; + $client = new DynamoDbStreamsClient([ + 'region' => 'us-east-1', + 'version' => 'latest', + 'credentials' => false, + 'http_handler' => $this->failingHandler($attempts), + ]); + $entries = $this->retryEntries($client); + $this->assertCount(1, $entries); + $this->assertSame(RetryV3Middleware::class, $entries[0]['middleware_class']); + + $client->listStreams(); + $this->assertSame(4, $attempts); + } + public function testStsRetriesFnInheritsParentWhenOptedOut(): void { // STS does not override the retry handler when opted out; it falls @@ -155,6 +273,31 @@ private function newDynamoDb(array $extra = []): DynamoDbClient ] + $extra); } + /** Handler that always returns HTTP 500 and counts invocations. */ + private function failingHandler(int &$attempts): callable + { + return function ($request, array $options) use (&$attempts) { + $attempts++; + return Create::promiseFor(new Response(500, [], '')); + }; + } + + /** Total attempts a DynamoDbClient makes for the given 'retries' config. */ + private function countDynamoDbAttempts(array $retries): int + { + $attempts = 0; + $client = new DynamoDbClient([ + 'region' => 'us-east-1', + 'version' => 'latest', + 'credentials' => false, + 'http_handler' => $this->failingHandler($attempts), + 'retries' => $retries, + ]); + $client->listTables(); + + return $attempts; + } + /** * Returns the list of registered 'retry' handlers and the runtime class * each one wraps the next handler with. The handler list keeps entries From 946171cf707772bdd6ea7c60b81f64f006e7c8a8 Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Mon, 12 Oct 2026 13:01:31 -0400 Subject: [PATCH 2/2] bugfix: Resolve retry_mode and max_attempts independently from env and shared config --- .../bugfix-retries-dynamodb-defaults.json | 2 +- src/DynamoDb/DynamoDbClient.php | 56 +++-------- src/DynamoDbStreams/DynamoDbStreamsClient.php | 2 +- src/Retry/ConfigurationProvider.php | 79 +++++++++++---- tests/Retry/ConfigurationProviderTest.php | 98 +++++++++++++++++++ tests/Retry/V3/RoutingTest.php | 28 +++--- 6 files changed, 194 insertions(+), 71 deletions(-) diff --git a/.changes/nextrelease/bugfix-retries-dynamodb-defaults.json b/.changes/nextrelease/bugfix-retries-dynamodb-defaults.json index d406b8bef6..9065cc1e4e 100644 --- a/.changes/nextrelease/bugfix-retries-dynamodb-defaults.json +++ b/.changes/nextrelease/bugfix-retries-dynamodb-defaults.json @@ -2,6 +2,6 @@ { "type": "bugfix", "category": "Retries", - "description": "Fixed the opt-in retry path (AWS_NEW_RETRIES_2026) for DynamoDB and DynamoDB Streams. A retry mode selected without max_attempts (via AWS_RETRY_MODE, retry_mode, or 'retries' => ['mode' => ...]) now uses the DynamoDB attempt defaults (4 in standard/adaptive mode, 11 in legacy mode) instead of 3. DynamoDbStreamsClient now applies the same defaults as DynamoDbClient instead of always using 12 legacy attempts." + "description": "retry_mode and max_attempts are now resolved independently, so a value set in the environment no longer hides the other value in the shared config file. The shared config file lookup now also honors [profile name] sections, and an invalid retry_mode or max_attempts in the shared config file now raises a ConfigurationException, as it already did for environment variables. On the opt-in retry path (AWS_NEW_RETRIES_2026), DynamoDB and DynamoDB Streams use the DynamoDB attempt defaults (4 in standard/adaptive mode, 11 in legacy mode) when max_attempts is not set, and DynamoDbStreamsClient applies the same defaults as DynamoDbClient." } ] diff --git a/src/DynamoDb/DynamoDbClient.php b/src/DynamoDb/DynamoDbClient.php index ed1e46b68c..561bee4a50 100644 --- a/src/DynamoDb/DynamoDbClient.php +++ b/src/DynamoDb/DynamoDbClient.php @@ -4,7 +4,6 @@ use Aws\Api\Parser\Crc32ValidatingParser; use Aws\AwsClient; use Aws\ClientResolver; -use Aws\Configuration\ConfigurationResolver; use Aws\Exception\AwsException; use Aws\HandlerList; use Aws\Middleware; @@ -1453,47 +1452,26 @@ public static function getArguments() /** * @internal Default retry-config provider for the AWS_NEW_RETRIES_2026 - * path. Resolves the mode from env/INI (falling back to the - * spec default). Unless max_attempts is set explicitly, the - * attempt count is the DynamoDB default for that mode: + * path. Resolves the mode and max_attempts independently from + * env and the shared config file. Unless max_attempts is set, + * the attempt count is the DynamoDB default for the mode: * {@see self::DYNAMODB_MAX_ATTEMPTS} attempts in standard and * adaptive mode, {@see self::DEFAULT_LEGACY_MAX_ATTEMPTS} * retries in legacy mode. + * + * @param array $args Client arguments. */ - public static function _defaultRetries() + public static function _defaultRetries(array $args = []) { - return function () { - $provider = RetryConfigurationProvider::chain( - RetryConfigurationProvider::env(), - RetryConfigurationProvider::ini(), - function () { - return Create::promiseFor( - new RetryConfiguration( - RetryConfigurationProvider::getDefaultMode(), - self::DYNAMODB_MAX_ATTEMPTS - ) - ); - } - ); - - return $provider()->then( - function (RetryConfigurationInterface $config) { - $configuredMaxAttempts = ConfigurationResolver::resolve( - RetryConfigurationProvider::INI_MAX_ATTEMPTS, - null, - 'int' - ); - if ($configuredMaxAttempts !== null) { - return $config; - } + return function () use ($args) { + $mode = RetryConfigurationProvider::resolveMode($args); + $maxAttempts = RetryConfigurationProvider::resolveMaxAttempts($args) + ?? ($mode === 'legacy' + ? self::DEFAULT_LEGACY_MAX_ATTEMPTS + 1 + : self::DYNAMODB_MAX_ATTEMPTS); - return new RetryConfiguration( - $config->getMode(), - $config->getMode() === 'legacy' - ? self::DEFAULT_LEGACY_MAX_ATTEMPTS + 1 - : self::DYNAMODB_MAX_ATTEMPTS - ); - } + return Create::promiseFor( + new RetryConfiguration($mode, $maxAttempts) ); }; } @@ -1567,8 +1545,7 @@ private static function resolveLegacyModeMaxRetries( RetryConfigurationInterface $config ): int { - if ( - NewRetriesOptIn::isEnabled() + if (NewRetriesOptIn::isEnabled() && is_array($value) && !isset($value['max_attempts']) ) { @@ -1583,8 +1560,7 @@ private static function resolveStandardModeConfig( RetryConfigurationInterface $config ): RetryConfigurationInterface { - if ( - NewRetriesOptIn::isEnabled() + if (NewRetriesOptIn::isEnabled() && is_array($value) && !isset($value['max_attempts']) ) { diff --git a/src/DynamoDbStreams/DynamoDbStreamsClient.php b/src/DynamoDbStreams/DynamoDbStreamsClient.php index 08dc61db43..e8d2d7502f 100644 --- a/src/DynamoDbStreams/DynamoDbStreamsClient.php +++ b/src/DynamoDbStreams/DynamoDbStreamsClient.php @@ -61,4 +61,4 @@ public static function getArguments() return $args; } -} \ No newline at end of file +} diff --git a/src/Retry/ConfigurationProvider.php b/src/Retry/ConfigurationProvider.php index a631906286..a686a675a5 100644 --- a/src/Retry/ConfigurationProvider.php +++ b/src/Retry/ConfigurationProvider.php @@ -3,6 +3,7 @@ use Aws\AbstractConfigurationProvider; use Aws\CacheInterface; +use Aws\Configuration\ConfigurationResolver; use Aws\ConfigurationProviderInterface; use Aws\Retry\Exception\ConfigurationException; use Aws\Retry\V3\OptIn; @@ -60,12 +61,11 @@ class ConfigurationProvider extends AbstractConfigurationProvider protected static $exceptionClass = ConfigurationException::class; /** - * Create a default config provider that first checks for environment - * variables, then checks for a specified profile in the environment-defined - * config file location (env variable is 'AWS_CONFIG_FILE', file location - * defaults to ~/.aws/config), then checks for the "default" profile in the - * environment-defined config file location, and failing those uses a default - * fallback set of configuration options. + * Create a default config provider that resolves the retry mode and + * max attempts independently. Each value is read from its environment + * variable, then from the profile selected by AWS_PROFILE (or "default") + * in the shared config file (AWS_CONFIG_FILE, defaulting to + * ~/.aws/config), and failing those falls back to its default. * * This provider is automatically wrapped in a memoize function that caches * previously provided config options. @@ -76,18 +76,14 @@ class ConfigurationProvider extends AbstractConfigurationProvider */ public static function defaultProvider(array $config = []) { - $configProviders = [self::env()]; - if ( - !isset($config['use_aws_shared_config_files']) - || $config['use_aws_shared_config_files'] != false - ) { - $configProviders[] = self::ini(); - } - $configProviders[] = self::fallback(); - - $memo = self::memoize( - call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders) - ); + $memo = self::memoize(function () use ($config) { + return Promise\Create::promiseFor( + new Configuration( + self::resolveMode($config), + self::resolveMaxAttempts($config) ?? self::DEFAULT_MAX_ATTEMPTS + ) + ); + }); if (isset($config['retries']) && $config['retries'] instanceof CacheInterface @@ -149,6 +145,53 @@ public static function getDefaultMode(): string return OptIn::isEnabled() ? 'standard' : self::DEFAULT_MODE; } + /** + * @internal Resolves retry_mode from AWS_RETRY_MODE, then the shared + * config file, then {@see self::getDefaultMode()}. The value + * is lowercased, matching {@see Configuration}. + * + * @param array $config Client arguments ('use_aws_shared_config_files'). + * + * @return string + */ + public static function resolveMode(array $config = []): string + { + $mode = ConfigurationResolver::resolve( + self::INI_MODE, + null, + 'string', + $config + ); + + return $mode === null || $mode === '' + ? self::getDefaultMode() + : strtolower($mode); + } + + /** + * @internal Resolves max_attempts from AWS_MAX_ATTEMPTS, then the shared + * config file. Returns null when neither sets a value so each + * caller can apply its own default. The value is returned + * unvalidated; {@see Configuration} rejects invalid values. + * + * @param array $config Client arguments ('use_aws_shared_config_files'). + * + * @return string|null + */ + public static function resolveMaxAttempts(array $config = []): ?string + { + // Resolved as a string so an empty "max_attempts =" stays '' (unset) + // rather than being coerced to 0. + $maxAttempts = ConfigurationResolver::resolve( + self::INI_MAX_ATTEMPTS, + null, + 'string', + $config + ); + + return $maxAttempts === '' ? null : $maxAttempts; + } + /** * Config provider that creates config using a config file whose location * is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to diff --git a/tests/Retry/ConfigurationProviderTest.php b/tests/Retry/ConfigurationProviderTest.php index 532acc7856..161fa02140 100644 --- a/tests/Retry/ConfigurationProviderTest.php +++ b/tests/Retry/ConfigurationProviderTest.php @@ -361,6 +361,104 @@ public function testSelectsEnvironmentOverIniConfiguration() unlink($dir . '/config'); } + #[DataProvider('perValueProvider')] + public function testDefaultProviderResolvesEachValueIndependently( + array $env, + string $ini, + string $expectedMode, + int $expectedMaxAttempts + ) { + $dir = $this->clearEnv(); + file_put_contents($dir . '/config', $ini); + putenv('HOME=' . dirname($dir)); + foreach ($env as $key => $value) { + putenv("$key=$value"); + } + + try { + $expected = new Configuration($expectedMode, $expectedMaxAttempts); + $result = call_user_func(ConfigurationProvider::defaultProvider())->wait(); + $this->assertSame($expected->toArray(), $result->toArray()); + } finally { + unlink($dir . '/config'); + } + } + + public static function perValueProvider(): array + { + return [ + 'env mode, ini max_attempts' => [ + [ConfigurationProvider::ENV_MODE => 'standard'], + "[default]\nmax_attempts = 8\n", + 'standard', + 8, + ], + 'env max_attempts, ini mode' => [ + [ConfigurationProvider::ENV_MAX_ATTEMPTS => '8'], + "[default]\nretry_mode = adaptive\n", + 'adaptive', + 8, + ], + 'env overrides ini per value' => [ + [ConfigurationProvider::ENV_MAX_ATTEMPTS => '8'], + "[default]\nretry_mode = adaptive\nmax_attempts = 5\n", + 'adaptive', + 8, + ], + 'profile prefix' => [ + [ConfigurationProvider::ENV_PROFILE => 'foo'], + "[profile foo]\nretry_mode = adaptive\nmax_attempts = 7\n", + 'adaptive', + 7, + ], + 'profile without prefix' => [ + [ConfigurationProvider::ENV_PROFILE => 'foo'], + "[foo]\nretry_mode = adaptive\nmax_attempts = 7\n", + 'adaptive', + 7, + ], + 'empty ini values' => [ + [], + "[default]\nretry_mode =\nmax_attempts =\n", + 'legacy', + 3, + ], + ]; + } + + public function testDefaultProviderRejectsInvalidMaxAttempts() + { + $this->expectException(ConfigurationException::class); + $this->clearEnv(); + putenv('HOME=/does/not/exist'); + putenv(ConfigurationProvider::ENV_MAX_ATTEMPTS . '=0'); + call_user_func(ConfigurationProvider::defaultProvider())->wait(); + } + + #[DataProvider('invalidIniProvider')] + public function testDefaultProviderRejectsInvalidIniValue(string $ini) + { + $this->expectException(ConfigurationException::class); + $dir = $this->clearEnv(); + file_put_contents($dir . '/config', $ini); + putenv('HOME=' . dirname($dir)); + + try { + call_user_func(ConfigurationProvider::defaultProvider())->wait(); + } finally { + unlink($dir . '/config'); + } + } + + public static function invalidIniProvider(): array + { + return [ + 'max_attempts 0' => ["[default]\nmax_attempts = 0\n"], + 'max_attempts not a number' => ["[default]\nmax_attempts = abc\n"], + 'unknown mode' => ["[default]\nretry_mode = foo\n"], + ]; + } + public function testsPersistsToCache() { $cache = new LruArrayCache(); diff --git a/tests/Retry/V3/RoutingTest.php b/tests/Retry/V3/RoutingTest.php index 959bd38a70..d6318c4606 100644 --- a/tests/Retry/V3/RoutingTest.php +++ b/tests/Retry/V3/RoutingTest.php @@ -12,6 +12,7 @@ use Aws\Sts\StsClient; use GuzzleHttp\Promise\Create; use GuzzleHttp\Psr7\Response; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; /** @@ -110,18 +111,18 @@ public function testDynamoDbUsesStandardMiddlewareWhenOptedIn(): void $this->assertSame(RetryV3Middleware::class, $entries[0]['middleware_class']); } - /** - * @dataProvider envOrIniModeProvider - */ + #[DataProvider('envOrIniModeProvider')] public function testDynamoDbDefaultAttemptsFromEnvOrIniWhenOptedIn( array $env, string $expectedMode, int $expectedAttempts, - ?string $ini = null - ): void { + ?string $ini = null, + array $args = [] + ): void + { // When only the mode comes from the environment or ~/.aws/config, // DynamoDB must apply its own max_attempts default (4 standard/adaptive, - // 11 legacy) rather than the generic 3 filled in by the env/ini providers. + // 11 legacy) rather than the generic 3. $this->enableFlag(); $saved = []; foreach (['AWS_RETRY_MODE', 'AWS_MAX_ATTEMPTS', 'AWS_CONFIG_FILE', 'AWS_PROFILE', 'HOME'] as $k) { @@ -132,6 +133,7 @@ public function testDynamoDbDefaultAttemptsFromEnvOrIniWhenOptedIn( try { putenv('AWS_CONFIG_FILE'); putenv('AWS_PROFILE'); + putenv('AWS_RETRY_MODE'); putenv('AWS_MAX_ATTEMPTS'); putenv("HOME=$home"); foreach ($env as $k => $v) { @@ -141,7 +143,7 @@ public function testDynamoDbDefaultAttemptsFromEnvOrIniWhenOptedIn( file_put_contents($home . '/.aws/config', $ini); } - $config = call_user_func(DynamoDbClient::_defaultRetries())->wait(); + $config = call_user_func(DynamoDbClient::_defaultRetries($args))->wait(); $this->assertSame($expectedMode, $config->getMode()); $this->assertSame($expectedAttempts, $config->getMaxAttempts()); } finally { @@ -165,16 +167,20 @@ public static function envOrIniModeProvider(): array 'ini legacy only' => [[], 'legacy', 11, "[default]\nretry_mode = legacy\n"], 'ini standard only' => [[], 'standard', 4, "[default]\nretry_mode = standard\n"], 'ini standard + explicit max_attempts' => [[], 'standard', 5, "[default]\nretry_mode = standard\nmax_attempts = 5\n"], + 'env mode + ini max_attempts' => [['AWS_RETRY_MODE' => 'standard'], 'standard', 8, "[default]\nmax_attempts = 8\n"], + 'env max_attempts + ini mode' => [['AWS_MAX_ATTEMPTS' => '8'], 'adaptive', 8, "[default]\nretry_mode = adaptive\n"], + 'ini profile prefix' => [['AWS_PROFILE' => 'foo'], 'adaptive', 7, "[profile foo]\nretry_mode = adaptive\nmax_attempts = 7\n"], + 'ini ignored when shared config disabled' => [[], 'standard', 4, "[default]\nretry_mode = adaptive\nmax_attempts = 7\n", ['use_aws_shared_config_files' => false]], + 'uppercase legacy' => [['AWS_RETRY_MODE' => 'LEGACY'], 'legacy', 11], ]; } - /** - * @dataProvider arrayModeProvider - */ + #[DataProvider('arrayModeProvider')] public function testDynamoDbDefaultAttemptsFromArrayWhenOptedIn( array $retries, int $expectedAttempts - ): void { + ): void + { // 'retries' => ['mode' => ...] without max_attempts must use the // DynamoDB attempt default (4 standard/adaptive, 11 legacy), not the // generic 3 that ConfigurationProvider::unwrap() fills in.