Skip to content

Commit

Permalink
Integrate loom-unit in QuarkusTest
Browse files Browse the repository at this point in the history
This commit brings the functionality from loom-unit (partially, but the important ones are there) into Quarkus.
Thus, @ShouldPin and @ShouldNotPin can be used on a @QuarkusTest directly as soon as the quarkus-junit5-virtual-threads dependency is in the project.

loom-unit original repo: https://github.com/cescoffier/loom-unit
  • Loading branch information
cescoffier committed Sep 18, 2023
1 parent 5a3a825 commit 9c30a25
Show file tree
Hide file tree
Showing 14 changed files with 660 additions and 0 deletions.
5 changes: 5 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2954,6 +2954,11 @@
<artifactId>quarkus-junit5-properties</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-virtual-threads</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security</artifactId>
Expand Down
35 changes: 35 additions & 0 deletions docs/src/main/asciidoc/virtual-threads.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,41 @@ quarkus.virtual-threads.name-prefix=
----

== Testing virtual thread applications

As mentioned above, virtual threads have a few limitations that can drastically affect your application performance and memory usage.
Quarkus provides a way to detect pinned carrier threads while running your tests.
Thus, you can eliminate one of the most prominent limitations or be aware of the problem.

To enable this detection:

* 1) Add the `quarkus-junit5-virtual-thread` dependency to your project:
[source, xml]
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-virtual-threads</artifactId>
<scope>test</scope>
</dependency>
----

* 2) In your test case, add the `io.quarkus.test.junit.virtual.ShouldNotPin` annotation:
[source, java]
----
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ShouldNotPin
class TodoResourceTest {
// ...
}
----

When you run your test (remember to use Java 21+), Quarkus detects pinned carrier threads.
When it happens, the test fails.

The `@ShouldNotPin` can also be used on methods directly.

NOTE: Quarkus also provides a `@ShouldPin` annotation for cases where pinning is unavoidable.

== Additional references

Expand Down
48 changes: 48 additions & 0 deletions test-framework/junit5-virtual-threads/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-framework</artifactId>
<version>999-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>quarkus-junit5-virtual-threads</artifactId>
<name>Quarkus - Test framework - JUnit 5 - Virtual Threads</name>

<description>Module that allows detecting virtual threads pinning</description>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>

<build>
<plugins>
<!-- The entity classes needs to be indexed -->
<plugin>
<groupId>io.smallrye</groupId>
<artifactId>jandex-maven-plugin</artifactId>
<executions>
<execution>
<id>make-index</id>
<goals>
<goal>jandex</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.test.junit.virtual;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marker indicating that the test method or class should not pin the carrier thread.
* If, during the execution of the test, a virtual thread pins the carrier thread, the test fails.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface ShouldNotPin {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.test.junit.virtual;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Indicates that the method or class can pin. At most can be set to indicate the maximum number of events.
* If, during the execution of the test, a virtual thread does not pin the carrier thread, or pins it more than
* the given {@code atMost} value, the test fails.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface ShouldPin {
int atMost() default Integer.MAX_VALUE;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package io.quarkus.test.junit.virtual.internal;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;

import jdk.jfr.consumer.RecordedEvent;

public class Collector implements Consumer<RecordedEvent> {
public static final String CARRIER_PINNED_EVENT_NAME = "jdk.VirtualThreadPinned";
private static final Logger LOGGER = Logger.getLogger(Collector.class.getName());

private final List<Function<RecordedEvent, Boolean>> observers = new CopyOnWriteArrayList<>();

private final List<RecordedEvent> events = new CopyOnWriteArrayList<>();

private final EventStreamFacade facade;

volatile State state = State.INIT;

public Collector() {
if (EventStreamFacade.available) {
facade = new EventStreamFacade();
facade.enable(CARRIER_PINNED_EVENT_NAME).withStackTrace();
facade.enable(InternalEvents.SHUTDOWN_EVENT_NAME).withoutStackTrace();
facade.enable(InternalEvents.CAPTURING_STARTED_EVENT_NAME).withoutStackTrace();
facade.enable(InternalEvents.CAPTURING_STOPPED_EVENT_NAME).withoutStackTrace();
facade.enable(InternalEvents.INITIALIZATION_EVENT_NAME).withoutStackTrace();
facade.setOrdered(true);
facade.setMaxSize(100);
facade.onEvent(this);
} else {
facade = null;
}
}

public void init() {
if (facade != null) {
long begin = System.nanoTime();
CountDownLatch latch = new CountDownLatch(1);
observers.add(re -> {
if (re.getEventType().getName().equals(InternalEvents.INITIALIZATION_EVENT_NAME)) {
latch.countDown();
return true;
}
return false;
});
facade.startAsync();
new InternalEvents.InitializationEvent().commit();
try {
if (latch.await(10, TimeUnit.SECONDS)) {
long end = System.nanoTime();
state = State.STARTED;
LOGGER.log(Level.FINE, "Event collection started in {0}s", (end - begin) / 1000000);
} else {
throw new IllegalStateException(
"Unable to start JFR collection, RecordingStartedEvent event not received after 10s");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}

}

public void start() {
if (facade != null) {
CountDownLatch latch = new CountDownLatch(1);
String id = UUID.randomUUID().toString();
long begin = System.nanoTime();
observers.add(re -> {
if (re.getEventType().getName().equals(InternalEvents.CAPTURING_STARTED_EVENT_NAME)) {
if (id.equals(re.getString("id"))) {
events.clear();
state = State.COLLECTING;
latch.countDown();
return true;
}
}
return false;
});

new InternalEvents.CapturingStartedEvent(id).commit();

try {
if (!latch.await(10, TimeUnit.SECONDS)) {
throw new IllegalStateException("Unable to start JFR collection, START_EVENT event not received after 10s");
}
long end = System.nanoTime();
LOGGER.log(Level.FINE, "Event capturing started in {0}s", (end - begin) / 1000000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}

public List<RecordedEvent> stop() {
if (facade != null) {
CountDownLatch latch = new CountDownLatch(1);
String id = UUID.randomUUID().toString();
var begin = System.nanoTime();
observers.add(re -> {
if (re.getEventType().getName().equals(InternalEvents.CAPTURING_STOPPED_EVENT_NAME)) {
state = State.STARTED;
latch.countDown();
return true;
}
return false;
});

new InternalEvents.CapturingStoppedEvent(id).commit();

try {
if (!latch.await(10, TimeUnit.SECONDS)) {
throw new IllegalStateException("Unable to start JFR collection, STOP_EVENT event not received after 10s");
}
var end = System.nanoTime();
LOGGER.log(Level.FINE, "Event collection stopped in {0}s", (end - begin) / 1000000);
return new ArrayList<>(events);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
return Collections.emptyList();
}

public void shutdown() {
if (facade != null) {
CountDownLatch latch = new CountDownLatch(1);
var begin = System.nanoTime();
observers.add(re -> {
if (re.getEventType().getName().equals(InternalEvents.SHUTDOWN_EVENT_NAME)) {
latch.countDown();
return true;
}
return false;
});
InternalEvents.ShutdownEvent event = new InternalEvents.ShutdownEvent();
event.commit();
try {
if (latch.await(10, TimeUnit.SECONDS)) {
state = State.INIT;
var end = System.nanoTime();
LOGGER.log(Level.FINE, "Event collector shutdown in {0}s", (end - begin) / 1000000);
facade.stop();
} else {
throw new IllegalStateException(
"Unable to stop JFR collection, RecordingStoppedEvent event not received at 10s");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}

@Override
public void accept(RecordedEvent re) {
if (state == State.COLLECTING) {
events.add(re);
}
List<Function<RecordedEvent, Boolean>> toBeRemoved = new ArrayList<>();
observers.forEach(c -> {
if (c.apply(re)) {
toBeRemoved.add(c);
}
});
observers.removeAll(toBeRemoved);
}

public List<RecordedEvent> getEvents() {
return new ArrayList<>(events);
}

enum State {
INIT,
STARTED,
COLLECTING
}

}
Loading

0 comments on commit 9c30a25

Please sign in to comment.