Skip to content

Commit d498d7c

Browse files
committed
feat(env): Add support for virtual thread detection
1 parent 30451d7 commit d498d7c

File tree

3 files changed

+151
-0
lines changed

3 files changed

+151
-0
lines changed

components/environment/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ val excludedClassesCoverage by extra {
2424
"datadog.environment.JavaVirtualMachine.JvmOptionsHolder", // depends on OS and JVM vendor
2525
"datadog.environment.JvmOptions", // depends on OS and JVM vendor
2626
"datadog.environment.OperatingSystem**", // depends on OS
27+
"datadog.environment.ThreadSupport", // requires Java 21
2728
)
2829
}
2930
val excludedClassesBranchCoverage by extra {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package datadog.environment;
2+
3+
import java.lang.invoke.MethodHandle;
4+
import java.lang.invoke.MethodHandles;
5+
import java.lang.invoke.MethodType;
6+
import java.util.Optional;
7+
import java.util.concurrent.ExecutorService;
8+
import java.util.concurrent.Executors;
9+
10+
/** Provides virtual thread capabilities if available. */
11+
public final class ThreadSupport {
12+
static final MethodHandle MH_IS_VIRTUAL = findIsVirtualMethodHandle();
13+
static final MethodHandle MH_NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR =
14+
findNewVirtualThreadPerTaskExecutorMethodHandle();
15+
16+
private ThreadSupport() {}
17+
18+
/**
19+
* Checks whether the current thread is a virtual thread.
20+
*
21+
* @return {@code true} if the current thread is a virtual thread, {@code false} otherwise.
22+
*/
23+
public static boolean isVirtual() {
24+
if (MH_IS_VIRTUAL == null) {
25+
return false;
26+
}
27+
return isVirtual(Thread.currentThread());
28+
}
29+
30+
/**
31+
* Checks whether the given thread is a virtual thread.
32+
*
33+
* @param thread The thread to check.
34+
* @return {@code true} if the given thread is virtual, {@code false} otherwise.
35+
*/
36+
public static boolean isVirtual(Thread thread) {
37+
if (MH_IS_VIRTUAL == null) {
38+
return false;
39+
}
40+
try {
41+
return (boolean) MH_IS_VIRTUAL.invoke(thread);
42+
} catch (Throwable e) {
43+
return false;
44+
}
45+
}
46+
47+
/**
48+
* Returns the virtual thread per task executor if available.
49+
*
50+
* @return The virtual thread per task executor if available wrapped into an {@link Optional}, or
51+
* {@link Optional#empty()} otherwise.
52+
*/
53+
public static Optional<ExecutorService> newVirtualThreadPerTaskExecutor() {
54+
if (MH_NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR == null) {
55+
return Optional.empty();
56+
} else {
57+
try {
58+
ExecutorService executorService =
59+
(ExecutorService) MH_NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR.invoke();
60+
return Optional.of(executorService);
61+
} catch (Throwable e) {
62+
return Optional.empty();
63+
}
64+
}
65+
}
66+
67+
private static MethodHandle findIsVirtualMethodHandle() {
68+
if (JavaVersion.getRuntimeVersion().isAtLeast(21, 0, 0)) {
69+
try {
70+
return MethodHandles.lookup()
71+
.findVirtual(Thread.class, "isVirtual", MethodType.methodType(boolean.class));
72+
} catch (Throwable ignored) {
73+
}
74+
}
75+
return null;
76+
}
77+
78+
private static MethodHandle findNewVirtualThreadPerTaskExecutorMethodHandle() {
79+
if (JavaVersion.getRuntimeVersion().isAtLeast(21, 0, 0)) {
80+
try {
81+
return MethodHandles.lookup()
82+
.findStatic(
83+
Executors.class,
84+
"newVirtualThreadPerTaskExecutor",
85+
MethodType.methodType(ExecutorService.class));
86+
} catch (Throwable ignored) {
87+
}
88+
}
89+
return null;
90+
}
91+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package datadog.environment;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.fail;
5+
import static org.junit.jupiter.api.condition.JRE.JAVA_21;
6+
7+
import java.util.concurrent.ExecutorService;
8+
import java.util.concurrent.Executors;
9+
import java.util.concurrent.Future;
10+
import org.junit.jupiter.api.AfterAll;
11+
import org.junit.jupiter.api.BeforeAll;
12+
import org.junit.jupiter.api.Test;
13+
import org.junit.jupiter.api.condition.EnabledOnJre;
14+
15+
class ThreadSupportTest {
16+
private static ExecutorService singleThreadExecutor;
17+
private static ExecutorService newVirtualThreadPerTaskExecutor;
18+
19+
@BeforeAll
20+
static void beforeAll() {
21+
singleThreadExecutor = Executors.newSingleThreadExecutor();
22+
newVirtualThreadPerTaskExecutor = ThreadSupport.newVirtualThreadPerTaskExecutor().orElse(null);
23+
}
24+
25+
@Test
26+
void testPlatformThread() {
27+
assertVirtualThread(singleThreadExecutor, false);
28+
}
29+
30+
@Test
31+
@EnabledOnJre(JAVA_21)
32+
void testVirtualThread() {
33+
assertVirtualThread(newVirtualThreadPerTaskExecutor, true);
34+
}
35+
36+
static void assertVirtualThread(ExecutorService executorService, boolean expected) {
37+
Future<Boolean> futureCurrent = executorService.submit(() -> ThreadSupport.isVirtual());
38+
Future<Boolean> futureGiven =
39+
executorService.submit(
40+
() -> {
41+
Thread thread = Thread.currentThread();
42+
return ThreadSupport.isVirtual(thread);
43+
});
44+
try {
45+
assertEquals(expected, futureCurrent.get(), "invalid current thread virtual status");
46+
assertEquals(expected, futureGiven.get(), "invalid given thread virtual status");
47+
} catch (Throwable e) {
48+
fail("Can't get thread virtual status", e);
49+
}
50+
}
51+
52+
@AfterAll
53+
static void afterAll() {
54+
singleThreadExecutor.shutdown();
55+
if (newVirtualThreadPerTaskExecutor != null) {
56+
newVirtualThreadPerTaskExecutor.shutdown();
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)