Asynchronously Calling REST Services in a Synchronous Quarkus Application

An example from practice: A Quarkus backend which collects data from several endpoints of a third-party REST API, aggregates the received data and sends it back to a web frontend. Additionally,

  • the third-party API is slow and needs 3-4 seconds to answer a request and
  • the backend is implemented in a classic synchronous way.

Calling the third-party API endpoints sequentially and waiting for each result is not an option, obviously. Luckily Quarkus also allows an event-driven reactive programming style by offering Smallrye Mutiny support. Although the backend in the above scenario is implemented in a synchronous, blocking way, it still can utilize the asynchronous nature of Mutiny to parallelize calls to other APIs. This can be achieved by combining Unis of the different REST calls into a new Uni which emits either an item when all calls succeed or a failure when at least one of the calls failed. The emitted item contains the responses of all calls, either as one of the predefined Tuple types or as custom type via a provided combinator function. The result can then be transformed and passed on to the web frontend. Mutiny also offers a retry mechanism in case Unis fail which comes in handy since calls to external APIs are inherently unreliable. At first glance, this all sounds complicated, but an example will make things clearer.

Going Asynchronous in a Synchronous Environment

We define a basic REST client for a fictional third-party greeting API like this

1@RegisterRestClient
2@Path("/")
3public interface RestClientFor3rdParty {
4    @GET
5    @Produces(MediaType.TEXT_PLAIN)
6    @Path("/hello")
7    Uni<String> getGreetingAsync(final @RestQuery String name);
8}

Note that the declared method getGreetingAsync returns a Uni<String> instead of a String which makes Quarkus schedule the REST call on one of the I/O threads instead of a classic worker thread, which in turn enables asynchronous, non-blocking execution. Next we need a service class which handles the third-party API calls. In this simple example, the service collects four greetings in parallel for different names from the third-party API endpoint. If all calls are successful, it transforms the four results into a nicely formatted string with one greeting per line and returns it.

 1@ApplicationScoped
 2public class GreetingService {
 3    @RestClient
 4    RestClientFor3rdParty restClientFor3rdParty;
 5
 6    public String collectGreetings() {
 7        return Uni.combine().all().unis(
 8                        restClientFor3rdParty.getGreetingAsync("John"),
 9                        restClientFor3rdParty.getGreetingAsync("Inbal"),
10                        restClientFor3rdParty.getGreetingAsync("Aiko"),
11                        restClientFor3rdParty.getGreetingAsync("Rasana")
12                )
13                .asTuple()
14                .map(tuple -> IntStream.range(0, tuple.size())
15                        .mapToObj(tuple::nth)
16                        .map(Object::toString)
17                        .collect(Collectors.joining("\n")))
18                .await()
19                .indefinitely();
20    }
21}

Going through the highlighted lines:

  • The Uni.combine() creates a new Uni combining other Unis.
  • The following all() basically says "emit an item with the results when each of the combined Unis has emitted an item", which means in our case when all REST calls have succeeded. If one of the Unis fail, the others are canceled and the failure is propagated. If you're happy when at least one of the combined Unis has emitted an event, use any() instead.
  • Via the unis() method we pass the concrete Unis to be combined, in our case the four REST call Unis.
  • Since we receive one result from each REST call and since the combined Uni can emit only one item, we need to join the results into a composite object. The asTuple() method returns one of the TupleX types with X ranging from 2 to 9. If the combined Uni contains more than 9 Unis, if you want to pass e.g. a List of Unis to the unis() method or if you prefer a different way of aggregating the result in general, the with() method can be used together with a combinator function.
  • map() is a shortcut for onItem().transform() which allows transforming an item when it is emitted. In our scenario, we want to return a string consisting of the four greetings nicely rendered in separate lines, which we achieve by using an IntStream.
  • As the rest of the backend doesn't follow asynchronous principles, we cannot return the Uni at this point. We have to execute the REST calls, wait for the result and finally return the transformed result to the caller. This is triggered by calling await(). Note that this blocks the caller thread.
  • Finally, we need to specify how long we are willing to wait for the completion of all REST calls. In this example we want to wait forever, hence we call indefinitely(), something you probably don't want to do in a productive system if no other timeout is active. As safer alternative e.g. atMost() can be used instead. It receives a Duration specifying how long to wait before throwing a TimeoutException.

