Skip to content
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

Closed
wants to merge 7 commits into from
Closed

feat: Add Retrier class #561

wants to merge 7 commits into from

Conversation

ajupazhamayil
Copy link
Contributor

No description provided.

Copy link
Contributor

@bshaffer bshaffer left a 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.

src/Middleware/RetryMiddleware.php Outdated Show resolved Hide resolved
@@ -56,6 +56,26 @@ public static function validateNotNull(array $arr, array $requiredKeys)
return self::validateImpl($arr, $requiredKeys, false);
}

/**
* @param array $arr Associative array
Copy link
Contributor

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(
Copy link
Contributor

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(),
Copy link
Contributor

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?

Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@bshaffer bshaffer left a 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;
Copy link
Contributor

@bshaffer bshaffer May 7, 2024

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)
Copy link
Contributor

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.

Copy link
Contributor Author

@ajupazhamayil ajupazhamayil May 9, 2024

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.

Copy link
Contributor

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)
Copy link
Contributor

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

Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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),
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +53 to +54
* @param int $deadlineMs TODO: Deprecate if possible.
* @param int $retryAttempts TODO: Deprecate if possible.
Copy link
Contributor

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: line lengths

Suggested change
* @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',
]
];
Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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();
Copy link
Contributor

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

@ajupazhamayil
Copy link
Contributor Author

We have decided not to go with gax Retrier logic, Instead we will continue to use the existing core's Retry logic. Hence closing this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants