From 9aedcc504ddc439bc8938182a6db96eb97955dff Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Mon, 24 Jul 2023 15:19:11 +0200 Subject: [PATCH] Verify the behavior of virtual threads - RESTEasy Reactive (check dispatching strategy, filters, no pinning...) - Reactive rest client (no pinning) - gRPC client (no pinning) - Mailer (both simple and template) (no pinning) - Redis (client and cache) (no pinning) --- .github/workflows/ci-actions-incremental.yml | 2 +- .../disable-native-profile | 1 - .../grpc-virtual-threads/pom.xml | 12 +- .../grpc/example/streaming/Endpoint.java | 30 +++++ .../src/main/resources/application.properties | 16 +-- .../grpc/example/streaming/NoPinningIT.java | 76 ++++++++++++ .../streaming/VirtualThreadTestBase.java | 10 ++ .../mailer-virtual-threads/pom.xml | 95 +++++++++++++++ .../io/quarkus/virtual/mail/AssertHelper.java | 71 +++++++++++ .../quarkus/virtual/mail/MailerResource.java | 43 +++++++ .../src/main/resources/application.properties | 3 + .../templates/MailerResource/hello.txt | 1 + .../quarkus/virtual/mail/MailHogResource.java | 35 ++++++ .../io/quarkus/virtual/mail/NoPinningIT.java | 76 ++++++++++++ .../virtual/mail/RunOnVirtualThreadIT.java | 8 ++ .../virtual/mail/RunOnVirtualThreadTest.java | 61 ++++++++++ integration-tests/virtual-threads/pom.xml | 111 +++++++++++++----- .../redis-virtual-threads/pom.xml | 107 +++++++++++++++++ .../quarkus/virtual/redis/AssertHelper.java | 71 +++++++++++ .../quarkus/virtual/redis/RedisResource.java | 43 +++++++ .../src/main/resources/application.properties | 4 + .../io/quarkus/virtual/redis/NoPinningIT.java | 76 ++++++++++++ .../virtual/redis/RunOnVirtualThreadIT.java | 8 ++ .../virtual/redis/RunOnVirtualThreadTest.java | 33 ++++++ .../pom.xml | 90 ++++++++++++++ .../io/quarkus/virtual/rest/AssertHelper.java | 71 +++++++++++ .../io/quarkus/virtual/rest/Greeting.java | 7 ++ .../virtual/rest/RestClientResource.java | 23 ++++ .../io/quarkus/virtual/rest/ServiceApi.java | 16 +++ .../quarkus/virtual/rest/ServiceClient.java | 13 ++ .../src/main/resources/application.properties | 1 + .../io/quarkus/virtual/rest/NoPinningIT.java | 76 ++++++++++++ .../virtual/rest/RunOnVirtualThreadIT.java | 8 ++ .../virtual/rest/RunOnVirtualThreadTest.java | 20 ++++ .../resteasy-reactive-virtual-threads/pom.xml | 72 ++++++++++++ .../io/quarkus/virtual/rr/AssertHelper.java | 71 +++++++++++ .../java/io/quarkus/virtual/rr/Counter.java | 16 +++ .../quarkus/virtual/rr/FilteredResource.java | 40 +++++++ .../java/io/quarkus/virtual/rr/Filters.java | 37 ++++++ .../io/quarkus/virtual/rr/MyResource.java | 72 ++++++++++++ .../virtual/rr/MyResourceWithVTOnClass.java | 49 ++++++++ .../src/main/resources/application.properties | 1 + .../io/quarkus/virtual/rr/NoPinningIT.java | 76 ++++++++++++ .../virtual/rr/RunOnVirtualThreadIT.java | 8 ++ .../virtual/rr/RunOnVirtualThreadTest.java | 99 ++++++++++++++++ 45 files changed, 1806 insertions(+), 53 deletions(-) delete mode 100644 integration-tests/virtual-threads/grpc-virtual-threads/disable-native-profile create mode 100644 integration-tests/virtual-threads/grpc-virtual-threads/src/main/java/io/quarkus/grpc/example/streaming/Endpoint.java create mode 100644 integration-tests/virtual-threads/grpc-virtual-threads/src/test/java/io/quarkus/grpc/example/streaming/NoPinningIT.java create mode 100644 integration-tests/virtual-threads/mailer-virtual-threads/pom.xml create mode 100644 integration-tests/virtual-threads/mailer-virtual-threads/src/main/java/io/quarkus/virtual/mail/AssertHelper.java create mode 100644 integration-tests/virtual-threads/mailer-virtual-threads/src/main/java/io/quarkus/virtual/mail/MailerResource.java create mode 100644 integration-tests/virtual-threads/mailer-virtual-threads/src/main/resources/application.properties create mode 100644 integration-tests/virtual-threads/mailer-virtual-threads/src/main/resources/templates/MailerResource/hello.txt create mode 100644 integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/MailHogResource.java create mode 100644 integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/NoPinningIT.java create mode 100644 integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadIT.java create mode 100644 integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadTest.java create mode 100644 integration-tests/virtual-threads/redis-virtual-threads/pom.xml create mode 100644 integration-tests/virtual-threads/redis-virtual-threads/src/main/java/io/quarkus/virtual/redis/AssertHelper.java create mode 100644 integration-tests/virtual-threads/redis-virtual-threads/src/main/java/io/quarkus/virtual/redis/RedisResource.java create mode 100644 integration-tests/virtual-threads/redis-virtual-threads/src/main/resources/application.properties create mode 100644 integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/NoPinningIT.java create mode 100644 integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/RunOnVirtualThreadIT.java create mode 100644 integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/RunOnVirtualThreadTest.java create mode 100644 integration-tests/virtual-threads/rest-client-reactive-virtual-threads/pom.xml create mode 100644 integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/AssertHelper.java create mode 100644 integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/Greeting.java create mode 100644 integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/RestClientResource.java create mode 100644 integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/ServiceApi.java create mode 100644 integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/ServiceClient.java create mode 100644 integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/resources/application.properties create mode 100644 integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/NoPinningIT.java create mode 100644 integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/RunOnVirtualThreadIT.java create mode 100644 integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/RunOnVirtualThreadTest.java create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/pom.xml create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/AssertHelper.java create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/Counter.java create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/FilteredResource.java create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/Filters.java create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/MyResource.java create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/MyResourceWithVTOnClass.java create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/resources/application.properties create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/NoPinningIT.java create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadIT.java create mode 100644 integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadTest.java diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index b25fbfbc961b0..05122939c3143 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -636,7 +636,7 @@ jobs: java-version: ${{ matrix.java.java-version }} - name: Run tests run: | - export LANG=en_US && ./mvnw -e -B -fae --settings .github/mvn-settings.xml -f integration-tests/virtual-threads clean verify -Dextra-args=${{matrix.java.extra-args}} + export LANG=en_US && ./mvnw -e -B -fae --settings .github/mvn-settings.xml -f integration-tests/virtual-threads clean verify -Dnative -Dextra-args=${{matrix.java.extra-args}} -Dquarkus.native.container-build=true -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-20 - name: Upload build reports (if build failed) uses: actions/upload-artifact@v3 if: ${{ failure() || cancelled() }} diff --git a/integration-tests/virtual-threads/grpc-virtual-threads/disable-native-profile b/integration-tests/virtual-threads/grpc-virtual-threads/disable-native-profile deleted file mode 100644 index 011a7cc4571d5..0000000000000 --- a/integration-tests/virtual-threads/grpc-virtual-threads/disable-native-profile +++ /dev/null @@ -1 +0,0 @@ -This file disables the native profile in the parent pom.xml of this module. \ No newline at end of file diff --git a/integration-tests/virtual-threads/grpc-virtual-threads/pom.xml b/integration-tests/virtual-threads/grpc-virtual-threads/pom.xml index b5988c620cc65..266391bf2da6a 100644 --- a/integration-tests/virtual-threads/grpc-virtual-threads/pom.xml +++ b/integration-tests/virtual-threads/grpc-virtual-threads/pom.xml @@ -84,14 +84,10 @@ io.quarkus quarkus-maven-plugin - - - - generate-code - build - - - + + + org.apache.maven.plugins + maven-surefire-plugin diff --git a/integration-tests/virtual-threads/grpc-virtual-threads/src/main/java/io/quarkus/grpc/example/streaming/Endpoint.java b/integration-tests/virtual-threads/grpc-virtual-threads/src/main/java/io/quarkus/grpc/example/streaming/Endpoint.java new file mode 100644 index 0000000000000..024eaa62a842f --- /dev/null +++ b/integration-tests/virtual-threads/grpc-virtual-threads/src/main/java/io/quarkus/grpc/example/streaming/Endpoint.java @@ -0,0 +1,30 @@ +package io.quarkus.grpc.example.streaming; + +import java.time.Duration; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import com.google.protobuf.ByteString; + +import io.grpc.testing.integration.Messages; +import io.grpc.testing.integration.TestService; +import io.quarkus.grpc.GrpcClient; +import io.smallrye.common.annotation.RunOnVirtualThread; + +@Path("/endpoint") +public class Endpoint { + + @GrpcClient("service") + TestService service; + + @GET + @RunOnVirtualThread + public String invokeGrpcService() { + var req = Messages.SimpleRequest.newBuilder() + .setPayload(Messages.Payload.newBuilder().setBody(ByteString.copyFromUtf8("hello")).build()) + .build(); + return service.unaryCall(req).await().atMost(Duration.ofSeconds(5)) + .getPayload().getBody().toStringUtf8(); + } +} diff --git a/integration-tests/virtual-threads/grpc-virtual-threads/src/main/resources/application.properties b/integration-tests/virtual-threads/grpc-virtual-threads/src/main/resources/application.properties index 011cd24e1144d..c5e8d4afaaaf6 100644 --- a/integration-tests/virtual-threads/grpc-virtual-threads/src/main/resources/application.properties +++ b/integration-tests/virtual-threads/grpc-virtual-threads/src/main/resources/application.properties @@ -1,15 +1,9 @@ -quarkus.grpc.clients.streaming.host=localhost -quarkus.grpc.clients.streaming.port=9001 +quarkus.grpc.clients.service.host=localhost +quarkus.grpc.clients.service.port=9001 -%vertx.quarkus.grpc.clients.streaming.port=8081 -%vertx.quarkus.grpc.clients.streaming.use-quarkus-grpc-client=true %vertx.quarkus.grpc.server.use-separate-server=false -%n2o.quarkus.grpc.server.use-separate-server=true -%o2n.quarkus.grpc.server.use-separate-server=false +quarkus.native.additional-build-args=--enable-preview -%n2o.quarkus.grpc.clients.streaming.port=9001 -%n2o.quarkus.grpc.clients.streaming.use-quarkus-grpc-client=true - -%o2n.quarkus.grpc.clients.streaming.port=8081 -%o2n.quarkus.grpc.clients.streaming.use-quarkus-grpc-client=false +%vertx.quarkus.grpc.clients.service.port=8081 +%vertx.quarkus.grpc.clients.service.use-quarkus-grpc-client=true diff --git a/integration-tests/virtual-threads/grpc-virtual-threads/src/test/java/io/quarkus/grpc/example/streaming/NoPinningIT.java b/integration-tests/virtual-threads/grpc-virtual-threads/src/test/java/io/quarkus/grpc/example/streaming/NoPinningIT.java new file mode 100644 index 0000000000000..459909827872f --- /dev/null +++ b/integration-tests/virtual-threads/grpc-virtual-threads/src/test/java/io/quarkus/grpc/example/streaming/NoPinningIT.java @@ -0,0 +1,76 @@ +package io.quarkus.grpc.example.streaming; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * An integration test reading the output of the unit test to verify that no tests where pinning the carrier thread. + * It reads the reports generated by surefire. + */ +public class NoPinningIT { + + @Test + void verify() throws IOException, ParserConfigurationException, SAXException { + var reports = new File("target", "surefire-reports"); + Assertions.assertTrue(reports.isDirectory(), + "Unable to find " + reports.getAbsolutePath() + ", did you run the tests with Maven before?"); + var list = reports.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.startsWith("TEST") && name.endsWith("Test.xml"); + } + }); + Assertions.assertNotNull(list, + "Unable to find " + reports.getAbsolutePath() + ", did you run the tests with Maven before?"); + + for (File report : list) { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(report); + var suite = document.getFirstChild(); + var cases = getChildren(suite.getChildNodes(), "testcase"); + for (Node c : cases) { + verify(report, c); + } + } + + } + + private void verify(File file, Node ca) { + var fullname = ca.getAttributes().getNamedItem("classname").getTextContent() + "." + + ca.getAttributes().getNamedItem("name").getTextContent(); + var output = getChildren(ca.getChildNodes(), "system-out"); + if (output.isEmpty()) { + return; + } + var sout = output.get(0).getTextContent(); + if (sout.contains("VThreadContinuation.onPinned")) { + throw new AssertionError("The test case " + fullname + " pinned the carrier thread, check " + file.getAbsolutePath() + + " for details (or the log of the test)"); + } + + } + + private List getChildren(NodeList nodes, String name) { + List list = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + var node = nodes.item(i); + if (node.getNodeName().equalsIgnoreCase(name)) { + list.add(node); + } + } + return list; + } + +} diff --git a/integration-tests/virtual-threads/grpc-virtual-threads/src/test/java/io/quarkus/grpc/example/streaming/VirtualThreadTestBase.java b/integration-tests/virtual-threads/grpc-virtual-threads/src/test/java/io/quarkus/grpc/example/streaming/VirtualThreadTestBase.java index 461cfa4441ba9..8aa995635dc75 100644 --- a/integration-tests/virtual-threads/grpc-virtual-threads/src/test/java/io/quarkus/grpc/example/streaming/VirtualThreadTestBase.java +++ b/integration-tests/virtual-threads/grpc-virtual-threads/src/test/java/io/quarkus/grpc/example/streaming/VirtualThreadTestBase.java @@ -1,6 +1,7 @@ package io.quarkus.grpc.example.streaming; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.is; import java.util.concurrent.atomic.AtomicInteger; @@ -12,6 +13,7 @@ import io.grpc.testing.integration.Messages; import io.grpc.testing.integration.TestServiceGrpc; import io.quarkus.grpc.GrpcClient; +import io.restassured.RestAssured; @SuppressWarnings("NewClassNamingConvention") public class VirtualThreadTestBase { @@ -45,4 +47,12 @@ void testStreamingOutputCall() { assertThat(count).hasValue(3); } + @Test + void testGrpcClient() { + RestAssured.get("/endpoint") + .then() + .statusCode(200) + .body(is("HELLO")); + } + } diff --git a/integration-tests/virtual-threads/mailer-virtual-threads/pom.xml b/integration-tests/virtual-threads/mailer-virtual-threads/pom.xml new file mode 100644 index 0000000000000..e567f6a56f49e --- /dev/null +++ b/integration-tests/virtual-threads/mailer-virtual-threads/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + + quarkus-virtual-threads-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-virtual-threads-mailer + Quarkus - Integration Tests - Virtual Threads - Mailer + + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-mailer + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + org.testcontainers + testcontainers + test + + + + + + io.quarkus + quarkus-resteasy-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-mailer-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + diff --git a/integration-tests/virtual-threads/mailer-virtual-threads/src/main/java/io/quarkus/virtual/mail/AssertHelper.java b/integration-tests/virtual-threads/mailer-virtual-threads/src/main/java/io/quarkus/virtual/mail/AssertHelper.java new file mode 100644 index 0000000000000..d89a517af7ec8 --- /dev/null +++ b/integration-tests/virtual-threads/mailer-virtual-threads/src/main/java/io/quarkus/virtual/mail/AssertHelper.java @@ -0,0 +1,71 @@ +package io.quarkus.virtual.mail; + +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/mailer-virtual-threads/src/main/java/io/quarkus/virtual/mail/MailerResource.java b/integration-tests/virtual-threads/mailer-virtual-threads/src/main/java/io/quarkus/virtual/mail/MailerResource.java new file mode 100644 index 0000000000000..83ab7da484334 --- /dev/null +++ b/integration-tests/virtual-threads/mailer-virtual-threads/src/main/java/io/quarkus/virtual/mail/MailerResource.java @@ -0,0 +1,43 @@ +package io.quarkus.virtual.mail; + +import java.time.Duration; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.quarkus.mailer.Mail; +import io.quarkus.mailer.MailTemplate; +import io.quarkus.mailer.Mailer; +import io.quarkus.qute.CheckedTemplate; +import io.smallrye.common.annotation.RunOnVirtualThread; + +@Path("/") +@RunOnVirtualThread +public class MailerResource { + + @Inject + Mailer mailer; + + @GET + public String send() { + AssertHelper.assertEverything(); + mailer.send(Mail.withText("roger-the-robot@quarkus.io", "test simple", "This email is sent from a virtual thread")); + return "OK"; + } + + @GET + @Path("/template") + public String sendWithTemplate() { + AssertHelper.assertEverything(); + Templates.hello("virtual threads").to("roger-the-robot@quarkus.io").subject("test template").send().await() + .atMost(Duration.ofSeconds(3)); + return "OK"; + } + + @CheckedTemplate + static class Templates { + public static native MailTemplate.MailTemplateInstance hello(String name); + } + +} diff --git a/integration-tests/virtual-threads/mailer-virtual-threads/src/main/resources/application.properties b/integration-tests/virtual-threads/mailer-virtual-threads/src/main/resources/application.properties new file mode 100644 index 0000000000000..182aeaefac333 --- /dev/null +++ b/integration-tests/virtual-threads/mailer-virtual-threads/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.native.additional-build-args=--enable-preview +quarkus.mailer.mock=false +quarkus.mailer.from=roger-the-robot@quarkus.io \ No newline at end of file diff --git a/integration-tests/virtual-threads/mailer-virtual-threads/src/main/resources/templates/MailerResource/hello.txt b/integration-tests/virtual-threads/mailer-virtual-threads/src/main/resources/templates/MailerResource/hello.txt new file mode 100644 index 0000000000000..9b65c69298d8a --- /dev/null +++ b/integration-tests/virtual-threads/mailer-virtual-threads/src/main/resources/templates/MailerResource/hello.txt @@ -0,0 +1 @@ +Hello {name}! \ No newline at end of file diff --git a/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/MailHogResource.java b/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/MailHogResource.java new file mode 100644 index 0000000000000..b100917de9a8e --- /dev/null +++ b/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/MailHogResource.java @@ -0,0 +1,35 @@ +package io.quarkus.virtual.mail; + +import java.util.Map; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class MailHogResource implements QuarkusTestResourceLifecycleManager { + + private static final String IMAGE = "mailhog/mailhog"; + private static final int SMTP_PORT = 1025; + private static final int HTTP_PORT = 8025; + private GenericContainer container; + + @Override + public Map start() { + container = new GenericContainer<>(IMAGE) + .withExposedPorts(1025, 8025) + .waitingFor(new LogMessageWaitStrategy().withTimes(1).withRegEx(".*Serving under.*")); + container.start(); + + return Map.of("quarkus.mailer.host", container.getHost(), + "quarkus.mailer.port", Integer.toString(container.getMappedPort(SMTP_PORT)), + "mailhog.url", "http://" + container.getHost() + ":" + container.getMappedPort(HTTP_PORT) + "/api/v2/messages"); + } + + @Override + public void stop() { + if (container != null) { + container.stop(); + } + } +} diff --git a/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/NoPinningIT.java b/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/NoPinningIT.java new file mode 100644 index 0000000000000..58ac0ddf09f59 --- /dev/null +++ b/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/NoPinningIT.java @@ -0,0 +1,76 @@ +package io.quarkus.virtual.mail; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * An integration test reading the output of the unit test to verify that no tests where pinning the carrier thread. + * It reads the reports generated by surefire. + */ +public class NoPinningIT { + + @Test + void verify() throws IOException, ParserConfigurationException, SAXException { + var reports = new File("target", "surefire-reports"); + Assertions.assertTrue(reports.isDirectory(), + "Unable to find " + reports.getAbsolutePath() + ", did you run the tests with Maven before?"); + var list = reports.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.startsWith("TEST") && name.endsWith("Test.xml"); + } + }); + Assertions.assertNotNull(list, + "Unable to find " + reports.getAbsolutePath() + ", did you run the tests with Maven before?"); + + for (File report : list) { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(report); + var suite = document.getFirstChild(); + var cases = getChildren(suite.getChildNodes(), "testcase"); + for (Node c : cases) { + verify(report, c); + } + } + + } + + private void verify(File file, Node ca) { + var fullname = ca.getAttributes().getNamedItem("classname").getTextContent() + "." + + ca.getAttributes().getNamedItem("name").getTextContent(); + var output = getChildren(ca.getChildNodes(), "system-out"); + if (output.isEmpty()) { + return; + } + var sout = output.get(0).getTextContent(); + if (sout.contains("VThreadContinuation.onPinned")) { + throw new AssertionError("The test case " + fullname + " pinned the carrier thread, check " + file.getAbsolutePath() + + " for details (or the log of the test)"); + } + + } + + private List getChildren(NodeList nodes, String name) { + List list = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + var node = nodes.item(i); + if (node.getNodeName().equalsIgnoreCase(name)) { + list.add(node); + } + } + return list; + } + +} diff --git a/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadIT.java b/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadIT.java new file mode 100644 index 0000000000000..22abcdce9792e --- /dev/null +++ b/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadIT.java @@ -0,0 +1,8 @@ +package io.quarkus.virtual.mail; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class RunOnVirtualThreadIT extends RunOnVirtualThreadTest { + +} diff --git a/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadTest.java b/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadTest.java new file mode 100644 index 0000000000000..d179f04cea365 --- /dev/null +++ b/integration-tests/virtual-threads/mailer-virtual-threads/src/test/java/io/quarkus/virtual/mail/RunOnVirtualThreadTest.java @@ -0,0 +1,61 @@ +package io.quarkus.virtual.mail; + +import static org.hamcrest.Matchers.is; + +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.config.ConfigProvider; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +@QuarkusTest +@QuarkusTestResource(MailHogResource.class) +class RunOnVirtualThreadTest { + + @Test + void test() { + RestAssured.get().then() + .assertThat().statusCode(200) + .body(is("OK")); + + var url = ConfigProvider.getConfig().getValue("mailhog.url", String.class); + var content = RestAssured.get(url).thenReturn().asPrettyString(); + + JsonObject json = new JsonObject(content); + String body = findMessageBody("test simple", json.getJsonArray("items")); + Assertions.assertThat(body).isEqualTo("This email is sent from a virtual thread"); + } + + @Test + void testWithTemplate() { + RestAssured.get("/template").then() + .assertThat().statusCode(200) + .body(is("OK")); + + var url = ConfigProvider.getConfig().getValue("mailhog.url", String.class); + var content = RestAssured.get(url).thenReturn().asPrettyString(); + + JsonObject json = new JsonObject(content); + String body = findMessageBody("test template", json.getJsonArray("items")); + Assertions.assertThat(body).isEqualTo("Hello virtual threads!"); + } + + private String findMessageBody(String subject, JsonArray items) { + for (Object item : items) { + var json = (JsonObject) item; + var content = json.getJsonObject("Content"); + if (content != null) { + var subjects = content.getJsonObject("Headers").getJsonArray("Subject"); + if (subjects != null && subject.equals(subjects.getString(0))) { + return content.getString("Body"); + } + } + } + return null; + } + +} diff --git a/integration-tests/virtual-threads/pom.xml b/integration-tests/virtual-threads/pom.xml index f478d14d2bf77..7d84384f550bb 100644 --- a/integration-tests/virtual-threads/pom.xml +++ b/integration-tests/virtual-threads/pom.xml @@ -64,33 +64,40 @@ true + + net.revelc.code + impsort-maven-plugin + + + sort-imports + + sort + + + + + ${impsort.skip} + + - - io.quarkus - quarkus-maven-plugin - ${project.version} - - true - ${quarkus.build.skip} - - org.apache.maven.plugins maven-surefire-plugin - - org.junit.jupiter.api.ClassOrderer$OrderAnnotation - + org.jboss.logmanager.LogManager + ${maven.home} + --enable-preview -Djdk.tracePinnedThreads + ${skipTests} - ${quarkus.platform.group-id} + io.quarkus quarkus-maven-plugin - ${quarkus.platform.version} + ${project.version} true @@ -101,6 +108,10 @@ + + true + ${quarkus.build.skip} + org.apache.maven.plugins @@ -113,19 +124,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - ${version.surefire.plugin} - - - org.jboss.logmanager.LogManager - ${maven.home} - - --enable-preview -Djdk.tracePinnedThreads - ${skipTests} - - maven-failsafe-plugin ${version.surefire.plugin} @@ -206,17 +204,66 @@ [20,) + + grpc-virtual-threads + resteasy-reactive-virtual-threads + mailer-virtual-threads + redis-virtual-threads + rest-client-reactive-virtual-threads + + + 20 + true + + - test-modules + native - !no-test-modules + native + + ${basedir}/disable-native-profile + - - grpc-virtual-threads - + + native + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${native.surefire.skip} + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + diff --git a/integration-tests/virtual-threads/redis-virtual-threads/pom.xml b/integration-tests/virtual-threads/redis-virtual-threads/pom.xml new file mode 100644 index 0000000000000..0ac0cd3aef8b5 --- /dev/null +++ b/integration-tests/virtual-threads/redis-virtual-threads/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + + quarkus-virtual-threads-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-virtual-threads-redis + Quarkus - Integration Tests - Virtual Threads - Redis Client and Cache + + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-redis-cache + + + io.quarkus + quarkus-redis-client + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + + + + io.quarkus + quarkus-resteasy-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-redis-cache-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-redis-client-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + diff --git a/integration-tests/virtual-threads/redis-virtual-threads/src/main/java/io/quarkus/virtual/redis/AssertHelper.java b/integration-tests/virtual-threads/redis-virtual-threads/src/main/java/io/quarkus/virtual/redis/AssertHelper.java new file mode 100644 index 0000000000000..f58f5d7a63ada --- /dev/null +++ b/integration-tests/virtual-threads/redis-virtual-threads/src/main/java/io/quarkus/virtual/redis/AssertHelper.java @@ -0,0 +1,71 @@ +package io.quarkus.virtual.redis; + +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/redis-virtual-threads/src/main/java/io/quarkus/virtual/redis/RedisResource.java b/integration-tests/virtual-threads/redis-virtual-threads/src/main/java/io/quarkus/virtual/redis/RedisResource.java new file mode 100644 index 0000000000000..998951142a742 --- /dev/null +++ b/integration-tests/virtual-threads/redis-virtual-threads/src/main/java/io/quarkus/virtual/redis/RedisResource.java @@ -0,0 +1,43 @@ +package io.quarkus.virtual.redis; + +import java.util.UUID; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.quarkus.cache.CacheResult; +import io.quarkus.redis.datasource.RedisDataSource; +import io.quarkus.redis.datasource.hash.HashCommands; +import io.smallrye.common.annotation.RunOnVirtualThread; + +@Path("/") +@RunOnVirtualThread +public class RedisResource { + + private final HashCommands hash; + + public RedisResource(RedisDataSource ds) { + hash = ds.hash(String.class); + } + + @GET + public String testRedis() { + AssertHelper.assertEverything(); + String value = UUID.randomUUID().toString(); + hash.hset("test", "test", value); + + var retrieved = hash.hget("test", "test"); + if (!retrieved.equals(value)) { + throw new IllegalStateException("Something wrong happened: " + retrieved + " != " + value); + } + return "OK"; + } + + @GET + @CacheResult(cacheName = "my-cache") + @Path("/cached") + public String testCache() { + return UUID.randomUUID().toString(); + } + +} diff --git a/integration-tests/virtual-threads/redis-virtual-threads/src/main/resources/application.properties b/integration-tests/virtual-threads/redis-virtual-threads/src/main/resources/application.properties new file mode 100644 index 0000000000000..651500d04fd6d --- /dev/null +++ b/integration-tests/virtual-threads/redis-virtual-threads/src/main/resources/application.properties @@ -0,0 +1,4 @@ +quarkus.native.additional-build-args=--enable-preview + +quarkus.cache.redis.value-type=java.lang.String +quarkus.cache.redis.ttl=10s \ No newline at end of file diff --git a/integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/NoPinningIT.java b/integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/NoPinningIT.java new file mode 100644 index 0000000000000..420d0200e2c6f --- /dev/null +++ b/integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/NoPinningIT.java @@ -0,0 +1,76 @@ +package io.quarkus.virtual.redis; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * An integration test reading the output of the unit test to verify that no tests where pinning the carrier thread. + * It reads the reports generated by surefire. + */ +public class NoPinningIT { + + @Test + void verify() throws IOException, ParserConfigurationException, SAXException { + var reports = new File("target", "surefire-reports"); + Assertions.assertTrue(reports.isDirectory(), + "Unable to find " + reports.getAbsolutePath() + ", did you run the tests with Maven before?"); + var list = reports.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.startsWith("TEST") && name.endsWith("Test.xml"); + } + }); + Assertions.assertNotNull(list, + "Unable to find " + reports.getAbsolutePath() + ", did you run the tests with Maven before?"); + + for (File report : list) { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(report); + var suite = document.getFirstChild(); + var cases = getChildren(suite.getChildNodes(), "testcase"); + for (Node c : cases) { + verify(report, c); + } + } + + } + + private void verify(File file, Node ca) { + var fullname = ca.getAttributes().getNamedItem("classname").getTextContent() + "." + + ca.getAttributes().getNamedItem("name").getTextContent(); + var output = getChildren(ca.getChildNodes(), "system-out"); + if (output.isEmpty()) { + return; + } + var sout = output.get(0).getTextContent(); + if (sout.contains("VThreadContinuation.onPinned")) { + throw new AssertionError("The test case " + fullname + " pinned the carrier thread, check " + file.getAbsolutePath() + + " for details (or the log of the test)"); + } + + } + + private List getChildren(NodeList nodes, String name) { + List list = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + var node = nodes.item(i); + if (node.getNodeName().equalsIgnoreCase(name)) { + list.add(node); + } + } + return list; + } + +} diff --git a/integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/RunOnVirtualThreadIT.java b/integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/RunOnVirtualThreadIT.java new file mode 100644 index 0000000000000..fd56d5f0942aa --- /dev/null +++ b/integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/RunOnVirtualThreadIT.java @@ -0,0 +1,8 @@ +package io.quarkus.virtual.redis; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class RunOnVirtualThreadIT extends RunOnVirtualThreadTest { + +} diff --git a/integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/RunOnVirtualThreadTest.java b/integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/RunOnVirtualThreadTest.java new file mode 100644 index 0000000000000..0b45efdd598d8 --- /dev/null +++ b/integration-tests/virtual-threads/redis-virtual-threads/src/test/java/io/quarkus/virtual/redis/RunOnVirtualThreadTest.java @@ -0,0 +1,33 @@ +package io.quarkus.virtual.redis; + +import static org.hamcrest.Matchers.is; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +class RunOnVirtualThreadTest { + + @Test + void test() { + RestAssured.get().then() + .assertThat().statusCode(200) + .body(is("OK")); + } + + @Test + void testCache() { + var value = RestAssured.get("/cached").then() + .assertThat().statusCode(200) + .extract().asPrettyString(); + + var value2 = RestAssured.get("/cached").then() + .assertThat().statusCode(200) + .extract().asPrettyString(); + + Assertions.assertThat(value).isEqualTo(value2); + } +} diff --git a/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/pom.xml b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/pom.xml new file mode 100644 index 0000000000000..263ea8ad3342d --- /dev/null +++ b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + + quarkus-virtual-threads-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-virtual-threads-rest-client-reactive + Quarkus - Integration Tests - Virtual Threads - REST Client Reactive + + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-rest-client-reactive-jackson + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + + + + io.quarkus + quarkus-resteasy-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-client-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + diff --git a/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/AssertHelper.java b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/AssertHelper.java new file mode 100644 index 0000000000000..9d9d971e41b93 --- /dev/null +++ b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/AssertHelper.java @@ -0,0 +1,71 @@ +package io.quarkus.virtual.rest; + +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/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/Greeting.java b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/Greeting.java new file mode 100644 index 0000000000000..780f2aa2b15d7 --- /dev/null +++ b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/Greeting.java @@ -0,0 +1,7 @@ +package io.quarkus.virtual.rest; + +public class Greeting { + + public String message; + +} diff --git a/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/RestClientResource.java b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/RestClientResource.java new file mode 100644 index 0000000000000..04658da72be13 --- /dev/null +++ b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/RestClientResource.java @@ -0,0 +1,23 @@ +package io.quarkus.virtual.rest; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.smallrye.common.annotation.RunOnVirtualThread; + +@Path("/") +@RunOnVirtualThread +public class RestClientResource { + + @RestClient + ServiceClient client; + + @GET + public Greeting test() { + AssertHelper.assertEverything(); + return client.hello(); + } + +} diff --git a/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/ServiceApi.java b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/ServiceApi.java new file mode 100644 index 0000000000000..4d41f7b7f7f73 --- /dev/null +++ b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/ServiceApi.java @@ -0,0 +1,16 @@ +package io.quarkus.virtual.rest; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/api") +public class ServiceApi { + + @GET + public Greeting hello() { + Greeting greeting = new Greeting(); + greeting.message = "hello"; + return greeting; + } + +} diff --git a/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/ServiceClient.java b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/ServiceClient.java new file mode 100644 index 0000000000000..622352ac9876a --- /dev/null +++ b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rest/ServiceClient.java @@ -0,0 +1,13 @@ +package io.quarkus.virtual.rest; + +import jakarta.ws.rs.GET; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(baseUri = "http://localhost:8081/api") +public interface ServiceClient { + + @GET + Greeting hello(); + +} diff --git a/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/resources/application.properties b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/resources/application.properties new file mode 100644 index 0000000000000..0bdbbf4a0cfe7 --- /dev/null +++ b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.native.additional-build-args=--enable-preview diff --git a/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/NoPinningIT.java b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/NoPinningIT.java new file mode 100644 index 0000000000000..0124c23c7be9b --- /dev/null +++ b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/NoPinningIT.java @@ -0,0 +1,76 @@ +package io.quarkus.virtual.rest; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * An integration test reading the output of the unit test to verify that no tests where pinning the carrier thread. + * It reads the reports generated by surefire. + */ +public class NoPinningIT { + + @Test + void verify() throws IOException, ParserConfigurationException, SAXException { + var reports = new File("target", "surefire-reports"); + Assertions.assertTrue(reports.isDirectory(), + "Unable to find " + reports.getAbsolutePath() + ", did you run the tests with Maven before?"); + var list = reports.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.startsWith("TEST") && name.endsWith("Test.xml"); + } + }); + Assertions.assertNotNull(list, + "Unable to find " + reports.getAbsolutePath() + ", did you run the tests with Maven before?"); + + for (File report : list) { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(report); + var suite = document.getFirstChild(); + var cases = getChildren(suite.getChildNodes(), "testcase"); + for (Node c : cases) { + verify(report, c); + } + } + + } + + private void verify(File file, Node ca) { + var fullname = ca.getAttributes().getNamedItem("classname").getTextContent() + "." + + ca.getAttributes().getNamedItem("name").getTextContent(); + var output = getChildren(ca.getChildNodes(), "system-out"); + if (output.isEmpty()) { + return; + } + var sout = output.get(0).getTextContent(); + if (sout.contains("VThreadContinuation.onPinned")) { + throw new AssertionError("The test case " + fullname + " pinned the carrier thread, check " + file.getAbsolutePath() + + " for details (or the log of the test)"); + } + + } + + private List getChildren(NodeList nodes, String name) { + List list = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + var node = nodes.item(i); + if (node.getNodeName().equalsIgnoreCase(name)) { + list.add(node); + } + } + return list; + } + +} diff --git a/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/RunOnVirtualThreadIT.java b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/RunOnVirtualThreadIT.java new file mode 100644 index 0000000000000..7ee8d56ec3d4a --- /dev/null +++ b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/RunOnVirtualThreadIT.java @@ -0,0 +1,8 @@ +package io.quarkus.virtual.rest; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class RunOnVirtualThreadIT extends RunOnVirtualThreadTest { + +} diff --git a/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/RunOnVirtualThreadTest.java b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/RunOnVirtualThreadTest.java new file mode 100644 index 0000000000000..a1b5318ba225a --- /dev/null +++ b/integration-tests/virtual-threads/rest-client-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rest/RunOnVirtualThreadTest.java @@ -0,0 +1,20 @@ +package io.quarkus.virtual.rest; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +class RunOnVirtualThreadTest { + + @Test + void test() { + RestAssured.get().then() + .assertThat().statusCode(200) + .body("message", is("hello")); + } + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/pom.xml b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/pom.xml new file mode 100644 index 0000000000000..2eff569b5e35d --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + + quarkus-virtual-threads-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-virtual-threads-reseteasy-reactive + Quarkus - Integration Tests - Virtual Threads - RESTEasy Reactive + + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.assertj + assertj-core + test + + + + + io.quarkus + quarkus-resteasy-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/AssertHelper.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/AssertHelper.java new file mode 100644 index 0000000000000..aaed9cbcb7c08 --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/AssertHelper.java @@ -0,0 +1,71 @@ +package io.quarkus.virtual.rr; + +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/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/Counter.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/Counter.java new file mode 100644 index 0000000000000..7b402e5f93c34 --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/Counter.java @@ -0,0 +1,16 @@ +package io.quarkus.virtual.rr; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.RequestScoped; + +@RequestScoped +public class Counter { + + private final AtomicInteger counter = new AtomicInteger(); + + public int increment() { + return counter.incrementAndGet(); + } + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/FilteredResource.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/FilteredResource.java new file mode 100644 index 0000000000000..ca3e2db5a18b6 --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/FilteredResource.java @@ -0,0 +1,40 @@ +package io.quarkus.virtual.rr; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +import org.jboss.logmanager.MDC; + +import io.smallrye.common.annotation.RunOnVirtualThread; +import io.vertx.core.Vertx; + +@Path("/filter") +public class FilteredResource { + + @Inject + Counter counter; + + @GET + @RunOnVirtualThread + public Response filtered() { + AssertHelper.assertEverything(); + + // Request scope + assert counter.increment() == 2; + + // DC + assert Vertx.currentContext().getLocal("filter").equals("test"); + Vertx.currentContext().putLocal("test", "test test"); + + // MDC + assert MDC.get("mdc").equals("test"); + MDC.put("mdc", "test test"); + + return Response.ok() + .header("X-filter", "true") + .entity("ok") + .build(); + } +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/Filters.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/Filters.java new file mode 100644 index 0000000000000..214e886f58917 --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/Filters.java @@ -0,0 +1,37 @@ +package io.quarkus.virtual.rr; + +import jakarta.enterprise.inject.spi.CDI; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; + +import org.jboss.logmanager.MDC; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; + +import io.vertx.core.Vertx; + +public class Filters { + @ServerRequestFilter(nonBlocking = true) + public void request(ContainerRequestContext requestContext) { + if (requestContext.getUriInfo().getPath().contains("/filter")) { + AssertHelper.assertNotOnVirtualThread(); + AssertHelper.assertThatItRunsOnADuplicatedContext(); + AssertHelper.assertThatTheRequestScopeIsActive(); + MDC.put("mdc", "test"); + CDI.current().select(Counter.class).get().increment(); + Vertx.currentContext().putLocal("filter", "test"); + } + } + + @ServerResponseFilter + public void getFilter(ContainerResponseContext responseContext) { + if (responseContext.getHeaders().get("X-filter") != null) { + AssertHelper.assertEverything(); + // the request filter, the method, and here. + assert CDI.current().select(Counter.class).get().increment() == 3; + assert Vertx.currentContext().getLocal("test").equals("test test"); + assert MDC.get("mdc").equals("test test"); + } + } + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/MyResource.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/MyResource.java new file mode 100644 index 0000000000000..78699a422a2f7 --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/MyResource.java @@ -0,0 +1,72 @@ +package io.quarkus.virtual.rr; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.common.annotation.RunOnVirtualThread; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@Path("/") +public class MyResource { + + private final Counter counter; + + MyResource(Counter counter) { + this.counter = counter; + } + + @GET + @RunOnVirtualThread + public String testGet() { + AssertHelper.assertEverything(); + return "hello-" + counter.increment(); + } + + @POST + @RunOnVirtualThread + public String testPost(String body) { + AssertHelper.assertEverything(); + return body + "-" + counter.increment(); + } + + @GET + @NonBlocking + @Path("/non-blocking") + public String testNonBlocking() { + AssertHelper.assertThatTheRequestScopeIsActive(); + AssertHelper.assertThatItRunsOnADuplicatedContext(); + AssertHelper.assertNotOnVirtualThread(); + return "ok"; + } + + @GET + @Path("/uni") + public Uni testUni() { + AssertHelper.assertThatTheRequestScopeIsActive(); + AssertHelper.assertThatItRunsOnADuplicatedContext(); + AssertHelper.assertNotOnVirtualThread(); + return Uni.createFrom().item("ok"); + } + + @GET + @Path("/multi") + public Multi testMulti() { + AssertHelper.assertThatTheRequestScopeIsActive(); + AssertHelper.assertThatItRunsOnADuplicatedContext(); + AssertHelper.assertNotOnVirtualThread(); + return Multi.createFrom().items("o", "k"); + } + + @GET + @Path("/blocking") + public String testBlocking() { + AssertHelper.assertThatTheRequestScopeIsActive(); + AssertHelper.assertThatItRunsOnADuplicatedContext(); + AssertHelper.assertNotOnVirtualThread(); + return "hello-" + counter.increment(); + } + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/MyResourceWithVTOnClass.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/MyResourceWithVTOnClass.java new file mode 100644 index 0000000000000..f13718281a86b --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/java/io/quarkus/virtual/rr/MyResourceWithVTOnClass.java @@ -0,0 +1,49 @@ +package io.quarkus.virtual.rr; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import io.smallrye.common.annotation.Blocking; +import io.smallrye.common.annotation.RunOnVirtualThread; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@Path("/class") +@RunOnVirtualThread +public class MyResourceWithVTOnClass { + + private final Counter counter; + + MyResourceWithVTOnClass(Counter counter) { + this.counter = counter; + } + + @GET + public String testGet() { + AssertHelper.assertEverything(); + return "hello-" + counter.increment(); + } + + @POST + public String testPost(String body) { + AssertHelper.assertEverything(); + return body + "-" + counter.increment(); + } + + @GET + @Path("/uni") + @Blocking // Mandatory, because it's really a weird case + public Uni testUni() { + return Uni.createFrom().item("ok"); + } + + @GET + @Path("/multi") + @Blocking // Mandatory, because it's really a weird case + public Multi testMulti() { + AssertHelper.assertEverything(); + return Multi.createFrom().items("o", "k"); + } + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/resources/application.properties b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/resources/application.properties new file mode 100644 index 0000000000000..0632584e49a8d --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.native.additional-build-args=--enable-preview \ No newline at end of file diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/NoPinningIT.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/NoPinningIT.java new file mode 100644 index 0000000000000..4be03272150ff --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/NoPinningIT.java @@ -0,0 +1,76 @@ +package io.quarkus.virtual.rr; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * An integration test reading the output of the unit test to verify that no tests where pinning the carrier thread. + * It reads the reports generated by surefire. + */ +public class NoPinningIT { + + @Test + void verify() throws IOException, ParserConfigurationException, SAXException { + var reports = new File("target", "surefire-reports"); + Assertions.assertTrue(reports.isDirectory(), + "Unable to find " + reports.getAbsolutePath() + ", did you run the tests with Maven before?"); + var list = reports.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.startsWith("TEST") && name.endsWith("Test.xml"); + } + }); + Assertions.assertNotNull(list, + "Unable to find " + reports.getAbsolutePath() + ", did you run the tests with Maven before?"); + + for (File report : list) { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(report); + var suite = document.getFirstChild(); + var cases = getChildren(suite.getChildNodes(), "testcase"); + for (Node c : cases) { + verify(report, c); + } + } + + } + + private void verify(File file, Node ca) { + var fullname = ca.getAttributes().getNamedItem("classname").getTextContent() + "." + + ca.getAttributes().getNamedItem("name").getTextContent(); + var output = getChildren(ca.getChildNodes(), "system-out"); + if (output.isEmpty()) { + return; + } + var sout = output.get(0).getTextContent(); + if (sout.contains("VThreadContinuation.onPinned")) { + throw new AssertionError("The test case " + fullname + " pinned the carrier thread, check " + file.getAbsolutePath() + + " for details (or the log of the test)"); + } + + } + + private List getChildren(NodeList nodes, String name) { + List list = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + var node = nodes.item(i); + if (node.getNodeName().equalsIgnoreCase(name)) { + list.add(node); + } + } + return list; + } + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadIT.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadIT.java new file mode 100644 index 0000000000000..27b07a1cc7e2b --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadIT.java @@ -0,0 +1,8 @@ +package io.quarkus.virtual.rr; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class RunOnVirtualThreadIT extends RunOnVirtualThreadTest { + +} diff --git a/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadTest.java b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadTest.java new file mode 100644 index 0000000000000..c03320d4c4dda --- /dev/null +++ b/integration-tests/virtual-threads/resteasy-reactive-virtual-threads/src/test/java/io/quarkus/virtual/rr/RunOnVirtualThreadTest.java @@ -0,0 +1,99 @@ +package io.quarkus.virtual.rr; + +import static org.hamcrest.Matchers.is; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +class RunOnVirtualThreadTest { + + @Test + void testGet() { + RestAssured.get().then() + .assertThat().statusCode(200) + .body(is("hello-1")); + RestAssured.get().then() + .assertThat().statusCode(200) + // Same value - request scoped bean + .body(is("hello-1")); + } + + @Test + void testPost() { + var body1 = UUID.randomUUID().toString(); + var body2 = UUID.randomUUID().toString(); + RestAssured + .given().body(body1) + .post().then() + .assertThat().statusCode(200) + .body(is(body1 + "-1")); + RestAssured + .given().body(body2) + .post().then() + .assertThat().statusCode(200) + // Same value - request scoped bean + .body(is(body2 + "-1")); + } + + @Test + void testFilter() { + // Request scope + // Routing Context + // Duplicated context + + // MDC + } + + @Test + void testNonBlocking() { + // Non Blocking + RestAssured.get("/non-blocking").then() + .assertThat().statusCode(200) + .body(is("ok")); + // Uni + RestAssured.get("/uni").then() + .assertThat().statusCode(200) + .body(is("ok")); + // Multi + RestAssured.get("/multi").then() + .assertThat().statusCode(200) + .body(is("ok")); + } + + @Test + void testRegularBlocking() { + RestAssured.get("/blocking").then() + .assertThat().statusCode(200) + .body(is("hello-1")); + } + + @Test + void testRunOnVirtualThreadOnClass() { + RestAssured.get("/class").then() + .assertThat().statusCode(200) + .body(is("hello-1")); + RestAssured.get("/class").then() + .assertThat().statusCode(200) + .body(is("hello-1")); + + RestAssured.get("/class/uni").then() + .assertThat().statusCode(200) + .body(is("ok")); + + RestAssured.get("/class/multi").then() + .assertThat().statusCode(200) + .body(is("ok")); + } + + @Test + void testFilters() { + RestAssured.get("/filter").then() + .assertThat().statusCode(200); + } + +}