-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add Retrier class #561
Conversation
fd4c45b
to
e30f44e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cannot track down anywhere the delay
config settings are used to delay the retry function. This is something we can look into adding. I'd call it a bug that it's missing, but since it hasn't been implemented yet it's more like a feature.
@@ -56,6 +56,26 @@ public static function validateNotNull(array $arr, array $requiredKeys) | |||
return self::validateImpl($arr, $requiredKeys, false); | |||
} | |||
|
|||
/** | |||
* @param array $arr Associative array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A description of what this function does will be helpful in the docbloc.
$this->initialRetryDelayMillis = $settings['initialRetryDelayMillis']; | ||
$this->retryDelayMultiplier = $settings['retryDelayMultiplier']; | ||
]; | ||
$this->validateAllKeysFromOneOf( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we mean self:: validateAllKeysFromOneOf
. I see it's a static function.
@@ -405,6 +461,9 @@ public function with(array $settings) | |||
'noRetriesRpcTimeoutMillis' => $this->getNoRetriesRpcTimeoutMillis(), | |||
'maxRetries' => $this->getMaxRetries(), | |||
'retryFunction' => $this->getRetryFunction(), | |||
'retryDelayFunction' => $this->getRetryDelayFunction(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we had the general agreement that we want to remove the delayFunction and only give option to add floats?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the delay needs to be retrieved from the server exception? Thats a usecase for the Spanner
* @return mixed | ||
* @throws \Exception | ||
*/ | ||
public function execute(callable $call, array $options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is certainly something new, but do we want to add the ability to change the $call
or $options
after a failed attempt.
We had this ability in Core where we allowed changing of headers using retryListeners
.
Although we don't need this functionality right now, but I just wanted to initiate a discussion on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I did not add this since it was not a requirement right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like the design here is slightly off - there's still too much logic happening in RetryMiddleware
- it should be as thin as possible. Also, having the $retryAttempts
as part of RetrySettings
is not what we want (think of RetrySettings
as an object that should be able to be passed around to different RPC calls without unexpected behavior).
* The number of retries that have already been attempted. | ||
* The original API call will have $retryAttempts set to 0. | ||
*/ | ||
private int $retryAttempts; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This object represents the settings for retrying, e.g. HOW the retrying will be performed.
$retryAttempts
is related to the actual retrying taking place. This should be handled by the Retrier, and NOT the RetrySettings.
* | ||
* @return PromiseInterface | ||
* @throws ApiException | ||
*/ | ||
private function retry(Call $call, array $options, string $status) | ||
private function retry(Call $call, array $options, ApiException $exception) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this entire function live in the Retrier
? That would also remove the need for so many public methods in the Retrier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bshaffer This function uses GuzzleHttp\Promise\PromiseInterface
. In our initial discussion, we wanted the Retrier
to be as generic as possible. Thats why I did not move this functionality to the Retrier
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guzzle's PromiseInterface
is pretty generic (see here). Although the package has "http" in the name (guzzlehttp/promises
), the interface has nothing to do with HTTP requests. I think it makes more sense to continue using the PromiseInterface
as the return type for the Retrier
.
If you don't like this, the other option is to do what we did for ExponentialBackoff
, and wrap the executable in a try/catch
inside the Retrier
.
I prefer the PromiseInterface
approach because it's already implemented, and we won't have to refactor it and run the risk of unexpected behavior. However, what we have here is the Retrier
and RetrierMiddleware
calling each other in a somewhat convoluted circle, which makes it hard for me to follow what each one's purpose is.
* @return void | ||
* @throws ApiException | ||
*/ | ||
public function checkDeadlineExceeded(float $currentTimeMillis) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like something that the retrier should check internally, and shouldn't need to be a public method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This made public to use it by the RetryMiddleware
* | ||
* @return float | ||
*/ | ||
public function calculateRetryDeadlineMillis(float $currentTimeMillis) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like something the retrier should be able to calculate internally, and that it shouldn't need to be a public method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This made public to use it by the RetryMiddleware
{ | ||
if ($this->retryDelayFunction) { | ||
return min( | ||
call_user_func($this->retryDelayFunction, $this->retryAttempts, $exception), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we passing in the $exception
in order to determine the length of the retry delay?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sometimes the exception contains the delay. See here: https://github.com/googleapis/google-cloud-php/blob/076287b03e801bfaff4f43809de8c4bfc7c9e5d6/Spanner/src/Database.php#L916
* @param int $deadlineMs TODO: Deprecate if possible. | ||
* @param int $retryAttempts TODO: Deprecate if possible. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If these need to be deprecated, we should deprecate them as part of this PR.
@@ -264,37 +281,72 @@ class RetrySettings | |||
* This option is experimental. | |||
* @type callable $retryFunction This function will be used to decide if we should retry or not. | |||
* Callable signature: `function (Exception $e, array $options): bool` | |||
* This option is experimental. | |||
* @type callable $retryDelayFunction Optional. This function will be used to decide the delay between retries. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: line lengths
* @type callable $retryDelayFunction Optional. This function will be used to decide the delay between retries. | |
* @type callable $retryDelayFunction Optional. This function will be used to decide the | |
* delay between retries. |
[ | ||
'retryDelayFunction', | ||
] | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a user supplies settings which are invalid, we should throw an exception, instead of having hidden logic which they have to read the code to understand.
if (!emtpy($settings['initialRetryDelayMillis']) && !emtpy($settings['retryDelayFunction'])) {
throw new InvalidArgumentException('Cannot supply both "retryableCodes" and "retryFunction" to RetrySettings');
}
* @return array Returns $arr for fluent use | ||
* @throws ValidationException | ||
*/ | ||
public static function validateAllKeysFromOneOf(array $arr, array $requiredKeys1, array $requiredKeys2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A function like this is a bit over engineered - The logic should be clearly defined and readable. The retrySettings
object should remain simple - we provide it with settings and it tells us what those settings are.
* | ||
* @return PromiseInterface | ||
* @throws ApiException | ||
*/ | ||
private function retry(Call $call, array $options, string $status) | ||
private function retry(Call $call, array $options, ApiException $exception) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guzzle's PromiseInterface
is pretty generic (see here). Although the package has "http" in the name (guzzlehttp/promises
), the interface has nothing to do with HTTP requests. I think it makes more sense to continue using the PromiseInterface
as the return type for the Retrier
.
If you don't like this, the other option is to do what we did for ExponentialBackoff
, and wrap the executable in a try/catch
inside the Retrier
.
I prefer the PromiseInterface
approach because it's already implemented, and we won't have to refactor it and run the risk of unexpected behavior. However, what we have here is the Retrier
and RetrierMiddleware
calling each other in a somewhat convoluted circle, which makes it hard for me to follow what each one's purpose is.
/** | ||
* The deadline in milliseconds. | ||
*/ | ||
private ?float $deadlineMillis; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised this is a float, since milliseconds are an integer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have followed this from RetryMiddleware here:
private ?float $deadlineMs; |
if (!$retrySettings->retriesEnabled() && $retrySettings->getNoRetriesRpcTimeoutMillis() > 0) { | ||
$options['timeoutMillis'] = $retrySettings->getNoRetriesRpcTimeoutMillis(); | ||
} elseif ($retrySettings->getInitialRpcTimeoutMillis() > 0) { | ||
$options['timeoutMillis'] = $retrySettings->getInitialRpcTimeoutMillis(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After discussing with @saranshdhingra, I think having a parameterUpdateFunction
as part of either Retrier
or or RetrySettings
is the way to handle this - we need to update this on every call, but only for calls made through RetryMiddleware
, so it should be generic.
I have a WIP here: 6a94036
We have decided not to go with |
No description provided.