Asynchronous programming with MicroProfile Fault Tolerance

MicroProfile Fault Tolerance provides annotations that can help you deal with failure. You can combine the @Asynchronous fault tolerance annotation with the CompletionStage interface to write asynchronous code that is resilient to faults.

The @Asynchronous annotation

A method that is annotated with the @Asynchronous annotation runs asynchronously, which means that the method doesn’t run immediately on the main thread when it’s called. Instead, the method runs sometime later, usually on another thread. To use the @Asynchronous annotation, add the annotation to a method, and ensure that the annotated method returns either a CompletionStage object or a Future object. The following example uses the CompletableFuture.completedFuture() method to make the result compatible with the CompletionStage return type:

@Asynchronous
public CompletionStage<String> serviceA() {
    ...
    return CompletableFuture.completedFuture("serviceA");
}

Usually when a method is called, it returns a result. However, when a method is annotated with the @Asynchronous annotation, fault tolerance intervenes. When the annotated method is called, fault tolerance schedules the method to run later on a different thread and returns a CompletionStage or Future object. This object isn’t the CompletionStage or Future object that’s returned from the method because the method hasn’t run yet. It’s an object that fault tolerance creates. Sometime later, the method runs and returns a CompletionStage or Future object.

If the method returns a Future object, fault tolerance updates the Future object that it previously returned to the user. The object is updated so that it delegates to the Future object that was returned from the method, which allows the user to see the result from the method. If the method returns a CompletionStage object, fault tolerance waits for the CompletionStage object to complete before it completes the CompletionStage object that it previously returned to the user.

@Asynchronous annotation use cases

The following two use cases demonstrate how you can use the @Asynchronous annotation to make your asynchronous programs resilient:

Fault tolerance applied to an asynchronous API call

You can use the @Asynchronous annotation to apply fault tolerance policies to an asynchronous API call that returns a CompletionStage object. Without the @Asynchronous annotation, the other fault tolerance annotations, such as @Retry, don’t behave as you might expect. In the following use case, the .rx() method from the JAX-RS Client API calls remote REST services asynchronously:

private Client client = ClientBuilder.newClient();

public CompletionStage<String> clientDemo() {
    CompletionStage<String> response = client.target("http://example.com/resource")
        .request(MediaType.TEXT_PLAIN)
        .rx()
        .get(String.class);
    return response;
}

After the clientDemo() method is called without any annotations, you receive a CompletionStage<String> return type, which is named response in this example. Then, you can add an action to take after the CompletionStage object completes:

response.thenAccept(System.out::println);
--> responseText

If the request fails, you might want to retry the request with the @Retry annotation. In this example, using the @Retry annotation doesn’t work because the method doesn’t throw an exception even if the request fails. The method always returns a CompletionStage object through which the success or failure of the request is reported. However, if you add the @Asynchronous annotation, then the @Retry annotation is applied when the CompletionStage object completes, rather than when the method returns:

@Asynchronous
@Retry
public CompletionStage<String> clientDemo() {
    ...
}

For more information about using the JAX-RS .rx() method, see the guide on Consuming RESTful services using the reactive JAX-RS client.

Multiple actions performed in parallel with resilience

To run multiple methods in parallel, you can write methods that call other services, annotate them with the @Asynchronous annotation, and call them, as shown in the following example:

@Inject
private RequestScopedClass1 requestScopedBean1;

@Inject
private RequestScopedClass2 requestScopedBean2;

public CompletionStage<String> callServicesAsynchronously()  {
    CompletionStage<String> result1 = requestScopedBean1.serviceA(); // Where serviceA is annotated with @Asynchronous
    CompletionStage<String> result2 = requestScopedBean2.serviceB(); // Where serviceB is annotated with @Asynchronous
    ...
}

In this example, the serviceA() method is called, and then the serviceB() method is called. However, because both services are annotated with the @Asynchronous annotation, they run simultaneously on different threads, rather than sequentially.

Other fault tolerance annotations can also be implemented with the @Asynchronous annotation. For example, you can add the @Retry annotation to the serviceA() method and the @Timeout annotation to the serviceB() method:

@RequestScoped
public class RequestScopedClass1 {

    @Retry
    @Asynchronous
    public CompletionStage<String> serviceA() {
        doSomethingWhichMightFail()
        return CompletableFuture.completedFuture("serviceA");
    }
}

@RequestScoped
public class RequestScopedClass2 {

    @Timeout
    @Asynchronous
    public CompletionStage<String> serviceB() {
        doSomethingWhichMightFail()
        return CompletableFuture.completedFuture("serviceB");
    }
}

In this case, if the serviceA() method needs several retries, then a call to retrieve the result, such as the CompletionStage.thenAccept() method, doesn’t return until all the retries are complete.

Differences in execution flow with the @Asynchronous annotation

