Skip to content

Commit da4baf3

Browse files
committed
feat(concurrency): Add structured task scope preview support for JDK 25
1 parent e4582ab commit da4baf3

File tree

4 files changed

+292
-0
lines changed

4 files changed

+292
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
ext {
2+
minJavaVersionForTests = JavaVersion.VERSION_25
3+
}
4+
5+
apply from: "$rootDir/gradle/java.gradle"
6+
apply plugin: 'idea'
7+
8+
muzzle {
9+
pass {
10+
coreJdk('25')
11+
}
12+
}
13+
14+
idea {
15+
module {
16+
jdkName = '25'
17+
}
18+
}
19+
20+
/*
21+
* Declare previewTest, a test suite that requires the Javac/Java --enable-preview feature flag
22+
*/
23+
addTestSuite('previewTest')
24+
// Configure groovy test file compilation
25+
compilePreviewTestGroovy.configure {
26+
javaLauncher = javaToolchains.launcherFor {
27+
languageVersion = JavaLanguageVersion.of(25)
28+
}
29+
options.compilerArgs.add("--enable-preview")
30+
}
31+
// Configure Java test files compilation
32+
compilePreviewTestJava.configure {
33+
options.compilerArgs.add("--enable-preview")
34+
}
35+
// Configure tests execution
36+
previewTest.configure {
37+
jvmArgs = ['--enable-preview']
38+
}
39+
// Require the preview test suite to run as part of module check
40+
tasks.named("check").configure {
41+
dependsOn "previewTest"
42+
}
43+
44+
dependencies {
45+
testImplementation project(':dd-java-agent:instrumentation:trace-annotation')
46+
}
47+
48+
// Set all compile tasks to use JDK21 but let instrumentation code targets 1.8 compatibility
49+
project.tasks.withType(AbstractCompile).configureEach {
50+
setJavaVersion(it, 25)
51+
}
52+
compileJava.configure {
53+
sourceCompatibility = JavaVersion.VERSION_1_8
54+
targetCompatibility = JavaVersion.VERSION_1_8
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package datadog.trace.instrumentation.java.concurrent.structuredconcurrency;
2+
3+
import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.capture;
4+
import static java.util.Collections.singletonMap;
5+
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
6+
7+
import com.google.auto.service.AutoService;
8+
import datadog.environment.JavaVirtualMachine;
9+
import datadog.trace.agent.tooling.Instrumenter;
10+
import datadog.trace.agent.tooling.InstrumenterModule;
11+
import datadog.trace.bootstrap.ContextStore;
12+
import datadog.trace.bootstrap.InstrumentationContext;
13+
import datadog.trace.bootstrap.instrumentation.java.concurrent.State;
14+
import java.util.Map;
15+
import net.bytebuddy.asm.Advice;
16+
17+
/**
18+
* This instrumentation captures the active span scope at StructuredTaskScope task creation
19+
* (SubtaskImpl). The scope is then activate and close through the Runnable instrumentation
20+
* (SubtaskImpl implementation Runnable).
21+
*/
22+
@SuppressWarnings("unused")
23+
@AutoService(InstrumenterModule.class)
24+
public class StructuredTaskScopeInstrumentation extends InstrumenterModule.Tracing
25+
implements Instrumenter.ForBootstrap, Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
26+
27+
private static final String SUBTASK_IMPL_CLASS_NAME =
28+
"java.util.concurrent.StructuredTaskScopeImpl.SubtaskImpl";
29+
30+
public StructuredTaskScopeInstrumentation() {
31+
super("java_concurrent", "structured_task_scope");
32+
}
33+
34+
@Override
35+
public String instrumentedType() {
36+
return SUBTASK_IMPL_CLASS_NAME;
37+
}
38+
39+
@Override
40+
public boolean isEnabled() {
41+
return JavaVirtualMachine.isJavaVersionAtLeast(25) && super.isEnabled();
42+
}
43+
44+
@Override
45+
public Map<String, String> contextStore() {
46+
return singletonMap(SUBTASK_IMPL_CLASS_NAME, State.class.getName());
47+
}
48+
49+
@Override
50+
public void methodAdvice(MethodTransformer transformer) {
51+
transformer.applyAdvice(isConstructor(), getClass().getName() + "$ConstructorAdvice");
52+
}
53+
54+
public static final class ConstructorAdvice {
55+
@Advice.OnMethodExit
56+
public static <T> void captureScope(
57+
@Advice.This Object task // StructuredTaskScopeImpl.SubtaskImpl (can't use the type)
58+
) {
59+
ContextStore<Object, State> contextStore =
60+
InstrumentationContext.get(
61+
SUBTASK_IMPL_CLASS_NAME,
62+
"datadog.trace.bootstrap.instrumentation.java.concurrent.State");
63+
capture(contextStore, task);
64+
}
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import datadog.trace.agent.test.AgentTestRunner
2+
import datadog.trace.api.Trace
3+
import spock.lang.Timeout
4+
5+
import java.util.concurrent.Callable
6+
import java.util.concurrent.StructuredTaskScope
7+
8+
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
9+
import static datadog.trace.agent.test.utils.TraceUtils.runnableUnderTrace
10+
11+
class StructuredConcurrencyTest extends AgentTestRunner {
12+
/**
13+
* Tests the structured task scope with a single task.
14+
*/
15+
@Timeout(10)
16+
def "test single task"() {
17+
setup:
18+
def taskScope = StructuredTaskScope.open()
19+
def result = false
20+
21+
when:
22+
runUnderTrace("parent") {
23+
def task = taskScope.fork(new Callable<Boolean>() {
24+
@Trace(operationName = "child")
25+
@Override
26+
Boolean call() throws Exception {
27+
return true
28+
}
29+
})
30+
taskScope.join()
31+
result = task.get()
32+
}
33+
taskScope.close()
34+
35+
then:
36+
result
37+
assertTraces(1) {
38+
sortSpansByStart()
39+
trace(2) {
40+
span(0) {
41+
parent()
42+
operationName "parent"
43+
}
44+
span(1) {
45+
childOfPrevious()
46+
operationName "child"
47+
}
48+
}
49+
}
50+
}
51+
52+
/**
53+
* Tests the structured task scope with a multiple tasks.
54+
* Here is the expected task/span structure:
55+
* <pre>
56+
* parent
57+
* |-- child1
58+
* |-- child2
59+
* \-- child3
60+
* </pre>
61+
*/
62+
@Timeout(10)
63+
def "test multiple tasks"() {
64+
setup:
65+
def taskScope = StructuredTaskScope.open()
66+
67+
when:
68+
runUnderTrace("parent") {
69+
taskScope.fork {
70+
runnableUnderTrace("child1") {}
71+
}
72+
taskScope.fork {
73+
runnableUnderTrace("child2") {}
74+
}
75+
taskScope.fork {
76+
runnableUnderTrace("child3") {}
77+
}
78+
taskScope.join()
79+
}
80+
taskScope.close()
81+
82+
then:
83+
assertTraces(1) {
84+
sortSpansByStart()
85+
trace(4) {
86+
span {
87+
parent()
88+
operationName "parent"
89+
}
90+
def parent = span(0)
91+
span {
92+
childOf(parent)
93+
assert span.operationName.toString().startsWith("child")
94+
}
95+
span {
96+
childOf(parent)
97+
assert span.operationName.toString().startsWith("child")
98+
}
99+
span {
100+
childOf(parent)
101+
assert span.operationName.toString().startsWith("child")
102+
}
103+
}
104+
}
105+
}
106+
107+
/**
108+
* Tests the structured task scope with a multiple nested tasks.
109+
* Here is the expected task/span structure:
110+
* <pre>
111+
* parent
112+
* |-- child1
113+
* | |-- great-child1-1
114+
* | \-- great-child1-2
115+
* \-- child2
116+
* </pre>
117+
*/
118+
@Timeout(10)
119+
def "test nested tasks"() {
120+
setup:
121+
def taskScope = StructuredTaskScope.open()
122+
123+
when:
124+
runUnderTrace("parent") {
125+
taskScope.fork {
126+
runnableUnderTrace("child1") {
127+
taskScope.fork {
128+
runnableUnderTrace("great-child1-1") {}
129+
}
130+
taskScope.fork {
131+
runnableUnderTrace("great-child1-2") {}
132+
}
133+
}
134+
}
135+
taskScope.fork {
136+
runnableUnderTrace("child2") {}
137+
}
138+
taskScope.join()
139+
}
140+
taskScope.close()
141+
142+
then:
143+
assertTraces(1) {
144+
sortSpansByStart()
145+
trace(5) {
146+
// Check parent span
147+
span {
148+
parent()
149+
operationName "parent"
150+
}
151+
def parent = span(0)
152+
// Check child and great child spans
153+
def child1 = null
154+
for (i in 0..<4) {
155+
span {
156+
def name = span.operationName.toString()
157+
if (name.startsWith("child")) {
158+
childOf(parent)
159+
if (name == "child1") {
160+
child1 = span
161+
}
162+
} else if (name.startsWith("great-child1")) {
163+
childOf(child1) // We can assume child1 will be set as spans are sorted by start time
164+
}
165+
}
166+
}
167+
}
168+
}
169+
}
170+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ include(
352352
":dd-java-agent:instrumentation:java-concurrent",
353353
":dd-java-agent:instrumentation:java-concurrent:java-completablefuture",
354354
":dd-java-agent:instrumentation:java-concurrent:java-concurrent-21",
355+
":dd-java-agent:instrumentation:java-concurrent:java-concurrent-25",
355356
":dd-java-agent:instrumentation:java-concurrent:lambda-testing",
356357
":dd-java-agent:instrumentation:java-directbytebuffer",
357358
":dd-java-agent:instrumentation:java-http-client",

0 commit comments

Comments
 (0)