diff --git a/components/environment/build.gradle.kts b/components/environment/build.gradle.kts index 8d9febfe679..7818fef2463 100644 --- a/components/environment/build.gradle.kts +++ b/components/environment/build.gradle.kts @@ -24,7 +24,7 @@ val excludedClassesCoverage by extra { "datadog.environment.JavaVirtualMachine.JvmOptionsHolder", // depends on OS and JVM vendor "datadog.environment.JvmOptions", // depends on OS and JVM vendor "datadog.environment.OperatingSystem**", // depends on OS - "datadog.environment.ThreadUtils", // depends on JVM version + "datadog.environment.ThreadSupport", // requires Java 21 ) } val excludedClassesBranchCoverage by extra { diff --git a/components/environment/src/main/java/datadog/environment/ThreadSupport.java b/components/environment/src/main/java/datadog/environment/ThreadSupport.java new file mode 100644 index 00000000000..861215ca837 --- /dev/null +++ b/components/environment/src/main/java/datadog/environment/ThreadSupport.java @@ -0,0 +1,142 @@ +package datadog.environment; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Helper class for working with {@link Thread}s. + * + *

Uses feature detection and provides static helpers to work with different versions of Java. + * + *

This class is designed to use {@link MethodHandle}s that constant propagate to minimize the + * overhead. + */ +public final class ThreadSupport { + static final MethodHandle THREAD_ID_MH = findThreadIdMethodHandle(); + static final MethodHandle IS_VIRTUAL_MH = findIsVirtualMethodHandle(); + static final MethodHandle NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR_MH = + findNewVirtualThreadPerTaskExecutorMethodHandle(); + + private ThreadSupport() {} + + /** + * Provides the best identifier available for the current {@link Thread}. Uses {@link + * Thread#threadId()} on 19+ or {@link Thread#getId()} on older JVMs. + * + * @return The best identifier available for the current {@link Thread}. + */ + public static long threadId() { + return threadId(Thread.currentThread()); + } + + /** + * Provides the best identifier available for the given {@link Thread}. Uses {@link + * Thread#threadId()} on 19+ or {@link Thread#getId()} on older JVMs. + * + * @return The best identifier available for the given {@link Thread}. + */ + public static long threadId(Thread thread) { + if (THREAD_ID_MH != null) { + try { + return (long) THREAD_ID_MH.invoke(thread); + } catch (Throwable ignored) { + } + } + return thread.getId(); + } + + /** + * Checks whether virtual threads are supported on this JVM. + * + * @return {@code true} if virtual threads are supported, {@code false} otherwise. + */ + public static boolean supportsVirtualThreads() { + return (IS_VIRTUAL_MH != null); + } + + /** + * Checks whether the current thread is a virtual thread. + * + * @return {@code true} if the current thread is a virtual thread, {@code false} otherwise. + */ + public static boolean isVirtual() { + // IS_VIRTUAL_MH will constant propagate -- then dead code eliminate -- and inline as needed + return IS_VIRTUAL_MH != null && isVirtual(Thread.currentThread()); + } + + /** + * Checks whether the given thread is a virtual thread. + * + * @param thread The thread to check. + * @return {@code true} if the given thread is virtual, {@code false} otherwise. + */ + public static boolean isVirtual(Thread thread) { + // IS_VIRTUAL_MH will constant propagate -- then dead code eliminate -- and inline as needed + if (IS_VIRTUAL_MH != null) { + try { + return (boolean) IS_VIRTUAL_MH.invoke(thread); + } catch (Throwable ignored) { + } + } + return false; + } + + /** + * Returns the virtual thread per task executor if available. + * + * @return The virtual thread per task executor if available wrapped into an {@link Optional}, or + * {@link Optional#empty()} otherwise. + */ + public static Optional newVirtualThreadPerTaskExecutor() { + if (NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR_MH != null) { + try { + ExecutorService executorService = + (ExecutorService) NEW_VIRTUAL_THREAD_PER_TASK_EXECUTOR_MH.invoke(); + return Optional.of(executorService); + } catch (Throwable ignored) { + } + } + return Optional.empty(); + } + + private static MethodHandle findThreadIdMethodHandle() { + if (JavaVirtualMachine.isJavaVersionAtLeast(19)) { + try { + return MethodHandles.lookup() + .findVirtual(Thread.class, "threadId", MethodType.methodType(long.class)); + } catch (Throwable ignored) { + return null; + } + } + return null; + } + + private static MethodHandle findIsVirtualMethodHandle() { + if (JavaVirtualMachine.isJavaVersionAtLeast(21)) { + try { + return MethodHandles.lookup() + .findVirtual(Thread.class, "isVirtual", MethodType.methodType(boolean.class)); + } catch (Throwable ignored) { + } + } + return null; + } + + private static MethodHandle findNewVirtualThreadPerTaskExecutorMethodHandle() { + if (JavaVirtualMachine.isJavaVersionAtLeast(21)) { + try { + return MethodHandles.lookup() + .findStatic( + Executors.class, + "newVirtualThreadPerTaskExecutor", + MethodType.methodType(ExecutorService.class)); + } catch (Throwable ignored) { + } + } + return null; + } +} diff --git a/components/environment/src/main/java/datadog/environment/ThreadUtils.java b/components/environment/src/main/java/datadog/environment/ThreadUtils.java deleted file mode 100644 index e07ccb44dce..00000000000 --- a/components/environment/src/main/java/datadog/environment/ThreadUtils.java +++ /dev/null @@ -1,85 +0,0 @@ -package datadog.environment; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; - -/** - * Helper class for working with Threads - * - *

Uses feature detection and provides static helpers to work with different versions of Java - * - *

This class is designed to use MethodHandles that constant propagate to minimize the overhead - */ -public final class ThreadUtils { - static final MethodHandle H_IS_VIRTUAL = lookupIsVirtual(); - static final MethodHandle H_ID = lookupId(); - - private ThreadUtils() {} - - /** Provides the best id available for the Thread Uses threadId on 19+; getId on older JVMs */ - public static final long threadId(Thread thread) { - try { - return (long) H_ID.invoke(thread); - } catch (Throwable t) { - return 0L; - } - } - - /** Indicates whether virtual threads are supported on this JVM */ - public static final boolean supportsVirtualThreads() { - return (H_IS_VIRTUAL != null); - } - - /** Indicates if the current thread is a virtual thread */ - public static final boolean isCurrentThreadVirtual() { - // H_IS_VIRTUAL will constant propagate -- then dead code eliminate -- and inline as needed - try { - return (H_IS_VIRTUAL != null) && (boolean) H_IS_VIRTUAL.invoke(Thread.currentThread()); - } catch (Throwable t) { - return false; - } - } - - /** Indicates if the provided thread is a virtual thread */ - public static final boolean isVirtual(Thread thread) { - // H_IS_VIRTUAL will constant propagate -- then dead code eliminate -- and inline as needed - try { - return (H_IS_VIRTUAL != null) && (boolean) H_IS_VIRTUAL.invoke(thread); - } catch (Throwable t) { - return false; - } - } - - private static final MethodHandle lookupIsVirtual() { - try { - return MethodHandles.lookup() - .findVirtual(Thread.class, "isVirtual", MethodType.methodType(boolean.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - return null; - } - } - - private static final MethodHandle lookupId() { - MethodHandle threadIdHandle = lookupThreadId(); - return threadIdHandle != null ? threadIdHandle : lookupGetId(); - } - - private static final MethodHandle lookupThreadId() { - try { - return MethodHandles.lookup() - .findVirtual(Thread.class, "threadId", MethodType.methodType(long.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - return null; - } - } - - private static final MethodHandle lookupGetId() { - try { - return MethodHandles.lookup() - .findVirtual(Thread.class, "getId", MethodType.methodType(long.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - return null; - } - } -} diff --git a/components/environment/src/test/java/datadog/environment/ThreadSupportTest.java b/components/environment/src/test/java/datadog/environment/ThreadSupportTest.java new file mode 100644 index 00000000000..be2032c5470 --- /dev/null +++ b/components/environment/src/test/java/datadog/environment/ThreadSupportTest.java @@ -0,0 +1,82 @@ +package datadog.environment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.condition.JRE.JAVA_21; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnJre; + +class ThreadSupportTest { + private static ExecutorService singleThreadExecutor; + private static ExecutorService newVirtualThreadPerTaskExecutor; + + @BeforeAll + static void beforeAll() { + singleThreadExecutor = Executors.newSingleThreadExecutor(); + newVirtualThreadPerTaskExecutor = ThreadSupport.newVirtualThreadPerTaskExecutor().orElse(null); + } + + @Test + public void testThreadId() throws InterruptedException { + AtomicLong threadId = new AtomicLong(); + Thread thread = new Thread(() -> threadId.set(ThreadSupport.threadId()), "foo"); + thread.start(); + try { + // always works on Thread's where getId isn't overridden by child class + assertEquals(thread.getId(), ThreadSupport.threadId(thread)); + } finally { + thread.join(); + } + assertEquals(thread.getId(), threadId.get()); + } + + @Test + void testSupportsVirtualThreads() { + assertEquals( + JavaVirtualMachine.isJavaVersionAtLeast(21), + ThreadSupport.supportsVirtualThreads(), + "expected virtual threads support status"); + } + + @Test + void testPlatformThread() { + assertVirtualThread(singleThreadExecutor, false); + } + + @Test + @EnabledOnJre(JAVA_21) + void testVirtualThread() { + assertVirtualThread(newVirtualThreadPerTaskExecutor, true); + } + + static void assertVirtualThread(ExecutorService executorService, boolean expected) { + Future futureCurrent = executorService.submit(() -> ThreadSupport.isVirtual()); + Future futureGiven = + executorService.submit( + () -> { + Thread thread = Thread.currentThread(); + return ThreadSupport.isVirtual(thread); + }); + try { + assertEquals(expected, futureCurrent.get(), "invalid current thread virtual status"); + assertEquals(expected, futureGiven.get(), "invalid given thread virtual status"); + } catch (Throwable e) { + fail("Can't get thread virtual status", e); + } + } + + @AfterAll + static void afterAll() { + singleThreadExecutor.shutdown(); + if (newVirtualThreadPerTaskExecutor != null) { + newVirtualThreadPerTaskExecutor.shutdown(); + } + } +} diff --git a/components/environment/src/test/java/datadog/environment/ThreadUtilsTest.java b/components/environment/src/test/java/datadog/environment/ThreadUtilsTest.java deleted file mode 100644 index 008897f6199..00000000000 --- a/components/environment/src/test/java/datadog/environment/ThreadUtilsTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package datadog.environment; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.jupiter.api.Test; - -public class ThreadUtilsTest { - @Test - public void threadId() throws InterruptedException { - Thread thread = new Thread("foo"); - thread.start(); - try { - // always works on Thread's where getId isn't overridden by child class - assertEquals(thread.getId(), ThreadUtils.threadId(thread)); - } finally { - thread.join(); - } - } - - @Test - public void supportsVirtualThreads() { - assertEquals( - JavaVersion.getRuntimeVersion().isAtLeast(21), ThreadUtils.supportsVirtualThreads()); - } - - @Test - public void isVirtualThread_false() throws InterruptedException { - Thread thread = new Thread("foo"); - thread.start(); - try { - assertFalse(ThreadUtils.isVirtual(thread)); - } finally { - thread.join(); - } - } - - @Test - public void isCurrentThreadVirtual_false() throws InterruptedException, ExecutionException { - ExecutorService executor = Executors.newSingleThreadExecutor(); - try { - assertFalse(executor.submit(() -> ThreadUtils.isCurrentThreadVirtual()).get()); - } finally { - executor.shutdown(); - } - } - - @Test - public void isVirtualThread_true() throws InterruptedException { - assumeTrue(ThreadUtils.supportsVirtualThreads()); - - Thread vThread = startVirtualThread(() -> {}); - try { - assertTrue(ThreadUtils.isVirtual(vThread)); - } finally { - vThread.join(); - } - } - - @Test - public void isCurrentThreadVirtual_true() throws InterruptedException { - assumeTrue(ThreadUtils.supportsVirtualThreads()); - - AtomicBoolean result = new AtomicBoolean(); - - Thread vThread = - startVirtualThread( - () -> { - result.set(ThreadUtils.isCurrentThreadVirtual()); - }); - - vThread.join(); - assertTrue(result.get()); - } - - /* - * Should only be called on JVMs that support virtual threads - */ - static final Thread startVirtualThread(Runnable runnable) { - MethodHandle h_startVThread; - try { - h_startVThread = - MethodHandles.lookup() - .findStatic( - Thread.class, - "startVirtualThread", - MethodType.methodType(Thread.class, Runnable.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - throw new IllegalStateException(e); - } - - try { - return (Thread) h_startVThread.invoke(runnable); - } catch (Throwable e) { - throw new IllegalStateException(e); - } - } -} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 05c213a9c18..a5fef36c443 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -25,7 +25,7 @@ import datadog.communication.monitor.Monitoring; import datadog.communication.monitor.Recording; import datadog.context.propagation.Propagators; -import datadog.environment.ThreadUtils; +import datadog.environment.ThreadSupport; import datadog.trace.api.ClassloaderConfigurationOverrides; import datadog.trace.api.Config; import datadog.trace.api.DDSpanId; @@ -1047,7 +1047,7 @@ static final ReusableSingleSpanBuilder reuseSingleSpanBuilder( final ReusableSingleSpanBuilderThreadLocalCache tlCache, final String instrumentationName, final CharSequence operationName) { - if (ThreadUtils.isCurrentThreadVirtual()) { + if (ThreadSupport.isVirtual()) { // Since virtual threads are created and destroyed often, // cautiously decided not to create a thread local for the virtual threads.