Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retry logic with CompletableFuture

I need to submit a task in an async framework I'm working on, but I need to catch for exceptions, and retry the same task multiple times before "aborting".

The code I'm working with is:

int retries = 0; public CompletableFuture<Result> executeActionAsync() {      // Execute the action async and get the future     CompletableFuture<Result> f = executeMycustomActionHere();      // If the future completes with exception:     f.exceptionally(ex -> {         retries++; // Increment the retry count         if (retries < MAX_RETRIES)             return executeActionAsync();  // <--- Submit one more time          // Abort with a null value         return null;     });      // Return the future         return f; } 

This currently doesn't compile because the return type of the lambda is wrong: it expects a Result, but the executeActionAsync returns a CompletableFuture<Result>.

How can I implement this fully async retry logic?

like image 895
xmas79 Avatar asked Nov 08 '16 11:11

xmas79


People also ask

How do you retry Completable?

CompletableFuture<T> firstAttempt = attempter. get(); All we have to do now is attach the retrying to it. The retry will, itself, return a CompletableFuture so it can retry in future.

How do you write retry logic in Java?

1. Simple for-loop with try-catch. A simple solution to implement retry logic in Java is to write your code inside a for loop that executes the specified number of times (the maximum retry value).

What is completedStage () method in CompletableFuture interface?

completedFuture​(U value) Returns a new CompletableFuture that is already completed with the given value. static <U> CompletionStage<U> completedStage​(U value) Returns a new CompletionStage that is already completed with the given value and supports only those methods in interface CompletionStage .

Is CompletableFuture get blocking?

The CompletableFuture. get() method is blocking. It waits until the Future is completed and returns the result after its completion.


2 Answers

Chaining subsequent retries can be straight-forward:

public CompletableFuture<Result> executeActionAsync() {     CompletableFuture<Result> f=executeMycustomActionHere();     for(int i=0; i<MAX_RETRIES; i++) {         f=f.exceptionally(t -> executeMycustomActionHere().join());     }     return f; } 

Read about the drawbacks below
This simply chains as many retries as intended, as these subsequent stages won’t do anything in the non-exceptional case.

One drawback is that if the first attempt fails immediately, so that f is already completed exceptionally when the first exceptionally handler is chained, the action will be invoked by the calling thread, removing the asynchronous nature of the request entirely. And generally, join() may block a thread (the default executor will start a new compensation thread then, but still, it’s discouraged). Unfortunately, there is neither, an exceptionallyAsync or an exceptionallyCompose method.

A solution not invoking join() would be

public CompletableFuture<Result> executeActionAsync() {     CompletableFuture<Result> f=executeMycustomActionHere();     for(int i=0; i<MAX_RETRIES; i++) {         f=f.thenApply(CompletableFuture::completedFuture)            .exceptionally(t -> executeMycustomActionHere())            .thenCompose(Function.identity());     }     return f; } 

demonstrating how involved combining “compose” and an “exceptionally” handler is.

Further, only the last exception will be reported, if all retries failed. A better solution should report the first exception, with subsequent exceptions of the retries added as suppressed exceptions. Such a solution can be build by chaining a recursive call, as hinted by Gili’s answer, however, in order to use this idea for exception handling, we have to use the steps to combine “compose” and “exceptionally” shown above:

public CompletableFuture<Result> executeActionAsync() {     return executeMycustomActionHere()         .thenApply(CompletableFuture::completedFuture)         .exceptionally(t -> retry(t, 0))         .thenCompose(Function.identity()); } private CompletableFuture<Result> retry(Throwable first, int retry) {     if(retry >= MAX_RETRIES) return CompletableFuture.failedFuture(first);     return executeMycustomActionHere()         .thenApply(CompletableFuture::completedFuture)         .exceptionally(t -> { first.addSuppressed(t); return retry(first, retry+1); })         .thenCompose(Function.identity()); } 

CompletableFuture.failedFuture is a Java 9 method, but it would be trivial to add a Java 8 compatible backport to your code if needed:

public static <T> CompletableFuture<T> failedFuture(Throwable t) {     final CompletableFuture<T> cf = new CompletableFuture<>();     cf.completeExceptionally(t);     return cf; } 
like image 166
Holger Avatar answered Sep 29 '22 05:09

Holger


Instead of implementing your own retry logic, I recommend using a proven library like failsafe, which has built-in support for futures (and seems more popular than guava-retrying). For your example, it would look something like:

private static RetryPolicy retryPolicy = new RetryPolicy()     .withMaxRetries(MAX_RETRIES);  public CompletableFuture<Result> executeActionAsync() {     return Failsafe.with(retryPolicy)         .with(executor)         .withFallback(null)         .future(this::executeMycustomActionHere); } 

Probably you should avoid .withFallback(null) and just have let the returned future's .get() method throw the resulting exception so the caller of your method can handle it specifically, but that's a design decision you'll have to make.

Other things to think about include whether you should retry immediately or wait some period of time between attempts, any sort of recursive backoff (useful when you're calling a web service that might be down), and whether there are specific exceptions that aren't worth retrying (e.g. if the parameters to the method are invalid).

like image 41
theazureshadow Avatar answered Sep 29 '22 07:09

theazureshadow