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
index 1293e7c2398..c55f24fdf35 100644
--- 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
@@ -1,9 +1,11 @@
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.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan;
+import static datadog.trace.bootstrap.instrumentation.java.concurrent.ConcurrentState.activateAndContinueContinuation;
+import static datadog.trace.bootstrap.instrumentation.java.concurrent.ConcurrentState.captureContinuation;
+import static datadog.trace.bootstrap.instrumentation.java.concurrent.ConcurrentState.closeScope;
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;
@@ -16,7 +18,7 @@
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 datadog.trace.bootstrap.instrumentation.java.concurrent.ConcurrentState;
import java.util.HashMap;
import java.util.Map;
import net.bytebuddy.asm.Advice;
@@ -24,14 +26,32 @@
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.
+ * Instruments {@code VirtualThread} to capture active state at creation, activate it on mount,
+ * close the scope on unmount, and cancel the continuation on thread termination.
+ *
+ *
The lifecycle is as follows:
+ *
+ *
+ *
{@code init()}: captures and holds a continuation from the active context (span due to
+ * legacy API).
+ *
{@code mount()}: activates the held continuation, restoring the context on the current
+ * carrier thread.
+ *
{@code unmount()}: closes the scope. The continuation survives as still hold.
+ *
Steps 2-3 repeat on each park/unpark cycle, potentially on different carrier threads.
+ *
{@code afterTerminate()} (for early versions of JDK 21 and 22 before GA), {@code afterDone}
+ * (for JDK 21 GA above): cancels the held continuation to let the context scope to be closed.
+ *
*
*
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).
+ * VirtualThread} inherits from {@link Runnable}) to store the captured {@link ConcurrentState} 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).
+ *
+ *
{@link ConcurrentState} is used instead of {@code State} because virtual threads can mount and
+ * unmount multiple times across different carrier threads. The held continuation in {@link
+ * ConcurrentState} survives multiple activate/close cycles without being consumed, and is
+ * explicitly canceled on thread termination.
*
*
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
@@ -62,7 +82,7 @@ public boolean isEnabled() {
@Override
public Map contextStore() {
Map contextStore = new HashMap<>();
- contextStore.put(Runnable.class.getName(), State.class.getName());
+ contextStore.put(Runnable.class.getName(), ConcurrentState.class.getName());
contextStore.put(VIRTUAL_THREAD_CLASS_NAME, AGENT_SCOPE_CLASS_NAME);
return contextStore;
}
@@ -72,36 +92,54 @@ 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");
+ transformer.applyAdvice(
+ isMethod().and(namedOneOf("afterTerminate", "afterDone")),
+ getClass().getName() + "$Terminate");
}
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);
+ captureContinuation(
+ InstrumentationContext.get(Runnable.class, ConcurrentState.class),
+ (Runnable) virtualThread,
+ activeSpan());
}
}
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