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.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "enhancement",
"category": "Retries",
"description": "Updated the opt-in retry path (AWS_NEW_RETRIES_2026) to emit SEP-compliant retry headers, honor AWS_MAX_ATTEMPTS and shared-config max_attempts, and align long-polling retry behavior with the new retry spec."
}
]
2 changes: 1 addition & 1 deletion src/Middleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ public static function invocationId()
RequestInterface $request
) use ($handler){
return $handler($command, $request->withHeader(
'aws-sdk-invocation-id',
'amz-sdk-invocation-id',
md5(uniqid(gethostname(), true))
));
};
Expand Down
33 changes: 23 additions & 10 deletions src/Retry/ConfigurationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,21 @@ public static function env()
return function () {
// Use config from environment variables, if available
$mode = getenv(self::ENV_MODE);
$maxAttempts = getenv(self::ENV_MAX_ATTEMPTS)
? getenv(self::ENV_MAX_ATTEMPTS)
: self::DEFAULT_MAX_ATTEMPTS;
if (!empty($mode)) {
$maxAttempts = getenv(self::ENV_MAX_ATTEMPTS);
$hasMode = $mode !== false && $mode !== '';
$hasMaxAttempts = $maxAttempts !== false && $maxAttempts !== '';

if ($hasMode || $hasMaxAttempts) {
return Promise\Create::promiseFor(
new Configuration($mode, $maxAttempts)
);
new Configuration(
$hasMode ? $mode : self::getDefaultMode(),
$hasMaxAttempts ? $maxAttempts : self::DEFAULT_MAX_ATTEMPTS
));
}

return self::reject('Could not find environment variable config'
. ' in ' . self::ENV_MODE);
. ' in ' . self::ENV_MODE
. ' or ' . self::ENV_MAX_ATTEMPTS);
};
}

