Skip to content

Commit fd8a076

Browse files
sarahchen6PerfectSlayer
authored andcommitted
Add smoke tests for java's concurrent API (#8438)
* Create initial app and ExecutorService demo. * Add ForkJoin demo. * Start writing tests. * Add simple OTel spans for now. * Add FibonacciCalculator interface and adjust tests and demos accordingly. * Change trace expectations to reality. * Extract calculations to helper function and update tests. * Check that span name is as expected. * Clean PR. * Update settings.gradle Co-authored-by: Bruce Bujon <[email protected]> * Update dd-smoke-tests/concurrent/src/main/java/datadog/smoketest/concurrent/demoExecutorService.java Co-authored-by: Bruce Bujon <[email protected]> * Update dd-smoke-tests/concurrent/src/main/java/datadog/smoketest/concurrent/demoForkJoin.java Co-authored-by: Bruce Bujon <[email protected]> * Update names. * Clean build.gradle. * Format tests. * Implement close method. * Organize methods in demo classes and add child span tests (but no child spans right now??). * Edit tests. * feat: Update executor app and checks * Propagate changes. * Refactor tests. * Fix variable name. * Adjust tests again. --------- Co-authored-by: Bruce Bujon <[email protected]> Co-authored-by: Bruce Bujon <[email protected]>
1 parent 4c2a2f5 commit fd8a076

File tree

11 files changed

+274
-7
lines changed

11 files changed

