diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/AdviceUtils.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/AdviceUtils.java
index 9fe033d7632..d536f859289 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/AdviceUtils.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/AdviceUtils.java
@@ -41,6 +41,19 @@ public static ContextScope startTaskScope(State state) {
return null;
}
+ @Nullable
+ public static ContextScope startTpeTaskScope(State state) {
+ if (state != null) {
+ final State.TpeContinuation continuation = state.getAndResetTpeContinuation();
+ if (continuation != null) {
+ final ContextScope scope = continuation.resume();
+ continuation.stopTiming();
+ return scope;
+ }
+ }
+ return null;
+ }
+
public static void endTaskScope(final ContextScope scope) {
if (null != scope) {
scope.close();
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/State.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/State.java
index 48cfe9078de..37c9b9e9d96 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/State.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/State.java
@@ -40,6 +40,46 @@ public boolean captureAndSetContinuation(final Context context) {
return false;
}
+ /**
+ * Captures context for an unwrapped {@code ThreadPoolExecutor} submission.
+ *
+ *
The tagged continuation can only be consumed by {@code beforeExecute}; regular Runnable
+ * advice deliberately ignores it. This prevents a direct invocation, or another submission of the
+ * same Runnable, from stealing the queued submission's context.
+ */
+ @Nullable
+ public TpeContinuation captureAndSetTpeContinuation(final Context context) {
+ while (true) {
+ ContextContinuation current = CONTINUATION.get(this);
+ if (current == null) {
+ if (!CONTINUATION.compareAndSet(this, null, CLAIMED)) {
+ continue;
+ }
+ try {
+ TpeContinuation continuation = new TpeContinuation(context.capture());
+ CONTINUATION.lazySet(this, continuation);
+ return continuation;
+ } catch (Throwable error) {
+ CONTINUATION.compareAndSet(this, CLAIMED, null);
+ throw error;
+ }
+ }
+ if (current == CLAIMED || current instanceof TpeContinuation) {
+ return null;
+ }
+ // Generic Executor advice can run before ThreadPoolExecutor advice for the same call.
+ // Transfer
+ // that continuation instead of treating the duplicate instrumentation as task reuse.
+ if (current.context() != context) {
+ return null;
+ }
+ TpeContinuation continuation = new TpeContinuation(current);
+ if (CONTINUATION.compareAndSet(this, current, continuation)) {
+ return continuation;
+ }
+ }
+ }
+
public boolean setOrCancelContinuation(final ContextContinuation continuation) {
if (CONTINUATION.compareAndSet(this, null, CLAIMED)) {
// lazy write is guaranteed to be seen by getAndSet
@@ -58,6 +98,28 @@ public void closeContinuation() {
}
}
+ @Nullable
+ public ContextContinuation getContinuation() {
+ ContextContinuation continuation = CONTINUATION.get(this);
+ return continuation == CLAIMED || continuation instanceof TpeContinuation ? null : continuation;
+ }
+
+ @Nullable
+ public ContextContinuation getCancellableContinuation() {
+ ContextContinuation continuation = CONTINUATION.get(this);
+ return continuation == CLAIMED ? null : continuation;
+ }
+
+ public void closeContinuation(ContextContinuation expected) {
+ if (expected != null && CONTINUATION.compareAndSet(this, expected, null)) {
+ if (expected instanceof TpeContinuation) {
+ ((TpeContinuation) expected).cancel();
+ } else {
+ expected.release();
+ }
+ }
+ }
+
public Context getContext() {
ContextContinuation continuation = CONTINUATION.get(this);
if (null == continuation || CLAIMED == continuation) {
@@ -68,20 +130,54 @@ public Context getContext() {
@Nullable
public ContextContinuation getAndResetContinuation() {
- ContextContinuation continuation = CONTINUATION.get(this);
- if (null == continuation || CLAIMED == continuation) {
- return null;
+ while (true) {
+ ContextContinuation continuation = CONTINUATION.get(this);
+ if (null == continuation
+ || CLAIMED == continuation
+ || continuation instanceof TpeContinuation) {
+ return null;
+ }
+ if (CONTINUATION.compareAndSet(this, continuation, null)) {
+ return continuation;
+ }
+ }
+ }
+
+ @Nullable
+ public TpeContinuation getAndResetTpeContinuation() {
+ while (true) {
+ ContextContinuation continuation = CONTINUATION.get(this);
+ if (!(continuation instanceof TpeContinuation)) {
+ return null;
+ }
+ if (CONTINUATION.compareAndSet(this, continuation, null)) {
+ return (TpeContinuation) continuation;
+ }
}
- CONTINUATION.compareAndSet(this, continuation, null);
- return continuation;
+ }
+
+ @Nullable
+ public TpeContinuation getTpeContinuation() {
+ ContextContinuation continuation = CONTINUATION.get(this);
+ return continuation instanceof TpeContinuation ? (TpeContinuation) continuation : null;
+ }
+
+ public void closeTpeContinuation(TpeContinuation expected) {
+ closeContinuation(expected);
}
public void setTiming(Timing timing) {
- TIMING.lazySet(this, timing);
+ TpeContinuation continuation = getTpeContinuation();
+ if (continuation != null) {
+ continuation.setTiming(timing);
+ } else {
+ TIMING.lazySet(this, timing);
+ }
}
public boolean isTimed() {
- return TIMING.get(this) != null;
+ TpeContinuation continuation = getTpeContinuation();
+ return TIMING.get(this) != null || (continuation != null && continuation.isTimed());
}
public void stopTiming() {
@@ -90,4 +186,55 @@ public void stopTiming() {
QueueTimerHelper.stopQueuingTimer(timing);
}
}
+
+ public static final class TpeContinuation implements ContextContinuation {
+ private final ContextContinuation delegate;
+ private volatile Timing timing;
+
+ private TpeContinuation(ContextContinuation delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public ContextContinuation hold() {
+ delegate.hold();
+ return this;
+ }
+
+ @Override
+ public Context context() {
+ return delegate.context();
+ }
+
+ @Override
+ public datadog.context.ContextScope resume() {
+ return delegate.resume();
+ }
+
+ @Override
+ public void release() {
+ delegate.release();
+ }
+
+ private void setTiming(Timing timing) {
+ this.timing = timing;
+ }
+
+ private boolean isTimed() {
+ return timing != null;
+ }
+
+ public void stopTiming() {
+ Timing timing = this.timing;
+ this.timing = null;
+ if (timing != null) {
+ QueueTimerHelper.stopQueuingTimer(timing);
+ }
+ }
+
+ private void cancel() {
+ release();
+ stopTiming();
+ }
+ }
}
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelper.java
index e95a6580849..b3354f98a6d 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelper.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelper.java
@@ -1,14 +1,23 @@
package datadog.trace.bootstrap.instrumentation.java.concurrent;
+import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.shouldCapture;
import static datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter.ExcludeType.RUNNABLE;
import static datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter.exclude;
+import datadog.context.Context;
+import datadog.context.ContextContinuation;
import datadog.context.ContextScope;
import datadog.trace.api.GenericClassValue;
import datadog.trace.api.InstrumenterConfig;
import datadog.trace.api.Platform;
import datadog.trace.bootstrap.ContextStore;
import java.util.Set;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
/**
@@ -63,17 +72,43 @@ public static boolean shouldPropagate(ThreadPoolExecutor executor) {
&& PROPAGATE.get(executor.getClass());
}
- public static void capture(ContextStore contextStore, Runnable task) {
+ public static Runnable captureOrWrap(
+ ContextStore contextStore,
+ Runnable task,
+ Context context,
+ ThreadPoolExecutor executor) {
+ if (task != null && !exclude(RUNNABLE, task) && shouldCapture(context)) {
+ State state = contextStore.getOrCreate(task, State.FACTORY);
+ if (state.captureAndSetTpeContinuation(context) == null) {
+ return canWrapCollision(executor.getQueue()) ? Wrapper.wrap(task, context) : null;
+ }
+ }
+ return task;
+ }
+
+ /**
+ * Retains the historical best-effort propagation for subclass overrides outside JDK admission.
+ */
+ public static void captureLegacy(ContextStore contextStore, Runnable task) {
if (task != null && !exclude(RUNNABLE, task)) {
AdviceUtils.capture(contextStore, task);
}
}
+ private static boolean canWrapCollision(BlockingQueue queue) {
+ return queue instanceof ArrayBlockingQueue
+ || queue instanceof LinkedBlockingQueue
+ || queue instanceof LinkedBlockingDeque
+ || queue instanceof LinkedTransferQueue
+ || queue instanceof SynchronousQueue;
+ }
+
public static ContextScope startScope(ContextStore contextStore, Runnable task) {
if (task == null || exclude(RUNNABLE, task)) {
return null;
}
- return AdviceUtils.startTaskScope(contextStore, task);
+ State state = contextStore.get(task);
+ return AdviceUtils.startTpeTaskScope(state);
}
public static void setThreadLocalScope(ContextScope scope, Runnable task) {
@@ -106,10 +141,53 @@ public static void endScope(ContextScope scope, Runnable task) {
AdviceUtils.endTaskScope(scope);
}
- public static void cancelTask(ContextStore contextStore, Runnable task) {
+ public static final class RejectedTask {
+ private final Wrapper> wrapper;
+ private final ContextContinuation continuation;
+ private final ContextScope scope;
+
+ public RejectedTask(Wrapper> wrapper, ContextContinuation continuation) {
+ this.wrapper = wrapper;
+ this.continuation = continuation;
+ this.scope =
+ wrapper != null
+ ? wrapper.activate()
+ : continuation == null ? null : continuation.resume();
+ }
+
+ public void close() {
+ try {
+ if (scope != null) {
+ scope.close();
+ }
+ } finally {
+ if (wrapper != null) {
+ wrapper.cancel();
+ } else if (continuation != null) {
+ continuation.release();
+ }
+ }
+ }
+ }
+
+ public static ContextContinuation prepareRejectedTask(
+ ContextStore contextStore, Runnable task) {
if (task == null || exclude(RUNNABLE, task)) {
- return;
+ return null;
+ }
+ State state = contextStore.get(task);
+ if (state == null) {
+ return null;
+ }
+ State.TpeContinuation tpeContinuation = state.getAndResetTpeContinuation();
+ if (tpeContinuation != null) {
+ tpeContinuation.stopTiming();
+ return tpeContinuation;
+ }
+ ContextContinuation continuation = state.getAndResetContinuation();
+ if (continuation != null) {
+ state.stopTiming();
}
- AdviceUtils.cancelTask(contextStore, task);
+ return continuation;
}
}
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/Wrapper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/Wrapper.java
index a26cc0c4535..22e1ab5cea4 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/Wrapper.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/Wrapper.java
@@ -13,23 +13,41 @@ public class Wrapper implements Runnable, AutoCloseable {
@SuppressWarnings({"unchecked", "rawtypes"})
public static Runnable wrap(T task) {
- if (task instanceof Wrapper
- || task instanceof RunnableFuture
- || task == null
- || exclude(RUNNABLE, task)) {
+ if (!isWrappable(task)) {
return task;
}
ContextContinuation continuation = captureActiveSpan();
if (continuation.context() != Context.root()) {
- if (task instanceof Comparable) {
- return new ComparableRunnable(task, continuation);
- }
- return new Wrapper<>(task, continuation);
+ return newWrapper(task, continuation);
}
// don't wrap unless there is scope to propagate
return task;
}
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public static Runnable wrap(T task, Context context) {
+ if (context == Context.root() || !isWrappable(task)) {
+ return task;
+ }
+ return newWrapper(task, context.capture());
+ }
+
+ private static boolean isWrappable(Runnable task) {
+ return task != null
+ && !(task instanceof Wrapper)
+ && !(task instanceof RunnableFuture)
+ && !exclude(RUNNABLE, task);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private static Runnable newWrapper(
+ T task, ContextContinuation continuation) {
+ if (task instanceof Comparable) {
+ return new ComparableRunnable(task, continuation);
+ }
+ return new Wrapper<>(task, continuation);
+ }
+
public static Runnable unwrap(Runnable task) {
return task instanceof Wrapper ? ((Wrapper>) task).unwrap() : task;
}
@@ -59,7 +77,7 @@ public T unwrap() {
return delegate;
}
- private ContextScope activate() {
+ public ContextScope activate() {
return null == continuation ? null : continuation.resume();
}
diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/java/concurrent/StateTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/java/concurrent/StateTest.java
new file mode 100644
index 00000000000..a6a621d4dd0
--- /dev/null
+++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/java/concurrent/StateTest.java
@@ -0,0 +1,110 @@
+package datadog.trace.bootstrap.instrumentation.java.concurrent;
+
+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.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import datadog.context.Context;
+import datadog.context.ContextContinuation;
+import datadog.trace.api.profiling.Timing;
+import org.junit.jupiter.api.Test;
+
+class StateTest {
+
+ @Test
+ void regularAdviceCannotConsumeTpeContinuation() {
+ State state = State.FACTORY.create();
+ Context context = mock(Context.class);
+ ContextContinuation delegate = mock(ContextContinuation.class);
+ when(context.capture()).thenReturn(delegate);
+
+ State.TpeContinuation continuation = state.captureAndSetTpeContinuation(context);
+
+ assertNull(state.getAndResetContinuation());
+ assertSame(continuation, state.getAndResetTpeContinuation());
+ }
+
+ @Test
+ void overlappingTpeCaptureDoesNotReplaceOwner() {
+ State state = State.FACTORY.create();
+ Context first = mock(Context.class);
+ Context second = mock(Context.class);
+ ContextContinuation firstDelegate = mock(ContextContinuation.class);
+ when(first.capture()).thenReturn(firstDelegate);
+
+ State.TpeContinuation continuation = state.captureAndSetTpeContinuation(first);
+
+ assertNull(state.captureAndSetTpeContinuation(second));
+ assertSame(continuation, state.getAndResetTpeContinuation());
+ verify(second, never()).capture();
+ }
+
+ @Test
+ void transfersDuplicateGenericCaptureForSameSubmission() {
+ State state = State.FACTORY.create();
+ Context context = mock(Context.class);
+ ContextContinuation delegate = mock(ContextContinuation.class);
+ when(context.capture()).thenReturn(delegate);
+ when(delegate.context()).thenReturn(context);
+ state.captureAndSetContinuation(context);
+
+ State.TpeContinuation continuation = state.captureAndSetTpeContinuation(context);
+
+ assertSame(continuation, state.getAndResetTpeContinuation());
+ verify(context).capture();
+ }
+
+ @Test
+ void staleCancellationCannotCancelLaterSubmission() {
+ State state = State.FACTORY.create();
+ Context first = mock(Context.class);
+ Context second = mock(Context.class);
+ ContextContinuation firstDelegate = mock(ContextContinuation.class);
+ ContextContinuation secondDelegate = mock(ContextContinuation.class);
+ when(first.capture()).thenReturn(firstDelegate);
+ when(second.capture()).thenReturn(secondDelegate);
+
+ State.TpeContinuation stale = state.captureAndSetTpeContinuation(first);
+ assertSame(stale, state.getAndResetTpeContinuation());
+ State.TpeContinuation current = state.captureAndSetTpeContinuation(second);
+
+ state.closeTpeContinuation(stale);
+
+ assertSame(current, state.getAndResetTpeContinuation());
+ verify(firstDelegate, never()).release();
+ verify(secondDelegate, never()).release();
+ }
+
+ @Test
+ void exactCancellationReleasesOwnedContinuation() {
+ State state = State.FACTORY.create();
+ Context context = mock(Context.class);
+ ContextContinuation delegate = mock(ContextContinuation.class);
+ when(context.capture()).thenReturn(delegate);
+ State.TpeContinuation continuation = state.captureAndSetTpeContinuation(context);
+
+ state.closeTpeContinuation(continuation);
+
+ assertNull(state.getAndResetTpeContinuation());
+ verify(delegate).release();
+ }
+
+ @Test
+ void taggedContinuationOwnsQueueTiming() {
+ State state = State.FACTORY.create();
+ Context context = mock(Context.class);
+ ContextContinuation delegate = mock(ContextContinuation.class);
+ Timing timing = mock(Timing.class);
+ when(context.capture()).thenReturn(delegate);
+ State.TpeContinuation continuation = state.captureAndSetTpeContinuation(context);
+
+ state.setTiming(timing);
+
+ assertTrue(state.isTimed());
+ assertSame(continuation, state.getAndResetTpeContinuation());
+ }
+}
diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelperTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelperTest.java
new file mode 100644
index 00000000000..d2bd193aa03
--- /dev/null
+++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/java/concurrent/TPEHelperTest.java
@@ -0,0 +1,28 @@
+package datadog.trace.bootstrap.instrumentation.java.concurrent;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import datadog.context.Context;
+import datadog.trace.bootstrap.ContextStore;
+import java.util.concurrent.ThreadPoolExecutor;
+import org.junit.jupiter.api.Test;
+
+class TPEHelperTest {
+
+ @Test
+ void doesNotInspectQueueWhenThereIsNoContextToCapture() {
+ @SuppressWarnings("unchecked")
+ ContextStore contextStore = mock(ContextStore.class);
+ ThreadPoolExecutor executor = mock(ThreadPoolExecutor.class);
+ Runnable task = mock(Runnable.class);
+
+ assertSame(task, TPEHelper.captureOrWrap(contextStore, task, Context.root(), executor));
+
+ verify(executor, never()).getQueue();
+ verifyNoInteractions(contextStore);
+ }
+}
diff --git a/dd-java-agent/benchmark/build.gradle b/dd-java-agent/benchmark/build.gradle
index 178eefd2def..d12ecd99338 100644
--- a/dd-java-agent/benchmark/build.gradle
+++ b/dd-java-agent/benchmark/build.gradle
@@ -5,6 +5,12 @@ plugins {
apply from: "$rootDir/gradle/java.gradle"
dependencies {
+ jmh(project(':internal-api')) {
+ transitive = false
+ }
+ jmh(project(':components:context')) {
+ transitive = false
+ }
jmh project(':dd-trace-api')
jmh libs.bytebuddyagent
}
@@ -38,8 +44,15 @@ jmh {
jmhVersion = libs.versions.jmh.get()
}
+// Copy the agent to a fixed path because @Fork arguments must be compile-time constants.
+def agentJarForBenchmarks = tasks.register('agentJarForBenchmarks', Copy) {
+ from project(':dd-java-agent').tasks.named('shadowJar')
+ into layout.buildDirectory.dir('agent')
+ rename { 'dd-java-agent.jar' }
+}
+
tasks.named('jmh') {
- dependsOn ':dd-java-agent:shadowJar'
+ dependsOn agentJarForBenchmarks
}
/*
@@ -48,4 +61,3 @@ tasks.named('jmh') {
(using https://github.com/brendangregg/FlameGraph)
./flamegraph.pl --color=java dd-java-agent/benchmark/build/reports/jmh/profiler-cleaned.txt > dd-java-agent/benchmark/build/reports/jmh/jmh-master.svg
*/
-
diff --git a/dd-java-agent/benchmark/src/jmh/java/executorbench/RunnableSubmissionBenchmark.java b/dd-java-agent/benchmark/src/jmh/java/executorbench/RunnableSubmissionBenchmark.java
new file mode 100644
index 00000000000..1502d945e3a
--- /dev/null
+++ b/dd-java-agent/benchmark/src/jmh/java/executorbench/RunnableSubmissionBenchmark.java
@@ -0,0 +1,326 @@
+package executorbench;
+
+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 datadog.trace.bootstrap.instrumentation.api.AgentScope;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Measures context propagation through standard and subclassed {@link ThreadPoolExecutor}s.
+ *
+ * The reusable, fresh, and root-context cases include a worker handoff. The overlapping case
+ * blocks the worker outside the measured interval so both submissions contend for the same task's
+ * ownership slot; its score covers two submissions. Allocation can be inspected with {@code -prof
+ * gc} for all cases except the overlapping benchmark, whose invocation-level fixture allocation is
+ * included in the profiler result.
+ *
+ *
Run from {@code dd-java-agent/benchmark} so the relative agent path resolves:
+ *
+ *
{@code
+ * java -jar build/libs/benchmark-*-jmh.jar 'RunnableSubmissionBenchmark.*' -prof gc
+ * }
+ *
+ * To compare commits with the same harness, replace {@code build/agent/dd-java-agent.jar} with
+ * each commit's agent before a run. Do not rebuild the JMH jar between arms.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(
+ value = 3,
+ jvmArgsAppend = {
+ "-javaagent:build/agent/dd-java-agent.jar",
+ "-Ddd.profiling.enabled=false",
+ "-Ddd.profiling.queueing.time.enabled=false",
+ "-Ddd.instrumentation.telemetry.enabled=false",
+ "-Ddd.remote_config.enabled=false",
+ "-Ddd.jmxfetch.enabled=false"
+ })
+public class RunnableSubmissionBenchmark {
+
+ @Benchmark
+ public void activeReusableTask(ExecutorState state) throws InterruptedException {
+ state.submitActive(state.reusableTask);
+ }
+
+ @Benchmark
+ public void activeFreshTask(ExecutorState state) throws InterruptedException {
+ state.submitActive(new CompletionTask(state, state.completion));
+ }
+
+ @Benchmark
+ public void rootReusableTask(ExecutorState state) throws InterruptedException {
+ state.submitRoot(state.reusableTask);
+ }
+
+ @Benchmark
+ public void activeLambda(ExecutorState state) throws InterruptedException {
+ state.submitActiveLambda();
+ }
+
+ @Benchmark
+ public void overlappingReusableTask(CollisionState state) {
+ state.submitOverlapping();
+ }
+
+ @State(Scope.Thread)
+ public static class ExecutorState {
+ @Param({"base", "delegating", "nondelegating"})
+ public String executorType;
+
+ private ThreadPoolExecutor executor;
+ private Phaser completion;
+ private CompletionTask reusableTask;
+ private AgentSpan span;
+ private volatile boolean propagationFailed;
+
+ @Setup(Level.Trial)
+ public void setup() {
+ executor = newExecutor(executorType);
+ executor.prestartAllCoreThreads();
+ completion = new Phaser(1);
+ reusableTask = new CompletionTask(this, completion);
+ span = startSpan("benchmark", "runnable-submission");
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() throws InterruptedException {
+ closeExecutor(executor);
+ span.finish();
+ if (propagationFailed) {
+ throw new AssertionError("task did not receive the submitted span");
+ }
+ }
+
+ private void submitActive(CompletionTask task) throws InterruptedException {
+ int phase = completion.getPhase();
+ task.expected = span;
+ try (AgentScope ignored = activateSpan(span)) {
+ executor.execute(task);
+ }
+ completion.awaitAdvanceInterruptibly(phase);
+ }
+
+ private void submitRoot(CompletionTask task) throws InterruptedException {
+ int phase = completion.getPhase();
+ task.expected = null;
+ executor.execute(task);
+ completion.awaitAdvanceInterruptibly(phase);
+ }
+
+ private void submitActiveLambda() throws InterruptedException {
+ int phase = completion.getPhase();
+ try (AgentScope ignored = activateSpan(span)) {
+ executor.execute(
+ () -> {
+ if (activeSpan() != span) {
+ propagationFailed = true;
+ }
+ completion.arrive();
+ });
+ }
+ completion.awaitAdvanceInterruptibly(phase);
+ }
+ }
+
+ @State(Scope.Thread)
+ public static class CollisionState {
+ @Param({"base", "delegating"})
+ public String executorType;
+
+ private ThreadPoolExecutor executor;
+ private BlockingTask blocker;
+ private CollisionTask task;
+ private AgentSpan firstSpan;
+ private AgentSpan secondSpan;
+
+ @Setup(Level.Invocation)
+ public void setup() throws InterruptedException {
+ executor = newExecutor(executorType);
+ executor.prestartAllCoreThreads();
+ blocker = new BlockingTask();
+ executor.execute(blocker);
+ blocker.started.await();
+ firstSpan = startSpan("benchmark", "first-submission");
+ secondSpan = startSpan("benchmark", "second-submission");
+ task = new CollisionTask(firstSpan, secondSpan);
+ }
+
+ @TearDown(Level.Invocation)
+ public void tearDown() throws InterruptedException {
+ blocker.release.countDown();
+ task.finished.await();
+ closeExecutor(executor);
+ firstSpan.finish();
+ secondSpan.finish();
+ if (task.propagationFailed) {
+ throw new AssertionError("overlapping submissions did not keep their own spans");
+ }
+ }
+
+ private void submitOverlapping() {
+ try (AgentScope ignored = activateSpan(firstSpan)) {
+ executor.execute(task);
+ }
+ try (AgentScope ignored = activateSpan(secondSpan)) {
+ executor.execute(task);
+ }
+ }
+ }
+
+ private static ThreadPoolExecutor newExecutor(String executorType) {
+ switch (executorType) {
+ case "base":
+ return new ThreadPoolExecutor(
+ 1, 1, 0, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(16));
+ case "delegating":
+ return new DelegatingExecutor();
+ case "nondelegating":
+ return new NonDelegatingExecutor();
+ default:
+ throw new IllegalArgumentException("Unknown executor type: " + executorType);
+ }
+ }
+
+ private static void closeExecutor(ThreadPoolExecutor executor) throws InterruptedException {
+ if (executor instanceof NonDelegatingExecutor) {
+ ((NonDelegatingExecutor) executor).closeWorker();
+ }
+ executor.shutdownNow();
+ if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
+ throw new AssertionError("executor did not terminate");
+ }
+ }
+
+ private static final class CompletionTask implements Runnable {
+ private final ExecutorState owner;
+ private final Phaser completion;
+ private volatile AgentSpan expected;
+
+ private CompletionTask(ExecutorState owner, Phaser completion) {
+ this.owner = owner;
+ this.completion = completion;
+ }
+
+ @Override
+ public void run() {
+ if (activeSpan() != expected) {
+ owner.propagationFailed = true;
+ }
+ completion.arrive();
+ }
+ }
+
+ private static final class CollisionTask implements Runnable {
+ private final AgentSpan firstExpected;
+ private final AgentSpan secondExpected;
+ private final CountDownLatch finished = new CountDownLatch(2);
+ private int execution;
+ private volatile boolean propagationFailed;
+
+ private CollisionTask(AgentSpan firstExpected, AgentSpan secondExpected) {
+ this.firstExpected = firstExpected;
+ this.secondExpected = secondExpected;
+ }
+
+ @Override
+ public void run() {
+ AgentSpan expected = execution++ == 0 ? firstExpected : secondExpected;
+ if (activeSpan() != expected) {
+ propagationFailed = true;
+ }
+ finished.countDown();
+ }
+ }
+
+ private static final class BlockingTask implements Runnable {
+ private final CountDownLatch started = new CountDownLatch(1);
+ private final CountDownLatch release = new CountDownLatch(1);
+
+ @Override
+ public void run() {
+ started.countDown();
+ try {
+ release.await();
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ private static final class DelegatingExecutor extends ThreadPoolExecutor {
+ private DelegatingExecutor() {
+ super(1, 1, 0, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(16));
+ }
+
+ @Override
+ public void execute(Runnable task) {
+ super.execute(task);
+ }
+ }
+
+ private static final class NonDelegatingExecutor extends ThreadPoolExecutor {
+ private static final Runnable STOP = () -> {};
+
+ private final BlockingQueue tasks = new LinkedBlockingQueue<>();
+ private final Thread worker;
+
+ private NonDelegatingExecutor() {
+ super(0, 1, 0, TimeUnit.MILLISECONDS, new SynchronousQueue());
+ worker = new Thread(this::runTasks, "nondelegating-benchmark-worker");
+ worker.setDaemon(true);
+ worker.start();
+ }
+
+ @Override
+ public void execute(Runnable task) {
+ tasks.add(task);
+ }
+
+ private void closeWorker() throws InterruptedException {
+ tasks.add(STOP);
+ worker.join(TimeUnit.SECONDS.toMillis(10));
+ if (worker.isAlive()) {
+ throw new AssertionError("non-delegating worker did not terminate");
+ }
+ }
+
+ private void runTasks() {
+ try {
+ while (true) {
+ Runnable task = tasks.take();
+ if (task == STOP) {
+ return;
+ }
+ task.run();
+ }
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/executor/RejectedExecutionHandlerInstrumentation.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/executor/RejectedExecutionHandlerInstrumentation.java
index e8b7b9effd3..b7a5f50cb6f 100644
--- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/executor/RejectedExecutionHandlerInstrumentation.java
+++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/executor/RejectedExecutionHandlerInstrumentation.java
@@ -4,17 +4,18 @@
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.nameEndsWith;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf;
-import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.cancelTask;
import static datadog.trace.instrumentation.java.concurrent.ConcurrentInstrumentationNames.EXECUTOR_INSTRUMENTATION_NAME;
import static java.util.Arrays.asList;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import datadog.context.ContextContinuation;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.api.Config;
import datadog.trace.api.InstrumenterConfig;
import datadog.trace.bootstrap.InstrumentationContext;
import datadog.trace.bootstrap.instrumentation.java.concurrent.State;
+import datadog.trace.bootstrap.instrumentation.java.concurrent.TPEHelper;
import datadog.trace.bootstrap.instrumentation.java.concurrent.Wrapper;
import datadog.trace.bootstrap.instrumentation.jfr.backpressure.BackpressureProfiling;
import java.util.concurrent.RunnableFuture;
@@ -69,12 +70,17 @@ public void methodAdvice(MethodTransformer transformer) {
public static final class Reject {
// remove our wrapper before calling the handler (save wrapper, so we can cancel it later)
@Advice.OnMethodEnter(suppress = Throwable.class)
- public static Wrapper> handle(
+ public static TPEHelper.RejectedTask handle(
@Advice.This Object zis, @Advice.Argument(readOnly = false, value = 0) Runnable runnable) {
Wrapper> wrapper = null;
+ ContextContinuation continuation = null;
if (runnable instanceof Wrapper) {
wrapper = (Wrapper>) runnable;
runnable = wrapper.unwrap();
+ } else {
+ continuation =
+ TPEHelper.prepareRejectedTask(
+ InstrumentationContext.get(Runnable.class, State.class), runnable);
}
if (Config.get().isProfilingBackPressureSamplingEnabled()) {
// record this event before the handler executes, which will help
@@ -82,28 +88,29 @@ public static Wrapper> handle(
// rejection policies which run on the caller (CallerRunsPolicy or user-provided)
BackpressureProfiling.getInstance().process(zis.getClass(), runnable);
}
- return wrapper;
+ return wrapper == null && continuation == null
+ ? null
+ : new TPEHelper.RejectedTask(wrapper, continuation);
}
// must execute after in case the handler actually runs the runnable,
// which is preferable to cancelling the continuation
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void reject(
- @Advice.Enter Wrapper> wrapper, @Advice.Argument(value = 0) Runnable runnable) {
+ @Advice.Enter TPEHelper.RejectedTask rejected,
+ @Advice.Argument(value = 0) Runnable runnable) {
// not handling rejected work (which will often not manifest in an exception being thrown)
// leads to unclosed continuations when executors get busy
// note that this does not handle rejection mechanisms used in Scala, so those need to be
// handled another way
- if (null != wrapper) {
- wrapper.cancel();
+ if (null != rejected) {
+ rejected.close();
} else {
if (runnable instanceof RunnableFuture) {
- cancelTask(
+ datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.cancelTask(
InstrumentationContext.get(RunnableFuture.class, State.class),
(RunnableFuture>) runnable);
}
- // paranoid about double instrumentation until RunnableInstrumentation is removed
- cancelTask(InstrumentationContext.get(Runnable.class, State.class), runnable);
}
}
}
diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/executor/ThreadPoolExecutorInstrumentation.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/executor/ThreadPoolExecutorInstrumentation.java
index b7340c0f434..00ec82afaa2 100644
--- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/executor/ThreadPoolExecutorInstrumentation.java
+++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/main/java/datadog/trace/instrumentation/java/concurrent/executor/ThreadPoolExecutorInstrumentation.java
@@ -3,6 +3,7 @@
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.extendsClass;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf;
+import static datadog.trace.bootstrap.instrumentation.api.Java8BytecodeBridge.currentContext;
import static datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter.ExcludeType.EXECUTOR;
import static datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter.ExcludeType.RUNNABLE;
import static datadog.trace.bootstrap.instrumentation.java.concurrent.ExcludeFilter.ExcludeType.RUNNABLE_FUTURE;
@@ -12,14 +13,19 @@
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+import datadog.context.ContextContinuation;
import datadog.context.ContextScope;
import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.bootstrap.ContextStore;
import datadog.trace.bootstrap.InstrumentationContext;
import datadog.trace.bootstrap.instrumentation.java.concurrent.QueueTimerHelper;
import datadog.trace.bootstrap.instrumentation.java.concurrent.State;
import datadog.trace.bootstrap.instrumentation.java.concurrent.TPEHelper;
import datadog.trace.bootstrap.instrumentation.java.concurrent.Wrapper;
+import java.util.List;
+import java.util.ListIterator;
import java.util.Queue;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.ThreadPoolExecutor;
@@ -56,13 +62,10 @@ public final class ThreadPoolExecutorInstrumentation
Instrumenter.ForTypeHierarchy,
Instrumenter.HasMethodAdvice {
- // executors which do their own wrapping before calling super,
- // leading to double wrapping, once at the child level and once
- // in ThreadPoolExecutor
- private static final ElementMatcher NO_WRAPPING_BEFORE_DELEGATION =
- not(
- isDeclaredBy(
- namedOneOf("org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor")));
+ // This executor decorates tasks before delegating, so its override must not use the legacy
+ // compatibility path. Exact ownership is captured later by ThreadPoolExecutor.execute.
+ private static final ElementMatcher DECORATES_BEFORE_DELEGATION =
+ isDeclaredBy(namedOneOf("org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor"));
@Override
public String hierarchyMarkerType() {
@@ -80,9 +83,19 @@ public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(
named("execute")
.and(isMethod())
- .and(NO_WRAPPING_BEFORE_DELEGATION)
+ .and(isDeclaredBy(named(ThreadPoolExecutor.class.getName())))
.and(takesArgument(0, named(Runnable.class.getName()))),
getClass().getName() + "$Execute");
+ transformer.applyAdvice(
+ // Preserve behavior for overrides that may bypass JDK admission. This path is
+ // intentionally best-effort; exact submission ownership begins in ThreadPoolExecutor.
+ named("execute")
+ .and(isMethod())
+ .and(not(isDeclaredBy(named(ThreadPoolExecutor.class.getName()))))
+ .and(not(DECORATES_BEFORE_DELEGATION))
+ .and(takesArgument(0, named(Runnable.class.getName())))
+ .and(takesArguments(1)),
+ getClass().getName() + "$ExecuteOverride");
transformer.applyAdvice(
named("beforeExecute")
.and(isMethod())
@@ -94,8 +107,19 @@ public void methodAdvice(MethodTransformer transformer) {
.and(takesArgument(0, named(Runnable.class.getName()))),
getClass().getName() + "$AfterExecute");
transformer.applyAdvice(
- named("remove").and(isMethod()).and(returns(Runnable.class)),
+ named("remove")
+ .and(isMethod())
+ .and(isDeclaredBy(named(ThreadPoolExecutor.class.getName())))
+ .and(takesArgument(0, named(Runnable.class.getName())))
+ .and(returns(boolean.class)),
getClass().getName() + "$Remove");
+ transformer.applyAdvice(
+ named("shutdownNow")
+ .and(isMethod())
+ .and(isDeclaredBy(named(ThreadPoolExecutor.class.getName())))
+ .and(takesArguments(0))
+ .and(returns(List.class)),
+ getClass().getName() + "$ShutdownNow");
}
public static final class Execute {
@@ -107,12 +131,21 @@ public static void capture(
if (TPEHelper.useWrapping(task)) {
task = Wrapper.wrap(task);
} else {
- TPEHelper.capture(InstrumentationContext.get(Runnable.class, State.class), task);
- // queue time needs to be handled separately because there are RunnableFutures which are
- // excluded as
- // Runnables but it is not until now that they will be put on the executor's queue
+ Runnable captured =
+ TPEHelper.captureOrWrap(
+ InstrumentationContext.get(Runnable.class, State.class),
+ task,
+ currentContext(),
+ tpe);
+ if (captured == null) {
+ return;
+ }
+ task = captured;
+ // queue time needs to be handled separately because there are RunnableFutures which
+ // are excluded as Runnables but it is not until now that they will be put on the
+ // executor's queue
if (!exclude(EXECUTOR, tpe)) {
- if (!exclude(RUNNABLE, task)) {
+ if (!(task instanceof Wrapper) && !exclude(RUNNABLE, task)) {
Queue> queue = tpe.getQueue();
QueueTimerHelper.startQueuingTimer(
InstrumentationContext.get(Runnable.class, State.class),
@@ -135,13 +168,32 @@ public static void capture(
}
}
+ public static final class ExecuteOverride {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void capture(
+ @Advice.This final ThreadPoolExecutor tpe,
+ @Advice.Argument(readOnly = false, value = 0) Runnable task) {
+ if (TPEHelper.shouldPropagate(tpe)) {
+ if (TPEHelper.useWrapping(task)) {
+ task = Wrapper.wrap(task);
+ } else {
+ TPEHelper.captureLegacy(InstrumentationContext.get(Runnable.class, State.class), task);
+ }
+ }
+ }
+ }
+
public static final class BeforeExecute {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static ContextScope beforeExecuteEnter(
@Advice.This final ThreadPoolExecutor tpe,
- @Advice.Argument(readOnly = false, value = 1) Runnable task) {
+ @Advice.Argument(readOnly = false, value = 1) Runnable task,
+ @Advice.Local("wrapper") Wrapper> wrapper) {
if (TPEHelper.shouldPropagate(tpe)) {
if (TPEHelper.useWrapping(task)) {
+ if (task instanceof Wrapper) {
+ wrapper = (Wrapper>) task;
+ }
task = Wrapper.unwrap(task);
} else {
return TPEHelper.startScope(
@@ -153,8 +205,16 @@ public static ContextScope beforeExecuteEnter(
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void beforeExecuteExit(
- @Advice.Enter final ContextScope scope, @Advice.Argument(value = 1) Runnable task) {
- if (scope != null) {
+ @Advice.Enter final ContextScope scope,
+ @Advice.Argument(value = 1) Runnable task,
+ @Advice.Thrown Throwable throwable,
+ @Advice.Local("wrapper") Wrapper> wrapper) {
+ if (throwable != null) {
+ TPEHelper.endScope(scope, task);
+ if (wrapper != null) {
+ wrapper.cancel();
+ }
+ } else if (scope != null) {
TPEHelper.setThreadLocalScope(scope, task);
}
}
@@ -185,19 +245,90 @@ public static void afterExecuteExit(
}
public static final class Remove {
+ @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class)
+ public static boolean enter(
+ @Advice.This final ThreadPoolExecutor tpe,
+ @Advice.Argument(0) Runnable task,
+ @Advice.Local("owner") Runnable owner,
+ @Advice.Local("continuation") ContextContinuation continuation) {
+ if (!TPEHelper.shouldPropagate(tpe)) {
+ return false;
+ }
+ if (task instanceof Wrapper) {
+ owner = task;
+ return false;
+ }
+ if (task == null) {
+ return false;
+ }
+ ContextStore contextStore =
+ InstrumentationContext.get(Runnable.class, State.class);
+ for (Runnable queued : tpe.getQueue()) {
+ Runnable logical = queued instanceof Wrapper ? ((Wrapper>) queued).unwrap() : queued;
+ if (task == logical || task.equals(logical)) {
+ if (queued instanceof Wrapper) {
+ if (tpe.getQueue().remove(queued)) {
+ ((Wrapper>) queued).cancel();
+ return true;
+ }
+ continue;
+ }
+ owner = queued;
+ State state = contextStore.get(queued);
+ continuation = state == null ? null : state.getCancellableContinuation();
+ break;
+ }
+ }
+ return false;
+ }
+
@Advice.OnMethodExit(suppress = Throwable.class)
public static void remove(
@Advice.This final ThreadPoolExecutor tpe,
- @Advice.Return(readOnly = false) Runnable removed) {
- if (TPEHelper.shouldPropagate(tpe)) {
- if (TPEHelper.useWrapping(removed)) {
- if (removed instanceof Wrapper) {
- Wrapper> wrapper = ((Wrapper>) removed);
+ @Advice.Enter boolean handled,
+ @Advice.Local("owner") Runnable owner,
+ @Advice.Local("continuation") ContextContinuation continuation,
+ @Advice.Return(readOnly = false) boolean removed) {
+ if (handled) {
+ removed = true;
+ return;
+ }
+ if (!TPEHelper.shouldPropagate(tpe)) {
+ return;
+ }
+ if (removed) {
+ if (owner instanceof Wrapper) {
+ Wrapper> wrapper = ((Wrapper>) owner);
+ wrapper.cancel();
+ } else if (owner != null) {
+ State state = InstrumentationContext.get(Runnable.class, State.class).get(owner);
+ if (state != null) {
+ state.closeContinuation(continuation);
+ }
+ }
+ }
+ }
+ }
+
+ public static final class ShutdownNow {
+ @Advice.OnMethodExit(suppress = Throwable.class)
+ public static void shutdown(
+ @Advice.This final ThreadPoolExecutor tpe, @Advice.Return List tasks) {
+ if (tasks != null && TPEHelper.shouldPropagate(tpe)) {
+ ContextStore contextStore =
+ InstrumentationContext.get(Runnable.class, State.class);
+ for (ListIterator iterator = tasks.listIterator(); iterator.hasNext(); ) {
+ Runnable task = iterator.next();
+ if (task instanceof Wrapper) {
+ Wrapper> wrapper = (Wrapper>) task;
wrapper.cancel();
- removed = wrapper.unwrap();
+ iterator.set(wrapper.unwrap());
+ } else {
+ State state = contextStore.get(task);
+ if (state != null) {
+ state.closeContinuation(state.getCancellableContinuation());
+ }
}
- } else {
- TPEHelper.cancelTask(InstrumentationContext.get(Runnable.class, State.class), removed);
}
}
}
diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/java/executor/ReusableRunnableSubmissionTest.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/java/executor/ReusableRunnableSubmissionTest.java
new file mode 100644
index 00000000000..bb7ad910110
--- /dev/null
+++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-1.8/src/test/java/executor/ReusableRunnableSubmissionTest.java
@@ -0,0 +1,459 @@
+package executor;
+
+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.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import datadog.context.Context;
+import datadog.context.ContextKey;
+import datadog.context.ContextScope;
+import datadog.trace.agent.test.AbstractInstrumentationTest;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.java.concurrent.Wrapper;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.Test;
+
+class ReusableRunnableSubmissionTest extends AbstractInstrumentationTest {
+
+ private static final Object TEST_CONTEXT = ContextKey.named("reusable-runnable-test");
+
+ @Test
+ void overlappingSubmissionsKeepTheirContextsAndCommonCaseIdentity() throws Exception {
+ BlockingTask blocker = new BlockingTask();
+ RecordingTask shared = new RecordingTask(2);
+ ThreadPoolExecutor pool = newPool(3);
+ try {
+ pool.execute(blocker);
+ assertTrue(blocker.started.await(10, TimeUnit.SECONDS));
+ submit(pool, shared, "first");
+ submit(pool, shared, "second");
+
+ Object[] queued = pool.getQueue().toArray();
+ assertSame(shared, queued[0]);
+ assertTrue(queued[1] instanceof Wrapper);
+
+ blocker.release.countDown();
+ assertTrue(shared.finished.await(10, TimeUnit.SECONDS));
+ assertEquals(asList("first", "second"), shared.observed);
+ } finally {
+ blocker.release.countDown();
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void directRunCannotStealQueuedSubmissionContext() throws Exception {
+ BlockingTask blocker = new BlockingTask();
+ RecordingTask shared = new RecordingTask(2);
+ ThreadPoolExecutor pool = newPool(2);
+ try {
+ pool.execute(blocker);
+ assertTrue(blocker.started.await(10, TimeUnit.SECONDS));
+ submit(pool, shared, "queued");
+
+ shared.run();
+ blocker.release.countDown();
+
+ assertTrue(shared.finished.await(10, TimeUnit.SECONDS));
+ assertEquals(asList(null, "queued"), shared.observed);
+ } finally {
+ blocker.release.countDown();
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void callerRunsUsesRejectedCollisionContext() throws Exception {
+ BlockingTask blocker = new BlockingTask();
+ RecordingTask shared = new RecordingTask(2);
+ ThreadPoolExecutor pool =
+ new ThreadPoolExecutor(
+ 1,
+ 1,
+ 0,
+ TimeUnit.SECONDS,
+ new ArrayBlockingQueue<>(1),
+ new ThreadPoolExecutor.CallerRunsPolicy());
+ try {
+ pool.execute(blocker);
+ assertTrue(blocker.started.await(10, TimeUnit.SECONDS));
+ submit(pool, shared, "queued");
+ submit(pool, shared, "rejected");
+
+ blocker.release.countDown();
+ assertTrue(shared.finished.await(10, TimeUnit.SECONDS));
+ assertEquals(asList("rejected", "queued"), shared.observed);
+ } finally {
+ blocker.release.countDown();
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void removeReleasesTheExactSubmissionSlot() throws Exception {
+ BlockingTask blocker = new BlockingTask();
+ RecordingTask shared = new RecordingTask(1);
+ ThreadPoolExecutor pool = newPool(2);
+ try {
+ pool.execute(blocker);
+ assertTrue(blocker.started.await(10, TimeUnit.SECONDS));
+ submit(pool, shared, "removed");
+ assertTrue(pool.remove(shared));
+
+ submit(pool, shared, "replacement");
+ assertSame(shared, pool.getQueue().peek());
+ blocker.release.countDown();
+
+ assertTrue(shared.finished.await(10, TimeUnit.SECONDS));
+ assertEquals(Collections.singletonList("replacement"), shared.observed);
+ } finally {
+ blocker.release.countDown();
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void overlappingSubmissionsCanBeRemovedBySubmittedIdentity() throws Exception {
+ BlockingTask blocker = new BlockingTask();
+ RecordingTask shared = new RecordingTask(2);
+ ThreadPoolExecutor pool = newPool(2);
+ try {
+ pool.execute(blocker);
+ assertTrue(blocker.started.await(10, TimeUnit.SECONDS));
+ submit(pool, shared, "first");
+ submit(pool, shared, "second");
+
+ assertTrue(pool.remove(shared));
+ assertTrue(pool.remove(shared));
+ assertTrue(pool.getQueue().isEmpty());
+ } finally {
+ blocker.release.countDown();
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void removeSelectsFirstLogicalOccurrenceAfterStateIsRecycled() throws Exception {
+ BlockingTask blocker = new BlockingTask();
+ RecyclingTask shared = new RecyclingTask();
+ ThreadPoolExecutor pool = newPool(3);
+ try {
+ pool.execute(blocker);
+ assertTrue(blocker.started.await(10, TimeUnit.SECONDS));
+ submit(pool, shared, "running");
+ submit(pool, shared, "wrapped");
+
+ blocker.release.countDown();
+ assertTrue(shared.firstStarted.await(10, TimeUnit.SECONDS));
+ submit(pool, shared, "recycled");
+
+ Object[] queued = pool.getQueue().toArray();
+ assertTrue(queued[0] instanceof Wrapper);
+ assertSame(shared, queued[1]);
+ assertTrue(pool.remove(shared));
+ assertSame(shared, pool.getQueue().peek());
+
+ shared.releaseFirst.countDown();
+ assertTrue(shared.finished.await(10, TimeUnit.SECONDS));
+ assertEquals(asList("running", "recycled"), shared.observed);
+ } finally {
+ blocker.release.countDown();
+ shared.releaseFirst.countDown();
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void priorityQueueCollisionPreservesApplicationBehavior() throws Exception {
+ BlockingTask blocker = new BlockingTask();
+ PriorityTask shared = new PriorityTask();
+ ThreadPoolExecutor pool =
+ new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new PriorityBlockingQueue());
+ try {
+ pool.execute(blocker);
+ assertTrue(blocker.started.await(10, TimeUnit.SECONDS));
+ submit(pool, shared, "first");
+ submit(pool, shared, "not-propagated");
+
+ Object[] queued = pool.getQueue().toArray();
+ assertSame(shared, queued[0]);
+ assertSame(shared, queued[1]);
+ blocker.release.countDown();
+
+ assertTrue(shared.finished.await(10, TimeUnit.SECONDS));
+ assertEquals(asList("first", null), shared.observed);
+ } finally {
+ blocker.release.countDown();
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void shutdownNowReturnsSubmittedIdentity() throws Exception {
+ BlockingTask blocker = new BlockingTask();
+ RecordingTask shared = new RecordingTask(2);
+ ThreadPoolExecutor pool = newPool(2);
+ try {
+ pool.execute(blocker);
+ assertTrue(blocker.started.await(10, TimeUnit.SECONDS));
+ submit(pool, shared, "first");
+ submit(pool, shared, "second");
+
+ List returned = pool.shutdownNow();
+ assertEquals(2, returned.size());
+ assertSame(shared, returned.get(0));
+ assertSame(shared, returned.get(1));
+ } finally {
+ blocker.release.countDown();
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void decoratingSubclassPropagatesAfterDecoration() throws Exception {
+ RecordingTask submitted = new RecordingTask(1);
+ DecoratingExecutor pool = new DecoratingExecutor();
+ try {
+ submit(pool, submitted, "decorated");
+ assertTrue(submitted.finished.await(10, TimeUnit.SECONDS));
+ assertEquals(Collections.singletonList("decorated"), submitted.observed);
+ } finally {
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void delegatingSubclassKeepsPerSubmissionOwnership() throws Exception {
+ BlockingTask blocker = new BlockingTask();
+ RecordingTask shared = new RecordingTask(2);
+ ThreadPoolExecutor pool = new DelegatingExecutor();
+ try {
+ pool.execute(blocker);
+ assertTrue(blocker.started.await(10, TimeUnit.SECONDS));
+ submit(pool, shared, "first");
+ submit(pool, shared, "second");
+
+ Object[] queued = pool.getQueue().toArray();
+ assertSame(shared, queued[0]);
+ assertTrue(queued[1] instanceof Wrapper);
+
+ blocker.release.countDown();
+ assertTrue(shared.finished.await(10, TimeUnit.SECONDS));
+ assertEquals(asList("first", "second"), shared.observed);
+ } finally {
+ blocker.release.countDown();
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void nonDelegatingSubclassKeepsNamedTaskPropagation() throws Exception {
+ RecordingTask submitted = new RecordingTask(1);
+ NonDelegatingExecutor pool = new NonDelegatingExecutor();
+ try {
+ submit(pool, submitted, "custom");
+
+ assertTrue(submitted.finished.await(10, TimeUnit.SECONDS));
+ assertEquals(Collections.singletonList("custom"), submitted.observed);
+ } finally {
+ pool.stopWorker();
+ pool.shutdownNow();
+ }
+ }
+
+ @Test
+ void nonDelegatingSubclassKeepsLambdaPropagation() throws Exception {
+ List observed = Collections.synchronizedList(new ArrayList<>());
+ CountDownLatch finished = new CountDownLatch(1);
+ Runnable submitted =
+ () -> {
+ observed.add(activeSpan());
+ finished.countDown();
+ };
+ NonDelegatingExecutor pool = new NonDelegatingExecutor();
+ AgentSpan parent = startSpan("test", "lambda-parent");
+ try {
+ try (ContextScope ignored = activateSpan(parent)) {
+ pool.execute(submitted);
+ assertTrue(finished.await(10, TimeUnit.SECONDS));
+ }
+
+ assertEquals(Collections.singletonList(parent), observed);
+ } finally {
+ parent.finish();
+ pool.stopWorker();
+ pool.shutdownNow();
+ }
+ }
+
+ private static ThreadPoolExecutor newPool(int queueCapacity) {
+ return new ThreadPoolExecutor(
+ 1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueCapacity));
+ }
+
+ private static void submit(ThreadPoolExecutor pool, Runnable task, String value) {
+ try (ContextScope ignored = Context.root().with(castContextKey(TEST_CONTEXT), value).attach()) {
+ pool.execute(task);
+ }
+ }
+
+ private static List asList(String first, String second) {
+ List values = new ArrayList<>(2);
+ values.add(first);
+ values.add(second);
+ return values;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static T castContextKey(Object key) {
+ return (T) key;
+ }
+
+ private static final class RecordingTask implements Runnable {
+ private final List observed = Collections.synchronizedList(new ArrayList<>());
+ private final CountDownLatch finished;
+
+ private RecordingTask(int executions) {
+ finished = new CountDownLatch(executions);
+ }
+
+ @Override
+ public void run() {
+ observed.add(Context.current().get(castContextKey(TEST_CONTEXT)));
+ finished.countDown();
+ }
+ }
+
+ private static final class BlockingTask implements Runnable {
+ private final CountDownLatch started = new CountDownLatch(1);
+ private final CountDownLatch release = new CountDownLatch(1);
+
+ @Override
+ public void run() {
+ started.countDown();
+ try {
+ release.await(10, TimeUnit.SECONDS);
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ private static final class RecyclingTask implements Runnable {
+ private final List observed = Collections.synchronizedList(new ArrayList<>());
+ private final AtomicInteger executions = new AtomicInteger();
+ private final CountDownLatch firstStarted = new CountDownLatch(1);
+ private final CountDownLatch releaseFirst = new CountDownLatch(1);
+ private final CountDownLatch finished = new CountDownLatch(2);
+
+ @Override
+ public void run() {
+ if (executions.getAndIncrement() == 0) {
+ firstStarted.countDown();
+ try {
+ releaseFirst.await(10, TimeUnit.SECONDS);
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ observed.add(Context.current().get(castContextKey(TEST_CONTEXT)));
+ finished.countDown();
+ }
+ }
+
+ private static final class PriorityTask implements Runnable, Comparable {
+ private final List observed = Collections.synchronizedList(new ArrayList<>());
+ private final CountDownLatch finished = new CountDownLatch(2);
+
+ @Override
+ public void run() {
+ observed.add(Context.current().get(castContextKey(TEST_CONTEXT)));
+ finished.countDown();
+ }
+
+ @Override
+ public int compareTo(PriorityTask ignored) {
+ return 0;
+ }
+ }
+
+ private static final class DecoratingExecutor extends ThreadPoolExecutor {
+ private DecoratingExecutor() {
+ super(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ super.execute(new DelegatingTask(command));
+ }
+ }
+
+ private static final class DelegatingExecutor extends ThreadPoolExecutor {
+ private DelegatingExecutor() {
+ super(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ super.execute(command);
+ }
+ }
+
+ private static final class NonDelegatingExecutor extends ThreadPoolExecutor {
+ private final BlockingQueue tasks = new LinkedBlockingQueue<>();
+ private final Thread worker;
+
+ private NonDelegatingExecutor() {
+ super(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));
+ worker = new Thread(this::runTasks, "non-delegating-executor-test");
+ worker.setDaemon(true);
+ worker.start();
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ tasks.add(command);
+ }
+
+ private void runTasks() {
+ try {
+ while (!Thread.currentThread().isInterrupted()) {
+ tasks.take().run();
+ }
+ } catch (InterruptedException interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private void stopWorker() throws InterruptedException {
+ worker.interrupt();
+ worker.join(TimeUnit.SECONDS.toMillis(10));
+ }
+ }
+
+ private static final class DelegatingTask implements Runnable {
+ private final Runnable delegate;
+
+ private DelegatingTask(Runnable delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void run() {
+ delegate.run();
+ }
+ }
+}