forked from quarkusio/quarkus
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
5a3a825
commit 9c30a25
Showing
14 changed files
with
660 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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> |
16 changes: 16 additions & 0 deletions
16
...work/junit5-virtual-threads/src/main/java/io/quarkus/test/junit/virtual/ShouldNotPin.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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 { | ||
|
||
} |
18 changes: 18 additions & 0 deletions
18
...amework/junit5-virtual-threads/src/main/java/io/quarkus/test/junit/virtual/ShouldPin.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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; | ||
|
||
} |
192 changes: 192 additions & 0 deletions
192
...unit5-virtual-threads/src/main/java/io/quarkus/test/junit/virtual/internal/Collector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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 | ||
} | ||
|
||
} |
Oops, something went wrong.