It is important to remember that null is explicitly allowed as item that can be emitted by a Uni. This case can occur, for example, if the third-party API itself returns null or also e.g. if it returns a HTTP 204 response. Depending on the concrete scenario, it may therefore be necessary to explicitly check the tuple values to ensure that they are not null before proceeding.

Retrying Failed Calls - Por Si Las Moscas 1

Retry with Mutiny

As mentioned, Mutiny offers a retry mechanism for failed Unis, and for GET REST calls it makes perfectly sense using it - at least if the called API follows common REST principles. The two options are:

  • retry only failed Unis
  • retry all Unis when at least one has failed

Just as the emitted item of a Uni can be processed via onItem(), errors can be handled via onFailure(), so all you have to do is to add the desired error handling in the right place. In the first case directly on the Unis of the REST calls

1restClientFor3rdParty.getGreetingAsync("Inbal")
2    .onFailure()
3    .retry().atMost(3)

and in the latter case on the combined Uni itself

1Uni.combine().all().unis( /* ... */  )
2    .asTuple()
3    .onFailure()
4    .retry().atMost(3)

The call to retry() is the entry point to the retry strategy, and it comes with a bunch of options like exponential backoff, jitter etc. which can be configured by subsequent calls in fluent style. The final call to e.g. indefinitely() or like in this example atMost() enables the configured retry strategy. Here, the retry is done three times and if the Uni fails one more time, the last failure is propagated.

Retry with the Quarkus Smallrye Fault Tolerance Extension

Alternatively, retries can be enabled by adding the Quarkus SmallRye Fault Tolerance extension and using the annotations it provides like e.g. @Retry and @ExponentialBackoff. Retrying only the failed Unis can be achieved by annotating the REST client method

 1@RegisterRestClient
 2@Path("/")
 3public interface RestClientFor3rdParty {
 4    @GET
 5    @Produces(MediaType.TEXT_PLAIN)
 6    @Path("/hello")
 7    @Retry
 8    @ExponentialBackoff
 9    Uni<String> getGreetingAsync(final @RestQuery String name);
10}

Retrying all Unis in case of failures can be achieved e.g. by annotating the method where the await().indefinitely() is called like

 1    @Retry
 2    @ExponentialBackoff
 3    public String collectGreetings() {
 4        return Uni.combine().all().unis(
 5                        restClientFor3rdParty.getGreetingAsync("John"),
 6                        restClientFor3rdParty.getGreetingAsync("Inbal"),
 7                        restClientFor3rdParty.getGreetingAsync("Aiko"),
 8                        restClientFor3rdParty.getGreetingAsync("Rasana")
 9                )
10                .asTuple()
11                .map(tuple -> IntStream.range(0, tuple.size())
12                        .mapToObj(tuple::nth)
13                        .map(Object::toString)
14                        .collect(Collectors.joining("\n")))
15                .await()
16                .indefinitely();
17    }

Which of the two retry mechanisms is chosen is therefore primarily a question of taste. Theoretically you can even use both together.

Conclusion

With the use of Mutiny it is possible to implement very efficient Quarkus applications in an elegant way by following reactive and asynchronous principles. As we have seen, we can parallelize REST calls and thus speed up an application by using combined Unis, even if the application is otherwise synchronous. Retrying failed calls is often advisable and easily possible with either Mutiny itself or the Smallrye Fault Tolerance extension.

As always, a full working example including a basic greeting API application and a sample client application can be found on my GitLab instance.


Feedback Welcome

I am always happy to receive comments, corrections, improvements or feedback in general! Please drop me a note on Mastodon or by E-Mail anytime.


  1. por si las moscas - just in case ↩︎