Skip to content

Commit 14dd05e

Browse files
committed
Reactive routes: virtual threads support
- resolves quarkusio#36430
1 parent b3a623a commit 14dd05e

File tree

15 files changed

+266
-13
lines changed

15 files changed

+266
-13
lines changed

docs/src/main/asciidoc/reactive-routes.adoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public void blocking(RoutingContext rc) {
138138
// ...
139139
}
140140
----
141-
When `@Blocking` is used, it ignores the `type` attribute of `@Route`.
141+
When `@Blocking` is used, the `type` attribute of the `@Route` is ignored.
142142
====
143143

144144
The `@Route` annotation is repeatable and so you can declare several routes for a single method:
@@ -164,6 +164,12 @@ String person() {
164164
----
165165
<1> If the `accept` header matches `text/html`, we set the content type automatically to `text/html`.
166166

167+
=== Executing route on a virtual thread
168+
169+
You can annotate a route method with `@io.smallrye.common.annotation.RunOnVirtualThread` in order to execute it on a virtual thread.
170+
However, keep in mind that not everything can run safely on virtual threads.
171+
You should read the xref:virtual-threads.adoc#run-code-on-virtual-threads-using-runonvirtualthread[Virtual thread support reference] carefully and get acquainted with all the details.
172+
167173
=== Handling conflicting routes
168174

169175
You may end up with multiple routes matching a given path.

docs/src/main/asciidoc/virtual-threads.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ In this scenario, it is worse than useless to have thousands of threads if we ha
7979
Even worse, when running a CPU-bound workload on a virtual thread, the virtual thread monopolizes the carrier thread on which it is mounted.
8080
It will either reduce the chance for the other virtual thread to run or will start creating new carrier threads, leading to high memory usage.
8181

82+
[[run-code-on-virtual-threads-using-runonvirtualthread]]
8283
== Run code on virtual threads using @RunOnVirtualThread
8384

8485
In Quarkus, the support of virtual thread is implemented using the link:{runonvthread}[@RunOnVirtualThread] annotation.

extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/HandlerDescriptor.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import org.jboss.jandex.Type.Kind;
77

88
import io.quarkus.hibernate.validator.spi.BeanValidationAnnotationsBuildItem;
9-
import io.quarkus.vertx.http.runtime.HandlerType;
109

1110
/**
1211
* Describe a request handler.
@@ -15,15 +14,15 @@ class HandlerDescriptor {
1514

1615
private final MethodInfo method;
1716
private final BeanValidationAnnotationsBuildItem validationAnnotations;
18-
private final HandlerType handlerType;
17+
private final boolean failureHandler;
1918
private final Type payloadType;
2019
private final String[] contentTypes;
2120

22-
HandlerDescriptor(MethodInfo method, BeanValidationAnnotationsBuildItem bvAnnotations, HandlerType handlerType,
21+
HandlerDescriptor(MethodInfo method, BeanValidationAnnotationsBuildItem bvAnnotations, boolean failureHandler,
2322
String[] producedTypes) {
2423
this.method = method;
2524
this.validationAnnotations = bvAnnotations;
26-
this.handlerType = handlerType;
25+
this.failureHandler = failureHandler;
2726
Type returnType = method.returnType();
2827
if (returnType.kind() == Kind.VOID) {
2928
payloadType = null;
@@ -120,8 +119,8 @@ boolean isPayloadMutinyBuffer() {
120119
return type.name().equals(DotNames.MUTINY_BUFFER);
121120
}
122121

123-
HandlerType getHandlerType() {
124-
return handlerType;
122+
boolean isFailureHandler() {
123+
return failureHandler;
125124
}
126125

127126
}

extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesProcessor.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ public boolean test(String name) {
445445
if (routeHandler == null) {
446446
String handlerClass = generateHandler(
447447
new HandlerDescriptor(businessMethod.getMethod(), beanValidationAnnotations.orElse(null),
448-
handlerType, produces),
448+
handlerType == HandlerType.FAILURE, produces),
449449
businessMethod.getBean(), businessMethod.getMethod(), classOutput, transformedAnnotations,
450450
routeString, reflectiveHierarchy, produces.length > 0 ? produces[0] : null,
451451
validatorAvailable, index);
@@ -458,6 +458,13 @@ public boolean test(String name) {
458458
// Wrap the route handler if necessary
459459
// Note that route annotations with the same values share a single handler implementation
460460
routeHandler = recorder.compressRouteHandler(routeHandler, businessMethod.getCompression());
461+
if (businessMethod.getMethod().hasDeclaredAnnotation(DotNames.RUN_ON_VIRTUAL_THREAD)) {
462+
LOGGER.debugf("Route %s#%s() will be executed on a virtual thread",
463+
businessMethod.getMethod().declaringClass().name(), businessMethod.getMethod().name());
464+
routeHandler = recorder.runOnVirtualThread(routeHandler);
465+
// The handler must be executed on the event loop
466+
handlerType = HandlerType.NORMAL;
467+
}
461468

462469
RouteMatcher matcher = new RouteMatcher(path, regex, produces, consumes, methods, order);
463470
matchers.put(matcher, businessMethod.getMethod());
@@ -489,7 +496,7 @@ public boolean test(String name) {
489496

490497
for (AnnotatedRouteFilterBuildItem filterMethod : routeFilterBusinessMethods) {
491498
String handlerClass = generateHandler(
492-
new HandlerDescriptor(filterMethod.getMethod(), beanValidationAnnotations.orElse(null), HandlerType.NORMAL,
499+
new HandlerDescriptor(filterMethod.getMethod(), beanValidationAnnotations.orElse(null), false,
493500
new String[0]),
494501
filterMethod.getBean(), filterMethod.getMethod(), classOutput, transformedAnnotations,
495502
filterMethod.getRouteFilter().toString(true), reflectiveHierarchy, null, validatorAvailable, index);
@@ -785,7 +792,7 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met
785792
defaultProduces == null ? invoke.loadNull() : invoke.load(defaultProduces));
786793

787794
// For failure handlers attempt to match the failure type
788-
if (descriptor.getHandlerType() == HandlerType.FAILURE) {
795+
if (descriptor.isFailureHandler()) {
789796
Type failureType = getFailureType(parameters, index);
790797
if (failureType != null) {
791798
ResultHandle failure = invoke.invokeInterfaceMethod(Methods.FAILURE, routingContext);

extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ public Handler<RoutingContext> createHandler(String handlerClassName) {
5151
}
5252
}
5353

54+
public Handler<RoutingContext> runOnVirtualThread(Handler<RoutingContext> routeHandler) {
55+
return new VirtualThreadsRouteHandler(routeHandler);
56+
}
57+
5458
public Handler<RoutingContext> compressRouteHandler(Handler<RoutingContext> routeHandler, HttpCompression compression) {
5559
if (httpBuildTimeConfig.enableCompression) {
5660
return new HttpCompressionHandler(routeHandler, compression,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.quarkus.vertx.web.runtime;
2+
3+
import io.quarkus.vertx.core.runtime.VertxCoreRecorder;
4+
import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle;
5+
import io.quarkus.virtual.threads.VirtualThreadsRecorder;
6+
import io.smallrye.common.vertx.VertxContext;
7+
import io.vertx.core.Context;
8+
import io.vertx.core.Handler;
9+
import io.vertx.ext.web.RoutingContext;
10+
11+
public class VirtualThreadsRouteHandler implements Handler<RoutingContext> {
12+
13+
private final Handler<RoutingContext> routeHandler;
14+
15+
public VirtualThreadsRouteHandler(Handler<RoutingContext> routeHandler) {
16+
this.routeHandler = routeHandler;
17+
}
18+
19+
@Override
20+
public void handle(RoutingContext context) {
21+
Context vertxContext = VertxContext.getOrCreateDuplicatedContext(VertxCoreRecorder.getVertx().get());
22+
VertxContextSafetyToggle.setContextSafe(vertxContext, true);
23+
vertxContext.runOnContext(new Handler<Void>() {
24+
@Override
25+
public void handle(Void event) {
26+
VirtualThreadsRecorder.getCurrent().execute(new Runnable() {
27+
@Override
28+
public void run() {
29+
routeHandler.handle(context);
30+
}
31+
});
32+
}
33+
});
34+
}
35+
36+
}

integration-tests/virtual-threads/pom.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
<module>vertx-event-bus-virtual-threads</module>
3636
<module>scheduler-virtual-threads</module>
3737
<module>quartz-virtual-threads</module>
38-
<module>virtual-threads-disabled</module>
38+
<module>virtual-threads-disabled</module>
39+
<module>reactive-routes-virtual-threads</module>
3940
</modules>
4041

4142
<build>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<artifactId>quarkus-virtual-threads-integration-tests-parent</artifactId>
9+
<groupId>io.quarkus</groupId>
10+
<version>999-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>quarkus-integration-test-virtual-threads-reactive-routes</artifactId>
14+
<name>Quarkus - Integration Tests - Virtual Threads - Reactive Routes</name>
15+
16+
<dependencies>
17+
<dependency>
18+
<groupId>io.quarkus</groupId>
19+
<artifactId>quarkus-reactive-routes</artifactId>
20+
</dependency>
21+
<dependency>
22+
<groupId>io.quarkus</groupId>
23+
<artifactId>quarkus-junit5</artifactId>
24+
<scope>test</scope>
25+
</dependency>
26+
<dependency>
27+
<groupId>io.quarkus.junit5</groupId>
28+
<artifactId>junit5-virtual-threads</artifactId>
29+
<scope>test</scope>
30+
</dependency>
31+
<dependency>
32+
<groupId>io.rest-assured</groupId>
33+
<artifactId>rest-assured</artifactId>
34+
<scope>test</scope>
35+
</dependency>
36+
<dependency>
37+
<groupId>org.awaitility</groupId>
38+
<artifactId>awaitility</artifactId>
39+
<scope>test</scope>
40+
</dependency>
41+
<dependency>
42+
<groupId>org.assertj</groupId>
43+
<artifactId>assertj-core</artifactId>
44+
<scope>test</scope>
45+
</dependency>
46+
47+
<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->
48+
<dependency>
49+
<groupId>io.quarkus</groupId>
50+
<artifactId>quarkus-reactive-routes-deployment</artifactId>
51+
<version>${project.version}</version>
52+
<type>pom</type>
53+
<scope>test</scope>
54+
<exclusions>
55+
<exclusion>
56+
<groupId>*</groupId>
57+
<artifactId>*</artifactId>
58+
</exclusion>
59+
</exclusions>
60+
</dependency>
61+
</dependencies>
62+
63+
<build>
64+
<plugins>
65+
<plugin>
66+
<groupId>io.quarkus</groupId>
67+
<artifactId>quarkus-maven-plugin</artifactId>
68+
</plugin>
69+
<plugin>
70+
<groupId>org.apache.maven.plugins</groupId>
71+
<artifactId>maven-surefire-plugin</artifactId>
72+
</plugin>
73+
</plugins>
74+
</build>
75+
76+
</project>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.quarkus.virtual.vertx.web;
2+
3+
import java.lang.reflect.Method;
4+
5+
import io.quarkus.arc.Arc;
6+
import io.smallrye.common.vertx.VertxContext;
7+
import io.vertx.core.Vertx;
8+
9+
public class AssertHelper {
10+
11+
/**
12+
* Asserts that the current method:
13+
* - runs on a duplicated context
14+
* - runs on a virtual thread
15+
* - has the request scope activated
16+
*/
17+
public static void assertEverything() {
18+
assertThatTheRequestScopeIsActive();
19+
assertThatItRunsOnVirtualThread();
20+
assertThatItRunsOnADuplicatedContext();
21+
}
22+
23+
public static void assertThatTheRequestScopeIsActive() {
24+
if (!Arc.container().requestContext().isActive()) {
25+
throw new AssertionError(("Expected the request scope to be active"));
26+
}
27+
}
28+
29+
public static void assertThatItRunsOnADuplicatedContext() {
30+
var context = Vertx.currentContext();
31+
if (context == null) {
32+
throw new AssertionError("The method does not run on a Vert.x context");
33+
}
34+
if (!VertxContext.isOnDuplicatedContext()) {
35+
throw new AssertionError("The method does not run on a Vert.x **duplicated** context");
36+
}
37+
}
38+
39+
public static void assertThatItRunsOnVirtualThread() {
40+
// We cannot depend on a Java 20.
41+
try {
42+
Method isVirtual = Thread.class.getMethod("isVirtual");
43+
isVirtual.setAccessible(true);
44+
boolean virtual = (Boolean) isVirtual.invoke(Thread.currentThread());
45+
if (!virtual) {
46+
throw new AssertionError("Thread " + Thread.currentThread() + " is not a virtual thread");
47+
}
48+
} catch (Exception e) {
49+
throw new AssertionError(
50+
"Thread " + Thread.currentThread() + " is not a virtual thread - cannot invoke Thread.isVirtual()", e);
51+
}
52+
}
53+
54+
public static void assertNotOnVirtualThread() {
55+
// We cannot depend on a Java 20.
56+
try {
57+
Method isVirtual = Thread.class.getMethod("isVirtual");
58+
isVirtual.setAccessible(true);
59+
boolean virtual = (Boolean) isVirtual.invoke(Thread.currentThread());
60+
if (virtual) {
61+
throw new AssertionError("Thread " + Thread.currentThread() + " is a virtual thread");
62+
}
63+
} catch (Exception e) {
64+
// Trying using Thread name.
65+
var name = Thread.currentThread().toString();
66+
if (name.toLowerCase().contains("virtual")) {
67+
throw new AssertionError("Thread " + Thread.currentThread() + " seems to be a virtual thread");
68+
}
69+
}
70+
}
71+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.quarkus.virtual.vertx.web;
2+
3+
import io.quarkus.vertx.web.Route;
4+
import io.smallrye.common.annotation.RunOnVirtualThread;
5+
6+
public class Routes {
7+
8+
@RunOnVirtualThread
9+
@Route
10+
String hello() {
11+
AssertHelper.assertEverything();
12+
// Quarkus specific - each VT has a unique name
13+
return Thread.currentThread().getName();
14+
}
15+
16+
}

0 commit comments

Comments
 (0)