When a method is annotated with the @Asynchronous annotation, things change in the flow of execution. The following diagram shows how the fault tolerance annotations work together without the @Asynchronous annotation:

a diagram of execution flow without the @Asynchronous annotation
Execution flow without the @Asynchronous annotation

In this diagram, a call to a fault tolerance-annotated method consists of at least one attempt. If retry is enabled, several attempts might be made. Each attempt takes the following steps:

  • The circuit breaker is checked. If the circuit breaker is open, then the result of the attempt is a CircuitBreakerException exception, and the rest of the steps for the attempt are skipped.

  • If the circuit breaker isn’t open, then the timeout timer starts and the method attempts to reserve space on the bulkhead.

  • If the bulkhead isn’t full, a reservation is acquired, and the method runs. The result of the attempt is the value that’s returned or the exception that’s thrown by the method. If the timeout expires while the method is running, then the thread that’s running the method is interrupted. After the method returns or throws an exception, the bulkhead reservation is released, which frees up the space on the bulkhead.

  • However, if the bulkhead is full, the method isn’t run, and the result of the attempt is a BulkheadException exception.

  • Next, the timeout stops. If the timeout expired before it stopped, then the result of the attempt is replaced with a TimeoutException exception.

  • The result of the attempt is recorded by the circuit breaker.

The attempt is now complete and has a result. If a retry is needed, then the result is discarded and a new attempt is started, meaning that the previous steps are repeated. After an attempt completes and no retry is needed, a fallback might be needed if the last attempt resulted in an exception. Whether a fallback is needed depends on the fallback configuration. If a fallback is needed, the fallback runs and the result of the fallback replaces the previous result. Finally, the result is returned to the user.

The next diagram shows how the fault tolerance annotations work together with the @Asynchronous annotation. The Return Future or CompletionStage box, which is green, is in addition to the boxes in the previous diagram:

a diagram of execution flow with the @Asynchronous annotation
Execution flow with the @Asynchronous annotation

The following changes in execution flow occur when you use the @Asynchronous annotation:

  • A CompletionStage or Future object is returned before the method runs. After the method runs, the result from the method is propagated to the CompletionStage or Future object so that the caller can access it.

  • In addition to either accepting or rejecting the execution, the bulkhead can also queue the execution to run later. If the method is accepted by the bulkhead, it’s then scheduled to run on another thread, rather than immediately.

  • When a timeout is used, then the method is interrupted if the timeout expires. If the timeout expires, the execution skips forward to the point noted in the Timeout Expires box in the diagram. The result is then processed as if the method finished with a TimeoutException exception.

  • If a fallback occurs, the fallback also runs asynchronously so that it’s scheduled to run on another thread.

Interactions with other fault tolerance annotations

Annotating a method with the @Asynchronous annotation impacts the following fault tolerance annotations:

Interaction with the @Bulkhead annotation

When you use the @Asynchronous and @Bulkhead annotations together, fault tolerance provides the option to queue up requests if the maximum number of requests are already running. If less than the maximum concurrent requests are running when you call the method, then your method is scheduled to run immediately.

If any requests are in the queue when one execution of the method finishes, then the first request from the queue starts. When the queue is full, then the method fails with a BulkheadException exception. The size of the queue can be configured with the waitingTaskQueue parameter on the @Bulkhead annotation.

Interaction with the @Timeout annotation

When you use the @Asynchronous and @Timeout annotations together, the CompletionStage or Future object that’s returned to the caller can be completed as soon as the timeout expires. Even if the method is still running, it’s running on another thread so you can signal to a different thread that the result is ready. The thread that’s running the method is interrupted so that it can stop working and save resources.

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. The operation might still run to completion, even though the timeout expired and you received a TimeoutException exception.

Limitations of returning a Future object

While the @Asynchronous annotation can make methods that return a Future object run asynchronously, fault tolerance policies can be applied only to asynchronous methods that return a CompletionStage object. A Future object has two ways of getting the result of its method. It either blocks and waits with the get() method, or it polls with the isDone() method. To implement fault tolerance around an asynchronous result, a callback is required so that you don’t need a second thread that waits or polls for the result. A CompletionStage object facilitates this necessary callback.

Without a callback, fault tolerance is applied around the method call, not around the method result. Because a Future object doesn’t have a callback, the following issues arise when you implement fault tolerance:

  • The method call is considered successful as soon as the Future object is returned, even if the result of the Future object is an exception.

  • The bulkhead is released when the method returns, even if the Future object isn’t complete.

  • The timeout ends when the method returns, even if the Future object isn’t complete.

Because of these concerns, returning a Future object is only suitable for running operations in parallel. In these situations, methods often end with the return CompletableFuture.completedFuture(result); statement, meaning that a Future object that completes exceptionally can’t be returned. Either the method throws an exception, or it returns a successful Future object.