Retry In The Future
Writing asynchronous code in Javascript is relatively easy.
// async function let attempt = 1; while (true) { try { const result = await operationThatMayFail(); // it didn't fail return result; } catch (error) { if (attempt >= maxAttempts || error !== 'Retryable') { // either unhandleable, or no further attempts throw error; } } attempt++; await sleep(pauseTime); }
This infinite loop runs until the operation succeeds, or it throws an error that we don’t like (not 'Retryable'
) or we run out of attempts. In between attempts it sleeps before retrying.
This apparently sequential code is made out of the async
/await
pattern and is easy to reason about, though the first await
statement might look like it could be replaced immediately returning, which it can’t…
The Promise
API in Javascript is very handy/powerful, but the flattening of it into what looks like blocking code is even better!
So How Do We Do This In Java?
Trigger warning – you don’t want to know the answer to this!!!
I’ll answer this in Java 11, though there’s an optimisation to be made with later versions.
I’ve produced an example library and its unit tests for you to play with, so go and have a look. This is terrifying code. The most weird thing about this code is that this isn’t the first time I’ve implemented one of these, though this implementation was written tonight from scratch.
The first thing we need to know is that Java 8 and onwards provides a CompletableFuture
which is very similar in intent to the Javascript Promise
. A CompletableFuture
says it WILL have an answer in the future, and there are various options for composing further transformations and behaviour upon it.
Our goal in this exercise is to write something which will allow us to execute a function that completes in the future a few times, until it succeeds. As each attempt needs to call the function again, let’s characterise attempts via an attempter
as Supplier<CompletableFuture<T>>
. In other words, something that can supply a promise to be doing the work in the future can be used to get our first attempt and can be used in retries to perform subsequent attempts. Easy!
The function we want to write, therefore, should take a thing which it can call do to the attempts, and will return a CompletableFuture
with the result, but somehow hide the fact that it’s baked some retries into the process.
Here’s a signature of the function we want:
/** * Compose a {@link CompletableFuture} using the <code>attempter</code> * to create the first * attempt and any retries permitted by the <code>shouldRetry</code> * predicate. All retries wait * for the <code>waitBetween</code> before going again, up to a * maximum number of attempts * @param attempter produce an attempt as a {@link CompletableFuture} * @param shouldRetry determines whether a {@link Throwable} is retryable * @param attempts the number of attempts to make before allowing failure * @param waitBetween the duration of waiting between attempts * @param <T> the type of value the future will return * @return a composite {@link CompletableFuture} that runs until success or total failure */ public static <T> CompletableFuture<T> withRetries( Supplier<CompletableFuture<T>> attempter, Predicate<Throwable> shouldRetry, int attempts, Duration waitBetween) { ... }
The above looks good… if you have a function that returns a CompletableFuture
already, it’s easy to harness this to repeatedly call it, and if you don’t, then you can easily use some local thread pool (or even the fork/join pool) to repeatedly schedule something to happen in the background and become a CompletableFuture
. Indeed, CompletableFuture.supplyAsync
will construct such an operation for you.
So how to do retries…
Retry Options
Java 11 doesn’t have the function we need (later Java versions do). It has the following methods of use to us on a CompletableFuture
:
thenApply
– which converts the eventual result of a future into somethingthenCompose
– which takes a function which produces aCompletionStage
out of the result of an existingCompletableFuture
and sort offlatMap
s it into aCompletableFuture
exceptionally
– which allows a completable future, which is presently in error state, to render itself as a different valuesupplyAsync
– allows a completable future to be created from a threadpool/Executor
to do something eventually
What we want to do is somehow tell a completable future –
completableFuture.ifErrorThenRetry(() -> likeThis())
And we can’t… and even if we could, we’d rather it did it asynchronously after waiting without blocking any threads!
Can We Cook With This?
We have all the ingredients and we can cook them together… but it’s a bit clunky.
We can make a scheduler that will do our retry later without blocking:
// here's an `Executor` that can do scheduling private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1); // which we can convert into an `Executor` functional interface // by simply creating a lambda that uses our `waitBetween` Duration // to do things later: Executor scheduler = runnable -> SCHEDULER.schedule(runnable, waitBetween.toMillis(), TimeUnit.MILLISECONDS);
So we have non-blocking waiting. A future that wants to have another go, can somehow schedule itself and become a new future which tries later… somehow.
We need the ability to flatten a future which may need to replace its return value with a future of a future:
private static <T> CompletableFuture<T> flatten( CompletableFuture<CompletableFuture<T>> completableCompletable) { return completableCompletable.thenCompose(Function.identity()); }
Squint and forget about it… it does the job.
Adding The First Try
Doing the first attempt is easy:
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. This means that using firstAttempt.exceptionally
needs the whole thing to become a future of a future..!!!
return flatten( firstAttempt.thenApply(CompletableFuture::completedFuture) .exceptionally(throwable -> retry(attempter, 1, throwable, shouldRetry, attempts, scheduler)));
We have to escalate the first attempt to become a future of a future on success (with thenApply)
so we can then use an alternate path with exceptionally
to produce a different future of a future on failure (with attempt 1)… and then we use the flatten
function to make it back into an easily consumer CompletableFuture
.
If this looks like voodoo then two points:
- it works
- you ain’t seen nothing yet!!!
Retrying in the Future of the Future of the Future
Great Scott Marty, this one’s tricky. We can have some easy guard logic in the start of our retry function:
int nextAttempt = attemptsSoFar + 1; if (nextAttempt > maxAttempts || !shouldRetry.test(throwable.getCause())) { return CompletableFuture.failedFuture(throwable); }
This does the equivalent of the catch block of our original Javascript. It checks the number of attempts, decides if the predicate likes the error or not… and fails the future if it really doesn’t like what it finds.
Then we have to somehow have another attempt and add the retry logic onto the back of it. As we have a supplier of a CompletableFuture
we need to use that with CompletableFuture.supplyAsync
. We can’t call get
on it, because we want it to happen in the future, according to the waiting time of the delaying Executor
we used to give us a gap between attempts.
So we have to use flatten(CompletableFuture.supplyAsync(attempter, scheduler))
to put the operation into the future and then make it back into a CompletableFuture
for onward use… and then… for reasons that are hard to fathom, we need to repeated the whole thenApply
and exceptionally
pattern and flatten the result again.
This is because we first need a future that will happen later, in a form where we can add stuff to it, and we can’t add stuff to it until… I mean, I understand it, but it’s just awkward:
return flatten(flatten(CompletableFuture.supplyAsync(attempter, scheduler)) .thenApply(CompletableFuture::completedFuture) .exceptionally(nextThrowable -> retry(attempter, nextAttempt, nextThrowable, shouldRetry, maxAttempts, scheduler)));
Well, if flattening’s so good, we may as well do it lots, eh?
Summary
So, there’s a working prototype over at the GitHub repo. I suspect there’s something funny about the exceptions getting wrapped up in other exceptions, which may be worth double checking… but it’s passing the tests, and is similar to a working example I have also made which does asynchronous retries really well.
This is very much write only code. Without the tests, I’d have no confidence in it.
You’ll be pleased to hear it was written test first, but it was also then hacked until they eventually passed!
By the way, if this article didn’t make sense, then perhaps give it 500milliseconds, then read it again… up to maximum of attempts.
Published on Java Code Geeks with permission by Ashley Frieze, partner at our JCG program. See the original article here: Retry In The Future Opinions expressed by Java Code Geeks contributors are their own. |