Skip to content

Commit 76c8e26

Browse files
committed
Retain per-mount context propagation on JDK 21
1 parent 73b3814 commit 76c8e26

3 files changed

Lines changed: 33 additions & 19 deletions

File tree

‎dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/lang/VirtualThreadState.java‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package datadog.trace.bootstrap.instrumentation.java.lang;
22

3+
import static datadog.environment.JavaVirtualMachine.isJavaVersion;
4+
35
import datadog.context.Context;
46
import datadog.context.ContextContinuation;
57
import datadog.trace.api.Config;
@@ -15,10 +17,15 @@
1517
*
1618
* <p>The legacy context manager's {@code swap()} wraps the current scope stack together with the
1719
* context so the original stack can be restored when the context is swapped back; doing that on
18-
* every mount/unmount is costly on the virtual-thread park/unpark hot path. So instead the context
19-
* is seeded once when the virtual thread starts running, then follows the thread across park/unpark
20-
* and carrier migration via its virtual-thread-aware {@code ThreadLocal} scope stack. The profiler
21-
* context, which is keyed by carrier thread, is rebound on mount and cleared on unmount.
20+
* every mount/unmount is costly on the virtual-thread park/unpark hot path. On JDK 22 and later the
21+
* context is instead seeded once when the virtual thread starts running, then follows the thread
22+
* across park/unpark and carrier migration via its virtual-thread-aware {@code ThreadLocal} scope
23+
* stack. The profiler context, which is keyed by carrier thread, is rebound on mount and cleared on
24+
* unmount.
25+
*
26+
* <p>JDK 21 retains the per-mount path because early update releases enter {@code run(Runnable)} on
27+
* the carrier thread before the first mount. Using one implementation for all JDK 21 updates avoids
28+
* relying on the internal lifecycle change introduced in later updates.
2229
*
2330
* <p>With the new context manager {@code swap()} is cheap and drives the profiler through its
2431
* context listener, so we simply swap in on mount and out on unmount.
@@ -27,7 +34,9 @@ public final class VirtualThreadState {
2734
// note: cws is relying on scope listener. This is disabled by default but when enabled
2835
// let's use the full swap logic since otherwise listeners won't be called
2936
private static final boolean USE_PER_MOUNT_CONTEXT =
30-
!InstrumenterConfig.get().isLegacyContextManagerEnabled() || Config.get().isCwsEnabled();
37+
isJavaVersion(21)
38+
|| !InstrumenterConfig.get().isLegacyContextManagerEnabled()
39+
|| Config.get().isCwsEnabled();
3140

3241
/** The virtual thread's saved context (scope stack snapshot). */
3342
private Context context;

‎dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/VirtualThreadInstrumentation.java‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,21 @@
4141
* <ol>
4242
* <li>{@code init()}: captures the current {@link Context} and an {@link ContextContinuation} to
4343
* prevent the enclosing context scope from completing early.
44-
* <li>{@code run(Runnable)}: seeds the virtual thread's saved context once and retains its state
45-
* in the continuation frame across park/unpark cycles.
44+
* <li>On JDK 22 and later, {@code run(Runnable)} seeds the virtual thread's saved context once
45+
* and retains its state in the continuation frame across park/unpark cycles.
4646
* <li>{@code mount()} / {@code unmount()}: rebind and clear carrier-local profiler context. The
47-
* state-backed swap path is retained when context listeners require per-mount notification.
47+
* state-backed swap path is retained on JDK 21 and when context listeners require per-mount
48+
* notification.
4849
* <li>{@code afterDone()} / {@code afterTerminate()} for early VirtualThread support: cancels the
4950
* help continuation, releasing the context scope to be closed.
5051
* </ol>
5152
*
52-
* <p>{@code run(Runnable)} is the one-shot continuation body and executes after the first mount,
53-
* when the current thread is the virtual thread. Its advice local is preserved with the
54-
* continuation across yields. {@code runContinuation()} cannot serve this purpose because it
55-
* executes once per mount and starts while the carrier is still the current thread.
53+
* <p>On JDK 22 and later, {@code run(Runnable)} is the one-shot continuation body and executes
54+
* after the first mount, when the current thread is the virtual thread. Its advice local is
55+
* preserved with the continuation across yields. {@code runContinuation()} cannot serve this
56+
* purpose because it executes once per mount and starts while the carrier is still the current
57+
* thread. JDK 21 retains the per-mount path because this internal ordering differs across its
58+
* update releases.
5659
*
5760
* @see VirtualThreadState
5861
*/
@@ -104,9 +107,11 @@ public Map<String, String> contextStore() {
104107
@Override
105108
public void methodAdvice(MethodTransformer transformer) {
106109
transformer.applyAdvice(isConstructor(), getClass().getName() + "$Construct");
107-
transformer.applyAdvice(
108-
isMethod().and(named("run")).and(takesArguments(Runnable.class)).and(returns(void.class)),
109-
getClass().getName() + "$Run");
110+
if (JavaVirtualMachine.isJavaVersionAtLeast(22)) {
111+
transformer.applyAdvice(
112+
isMethod().and(named("run")).and(takesArguments(Runnable.class)).and(returns(void.class)),
113+
getClass().getName() + "$Run");
114+
}
110115
transformer.applyAdvice(isMethod().and(named("mount")), getClass().getName() + "$Mount");
111116
transformer.applyAdvice(isMethod().and(named("unmount")), getClass().getName() + "$Unmount");
112117
transformer.applyAdvice(

‎dd-java-agent/instrumentation/java/java-lang/java-lang-21.0/src/test/java/testdog/trace/instrumentation/java/lang/jdk21/VirtualThreadLifeCycleTest.java‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package testdog.trace.instrumentation.java.lang.jdk21;
22

3+
import static datadog.environment.JavaVirtualMachine.isJavaVersion;
34
import static datadog.trace.agent.test.assertions.SpanMatcher.span;
45
import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME;
56
import static datadog.trace.agent.test.assertions.TraceMatcher.trace;
67
import static org.junit.jupiter.api.Assertions.assertEquals;
7-
import static org.junit.jupiter.api.Assertions.assertFalse;
88
import static org.junit.jupiter.api.Assertions.assertNotEquals;
99

1010
import datadog.trace.agent.test.AbstractInstrumentationTest;
@@ -23,10 +23,10 @@
2323
public class VirtualThreadLifeCycleTest extends AbstractInstrumentationTest {
2424
private static final Duration TIMEOUT = Duration.ofSeconds(10);
2525

26-
@DisplayName("test seed-once path is selected by default")
26+
@DisplayName("test context path selected for runtime")
2727
@Test
28-
void testSeedOncePathSelectedByDefault() {
29-
assertFalse(VirtualThreadState.usePerMountContext());
28+
void testContextPathSelectedForRuntime() {
29+
assertEquals(isJavaVersion(21), VirtualThreadState.usePerMountContext());
3030
}
3131

3232
@DisplayName("test context restored after virtual thread remounts")

0 commit comments

Comments
 (0)