-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #34913 from cescoffier/virtual-thread-doc
- Loading branch information
Showing
3 changed files
with
590 additions
and
315 deletions.
There are no files selected for viewing
316 changes: 316 additions & 0 deletions
316
docs/src/main/asciidoc/resteasy-reactive-virtual-threads.adoc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.