diff --git a/docs/src/main/asciidoc/reactive-routes.adoc b/docs/src/main/asciidoc/reactive-routes.adoc index 496ffac6e7e7a..6dd76ecb21f89 100644 --- a/docs/src/main/asciidoc/reactive-routes.adoc +++ b/docs/src/main/asciidoc/reactive-routes.adoc @@ -138,7 +138,7 @@ public void blocking(RoutingContext rc) { // ... } ---- -When `@Blocking` is used, it ignores the `type` attribute of `@Route`. +When `@Blocking` is used, the `type` attribute of the `@Route` is ignored. ==== The `@Route` annotation is repeatable and so you can declare several routes for a single method: @@ -164,6 +164,12 @@ String person() { ---- <1> If the `accept` header matches `text/html`, we set the content type automatically to `text/html`. +=== Executing route on a virtual thread + +You can annotate a route method with `@io.smallrye.common.annotation.RunOnVirtualThread` in order to execute it on a virtual thread. +However, keep in mind that not everything can run safely on virtual threads. +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. + === Handling conflicting routes You may end up with multiple routes matching a given path. diff --git a/docs/src/main/asciidoc/virtual-threads.adoc b/docs/src/main/asciidoc/virtual-threads.adoc index 6851a32c28455..e409077fdb982 100644 --- a/docs/src/main/asciidoc/virtual-threads.adoc +++ b/docs/src/main/asciidoc/virtual-threads.adoc @@ -79,6 +79,7 @@ In this scenario, it is worse than useless to have thousands of threads if we ha Even worse, when running a CPU-bound workload on a virtual thread, the virtual thread monopolizes the carrier thread on which it is mounted. 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. +[[run-code-on-virtual-threads-using-runonvirtualthread]] == Run code on virtual threads using @RunOnVirtualThread In Quarkus, the support of virtual thread is implemented using the link:{runonvthread}[@RunOnVirtualThread] annotation. diff --git a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/HandlerDescriptor.java b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/HandlerDescriptor.java index 61eafeee5f513..b4919b73d26a2 100644 --- a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/HandlerDescriptor.java +++ b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/HandlerDescriptor.java @@ -6,7 +6,6 @@ import org.jboss.jandex.Type.Kind; import io.quarkus.hibernate.validator.spi.BeanValidationAnnotationsBuildItem; -import io.quarkus.vertx.http.runtime.HandlerType; /** * Describe a request handler. @@ -15,15 +14,15 @@ class HandlerDescriptor { private final MethodInfo method; private final BeanValidationAnnotationsBuildItem validationAnnotations; - private final HandlerType handlerType; + private final boolean failureHandler; private final Type payloadType; private final String[] contentTypes; - HandlerDescriptor(MethodInfo method, BeanValidationAnnotationsBuildItem bvAnnotations, HandlerType handlerType, + HandlerDescriptor(MethodInfo method, BeanValidationAnnotationsBuildItem bvAnnotations, boolean failureHandler, String[] producedTypes) { this.method = method; this.validationAnnotations = bvAnnotations; - this.handlerType = handlerType; + this.failureHandler = failureHandler; Type returnType = method.returnType(); if (returnType.kind() == Kind.VOID) { payloadType = null; @@ -120,8 +119,8 @@ boolean isPayloadMutinyBuffer() { return type.name().equals(DotNames.MUTINY_BUFFER); } - HandlerType getHandlerType() { - return handlerType; + boolean isFailureHandler() { + return failureHandler; } } diff --git a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesProcessor.java b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesProcessor.java index 3f6872a54ac88..d36890773bcc2 100644 --- a/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesProcessor.java +++ b/extensions/reactive-routes/deployment/src/main/java/io/quarkus/vertx/web/deployment/ReactiveRoutesProcessor.java @@ -445,7 +445,7 @@ public boolean test(String name) { if (routeHandler == null) { String handlerClass = generateHandler( new HandlerDescriptor(businessMethod.getMethod(), beanValidationAnnotations.orElse(null), - handlerType, produces), + handlerType == HandlerType.FAILURE, produces), businessMethod.getBean(), businessMethod.getMethod(), classOutput, transformedAnnotations, routeString, reflectiveHierarchy, produces.length > 0 ? produces[0] : null, validatorAvailable, index); @@ -458,6 +458,13 @@ public boolean test(String name) { // Wrap the route handler if necessary // Note that route annotations with the same values share a single handler implementation routeHandler = recorder.compressRouteHandler(routeHandler, businessMethod.getCompression()); + if (businessMethod.getMethod().hasDeclaredAnnotation(DotNames.RUN_ON_VIRTUAL_THREAD)) { + LOGGER.debugf("Route %s#%s() will be executed on a virtual thread", + businessMethod.getMethod().declaringClass().name(), businessMethod.getMethod().name()); + routeHandler = recorder.runOnVirtualThread(routeHandler); + // The handler must be executed on the event loop + handlerType = HandlerType.NORMAL; + } RouteMatcher matcher = new RouteMatcher(path, regex, produces, consumes, methods, order); matchers.put(matcher, businessMethod.getMethod()); @@ -489,7 +496,7 @@ public boolean test(String name) { for (AnnotatedRouteFilterBuildItem filterMethod : routeFilterBusinessMethods) { String handlerClass = generateHandler( - new HandlerDescriptor(filterMethod.getMethod(), beanValidationAnnotations.orElse(null), HandlerType.NORMAL, + new HandlerDescriptor(filterMethod.getMethod(), beanValidationAnnotations.orElse(null), false, new String[0]), filterMethod.getBean(), filterMethod.getMethod(), classOutput, transformedAnnotations, filterMethod.getRouteFilter().toString(true), reflectiveHierarchy, null, validatorAvailable, index); @@ -785,7 +792,7 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met defaultProduces == null ? invoke.loadNull() : invoke.load(defaultProduces)); // For failure handlers attempt to match the failure type - if (descriptor.getHandlerType() == HandlerType.FAILURE) { + if (descriptor.isFailureHandler()) { Type failureType = getFailureType(parameters, index); if (failureType != null) { ResultHandle failure = invoke.invokeInterfaceMethod(Methods.FAILURE, routingContext); diff --git a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java index 6fd9f8519fc71..e87bf25a4a50e 100644 --- a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java +++ b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VertxWebRecorder.java @@ -51,6 +51,10 @@ public Handler createHandler(String handlerClassName) { } } + public Handler runOnVirtualThread(Handler routeHandler) { + return new VirtualThreadsRouteHandler(routeHandler); + } + public Handler compressRouteHandler(Handler routeHandler, HttpCompression compression) { if (httpBuildTimeConfig.enableCompression) { return new HttpCompressionHandler(routeHandler, compression, diff --git a/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VirtualThreadsRouteHandler.java b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VirtualThreadsRouteHandler.java new file mode 100644 index 0000000000000..4e0f7b3d2540c --- /dev/null +++ b/extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/VirtualThreadsRouteHandler.java @@ -0,0 +1,36 @@ +package io.quarkus.vertx.web.runtime; + +import io.quarkus.vertx.core.runtime.VertxCoreRecorder; +import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; +import io.quarkus.virtual.threads.VirtualThreadsRecorder; +import io.smallrye.common.vertx.VertxContext; +import io.vertx.core.Context; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class VirtualThreadsRouteHandler implements Handler { + + private final Handler routeHandler; + + public VirtualThreadsRouteHandler(Handler routeHandler) { + this.routeHandler = routeHandler; + } + + @Override + public void handle(RoutingContext context) { + Context vertxContext = VertxContext.getOrCreateDuplicatedContext(VertxCoreRecorder.getVertx().get()); + VertxContextSafetyToggle.setContextSafe(vertxContext, true); + vertxContext.runOnContext(new Handler() { + @Override + public void handle(Void event) { + VirtualThreadsRecorder.getCurrent().execute(new Runnable() { + @Override + public void run() { + routeHandler.handle(context); + } + }); + } + }); + } + +} diff --git a/integration-tests/virtual-threads/pom.xml b/integration-tests/virtual-threads/pom.xml index 853d39c10638b..e348c119ee410 100644 --- a/integration-tests/virtual-threads/pom.xml +++ b/integration-tests/virtual-threads/pom.xml @@ -35,7 +35,8 @@ vertx-event-bus-virtual-threads scheduler-virtual-threads quartz-virtual-threads - virtual-threads-disabled + virtual-threads-disabled + reactive-routes-virtual-threads diff --git a/integration-tests/virtual-threads/reactive-routes-virtual-threads/pom.xml b/integration-tests/virtual-threads/reactive-routes-virtual-threads/pom.xml new file mode 100644 index 0000000000000..e1b096a0c4cff --- /dev/null +++ b/integration-tests/virtual-threads/reactive-routes-virtual-threads/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + + quarkus-virtual-threads-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-virtual-threads-reactive-routes + Quarkus - Integration Tests - Virtual Threads - Reactive Routes + + + + io.quarkus + quarkus-reactive-routes + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus.junit5 + junit5-virtual-threads + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + + + io.quarkus + quarkus-reactive-routes-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + diff --git a/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/main/java/io/quarkus/virtual/vertx/web/AssertHelper.java b/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/main/java/io/quarkus/virtual/vertx/web/AssertHelper.java new file mode 100644 index 0000000000000..a96082b02791e --- /dev/null +++ b/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/main/java/io/quarkus/virtual/vertx/web/AssertHelper.java @@ -0,0 +1,71 @@ +package io.quarkus.virtual.vertx.web; + +import java.lang.reflect.Method; + +import io.quarkus.arc.Arc; +import io.smallrye.common.vertx.VertxContext; +import io.vertx.core.Vertx; + +public class AssertHelper { + + /** + * Asserts that the current method: + * - runs on a duplicated context + * - runs on a virtual thread + * - has the request scope activated + */ + public static void assertEverything() { + assertThatTheRequestScopeIsActive(); + assertThatItRunsOnVirtualThread(); + assertThatItRunsOnADuplicatedContext(); + } + + public static void assertThatTheRequestScopeIsActive() { + if (!Arc.container().requestContext().isActive()) { + throw new AssertionError(("Expected the request scope to be active")); + } + } + + public static void assertThatItRunsOnADuplicatedContext() { + var context = Vertx.currentContext(); + if (context == null) { + throw new AssertionError("The method does not run on a Vert.x context"); + } + if (!VertxContext.isOnDuplicatedContext()) { + throw new AssertionError("The method does not run on a Vert.x **duplicated** context"); + } + } + + public static void assertThatItRunsOnVirtualThread() { + // We cannot depend on a Java 20. + try { + Method isVirtual = Thread.class.getMethod("isVirtual"); + isVirtual.setAccessible(true); + boolean virtual = (Boolean) isVirtual.invoke(Thread.currentThread()); + if (!virtual) { + throw new AssertionError("Thread " + Thread.currentThread() + " is not a virtual thread"); + } + } catch (Exception e) { + throw new AssertionError( + "Thread " + Thread.currentThread() + " is not a virtual thread - cannot invoke Thread.isVirtual()", e); + } + } + + public static void assertNotOnVirtualThread() { + // We cannot depend on a Java 20. + try { + Method isVirtual = Thread.class.getMethod("isVirtual"); + isVirtual.setAccessible(true); + boolean virtual = (Boolean) isVirtual.invoke(Thread.currentThread()); + if (virtual) { + throw new AssertionError("Thread " + Thread.currentThread() + " is a virtual thread"); + } + } catch (Exception e) { + // Trying using Thread name. + var name = Thread.currentThread().toString(); + if (name.toLowerCase().contains("virtual")) { + throw new AssertionError("Thread " + Thread.currentThread() + " seems to be a virtual thread"); + } + } + } +} diff --git a/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/main/java/io/quarkus/virtual/vertx/web/Routes.java b/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/main/java/io/quarkus/virtual/vertx/web/Routes.java new file mode 100644 index 0000000000000..6e56e5494b353 --- /dev/null +++ b/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/main/java/io/quarkus/virtual/vertx/web/Routes.java @@ -0,0 +1,16 @@ +package io.quarkus.virtual.vertx.web; + +import io.quarkus.vertx.web.Route; +import io.smallrye.common.annotation.RunOnVirtualThread; + +public class Routes { + + @RunOnVirtualThread + @Route + String hello() { + AssertHelper.assertEverything(); + // Quarkus specific - each VT has a unique name + return Thread.currentThread().getName(); + } + +} diff --git a/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/main/resources/application.properties b/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/main/resources/application.properties new file mode 100644 index 0000000000000..43b1e230c2184 --- /dev/null +++ b/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.native.additional-build-args=--enable-preview + +quarkus.package.quiltflower.enabled=true \ No newline at end of file diff --git a/integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadIT.java b/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/test/java/io/quarkus/virtual/vertx/web/RunOnVirtualThreadIT.java similarity index 78% rename from integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadIT.java rename to integration-tests/virtual-threads/reactive-routes-virtual-threads/src/test/java/io/quarkus/virtual/vertx/web/RunOnVirtualThreadIT.java index 22abcdce9792e..609672a7779ef 100644 --- a/integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadIT.java +++ b/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/test/java/io/quarkus/virtual/vertx/web/RunOnVirtualThreadIT.java @@ -1,4 +1,4 @@ -package io.quarkus.virtual.mail; +package io.quarkus.virtual.vertx.web; import io.quarkus.test.junit.QuarkusIntegrationTest; diff --git a/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/test/java/io/quarkus/virtual/vertx/web/RunOnVirtualThreadTest.java b/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/test/java/io/quarkus/virtual/vertx/web/RunOnVirtualThreadTest.java new file mode 100644 index 0000000000000..041b50df5dc3e --- /dev/null +++ b/integration-tests/virtual-threads/reactive-routes-virtual-threads/src/test/java/io/quarkus/virtual/vertx/web/RunOnVirtualThreadTest.java @@ -0,0 +1,25 @@ +package io.quarkus.virtual.vertx.web; + +import static io.restassured.RestAssured.get; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit5.virtual.ShouldNotPin; +import io.quarkus.test.junit5.virtual.VirtualThreadUnit; + +@QuarkusTest +@VirtualThreadUnit +@ShouldNotPin +class RunOnVirtualThreadTest { + + @Test + void testRoute() { + String bodyStr = get("/hello").then().statusCode(200).extract().asString(); + // Each VT has a unique name in quarkus + assertNotEquals(bodyStr, get("/hello").then().statusCode(200).extract().asString()); + + } + +} diff --git a/integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/scheduler/RunOnVirtualThreadIT.java b/integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/scheduler/RunOnVirtualThreadIT.java new file mode 100644 index 0000000000000..ca669732041a8 --- /dev/null +++ b/integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/scheduler/RunOnVirtualThreadIT.java @@ -0,0 +1,8 @@ +package io.quarkus.virtual.scheduler; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class RunOnVirtualThreadIT extends RunOnVirtualThreadTest { + +} diff --git a/integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadTest.java b/integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/scheduler/RunOnVirtualThreadTest.java similarity index 97% rename from integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadTest.java rename to integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/scheduler/RunOnVirtualThreadTest.java index 2a6244806d48b..42ad3e929cf15 100644 --- a/integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadTest.java +++ b/integration-tests/virtual-threads/scheduler-virtual-threads/src/test/java/io/quarkus/virtual/scheduler/RunOnVirtualThreadTest.java @@ -1,4 +1,4 @@ -package io.quarkus.virtual.mail; +package io.quarkus.virtual.scheduler; import java.time.Duration; import java.util.List;