Skip to content

Commit dacc5a5

Browse files
Add support for Java 25 Structured Concurrency API (#9276)
* feat(build): Add support for JDK 25 discovery * feat(concurrency): Improve JDK 21 structured task scope instrumentation * Add instrumentation alias * Limit activation to Java < 25 * Fix context store declaration and usage to match RunnableInstrumentation * Remove unused generic * feat(concurrency): Add structured task scope preview support for JDK 25
1 parent 81cdab0 commit dacc5a5

File tree

17 files changed

+480
-22
lines changed

17 files changed

+480
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
1-
package datadog.trace.instrumentation.java.concurrent.structuredconcurrency;
1+
package datadog.trace.instrumentation.java.concurrent.structuredconcurrency21;
22

3+
import static datadog.environment.JavaVirtualMachine.isJavaVersionBetween;
4+
import static datadog.trace.bootstrap.InstrumentationContext.get;
35
import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.capture;
46
import static java.util.Collections.singletonMap;
57
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
68

79
import com.google.auto.service.AutoService;
8-
import datadog.environment.JavaVirtualMachine;
910
import datadog.trace.agent.tooling.Instrumenter;
1011
import datadog.trace.agent.tooling.InstrumenterModule;
1112
import datadog.trace.bootstrap.ContextStore;
12-
import datadog.trace.bootstrap.InstrumentationContext;
1313
import datadog.trace.bootstrap.instrumentation.java.concurrent.State;
1414
import java.util.Map;
15-
import net.bytebuddy.asm.Advice;
15+
import net.bytebuddy.asm.Advice.OnMethodExit;
16+
import net.bytebuddy.asm.Advice.This;
1617

1718
/**
1819
* 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).
20+
* (SubtaskImpl). The scope is then activate and close through the {@link Runnable} instrumentation
21+
* (SubtaskImpl implementing {@link Runnable}).
2122
*/
2223
@SuppressWarnings("unused")
2324
@AutoService(InstrumenterModule.class)
24-
public class StructuredTaskScopeInstrumentation extends InstrumenterModule.Tracing
25+
public class StructuredTaskScope21Instrumentation extends InstrumenterModule.Tracing
2526
implements Instrumenter.ForBootstrap, Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
2627

27-
public StructuredTaskScopeInstrumentation() {
28-
super("java_concurrent", "structured_task_scope");
28+
public StructuredTaskScope21Instrumentation() {
29+
super("java_concurrent", "structured-task-scope", "structured-task-scope-21");
2930
}
3031

3132
@Override
@@ -35,13 +36,12 @@ public String instrumentedType() {
3536

3637
@Override
3738
public boolean isEnabled() {
38-
return JavaVirtualMachine.isJavaVersionAtLeast(21) && super.isEnabled();
39+
return isJavaVersionBetween(21, 25) && super.isEnabled();
3940
}
4041

4142
@Override
4243
public Map<String, String> contextStore() {
43-
return singletonMap(
44-
"java.util.concurrent.StructuredTaskScope.SubtaskImpl", State.class.getName());
44+
return singletonMap(Runnable.class.getName(), State.class.getName());
4545
}
4646

4747
@Override
@@ -50,15 +50,17 @@ public void methodAdvice(MethodTransformer transformer) {
5050
}
5151

5252
public static final class ConstructorAdvice {
53-
@Advice.OnMethodExit
54-
public static <T> void captureScope(
55-
@Advice.This Object task // StructuredTaskScope.SubtaskImpl (can't use the type)
56-
) {
57-
ContextStore<Object, State> contextStore =
58-
InstrumentationContext.get(
59-
"java.util.concurrent.StructuredTaskScope.SubtaskImpl",
60-
"datadog.trace.bootstrap.instrumentation.java.concurrent.State");
61-
capture(contextStore, task);
53+
/**
54+
* Captures task scope to be restored at the start of VirtualThread.run() method by {@link
55+
* Runnable} instrumentation.
56+
*
57+
* @param subTaskImpl The StructuredTaskScope.SubtaskImpl object (the advice are compile against
58+
* Java 8 so the type from JDK25 can't be referred, using {@link Object} instead
59+
*/
60+
@OnMethodExit(suppress = Throwable.class)
61+
public static void captureScope(@This Object subTaskImpl) {
62+
ContextStore<Runnable, State> contextStore = get(Runnable.class, State.class);
63+
capture(contextStore, (Runnable) subTaskImpl);
6264
}
6365
}
6466
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apply from: "$rootDir/gradle/java.gradle"
2+
3+
muzzle {
4+
pass {
5+
coreJdk('25')
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package datadog.trace.instrumentation.java.concurrent.structuredconcurrency25;
2+
3+
import static datadog.environment.JavaVirtualMachine.isJavaVersionAtLeast;
4+
import static datadog.trace.bootstrap.InstrumentationContext.get;
5+
import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.capture;
6+
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
7+
8+
import com.google.auto.service.AutoService;
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.instrumentation.java.concurrent.State;
13+
import net.bytebuddy.asm.Advice.OnMethodExit;
14+
import net.bytebuddy.asm.Advice.This;
15+
16+
// WARNING:
17+
// This instrumentation is tested using smoke tests as instrumented tests cannot run using Java 25.
18+
// Instrumented tests rely on Spock / Groovy which cannot run using Java 25 due to byte-code
19+
// compatibility. Check dd-java-agent/instrumentation/java-concurrent/java-concurrent-25 for this
20+
// instrumentation test suite.
21+
22+
/**
23+
* This instrumentation captures the active span scope at StructuredTaskScope task creation
24+
* (SubtaskImpl). The scope is then activate and close through the {@link Runnable} instrumentation
25+
* (SubtaskImpl implementing {@link Runnable}).
26+
*/
27+
@SuppressWarnings("unused")
28+
@AutoService(InstrumenterModule.class)
29+
public class StructuredTaskScope25Instrumentation extends InstrumenterModule.Tracing
30+
implements Instrumenter.ForBootstrap, Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
31+
32+
public StructuredTaskScope25Instrumentation() {
33+
super("java_concurrent", "structured-task-scope", "structured-task-scope-25");
34+
}
35+
36+
@Override
37+
public String instrumentedType() {
38+
return "java.util.concurrent.StructuredTaskScopeImpl$SubtaskImpl";
39+
}
40+
41+
@Override
42+
public boolean isEnabled() {
43+
return isJavaVersionAtLeast(25) && super.isEnabled();
44+
}
45+
46+
@Override
47+
public void methodAdvice(MethodTransformer transformer) {
48+
transformer.applyAdvice(isConstructor(), getClass().getName() + "$ConstructorAdvice");
49+
}
50+
51+
public static final class ConstructorAdvice {
52+
/**
53+
* Captures task scope to be restored at the start of VirtualThread.run() method by {@link
54+
* Runnable} instrumentation.
55+
*
56+
* @param subTaskImpl The StructuredTaskScopeImpl.SubtaskImpl object (the advice are compile
57+
* against Java 8 so the type from JDK25 can't be referred, using {@link Object} instead
58+
*/
59+
@OnMethodExit(suppress = Throwable.class)
60+
public static void captureScope(@This Object subTaskImpl) {
61+
ContextStore<Runnable, State> contextStore = get(Runnable.class, State.class);
62+
capture(contextStore, (Runnable) subTaskImpl);
63+
}
64+
}
65+
}

dd-smoke-tests/concurrent/java-21/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ plugins {
55

66
ext {
77
minJavaVersionForTests = JavaVersion.VERSION_21
8+
maxJavaVersionForTests = JavaVersion.VERSION_25
89
}
910

1011
apply from: "$rootDir/gradle/java.gradle"
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
plugins {
2+
id 'application'
3+
id 'com.gradleup.shadow'
4+
}
5+
6+
ext {
7+
// This smoke test should be limited to Java 25 and above
8+
// But the groovy testing framework cannot run on Java 25
9+
// Using Java 8 for now and runs a JVM 25 when forking tests process.
10+
// Relying on forked JVM 25 is hardcoded in the test suites (see createProcessBuilder()).
11+
minJavaVersionForTests = JavaVersion.VERSION_1_8
12+
maxJavaVersionForTests = JavaVersion.VERSION_1_8
13+
}
14+
15+
apply from: "$rootDir/gradle/java.gradle"
16+
17+
description = 'JDK 25 Concurrent Integration Tests'
18+
19+
tasks.named('compileJava').configure {
20+
setJavaVersion(it, 25)
21+
sourceCompatibility = JavaVersion.VERSION_25
22+
targetCompatibility = JavaVersion.VERSION_25
23+
options.compilerArgs.add("--enable-preview")
24+
javaToolchains {
25+
java {
26+
toolchain {
27+
languageVersion = JavaLanguageVersion.of(25)
28+
}
29+
}
30+
}
31+
}
32+
33+
tasks.named('compileTestGroovy').configure {
34+
setJavaVersion(it, 8)
35+
sourceCompatibility = JavaVersion.VERSION_1_8
36+
targetCompatibility = JavaVersion.VERSION_1_8
37+
}
38+
39+
// Disable plugin tasks that do not support Java 25:
40+
// * forbiddenApis is missing classes
41+
// * spotless as the google-java-format version does not support Java 25 and can't be changed once applied
42+
// * spotbugs failed to read class using newer bytecode versions
43+
forbiddenApisMain {
44+
failOnMissingClasses = false
45+
}
46+
['spotlessApply', 'spotlessCheck', 'spotlessJava', 'spotbugsMain'].each {
47+
tasks.named(it).configure { enabled = false }
48+
}
49+
50+
application {
51+
mainClass = 'datadog.smoketest.concurrent.ConcurrentApp'
52+
}
53+
54+
dependencies {
55+
implementation group: 'io.opentelemetry.instrumentation', name: 'opentelemetry-instrumentation-annotations', version: '2.19.0'
56+
testImplementation project(':dd-smoke-tests')
57+
}
58+
59+
tasks.withType(Test).configureEach {
60+
dependsOn "shadowJar"
61+
jvmArgs "-Ddatadog.smoketest.shadowJar.path=${tasks.shadowJar.archiveFile.get()}"
62+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package datadog.smoketest.concurrent;
2+
3+
public class ConcurrentApp {
4+
void main(String[] args) {
5+
if (args.length != 1) {
6+
System.err.println("Missing test case argument");
7+
System.exit(1);
8+
}
9+
String name = args[0];
10+
try {
11+
fromName(name).run();
12+
} catch (InterruptedException e) {
13+
throw new RuntimeException("Failed to test case " + name, e);
14+
}
15+
}
16+
17+
private static TestCase fromName(String name) {
18+
return switch (name) {
19+
case "NestedTasks" -> new NestedTasks();
20+
case "MultipleTasks" -> new MultipleTasks();
21+
case "SimpleCallableTask" -> new SimpleCallableTask();
22+
case "SimpleRunnableTask" -> new SimpleRunnableTask();
23+
default -> throw new IllegalArgumentException("Invalid test case name " + name);
24+
};
25+
}
26+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package datadog.smoketest.concurrent;
2+
3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
4+
import java.util.concurrent.StructuredTaskScope;
5+
6+
public final class MultipleTasks implements TestCase {
7+
@WithSpan("parent")
8+
public void run() throws InterruptedException {
9+
try (var scope = StructuredTaskScope.open()) {
10+
scope.fork(this::runnableTask1);
11+
scope.fork(this::callableTask2);
12+
scope.fork(this::runnableTask3);
13+
scope.join();
14+
}
15+
}
16+
17+
@WithSpan("child1")
18+
void runnableTask1() {
19+
// Some basic computations here
20+
}
21+
22+
@WithSpan("child2")
23+
Boolean callableTask2() {
24+
// Some basic computations here
25+
return true;
26+
}
27+
28+
@WithSpan("child3")
29+
void runnableTask3() {
30+
// Some basic computations here
31+
}
32+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package datadog.smoketest.concurrent;
2+
3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
4+
import java.util.concurrent.StructuredTaskScope;
5+
6+
public final class NestedTasks implements TestCase {
7+
@WithSpan("parent")
8+
public void run() throws InterruptedException {
9+
try (var scope = StructuredTaskScope.open()) {
10+
scope.fork(this::task1);
11+
scope.fork(this::task2);
12+
scope.join();
13+
}
14+
}
15+
16+
@WithSpan("child1")
17+
void task1() {
18+
try (var scope = StructuredTaskScope.open()) {
19+
scope.fork(this::subTask1);
20+
scope.fork(this::subTask2);
21+
scope.join();
22+
} catch (InterruptedException e) {
23+
Thread.currentThread().interrupt();
24+
}
25+
}
26+
27+
@WithSpan("child2")
28+
void task2() {
29+
// Some basic computations here
30+
}
31+
32+
@WithSpan("great-child1-1")
33+
void subTask1() {
34+
// Some basic computations here
35+
}
36+
@WithSpan("great-child1-2")
37+
void subTask2() {
38+
// Some basic computations here
39+
}
40+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package datadog.smoketest.concurrent;
2+
3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
4+
import java.util.concurrent.StructuredTaskScope;
5+
6+
public final class SimpleCallableTask implements TestCase {
7+
@WithSpan("parent")
8+
public void run() throws InterruptedException {
9+
try (var scope = StructuredTaskScope.open()) {
10+
scope.fork(this::doSomething);
11+
scope.join();
12+
}
13+
}
14+
15+
@WithSpan("child")
16+
Boolean doSomething() {
17+
// Some basic computations here
18+
return true;
19+
}
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package datadog.smoketest.concurrent;
2+
3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
4+
import java.util.concurrent.StructuredTaskScope;
5+
6+
public final class SimpleRunnableTask implements TestCase {
7+
@WithSpan("parent")
8+
public void run() throws InterruptedException {
9+
try (var scope = StructuredTaskScope.open()) {
10+
scope.fork(this::doSomething);
11+
scope.join();
12+
}
13+
}
14+
15+
@WithSpan("child")
16+
void doSomething() {
17+
// Some basic computations here
18+
}
19+
}

0 commit comments

Comments
 (0)