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, useany()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 theTupleXtypes withXranging from 2 to 9. If the combined Uni contains more than 9 Unis, if you want to pass e.g. aListof Unis to theunis()method or if you prefer a different way of aggregating the result in general, thewith()method can be used together with a combinator function. map()is a shortcut foronItem().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 anIntStream.- 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 aDurationspecifying how long to wait before throwing aTimeoutException.
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.
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.
-
por si las moscas - just in case ↩︎