Skip to content

Commit ef0789b

Browse files
feat: Add custom retries in gax (#489)
Co-authored-by: Brent Shaffer <[email protected]>
1 parent 98e1861 commit ef0789b

File tree

4 files changed

+391
-23
lines changed

4 files changed

+391
-23
lines changed

src/Middleware/RetryMiddleware.php

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,22 @@ class RetryMiddleware
5151
/** @var float|null */
5252
private $deadlineMs;
5353

54+
/*
55+
* The number of retries that have already been attempted.
56+
* The original API call will have $retryAttempts set to 0.
57+
*/
58+
private int $retryAttempts;
59+
5460
public function __construct(
5561
callable $nextHandler,
5662
RetrySettings $retrySettings,
57-
$deadlineMs = null
63+
$deadlineMs = null,
64+
$retryAttempts = 0
5865
) {
5966
$this->nextHandler = $nextHandler;
6067
$this->retrySettings = $retrySettings;
6168
$this->deadlineMs = $deadlineMs;
69+
$this->retryAttempts = $retryAttempts;
6270
}
6371

6472
/**
@@ -86,14 +94,23 @@ public function __invoke(Call $call, array $options)
8694
}
8795

8896
return $nextHandler($call, $options)->then(null, function ($e) use ($call, $options) {
89-
if (!$e instanceof ApiException) {
97+
$retryFunction = $this->getRetryFunction();
98+
99+
// If the number of retries has surpassed the max allowed retries
100+
// then throw the exception as we normally would.
101+
// If the maxRetries is set to 0, then we don't check this condition.
102+
if (0 !== $this->retrySettings->getMaxRetries()
103+
&& $this->retryAttempts >= $this->retrySettings->getMaxRetries()
104+
) {
90105
throw $e;
91106
}
92-
93-
if (!in_array($e->getStatus(), $this->retrySettings->getRetryableCodes())) {
107+
// If the retry function returns false then throw the
108+
// exception as we normally would.
109+
if (!$retryFunction($e, $options)) {
94110
throw $e;
95111
}
96112

113+
// Retry function returned true, so we attempt another retry
97114
return $this->retry($call, $options, $e->getStatus());
98115
});
99116
}
@@ -139,7 +156,8 @@ private function retry(Call $call, array $options, string $status)
139156
$this->retrySettings->with([
140157
'initialRetryDelayMillis' => $delayMs,
141158
]),
142-
$deadlineMs
159+
$deadlineMs,
160+
$this->retryAttempts + 1
143161
);
144162

145163
// Set the timeout for the call
@@ -155,4 +173,26 @@ protected function getCurrentTimeMs()
155173
{
156174
return microtime(true) * 1000.0;
157175
}
176+
177+
/**
178+
* This is the default retry behaviour.
179+
*/
180+
private function getRetryFunction()
181+
{
182+
return $this->retrySettings->getRetryFunction() ??
183+
function (\Exception $e, array $options): bool {
184+
// This is the default retry behaviour, i.e. we don't retry an ApiException
185+
// and for other exception types, we only retry when the error code is in
186+
// the list of retryable error codes.
187+
if (!$e instanceof ApiException) {
188+
return false;
189+
}
190+
191+
if (!in_array($e->getStatus(), $this->retrySettings->getRetryableCodes())) {
192+
return false;
193+
}
194+
195+
return true;
196+
};
197+
}
158198
}

src/RetrySettings.php

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
*/
3232
namespace Google\ApiCore;
3333

