11package datadog .trace .instrumentation .java .lang .jdk21 ;
22
3+ import static datadog .context .Context .current ;
4+ import static datadog .context .Context .root ;
35import static datadog .trace .agent .tooling .bytebuddy .matcher .NameMatchers .named ;
4- import static datadog .trace .bootstrap .instrumentation .api .AgentTracer .activeSpan ;
5- import static datadog .trace .bootstrap .instrumentation .java .concurrent .ConcurrentState .activateAndContinueContinuation ;
6- import static datadog .trace .bootstrap .instrumentation .java .concurrent .ConcurrentState .captureContinuation ;
7- import static datadog .trace .bootstrap .instrumentation .java .concurrent .ConcurrentState .closeScope ;
6+ import static datadog .trace .bootstrap .instrumentation .api .AgentTracer .captureActiveSpan ;
87import static datadog .trace .bootstrap .instrumentation .java .concurrent .ExcludeFilter .ExcludeType .RUNNABLE ;
9- import static datadog .trace .bootstrap .instrumentation .java .lang .VirtualThreadHelper .AGENT_SCOPE_CLASS_NAME ;
108import static datadog .trace .bootstrap .instrumentation .java .lang .VirtualThreadHelper .VIRTUAL_THREAD_CLASS_NAME ;
9+ import static datadog .trace .bootstrap .instrumentation .java .lang .VirtualThreadHelper .VIRTUAL_THREAD_STATE_CLASS_NAME ;
1110import static java .util .Collections .singletonList ;
1211import static java .util .Collections .singletonMap ;
1312import static net .bytebuddy .matcher .ElementMatchers .isConstructor ;
1413import static net .bytebuddy .matcher .ElementMatchers .isMethod ;
1514import static net .bytebuddy .matcher .ElementMatchers .takesArguments ;
1615
1716import com .google .auto .service .AutoService ;
17+ import datadog .context .Context ;
1818import datadog .environment .JavaVirtualMachine ;
1919import datadog .trace .agent .tooling .ExcludeFilterProvider ;
2020import datadog .trace .agent .tooling .Instrumenter ;
2121import datadog .trace .agent .tooling .InstrumenterModule ;
2222import datadog .trace .bootstrap .ContextStore ;
2323import datadog .trace .bootstrap .InstrumentationContext ;
24- import datadog .trace .bootstrap .instrumentation .api .AgentScope ;
25- import datadog .trace .bootstrap .instrumentation .java .concurrent .ConcurrentState ;
24+ import datadog .trace .bootstrap .instrumentation .api .AgentScope .Continuation ;
2625import datadog .trace .bootstrap .instrumentation .java .concurrent .ExcludeFilter ;
26+ import datadog .trace .bootstrap .instrumentation .java .lang .VirtualThreadState ;
2727import java .util .Collection ;
28- import java .util .HashMap ;
2928import java .util .Map ;
3029import net .bytebuddy .asm .Advice ;
3130import net .bytebuddy .asm .Advice .OnMethodEnter ;
3231import net .bytebuddy .asm .Advice .OnMethodExit ;
3332
3433/**
35- * Instruments {@code VirtualThread} to capture active state at creation, activate it on mount,
36- * close the scope on unmount, and cancel the continuation on thread termination.
34+ * Instruments {@code VirtualThread} to propagate the context across mount/unmount cycles using
35+ * {@link Context#swap()}, and {@link Continuation} to prevent context scope to complete before the
36+ * thread finishes.
3737 *
3838 * <p>The lifecycle is as follows:
3939 *
4040 * <ol>
41- * <li>{@code init()}: captures and holds a continuation from the active context (span due to
42- * legacy API).
43- * <li>{@code mount()}: activates the held continuation, restoring the context on the current
44- * carrier thread.
45- * <li>{@code unmount()}: closes the scope. The continuation survives as still hold.
41+ * <li>{@code init()}: captures the current {@link Context} and an {@link Continuation} to prevent
42+ * the enclosing context scope from completing early.
43+ * <li>{@code mount()}: swaps the virtual thread's saved context into the carrier thread, saving
44+ * the carrier thread's context.
45+ * <li>{@code unmount()}: swaps the carrier thread's original context back, saving the virtual
46+ * thread's (possibly modified) context for the next mount.
4647 * <li>Steps 2-3 repeat on each park/unpark cycle, potentially on different carrier threads.
47- * <li>{@code afterDone}: cancels the held continuation to let the context scope to be closed.
48+ * <li>{@code afterDone()}: cancels the help continuation, releasing the context scope to be
49+ * closed.
4850 * </ol>
4951 *
50- * <p>The instrumentation uses two context stores. The first from {@link Runnable} (as {@code
51- * VirtualThread} inherits from {@link Runnable}) to store the captured {@link ConcurrentState} to
52- * restore later. It additionally stores the {@link AgentScope} to be able to close it later as
53- * activation / close is not done around the same method (so passing the scope from {@link
54- * OnMethodEnter} / {@link OnMethodExit} using advice return value is not possible).
55- *
56- * <p>{@link ConcurrentState} is used instead of {@code State} because virtual threads can mount and
57- * unmount multiple times across different carrier threads. The held continuation in {@link
58- * ConcurrentState} survives multiple activate/close cycles without being consumed, and is
59- * explicitly canceled on thread termination.
60- *
6152 * <p>Instrumenting the internal {@code VirtualThread.runContinuation()} method does not work as the
6253 * current thread is still the carrier thread and not a virtual thread. Activating the state when on
6354 * the carrier thread (ie a platform thread) would store the active context into ThreadLocal using
6455 * the platform thread as key, making the tracer unable to retrieve the stored context from the
6556 * current virtual thread (ThreadLocal will not return the value associated to the underlying
6657 * platform thread as they are considered to be different).
58+ *
59+ * @see VirtualThreadState
6760 */
6861@ SuppressWarnings ("unused" )
6962@ AutoService (InstrumenterModule .class )
@@ -95,10 +88,7 @@ public boolean isEnabled() {
9588
9689 @ Override
9790 public Map <String , String > contextStore () {
98- Map <String , String > contextStore = new HashMap <>();
99- contextStore .put (Runnable .class .getName (), ConcurrentState .class .getName ());
100- contextStore .put (VIRTUAL_THREAD_CLASS_NAME , AGENT_SCOPE_CLASS_NAME );
101- return contextStore ;
91+ return singletonMap (VIRTUAL_THREAD_CLASS_NAME , VIRTUAL_THREAD_STATE_CLASS_NAME );
10292 }
10393
10494 @ Override
@@ -113,47 +103,51 @@ public void methodAdvice(MethodTransformer transformer) {
113103
114104 public static final class Construct {
115105 @ OnMethodExit (suppress = Throwable .class )
116- public static void capture (@ Advice .This Object virtualThread ) {
117- captureContinuation (
118- InstrumentationContext .get (Runnable .class , ConcurrentState .class ),
119- (Runnable ) virtualThread ,
120- activeSpan ());
106+ public static void afterInit (@ Advice .This Object virtualThread ) {
107+ Context context = current ();
108+ if (context == root ()) {
109+ return ; // no active context to propagate
110+ }
111+ VirtualThreadState state = new VirtualThreadState (context , captureActiveSpan ());
112+ ContextStore <Object , Object > store =
113+ InstrumentationContext .get (VIRTUAL_THREAD_CLASS_NAME , VIRTUAL_THREAD_STATE_CLASS_NAME );
114+ store .put (virtualThread , state );
121115 }
122116 }
123117
124118 public static final class Mount {
125119 @ OnMethodExit (suppress = Throwable .class )
126- public static void activate (@ Advice .This Object virtualThread ) {
127- AgentScope scope =
128- activateAndContinueContinuation (
129- InstrumentationContext .get (Runnable .class , ConcurrentState .class ),
130- (Runnable ) virtualThread );
131- ContextStore <Object , AgentScope > scopeStore =
132- InstrumentationContext .get (VIRTUAL_THREAD_CLASS_NAME , AGENT_SCOPE_CLASS_NAME );
133- scopeStore .put (virtualThread , scope );
120+ public static void onMount (@ Advice .This Object virtualThread ) {
121+ ContextStore <Object , VirtualThreadState > store =
122+ InstrumentationContext .get (VIRTUAL_THREAD_CLASS_NAME , VIRTUAL_THREAD_STATE_CLASS_NAME );
123+ VirtualThreadState state = store .get (virtualThread );
124+ if (state != null ) {
125+ state .onMount ();
126+ }
134127 }
135128 }
136129
137130 public static final class Unmount {
138131 @ OnMethodEnter (suppress = Throwable .class )
139- public static void close (@ Advice .This Object virtualThread ) {
140- ContextStore <Object , AgentScope > scopeStore =
141- InstrumentationContext .get (VIRTUAL_THREAD_CLASS_NAME , AGENT_SCOPE_CLASS_NAME );
142- AgentScope scope = scopeStore .remove (virtualThread );
143- closeScope (
144- InstrumentationContext .get (Runnable .class , ConcurrentState .class ),
145- (Runnable ) virtualThread ,
146- scope ,
147- null );
132+ public static void onUnmount (@ Advice .This Object virtualThread ) {
133+ ContextStore <Object , VirtualThreadState > store =
134+ InstrumentationContext .get (VIRTUAL_THREAD_CLASS_NAME , VIRTUAL_THREAD_STATE_CLASS_NAME );
135+ VirtualThreadState state = store .get (virtualThread );
136+ if (state != null ) {
137+ state .onUnmount ();
138+ }
148139 }
149140 }
150141
151142 public static final class AfterDone {
152143 @ OnMethodEnter (suppress = Throwable .class )
153- public static void clear (@ Advice .This Object virtualThread ) {
154- ConcurrentState .cancelAndClearContinuation (
155- InstrumentationContext .get (Runnable .class , ConcurrentState .class ),
156- (Runnable ) virtualThread );
144+ public static void onTerminate (@ Advice .This Object virtualThread ) {
145+ ContextStore <Object , VirtualThreadState > store =
146+ InstrumentationContext .get (VIRTUAL_THREAD_CLASS_NAME , VIRTUAL_THREAD_STATE_CLASS_NAME );
147+ VirtualThreadState state = store .remove (virtualThread );
148+ if (state != null ) {
149+ state .onTerminate ();
150+ }
157151 }
158152 }
159153}
0 commit comments