+274
-7
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
plugins {
2+
id 'java'
3+
id 'application'
4+
id 'com.github.johnrengelman.shadow'
5+
}
6+
7+
apply from: "$rootDir/gradle/java.gradle"
8+
9+
description = 'Concurrent Integration Tests.'
10+
11+
application {
12+
mainClassName = 'datadog.smoketest.concurrent.ConcurrentApp'
13+
}
14+
15+
dependencies {
16+
implementation('io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.13.1')
17+
implementation project(':dd-trace-api')
18+
testImplementation project(':dd-smoke-tests')
19+
20+
testImplementation platform('org.junit:junit-bom:5.10.0')
21+
testImplementation 'org.junit.jupiter:junit-jupiter'
22+
}
23+
24+
test {
25+
useJUnitPlatform()
26+
}
27+
28+
tasks.withType(Test).configureEach {
29+
dependsOn "shadowJar"
30+
31+
jvmArgs "-Ddatadog.smoketest.shadowJar.path=${tasks.shadowJar.archiveFile.get()}"
32+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package datadog.smoketest.concurrent;
2+
3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
4+
import java.util.concurrent.ExecutionException;
5+
6+
public class ConcurrentApp {
7+
@WithSpan("main")
8+
public static void main(String[] args) {
9+
// Calculate fibonacci using concurrent strategies
10+
for (String arg : args) {
11+
try (FibonacciCalculator calc = getCalculator(arg)) {
12+
calc.computeFibonacci(10);
13+
} catch (ExecutionException | InterruptedException e) {
14+
throw new RuntimeException("Failed to compute fibonacci number.", e);
15+
}
16+
}
17+
}
18+
19+
private static FibonacciCalculator getCalculator(String name) {
20+
if (name.equalsIgnoreCase("executorService")) {
21+
return new DemoExecutorService();
22+
} else if (name.equalsIgnoreCase("forkJoin")) {
23+
return new DemoForkJoin();
24+
}
25+
throw new IllegalArgumentException("Unknown calculator: " + name);
26+
}
27+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package datadog.smoketest.concurrent;
2+
3+
import static java.util.concurrent.TimeUnit.SECONDS;
4+
5+
import io.opentelemetry.instrumentation.annotations.WithSpan;
6+
import java.util.concurrent.Callable;
7+
import java.util.concurrent.ExecutionException;
8+
import java.util.concurrent.ExecutorService;
9+
import java.util.concurrent.Executors;
10+
import java.util.concurrent.Future;
11+
12+
public class DemoExecutorService implements FibonacciCalculator {
13+
private final ExecutorService executorService;
14+
15+
public DemoExecutorService() {
16+
executorService = Executors.newFixedThreadPool(10);
17+
}
18+
19+
@WithSpan("compute")
20+
@Override
21+
public long computeFibonacci(int n) throws ExecutionException, InterruptedException {
22+
Future<Long> future = executorService.submit(new FibonacciTask(n));
23+
return future.get();
24+
}
25+
26+
private class FibonacciTask implements Callable<Long> {
27+
private final int n;
28+
29+
public FibonacciTask(int n) {
30+
this.n = n;
31+
}
32+
33+
@Override
34+
public Long call() throws ExecutionException, InterruptedException {
35+
if (n <= 1) {
36+
return (long) n;
37+
}
38+
return computeFibonacci(n - 1) + computeFibonacci(n - 2);
39+
}
40+
}
41+
42+
@Override
43+
public void close() {
44+
executorService.shutdown();
45+
try {
46+
if (!executorService.awaitTermination(10, SECONDS)) {
47+
executorService.shutdownNow();
48+
}
49+
} catch (InterruptedException e) {
50+
executorService.shutdownNow();
51+
}
52+
}
53+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package datadog.smoketest.concurrent;
2+
3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
4+
import java.util.concurrent.ForkJoinPool;
5+
import java.util.concurrent.RecursiveTask;
6+
7+
public class DemoForkJoin implements FibonacciCalculator {
8+
private final ForkJoinPool forkJoinPool;
9+
10+
public DemoForkJoin() {
11+
forkJoinPool = new ForkJoinPool();
12+
}
13+
14+
@Override
15+
public long computeFibonacci(int n) {
16+
return forkJoinPool.invoke(new FibonacciTask(n));
17+
}
18+
19+
private class FibonacciTask extends RecursiveTask<Long> {
20+
private final int n;
21+
22+
public FibonacciTask(int n) {
23+
this.n = n;
24+
}
25+
26+
@WithSpan("compute")
27+
@Override
28+
protected Long compute() {
29+
if (n <= 1) {
30+
return (long) n;
31+
}
32+
FibonacciTask taskOne = new FibonacciTask(n - 1);
33+
taskOne.fork();
34+
FibonacciTask taskTwo = new FibonacciTask(n - 2);
35+
return taskTwo.compute() + taskOne.join();
36+
}
37+
}
38+
39+
@Override
40+
public void close() {
41+
forkJoinPool.shutdown();
42+
}
43+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package datadog.smoketest.concurrent;
2+
3+
import java.util.concurrent.ExecutionException;
4+
5+
public interface FibonacciCalculator extends AutoCloseable {
6+
long computeFibonacci(int n) throws ExecutionException, InterruptedException;
7+
8+
@Override
9+
void close();
10+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package datadog.smoketest
2+
3+
import static java.util.concurrent.TimeUnit.SECONDS
4+
import datadog.trace.test.agent.decoder.DecodedTrace
5+
import java.util.function.Function
6+
7+
abstract class AbstractDemoTest extends AbstractSmokeTest {
8+
protected static final int TIMEOUT_SECS = 10
9+
protected abstract List<String> getTestArguments()
10+
11+
@Override
12+
ProcessBuilder createProcessBuilder() {
13+
def jarPath = System.getProperty("datadog.smoketest.shadowJar.path")
14+
def command = new ArrayList<String>()
15+
command.add(javaPath())
16+
command.addAll(defaultJavaProperties)
17+
command.add("-Ddd.trace.otel.enabled=true")
18+
command.addAll(["-jar", jarPath])
19+
command.addAll(getTestArguments())
20+
21+
ProcessBuilder processBuilder = new ProcessBuilder(command)
22+
processBuilder.directory(new File(buildDirectory))
23+
}
24+
25+
@Override
26+
Closure decodedTracesCallback() {
27+
return {} // force traces decoding
28+
}
29+
30+
protected static Function<DecodedTrace, Boolean> checkTrace() {
31+
return {
32+
trace ->
33+
// Check for 'main' span
34+
def mainSpan = trace.spans.find { it.name == 'main' }
35+
if (!mainSpan) {
36+
return false
37+
}
38+
// Check that there are only 'main' and 'compute' spans
39+
def otherSpans = trace.spans.findAll { it.name != 'main' && it.name != 'compute' }
40+
if (!otherSpans.isEmpty()) {
41+
return false
42+
}
43+
// Check that every 'compute' span is in the same trace and is either a child of the 'main' span or another 'compute' span
44+
def computeSpans = trace.spans.findAll { it.name == 'compute' }
45+
if (computeSpans.isEmpty()) {
46+
return false
47+
}
48+
return computeSpans.every {
49+
if (it.traceId != mainSpan.traceId) {
50+
return false
51+
}
52+
if (it.parentId != mainSpan.spanId && trace.spans.find(s -> s.spanId == it.parentId).name != 'compute') {
53+
return false
54+
}
55+
return true
56+
}
57+
}
58+
}
59+
60+
protected void receivedCorrectTrace() {
61+
waitForTrace(defaultPoll, checkTrace())
62+
assert traceCount.get() == 1
63+
assert testedProcess.waitFor(TIMEOUT_SECS, SECONDS)
64+
assert testedProcess.exitValue() == 0
65+
}
66+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package datadog.smoketest
2+
3+
class DemoExecutorServiceTest extends AbstractDemoTest {
4+
protected List<String> getTestArguments() {
5+
return ["executorService"]
6+
}
7+
8+
def 'receive one correct trace when using ExecutorService'() {
9+
expect:
10+
receivedCorrectTrace()
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package datadog.smoketest
2+
3+
class DemoForkJoinTest extends AbstractDemoTest {
4+
protected List<String> getTestArguments() {
5+
return ["forkJoin"]
6+
}
7+
8+
def 'receive one correct trace when using ForkJoin'() {
9+
expect:
10+
receivedCorrectTrace()
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package datadog.smoketest
2+
3+
class DemoMultipleConcurrenciesTest extends AbstractDemoTest {
4+
protected List<String> getTestArguments() {
5+
return ["executorService", "forkJoin"]
6+
}
7+
8+
def 'receive one correct trace when using multiple concurrency strategies (ExecutorService and ForkJoin)'() {
9+
expect:
10+
receivedCorrectTrace()
11+
}
12+
}

dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ abstract class AbstractSmokeTest extends ProcessManager {
5050
@Shared
5151
protected TestHttpServer.Headers lastTraceRequestHeaders = null
5252

53+
@Shared
54+
protected final PollingConditions defaultPoll = new PollingConditions(timeout: 30, initialDelay: 0, delay: 1, factor: 1)
55+
5356
@Shared
5457
@AutoCleanup
5558
protected TestHttpServer server = httpServer {
@@ -292,8 +295,7 @@ abstract class AbstractSmokeTest extends ProcessManager {
292295
}
293296

294297
int waitForTraceCount(int count) {
295-
def conditions = new PollingConditions(timeout: 30, initialDelay: 0, delay: 0.5, factor: 1)
296-
return waitForTraceCount(count, conditions)
298+
return waitForTraceCount(count, defaultPoll)
297299
}
298300

299301
int waitForTraceCount(int count, PollingConditions conditions) {
@@ -325,8 +327,7 @@ abstract class AbstractSmokeTest extends ProcessManager {
325327
}
326328

327329
void waitForTelemetryCount(final int count) {
328-
def conditions = new PollingConditions(timeout: 30, initialDelay: 0, delay: 1, factor: 1)
329-
waitForTelemetryCount(conditions, count)
330+
waitForTelemetryCount(defaultPoll, count)
330331
}
331332

332333
void waitForTelemetryCount(final PollingConditions poll, final int count) {
@@ -336,8 +337,7 @@ abstract class AbstractSmokeTest extends ProcessManager {
336337
}
337338

338339
void waitForTelemetryFlat(final Function<Map<String, Object>, Boolean> predicate) {
339-
def conditions = new PollingConditions(timeout: 30, initialDelay: 0, delay: 1, factor: 1)
340-
waitForTelemetryFlat(conditions, predicate)
340+
waitForTelemetryFlat(defaultPoll, predicate)
341341
}
342342

343343
void waitForTelemetryFlat(final PollingConditions poll, final Function<Map<String, Object>, Boolean> predicate) {

settings.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ include ':dd-smoke-tests:apm-tracing-disabled'
9898
include ':dd-smoke-tests:armeria-grpc'
9999
include ':dd-smoke-tests:backend-mock'
100100
include ':dd-smoke-tests:cli'
101+
include ':dd-smoke-tests:concurrent'
101102
include ':dd-smoke-tests:crashtracking'
102103
include ':dd-smoke-tests:custom-systemloader'
103104
include ':dd-smoke-tests:dynamic-config'
@@ -534,4 +535,3 @@ include ':dd-java-agent:benchmark'
534535
include ':dd-java-agent:benchmark-integration'
535536
include ':dd-java-agent:benchmark-integration:jetty-perftest'
536537
include ':dd-java-agent:benchmark-integration:play-perftest'
537-

0 commit comments

Comments
 (0)