diff --git a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/build.gradle b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/build.gradle
index 670814a6b77..3487436e502 100644
--- a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/build.gradle
+++ b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/build.gradle
@@ -39,8 +39,25 @@ configurations {
sourceSets {
latestDepTest.groovy.srcDir sourceSets.baseTest.groovy
latestDepTest.scala.srcDir sourceSets.baseTest.scala
+ latestDepTest.java.srcDir sourceSets.baseTest.java
latestPekko10Test.groovy.srcDir sourceSets.baseTest.groovy
latestPekko10Test.scala.srcDir sourceSets.baseTest.scala
+ latestPekko10Test.java.srcDir sourceSets.baseTest.java
+}
+
+// This module has no 'test' source set, so the default forked test task runs nothing here. Register
+// one per Scala generation instead.
+['baseTest', 'latestDepTest'].each { suiteName ->
+ addForkedTestTask(suiteName).configure {
+ // The other *ForkedTest classes in this module have never run, and the client ones currently
+ // fail on an unrelated NPE in PekkoHttpClientHelpers$PekkoHttpHeaders during Data Streams
+ // injection. Keep these tasks scoped until that is fixed separately.
+ setIncludes(['**/PekkoHttpAsyncHandlerWrapper*ForkedTest.class'])
+ }
+ tasks.named(suiteName, Test) {
+ // finalizedBy, not dependsOn, so a failure is reported against the task that actually ran.
+ finalizedBy "${suiteName}ForkedTest"
+ }
}
dependencies {
diff --git a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/baseTest/java/AbstractPekkoHttpAsyncHandlerWrapperTest.java b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/baseTest/java/AbstractPekkoHttpAsyncHandlerWrapperTest.java
new file mode 100644
index 00000000000..b05366cefaa
--- /dev/null
+++ b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/baseTest/java/AbstractPekkoHttpAsyncHandlerWrapperTest.java
@@ -0,0 +1,217 @@
+import static datadog.trace.agent.test.assertions.SpanMatcher.span;
+import static datadog.trace.agent.test.assertions.TraceMatcher.trace;
+import static datadog.trace.api.DDSpanTypes.HTTP_SERVER;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import datadog.context.Context;
+import datadog.context.ContextScope;
+import datadog.trace.agent.test.AbstractInstrumentationTest;
+import datadog.trace.agent.test.assertions.SpanMatcher;
+import datadog.trace.instrumentation.pekkohttp.DatadogAsyncHandlerWrapper;
+import java.lang.reflect.Field;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
+import org.apache.pekko.http.scaladsl.model.HttpRequest;
+import org.apache.pekko.http.scaladsl.model.HttpRequest$;
+import org.apache.pekko.http.scaladsl.model.HttpResponse;
+import org.apache.pekko.http.scaladsl.model.HttpResponse$;
+import org.apache.pekko.http.scaladsl.model.Uri$;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import scala.concurrent.ExecutionContext$;
+import scala.concurrent.ExecutionContextExecutorService;
+import scala.concurrent.Future;
+import scala.concurrent.Promise;
+import scala.concurrent.Promise$;
+import scala.runtime.AbstractFunction1;
+import scala.util.Failure;
+import scala.util.Success;
+import scala.util.Try;
+
+/**
+ * Reproduces a request-context leak from the async-handler wrapper into Pekko's completion
+ * callbacks.
+ *
+ *
{@link DatadogAsyncHandlerWrapper} creates a server span and Pekko registers callbacks on the
+ * returned handler {@link Future} after the wrapper has closed the request scope. Those callbacks
+ * are framework bookkeeping and should therefore not inherit the request context.
+ *
+ *
The regression was caused by completing the Future exposed to Pekko while the request context
+ * was active. Scala's promise instrumentation consequently captured that context for Pekko's
+ * otherwise contextless completion callback. The captured continuation kept the finished trace
+ * buffered until the callback exited. The test depends on the scala-promise instrumentation in this
+ * module's test runtime to model that production propagation behavior.
+ *
+ *
The test makes the race deterministic by holding the simulated Pekko callback on a latch. The
+ * expected behavior is that the finished request trace is reported while that callback remains
+ * blocked. Before the instrumentation is fixed, the assertion fails because the trace is reported
+ * only after test cleanup releases the callback.
+ */
+abstract class AbstractPekkoHttpAsyncHandlerWrapperTest extends AbstractInstrumentationTest {
+
+ /**
+ * The operation name is a {@code UTF8BytesString}; {@link SpanMatcher#operationName(String)}
+ * compares object types, while the {@link Pattern} overload compares {@code CharSequence}
+ * content.
+ */
+ private static final Pattern OPERATION_NAME = Pattern.compile("pekko-http\\.request");
+
+ protected abstract boolean expectedCompletionPriority();
+
+ @BeforeEach
+ void verifyPropagationMode() throws Exception {
+ // Load the injected helper only after the test agent is installed, then verify each concrete
+ // variant is exercising the intended Scala Promise propagation mode.
+ assertEquals(
+ expectedCompletionPriority(),
+ readStaticBoolean(
+ Class.forName("datadog.trace.instrumentation.scala.PromiseHelper"),
+ "completionPriority"),
+ "Unexpected Scala Promise propagation mode");
+ // The wrapper resolves the same configuration independently, so pin the two together. A wrapper
+ // that stopped tracking this mode would otherwise silently skip the defensive Try copy while
+ // these tests kept passing.
+ assertEquals(
+ expectedCompletionPriority(),
+ readStaticBoolean(DatadogAsyncHandlerWrapper.class, "COMPLETION_PRIORITY"),
+ "Wrapper propagation mode disagrees with the Scala Promise instrumentation");
+ }
+
+ private static boolean readStaticBoolean(final Class> type, final String name)
+ throws Exception {
+ final Field field = type.getDeclaredField(name);
+ field.setAccessible(true);
+ return (Boolean) field.get(null);
+ }
+
+ /**
+ * Covers the failed-response path. On Scala 2.12 {@code Promise.resolveTry} allocates a fresh
+ * {@code Failure}, which incidentally drops any context associated with the completing {@code
+ * Try}, so this path exercises the root-context attachment only. Scala 2.13 passes the {@code
+ * Try} through, so there it exercises both defenses.
+ */
+ @Test
+ void doesNotPropagateRequestContextWhenHandlerFails() throws Exception {
+ assertRequestTraceIsNotRetained(
+ new Failure<>(new Exception("controller exception")),
+ span().root().operationName(OPERATION_NAME).type(HTTP_SERVER).error());
+ }
+
+ /**
+ * Covers the successful-response path. Both Scala generations pass a {@code Success} through
+ * completion unchanged, so this is the case that pins the defensive {@code Try} copy in
+ * completion-priority mode.
+ */
+ @Test
+ void doesNotPropagateRequestContextWhenHandlerSucceeds() throws Exception {
+ assertRequestTraceIsNotRetained(
+ new Success<>(emptyResponse()),
+ span().root().operationName(OPERATION_NAME).type(HTTP_SERVER).error(false));
+ }
+
+ private void assertRequestTraceIsNotRetained(
+ final Try handlerResult, final SpanMatcher expectedSpan) throws Exception {
+ try (AsyncHandlerWrapperReproducer reproducer =
+ new AsyncHandlerWrapperReproducer(handlerResult)) {
+ reproducer.start();
+
+ assertTrue(reproducer.awaitFrameworkCallback(), "Framework callback did not start");
+ assertTrue(
+ writer.waitForTracesMax(1, 5),
+ "Request trace was held by the contextless framework callback");
+ assertTraces(trace(expectedSpan));
+ }
+ }
+
+ private static HttpResponse emptyResponse() {
+ return HttpResponse$.MODULE$.apply(
+ HttpResponse$.MODULE$.apply$default$1(),
+ HttpResponse$.MODULE$.apply$default$2(),
+ HttpResponse$.MODULE$.apply$default$3(),
+ HttpResponse$.MODULE$.apply$default$4());
+ }
+
+ private static final class AsyncHandlerWrapperReproducer implements AutoCloseable {
+ private final Try handlerResult;
+ private final Promise handlerPromise = Promise$.MODULE$.apply();
+ private final AtomicReference requestContext = new AtomicReference<>();
+ private final CountDownLatch callbackStarted = new CountDownLatch(1);
+ private final CountDownLatch releaseCallback = new CountDownLatch(1);
+
+ private final ExecutionContextExecutorService handlerExecutor =
+ ExecutionContext$.MODULE$.fromExecutorService(Executors.newSingleThreadExecutor());
+ private final ExecutionContextExecutorService frameworkExecutor =
+ ExecutionContext$.MODULE$.fromExecutorService(Executors.newSingleThreadExecutor());
+
+ AsyncHandlerWrapperReproducer(final Try handlerResult) {
+ this.handlerResult = handlerResult;
+ }
+
+ void start() {
+ DatadogAsyncHandlerWrapper wrapper =
+ new DatadogAsyncHandlerWrapper(
+ new AbstractFunction1>() {
+ @Override
+ public Future apply(HttpRequest request) {
+ requestContext.set(Context.current());
+ return handlerPromise.future();
+ }
+ },
+ handlerExecutor);
+
+ HttpRequest request =
+ HttpRequest$.MODULE$.apply(
+ HttpRequest$.MODULE$.apply$default$1(),
+ Uri$.MODULE$.apply("/exception"),
+ HttpRequest$.MODULE$.apply$default$3(),
+ HttpRequest$.MODULE$.apply$default$4(),
+ HttpRequest$.MODULE$.apply$default$5());
+ Future response = wrapper.apply(request);
+
+ // Model a callback registered by Pekko after the wrapper has closed the request scope. It
+ // should not inherit the request context when the handler Future completes.
+ response.onComplete(
+ new AbstractFunction1, Void>() {
+ @Override
+ public Void apply(Try result) {
+ callbackStarted.countDown();
+ try {
+ releaseCallback.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return null;
+ }
+ },
+ frameworkExecutor);
+
+ // Application futures normally complete from callbacks running with the propagated request
+ // context. Model that completion path so the test also covers context captured at promise
+ // dispatch time, not only at callback construction time.
+ try (ContextScope ignored = requestContext.get().attach()) {
+ handlerPromise.complete(handlerResult);
+ }
+ }
+
+ boolean awaitFrameworkCallback() throws InterruptedException {
+ return callbackStarted.await(5, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void close() throws InterruptedException {
+ releaseCallback.countDown();
+ handlerExecutor.shutdown();
+ frameworkExecutor.shutdown();
+ assertTrue(
+ handlerExecutor.awaitTermination(5, TimeUnit.SECONDS),
+ "Handler executor did not terminate");
+ assertTrue(
+ frameworkExecutor.awaitTermination(5, TimeUnit.SECONDS),
+ "Framework executor did not terminate");
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/baseTest/java/PekkoHttpAsyncHandlerWrapperForkedTest.java b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/baseTest/java/PekkoHttpAsyncHandlerWrapperForkedTest.java
new file mode 100644
index 00000000000..63d53d19d9a
--- /dev/null
+++ b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/baseTest/java/PekkoHttpAsyncHandlerWrapperForkedTest.java
@@ -0,0 +1,17 @@
+import datadog.trace.test.junit.utils.config.WithConfig;
+
+/**
+ * Runs the async-handler context-retention reproducer with completion-priority propagation, which
+ * associates the completing context with the resolved {@code Try} instead of the thread.
+ *
+ * {@link WithConfig} applies before the test agent is installed, so the Scala Promise
+ * instrumentation registers the advice that creates that association.
+ */
+@WithConfig(key = "trace.integration.scala_promise_completion_priority.enabled", value = "true")
+class PekkoHttpAsyncHandlerWrapperForkedTest extends AbstractPekkoHttpAsyncHandlerWrapperTest {
+
+ @Override
+ protected boolean expectedCompletionPriority() {
+ return true;
+ }
+}
diff --git a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/baseTest/java/PekkoHttpAsyncHandlerWrapperTest.java b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/baseTest/java/PekkoHttpAsyncHandlerWrapperTest.java
new file mode 100644
index 00000000000..d71b317fe7a
--- /dev/null
+++ b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/baseTest/java/PekkoHttpAsyncHandlerWrapperTest.java
@@ -0,0 +1,8 @@
+/** Runs the async-handler context-retention reproducer with default Scala Promise propagation. */
+class PekkoHttpAsyncHandlerWrapperTest extends AbstractPekkoHttpAsyncHandlerWrapperTest {
+
+ @Override
+ protected boolean expectedCompletionPriority() {
+ return false;
+ }
+}
diff --git a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/DatadogAsyncHandlerWrapper.java b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/DatadogAsyncHandlerWrapper.java
index 0578eda8aba..1926e420f6a 100644
--- a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/DatadogAsyncHandlerWrapper.java
+++ b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/DatadogAsyncHandlerWrapper.java
@@ -1,18 +1,25 @@
package datadog.trace.instrumentation.pekkohttp;
-import static datadog.trace.bootstrap.instrumentation.api.AgentSpan.fromContext;
-
+import datadog.context.Context;
import datadog.context.ContextScope;
-import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.api.InstrumenterConfig;
import org.apache.pekko.http.scaladsl.model.HttpRequest;
import org.apache.pekko.http.scaladsl.model.HttpResponse;
import scala.Function1;
import scala.concurrent.ExecutionContext;
import scala.concurrent.Future;
+import scala.concurrent.Promise;
+import scala.concurrent.Promise$;
import scala.runtime.AbstractFunction1;
+import scala.util.Failure;
+import scala.util.Success;
+import scala.util.Try;
public class DatadogAsyncHandlerWrapper
extends AbstractFunction1> {
+ private static final boolean COMPLETION_PRIORITY =
+ InstrumenterConfig.get().isScalaPromiseCompletionPriorityEnabled();
+
private final Function1> userHandler;
private final ExecutionContext executionContext;
@@ -26,33 +33,56 @@ public DatadogAsyncHandlerWrapper(
@Override
public Future apply(final HttpRequest request) {
final ContextScope scope = DatadogWrapperHelper.createSpan(request);
- AgentSpan span = fromContext(scope.context());
+ final Context context = scope.context();
Future futureResponse;
try {
futureResponse = userHandler.apply(request);
} catch (final Throwable t) {
scope.close();
- DatadogWrapperHelper.finishSpan(scope.context(), t);
+ DatadogWrapperHelper.finishSpan(context, t);
throw t;
}
- final Future wrapped =
- futureResponse.transform(
- new AbstractFunction1() {
- @Override
- public HttpResponse apply(final HttpResponse response) {
- DatadogWrapperHelper.finishSpan(scope.context(), response);
- return response;
- }
- },
- new AbstractFunction1() {
- @Override
- public Throwable apply(final Throwable t) {
- DatadogWrapperHelper.finishSpan(scope.context(), t);
- return t;
- }
- },
- executionContext);
scope.close();
- return wrapped;
+ final Promise wrapped = Promise$.MODULE$.apply();
+ futureResponse.onComplete(
+ new AbstractFunction1, Void>() {
+ @Override
+ public Void apply(final Try result) {
+ Try wrappedResult = result;
+ try {
+ if (result.isSuccess()) {
+ DatadogWrapperHelper.finishSpan(context, result.get());
+ } else {
+ DatadogWrapperHelper.finishSpan(
+ context, ((Failure) result).exception());
+ }
+ } catch (final Throwable t) {
+ // Preserve transform's behavior when span decoration fails. Pekko does not support
+ // response blocking, and this wrapper has no Materializer for discarding the
+ // successful response entity that is replaced by this failure.
+ wrappedResult = new Failure<>(t);
+ }
+ final Try resultForPekko;
+ if (COMPLETION_PRIORITY) {
+ // Completion-priority mode can associate context directly with a Try. Copy the
+ // result so neither that association nor the active thread context reaches Pekko.
+ resultForPekko =
+ wrappedResult.isSuccess()
+ ? new Success<>(wrappedResult.get())
+ : new Failure<>(((Failure) wrappedResult).exception());
+ } else {
+ resultForPekko = wrappedResult;
+ }
+ // The application Future can complete while the request context is active. Complete
+ // the Future exposed to Pekko under the root context so its framework callbacks do not
+ // capture and retain the request trace.
+ try (ContextScope ignored = Context.root().attach()) {
+ wrapped.complete(resultForPekko);
+ }
+ return null;
+ }
+ },
+ executionContext);
+ return wrapped.future();
}
}
diff --git a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/DatadogWrapperHelper.java b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/DatadogWrapperHelper.java
index d4565c68bfc..7d206c43f1c 100644
--- a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/DatadogWrapperHelper.java
+++ b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/DatadogWrapperHelper.java
@@ -22,18 +22,22 @@ public static ContextScope createSpan(final HttpRequest request) {
public static void finishSpan(final Context context, final HttpResponse response) {
final AgentSpan span = fromContext(context);
- DECORATE.onResponse(span, response);
- DECORATE.beforeFinish(context);
-
- span.finish();
+ try {
+ DECORATE.onResponse(span, response);
+ DECORATE.beforeFinish(context);
+ } finally {
+ span.finish();
+ }
}
public static void finishSpan(final Context context, final Throwable t) {
final AgentSpan span = fromContext(context);
- DECORATE.onError(span, t);
- span.setHttpStatusCode(500);
- DECORATE.beforeFinish(context);
-
- span.finish();
+ try {
+ DECORATE.onError(span, t);
+ span.setHttpStatusCode(500);
+ DECORATE.beforeFinish(context);
+ } finally {
+ span.finish();
+ }
}
}
diff --git a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/PekkoHttp2ServerInstrumentation.java b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/PekkoHttp2ServerInstrumentation.java
index 9899e04f6af..a3b06305b15 100644
--- a/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/PekkoHttp2ServerInstrumentation.java
+++ b/dd-java-agent/instrumentation/pekko/pekko-http-1.0/src/main/java/datadog/trace/instrumentation/pekkohttp/PekkoHttp2ServerInstrumentation.java
@@ -44,7 +44,6 @@ public String[] helperClassNames() {
packageName + ".DatadogWrapperHelper",
packageName + ".DatadogAsyncHandlerWrapper",
packageName + ".DatadogAsyncHandlerWrapper$1",
- packageName + ".DatadogAsyncHandlerWrapper$2",
packageName + ".PekkoHttpServerHeaders",
packageName + ".PekkoHttpServerDecorator",
packageName + ".UriAdapter",
diff --git a/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-2.10/src/main/java/datadog/trace/instrumentation/scala210/concurrent/ScalaPromiseModule.java b/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-2.10/src/main/java/datadog/trace/instrumentation/scala210/concurrent/ScalaPromiseModule.java
index 80a0f8cf947..a5916f54932 100644
--- a/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-2.10/src/main/java/datadog/trace/instrumentation/scala210/concurrent/ScalaPromiseModule.java
+++ b/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-2.10/src/main/java/datadog/trace/instrumentation/scala210/concurrent/ScalaPromiseModule.java
@@ -14,7 +14,6 @@
import datadog.trace.bootstrap.instrumentation.java.concurrent.State;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -65,8 +64,7 @@ public List typeInstrumentations() {
}
// Only enable this if integrations have been enabled and the extra "integration"
// scala_promise_completion_priority has been enabled specifically
- if (config.isIntegrationEnabled(
- Collections.singletonList("scala_promise_completion_priority"), false)) {
+ if (config.isScalaPromiseCompletionPriorityEnabled()) {
instrumenters.add(new PromiseObjectInstrumentation());
}
return instrumenters;
diff --git a/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-2.13/src/main/java/datadog/trace/instrumentation/scala213/concurrent/ScalaPromiseModule.java b/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-2.13/src/main/java/datadog/trace/instrumentation/scala213/concurrent/ScalaPromiseModule.java
index 42d013039ab..0ec164f1379 100644
--- a/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-2.13/src/main/java/datadog/trace/instrumentation/scala213/concurrent/ScalaPromiseModule.java
+++ b/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-2.13/src/main/java/datadog/trace/instrumentation/scala213/concurrent/ScalaPromiseModule.java
@@ -14,7 +14,6 @@
import datadog.trace.bootstrap.instrumentation.java.concurrent.State;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -61,8 +60,7 @@ public List typeInstrumentations() {
final InstrumenterConfig config = InstrumenterConfig.get();
// Only enable this if integrations have been enabled and the extra "integration"
// scala_promise_completion_priority has been enabled specifically
- if (config.isIntegrationEnabled(
- Collections.singletonList("scala_promise_completion_priority"), false)) {
+ if (config.isScalaPromiseCompletionPriorityEnabled()) {
ret.add(new DefaultPromiseInstrumentation());
ret.add(new PromiseObjectInstrumentation());
}
diff --git a/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-common/src/main/java/datadog/trace/instrumentation/scala/PromiseHelper.java b/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-common/src/main/java/datadog/trace/instrumentation/scala/PromiseHelper.java
index b3e6b27d7ac..6506bf44d37 100644
--- a/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-common/src/main/java/datadog/trace/instrumentation/scala/PromiseHelper.java
+++ b/dd-java-agent/instrumentation/scala/scala-promise/scala-promise-common/src/main/java/datadog/trace/instrumentation/scala/PromiseHelper.java
@@ -7,16 +7,13 @@
import datadog.trace.bootstrap.ContextStore;
import datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils;
import datadog.trace.bootstrap.instrumentation.java.concurrent.State;
-import java.util.Collections;
import scala.util.Failure;
import scala.util.Success;
import scala.util.Try;
public class PromiseHelper {
public static final boolean completionPriority =
- InstrumenterConfig.get()
- .isIntegrationEnabled(
- Collections.singletonList("scala_promise_completion_priority"), false);
+ InstrumenterConfig.get().isScalaPromiseCompletionPriorityEnabled();
/**
* Get the {@code Try} that should be associated with the {@code Context}. Will create a new copy
diff --git a/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java b/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java
index 74bff640024..1e218a9d9c3 100644
--- a/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java
+++ b/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java
@@ -98,6 +98,7 @@
import static datadog.trace.api.config.UsmConfig.USM_ENABLED;
import static datadog.trace.util.CollectionUtils.tryMakeImmutableList;
import static datadog.trace.util.CollectionUtils.tryMakeImmutableSet;
+import static java.util.Collections.singletonList;
import datadog.environment.JavaVirtualMachine;
import datadog.trace.api.profiling.ProfilingEnablement;
@@ -141,6 +142,15 @@ public class InstrumenterConfig {
}
}
+ /**
+ * Name of the opt-in Scala Promise integration that gives the context completing a {@code
+ * Promise} priority over the context that registered callbacks on it. Defined here so that the
+ * instrumentations enabling this mode and the ones that have to compensate for it cannot drift
+ * apart.
+ */
+ private static final String SCALA_PROMISE_COMPLETION_PRIORITY =
+ "scala_promise_completion_priority";
+
private final ConfigProvider configProvider;
private final boolean triageEnabled;
@@ -228,6 +238,7 @@ public class InstrumenterConfig {
private final boolean appLogsCollectionEnabled;
private final boolean legacyContextManagerEnabled;
+ private final boolean scalaPromiseCompletionPriorityEnabled;
static {
// Bind telemetry collector to config module before initializing ConfigProvider
@@ -394,6 +405,9 @@ private InstrumenterConfig() {
configProvider.getBoolean(APP_LOGS_COLLECTION_ENABLED, DEFAULT_APP_LOGS_COLLECTION_ENABLED);
legacyContextManagerEnabled = configProvider.getBoolean(LEGACY_CONTEXT_MANAGER_ENABLED, true);
+
+ scalaPromiseCompletionPriorityEnabled =
+ isIntegrationEnabled(singletonList(SCALA_PROMISE_COMPLETION_PRIORITY), false);
}
public boolean isCodeOriginEnabled() {
@@ -447,6 +461,20 @@ public boolean isIntegrationEnabled(
return anyEnabled;
}
+ /**
+ * Whether the Scala Promise instrumentation gives the context completing a {@code Promise}
+ * priority over the context that registered callbacks on it.
+ *
+ * This mode associates the completing context with the resolved {@code Try} itself, so it is
+ * also read by instrumentations that must keep such an association from reaching a framework
+ * callback.
+ *
+ * @return {@code true} if completion-priority propagation is enabled, else {@code false}
+ */
+ public boolean isScalaPromiseCompletionPriorityEnabled() {
+ return scalaPromiseCompletionPriorityEnabled;
+ }
+
public boolean isIntegrationShortcutMatchingEnabled(
final Iterable integrationNames, final boolean defaultEnabled) {
return configProvider.isEnabled(
@@ -870,6 +898,8 @@ public String toString() {
+ apiSecurityEndpointCollectionEnabled
+ ", legacyContextManagerEnabled="
+ legacyContextManagerEnabled
+ + ", scalaPromiseCompletionPriorityEnabled="
+ + scalaPromiseCompletionPriorityEnabled
+ '}';
}
}