34+
use Closure;
35+
3436
/**
3537
* The RetrySettings class is used to configure retrying and timeouts for RPCs.
3638
* This class can be passed as an optional parameter to RPC methods, or as part
@@ -203,6 +205,8 @@ class RetrySettings
203205
{
204206
use ValidationTrait;
205207

208+
const DEFAULT_MAX_RETRIES = 0;
209+
206210
private $retriesEnabled;
207211

208212
private $retryableCodes;
@@ -217,6 +221,20 @@ class RetrySettings
217221

218222
private $noRetriesRpcTimeoutMillis;
219223

224+
/**
225+
* The number of maximum retries an operation can do.
226+
* This doesn't include the original API call.
227+
* Setting this to 0 means no limit.
228+
*/
229+
private int $maxRetries;
230+
231+
/**
232+
* When set, this function will be used to evaluate if the retry should
233+
* take place or not. The callable will have the following signature:
234+
* function (Exception $e, array $options): bool
235+
*/
236+
private ?Closure $retryFunction;
237+
220238
/**
221239
* Constructs an instance.
222240
*
@@ -225,22 +243,28 @@ class RetrySettings
225243
* $retriesEnabled and $noRetriesRpcTimeoutMillis, which are optional and have defaults
226244
* determined based on the other settings provided.
227245
*
228-
* @type bool $retriesEnabled Optional. Enables retries. If not specified, the value is
229-
* determined using the $retryableCodes setting. If $retryableCodes is empty,
230-
* then $retriesEnabled is set to false; otherwise, it is set to true.
231-
* @type int $noRetriesRpcTimeoutMillis Optional. The timeout of the rpc call to be used
232-
* if $retriesEnabled is false, in milliseconds. It not specified, the value
233-
* of $initialRpcTimeoutMillis is used.
234-
* @type array $retryableCodes The Status codes that are retryable. Each status should be
235-
* either one of the string constants defined on {@see \Google\ApiCore\ApiStatus}
236-
* or an integer constant defined on {@see \Google\Rpc\Code}.
237-
* @type int $initialRetryDelayMillis The initial delay of retry in milliseconds.
238-
* @type int $retryDelayMultiplier The exponential multiplier of retry delay.
239-
* @type int $maxRetryDelayMillis The max delay of retry in milliseconds.
240-
* @type int $initialRpcTimeoutMillis The initial timeout of rpc call in milliseconds.
241-
* @type int $rpcTimeoutMultiplier The exponential multiplier of rpc timeout.
242-
* @type int $maxRpcTimeoutMillis The max timeout of rpc call in milliseconds.
243-
* @type int $totalTimeoutMillis The max accumulative timeout in total.
246+
* @type bool $retriesEnabled Optional. Enables retries. If not specified, the value is
247+
* determined using the $retryableCodes setting. If $retryableCodes is empty,
248+
* then $retriesEnabled is set to false; otherwise, it is set to true.
249+
* @type int $noRetriesRpcTimeoutMillis Optional. The timeout of the rpc call to be used
250+
* if $retriesEnabled is false, in milliseconds. It not specified, the value
251+
* of $initialRpcTimeoutMillis is used.
252+
* @type array $retryableCodes The Status codes that are retryable. Each status should be
253+
* either one of the string constants defined on {@see \Google\ApiCore\ApiStatus}
254+
* or an integer constant defined on {@see \Google\Rpc\Code}.
255+
* @type int $initialRetryDelayMillis The initial delay of retry in milliseconds.
256+
* @type int $retryDelayMultiplier The exponential multiplier of retry delay.
257+
* @type int $maxRetryDelayMillis The max delay of retry in milliseconds.
258+
* @type int $initialRpcTimeoutMillis The initial timeout of rpc call in milliseconds.
259+
* @type int $rpcTimeoutMultiplier The exponential multiplier of rpc timeout.
260+
* @type int $maxRpcTimeoutMillis The max timeout of rpc call in milliseconds.
261+
* @type int $totalTimeoutMillis The max accumulative timeout in total.
262+
* @type int $maxRetries The max retries allowed for an operation.
263+
* Defaults to the value of the DEFAULT_MAX_RETRIES constant.
264+
* This option is experimental.
265+
* @type callable $retryFunction This function will be used to decide if we should retry or not.
266+
* Callable signature: `function (Exception $e, array $options): bool`
267+
* This option is experimental.
244268
* }
245269
*/
246270
public function __construct(array $settings)
@@ -269,6 +293,8 @@ public function __construct(array $settings)
269293
$this->noRetriesRpcTimeoutMillis = array_key_exists('noRetriesRpcTimeoutMillis', $settings)
270294
? $settings['noRetriesRpcTimeoutMillis']
271295
: $this->initialRpcTimeoutMillis;
296+
$this->maxRetries = $settings['maxRetries'] ?? self::DEFAULT_MAX_RETRIES;
297+
$this->retryFunction = $settings['retryFunction'] ?? null;
272298
}
273299

274300
/**
@@ -348,7 +374,9 @@ public static function constructDefault()
348374
'rpcTimeoutMultiplier' => 1,
349375
'maxRpcTimeoutMillis' => 20000,
350376
'totalTimeoutMillis' => 600000,
351-
'retryableCodes' => []]);
377+
'retryableCodes' => [],
378+
'maxRetries' => self::DEFAULT_MAX_RETRIES,
379+
'retryFunction' => null]);
352380
}
353381

354382
/**
@@ -375,6 +403,8 @@ public function with(array $settings)
375403
'retryableCodes' => $this->getRetryableCodes(),
376404
'retriesEnabled' => $this->retriesEnabled(),
377405
'noRetriesRpcTimeoutMillis' => $this->getNoRetriesRpcTimeoutMillis(),
406+
'maxRetries' => $this->getMaxRetries(),
407+
'retryFunction' => $this->getRetryFunction(),
378408
];
379409
return new RetrySettings($settings + $existingSettings);
380410
}
@@ -489,6 +519,22 @@ public function getTotalTimeoutMillis()
489519
return $this->totalTimeoutMillis;
490520
}
491521

522+
/**
523+
* @experimental
524+
*/
525+
public function getMaxRetries()
526+
{
527+
return $this->maxRetries;
528+
}
529+
530+
/**
531+
* @experimental
532+
*/
533+
public function getRetryFunction()
534+
{
535+
return $this->retryFunction;
536+
}
537+
492538
private static function convertArrayFromSnakeCase(array $settings)
493539
{
494540
$camelCaseSettings = [];

0 commit comments

Comments
 (0)