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 + '}'; } }