Skip to content

Commit 2018e0c

Browse files
PerfectSlayeramarziali
authored andcommitted
feat(env): Add support for virtual thread detection (#9852)
1 parent e3a274f commit 2018e0c

File tree

6 files changed

+227
-195
lines changed

6 files changed

+227
-195
lines changed

components/environment/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +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.ThreadUtils", // depends on JVM version
27+
"datadog.environment.ThreadSupport", // requires Java 21
2828
)
2929
}
3030
val excludedClassesBranchCoverage by extra {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
/**
11+
* Helper class for working with {@link Thread}s.
12+
*
13+
* <p>Uses feature detection and provides static helpers to work with different versions of Java.
14+
*
15+
* <p>This class is designed to use {@link MethodHandle}s that constant propagate to minimize the
16+
* overhead.
17+
*/
18+
public final class ThreadSupport {
19+
static final MethodHandle THREAD_ID_MH = findThreadIdMethodHandle();
20+
static final MethodHandle IS_VIRTUAL_MH = findIsVirtualMethodHandle();
21+
static final MethodHandle NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR_MH =
22+
findNewVirtualThreadPerTaskExecutorMethodHandle();
23+
24+
private ThreadSupport() {}
25+
26+
/**
27+
* Provides the best identifier available for the current {@link Thread}. Uses {@link
28+
* Thread#threadId()} on 19+ or {@link Thread#getId()} on older JVMs.
29+
*
30+
* @return The best identifier available for the current {@link Thread}.
31+
*/
32+
public static long threadId() {
33+
return threadId(Thread.currentThread());
34+
}
35+
36+
/**
37+
* Provides the best identifier available for the given {@link Thread}. Uses {@link
38+
* Thread#threadId()} on 19+ or {@link Thread#getId()} on older JVMs.
39+
*
40+
* @return The best identifier available for the given {@link Thread}.
41+
*/
42+
public static long threadId(Thread thread) {
43+
if (THREAD_ID_MH != null) {
44+
try {
45+
return (long) THREAD_ID_MH.invoke(thread);
46+
} catch (Throwable ignored) {
47+
}
48+
}
49+
return thread.getId();
50+
}
51+
52+
/**
53+
* Checks whether virtual threads are supported on this JVM.
54+
*
55+
* @return {@code true} if virtual threads are supported, {@code false} otherwise.
56+
*/
57+
public static boolean supportsVirtualThreads() {
58+
return (IS_VIRTUAL_MH != null);
59+
}
60+
61+
/**
62+
* Checks whether the current thread is a virtual thread.
63+
*
64+
* @return {@code true} if the current thread is a virtual thread, {@code false} otherwise.
65+
*/
66+
public static boolean isVirtual() {
67+
// IS_VIRTUAL_MH will constant propagate -- then dead code eliminate -- and inline as needed
68+
return IS_VIRTUAL_MH != null && isVirtual(Thread.currentThread());
69+
}
70+
71+
/**
72+
* Checks whether the given thread is a virtual thread.
73+
*
74+
* @param thread The thread to check.
75+
* @return {@code true} if the given thread is virtual, {@code false} otherwise.
76+
*/
77+
public static boolean isVirtual(Thread thread) {
78+
// IS_VIRTUAL_MH will constant propagate -- then dead code eliminate -- and inline as needed
79+
if (IS_VIRTUAL_MH != null) {
80+
try {
81+
return (boolean) IS_VIRTUAL_MH.invoke(thread);
82+
} catch (Throwable ignored) {
83+
}
84+
}
85+
return false;
86+
}
87+
88+
/**
89+
* Returns the virtual thread per task executor if available.
90+
*
91+
* @return The virtual thread per task executor if available wrapped into an {@link Optional}, or
92+
* {@link Optional#empty()} otherwise.
93+
*/
94+
public static Optional<ExecutorService> newVirtualThreadPerTaskExecutor() {
95+
if (NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR_MH != null) {
96+
try {
97+
ExecutorService executorService =
98+
(ExecutorService) NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR_MH.invoke();
99+
return Optional.of(executorService);
100+
} catch (Throwable ignored) {
101+
}
102+
}
103+
return Optional.empty();
104+
}
105+
106+
private static MethodHandle findThreadIdMethodHandle() {
107+
if (JavaVirtualMachine.isJavaVersionAtLeast(19)) {
108+
try {
109+
return MethodHandles.lookup()
110+
.findVirtual(Thread.class, "threadId", MethodType.methodType(long.class));
111+
} catch (Throwable ignored) {
112+
return null;
113+
}
114+
}
115+
return null;
116+
}
117+
118+
private static MethodHandle findIsVirtualMethodHandle() {
119+
if (JavaVirtualMachine.isJavaVersionAtLeast(21)) {
120+
try {
121+
return MethodHandles.lookup()
122+
.findVirtual(Thread.class, "isVirtual", MethodType.methodType(boolean.class));
123+
} catch (Throwable ignored) {
124+
}
125+
}
126+
return null;
127+
}
128+
129+
private static MethodHandle findNewVirtualThreadPerTaskExecutorMethodHandle() {
130+
if (JavaVirtualMachine.isJavaVersionAtLeast(21)) {
131+
try {
132+
return MethodHandles.lookup()
133+
.findStatic(
134+
Executors.class,
135+
"newVirtualThreadPerTaskExecutor",
136+
MethodType.methodType(ExecutorService.class));
137+
} catch (Throwable ignored) {
138+
}
139+
}
140+
return null;
141+
}
142+
}

components/environment/src/main/java/datadog/environment/ThreadUtils.java

Lines changed: 0 additions & 85 deletions
This file was deleted.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 java.util.concurrent.atomic.AtomicLong;
11+
import org.junit.jupiter.api.AfterAll;
12+
import org.junit.jupiter.api.BeforeAll;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.condition.EnabledOnJre;
15+
16+
class ThreadSupportTest {
17+
private static ExecutorService singleThreadExecutor;
18+
private static ExecutorService newVirtualThreadPerTaskExecutor;
19+
20+
@BeforeAll
21+
static void beforeAll() {
22+
singleThreadExecutor = Executors.newSingleThreadExecutor();
23+
newVirtualThreadPerTaskExecutor = ThreadSupport.newVirtualThreadPerTaskExecutor().orElse(null);
24+
}
25+
26+
@Test
27+
public void testThreadId() throws InterruptedException {
28+
AtomicLong threadId = new AtomicLong();
29+
Thread thread = new Thread(() -> threadId.set(ThreadSupport.threadId()), "foo");
30+
thread.start();
31+
try {
32+
// always works on Thread's where getId isn't overridden by child class
33+
assertEquals(thread.getId(), ThreadSupport.threadId(thread));
34+
} finally {
35+
thread.join();
36+
}
37+
assertEquals(thread.getId(), threadId.get());
38+
}
39+
40+
@Test
41+
void testSupportsVirtualThreads() {
42+
assertEquals(
43+
JavaVirtualMachine.isJavaVersionAtLeast(21),
44+
ThreadSupport.supportsVirtualThreads(),
45+
"expected virtual threads support status");
46+
}
47+
48+
@Test
49+
void testPlatformThread() {
50+
assertVirtualThread(singleThreadExecutor, false);
51+
}
52+
53+
@Test
54+
@EnabledOnJre(JAVA_21)
55+
void testVirtualThread() {
56+
assertVirtualThread(newVirtualThreadPerTaskExecutor, true);
57+
}
58+
59+
static void assertVirtualThread(ExecutorService executorService, boolean expected) {
60+
Future<Boolean> futureCurrent = executorService.submit(() -> ThreadSupport.isVirtual());
61+
Future<Boolean> futureGiven =
62+
executorService.submit(
63+
() -> {
64+
Thread thread = Thread.currentThread();
65+
return ThreadSupport.isVirtual(thread);
66+
});
67+
try {
68+
assertEquals(expected, futureCurrent.get(), "invalid current thread virtual status");
69+
assertEquals(expected, futureGiven.get(), "invalid given thread virtual status");
70+
} catch (Throwable e) {
71+
fail("Can't get thread virtual status", e);
72+
}
73+
}
74+
75+
@AfterAll
76+
static void afterAll() {
77+
singleThreadExecutor.shutdown();
78+
if (newVirtualThreadPerTaskExecutor != null) {
79+
newVirtualThreadPerTaskExecutor.shutdown();
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)