Skip to content

Commit fbc6b99

Browse files
Added script that will collect thread and heap dumps in case test timeout on CI. (#9414)
1 parent 6e03d19 commit fbc6b99

File tree

2 files changed

+78
-1
lines changed

2 files changed

+78
-1
lines changed

gradle/configure_tests.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import java.time.Duration
22
import java.time.temporal.ChronoUnit
33

4+
apply from: "$rootDir/gradle/dump_hanging_test.gradle"
5+
46
def isTestingInstrumentation(Project project) {
57
return [
68
"junit-4.10",
@@ -128,7 +130,7 @@ if (!project.property("activePartition")) {
128130
}
129131
}
130132

131-
tasks.withType(Test) {
133+
tasks.withType(Test).configureEach {
132134
// https://docs.gradle.com/develocity/flaky-test-detection/
133135
// https://docs.gradle.com/develocity/gradle-plugin/current/#test_retry
134136
develocity.testRetry {

gradle/dump_hanging_test.gradle

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import java.util.concurrent.Executors
2+
import java.util.concurrent.TimeUnit
3+
4+
// Schedule thread and heap dumps collection near test timeout.
5+
tasks.withType(Test).configureEach { testTask ->
6+
doFirst {
7+
def scheduler = Executors.newSingleThreadScheduledExecutor({ r ->
8+
Thread t = new Thread(r, 'dump-scheduler')
9+
t.daemon = true
10+
t
11+
})
12+
13+
// Calculate delay for taking dumps as test timeout minus 2 minutes, but no less than 1 minute.
14+
def delayMinutes = Math.max(1L, timeout.get().minusMinutes(2).toMinutes())
15+
16+
def future = scheduler.schedule({
17+
try {
18+
// Use Gradle's build dir and adjust for CI artifacts collection if needed.
19+
def dumpDir = layout.buildDirectory.dir('dumps').map {
20+
if (providers.environmentVariable("CI").isPresent()) {
21+
// Move reports into the folder collected by the collect_reports.sh script.
22+
new File(it.getAsFile().absolutePath.replace('dd-trace-java/dd-java-agent', 'dd-trace-java/workspace/dd-java-agent'))
23+
} else {
24+
it.asFile
25+
}
26+
}.get()
27+
28+
dumpDir.mkdirs()
29+
30+
// Collect PIDs of all Java processes.
31+
def jvmProcesses = 'jcmd -l'.execute().text.readLines()
32+
33+
// Collect pids for 'Gradle test executors'.
34+
def pids = jvmProcesses
35+
.findAll({ it.contains('Gradle Test Executor') })
36+
.collect({ it.substring(0, it.indexOf(' ')) })
37+
38+
pids.each { pid ->
39+
logger.warn("Taking dumps for: ${testTask.getPath()}")
40+
41+
// Collect thread dump.
42+
def threadDumpFile = new File(dumpDir, "${pid}-thread-dump-${System.currentTimeMillis()}.log")
43+
new ProcessBuilder('jcmd', pid, 'Thread.print', '-l')
44+
.redirectErrorStream(true)
45+
.redirectOutput(threadDumpFile)
46+
.start()
47+
.waitFor()
48+
49+
// Collect heap dump.
50+
def heapDumpFile = new File(dumpDir, "${pid}-heap-dump-${System.currentTimeMillis()}.hprof").absolutePath
51+
def cmd = "jcmd ${pid} GC.heap_dump ${heapDumpFile}"
52+
cmd.execute().waitFor()
53+
}
54+
} catch (Throwable e) {
55+
logger.warn("Dumping failed: ${e.message}")
56+
}
57+
finally {
58+
scheduler.shutdown()
59+
}
60+
}, delayMinutes, TimeUnit.MINUTES)
61+
62+
// Store handles for cancellation in doLast.
63+
ext.dumpFuture = future
64+
ext.dumpScheduler = scheduler
65+
}
66+
67+
doLast {
68+
// Cancel if the task finished before the scheduled dump.
69+
try {
70+
ext.dumpFuture?.cancel(false)
71+
} finally {
72+
ext.dumpScheduler?.shutdownNow()
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)