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

Add short-circuit support in retry pattern #32035

Merged
merged 2 commits into from
Aug 15, 2023

Conversation

leviramsey
Copy link
Contributor

@leviramsey leviramsey commented Aug 13, 2023

There are situations where the nature of failure of a future indicates that any retry (regardless of delay) is extremely likely to fail in the same way. In such a situation, the retry is unlikely to be of value and it may be better to surface the failure more quickly.

Consider, for instance, an API which validates inputs asynchronously (e.g. imagine that the validations to perform are dynamically obtained from a server, but the validations themselves only change on a fairly long timeframe). It is unlikely that the validations change from one retry to another, so retrying the validations is unlikely to change the result. However, we'd like the API calls to be retried if they fail for some other reason that's more amenable to retries.

This change adds support for classifying failed futures as "should retry" or "should not retry" based on the nature of the failure (in HTTP terms, think of the difference between 4xx and 5xx, ignoring that Akka HTTP will give successful futures for 4xx/5xx responses).

An alternative approach in the current API is to have a recoverWith as part of the attempt which transforms the "should not retry" failures into distinguished successes so that no more attempts are performed, and a flatMap outside of the retry transforms those distinguished successes back to a failure:

// implementation of the new API in this PR in terms of the old
def retry[T](
    attempt: () => Future[T],
    attempts: Int,
    delayFunction: Int => Option[FiniteDuration],
    shouldRetry: Throwable => Boolean)(implicit ec: ExecutionContext, scheduler: Scheduler): Future[T] =
  retry(
    () => {
      val baseFuture: Future[Try[T]] = attempt().map(Success(_))
      baseFuture.recoverWith {
        case ex if !(shouldRetry(ex)) => Future.successful(Failure(ex))
        case _ => baseFuture
      }
    },
    attempts,
    delayFunction).flatMap(Future.fromTry(_))

Copy link
Member

@johanandren johanandren left a comment

Choose a reason for hiding this comment

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

Seems useful

* shouldRetry = { (ex) => ex.isInstanceOf[IllegalArgumentException] })
* }}}
*/
def retry[T](attempt: () => Future[T], shouldRetry: Throwable => Boolean, attempts: Int)(
Copy link
Member

Choose a reason for hiding this comment

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

Align order of parameters with the other signature with more params, either attempts in between in both or lambdas first in both? (Unless that causes compiler ambiguity, but I don't think it should)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

attempts in-between does cause compiler confusion (with the delayFunction variants), so would break source compatibility, but shouldRetry after attempt does work.


private val alwaysRetry: Throwable => Boolean = { _ =>
true
}
Copy link
Member

Choose a reason for hiding this comment

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

Use ConstantFun.anyToTrue instead

Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

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

looking good

Copy link
Member

@patriknw patriknw left a comment

Choose a reason for hiding this comment

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

LGTM

@He-Pin
Copy link
Member

He-Pin commented Aug 15, 2023

I have something like this too,eg:

    @SneakyThrows
    public static <V> V retryWithBackoff(
        final Callable<V> callable,
        final Predicate<V> shouldRetryPredicate,
        final int maxRetryTime,
        final int sleepInMills,
        final String hint) {
        return retryWithBackoff0(callable, shouldRetryPredicate, maxRetryTime, 0, sleepInMills, hint);
    }

So I think the shouldRetry can be Try[T] => Boolean in Scala and (Throwable, T) => Boolean in Java.

@johanandren johanandren merged commit 101bb71 into akka:main Aug 15, 2023
6 checks passed
@johanandren johanandren added this to the 2.8.4 milestone Aug 15, 2023
@leviramsey leviramsey deleted the retry-classify branch August 30, 2023 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants