Bug description
The RetryMiddleware in google/gax (PHP) calculates the exponential backoff delay correctly via initialRetryDelayMillis, but never actually delays the execution (e.g., via usleep or sleep) before firing the next request. This causes the SDK to immediately fire all retry attempts in rapid succession (within milliseconds), defeating the entire purpose of exponential backoff. This frequently leads to severe Rate Limiting penalties (like Code 8: RESOURCE_EXHAUSTED) or WAF blocks.
Environment
- OS: Linux 7.0.10
- PHP version: 8.3.31
- Package name and version: cloud-ai-platform:v1.13.0
Steps to reproduce
Configure a PredictionServiceClient (or any GAPIC client) with custom RetrySettings.
Mock the network to return a 429 Too Many Requests (or throw an ApiException with Code 8 / RESOURCE_EXHAUSTED).
Set initialRetryDelayMillis to a high value (e.g., 5000 / 5 seconds) and set max retries.
Execute the call.
Notice that all retries fail and the script terminates in < 1 second instead of taking the mathematically expected time.
Root cause analysis
Looking at src/Middleware/RetryMiddleware.php: Around the retry() method:
$delayMs = min($delayMs * $delayMult, $maxDelayMs);
$nextHandler = new RetryMiddleware(
$this->nextHandler,
$this->retrySettings->with([
'initialRetryDelayMillis' => $delayMs,
]),
$deadlineMs,
$this->retryAttempts + 1
);
// Set the timeout for the call
$options['timeoutMillis'] = $timeoutMs;
return $nextHandler(
$call,
$options
);
The $delayMs is calculated and stored in the new RetryMiddleware instance's RetrySettings, but the actual delay/sleep is never executed anywhere in the middleware chain before $nextHandler($call, $options) invokes the HTTP/gRPC transport layer.
Expected behavior
The SDK should pause execution (e.g., via usleep($delayMs * 1000)) or schedule the promise resolution to respect the initialRetryDelayMillis before dispatching the next request to the server.
Bug description
The RetryMiddleware in google/gax (PHP) calculates the exponential backoff delay correctly via initialRetryDelayMillis, but never actually delays the execution (e.g., via usleep or sleep) before firing the next request. This causes the SDK to immediately fire all retry attempts in rapid succession (within milliseconds), defeating the entire purpose of exponential backoff. This frequently leads to severe Rate Limiting penalties (like Code 8: RESOURCE_EXHAUSTED) or WAF blocks.
Environment
Steps to reproduce
Configure a PredictionServiceClient (or any GAPIC client) with custom RetrySettings.
Mock the network to return a 429 Too Many Requests (or throw an ApiException with Code 8 / RESOURCE_EXHAUSTED).
Set initialRetryDelayMillis to a high value (e.g., 5000 / 5 seconds) and set max retries.
Execute the call.
Notice that all retries fail and the script terminates in < 1 second instead of taking the mathematically expected time.
Root cause analysis
Looking at src/Middleware/RetryMiddleware.php: Around the retry() method:
The $delayMs is calculated and stored in the new RetryMiddleware instance's RetrySettings, but the actual delay/sleep is never executed anywhere in the middleware chain before $nextHandler($call, $options) invokes the HTTP/gRPC transport layer.
Expected behavior
The SDK should pause execution (e.g., via usleep($delayMs * 1000)) or schedule the promise resolution to respect the initialRetryDelayMillis before dispatching the next request to the server.