From d1f47ff608aaddf21207dd9008059be210b1b6d1 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Thu, 27 Nov 2025 08:39:08 +0100 Subject: [PATCH 1/3] feat(java-concurrent): Improve java-concurrent-21 instrumentation Add suppressed from Advice Improve test naming --- .../virtualthread/TaskRunnerInstrumentation.java | 9 ++++++--- ...st.groovy => VirtualThreadPerTaskExecutorTest.groovy} | 5 ++--- 2 files changed, 8 insertions(+), 6 deletions(-) rename dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/test/groovy/{VirtualThreadTest.groovy => VirtualThreadPerTaskExecutorTest.groovy} (97%) diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/main/java/datadog/trace/instrumentation/java/concurrent/virtualthread/TaskRunnerInstrumentation.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/main/java/datadog/trace/instrumentation/java/concurrent/virtualthread/TaskRunnerInstrumentation.java index e4e43fa9835..a53e7657282 100644 --- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/main/java/datadog/trace/instrumentation/java/concurrent/virtualthread/TaskRunnerInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/main/java/datadog/trace/instrumentation/java/concurrent/virtualthread/TaskRunnerInstrumentation.java @@ -17,11 +17,14 @@ import datadog.trace.bootstrap.instrumentation.java.concurrent.State; import java.util.Map; import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.OnMethodEnter; +import net.bytebuddy.asm.Advice.OnMethodExit; /** * Instruments {@code TaskRunner}, internal runnable for {@code ThreadPerTaskExecutor} (JDK 19+ as * preview, 21+ as stable), the executor with default virtual thread factory. */ +@SuppressWarnings("unused") @AutoService(InstrumenterModule.class) public final class TaskRunnerInstrumentation extends InstrumenterModule.Tracing implements Instrumenter.ForBootstrap, Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { @@ -51,19 +54,19 @@ public void methodAdvice(MethodTransformer transformer) { } public static final class Construct { - @Advice.OnMethodExit + @OnMethodExit(suppress = Throwable.class) public static void captureScope(@Advice.This Runnable task) { capture(InstrumentationContext.get(Runnable.class, State.class), task); } } public static final class Run { - @Advice.OnMethodEnter + @OnMethodEnter(suppress = Throwable.class) public static AgentScope activate(@Advice.This Runnable task) { return startTaskScope(InstrumentationContext.get(Runnable.class, State.class), task); } - @Advice.OnMethodExit(onThrowable = Throwable.class) + @OnMethodExit(suppress = Throwable.class) public static void close(@Advice.Enter AgentScope scope) { endTaskScope(scope); } diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/test/groovy/VirtualThreadTest.groovy b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/test/groovy/VirtualThreadPerTaskExecutorTest.groovy similarity index 97% rename from dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/test/groovy/VirtualThreadTest.groovy rename to dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/test/groovy/VirtualThreadPerTaskExecutorTest.groovy index 7bb36c7f024..cbdc42a4993 100644 --- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/test/groovy/VirtualThreadTest.groovy +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/test/groovy/VirtualThreadPerTaskExecutorTest.groovy @@ -1,14 +1,13 @@ import datadog.trace.agent.test.InstrumentationSpecification import datadog.trace.api.Trace import datadog.trace.core.DDSpan -import spock.lang.Shared - import java.util.concurrent.Callable import java.util.concurrent.ExecutorCompletionService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import spock.lang.Shared -class VirtualThreadTest extends InstrumentationSpecification { +class VirtualThreadPerTaskExecutorTest extends InstrumentationSpecification { @Shared def executeRunnable = { e, c -> e.execute((Runnable) c) } @Shared From af48d684bc53d3908f0f7f8f0ecf3f70fa71e8e8 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Thu, 27 Nov 2025 08:40:04 +0100 Subject: [PATCH 2/3] feat(java-lang): Add support for VirtualThread Add context tracking for VirtualThread API --- .../java/lang/VirtualThreadHelper.java | 12 ++ .../bytebuddy/matcher/ignored_class_name.trie | 2 + .../java-lang/java-lang-21.0/build.gradle | 32 ++++ .../jdk21/VirtualThreadInstrumentation.java | 107 +++++++++++ .../test/groovy/VirtualThreadApiTest.groovy | 166 ++++++++++++++++++ .../src/test/java/JavaAsyncChild.java | 44 +++++ settings.gradle.kts | 1 + 7 files changed, 364 insertions(+) create mode 100644 dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/VirtualThreadHelper.java create mode 100644 dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/build.gradle create mode 100644 dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java create mode 100644 dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/groovy/VirtualThreadApiTest.groovy create mode 100644 dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/JavaAsyncChild.java diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/VirtualThreadHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/VirtualThreadHelper.java new file mode 100644 index 00000000000..f51ee7d978c --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/VirtualThreadHelper.java @@ -0,0 +1,12 @@ +package datadog.trace.bootstrap.instrumentation.java.lang; + +public final class VirtualThreadHelper { + public static final String VIRTUAL_THREAD_CLASS_NAME = "java.lang.VirtualThread"; + + /** + * {@link datadog.trace.bootstrap.instrumentation.api.AgentScope} class name as string literal. + * This is mandatory for {@link datadog.trace.bootstrap.ContextStore} API call. + */ + public static final String AGENT_SCOPE_CLASS_NAME = + "datadog.trace.bootstrap.instrumentation.api.AgentScope"; +} diff --git a/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie b/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie index df1d38cd392..5430117b39a 100644 --- a/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie +++ b/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie @@ -50,6 +50,8 @@ 0 java.lang.ProcessImpl # allow Runtime instrumentation for RASP 0 java.lang.Runtime +# allow context tracking for VirtualThread +0 java.lang.VirtualThread 0 java.net.http.* 0 java.net.HttpURLConnection 0 java.net.Socket diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/build.gradle b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/build.gradle new file mode 100644 index 00000000000..00757ef832a --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'idea' +} + +apply from: "$rootDir/gradle/java.gradle" +// Use slf4j-simple as default; logback has a high chance of getting stuck in a deadlock on CI. +apply from: "$rootDir/gradle/slf4j-simple.gradle" + +testJvmConstraints { + minJavaVersion = JavaVersion.VERSION_21 +} + +muzzle { + pass { + coreJdk('21') + } +} + +idea { + module { + jdkName = '21' + } +} + +// Set all compile tasks to use JDK21 but let instrumentation code targets 1.8 compatibility +tasks.withType(AbstractCompile).configureEach { + configureCompiler(it, 21, JavaVersion.VERSION_1_8) +} + +dependencies { + testImplementation project(':dd-java-agent:instrumentation:trace-annotation') +} diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java new file mode 100644 index 00000000000..f590299e30e --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java @@ -0,0 +1,107 @@ +package datadog.trace.instrumentation.java.lang.jdk21; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.capture; +import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.endTaskScope; +import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.startTaskScope; +import static datadog.trace.bootstrap.instrumentation.java.lang.VirtualThreadHelper.AGENT_SCOPE_CLASS_NAME; +import static datadog.trace.bootstrap.instrumentation.java.lang.VirtualThreadHelper.VIRTUAL_THREAD_CLASS_NAME; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; + +import com.google.auto.service.AutoService; +import datadog.environment.JavaVirtualMachine; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.InstrumentationContext; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.java.concurrent.State; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.OnMethodEnter; +import net.bytebuddy.asm.Advice.OnMethodExit; + +/** + * Instruments {@code VirtualThread} to capture active state at creation, activate it on + * continuation mount, and close the scope from activation on continuation unmount. + * + *

The instrumentation uses two context stores. The first from {@link Runnable} (as {@code + * VirtualThread} inherits from {@link Runnable}) to store the captured {@link State} to restore + * later. It additionally stores the {@link AgentScope} to be able to close it later as activation / + * close is not done around the same method (so passing the scope from {@link OnMethodEnter} / + * {@link OnMethodExit} using advice return value is not possible). + * + *

Instrumenting the internal {@code VirtualThread.runContinuation()} method does not work as the + * current thread is still the carrier thread and not a virtual thread. Activating the state when on + * the carrier thread (ie a platform thread) would store the active context into ThreadLocal using + * the platform thread as key, making the tracer unable to retrieve the stored context from the + * current virtual thread (ThreadLocal will not return the value associated to the underlying + * platform thread as they are considered to be different). + */ +@SuppressWarnings("unused") +@AutoService(InstrumenterModule.class) +public final class VirtualThreadInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForBootstrap, Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + public VirtualThreadInstrumentation() { + super("java-lang", "java-lang-21", "virtual-thread"); + } + + @Override + public String instrumentedType() { + return VIRTUAL_THREAD_CLASS_NAME; + } + + @Override + public boolean isEnabled() { + return JavaVirtualMachine.isJavaVersionAtLeast(21) && super.isEnabled(); + } + + @Override + public Map contextStore() { + Map contextStore = new HashMap<>(); + contextStore.put(Runnable.class.getName(), State.class.getName()); + contextStore.put(VIRTUAL_THREAD_CLASS_NAME, AGENT_SCOPE_CLASS_NAME); + return contextStore; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice(isConstructor(), getClass().getName() + "$Construct"); + transformer.applyAdvice(isMethod().and(named("mount")), getClass().getName() + "$Activate"); + transformer.applyAdvice(isMethod().and(named("unmount")), getClass().getName() + "$Close"); + } + + public static final class Construct { + @OnMethodExit(suppress = Throwable.class) + public static void captureScope(@Advice.This Object virtualThread) { + capture(InstrumentationContext.get(Runnable.class, State.class), (Runnable) virtualThread); + } + } + + public static final class Activate { + @OnMethodExit(suppress = Throwable.class) + public static void activate(@Advice.This Object virtualThread) { + ContextStore stateStore = + InstrumentationContext.get(Runnable.class, State.class); + ContextStore scopeStore = + InstrumentationContext.get(VIRTUAL_THREAD_CLASS_NAME, AGENT_SCOPE_CLASS_NAME); + AgentScope agentScope = startTaskScope(stateStore, (Runnable) virtualThread); + scopeStore.put(virtualThread, agentScope); + } + } + + public static final class Close { + @OnMethodEnter(suppress = Throwable.class) + public static void close(@Advice.This Object virtualThread) { + ContextStore scopeStore = + InstrumentationContext.get(VIRTUAL_THREAD_CLASS_NAME, AGENT_SCOPE_CLASS_NAME); + Object agentScope = scopeStore.get(virtualThread); + if (agentScope instanceof AgentScope) { + endTaskScope((AgentScope) agentScope); + } + } + } +} diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/groovy/VirtualThreadApiTest.groovy b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/groovy/VirtualThreadApiTest.groovy new file mode 100644 index 00000000000..8b2f201463c --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/groovy/VirtualThreadApiTest.groovy @@ -0,0 +1,166 @@ +import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.api.Trace +import datadog.trace.core.DDSpan + +// Note: test builder x2 + test factory can be refactored but are kept simple to ease with debugging. +class VirtualThreadApiTest extends InstrumentationSpecification { + def "test Thread.Builder.OfVirtual - start()"() { + setup: + def threadBuilder = Thread.ofVirtual().name("builder - started") + + when: + new Runnable() { + @Override + @Trace(operationName = "parent") + void run() { + // this child will have a span + threadBuilder.start(new JavaAsyncChild()) + // this child won't + threadBuilder.start(new JavaAsyncChild(false, false)) + blockUntilChildSpansFinished(1) + } + }.run() + + then: + TEST_WRITER.waitForTraces(1) + List trace = TEST_WRITER.get(0) + + expect: + TEST_WRITER.size() == 1 + trace.size() == 2 + trace.get(0).operationName == "parent" + trace.get(1).operationName == "asyncChild" + trace.get(1).parentId == trace.get(0).spanId + } + + def "test Thread.Builder.OfVirtual - unstarted()"() { + setup: + def threadBuilder = Thread.ofVirtual().name("builder - unstarted") + + when: + new Runnable() { + @Override + @Trace(operationName = "parent") + void run() { + // this child will have a span + threadBuilder.unstarted(new JavaAsyncChild()).start() + // this child won't + threadBuilder.unstarted(new JavaAsyncChild(false, false)).start() + blockUntilChildSpansFinished(1) + } + }.run() + + + then: + TEST_WRITER.waitForTraces(1) + List trace = TEST_WRITER.get(0) + + expect: + TEST_WRITER.size() == 1 + trace.size() == 2 + trace.get(0).operationName == "parent" + trace.get(1).operationName == "asyncChild" + trace.get(1).parentId == trace.get(0).spanId + } + + def "test Thread.startVirtual()"() { + when: + new Runnable() { + @Override + @Trace(operationName = "parent") + void run() { + // this child will have a span + Thread.startVirtualThread(new JavaAsyncChild()) + // this child won't + Thread.startVirtualThread(new JavaAsyncChild(false, false)) + blockUntilChildSpansFinished(1) + } + }.run() + + then: + TEST_WRITER.waitForTraces(1) + List trace = TEST_WRITER.get(0) + + expect: + TEST_WRITER.size() == 1 + trace.size() == 2 + trace.get(0).operationName == "parent" + trace.get(1).operationName == "asyncChild" + trace.get(1).parentId == trace.get(0).spanId + } + + def "test virtual ThreadFactory"() { + setup: + def threadFactory = Thread.ofVirtual().factory() + + when: + new Runnable() { + @Override + @Trace(operationName = "parent") + void run() { + // this child will have a span + threadFactory.newThread(new JavaAsyncChild()).start() + // this child won't + threadFactory.newThread(new JavaAsyncChild(false, false)).start() + blockUntilChildSpansFinished(1) + } + }.run() + + then: + TEST_WRITER.waitForTraces(1) + List trace = TEST_WRITER.get(0) + + expect: + TEST_WRITER.size() == 1 + trace.size() == 2 + trace.get(0).operationName == "parent" + trace.get(1).operationName == "asyncChild" + trace.get(1).parentId == trace.get(0).spanId + } + + def "test nested virtual threads"() { + setup: + def threadBuilder = Thread.ofVirtual() + + when: + new Runnable() { + @Trace(operationName = "parent") + @Override + void run() { + threadBuilder.start(new Runnable() { + @Trace(operationName = "child") + @Override + void run() { + threadBuilder.start(new Runnable() { + @Trace(operationName = "great-child") + @Override + void run() { + println "complete" + } + }) + blockUntilChildSpansFinished(1) + } + }) + blockUntilChildSpansFinished(1) + } + }.run() + + then: + assertTraces(1) { + sortSpansByStart() + trace(3) { + span { + operationName "parent" + } + span { + childOfPrevious() + operationName "child" + } + span { + childOfPrevious() + operationName "great-child" + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/JavaAsyncChild.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/JavaAsyncChild.java new file mode 100644 index 00000000000..b93e43a5fa0 --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/JavaAsyncChild.java @@ -0,0 +1,44 @@ +import datadog.trace.api.Trace; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; + +public class JavaAsyncChild implements Runnable, Callable { + private final AtomicBoolean blockThread; + private final boolean doTraceableWork; + + public JavaAsyncChild() { + this(true, false); + } + + public JavaAsyncChild(final boolean doTraceableWork, final boolean blockThread) { + this.doTraceableWork = doTraceableWork; + this.blockThread = new AtomicBoolean(blockThread); + } + + public void unblock() { + blockThread.set(false); + } + + @Override + public void run() { + runImpl(); + } + + @Override + public Void call() { + runImpl(); + return null; + } + + private void runImpl() { + while (blockThread.get()) { + // busy-wait to block thread + } + if (doTraceableWork) { + asyncChild(); + } + } + + @Trace(operationName = "asyncChild") + private void asyncChild() {} +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 8f99a3a324d..fe19bd62cba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -384,6 +384,7 @@ include( ":dd-java-agent:instrumentation:java:java-lang:java-lang-11.0", ":dd-java-agent:instrumentation:java:java-lang:java-lang-15.0", ":dd-java-agent:instrumentation:java:java-lang:java-lang-17.0", + ":dd-java-agent:instrumentation:java:java-lang:java-lang-21.0", ":dd-java-agent:instrumentation:java:java-lang:java-lang-9.0", ":dd-java-agent:instrumentation:java:java-net:java-net-1.8", ":dd-java-agent:instrumentation:java:java-net:java-net-11.0", From 1934b72f79aa3dd34697c0e871e1825e65ad2c6d Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 5 Dec 2025 08:55:35 +0100 Subject: [PATCH 3/3] feat(java-concurrent): Add smoke test for Thread.ofVirtualThread() --- .../smoketest/concurrent/ConcurrentApp.java | 1 + .../VirtualThreadExecuteCalculator.java | 10 ++--- .../VirtualThreadInvokeAllCalculator.java | 10 ++--- .../VirtualThreadInvokeAnyCalculator.java | 10 ++--- .../VirtualThreadStartCalculator.java | 45 +++++++++++++++++++ ...VirtualThreadSubmitCallableCalculator.java | 10 ++--- ...VirtualThreadSubmitRunnableCalculator.java | 10 ++--- .../concurrent/VirtualThreadTest.groovy | 12 +++++ 8 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadStartCalculator.java diff --git a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/ConcurrentApp.java b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/ConcurrentApp.java index aa58e12286d..99d4e474420 100644 --- a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/ConcurrentApp.java +++ b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/ConcurrentApp.java @@ -17,6 +17,7 @@ public static void main(String[] args) { private static FibonacciCalculator getCalculator(String name) { return switch (name) { + case "virtualThreadStart" -> new VirtualThreadStartCalculator(); case "virtualThreadExecute" -> new VirtualThreadExecuteCalculator(); case "virtualThreadSubmitRunnable" -> new VirtualThreadSubmitRunnableCalculator(); case "virtualThreadSubmitCallable" -> new VirtualThreadSubmitCallableCalculator(); diff --git a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadExecuteCalculator.java b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadExecuteCalculator.java index 119c227f782..58aa348a242 100644 --- a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadExecuteCalculator.java +++ b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadExecuteCalculator.java @@ -20,6 +20,11 @@ public long computeFibonacci(int n) throws ExecutionException, InterruptedExcept return task.result.get(); } + @Override + public void close() { + this.executor.shutdown(); + } + public class FibonacciExecuteTask implements Runnable { private final long n; private final CompletableFuture result; @@ -46,9 +51,4 @@ public void run() { } } } - - @Override - public void close() { - this.executor.shutdown(); - } } diff --git a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadInvokeAllCalculator.java b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadInvokeAllCalculator.java index 67ca6e4a46a..9d2a1a9d28a 100644 --- a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadInvokeAllCalculator.java +++ b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadInvokeAllCalculator.java @@ -23,6 +23,11 @@ public long computeFibonacci(int n) throws ExecutionException, InterruptedExcept return this.executor.invokeAll(of(task)).getFirst().get(); } + @Override + public void close() { + this.executor.shutdown(); + } + public class FibonacciSubmitTask implements Callable { private final long n; @@ -41,9 +46,4 @@ public Long call() throws ExecutionException, InterruptedException { return futures.getFirst().get() + futures.getLast().get(); } } - - @Override - public void close() { - this.executor.shutdown(); - } } diff --git a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadInvokeAnyCalculator.java b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadInvokeAnyCalculator.java index c7b9205e273..11097b21f76 100644 --- a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadInvokeAnyCalculator.java +++ b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadInvokeAnyCalculator.java @@ -21,6 +21,11 @@ public long computeFibonacci(int n) throws ExecutionException, InterruptedExcept return this.executor.invokeAny(of(task)); } + @Override + public void close() { + this.executor.shutdown(); + } + public class FibonacciSubmitTask implements Callable { private final long n; @@ -38,9 +43,4 @@ public Long call() throws ExecutionException, InterruptedException { return executor.invokeAny(of(task1)) + executor.invokeAny(of(task2)); } } - - @Override - public void close() { - this.executor.shutdown(); - } } diff --git a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadStartCalculator.java b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadStartCalculator.java new file mode 100644 index 00000000000..a58a14022eb --- /dev/null +++ b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadStartCalculator.java @@ -0,0 +1,45 @@ +package datadog.smoketest.concurrent; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class VirtualThreadStartCalculator implements FibonacciCalculator { + @Override + public long computeFibonacci(int n) throws ExecutionException, InterruptedException { + FibonacciExecuteTask task = new FibonacciExecuteTask(n); + Thread.startVirtualThread(task); + return task.result.get(); + } + + @Override + public void close() { + } + + public static class FibonacciExecuteTask implements Runnable { + private final long n; + private final CompletableFuture result; + + public FibonacciExecuteTask(long n) { + this.n = n; + this.result = new CompletableFuture<>(); + } + + @WithSpan("compute") + public void run() { + if (this.n <= 1) { + this.result.complete(this.n); + return; + } + FibonacciExecuteTask task1 = new FibonacciExecuteTask(this.n - 1); + FibonacciExecuteTask task2 = new FibonacciExecuteTask(this.n - 2); + Thread.startVirtualThread(task1); + Thread.startVirtualThread(task2); + try { + this.result.complete(task1.result.get() + task2.result.get()); + } catch (InterruptedException | ExecutionException e) { + this.result.completeExceptionally(e); + } + } + } +} diff --git a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadSubmitCallableCalculator.java b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadSubmitCallableCalculator.java index 6281287b413..0043b8fbd9a 100644 --- a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadSubmitCallableCalculator.java +++ b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadSubmitCallableCalculator.java @@ -20,6 +20,11 @@ public long computeFibonacci(int n) throws ExecutionException, InterruptedExcept return this.executor.submit(task).get(); } + @Override + public void close() { + this.executor.shutdown(); + } + public class FibonacciSubmitTask implements Callable { private final long n; @@ -39,9 +44,4 @@ public Long call() throws ExecutionException, InterruptedException { return future1.get() + future2.get(); } } - - @Override - public void close() { - this.executor.shutdown(); - } } diff --git a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadSubmitRunnableCalculator.java b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadSubmitRunnableCalculator.java index d1a0940b4d7..0c1616900e8 100644 --- a/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadSubmitRunnableCalculator.java +++ b/dd-smoke-tests/concurrent/java-21/src/main/java/datadog/smoketest/concurrent/VirtualThreadSubmitRunnableCalculator.java @@ -20,6 +20,11 @@ public long computeFibonacci(int n) throws ExecutionException, InterruptedExcept return task.result.get(); } + @Override + public void close() { + this.executor.shutdown(); + } + public class FibonacciSubmitTask implements Runnable { private final long n; private final CompletableFuture result; @@ -46,9 +51,4 @@ public void run() { } } } - - @Override - public void close() { - this.executor.shutdown(); - } } diff --git a/dd-smoke-tests/concurrent/java-21/src/test/groovy/datadog/smoketest/concurrent/VirtualThreadTest.groovy b/dd-smoke-tests/concurrent/java-21/src/test/groovy/datadog/smoketest/concurrent/VirtualThreadTest.groovy index a3903ba9dc8..84e40e98dd4 100644 --- a/dd-smoke-tests/concurrent/java-21/src/test/groovy/datadog/smoketest/concurrent/VirtualThreadTest.groovy +++ b/dd-smoke-tests/concurrent/java-21/src/test/groovy/datadog/smoketest/concurrent/VirtualThreadTest.groovy @@ -1,5 +1,17 @@ package datadog.smoketest.concurrent +class VirtualThreadStartTest extends AbstractConcurrentTest { + @Override + protected List getTestArguments() { + return ['virtualThreadStart'] + } + + def 'test Thread.startVirtualThread() runnable'() { + expect: + receivedCorrectTrace() + } +} + class VirtualThreadExecuteTest extends AbstractConcurrentTest { @Override protected List getTestArguments() {