Asynchronous programming made resilient with MicroProfile Fault Tolerance: Part 2
You can use the @Asynchronous annotation with the CompletionStage interface to write asynchronous code that’s resilient to faults.
In our previous blog post, we introduced what @Asynchronous
does and some basic use cases that return a CompletionStage
.
If you want introductory information to familiarize yourself with @Asynchronous
, go back and check out that post.
Otherwise, you might now be ready to dive deeper into practical details of using @Asynchronous
and CompletionStage
to get more out of MicroProfile Fault Tolerance.
After reading our initial post, you may’ve been left with some questions:
What if I want to use other Fault Tolerance annotations with @Asynchronous
? Why should I return a CompletionStage
instead of a Future
?
In this post, we continue our discussion of building resiliency into asynchoronous programming.
Specifically, we cover the following topics related to using @Asynchronous
:
Interactions with other Fault Tolerance annotations
In our last post, we covered two use cases—one about applying Fault Tolerance to an asynchronous API call and the other about running multiple methods in parallel.
Now, let’s look at how using the @Asynchronous
annotation impacts other Fault Tolerance annotations.
@Timeout annotation
When you use the @Asynchronous
and @Timeout
annotations together, the CompletionStage
or Future
returned to the caller can be completed as soon as the timeout expires, even if the method is still running.
This is because the method is running on another thread, so even though that thread is still occupied, you can signal to another thread that the result is ready.
The thread that’s running the method is interrupted, so it can stop what it’s working on and save resources.
But if you need to apply a timeout to a long-running operation that doesn’t respond to being interrupted, you can use the @Asynchronous
annotation.
You should be aware that the operation may still run to completion, even though the timeout has expired and you received a TimeoutException
.
@Bulkhead annotation
When you use the @Asynchronous
and @Bulkhead
annotations together, Fault Tolerance provides the option to queue up executions if the maximum number of executions are already running.
This is allowed because any calling code was written with the knowledge that the method is asynchronous and won’t return immediately.
If there are less than the maximum concurrent executions running when you call the method, then your method is scheduled to run immediately.
Otherwise, it’s added to a queue.
If there are any requests in the queue when one execution of the method finishes, then the first execution from the queue starts.
If the queue is full, then the method fails with a BulkheadException
.
Just like the number of concurrent executions, the size of the queue can be configured using the waitingTaskQueue
parameter on the @Bulkhead
annotation.
Asynchronous flow of execution
When a method is annotated with @Asynchronous
, a few things change in the flow of execution.
For context, let’s first look at how the Fault Tolerance annotations (@Retry
, @Timeout
, @CircuitBreaker
, @Bulkhead
, and @Fallback
) work together without the presence of @Asynchronous
:
The differences in flow between synchronous and asynchronous execution are highlighted in dark green and discussed after the following diagram:
The first difference is that with asynchoronous execution, a CompletionStage
or Future
is returned before the method runs.
When the method has actually returned, the result from the method is then propagated to the CompletionStage
or Future
so that the caller can get it.
The next difference is found in the bulkhead. In addition to either accepting or rejecting the execution, the bulkhead can also queue it to run later. If the method is accepted by the bulkhead, it’s then scheduled to run on another thread, rather than immediately.
Another difference occurs with the timeout. When a timeout is used with @Asynchronous
, then the method is interrupted if the timeout expires, and the execution skips forward to the point highlighted in the diagram (see the Timeout Expires block).
The result is then processed as if the method finished with a TimeoutException
.
The last difference is that if there’s a fallback, it also runs asynchronously, so it’s scheduled to run on another thread.
Limitations of using Future
While @Asynchronous
can make methods that return a Future
run asynchronously, Fault Tolerance can only be applied to asynchronous methods that return a CompletionStage
(described here).
But why is this?
Future
fundamentally has two ways of getting the result of its method: blocking and waiting with get()
or polling with isDone()
.
To implement Fault Tolerance around an asynchronous result, a callback is required so that you don’t need a second thread that just waits or polls for the result. CompletionStage
facilitates this necessary callback.
Without a callback, Fault Tolerance is applied around the method call, not around the method result.
This means that for a Future
:
-
The timeout ends when the method returns (even if not completed).
-
The bulkhead is released when the method returns (even if not completed).
-
The method call is considered successful as soon as the
Future
is returned, even if the result of theFuture
is an exception.
These are not desired behaviours.
Because of these concerns, using a Future
is only suitable for running operations in parallel.
In these situations, your method usually ends with return CompletableFuture.completedFuture(result);
, meaning there’s no possibility of returning a Future
that completes exceptionally.
Either your method throws an exception, or it returns a successful Future
.
Thanks for reading!
We hope you’ve learned more about how you can use the @Asynchronous
annotation in different scenarios to make your applications more resilient.
If you want to learn more about Fault Tolerance, check out some Open Liberty Fault Tolerance guides.
If you want to get involved in MicroProfile Fault Tolerance, check out the Git repo.