Quarkus RestClient vs Quarkus Vert.x: performance comparison for asynchronous tasks.
Reactive programming is based on the idea of asynchronous event processing. Asynchronous processing means that the processing of an event does not block the processing of other events. In response to popular demand, this article delves into the comparison between Quarkus Mutiny and Quarkus Vert.x. However, let’s give a brief overview about Quarkus and why is so famous today.
Quarkus was created to enable Java developers to create applications for a modern, cloud-native world. Quarkus is designed to seamlessly combine the familiar imperative style code and the non-blocking, reactive style when developing applications. If your endpoint method needs to accomplish an asynchronous or reactive task before being able to answer, you can declare your method to return the Uni
type from Mutiny.
For our Quarkus tests projects we are going to use the Quarkus version 3.5+.
Quarkus Mutiny is an intuitive, reactive programming library. It is the primary model to write reactive applications with Quarkus. Mutiny is very different from the other reactive programming libraries. With Mutiny everything is event-driven: you receive events, and you react to them.
Mutiny offers two types that are both event-driven and lazy:
- A Uni emits a single event (an item or a failure). Unis are convenient to represent asynchronous actions that return 0 or 1 result. A good example is the result of sending a message to a message broker queue.
- A Multi emits multiple events (n items, 1 failure or 1 completion). Multis can represent streams of items, potentially unbounded. A good example is receiving messages from a message broker queue.
On this article we will be exposing reactive APIs using Uni type.
Using the following maven dependency we can start working with Quarkus Mutiny.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
From the other side, Eclipse Vert.x is a toolkit used for building reactive applications on the JVM using an asynchronous and non-blocking execution model. Vert.x handle more requests with less resources compared to traditional stacks and frameworks based on blocking I/O. Also, is a great fit for all kinds of execution environments, including constrained environments like virtual machines and containers.
Quarkus uses Vert.x underneath, for this reason Quarkus applications can access and use the Vert.x APIs.
Using the following maven dependency we can start working with Quarkus Vertx.
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-web-client</artifactId>
</dependency>
Getting Started
First of all, make you have the following software installed.
- JDK 21
- Apache Maven 3.9+
- Eclipse 4.29+
- JMeter 5.5
Remember to setup JAVA_HOME and M2_HOME before to start.
To assess Quarkus performance, a series of rigorous tests were conducted on a Windows 11 Core i7 with 40GB of RAM. The load testing tool employed for these evaluations was JMeter, a reliable choice for measuring application performance under varying loads. The tests were carried out using Java 21, ensuring the most recent improvements and optimizations were accounted for in the assessment.
Architecture
As part of this performance comparison, we’re going to create two Quarkus projects.
Factorial External Api, is a quarkus project with just one Get Rest API.
- /getFactorialValue: return the factorial value for its input.
Factorial Reactive, is a quarkus project with two Get Rest APIs.
- /getFactorialUsingVertxWebClient: Use a vertx implementation to consume an external API.
- /getFactorialUsingRestClient: Use a RestClient implementation to consume an external API.
The previous picture shows the architecture diagram used to test the performance for 300 requests in one second using Apache JMeter. Then, we will test for 100 and 200 requests too.
Now, let’s delve into the specifics of the application code for each project.
Factorial External API
First, we need to create a new project. Let’s run the following maven command:
mvn io.quarkus.platform:quarkus-maven-plugin:3.5.1:create
-DprojectGroupId=org.acme
-DprojectArtifactId=factorial-external-api
-Dextensions=resteasy-reactive-jsonb
- Group: org.acme
- Artifact: factorial-external-api
- Extensions: RESTEasy Reactive Jsonb
Currently, we can’t create a Quarkus project from Quarkus Configure Application (https://code.quarkus.io/) because it doesn’t support Java 21 yet.
Import maven project in Eclipse IDE and add the following classes.
Java code
Factorial, is a Java Bean used to build the API response.
package org.acme;
public class Factorial {
private int input;
private long output;
private String timeElapsed;
private String uuid;
public Factorial(){
}
public Factorial(int input, long output, String timeElapsed, String uuid) {
this.input = input;
this.output = output;
this.timeElapsed = timeElapsed;
this.uuid = uuid;
}
public int getInput() {
return input;
}
public long getOutput() {
return output;
}
public String getTimeElapsed() {
return timeElapsed;
}
public String getUuid() {
return uuid;
}
}
FactorialService, is a service class used to calculate the factorial value and return an Uni response. Furthermore, the code generates an internal random number to sleep the current Thread for some Milliseconds. This sleep time helps us to simulate a slow or fast request.
package org.acme;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.core.Response;
@ApplicationScoped
public class FactorialService {
Logger LOGGER = LoggerFactory.getLogger(FactorialService.class);
public Uni<Response> getFactorialValue(int input, String uuid) {
long startTime = System.currentTimeMillis();
long output = calculateFactorialValue(input);
// GENERATE A RANDOM NUMBER TO SELECT SLEEP TIME
int sleepTime = 0;
if((getRandomNumber(1,10)%2)==0){
sleepTime = getRandomNumber(1,3) * 1000; // Sleep <1000,3000> MS for SLOW Request
} else {
sleepTime = 10; // Sleep 10 MS for FAST Request
}
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
LOGGER.error("Error while Thread sleep process for uuid [{}] ", uuid,e);
Response errorResponse = Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Error while Thread sleep process for uuid "+uuid).build();
return Uni.createFrom().item(errorResponse);
}
long endTime = System.currentTimeMillis();
long timeElapsed = endTime - startTime;
Factorial factorial = new Factorial(input, output, timeElapsed+" MS", uuid);
Response response = Response.ok(factorial).build();
LOGGER.info("END PROCESS, input [{}] output [{}] timeElapsed [{} MS] uuid [{}]", input, output, timeElapsed, uuid);
return Uni.createFrom().item(response);
}
private long calculateFactorialValue(int n) {
long fact = 1;
for (int i = 2; i <= n; i++) {
fact = fact * i;
}
return fact;
}
private int getRandomNumber(int min, int max) {
return (int) ((Math.random() * (max - min)) + min);
}
}
FactorialExternalAPI, this class exposes the Get Uni Response API to consume.
package org.acme;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("/factorialExternalApi")
@Produces(MediaType.APPLICATION_JSON)
public class FactorialExternalAPI {
@Inject
FactorialService factorialService;
@GET
@Path("/calculate/{input}/uuid/{uuid}")
public Uni<Response> getFactorialValue(@PathParam("input") int input, @PathParam("uuid") String uuid){
return factorialService.getFactorialValue(input, uuid);
}
}
Run application
Configure Maven Build from eclipse and Run application.
Also, you can use maven. Navigate into project directory and launch your application with maven command.
mvn compile quarkus:dev
Now, we can test the API from web browser
Factorial Reactive
Create a new project with the following maven command:
mvn io.quarkus.platform:quarkus-maven-plugin:3.5.1:create
-DprojectGroupId=org.acme
-DprojectArtifactId=factorial-reactive
-Dextensions=rest-client-reactive,resteasy-reactive
- Group: org.acme
- Artifact: factorial-reactive
- Extensions: Smallrye Mutiny Vertx Web Client, Rest Client Reactive, RESTEasy Reactive
Now, let’s add the following maven dependency in pom.xml file.
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-web-client</artifactId>
</dependency>
Import maven project in Eclipse IDE and add the following classes.
Java code
FactorialService, Interface created to consume ‘factorial-external-api’ project. REST Client Reactive is as simple as creating an interface using the proper Jakarta REST and MicroProfile annotations.
package org.acme;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@RegisterRestClient(configKey = "factorial-external-api")
@Produces(MediaType.APPLICATION_JSON)
public interface FactorialService {
@GET
@Path("/calculate/{input}/uuid/{uuid}")
Uni<Response> getFactorialValueFromClient(@PathParam("input") String input, @PathParam("uuid") String uuid);
}
FactorialReactive, is a class created that expose the two Get APIs. Both APIs will consume the Get API from ‘factorial-external-api’ project.
package org.acme;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.mutiny.core.Vertx;
import io.vertx.mutiny.ext.web.client.WebClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
@Path("/factorialApi")
public class FactorialReactive {
Logger LOGGER = LoggerFactory.getLogger(FactorialReactive.class);
private final WebClient client;
private static AtomicInteger atomicThreadCounter = new AtomicInteger();
@Inject
@RestClient
FactorialService factorialService;
public FactorialReactive(Vertx vertx) {
this.client = WebClient.create(vertx,
new WebClientOptions().setDefaultHost("localhost").setDefaultPort(8080).setSsl(false)
.setTrustAll(true));
}
@GET
@Path("/vertxWebClient")
public Uni<Response> getFactorialUsingVerxtWebClient() {
var idProcess = UUID.randomUUID();
LOGGER.info("VERTX, START idProcess [{}] with ATOMIC_VALUE [{}]", idProcess, atomicThreadCounter.getAndIncrement());
// GENERATE A RANDOM NUMBER BETWEEN 2 TO 10
int valueToCalculate = getRandomNumber(2,10);
return client.get("/factorialExternalApi/calculate/" + valueToCalculate+"/uuid/"+idProcess)
.send()
.onFailure()
.invoke(ex -> LOGGER.error("Couldn't execute EXTERNAL Factorial API", ex))
.map(resp -> {
if (resp.statusCode() == 200) {
LOGGER.info("VERTX, END idProcess [{}] with ATOMIC_VALUE [{}]", idProcess, atomicThreadCounter.getAndDecrement());
return Response.ok(resp.bodyAsJsonObject()).build();
} else {
String errorMessage = "Error connecting with factorial external API";
LOGGER.error(errorMessage);
return Response.status(resp.statusCode()).entity(errorMessage).build();
}
});
}
@GET
@Path("/restClient")
public Uni<Response> getFactorialUsingRestClient() {
var idProcess = UUID.randomUUID();
try {
LOGGER.info("UNI, START idProcess [{}] with ATOMIC_VALUE [{}]", idProcess, atomicThreadCounter.getAndIncrement());
int valueToCalculate = getRandomNumber(2,10);
Uni<Response> resp = factorialService.getFactorialValueFromClient(String.valueOf(valueToCalculate), String.valueOf(idProcess));
return resp;
} finally {
LOGGER.info("UNI, END idProcess [{}] with ATOMIC_VALUE [{}]", idProcess, atomicThreadCounter.getAndDecrement());
}
}
private int getRandomNumber(int min, int max) {
return (int) ((Math.random() * (max - min)) + min);
}
}
The two GET API methods have an atomic variable named ‘atomicThreadCounter’. This variable is used to demonstrate the APIs are receiving concurrent requests around one second.
application.properties
- Let’s configure the port where to run the application. It should be different from the previous project.
- Configure the client for ‘factorial-external-api’ project with the correct URL and port.
quarkus.http.port=8083
# Factorial External API Client
factorial-external-api/mp-rest/url=http://localhost:8080/factorialExternalApi
Run application
Configure Maven Build from eclipse and Run application.
Let’s keep the ‘factorial-external-api’ project running too.
Then, test both APIs from web browser with the following requests.
Now we are ready to test both APIs using JMeter.
Testing from JMeter
Let’s use JMeter to send 100, 200 and 300 requests around one second for both APIs and compare results. The following diagrams just show the results for 300 requests and we will show the results summary for 100 and 200 requests in the next section.
FactorialVertxWebClient
After run the application JMetter will send 300 requests in around one second. The console log shows the atomic variable increase until reach 300 requests, then reduce them until 1.
JMetter shows all 300 requests were processed successfully.
Now, let’s test using the Quarkus RestClient API.
FactorialUniRestClient
After run the application JMetter will send 300 requests around one second. The console log shows the atomic variable increases its value to 1 and reduce to 0 for each request.
We can conclude that the API is processing all requests around one second in asynchronous mode. It doesn’t wait to complete the task to reply a response.
Furthermore, we are seeing many timeout error messages for many requests.
17:29:32,161 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (vert.x-eventloop-thread-1) HTTP Request to /factorialApi/restClient failed, error id: e2497af0–43a1–4e46–98a7–139b3d506f0f-1: io.vertx.core.http.impl.NoStackTraceTimeoutException: The timeout of 30000 ms has been exceeded when getting a connection to localhost:8080
There are more than 150 error requests in JMetter.
PERFORMANCE COMPARISON
After sending 100, 200 and 300 requests from JMetter where each group of requests will be sent in a period of one second. The results will be summarized in the next chart. It shows the time taken (hh:mm:ss:SSS) and number of success/failure requests for each group or requests.
From the chart, we can establish:
- For 100 requests, Vertx and RestClient (Uni) got almost a similar response time and all of them were successful.
- For 200 requests, there are approximately 72 failure for RestClient requests. Meanwhile, Vertx process all of them successfully.
- For 300 requests, RestClient fails more than the middle of their requests. However, Vertx process all of them successfully again.
Increase requests number for Vertx
Previously, we have tested Vertx with 300 requests in around one second. Let’s increase this number to 500, 750 and 1000 using JMetter. Let’s see the results in the following chart.
As we can see, Vertx supports up to 1000 requests in one second without any failure. Also, the memory consumption remains stable for all these numbers of requests. Seeing the results, we can establish that Vertx is a great choice if your application expects to receive concurrent requests and avoid failure requests.
Next steps
- Testing and comparing the results using Bombardier load test tool.
- Testing and comparing the results between Quarkus Vertx and SpringBoot Webflux.
- Delve into the configuration of the Vertx instance from the application.properties file.
- Understand the concepts about IO thread and worker thread in Quarkus. So, we can use them better for imperative and reactive workloads.
Thanks for reading!