Skip to content

Commit 2e9958a

Browse files
chore: compact tests cases
- Compact standard retry test cases
1 parent aed21c3 commit 2e9958a

8 files changed

Lines changed: 474 additions & 437 deletions

File tree

src/ClientResolver.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -564,9 +564,6 @@ public static function _apply_retries(mixed $value, array &$args, HandlerList $l
564564
'collect_stats' => $args['stats']['retries'],
565565
'service' => $args['service'],
566566
];
567-
if ($args['service'] === 'sts') {
568-
$retryOptions['transient_error_codes'] = ['IDPCommunicationError'];
569-
}
570567
$list->appendSign(
571568
RetryMiddlewareV2::wrap(
572569
$config,

src/DynamoDb/DynamoDbClient.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
use Aws\Exception\AwsException;
88
use Aws\HandlerList;
99
use Aws\Middleware;
10+
use Aws\Retry\ConfigurationProvider;
1011
use Aws\RetryMiddleware;
1112
use Aws\RetryMiddlewareV2;
13+
use GuzzleHttp\Promise\Create;
1214

1315
/**
1416
* This client is used to interact with the **Amazon DynamoDB** service.
@@ -132,6 +134,7 @@ class DynamoDbClient extends AwsClient
132134
{
133135
/** @internal */
134136
const DYNAMODB_MAX_ATTEMPTS = 4;
137+
const DEFAULT_BASE_DELAY_MS = 25;
135138

136139
public static function getArguments(): array
137140
{
@@ -146,13 +149,13 @@ public static function getArguments(): array
146149
/** @internal */
147150
public static function _defaultRetries(): callable
148151
{
149-
return \Aws\Retry\ConfigurationProvider::chain(
150-
\Aws\Retry\ConfigurationProvider::env(),
151-
\Aws\Retry\ConfigurationProvider::ini(),
152+
return ConfigurationProvider::chain(
153+
ConfigurationProvider::env(),
154+
ConfigurationProvider::ini(),
152155
function () {
153-
return \GuzzleHttp\Promise\Create::promiseFor(
156+
return Create::promiseFor(
154157
new \Aws\Retry\Configuration(
155-
\Aws\Retry\ConfigurationProvider::DEFAULT_MODE,
158+
ConfigurationProvider::DEFAULT_MODE,
156159
self::DYNAMODB_MAX_ATTEMPTS
157160
)
158161
);
@@ -180,7 +183,7 @@ public function registerSessionHandler(array $config = [])
180183
public static function _applyRetryConfig(mixed $value, array &$args, HandlerList $list): void
181184
{
182185
if ($value) {
183-
$config = \Aws\Retry\ConfigurationProvider::unwrap($value);
186+
$config = ConfigurationProvider::unwrap($value);
184187

185188
if ($config->getMode() === 'legacy') {
186189
$list->appendSign(
@@ -194,9 +197,7 @@ function ($retries) {
194197
? RetryMiddleware::exponentialDelay($retries) / 2
195198
: 0;
196199
},
197-
isset($args['stats']['retries'])
198-
? (bool)$args['stats']['retries']
199-
: false
200+
isset($args['stats']['retries']) && $args['stats']['retries']
200201
),
201202
'retry'
202203
);
@@ -206,7 +207,7 @@ function ($retries) {
206207
$config,
207208
[
208209
'collect_stats' => $args['stats']['retries'],
209-
'base_delay' => 0.025,
210+
'base_delay' => self::DEFAULT_BASE_DELAY_MS,
210211
'service' => $args['service'],
211212
'transient_error_codes' => ['TransactionInProgressException'],
212213
]

src/Retry/Configuration.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Configuration implements ConfigurationInterface
1414
private readonly string $mode;
1515
private readonly int $maxAttempts;
1616

17-
public function __construct(string $mode = 'legacy', int|string $maxAttempts = 3)
17+
public function __construct($mode = 'legacy', $maxAttempts = 3)
1818
{
1919
$mode = strtolower($mode);
2020
if (!in_array($mode, self::VALID_MODES, true)) {
@@ -36,23 +36,23 @@ public function __construct(string $mode = 'legacy', int|string $maxAttempts = 3
3636
/**
3737
* {@inheritdoc}
3838
*/
39-
public function getMode(): string
39+
public function getMode()
4040
{
4141
return $this->mode;
4242
}
4343

4444
/**
4545
* {@inheritdoc}
4646
*/
47-
public function getMaxAttempts(): int
47+
public function getMaxAttempts()
4848
{
4949
return $this->maxAttempts;
5050
}
5151

5252
/**
5353
* {@inheritdoc}
5454
*/
55-
public function toArray(): array
55+
public function toArray()
5656
{
5757
return [
5858
'mode' => $this->getMode(),

src/Retry/ConfigurationInterface.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@ interface ConfigurationInterface
1212
*
1313
* @return string
1414
*/
15-
public function getMode(): string;
15+
public function getMode();
1616

1717
/**
1818
* Returns the maximum number of attempts that will be used for a request
1919
*
2020
* @return int
2121
*/
22-
public function getMaxAttempts(): int;
22+
public function getMaxAttempts();
2323

2424
/**
2525
* Returns the configuration as an associative array
2626
*
2727
* @return array
2828
*/
29-
public function toArray(): array;
29+
public function toArray();
3030
}

src/RetryMiddlewareV2.php

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ class RetryMiddlewareV2
2121
{
2222
use RetryHelperTrait;
2323

24-
const THROTTLING_BASE_DELAY = 1.0;
25-
const DEFAULT_BASE_DELAY = 0.05;
26-
const DYNAMODB_BASE_DELAY = 0.025;
24+
private const THROTTLING_BASE_DELAY_MS = 1_000;
25+
private const DEFAULT_BASE_DELAY_MS = 50;
26+
private const DEFAULT_MAX_BACKOFF_MS = 20_000;
27+
private const RETRY_AFTER_HEADER = 'x-amz-retry-after';
2728

2829
private static array $standardThrottlingErrors = [
2930
'Throttling' => true,
@@ -57,16 +58,19 @@ class RetryMiddlewareV2
5758
private static array $longPollingOperations = [
5859
'sqs' => ['ReceiveMessage' => true],
5960
'states' => ['GetActivityTask' => true],
60-
'swf' => ['PollForActivityTask' => true, 'PollForDecisionTask' => true],
61+
'swf' => [
62+
'PollForActivityTask' => true,
63+
'PollForDecisionTask' => true
64+
],
6165
];
6266

63-
private float $baseDelay;
67+
private float $baseDelayMs;
6468
private bool $collectStats;
6569
private ?\Closure $customDecider;
6670
private ?\Closure $delayer;
6771
private ?float $exponentialBase;
6872
private int $maxAttempts;
69-
private int $maxBackoff;
73+
private int $maxBackoffMs;
7074
private string $mode;
7175
private \Closure $nextHandler;
7276
private array $options;
@@ -112,17 +116,17 @@ public function __construct(
112116
$this->nextHandler = $handler(...);
113117
$this->service = $options['service'] ?? null;
114118
$this->quotaManager = $options['quota_manager'] ?? new QuotaManager();
115-
$this->maxBackoff = $options['max_backoff'] ?? 20000;
116-
$this->baseDelay = $options['base_delay'] ?? self::DEFAULT_BASE_DELAY;
119+
$this->maxBackoffMs = $options['max_backoff'] ?? self::DEFAULT_MAX_BACKOFF_MS;
120+
$this->baseDelayMs = $options['base_delay'] ?? self::DEFAULT_BASE_DELAY_MS;
117121
$this->exponentialBase = $options['exponential_base'] ?? null;
118122
$this->collectStats = (bool) ($options['collect_stats'] ?? false);
119123

120124
$this->customDecider = isset($options['decider'])
121-
? \Closure::fromCallable($options['decider'])
125+
? ($options['decider'])(...)
122126
: null;
123127

124128
$this->delayer = isset($options['delayer'])
125-
? \Closure::fromCallable($options['delayer'])
129+
? ($options['delayer'])(...)
126130
: null;
127131

128132
$this->retryCurlErrors = [];
@@ -234,14 +238,22 @@ public function __invoke(CommandInterface $cmd, RequestInterface $req): PromiseI
234238
return $this->bindStatsToReturn($value, $requestStats);
235239
}
236240

237-
// Acquire retry quota
241+
// Compute delay
238242
$isThrottling = $this->isThrottlingError($value);
243+
$attemptIndex = $attempts - 1; // 0-based for backoff formula
244+
$delayByMs = $this->computeRetryDelay(
245+
$attemptIndex,
246+
$isThrottling,
247+
$value
248+
);
249+
250+
// Acquire retry quota
239251
$acquired = $this->quotaManager->acquireRetryQuota($isThrottling);
240252
if ($acquired === false) {
241253
// Long-polling: sleep and return without retrying
242254
if ($this->isLongPollingOperation($cmd)) {
243-
$sleepMs = (int) ($this->baseDelay * 1000);
244-
usleep($sleepMs * 1000);
255+
$cmd['@http']['delay'] = $delayByMs;
256+
usleep((int) ($delayByMs * 1000));
245257
}
246258

247259
if ($isError) {
@@ -253,23 +265,21 @@ public function __invoke(CommandInterface $cmd, RequestInterface $req): PromiseI
253265
}
254266
$capacityUsed = $acquired;
255267

256-
// Compute delay
257-
$attemptIndex = $attempts - 1; // 0-based for backoff formula
268+
// Override delayBy if a custom delayer was provided
258269
if ($this->delayer !== null) {
259-
$delayBy = ($this->delayer)($attempts);
260-
} else {
261-
$delayBy = $this->computeRetryDelay($attemptIndex, $isThrottling, $value);
270+
$delayByMs = ($this->delayer)($attempts);
262271
}
263272

264273
$attempts++;
265-
$cmd['@http']['delay'] = $delayBy;
274+
// Request is delayed by guzzle http client
275+
$cmd['@http']['delay'] = $delayByMs;
266276

267277
if ($this->collectStats) {
268-
$this->updateStats($attempts - 1, $delayBy, $requestStats);
278+
$this->updateStats($attempts - 1, $delayByMs, $requestStats);
269279
}
270280

271281
// Update retry header with retry count and delayBy
272-
$req = $this->addRetryHeader($req, $attempts - 1, $delayBy);
282+
$req = $this->addRetryHeader($req, $attempts - 1, $delayByMs);
273283

274284
// Get token from rate limiter, which will sleep if necessary
275285
if ($this->mode === 'adaptive') {
@@ -296,21 +306,27 @@ public function __invoke(CommandInterface $cmd, RequestInterface $req): PromiseI
296306
*/
297307
public function exponentialDelayWithJitter(int $attempts): int
298308
{
299-
return $this->computeRetryDelay($attempts - 1, false, null);
309+
return $this->computeRetryDelay(
310+
$attempts - 1,
311+
false,
312+
null
313+
);
300314
}
301315

302316
/**
303317
* Computes the retry delay in milliseconds.
304318
*
305-
* @param int $i 0-based attempt index
319+
* @param int $attemptIndex 0-based attempt index
306320
* @param bool $isThrottling Whether the error is a throttling error
307321
* @param mixed $value The result/exception to check for retry-after header
308322
*
309323
* @return int Delay in milliseconds
310324
*/
311-
private function computeRetryDelay(int $i, bool $isThrottling, mixed $value): int
325+
private function computeRetryDelay(int $attemptIndex, bool $isThrottling, mixed $value): int
312326
{
313-
$base = $isThrottling ? self::THROTTLING_BASE_DELAY : $this->baseDelay;
327+
$baseMs = $isThrottling
328+
? self::THROTTLING_BASE_DELAY_MS
329+
: $this->baseDelayMs;
314330

315331
if ($this->exponentialBase !== null) {
316332
$jitter = $this->exponentialBase;
@@ -323,27 +339,33 @@ private function computeRetryDelay(int $i, bool $isThrottling, mixed $value): in
323339
}
324340
}
325341

326-
$maxBackoffSeconds = $this->maxBackoff / 1000.0;
327-
$delaySeconds = $jitter * min($base * pow(2, $i), $maxBackoffSeconds);
328-
$delayMs = (int) round($delaySeconds * 1000);
342+
$delayMs = $jitter * min(
343+
$baseMs * pow(2, $attemptIndex),
344+
$this->maxBackoffMs
345+
);
329346

330347
// Check x-amz-retry-after header
348+
$retryAfterHeader = null;
331349
if ($value instanceof AwsException) {
332350
$response = $value->getResponse();
333-
if ($response?->hasHeader('x-amz-retry-after')) {
334-
$retryAfterStr = $response->getHeaderLine('x-amz-retry-after');
335-
if (is_numeric($retryAfterStr)) {
336-
$retryAfterMs = (int) $retryAfterStr;
337-
// Clamp to [delayMs, 5000 + delayMs]
338-
$retryAfterMs = max($retryAfterMs, $delayMs);
339-
$retryAfterMs = min($retryAfterMs, 5000 + $delayMs);
340-
$delayMs = $retryAfterMs;
341-
}
342-
// Invalid values fall through to computed delay
343-
}
351+
$retryAfterHeader = $response?->getHeaderLine(
352+
self::RETRY_AFTER_HEADER
353+
) ?? null;
354+
} elseif($value instanceof ResultInterface) {
355+
$retryAfterHeader = $value['@metadata']['headers'][
356+
self::RETRY_AFTER_HEADER
357+
] ?? null;
358+
}
359+
360+
if ($retryAfterHeader && preg_match("/^\d+$/", $retryAfterHeader,$matches)) {
361+
$retryAfterMs = (int) $matches[0];
362+
// Clamp to [delayMs, 5000 + delayMs]
363+
$retryAfterMs = max($retryAfterMs, $delayMs);
364+
$retryAfterMs = min($retryAfterMs, 5000 + $delayMs);
365+
$delayMs = $retryAfterMs;
344366
}
345367

346-
return $delayMs;
368+
return (int) $delayMs;
347369
}
348370

349371
private static function isRetryable(

src/Sts/StsClient.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
use Aws\AwsClient;
66
use Aws\CacheInterface;
77
use Aws\Credentials\Credentials;
8+
use Aws\HandlerList;
89
use Aws\Result;
10+
use Aws\RetryMiddlewareV2;
911
use Aws\Sts\RegionalEndpoints\ConfigurationProvider;
1012

1113
/**
@@ -68,6 +70,36 @@ public function __construct(array $args)
6870
parent::__construct($args);
6971
}
7072

73+
public static function getArguments(): array
74+
{
75+
$args = parent::getArguments();
76+
$args['retries']['fn'] = [__CLASS__, '_applyRetryConfig'];
77+
78+
return $args;
79+
}
80+
81+
public static function _applyRetryConfig(
82+
mixed $value,
83+
array &$args,
84+
HandlerList $list
85+
): void
86+
{
87+
if ($value) {
88+
$config = \Aws\Retry\ConfigurationProvider::unwrap($value);
89+
if ($config->getMode() !== 'legacy') {
90+
$list->appendSign(
91+
RetryMiddlewareV2::wrap(
92+
$config,
93+
[
94+
'transient_error_codes' => ['IDPCommunicationError'],
95+
]
96+
),
97+
'retry'
98+
);
99+
}
100+
}
101+
}
102+
71103
/**
72104
* Creates credentials from the result of an STS operations
73105
*

tests/DynamoDb/DynamoDbClientTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,9 @@ public function testValidatesAndRetriesCrc32()
164164
];
165165

166166
$handler = function ($request, $options) use (&$queue) {
167-
// Test the custom retry policy.
167+
// On retry, verify delay is within DynamoDB's 25ms base backoff range
168168
if (count($queue) == 1) {
169-
$this->assertSame(0, $options['delay']);
169+
$this->assertLessThanOrEqual(25, $options['delay']);
170170
}
171171

172172
return \GuzzleHttp\Promise\Create::promiseFor(array_shift($queue));

0 commit comments

Comments
 (0)