Friday, February 26, 2021

Retry In The Future

Oracle Java Tutorial and Material, Oracle Java Preparation, Core Java, Oracle Java Learning, Oracle Java Career

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 something

◉ thenCompose – which takes a function which produces a CompletionStage out of the result of an existing CompletableFuture and sort of flatMaps it into a CompletableFuture

◉ exceptionally – which allows a completable future, which is presently in error state, to render itself as a different value

◉ supplyAsync – 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


Oracle Java Tutorial and Material, Oracle Java Preparation, Core Java, Oracle Java Learning, Oracle Java Career
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?

Related Posts

0 comments:

Post a Comment