Repository navigation
Prevent context retention in OpenJDK virtual pollers and HttpClient selector #12626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
gh-worker-dd-mergequeue-cf854d
merged 7 commits into
master
from
andrea.marziali/fix-jdk-poller-context
Oct 12, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
b68ad46
Prevent JDK virtual I/O threads from retaining request context
amarziali 99ccea5
Use a classvalue
amarziali b5dcae4
swap the context
amarziali e8577fc
Merge branch 'master' into andrea.marziali/fix-jdk-poller-context
amarziali 9e53409
try-with-resources
amarziali 4086af5
Add support for jdk27
amarziali 47a4b17
remove redundant codeowner change
amarziali File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
....0/src/main/java/datadog/trace/instrumentation/java/lang/jdk21/PollerInstrumentation.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package datadog.trace.instrumentation.java.lang.jdk21; | ||
|
|
||
| import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; | ||
| import static datadog.trace.bootstrap.instrumentation.api.Java8BytecodeBridge.rootContext; | ||
| import static net.bytebuddy.matcher.ElementMatchers.returns; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArguments; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import datadog.context.Context; | ||
| import datadog.environment.JavaVirtualMachine; | ||
| import datadog.trace.agent.tooling.Instrumenter; | ||
| import datadog.trace.agent.tooling.InstrumenterModule; | ||
| import net.bytebuddy.asm.Advice; | ||
|
|
||
| /** Keeps JDK I/O pollers independent of the request that first needs them. */ | ||
| @AutoService(InstrumenterModule.class) | ||
| public final class PollerInstrumentation extends InstrumenterModule.ContextTracking | ||
| implements Instrumenter.ForBootstrap, Instrumenter.ForKnownTypes, Instrumenter.HasMethodAdvice { | ||
| public PollerInstrumentation() { | ||
| super("java-lang", "java-lang-21", "virtual-thread"); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isEnabled() { | ||
| return JavaVirtualMachine.isJavaVersionAtLeast(22) && super.isEnabled(); | ||
| } | ||
|
|
||
| @Override | ||
| public String[] knownMatchingTypes() { | ||
| return new String[] { | ||
| "sun.nio.ch.Poller$Pollers", | ||
| "sun.nio.ch.Poller$VThreadsPollerGroup", | ||
| "sun.nio.ch.Poller$PollerPerCarrierPollerGroup" | ||
| }; | ||
| } | ||
|
|
||
| @Override | ||
| public void methodAdvice(MethodTransformer transformer) { | ||
| transformer.applyAdvice( | ||
| named("start") | ||
| .and(takesArguments(0)) | ||
| .and(returns(void.class)) | ||
| .or( | ||
| named("startReadPoller") | ||
| .and(takesArguments(0)) | ||
| .and(returns(named("sun.nio.ch.Poller")))), | ||
| getClass().getName() + "$StartAdvice"); | ||
| } | ||
|
|
||
| public static final class StartAdvice { | ||
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static Context enter() { | ||
| // Swap clears the raw context even when the caller has reached the scope-depth limit. | ||
| return rootContext().swap(); | ||
| } | ||
|
|
||
| @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) | ||
| public static void exit(@Advice.Enter Context previous) { | ||
| if (previous != null) { | ||
| previous.swap(); | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
...va/testdog/trace/instrumentation/java/lang/jdk21/HttpClientSelectorContextForkedTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package testdog.trace.instrumentation.java.lang.jdk21; | ||
|
|
||
| import static datadog.trace.agent.test.assertions.SpanMatcher.span; | ||
| import static datadog.trace.agent.test.assertions.TraceMatcher.trace; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; | ||
| import static org.junit.jupiter.api.Assertions.assertNull; | ||
| import static org.junit.jupiter.api.Assertions.assertSame; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.junit.jupiter.api.Assumptions.assumeTrue; | ||
| import static testdog.trace.instrumentation.java.lang.jdk21.PollerContextForkedTest.field; | ||
| import static testdog.trace.instrumentation.java.lang.jdk21.PollerContextForkedTest.virtualThreadState; | ||
|
|
||
| import datadog.context.ContextScope; | ||
| import datadog.trace.agent.test.AbstractInstrumentationTest; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import java.net.http.HttpClient; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class HttpClientSelectorContextForkedTest extends AbstractInstrumentationTest { | ||
| @Test | ||
| void selectorDoesNotRetainCreatingRequest() throws Exception { | ||
| assumeTrue(Runtime.version().feature() >= 26); | ||
| AgentSpan parent = startSpan("test", "client-owner"); | ||
| try (HttpClient client = newHttpClient(parent)) { | ||
| try (ContextScope ignored = activateSpan(parent)) { | ||
| Object impl = field(client.getClass(), "impl").get(client); | ||
| Thread selector = (Thread) field(impl.getClass(), "selmgrThread").get(impl); | ||
| assertTrue(selector.isVirtual()); | ||
| assertTrue(selector.isAlive()); | ||
| assertNull(virtualThreadState(selector), "live selector must not retain request context"); | ||
| assertSame(parent, activeSpan()); | ||
| } finally { | ||
| parent.finish(); | ||
| } | ||
| // Publish while the client is still alive: closing it must not be needed to release parent. | ||
| assertTraces(trace(span().root().operationName("client-owner"))); | ||
| } | ||
| } | ||
|
|
||
| private static HttpClient newHttpClient(Object parentSpan) { | ||
| AgentSpan parent = (AgentSpan) parentSpan; | ||
| try (ContextScope ignored = activateSpan(parent)) { | ||
| return HttpClient.newHttpClient(); | ||
| } catch (Throwable error) { | ||
| parent.finish(); | ||
| throw error; | ||
| } | ||
| } | ||
| } |
100 changes: 100 additions & 0 deletions
100
...java/testdog/trace/instrumentation/java/lang/jdk21/PerCarrierPollerContextForkedTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| package testdog.trace.instrumentation.java.lang.jdk21; | ||
|
|
||
| import static datadog.trace.agent.test.assertions.SpanMatcher.span; | ||
| import static datadog.trace.agent.test.assertions.TraceMatcher.trace; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; | ||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertNull; | ||
| import static org.junit.jupiter.api.Assertions.assertSame; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.junit.jupiter.api.Assumptions.assumeTrue; | ||
| import static testdog.trace.instrumentation.java.lang.jdk21.PollerContextForkedTest.field; | ||
| import static testdog.trace.instrumentation.java.lang.jdk21.PollerContextForkedTest.virtualThreadState; | ||
|
|
||
| import datadog.context.ContextScope; | ||
| import datadog.trace.agent.test.AbstractInstrumentationTest; | ||
| import datadog.trace.agent.tooling.bytebuddy.matcher.GlobalIgnores; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import datadog.trace.test.util.PollingConditions; | ||
| import java.net.InetAddress; | ||
| import java.net.ServerSocket; | ||
| import java.net.Socket; | ||
| import java.time.Duration; | ||
| import java.util.Set; | ||
| import java.util.concurrent.atomic.AtomicReference; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.condition.EnabledOnOs; | ||
| import org.junit.jupiter.api.condition.OS; | ||
|
|
||
| @EnabledOnOs({OS.LINUX, OS.MAC}) | ||
| class PerCarrierPollerContextForkedTest extends AbstractInstrumentationTest { | ||
| @Test | ||
| void firstReadOnCarrierDoesNotRetainRequest() throws Exception { | ||
| assumeTrue(Runtime.version().feature() >= 27); | ||
| // A separate fork is needed: poller mode is fixed by Poller.<clinit>. | ||
| System.setProperty("jdk.pollerMode", "3"); | ||
| System.setProperty("jdk.writePollers", "1"); | ||
| assertFalse(GlobalIgnores.isIgnored("sun.nio.ch.Poller$PollerPerCarrierPollerGroup", false)); | ||
|
|
||
| AgentSpan parent = startSpan("test", "carrier-poller-owner"); | ||
| Object parentSpan = parent; | ||
| AtomicReference<Throwable> failure = new AtomicReference<>(); | ||
| Thread reader = null; | ||
| try (ContextScope ignored = activateSpan(parent)) { | ||
| Class<?> poller = Class.forName("sun.nio.ch.Poller"); | ||
| Object group = field(poller, "POLLER_GROUP").get(null); | ||
| @SuppressWarnings("unchecked") | ||
| Set<Object> readPollers = (Set<Object>) field(group.getClass(), "readPollers").get(group); | ||
| assertTrue(readPollers.isEmpty(), "read pollers must be created lazily by I/O"); | ||
|
|
||
| try (ServerSocket server = new ServerSocket(0, 1, InetAddress.getLoopbackAddress()); | ||
| Socket socket = new Socket(InetAddress.getLoopbackAddress(), server.getLocalPort()); | ||
| Socket peer = server.accept()) { | ||
| socket.setSoTimeout(10_000); | ||
| reader = | ||
| Thread.ofVirtual() | ||
| .start( | ||
| () -> { | ||
| try { | ||
| assertSame(parentSpan, activeSpan()); | ||
| assertEquals(1, socket.getInputStream().read()); | ||
| assertSame( | ||
| parentSpan, | ||
| activeSpan(), | ||
| "poller creation must restore request context"); | ||
| } catch (Throwable error) { | ||
| failure.set(error); | ||
| } | ||
| }); | ||
|
|
||
| new PollingConditions(10) | ||
| .eventually( | ||
| () -> { | ||
| assertFalse(readPollers.isEmpty()); | ||
| for (Object readPoller : readPollers) { | ||
| Thread owner = (Thread) field(poller, "owner").get(readPoller); | ||
| assertTrue(owner != null && owner.isAlive()); | ||
| assertTrue(owner.isVirtual()); | ||
| assertNull( | ||
| virtualThreadState(owner), | ||
| "live carrier poller must retain neither context nor continuation"); | ||
| } | ||
| }); | ||
| peer.getOutputStream().write(1); | ||
| assertTrue(reader.join(Duration.ofSeconds(10))); | ||
| assertNull(failure.get()); | ||
| assertSame(parent, activeSpan()); | ||
| } | ||
| } finally { | ||
| // Closing the sockets also releases the reader if an assertion above fails. | ||
| if (reader != null) { | ||
| reader.join(Duration.ofSeconds(10)); | ||
| } | ||
| parent.finish(); | ||
| } | ||
| assertTraces(trace(span().root().operationName("carrier-poller-owner"))); | ||
| } | ||
| } |
75 changes: 75 additions & 0 deletions
75
.../src/test/java/testdog/trace/instrumentation/java/lang/jdk21/PollerContextForkedTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package testdog.trace.instrumentation.java.lang.jdk21; | ||
|
|
||
| import static datadog.trace.agent.test.assertions.SpanMatcher.span; | ||
| import static datadog.trace.agent.test.assertions.TraceMatcher.trace; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; | ||
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; | ||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertNull; | ||
| import static org.junit.jupiter.api.Assertions.assertSame; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| import datadog.context.ContextScope; | ||
| import datadog.trace.agent.test.AbstractInstrumentationTest; | ||
| import datadog.trace.agent.tooling.bytebuddy.matcher.GlobalIgnores; | ||
| import datadog.trace.bootstrap.FieldBackedContextStores; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import java.lang.reflect.Field; | ||
| import java.util.Set; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.condition.EnabledForJreRange; | ||
| import org.junit.jupiter.api.condition.EnabledOnOs; | ||
| import org.junit.jupiter.api.condition.JRE; | ||
| import org.junit.jupiter.api.condition.OS; | ||
|
|
||
| @EnabledOnOs({OS.LINUX, OS.MAC}) | ||
| class PollerContextForkedTest extends AbstractInstrumentationTest { | ||
| @Test | ||
| @EnabledForJreRange(min = JRE.JAVA_22) | ||
| void firstPollerInitializationDoesNotRetainRequest() throws Exception { | ||
|
amarziali marked this conversation as resolved.
|
||
| // Set these before Poller.<clinit>; changing them afterward cannot reconfigure its pollers. | ||
| // The fork isolates the properties and poller initialization from other tests. | ||
| System.setProperty("jdk.pollerMode", "2"); | ||
| System.setProperty("jdk.readPollers", "1"); | ||
| System.setProperty("jdk.writePollers", "1"); | ||
| assertFalse(GlobalIgnores.isIgnored("sun.nio.ch.Poller$Pollers", false)); | ||
| assertFalse(GlobalIgnores.isIgnored("sun.nio.ch.Poller$VThreadsPollerGroup", false)); | ||
| assertTrue(GlobalIgnores.isIgnored("sun.nio.ch.Poller", false)); | ||
| AgentSpan parent = startSpan("test", "poller-owner"); | ||
| try (ContextScope ignored = activateSpan(parent)) { | ||
| Class<?> poller = Class.forName("sun.nio.ch.Poller"); | ||
| Object pollers = | ||
| field(poller, Runtime.version().feature() >= 27 ? "POLLER_GROUP" : "POLLERS").get(null); | ||
| Object executor = field(pollers.getClass(), "executor").get(pollers); | ||
| @SuppressWarnings("unchecked") | ||
| Set<Thread> threads = (Set<Thread>) field(executor.getClass(), "threads").get(executor); | ||
| assertEquals(2, threads.size(), "one read poller and one write poller must be running"); | ||
| for (Thread thread : threads) { | ||
| assertTrue(thread.isVirtual()); | ||
| assertTrue(thread.isAlive()); | ||
| assertNull( | ||
| virtualThreadState(thread), "poller must retain neither context nor continuation"); | ||
| } | ||
| assertSame(parent, activeSpan(), "poller startup must restore the caller context"); | ||
| } finally { | ||
| parent.finish(); | ||
| } | ||
| assertTraces(trace(span().root().operationName("poller-owner"))); | ||
| } | ||
|
|
||
| static Object virtualThreadState(Thread thread) { | ||
| int storeId = | ||
| FieldBackedContextStores.getContextStoreId( | ||
| "java.lang.VirtualThread", | ||
| "datadog.trace.bootstrap.instrumentation.java.lang.VirtualThreadState"); | ||
| return FieldBackedContextStores.getContextStore(storeId).get(thread); | ||
| } | ||
|
|
||
| static Field field(Class<?> type, String name) throws NoSuchFieldException { | ||
| Field field = type.getDeclaredField(name); | ||
| field.setAccessible(true); | ||
| return field; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.