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?
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.
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).
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 .
The CompletableFuture. get() method is blocking. It waits until the Future is completed and returns the result after its completion.
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; }
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With