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);
0 comments:
Post a Comment