Skip to content

Commit

Permalink
Merge pull request #34913 from cescoffier/virtual-thread-doc
Browse files Browse the repository at this point in the history
  • Loading branch information
cescoffier authored Jul 24, 2023
2 parents fa1dced + d538a25 commit 13ecb78
Show file tree
Hide file tree
Showing 3 changed files with 590 additions and 315 deletions.
316 changes: 316 additions & 0 deletions docs/src/main/asciidoc/resteasy-reactive-virtual-threads.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
////
This guide is maintained in the main Quarkus repository
and pull requests should be submitted there:
https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
////
[[resteasy-reactive-virtual-threads]]
= Use virtual threads in REST applications
include::_attributes.adoc[]
:diataxis-type: howto
:categories: web, core
:summary: How to use virtual threads in a REST application

In this guide, we see how you can use virtual threads in a REST application.
Because virtual threads are all about I/O, we will also use the REST client.

== Prerequisites

include::{includes}/prerequisites.adoc[]

== Architecture

The application built into this guide is quite simple.
It calls a weather service for two cities (Valence, France, and Athens, Greece) and decides the best place based on the current temperature.


== Create the Maven project

First, we need a new project. Create a new project with the following command:

:create-app-artifact-id: rest-virtual-threads
:create-app-extensions: resteasy-reactive-jackson,quarkus-rest-client-reactive-jackson
include::{includes}/devtools/create-app.adoc[]

This command generates a new project importing the RESTEasy Reactive, Reactive REST client, and https://github.com/FasterXML/jackson[Jackson] extensions,
and in particular, adds the following dependencies:

[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
</dependency>
----

[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"]
.build.gradle
----
implementation("io.quarkus:quarkus-resteasy-reactive-jackson")
implementation("quarkus-rest-client-reactive-jackson")
----

[NOTE]
====
You might wonder why we use _reactive_ extensions.
Virtual threads have limitations, and we can only integrate them properly when using the reactive extensions.
No worries: your code will be written 100% in a synchronous / imperative style.
Check the xref:./virtual-threads.adoc#why-not[virtual thread reference guide] for details.
====

== Prepare the `pom.xml` file

We need to customize the `pom.xml` file to use virtual threads.

1) Locate the line with `<maven.compiler.release>17</maven.compiler.release>`, and replace it with:

[source, xml]
----
<maven.compiler.release>20</maven.compiler.release>
----

2) In the maven-compiler-plugin configuration, add the `--enable-preview` arg (only if you use Java 19 or Java 20)

[source, xml]
----
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
<arg>--enable-preview</arg> <!-- Added line -->
</compilerArgs>
</configuration>
</plugin>
----

3) In the maven-surefire-plugin and maven-failsafe-plugin configurations, add the following `argLine` parameter:

[source, xml]
----
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
<argLine>--enable-preview -Djdk.tracePinnedThreads</argLine> <!-- Added line -->
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
<argLine>--enable-preview -Djdk.tracePinnedThreads</argLine> <!-- Added line -->
</configuration>
</execution>
</executions>
</plugin>
----


