Skip to content
Merged
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/bugfix-retries-dynamodb-defaults.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "bugfix",
"category": "Retries",
"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."
}
]
62 changes: 43 additions & 19 deletions src/DynamoDb/DynamoDbClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -1452,24 +1452,28 @@ 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 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 RetryConfigurationProvider::chain(
RetryConfigurationProvider::env(),
RetryConfigurationProvider::ini(),
function () {
return Create::promiseFor(
new RetryConfiguration(
RetryConfigurationProvider::getDefaultMode(),
self::DYNAMODB_MAX_ATTEMPTS
)
);
}
);
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 Create::promiseFor(
new RetryConfiguration($mode, $maxAttempts)
);
};
}

/**
Expand Down Expand Up @@ -1503,7 +1507,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;
}

Expand Down Expand Up @@ -1541,8 +1545,7 @@ private static function resolveLegacyModeMaxRetries(
RetryConfigurationInterface $config
): int
{
if (
NewRetriesOptIn::isEnabled()
if (NewRetriesOptIn::isEnabled()
&& is_array($value)
&& !isset($value['max_attempts'])
) {
Expand All @@ -1552,6 +1555,24 @@ 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,
Expand All @@ -1571,11 +1592,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,
Expand Down
7 changes: 5 additions & 2 deletions src/DynamoDbStreams/DynamoDbStreamsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -53,9 +54,11 @@ 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;
}
}
}
79 changes: 61 additions & 18 deletions src/Retry/ConfigurationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
98 changes: 98 additions & 0 deletions tests/Retry/ConfigurationProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading