@@ -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 (
0 commit comments