Expand Down Expand Up @@ -175,18 +179,27 @@ public static function ini(
if (!isset($data[$profile])) {
return self::reject("'$profile' not found in config file");
}
if (!isset($data[$profile][self::INI_MODE])) {
$hasMode = isset($data[$profile][self::INI_MODE])
&& $data[$profile][self::INI_MODE] !== '';
$hasMaxAttempts = array_key_exists(
self::INI_MAX_ATTEMPTS,
$data[$profile]
) && $data[$profile][self::INI_MAX_ATTEMPTS] !== '';

if (!$hasMode && !$hasMaxAttempts) {
return self::reject("Required retry config values
not present in INI profile '{$profile}' ({$filename})");
}

$maxAttempts = isset($data[$profile][self::INI_MAX_ATTEMPTS])
$maxAttempts = $hasMaxAttempts
? $data[$profile][self::INI_MAX_ATTEMPTS]
: self::DEFAULT_MAX_ATTEMPTS;

return Promise\Create::promiseFor(
new Configuration(
$data[$profile][self::INI_MODE],
$hasMode
? $data[$profile][self::INI_MODE]
: self::getDefaultMode(),
$maxAttempts
)
);
Expand Down
9 changes: 7 additions & 2 deletions src/Retry/RetryHelperTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@

trait RetryHelperTrait
{
private function addRetryHeader($request, $retries, $delayBy)
private function addRetryHeader($request, $retries, $maxAttempts = null)
Comment thread
stobrien89 marked this conversation as resolved.
{
return $request->withHeader('aws-sdk-retry', "{$retries}/{$delayBy}");
$header = ['attempt=' . ($retries + 1)];
if ($maxAttempts !== null) {
$header[] = 'max=' . $maxAttempts;
}

return $request->withHeader('amz-sdk-request', implode('; ', $header));
}


Expand Down
29 changes: 24 additions & 5 deletions src/Retry/V3/RetryMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ class RetryMiddleware

public static function wrap(ConfigurationInterface $config, array $options): \Closure
{
if (!isset($options['quota_manager'])) {
$options['quota_manager'] = new QuotaManager();
}

if ($config->getMode() === 'adaptive'
&& !isset($options['rate_limiter'])
) {
$options['rate_limiter'] = new RateLimiter();
}

return function (callable $handler) use ($config, $options) {
return new static($config, $handler, $options);
};
Expand Down Expand Up @@ -134,7 +144,7 @@ public function __invoke(CommandInterface $cmd, RequestInterface $req): PromiseI
$requestStats = [];
$capacityUsed = null;

$req = $this->addRetryHeader($req, 0, 0);
$req = $this->addRetryHeader($req, 0, $this->resolveMaxAttempts($cmd));

$callback = function ($value) use (
$handler,
Expand Down Expand Up @@ -200,9 +210,7 @@ public function __invoke(CommandInterface $cmd, RequestInterface $req): PromiseI
}

// Max attempts is checked before retry quota.
$maxAttempts = ($cmd['@retries'] !== null)
? $cmd['@retries'] + 1
: $this->maxAttempts;
$maxAttempts = $this->resolveMaxAttempts($cmd);

if ($attempts >= $maxAttempts) {
if ($value instanceof AwsException) {
Expand Down Expand Up @@ -256,7 +264,11 @@ public function __invoke(CommandInterface $cmd, RequestInterface $req): PromiseI
$this->updateStats($attempts - 1, $delayByMs, $requestStats);
}

$req = $this->addRetryHeader($req, $attempts - 1, $delayByMs);
$req = $this->addRetryHeader(
$req,
$attempts - 1,
$maxAttempts
);

if ($this->mode === 'adaptive') {
$this->rateLimiter->getSendToken();
Expand Down Expand Up @@ -415,4 +427,11 @@ private function isThrottlingError(mixed $result): bool

return false;
}

private function resolveMaxAttempts(CommandInterface $cmd): int
{
return ($cmd['@retries'] !== null)
? $cmd['@retries'] + 1
: $this->maxAttempts;
}
}
12 changes: 10 additions & 2 deletions src/RetryMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,11 @@ public function __invoke(
$decider = $this->decider;
$delay = $this->delay;

$request = $this->addRetryHeader($request, 0, 0);
$request = $this->addRetryHeader(
$request,
0,
$command['@retries'] !== null ? $command['@retries'] + 1 : null
);

$g = function ($value) use (
$handler,
Expand Down Expand Up @@ -234,7 +238,11 @@ public function __invoke(
}

// Update retry header with retry count and delayBy
$request = $this->addRetryHeader($request, $retries, $delayBy);
$request = $this->addRetryHeader(
$request,
$retries,
$command['@retries'] !== null ? $command['@retries'] + 1 : null
);

return $handler($command, $request)->then($g, $g);
};
Expand Down
15 changes: 13 additions & 2 deletions src/RetryMiddlewareV2.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public function __invoke(CommandInterface $cmd, RequestInterface $req)
$monitoringEvents = [];
$requestStats = [];

$req = $this->addRetryHeader($req, 0, 0);
$req = $this->addRetryHeader($req, 0, $this->resolveMaxAttempts($cmd));

$callback = function ($value) use (
$handler,
Expand Down Expand Up @@ -215,7 +215,11 @@ public function __invoke(CommandInterface $cmd, RequestInterface $req)
}

// Update retry header with retry count and delayBy
$req = $this->addRetryHeader($req, $attempts - 1, $delayBy);
$req = $this->addRetryHeader(
$req,
$attempts - 1,
$this->resolveMaxAttempts($cmd)
);

// Get token from rate limiter, which will sleep if necessary
if ($this->mode === 'adaptive') {
Expand Down Expand Up @@ -352,4 +356,11 @@ private function isThrottlingError($result)

return false;
}

private function resolveMaxAttempts(CommandInterface $cmd)
{
return $cmd['@retries'] !== null
? $cmd['@retries'] + 1
: $this->maxAttempts;
}
}
8 changes: 4 additions & 4 deletions src/Signature/SignatureV4.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ protected function getHeaderBlacklist()
'referer' => true,
'user-agent' => true,
'x-amzn-trace-id' => true,
'aws-sdk-invocation-id' => true,
'aws-sdk-retry' => true,
'amz-sdk-invocation-id' => true,
'amz-sdk-request' => true,
];
}

Expand Down Expand Up @@ -498,8 +498,8 @@ private function removeIllegalV4aHeaders(&$request)
{
static $illegalV4aHeaders = [
self::AMZ_CONTENT_SHA256_HEADER,
'aws-sdk-invocation-id',
'aws-sdk-retry',
'amz-sdk-invocation-id',
'amz-sdk-request',
'x-amz-region-set',
'transfer-encoding',
];
Expand Down
2 changes: 1 addition & 1 deletion tests/MiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public function testAddInvocationId()
{
$list = new HandlerList();
$mock = function ($command, $request) {
$this->assertTrue($request->hasHeader('aws-sdk-invocation-id'));
$this->assertTrue($request->hasHeader('amz-sdk-invocation-id'));
return Promise\Create::promiseFor(
new Result(['@metadata' => ['statusCode' => 200]])
);
Expand Down
24 changes: 24 additions & 0 deletions tests/Retry/ConfigurationProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ public function testCreatesFromEnvironmentVariables()
$this->assertSame($expected->toArray(), $result->toArray());
}

public function testCreatesFromEnvironmentMaxAttemptsUsingDefaultMode()
{
$this->clearEnv();
putenv(OptIn::ENV . '=true');
OptIn::reset();
$expected = new Configuration('standard', 17);
putenv(ConfigurationProvider::ENV_MAX_ATTEMPTS . '=17');
$result = call_user_func(ConfigurationProvider::env())->wait();
$this->assertSame($expected->toArray(), $result->toArray());
}

public function testRejectsOnNoEnvironmentVars()
{
$this->clearEnv();
Expand Down Expand Up @@ -202,6 +213,19 @@ public function testCreatesFromIniFileWithSpecifiedProfile()
unlink($dir . '/config');
}

public function testCreatesFromIniFileWithMaxAttemptsOnlyUsingDefaultMode()
{
$dir = $this->clearEnv();
putenv(OptIn::ENV . '=true');
OptIn::reset();
$expected = new Configuration('standard', 7);
file_put_contents($dir . '/config', "[default]\nmax_attempts = 7\n");
putenv('HOME=' . dirname($dir));
$result = call_user_func(ConfigurationProvider::ini(null, null))->wait();
$this->assertSame($expected->toArray(), $result->toArray());
unlink($dir . '/config');
}

public function testEnsuresIniFileExists()
{
$this->expectException(\Aws\Retry\Exception\ConfigurationException::class);
Expand Down
Loading