diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/StructuredTaskScopeHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/StructuredTaskScopeHelper.java new file mode 100644 index 00000000000..ba0945ba2e2 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/concurrent/StructuredTaskScopeHelper.java @@ -0,0 +1,42 @@ +package datadog.trace.bootstrap.instrumentation.java.concurrent; + +import static java.lang.invoke.MethodHandles.publicLookup; +import static java.lang.invoke.MethodType.methodType; + +import java.lang.invoke.MethodHandle; + +/** Helper for the java-concurrent-25.0 {@code StructuredTaskScopeForkInstrumentation}. */ +public final class StructuredTaskScopeHelper { + private static final MethodHandle IS_CANCELLED = resolveIsCancelled(); + + private StructuredTaskScopeHelper() {} + + private static MethodHandle resolveIsCancelled() { + try { + ClassLoader classLoader = StructuredTaskScopeHelper.class.getClassLoader(); + Class scopeClass = + Class.forName("java.util.concurrent.StructuredTaskScope", false, classLoader); + return publicLookup().findVirtual(scopeClass, "isCancelled", methodType(boolean.class)); + } catch (Throwable ignored) { + return null; + } + } + + /** + * Returns whether the given {@code StructuredTaskScope} has been cancelled. + * + * @param scope a {@code java.util.concurrent.StructuredTaskScope} instance. + * @return {@code true} if the scope is cancelled; {@code false} otherwise, including when {@code + * isCancelled()} could not be resolved or invoked. + */ + public static boolean isCancelled(Object scope) { + if (IS_CANCELLED == null || scope == null) { + return false; + } + try { + return (boolean) IS_CANCELLED.invoke(scope); + } catch (Throwable ignored) { + return false; + } + } +} diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/build.gradle b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/build.gradle index d95e9432796..5a7b31fe8b1 100644 --- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/build.gradle +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/build.gradle @@ -1,3 +1,6 @@ +import static org.gradle.api.JavaVersion.VERSION_1_8 +import static org.gradle.api.JavaVersion.VERSION_24 + plugins { id 'idea' } @@ -31,13 +34,13 @@ tasks.named("previewTest").configure { testJvmConstraints { // Structured concurrency is a preview feature in Java 21. Methods (e.g. ShutdownOnFailure) used in this instrumentation test are no longer available in Java 25, so we set the max version to 24. // See: https://download.java.net/java/early_access/loom/docs/api/java.base/java/util/concurrent/StructuredTaskScope.html - maxJavaVersion = JavaVersion.VERSION_24 + maxJavaVersion = VERSION_24 } } // Set all compile tasks to use JDK21 but let instrumentation code targets 1.8 compatibility tasks.withType(AbstractCompile).configureEach { - configureCompiler(it, 21, JavaVersion.VERSION_1_8) + configureCompiler(it, 21, VERSION_1_8) } // Configure groovy test file compilation diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/previewTest/groovy/StructuredConcurrencyTest.groovy b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/previewTest/groovy/StructuredConcurrencyTest.groovy index 8ef8fce7df4..4c839bed7ef 100644 --- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/previewTest/groovy/StructuredConcurrencyTest.groovy +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-21.0/src/previewTest/groovy/StructuredConcurrencyTest.groovy @@ -49,7 +49,7 @@ class StructuredConcurrencyTest extends InstrumentationSpecification { } /** - * Tests the structured task scope with a multiple tasks. + * Tests the structured task scope with multiple tasks. * Here is the expected task/span structure: *
    *   parent
diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/build.gradle b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/build.gradle
index 25ab9e9485a..35992b6be40 100644
--- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/build.gradle
+++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/build.gradle
@@ -1,3 +1,7 @@
+import static org.gradle.api.JavaVersion.VERSION_25
+import static org.gradle.api.JavaVersion.VERSION_26
+import static org.gradle.api.JavaVersion.VERSION_27
+
 plugins {
   id 'idea'
 }
@@ -6,6 +10,11 @@ apply from: "$rootDir/gradle/java.gradle"
 // Use slf4j-simple as default; logback has a high chance of getting stuck in a deadlock on CI.
 apply from: "$rootDir/gradle/slf4j-simple.gradle"
 
+testJvmConstraints {
+  // The new StructuredTaskScope API (JEP 505) only exists from Java 25
+  minJavaVersion = VERSION_25
+}
+
 muzzle {
   pass {
     coreJdk('25')
@@ -17,3 +26,65 @@ idea {
     jdkName = '25'
   }
 }
+
+/*
+ * Declare previewTest, a test suite that requires the Javac/Java --enable-preview feature flag.
+ * --enable-preview class files are version-locked to the compiling JDK, so the suite is compiled with
+ * the SAME JDK that runs it (the selected test JVM, clamped to >= 25).
+ */
+addTestSuite('previewTest')
+
+def previewTestJvm = tasks.named("previewTest", Test)
+  .flatMap { it.javaLauncher }
+  .map { Math.max(25, it.metadata.languageVersion.asInt()) }
+  .orElse(25)
+
+tasks.named("compilePreviewTestJava", JavaCompile) {
+  javaCompiler = javaToolchains.compilerFor {
+    languageVersion = previewTestJvm.map {
+      JavaLanguageVersion.of(it)
+    }
+  }
+  options.release = previewTestJvm
+  options.compilerArgs.add("--enable-preview")
+}
+tasks.named("previewTest", Test) {
+  jvmArgs = ['--enable-preview']
+}
+
+/*
+ * Per-JDK suites test:
+ * Each suite has its own source set, is compiled for one JDK,
+ * and is pinned to it (preview class files are version-locked).
+ */
+def previewCheckSuites = ['previewTest']
+def addJoinerLeakSuite = { int jdk, JavaVersion version ->
+  String suite = "previewTestJdk$jdk"
+  addTestSuite(suite)
+  tasks.named("compile${suite.capitalize()}Java", JavaCompile) {
+    configureCompiler(it, jdk)
+    options.compilerArgs.add("--enable-preview")
+  }
+  tasks.named(suite, Test) {
+    jvmArgs = ['--enable-preview']
+    testJvmConstraints {
+      minJavaVersion = version
+      maxJavaVersion = version
+    }
+  }
+  previewCheckSuites << suite
+}
+
+addJoinerLeakSuite(25, VERSION_25)
+if (System.getenv('JAVA_26_HOME') != null) {
+  addJoinerLeakSuite(26, VERSION_26)
+}
+if (System.getenv('JAVA_27_HOME') != null) {
+  addJoinerLeakSuite(27, VERSION_27)
+}
+
+// Require the preview test suites to run as part of module check
+tasks.named("check") {
+  dependsOn previewCheckSuites
+}
+
diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/main/java/datadog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25Instrumentation.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/main/java/datadog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25Instrumentation.java
index 263ca03b889..61b44251dc7 100644
--- a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/main/java/datadog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25Instrumentation.java
+++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/main/java/datadog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25Instrumentation.java
@@ -1,66 +1,60 @@
 package datadog.trace.instrumentation.java.concurrent.structuredconcurrency25;
 
-import static datadog.environment.JavaVirtualMachine.isJavaVersionAtLeast;
+import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
 import static datadog.trace.bootstrap.InstrumentationContext.get;
-import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.capture;
-import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
+import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.cancelTask;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
 
-import com.google.auto.service.AutoService;
 import datadog.trace.agent.tooling.Instrumenter;
-import datadog.trace.agent.tooling.InstrumenterModule;
 import datadog.trace.bootstrap.ContextStore;
 import datadog.trace.bootstrap.instrumentation.java.concurrent.State;
+import datadog.trace.bootstrap.instrumentation.java.concurrent.StructuredTaskScopeHelper;
+import java.util.concurrent.Callable;
 import net.bytebuddy.asm.Advice.OnMethodExit;
+import net.bytebuddy.asm.Advice.Return;
 import net.bytebuddy.asm.Advice.This;
 
-// WARNING:
-// This instrumentation is tested using smoke tests as instrumented tests cannot run using Java 25.
-// Instrumented tests rely on Spock / Groovy which cannot run using Java 25 due to byte-code
-// compatibility. Check
-// dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0 for this
-// instrumentation test suite.
-
 /**
- * This instrumentation captures the active span scope at StructuredTaskScope task creation
- * (SubtaskImpl). The scope is then activate and close through the {@link Runnable} instrumentation
- * (SubtaskImpl implementing {@link Runnable}).
+ * This instrumentation cancels the continuation captured by {@link
+ * StructuredTaskScope25TaskInstrumentation} for a subtask forked into an already-canceled scope.
+ *
+ * 

In that case, the subtask's thread is never started, so {@code SubtaskImpl.run()} never runs, + * and the {@link Runnable} instrumentation never activates/closes the captured continuation. It + * leads to continuation leak. This instrumentation verifies the subtask was canceled and releases + * the related continuation. */ @SuppressWarnings("unused") -@AutoService(InstrumenterModule.class) -public class StructuredTaskScope25Instrumentation extends InstrumenterModule.ContextTracking +public class StructuredTaskScope25Instrumentation implements Instrumenter.ForBootstrap, Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { - public StructuredTaskScope25Instrumentation() { - super("java_concurrent", "structured-task-scope", "structured-task-scope-25"); - } - @Override public String instrumentedType() { - return "java.util.concurrent.StructuredTaskScopeImpl$SubtaskImpl"; - } - - @Override - public boolean isEnabled() { - return isJavaVersionAtLeast(25) && super.isEnabled(); + return "java.util.concurrent.StructuredTaskScopeImpl"; } @Override public void methodAdvice(MethodTransformer transformer) { - transformer.applyAdvice(isConstructor(), getClass().getName() + "$ConstructorAdvice"); + transformer.applyAdvice( + named("fork").and(takesArgument(0, Callable.class)), getClass().getName() + "$ForkAdvice"); } - public static final class ConstructorAdvice { + public static final class ForkAdvice { /** - * Captures task scope to be restored at the start of VirtualThread.run() method by {@link - * Runnable} instrumentation. + * Cancels the task scope continuation captured at task creation when the subtask is forked into + * an already-canceled scope: its thread never starts, so the {@link Runnable} instrumentation + * never releases the continuation. * - * @param subTaskImpl The StructuredTaskScopeImpl.SubtaskImpl object (the advice are compile - * against Java 8 so the type from JDK25 can't be referred, using {@link Object} instead + * @param scope The StructuredTaskScopeImpl object (the advice is compiled against Java 8 so the + * type from JDK25 can't be referred, using {@link Object} instead). + * @param subtask The StructuredTaskScopeImpl.SubtaskImpl object (the advice is compiled against + * Java 8 so the type from JDK25 can't be referred, using {@link Object} instead). */ @OnMethodExit(suppress = Throwable.class) - public static void captureScope(@This Object subTaskImpl) { - ContextStore contextStore = get(Runnable.class, State.class); - capture(contextStore, (Runnable) subTaskImpl); + public static void afterFork(@This Object scope, @Return Object subtask) { + if (subtask instanceof Runnable && StructuredTaskScopeHelper.isCancelled(scope)) { + ContextStore contextStore = get(Runnable.class, State.class); + cancelTask(contextStore, (Runnable) subtask); + } } } } diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/main/java/datadog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25Module.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/main/java/datadog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25Module.java new file mode 100644 index 00000000000..6afac978632 --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/main/java/datadog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25Module.java @@ -0,0 +1,40 @@ +package datadog.trace.instrumentation.java.concurrent.structuredconcurrency25; + +import static datadog.environment.JavaVirtualMachine.isJavaVersionAtLeast; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonMap; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.bootstrap.instrumentation.java.concurrent.State; +import java.util.List; +import java.util.Map; + +/** + * This module propagates context across {@code java.util.concurrent.StructuredTaskScope} forked + * subtasks (JDK 25+). + */ +@SuppressWarnings("unused") +@AutoService(InstrumenterModule.class) +public class StructuredTaskScope25Module extends InstrumenterModule.ContextTracking { + public StructuredTaskScope25Module() { + super("java_concurrent", "structured-task-scope", "structured-task-scope-25"); + } + + @Override + public boolean isEnabled() { + return isJavaVersionAtLeast(25) && super.isEnabled(); + } + + @Override + public Map contextStore() { + return singletonMap(Runnable.class.getName(), State.class.getName()); + } + + @Override + public List typeInstrumentations() { + return asList( + new StructuredTaskScope25TaskInstrumentation(), new StructuredTaskScope25Instrumentation()); + } +} diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/main/java/datadog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25TaskInstrumentation.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/main/java/datadog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25TaskInstrumentation.java new file mode 100644 index 00000000000..27deddd7361 --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/main/java/datadog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25TaskInstrumentation.java @@ -0,0 +1,46 @@ +package datadog.trace.instrumentation.java.concurrent.structuredconcurrency25; + +import static datadog.trace.bootstrap.InstrumentationContext.get; +import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.capture; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; + +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.instrumentation.java.concurrent.State; +import net.bytebuddy.asm.Advice.OnMethodExit; +import net.bytebuddy.asm.Advice.This; + +/** + * This instrumentation captures the active span scope at StructuredTaskScope task creation + * (SubtaskImpl). The scope is then activate and close through the {@link Runnable} instrumentation + * (SubtaskImpl implementing {@link Runnable}). + */ +@SuppressWarnings("unused") +public class StructuredTaskScope25TaskInstrumentation + implements Instrumenter.ForBootstrap, Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + @Override + public String instrumentedType() { + return "java.util.concurrent.StructuredTaskScopeImpl$SubtaskImpl"; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice(isConstructor(), getClass().getName() + "$ConstructorAdvice"); + } + + public static final class ConstructorAdvice { + /** + * Captures task scope to be restored at the start of VirtualThread.run() method by {@link + * Runnable} instrumentation. + * + * @param subTaskImpl The StructuredTaskScopeImpl.SubtaskImpl object (the advice is compiled + * against Java 8 so the type from JDK25 can't be referred, using {@link Object} instead). + */ + @OnMethodExit(suppress = Throwable.class) + public static void captureScope(@This Object subTaskImpl) { + ContextStore contextStore = get(Runnable.class, State.class); + capture(contextStore, (Runnable) subTaskImpl); + } + } +} diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTest/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25Test.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTest/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25Test.java new file mode 100644 index 00000000000..c7dc13b9ef3 --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTest/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScope25Test.java @@ -0,0 +1,162 @@ +package testdog.trace.instrumentation.java.concurrent.structuredconcurrency25; + +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static java.util.Comparator.comparing; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.api.DDSpanId; +import datadog.trace.core.DDSpan; +import java.util.Comparator; +import java.util.concurrent.StructuredTaskScope; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("preview") +public class StructuredTaskScope25Test extends AbstractInstrumentationTest { + /** Tests the structured task scope with a single task. */ + @Test + void testSingleTaskTracking() throws Exception { + var span = tracer.startSpan("test", "parent"); + try (var ignored = tracer.activateSpan(span)) { + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> task("child")); + scope.join(); + } + } + span.finish(); + + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName("parent"), + span().childOfPrevious().operationName("child"))); + } + + /** + * Tests the structured task scope with multiple tasks. Here is the expected task/span structure: + * + *

+   *   parent
+   *   |-- child1
+   *   |-- child2
+   *   \-- child3
+   * 
+ */ + @Test + void testMultipleTasksTracking() throws Exception { + var span = tracer.startSpan("test", "parent"); + try (var ignored = tracer.activateSpan(span)) { + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> task("child1")); + scope.fork(() -> task("child2")); + scope.fork(() -> task("child3")); + scope.join(); + } + } + span.finish(); + + assertTraces( + trace( + SORT_BY_START_TIME, + span().root().operationName("parent"), + span().childOfIndex(0).operationName("child1"), + span().childOfIndex(0).operationName("child2"), + span().childOfIndex(0).operationName("child3"))); + } + + /** + * Tests the structured task scope with multiple nested tasks. Here is the expected task/span + * structure: + * + *
+   *   parent
+   *   |-- child1
+   *   |   |-- grandchild1
+   *   |   \-- grandchild2
+   *   \-- child2
+   * 
+ */ + @Test + void testNestedTasksTracking() throws Exception { + var span = tracer.startSpan("test", "parent"); + try (var ignored = tracer.activateSpan(span)) { + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> nestedChildren("child1")); + scope.fork(() -> task("child2")); + scope.join(); + } + } + span.finish(); + + assertTraces( + trace( + options -> options.sorter(SORT_BY_OPERATION_NAME), + span().childOfIndex(4).operationName("child1"), + span().childOfIndex(4).operationName("child2"), + span().childOfIndex(0).operationName("grandchild1"), + span().childOfIndex(0).operationName("grandchild2"), + span().root().operationName("parent"))); + } + + Comparator SORT_BY_OPERATION_NAME = + comparing(DDSpan::getOperationName, comparing(CharSequence::toString)); + + Void nestedChildren(String name) throws Exception { + var span = tracer.startSpan("test", name); + try (var ignored = tracer.activateSpan(span)) { + try (var scope = StructuredTaskScope.open()) { + scope.fork(() -> task("grandchild1")); + scope.fork(() -> task("grandchild2")); + scope.join(); + } + } + span.finish(); + return null; + } + + @Test + void testTasksInheritContext() throws Exception { + var span = tracer.startSpan("test", "parent"); + var expectedSpanId = DDSpanId.toString(span.getSpanId()); + try (var ignored = tracer.activateSpan(span)) { + try (var scope = StructuredTaskScope.open()) { + for (int i = 0; i < 5; i++) { + scope.fork(() -> assertEquals(expectedSpanId, tracer.getSpanId())); + } + scope.join(); + } + } + span.finish(); + + assertTraces(trace(span().root().operationName("parent"))); + } + + @Test + void testFailingTaskDoesNotLeakContinuation() { + var span = tracer.startSpan("test", "parent"); + var parentSpanId = DDSpanId.toString(span.getSpanId()); + try (var ignored = tracer.activateSpan(span)) { + try (var scope = StructuredTaskScope.open()) { + scope.fork(this::failingTask); + assertThrows(Exception.class, scope::join); + } + assertEquals( + parentSpanId, tracer.getSpanId(), "parent context should be restored after the scope"); + } + span.finish(); + + assertTraces(trace(span().root().operationName("parent"))); + } + + Void task(String name) { + tracer.startSpan("test", name).finish(); + return null; + } + + Void failingTask() { + throw new IllegalStateException("failing"); + } +} diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTestJdk25/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScopeCancelTest.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTestJdk25/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScopeCancelTest.java new file mode 100644 index 00000000000..7daf6b9dc36 --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTestJdk25/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScopeCancelTest.java @@ -0,0 +1,47 @@ +package testdog.trace.instrumentation.java.concurrent.structuredconcurrency25; + +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import java.util.concurrent.StructuredTaskScope; +import org.junit.jupiter.api.Test; + +/** + * JDK specific tests for the fork-into-canceled-scope continuation leak, isolated from {@code + * StructuredTaskScope25Test} because it uses the Java 25 {@code StructuredTaskScope.Joiner} API. + */ +@SuppressWarnings("preview") +public class StructuredTaskScopeCancelTest extends AbstractInstrumentationTest { + @Test + void testForkIntoCancelledScopeDoesNotLeakContinuation() throws Exception { + var span = tracer.startSpan("test", "parent"); + try (var ignored = tracer.activateSpan(span)) { + try (var scope = StructuredTaskScope.open(new CancelOnForkJoiner<>())) { + scope.fork(this::task); + scope.join(); + } + } + span.finish(); + + assertTraces(trace(span().root().operationName("parent"))); + } + + Void task() { + tracer.startSpan("test", "child").finish(); + return null; + } + + /** Cancels the scope as soon as the first subtask is forked. */ + static final class CancelOnForkJoiner implements StructuredTaskScope.Joiner { + @Override + public boolean onFork(StructuredTaskScope.Subtask subtask) { + return true; // Cancel the scope as soon as the first subtask is forked. + } + + @Override + public Void result() { + return null; + } + } +} diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTestJdk26/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScopeCancelTest.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTestJdk26/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScopeCancelTest.java new file mode 100644 index 00000000000..66aa08ae39b --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTestJdk26/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScopeCancelTest.java @@ -0,0 +1,47 @@ +package testdog.trace.instrumentation.java.concurrent.structuredconcurrency25; + +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import java.util.concurrent.StructuredTaskScope; +import org.junit.jupiter.api.Test; + +/** + * JDK specific tests for the fork-into-canceled-scope continuation leak, isolated from {@code + * StructuredTaskScope25Test} because it uses the Java 26 {@code StructuredTaskScope.Joiner} API. + */ +@SuppressWarnings("preview") +public class StructuredTaskScopeCancelTest extends AbstractInstrumentationTest { + @Test + void testForkIntoCancelledScopeDoesNotLeakContinuation() throws Exception { + var span = tracer.startSpan("test", "parent"); + try (var ignored = tracer.activateSpan(span)) { + try (var scope = StructuredTaskScope.open(new CancelOnForkJoiner<>())) { + scope.fork(this::task); + scope.join(); + } + } + span.finish(); + + assertTraces(trace(span().root().operationName("parent"))); + } + + Void task() { + tracer.startSpan("test", "child").finish(); + return null; + } + + /** Cancels the scope as soon as the first subtask is forked. */ + static final class CancelOnForkJoiner implements StructuredTaskScope.Joiner { + @Override + public boolean onFork(StructuredTaskScope.Subtask subtask) { + return true; // Cancel the scope as soon as the first subtask is forked. + } + + @Override + public Void result() { + return null; + } + } +} diff --git a/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTestJdk27/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScopeCancelTest.java b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTestJdk27/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScopeCancelTest.java new file mode 100644 index 00000000000..cfa2cdea42f --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-concurrent/java-concurrent-25.0/src/previewTestJdk27/java/testdog/trace/instrumentation/java/concurrent/structuredconcurrency25/StructuredTaskScopeCancelTest.java @@ -0,0 +1,48 @@ +package testdog.trace.instrumentation.java.concurrent.structuredconcurrency25; + +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import java.util.concurrent.StructuredTaskScope; +import org.junit.jupiter.api.Test; + +/** + * JDK specific tests for the fork-into-canceled-scope continuation leak, isolated from {@code + * StructuredTaskScope25Test} because it uses the Java 27 {@code StructuredTaskScope.Joiner} API. + */ +@SuppressWarnings("preview") +public class StructuredTaskScopeCancelTest extends AbstractInstrumentationTest { + @Test + void testForkIntoCancelledScopeDoesNotLeakContinuation() throws Exception { + var span = tracer.startSpan("test", "parent"); + try (var ignored = tracer.activateSpan(span)) { + try (var scope = StructuredTaskScope.open(new CancelOnForkJoiner<>())) { + scope.fork(this::task); + scope.join(); + } + } + span.finish(); + + assertTraces(trace(span().root().operationName("parent"))); + } + + Void task() { + tracer.startSpan("test", "child").finish(); + return null; + } + + /** Cancels the scope as soon as the first subtask is forked. */ + static final class CancelOnForkJoiner + implements StructuredTaskScope.Joiner { + @Override + public boolean onFork(StructuredTaskScope.Subtask subtask) { + return true; // Cancel the scope as soon as the first subtask is forked. + } + + @Override + public Void result() { + return null; + } + } +}