diff --git a/.gitlab/collect_reports.sh b/.gitlab/collect_reports.sh index 83c3d99d38a..1b30beb6a6e 100755 --- a/.gitlab/collect_reports.sh +++ b/.gitlab/collect_reports.sh @@ -62,6 +62,7 @@ function process_reports () { cp -r workspace/$project_to_save/build/reports/* $report_path/ 2>/dev/null || true cp workspace/$project_to_save/build/hs_err_pid*.log $report_path/ 2>/dev/null || true cp workspace/$project_to_save/build/javacore*.txt $report_path/ 2>/dev/null || true + cp workspace/$project_to_save/build/*.* $report_path/ 2>/dev/null || true fi } diff --git a/dd-java-agent/instrumentation/java-concurrent/java-concurrent-21/src/previewTest/groovy/StructuredConcurrencyTest.groovy b/dd-java-agent/instrumentation/java-concurrent/java-concurrent-21/src/previewTest/groovy/StructuredConcurrencyTest.groovy index 0c0280afbaf..37a97aae974 100644 --- a/dd-java-agent/instrumentation/java-concurrent/java-concurrent-21/src/previewTest/groovy/StructuredConcurrencyTest.groovy +++ b/dd-java-agent/instrumentation/java-concurrent/java-concurrent-21/src/previewTest/groovy/StructuredConcurrencyTest.groovy @@ -1,14 +1,46 @@ +import com.sun.management.HotSpotDiagnosticMXBean import datadog.trace.agent.test.AgentTestRunner import datadog.trace.api.Trace +import javax.management.MBeanServer +import java.lang.management.ManagementFactory import java.util.concurrent.Callable +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture import java.util.concurrent.StructuredTaskScope +import java.util.concurrent.TimeUnit import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace import static datadog.trace.agent.test.utils.TraceUtils.runnableUnderTrace import static java.time.Instant.now class StructuredConcurrencyTest extends AgentTestRunner { + ThreadDumpLogger threadDumpLogger + + def setup() { + File reportDir = new File("build") + String fullPath = reportDir.absolutePath.replace("dd-trace-java/dd-java-agent", + "dd-trace-java/workspace/dd-java-agent") + + reportDir = new File(fullPath) + if (!reportDir.exists()) { + println("Folder not found: " + fullPath) + reportDir.mkdirs() + } else println("Folder found: " + fullPath) + + // Use the current feature name as the test name + String testName = "${specificationContext?.currentSpec?.name ?: "unknown-spec"} : ${specificationContext?.currentFeature?.name ?: "unknown-test"}" + + threadDumpLogger = new ThreadDumpLogger(testName, reportDir) + threadDumpLogger.start() + } + + def cleanup() { + threadDumpLogger.stop() + } + + /** * Tests the structured task scope with a single task. */ @@ -20,12 +52,12 @@ class StructuredConcurrencyTest extends AgentTestRunner { when: runUnderTrace("parent") { def task = taskScope.fork(new Callable() { - @Trace(operationName = "child") - @Override - Boolean call() throws Exception { - return true - } - }) + @Trace(operationName = "child") + @Override + Boolean call() throws Exception { + return true + } + }) taskScope.joinUntil(now() + 10) // Wait for 10 seconds at maximum result = task.get() } @@ -164,4 +196,48 @@ class StructuredConcurrencyTest extends AgentTestRunner { } } } + // 🔒 Private helper class for thread dump logging + private static class ThreadDumpLogger { + private final String testName + private final File outputDir + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor() + private ScheduledFuture task + + ThreadDumpLogger(String testName, File outputDir) { + this.testName = testName + this.outputDir = outputDir + } + + void start() { + task = scheduler.scheduleAtFixedRate({ + heapDump("test_1") + + def reportFile = new File(outputDir, "${System.currentTimeMillis()}-thread-dump.log") + try (def writer = new FileWriter(reportFile)) { + writer.write("=== Test: ${testName} ===\n") + writer.write("=== Thread Dump Triggered at ${new Date()} ===\n") + Thread.getAllStackTraces().each { thread, stack -> + writer.write("Thread: ${thread.name}, daemon: ${thread.daemon}\n") + stack.each { writer.write("\tat ${it}\n") } + } + writer.write("==============================================\n") + } + + heapDump("test_2") + }, 100, 60010, TimeUnit.MILLISECONDS) + } + + void heapDump(String kind) { + def heapDumpFile = new File(outputDir, "${System.currentTimeMillis()}-heap-dump-${kind}.hprof").absolutePath + MBeanServer server = ManagementFactory.getPlatformMBeanServer() + HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy( + server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class) + mxBean.dumpHeap(heapDumpFile, true) + } + + void stop() { + task?.cancel(false) + scheduler.shutdownNow() + } + } } diff --git a/dd-java-agent/instrumentation/lettuce-4/src/test/groovy/Lettuce4ClientTestBase.groovy b/dd-java-agent/instrumentation/lettuce-4/src/test/groovy/Lettuce4ClientTestBase.groovy index 6d178e33622..f7881bdb31a 100644 --- a/dd-java-agent/instrumentation/lettuce-4/src/test/groovy/Lettuce4ClientTestBase.groovy +++ b/dd-java-agent/instrumentation/lettuce-4/src/test/groovy/Lettuce4ClientTestBase.groovy @@ -3,11 +3,19 @@ import com.lambdaworks.redis.RedisClient import com.lambdaworks.redis.api.StatefulConnection import com.lambdaworks.redis.api.async.RedisAsyncCommands import com.lambdaworks.redis.api.sync.RedisCommands +import com.sun.management.HotSpotDiagnosticMXBean import datadog.trace.agent.test.naming.VersionedNamingTestBase import datadog.trace.agent.test.utils.PortUtils import redis.embedded.RedisServer import spock.lang.Shared +import javax.management.MBeanServer +import java.lang.management.ManagementFactory +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace abstract class Lettuce4ClientTestBase extends VersionedNamingTestBase { @@ -32,6 +40,8 @@ abstract class Lettuce4ClientTestBase extends VersionedNamingTestBase { @Shared RedisServer redisServer + ThreadDumpLogger threadDumpLogger + @Shared Map testHashMap = [ firstname: "John", @@ -53,14 +63,30 @@ abstract class Lettuce4ClientTestBase extends VersionedNamingTestBase { embeddedDbUri = "redis://" + dbAddr redisServer = RedisServer.newRedisServer() - // bind to localhost to avoid firewall popup - .setting("bind " + HOST) - // set max memory to avoid problems in CI - .setting("maxmemory 128M") - .port(port).build() + // bind to localhost to avoid firewall popup + .setting("bind " + HOST) + // set max memory to avoid problems in CI + .setting("maxmemory 128M") + .port(port).build() } def setup() { + File reportDir = new File("build") + String fullPath = reportDir.absolutePath.replace("dd-trace-java/dd-java-agent", + "dd-trace-java/workspace/dd-java-agent") + + reportDir = new File(fullPath) + if (!reportDir.exists()) { + println("Folder not found: " + fullPath) + reportDir.mkdirs() + } else println("Folder found: " + fullPath) + + // Use the current feature name as the test name + String testName = "${specificationContext?.currentSpec?.name ?: "unknown-spec"} : ${specificationContext?.currentFeature?.name ?: "unknown-test"}" + + threadDumpLogger = new ThreadDumpLogger(testName, reportDir) + threadDumpLogger.start() + redisServer.start() redisClient = RedisClient.create(embeddedDbUri) @@ -79,8 +105,55 @@ abstract class Lettuce4ClientTestBase extends VersionedNamingTestBase { } def cleanup() { + threadDumpLogger.stop() + connection.close() redisClient.shutdown() redisServer.stop() } + + // 🔒 Private helper class for thread dump logging + private static class ThreadDumpLogger { + private final String testName + private final File outputDir + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor() + private ScheduledFuture task + + ThreadDumpLogger(String testName, File outputDir) { + this.testName = testName + this.outputDir = outputDir + } + + void start() { + // new File(outputDir, "${System.currentTimeMillis()}-start-mark.txt") << testName + + task = scheduler.scheduleAtFixedRate({ + heapDump("test") + + def reportFile = new File(outputDir, "${System.currentTimeMillis()}-thread-dump.log") + try (def writer = new FileWriter(reportFile)) { + writer.write("=== Test: ${testName} ===\n") + writer.write("=== Thread Dump Triggered at ${new Date()} ===\n") + Thread.getAllStackTraces().each { thread, stack -> + writer.write("Thread: ${thread.name}, daemon: ${thread.daemon}\n") + stack.each { writer.write("\tat ${it}\n") } + } + writer.write("==============================================\n") + } + }, 100, 60000, TimeUnit.MILLISECONDS) + } + + void heapDump(String kind) { + def heapDumpFile = new File(outputDir, "${System.currentTimeMillis()}-heap-dump-${kind}.hprof").absolutePath + MBeanServer server = ManagementFactory.getPlatformMBeanServer() + HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy( + server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class) + mxBean.dumpHeap(heapDumpFile, true) + } + + void stop() { + task?.cancel(false) + scheduler.shutdownNow() + } + } } diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy index 96383049879..5ef385bf1a9 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy @@ -275,6 +275,7 @@ abstract class HttpClientTest extends VersionedNamingTestBase { body = (1..10000).join(" ") } + @Flaky(suites = ["ApacheHttpAsyncClient5NamingV0Test"]) def "basic #method request with parent"() { when: def status = runUnderTrace("parent") { @@ -308,7 +309,7 @@ abstract class HttpClientTest extends VersionedNamingTestBase { body = (1..10000).join(" ") } - @Flaky(suites = ["ApacheHttpAsyncClient5Test"]) + @Flaky(suites = ["ApacheHttpAsyncClient5NamingV0Test"]) def "server error request with parent"() { setup: def uri = server.address.resolve("/error") @@ -346,7 +347,7 @@ abstract class HttpClientTest extends VersionedNamingTestBase { "POST" | _ } - @Flaky(suites = ["ApacheHttpAsyncClient5Test", "ApacheHttpAsyncClient5NamingV0Test"]) + @Flaky(suites = ["ApacheHttpAsyncClient5NamingV0Test"]) def "client error request with parent"() { setup: def uri = server.address.resolve("/secured") @@ -415,7 +416,6 @@ abstract class HttpClientTest extends VersionedNamingTestBase { method = "HEAD" } - @Flaky(suites = ["ApacheHttpAsyncClient5Test"]) def "trace request without propagation"() { when: injectSysConfig(HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, "$renameService")