The `-Djdk.tracePinnedThreads` will detect pinned carrier threads while running tests (See xref:./virtual-threads.adoc#pinning[the virtual thread reference guide for details]).

[IMPORTANT]
.--enable-preview and Java 21
====
The `--enable-preview` flag is only required if you use Java 19 or 20.
====

== Create the weather client

This section is not about virtual threads.
Because we need to do some I/O to demonstrate virtual threads usage, we need a client doing I/O operations.
In addition, the reactive REST client is virtual thread friendly: it does not pin and handle propagation correctly.

Create the `src/main/java/org/acme/WeatherService.java` class with the following content:

[source, java]
----
package org.acme;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.quarkus.rest.client.reactive.ClientQueryParam;
import jakarta.ws.rs.GET;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.reactive.RestQuery;
@RegisterRestClient(baseUri = "https://api.open-meteo.com/v1/forecast")
public interface WeatherService {
@GET
@ClientQueryParam(name = "current_weather", value = "true")
WeatherResponse getWeather(@RestQuery double latitude, @RestQuery double longitude);
record WeatherResponse(@JsonProperty("current_weather") Weather weather) {
// represents the response
}
record Weather(double temperature, double windspeed) {
// represents the inner object
}
}
----

This class models the HTTP interaction with the weather service.
Read more about the rest client in the dedicated xref:./rest-client-reactive.adoc[guide].

== Create the HTTP endpoint

Now, create the `src/main/java/org/acme/TheBestPlaceToBeResource.java` class with the following content:

[source, java]
----
package org.acme;
import io.smallrye.common.annotation.RunOnVirtualThread;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.inject.RestClient;
@Path("/")
public class TheBestPlaceToBeResource {
static final double VALENCE_LATITUDE = 44.9;
static final double VALENCE_LONGITUDE = 4.9;
static final double ATHENS_LATITUDE = 37.9;
static final double ATHENS_LONGITUDE = 23.7;
@RestClient WeatherService service;
@GET
@RunOnVirtualThread <1>
public String getTheBestPlaceToBe() {
var valence = service.getWeather(VALENCE_LATITUDE, VALENCE_LONGITUDE).weather().temperature();
var athens = service.getWeather(ATHENS_LATITUDE, ATHENS_LONGITUDE).weather().temperature();
// Advanced decision tree
if (valence > athens && valence <= 35) {
return "Valence! (" + Thread.currentThread() + ")";
} else if (athens > 35) {
return "Valence! (" + Thread.currentThread() + ")";
} else {
return "Athens (" + Thread.currentThread() + ")";
}
}
}
----
<1> Instructs Quarkus to invoke this method on a virtual thread

== Run the application in dev mode

Make sure that you use OpenJDK and JVM versions supporting virtual thread and launch the _dev mode_ with `./mvnw quarkus:dev`:

[source, shell]
----
> java --version
openjdk 20.0.1 2023-04-18 <1>
OpenJDK Runtime Environment Temurin-20.0.1+9 (build 20.0.1+9)
OpenJDK 64-Bit Server VM Temurin-20.0.1+9 (build 20.0.1+9, mixed mode)
> ./mvnw quarkus:dev <2>
----
<1> Must be 19+
<2> Launch the dev mode

Then, in a browser, open http://localhost:8080.
You should get something like:

[source, text]
----
Valence! (VirtualThread[#144]/runnable@ForkJoinPool-1-worker-6)
----

As you can see, the endpoint runs on a virtual thread.

It's essential to understand what happened behind the scene:

1. Quarkus creates the virtual thread to invoke your endpoint (because of the `@RunOnVirtualThread` annotation).
2. When the code invokes the rest client, the virtual thread is blocked, BUT the carrier thread is not blocked (that's the virtual thread _magic touch_).
3. Once the first invocation of the rest client completes, the virtual thread is rescheduled and continues its execution.
4. The second rest client invocation happens, and the virtual thread is blocked again (but not the carrier thread).
5. Finally, when the second invocation of the rest client completes, the virtual thread is rescheduled and continues its execution.
6. The method returns the result. The virtual thread terminates.
7. The result is captured by Quarkus and written in the HTTP response

== Verify pinning using tests

In the `pom.xml,` we added an `argLine` argument to the surefire and failsafe plugins:

[source, xml]
----
<argLine>--enable-preview -Djdk.tracePinnedThreads</argLine>
----

The `-Djdk.tracePinnedThreads` dumps the stack trace if a virtual thread cannot be _unmounted_ smoothly (meaning that it blocks the carrier thread).
That's what we call _pinning_ (more info in xref:./virtual-threads.adoc#pinning[the virtual thread reference guide]).

We recommend enabling this flag in tests.
Thus, you can check that your application behaves correctly when using virtual threads.
Just check your log after having run the test.
If you see a stack trace... better check what's wrong.
If your code (or one of your dependencies) pins, it might be better to use regular worker thread instead.

Create the `src/test/java/org/acme/TheBestPlaceToBeResourceTest.java` class with the following content:

[source, java]
----
package org.acme;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;
@QuarkusTest
class TheBestPlaceToBeResourceTest {
@Test
void verify() {
RestAssured.get("/")
.then()
.statusCode(200);
}
}
----

It is a straightforward test, but at least it will detect if our application is pinning.
Run the test with either:

- `r` in dev mode (using continuous testing)
- `./mvnw test`

As you will see, it does not pin - no stack trace.
It is because the reactive REST client is implemented in a virtual-thread-friendly way.

The same approach can be used with integration tests.

== Conclusion

This guide shows how you can use virtual threads with RESTEasy Reactive and the reactive REST client.
Learn more about virtual threads support on:

- xref:./messaging-virtual-threads.adoc[@RunOnVirtualThread in messaging applications] (this guide covers Apache Kafka)
- xref:./grpc-virtual-threads.adoc[@RunOnVirtualThread in gRPC services]
- xref:./virtual-threads.adoc[the virtual thread reference guide] (include native compilation and containerization)
3 changes: 2 additions & 1 deletion docs/src/main/asciidoc/resteasy-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1846,14 +1846,15 @@ how to use it.
Here are some more advanced topics that you may not need to know about initially, but
could prove useful for more complex use cases.

[#execution-model-blocking-non-blocking]
=== Execution model, blocking, non-blocking

[[execution-model]]

RESTEasy Reactive is implemented using two main thread types:

- Event-loop threads: which are responsible, among other things, for reading bytes from the HTTP request and
writing bytes back to the HTTP response
writing bytes back to the HTTP response
- Worker threads: they are pooled and can be used to offload long-running operations

The event-loop threads (also called IO threads) are responsible for actually performing all the IO
Expand Down
Loading

0 comments on commit 13ecb78

Please sign in to comment.