11package datadog .trace .instrumentation .java .lang .jdk21 ;
22
33import static datadog .trace .agent .tooling .bytebuddy .matcher .NameMatchers .named ;
4- import static datadog .trace .bootstrap .instrumentation .java .concurrent .AdviceUtils .capture ;
5- import static datadog .trace .bootstrap .instrumentation .java .concurrent .AdviceUtils .endTaskScope ;
6- import static datadog .trace .bootstrap .instrumentation .java .concurrent .AdviceUtils .startTaskScope ;
4+ import static datadog .trace .agent .tooling .bytebuddy .matcher .NameMatchers .namedOneOf ;
5+ import static datadog .trace .bootstrap .instrumentation .api .AgentTracer .activeSpan ;
6+ import static datadog .trace .bootstrap .instrumentation .java .concurrent .ConcurrentState .activateAndContinueContinuation ;
7+ import static datadog .trace .bootstrap .instrumentation .java .concurrent .ConcurrentState .captureContinuation ;
8+ import static datadog .trace .bootstrap .instrumentation .java .concurrent .ConcurrentState .closeScope ;
79import static datadog .trace .bootstrap .instrumentation .java .lang .VirtualThreadHelper .AGENT_SCOPE_CLASS_NAME ;
810import static datadog .trace .bootstrap .instrumentation .java .lang .VirtualThreadHelper .VIRTUAL_THREAD_CLASS_NAME ;
911import static net .bytebuddy .matcher .ElementMatchers .isConstructor ;
1618import datadog .trace .bootstrap .ContextStore ;
1719import datadog .trace .bootstrap .InstrumentationContext ;
1820import datadog .trace .bootstrap .instrumentation .api .AgentScope ;
19- import datadog .trace .bootstrap .instrumentation .java .concurrent .State ;
21+ import datadog .trace .bootstrap .instrumentation .java .concurrent .ConcurrentState ;
2022import java .util .HashMap ;
2123import java .util .Map ;
2224import net .bytebuddy .asm .Advice ;
2325import net .bytebuddy .asm .Advice .OnMethodEnter ;
2426import net .bytebuddy .asm .Advice .OnMethodExit ;
2527
2628/**
27- * Instruments {@code VirtualThread} to capture active state at creation, activate it on
28- * continuation mount, and close the scope from activation on continuation unmount.
29+ * Instruments {@code VirtualThread} to capture active state at creation, activate it on mount,
30+ * close the scope on unmount, and cancel the continuation on thread termination.
31+ *
32+ * <p>The lifecycle is as follows:
33+ *
34+ * <ol>
35+ * <li>{@code init()}: captures and holds a continuation from the active context (span due to
36+ * legacy API).
37+ * <li>{@code mount()}: activates the held continuation, restoring the context on the current
38+ * carrier thread.
39+ * <li>{@code unmount()}: closes the scope. The continuation survives as still hold.
40+ * <li>Steps 2-3 repeat on each park/unpark cycle, potentially on different carrier threads.
41+ * <li>{@code afterTerminate()} (for early versions of JDK 21 and 22 before GA), {@code afterDone}
42+ * (for JDK 21 GA above): cancels the held continuation to let the context scope to be closed.
43+ * </ol>
2944 *
3045 * <p>The instrumentation uses two context stores. The first from {@link Runnable} (as {@code
31- * VirtualThread} inherits from {@link Runnable}) to store the captured {@link State} to restore
32- * later. It additionally stores the {@link AgentScope} to be able to close it later as activation /
33- * close is not done around the same method (so passing the scope from {@link OnMethodEnter} /
34- * {@link OnMethodExit} using advice return value is not possible).
46+ * VirtualThread} inherits from {@link Runnable}) to store the captured {@link ConcurrentState} to
47+ * restore later. It additionally stores the {@link AgentScope} to be able to close it later as
48+ * activation / close is not done around the same method (so passing the scope from {@link
49+ * OnMethodEnter} / {@link OnMethodExit} using advice return value is not possible).
50+ *
51+ * <p>{@link ConcurrentState} is used instead of {@code State} because virtual threads can mount and
52+ * unmount multiple times across different carrier threads. The held continuation in {@link
53+ * ConcurrentState} survives multiple activate/close cycles without being consumed, and is
54+ * explicitly canceled on thread termination.
3555 *
3656 * <p>Instrumenting the internal {@code VirtualThread.runContinuation()} method does not work as the
3757 * current thread is still the carrier thread and not a virtual thread. Activating the state when on
@@ -62,7 +82,7 @@ public boolean isEnabled() {
6282 @ Override
6383 public Map <String , String > contextStore () {
6484 Map <String , String > contextStore = new HashMap <>();
65- contextStore .put (Runnable .class .getName (), State .class .getName ());
85+ contextStore .put (Runnable .class .getName (), ConcurrentState .class .getName ());
6686 contextStore .put (VIRTUAL_THREAD_CLASS_NAME , AGENT_SCOPE_CLASS_NAME );
6787 return contextStore ;
6888 }
@@ -72,36 +92,54 @@ public void methodAdvice(MethodTransformer transformer) {
7292 transformer .applyAdvice (isConstructor (), getClass ().getName () + "$Construct" );
7393 transformer .applyAdvice (isMethod ().and (named ("mount" )), getClass ().getName () + "$Activate" );
7494 transformer .applyAdvice (isMethod ().and (named ("unmount" )), getClass ().getName () + "$Close" );
95+ transformer .applyAdvice (
96+ isMethod ().and (namedOneOf ("afterTerminate" , "afterDone" )),
97+ getClass ().getName () + "$Terminate" );
7598 }
7699
77100 public static final class Construct {
78101 @ OnMethodExit (suppress = Throwable .class )
79102 public static void captureScope (@ Advice .This Object virtualThread ) {
80- capture (InstrumentationContext .get (Runnable .class , State .class ), (Runnable ) virtualThread );
103+ captureContinuation (
104+ InstrumentationContext .get (Runnable .class , ConcurrentState .class ),
105+ (Runnable ) virtualThread ,
106+ activeSpan ());
81107 }
82108 }
83109
84110 public static final class Activate {
85111 @ OnMethodExit (suppress = Throwable .class )
86112 public static void activate (@ Advice .This Object virtualThread ) {
87- ContextStore <Runnable , State > stateStore =
88- InstrumentationContext .get (Runnable .class , State .class );
89- ContextStore <Object , Object > scopeStore =
113+ AgentScope scope =
114+ activateAndContinueContinuation (
115+ InstrumentationContext .get (Runnable .class , ConcurrentState .class ),
116+ (Runnable ) virtualThread );
117+ ContextStore <Object , AgentScope > scopeStore =
90118 InstrumentationContext .get (VIRTUAL_THREAD_CLASS_NAME , AGENT_SCOPE_CLASS_NAME );
91- AgentScope agentScope = startTaskScope (stateStore , (Runnable ) virtualThread );
92- scopeStore .put (virtualThread , agentScope );
119+ scopeStore .put (virtualThread , scope );
93120 }
94121 }
95122
96123 public static final class Close {
97124 @ OnMethodEnter (suppress = Throwable .class )
98125 public static void close (@ Advice .This Object virtualThread ) {
99- ContextStore <Object , Object > scopeStore =
126+ ContextStore <Object , AgentScope > scopeStore =
100127 InstrumentationContext .get (VIRTUAL_THREAD_CLASS_NAME , AGENT_SCOPE_CLASS_NAME );
101- Object agentScope = scopeStore .get (virtualThread );
102- if (agentScope instanceof AgentScope ) {
103- endTaskScope ((AgentScope ) agentScope );
104- }
128+ AgentScope scope = scopeStore .remove (virtualThread );
129+ closeScope (
130+ InstrumentationContext .get (Runnable .class , ConcurrentState .class ),
131+ (Runnable ) virtualThread ,
132+ scope ,
133+ null );
134+ }
135+ }
136+
137+ public static final class Terminate {
138+ @ OnMethodEnter (suppress = Throwable .class )
139+ public static void terminate (@ Advice .This Object virtualThread ) {
140+ ConcurrentState .cancelAndClearContinuation (
141+ InstrumentationContext .get (Runnable .class , ConcurrentState .class ),
142+ (Runnable ) virtualThread );
105143 }
106144 }
107145}
0 commit comments