Skip to content

Commit

Permalink
Add a test for failed starting and unparking
Browse files Browse the repository at this point in the history
  • Loading branch information
jonatan-ivanov committed Sep 14, 2024
1 parent d7ec62f commit bfb471a
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 1 deletion.
5 changes: 5 additions & 0 deletions micrometer-java21/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ tasks.withType(JavaCompile).configureEach {
targetCompatibility = JavaVersion.VERSION_21
options.release = 21
}

test {
// This hack is needed since JfrVirtualThreadEventMetricsTests utilizes reflection against java.lang, see its javadoc
jvmArgs += ['--add-opens', 'java.base/java.lang=ALL-UNNAMED']
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,38 @@
*/
package io.micrometer.java21.instrument.binder.jfr;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Constructor;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.locks.LockSupport;

import static java.lang.Thread.State.WAITING;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;

/**
* Tests for {@link JfrVirtualThreadEventMetrics}.
* Tests for {@link JfrVirtualThreadEventMetrics}. If you run these tests from your IDE,
* {@link #submitFailedEventsShouldBeRecorded()} might fail depending on your setup. This
* is because the test (through {@link #virtualThreadFactoryFor(Executor)}) utilizes
* reflection against the java.lang package which needs to be explicitly enabled. If you
* run into such an issue you can either: - Change your setup and let your IDE run the
* tests utilizing the build system (Gradle) - Add the following JVM arg to your test
* config: {@code --add-opens java.base/java.lang=ALL-UNNAMED}
*
* @author Artyom Gabeev
* @author Jonatan Ivanov
*/
class JfrVirtualThreadEventMetricsTests {

Expand Down Expand Up @@ -75,6 +89,34 @@ void pinnedEventsShouldBeRecorded() {
}
}

/**
* Uses a similar approach as the JDK tests to make starting or unparking a virtual
* thread fail, see {@link #virtualThreadFactoryFor(Executor)} and
* https://github.com/openjdk/jdk/blob/fdfe503d016086cf78b5a8c27dbe45f0261c68ab/test/jdk/java/lang/Thread/virtual/JfrEvents.java#L143-L187
*/
@Test
void submitFailedEventsShouldBeRecorded() {
try (ExecutorService cachedPool = Executors.newCachedThreadPool()) {
ThreadFactory factory = virtualThreadFactoryFor(cachedPool);
Thread thread = factory.newThread(LockSupport::park);
thread.start();

await().atMost(Duration.ofSeconds(2)).until(() -> thread.getState() == WAITING);
cachedPool.shutdown();

// unpark, the pool was shut down, this should fail
assertThatThrownBy(() -> LockSupport.unpark(thread)).isInstanceOf(RejectedExecutionException.class);

Counter counter = registry.get("jvm.threads.virtual.submit.failed").tags(TAGS).counter();
await().atMost(Duration.ofSeconds(2)).until(() -> counter.count() == 1);

// park, the pool was shut down, this should fail
assertThatThrownBy(() -> factory.newThread(LockSupport::park).start())
.isInstanceOf(RejectedExecutionException.class);
await().atMost(Duration.ofSeconds(2)).until(() -> counter.count() == 2);
}
}

private void pinCurrentThreadAndAwait(CountDownLatch latch) {
synchronized (new Object()) { // assumes that synchronized pins the thread
try {
Expand Down Expand Up @@ -109,4 +151,26 @@ private void waitFor(Future<?> future) {
}
}

/**
* Creates a {@link ThreadFactory} for virtual threads. The created virtual threads
* will be bound to the provided platform thread pool instead of a default
* ForkJoinPool. At its current form, this is a hack, it utilizes reflection to supply
* the platform thread pool. It seems though there is no other way of doing this, the
* JDK tests are also utilizing reflection to do the same, see:
* https://github.com/openjdk/jdk/blob/fdfe503d016086cf78b5a8c27dbe45f0261c68ab/test/lib/jdk/test/lib/thread/VThreadScheduler.java#L71-L90
* @param pool platform pool
* @return virtual thread factory bound to the provided platform pool
*/
private static ThreadFactory virtualThreadFactoryFor(Executor pool) {
try {
Class<?> clazz = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder");
Constructor<?> constructor = clazz.getDeclaredConstructor(Executor.class);
constructor.setAccessible(true);
return ((Thread.Builder.OfVirtual) constructor.newInstance(pool)).factory();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}

}

0 comments on commit bfb471a

Please sign in to comment.