From c05c3aa4322fe0d47479764e05362fde3d17ce7a Mon Sep 17 00:00:00 2001 From: RomanDavlyatshin Date: Thu, 28 May 2026 17:06:26 +0400 Subject: [PATCH 1/4] chore: empty commit From 963ce61e07c57c59ba4f05fea854e91208fd0bcc Mon Sep 17 00:00:00 2001 From: RomanDavlyatshin Date: Tue, 2 Jun 2026 21:15:15 +0400 Subject: [PATCH 2/4] feat: add shutdown coordinator & flush data on JVM shutdown --- .../configuration/ParameterDefinitions.kt | 5 + .../lifecycle/AgentShutdownCoordinator.kt | 21 +++ .../kotlin/com/epam/drill/agent/Agent.kt | 4 +- .../lifecycle/AgentShutdownCoordinator.kt | 82 ++++++++ .../agent/test/sending/TestInfoSender.kt | 24 ++- .../agent/test/session/SessionController.kt | 5 +- .../transport/DataIngestMessageSender.kt | 20 +- .../kotlin/com/epam/drill/agent/Agent.kt | 8 +- .../com/epam/drill/agent/jvmti/JvmtiEvents.kt | 4 - .../lifecycle/AgentShutdownCoordinator.kt | 26 +++ .../transport/QueuedAgentMessageSender.kt | 60 ++++-- lib-jvm-shared/gradlew.bat | 178 +++++++++--------- .../epam/drill/agent/test2code/Test2Code.kt | 5 +- .../test2code/coverage/CoverageManager.kt | 4 + .../test2code/coverage/CoverageSender.kt | 22 ++- .../coverage/GlobalCoverageRecorder.kt | 3 + 16 files changed, 338 insertions(+), 133 deletions(-) create mode 100644 java-agent/src/commonMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt create mode 100644 java-agent/src/jvmMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt create mode 100644 java-agent/src/nativeMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt diff --git a/java-agent/src/commonMain/kotlin/com/epam/drill/agent/configuration/ParameterDefinitions.kt b/java-agent/src/commonMain/kotlin/com/epam/drill/agent/configuration/ParameterDefinitions.kt index 894669d68..d1abaa4b8 100644 --- a/java-agent/src/commonMain/kotlin/com/epam/drill/agent/configuration/ParameterDefinitions.kt +++ b/java-agent/src/commonMain/kotlin/com/epam/drill/agent/configuration/ParameterDefinitions.kt @@ -36,6 +36,11 @@ object ParameterDefinitions: AgentParameterDefinitionCollection() { defaultValue = "QUEUED").register() val MESSAGE_QUEUE_LIMIT = AgentParameterDefinition.forString(name = "messageQueueLimit", defaultValue = "512Mb").register() val MESSAGE_MAX_RETRIES = AgentParameterDefinition.forInt(name = "messageMaxRetries", defaultValue = Int.MAX_VALUE).register() + val SHUTDOWN_FLUSH_TIMEOUT_MS = AgentParameterDefinition.forInt( + name = "shutdownFlushTimeoutMs", + description = "Maximum time in milliseconds to stop producers and flush messages during JVM shutdown", + defaultValue = 60_000, + ).register() val SSL_TRUSTSTORE = NullableAgentParameterDefinition.forString(name = "sslTruststore").register() val SSL_TRUSTSTORE_PASSWORD = NullableAgentParameterDefinition.forString(name = "sslTruststorePassword").register() val LOG_LEVEL = AgentParameterDefinition.forString(name = "logLevel", defaultValue = "INFO").register() diff --git a/java-agent/src/commonMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt b/java-agent/src/commonMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt new file mode 100644 index 000000000..c08d30b67 --- /dev/null +++ b/java-agent/src/commonMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt @@ -0,0 +1,21 @@ +/** + * Copyright 2020 - 2022 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.epam.drill.agent.lifecycle + +expect object AgentShutdownCoordinator { + fun install() + fun shutdown() +} diff --git a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/Agent.kt b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/Agent.kt index d3c0ad205..230c7687b 100644 --- a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/Agent.kt +++ b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/Agent.kt @@ -27,6 +27,7 @@ import com.epam.drill.agent.module.JvmModuleLoader import com.epam.drill.agent.test.session.SessionController import com.epam.drill.agent.test2code.Test2Code import com.epam.drill.agent.test2code.configuration.Test2CodeParameterDefinitions +import com.epam.drill.agent.lifecycle.AgentShutdownCoordinator import com.epam.drill.agent.transport.JvmModuleMessageSender import mu.KotlinLogging import java.lang.instrument.ClassFileTransformer @@ -61,6 +62,7 @@ fun premain(agentArgs: String?, inst: Instrumentation) { } SessionController.startSession() + AgentShutdownCoordinator.install() } catch (e: Throwable) { println("Drill4J Initialization Error:\n${e.message ?: e::class.java.name}") } @@ -82,7 +84,7 @@ fun main(args: Array) { JvmModuleMessageSender.sendBuildMetadata() val test2Code = JvmModuleLoader.loadJvmModule(Test2Code::class.java.name) as Test2Code test2Code.scanAndSendMetadataClasses() - Runtime.getRuntime().addShutdownHook(Thread { JvmModuleMessageSender.shutdown() }) + AgentShutdownCoordinator.install() exitProcess(0) } catch (e: Throwable) { println("Drill4J Initialization Error:\n${e.message ?: e::class.java.name}") diff --git a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt new file mode 100644 index 000000000..af0b592f8 --- /dev/null +++ b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt @@ -0,0 +1,82 @@ +/** + * Copyright 2020 - 2022 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.epam.drill.agent.lifecycle + +import com.epam.drill.agent.common.lifecycle.AgentShutdownRegistry +import com.epam.drill.agent.configuration.Configuration +import com.epam.drill.agent.configuration.ParameterDefinitions +import com.epam.drill.agent.transport.DataIngestMessageSender +import mu.KotlinLogging +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Coordinates JVM shutdown: stop producers, then flush the shared message sender. + */ +actual object AgentShutdownCoordinator { + private val logger = KotlinLogging.logger {} + private val hookInstalled = AtomicBoolean(false) + private val shutdownCompleted = AtomicBoolean(false) + + actual fun install() { + if (!hookInstalled.compareAndSet(false, true)) return + Runtime.getRuntime().addShutdownHook( + Thread({ shutdown() }, "drill-shutdown-hook") + ) + logger.debug { "Agent shutdown hook installed." } + } + + actual fun shutdown() { + if (!shutdownCompleted.compareAndSet(false, true)) return + runShutdown() + } + + private fun runShutdown() { + val flushTimeoutMs = Configuration.parameters[ParameterDefinitions.SHUTDOWN_FLUSH_TIMEOUT_MS].toLong() + val deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(flushTimeoutMs) + logger.debug { "Agent shutdown started, flush timeout is ${flushTimeoutMs}ms." } + + for (task in AgentShutdownRegistry.tasks()) { + val remainingMs = remainingMs(deadlineNanos) + if (remainingMs <= 0) { + logger.warn { "Shutdown flush timeout reached before task '${task.name}'." } + break + } + runCatching { + logger.debug { "Running shutdown task '${task.name}' (${remainingMs}ms remaining)." } + task.action(remainingMs) + }.onFailure { + logger.error(it) { "Shutdown task '${task.name}' failed." } + } + } + + val remainingMs = remainingMs(deadlineNanos) + if (remainingMs <= 0) { + logger.warn { "Shutdown flush timeout exhausted, attempting a best-effort message sender flush." } + } else { + logger.debug { "Flushing message sender, ${remainingMs}ms remaining." } + } + runCatching { + DataIngestMessageSender.shutdownWithTimeout(remainingMs) + }.onFailure { + logger.error(it) { "Message sender shutdown failed." } + } + logger.debug { "Agent shutdown completed." } + } + + private fun remainingMs(deadlineNanos: Long): Long = + TimeUnit.NANOSECONDS.toMillis(deadlineNanos - System.nanoTime()).coerceAtLeast(0) +} diff --git a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/test/sending/TestInfoSender.kt b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/test/sending/TestInfoSender.kt index 20634f2a4..9914153d0 100644 --- a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/test/sending/TestInfoSender.kt +++ b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/test/sending/TestInfoSender.kt @@ -27,7 +27,7 @@ import java.util.concurrent.TimeUnit interface TestInfoSender { fun startSendingTests() - fun stopSendingTests() + fun stopSendingTests(remainingMs: Long) } class IntervalTestInfoSender( @@ -36,11 +36,21 @@ class IntervalTestInfoSender( private val collectTests: () -> List = { emptyList() } ) : TestInfoSender { private val logger = KotlinLogging.logger {} - private val scheduledThreadPool = Executors.newSingleThreadScheduledExecutor() + private val scheduledThreadPool = Executors.newSingleThreadScheduledExecutor { r -> + Thread(r, "drill-test-info-sender").apply { + isDaemon = true + } + } override fun startSendingTests() { scheduledThreadPool.scheduleAtFixedRate( - { sendTests(collectTests()) }, + { + try { + sendTests(collectTests()) + } catch (t: Throwable) { + logger.error(t) { "Test sending job failed" } + } + }, 0, intervalMs, TimeUnit.MILLISECONDS @@ -48,13 +58,11 @@ class IntervalTestInfoSender( logger.debug { "Test sending job is started." } } - override fun stopSendingTests() { + override fun stopSendingTests(remainingMs: Long) { sendTests(collectTests()) - messageSender.shutdown() scheduledThreadPool.shutdown() - if (!scheduledThreadPool.awaitTermination(1, TimeUnit.SECONDS)) { - logger.error("Failed to send some tests prior to shutdown") - scheduledThreadPool.shutdownNow(); + if (remainingMs > 0 && !scheduledThreadPool.awaitTermination(remainingMs, TimeUnit.MILLISECONDS)) { + logger.warn { "Test sending scheduler did not stop within ${remainingMs}ms; leaving it for JVM exit." } } logger.info { "Test sending job is stopped." } } diff --git a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/test/session/SessionController.kt b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/test/session/SessionController.kt index f9dd6ca21..b34e44aaa 100644 --- a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/test/session/SessionController.kt +++ b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/test/session/SessionController.kt @@ -30,6 +30,7 @@ import com.epam.drill.agent.test.execution.TestController import com.epam.drill.agent.test.execution.TestExecutionInfo import com.epam.drill.agent.test.sending.TestDefinitionPayload import com.epam.drill.agent.test.sending.TestLaunchPayload +import com.epam.drill.agent.common.lifecycle.AgentShutdownRegistry import com.epam.drill.agent.transport.DataIngestMessageSender import mu.KotlinLogging import java.time.Instant @@ -63,7 +64,9 @@ actual object SessionController { logger.info { "Test session: $sessionId" } testInfoSender.startSendingTests() - Runtime.getRuntime().addShutdownHook(Thread { testInfoSender.stopSendingTests() }) + AgentShutdownRegistry.register("test-info-sender") { remainingMs -> + testInfoSender.stopSendingTests(remainingMs) + } val builds = takeIf { Configuration.parameters[ParameterDefinitions.RECOMMENDED_TESTS_TARGET_APP_ID].isNotEmpty() }?.let { diff --git a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/DataIngestMessageSender.kt b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/DataIngestMessageSender.kt index 245ad926a..1279af9c4 100644 --- a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/DataIngestMessageSender.kt +++ b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/DataIngestMessageSender.kt @@ -15,6 +15,7 @@ */ package com.epam.drill.agent.transport +import com.epam.drill.agent.common.transport.AgentMessageDestination import com.epam.drill.agent.common.transport.AgentMessageSender import com.epam.drill.agent.configuration.Configuration import com.epam.drill.agent.configuration.DefaultParameterDefinitions @@ -22,6 +23,7 @@ import com.epam.drill.agent.configuration.ParameterDefinitions import com.epam.drill.agent.transport.http.HttpAgentMessageTransport import io.aesy.datasize.ByteUnit import io.aesy.datasize.DataSize +import kotlinx.serialization.KSerializer import mu.KotlinLogging import java.io.File import kotlin.takeIf @@ -29,7 +31,21 @@ import kotlin.takeIf private const val QUEUE_DEFAULT_SIZE: Long = 512L * 1024 * 1024 private val logger = KotlinLogging.logger {} -object DataIngestMessageSender : AgentMessageSender by messageSender() +object DataIngestMessageSender : AgentMessageSender { + private val delegate: AgentMessageSender = messageSender() + + override fun send(destination: AgentMessageDestination, message: T, serializer: KSerializer) = + delegate.send(destination, message, serializer) + + override fun shutdown() = shutdownWithTimeout(Long.MAX_VALUE) + + fun shutdownWithTimeout(flushTimeoutMs: Long) { + when (val sender = delegate) { + is QueuedAgentMessageSender -> sender.shutdown(flushTimeoutMs) + else -> sender.shutdown() + } + } +} fun messageSender(): AgentMessageSender { val transport = agentMessageTransport() @@ -86,4 +102,4 @@ private fun parseBytes(value: String): Long = value.run { .onFailure(logError) .getOrDefault(DataSize.of(QUEUE_DEFAULT_SIZE, ByteUnit.BYTE)) .toUnit(ByteUnit.BYTE).value.toLong() -} \ No newline at end of file +} diff --git a/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/Agent.kt b/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/Agent.kt index 445f2c89c..b72d53143 100644 --- a/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/Agent.kt +++ b/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/Agent.kt @@ -31,11 +31,11 @@ import com.epam.drill.agent.configuration.DefaultParameterDefinitions import com.epam.drill.agent.configuration.DefaultParameterDefinitions.INSTALLATION_DIR import com.epam.drill.agent.configuration.ParameterDefinitions import com.epam.drill.agent.jvmti.classFileLoadHook -import com.epam.drill.agent.jvmti.vmDeathEvent import com.epam.drill.agent.jvmti.vmInitEvent import com.epam.drill.agent.module.JvmModuleLoader import com.epam.drill.agent.transport.JvmModuleMessageSender import com.epam.drill.agent.jvmapi.gen.* +import com.epam.drill.agent.lifecycle.AgentShutdownCoordinator import com.epam.drill.agent.test.session.SessionController import kotlinx.cinterop.ExperimentalForeignApi import kotlin.experimental.ExperimentalNativeApi @@ -89,10 +89,7 @@ object Agent { JvmModuleMessageSender.sendAgentMetadata() } SessionController.startSession() - } - - fun agentOnVmDeath() { - logger.debug { "agentOnVmDeath" } + AgentShutdownCoordinator.install() } @OptIn(ExperimentalForeignApi::class) @@ -107,7 +104,6 @@ object Agent { private fun setEventCallbacks() = memScoped { val alloc = alloc() alloc.VMInit = staticCFunction(::vmInitEvent) - alloc.VMDeath = staticCFunction(::vmDeathEvent) alloc.ClassFileLoadHook = staticCFunction(::classFileLoadHook) SetEventCallbacks(alloc.ptr, sizeOf().toInt()) SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, null) diff --git a/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/jvmti/JvmtiEvents.kt b/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/jvmti/JvmtiEvents.kt index 889c5632d..74f06439c 100644 --- a/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/jvmti/JvmtiEvents.kt +++ b/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/jvmti/JvmtiEvents.kt @@ -33,10 +33,6 @@ import kotlinx.cinterop.ExperimentalForeignApi @Suppress("unused_parameter") fun vmInitEvent(env: CPointer?, jniEnv: CPointer?, thread: jthread?) = Agent.agentOnVmInit() -@OptIn(ExperimentalForeignApi::class) -@Suppress("unused_parameter") -fun vmDeathEvent(jvmtiEnv: CPointer?, jniEnv: CPointer?) = Agent.agentOnVmDeath() - @OptIn(ExperimentalForeignApi::class) @Suppress("unused_parameter") fun classFileLoadHook( diff --git a/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt b/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt new file mode 100644 index 000000000..cba4fb287 --- /dev/null +++ b/java-agent/src/nativeMain/kotlin/com/epam/drill/agent/lifecycle/AgentShutdownCoordinator.kt @@ -0,0 +1,26 @@ +/** + * Copyright 2020 - 2022 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.epam.drill.agent.lifecycle + +import com.epam.drill.agent.jvmapi.callObjectVoidMethod + +actual object AgentShutdownCoordinator { + actual fun install(): Unit = + callObjectVoidMethod(AgentShutdownCoordinator::class, AgentShutdownCoordinator::install) + + actual fun shutdown(): Unit = + callObjectVoidMethod(AgentShutdownCoordinator::class, AgentShutdownCoordinator::shutdown) +} diff --git a/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/QueuedAgentMessageSender.kt b/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/QueuedAgentMessageSender.kt index 01b1930a8..8393def72 100644 --- a/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/QueuedAgentMessageSender.kt +++ b/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/QueuedAgentMessageSender.kt @@ -42,7 +42,11 @@ open class QueuedAgentMessageSender( private val maxRetries: Int = 5 ) : AgentMessageSender { private val logger = KotlinLogging.logger {} - private val executor: ExecutorService = Executors.newFixedThreadPool(maxThreads) + private val executor: ExecutorService = Executors.newFixedThreadPool( + maxThreads, + drillDaemonThreadFactory("drill-message-sender"), + ) + private val isRunning = AtomicBoolean(true) init { @@ -68,19 +72,43 @@ open class QueuedAgentMessageSender( } override fun shutdown() { + shutdown(Long.MAX_VALUE) + } + + fun shutdown(flushTimeoutMs: Long) { if (!isRunning.compareAndSet(true, false)) return + logger.info { "Shutting down queued message sender, flush timeout is ${flushTimeoutMs}ms." } + val deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(flushTimeoutMs.coerceAtLeast(0)) executor.shutdown() + unloadQueue("sender is shutting down", deadlineNanos) + awaitExecutorDrain(deadlineNanos) + val remainingMs = remainingMs(deadlineNanos) try { - if (!executor.awaitTermination(1, TimeUnit.SECONDS)) { - executor.shutdownNow() + if (remainingMs > 0 && !executor.awaitTermination(remainingMs, TimeUnit.MILLISECONDS)) { + logger.warn { + "Message sender executor did not terminate in ${remainingMs}ms; " + + "leaving worker threads for JVM exit instead of forcing shutdown." + } } } catch (e: InterruptedException) { - logger.error(e) { "Failed to send some messages prior to shutdown" } - executor.shutdownNow() + logger.warn(e) { "Interrupted while waiting for message sender executor to terminate." } + Thread.currentThread().interrupt() + } + if (messageQueue.size() > 0) { + logger.warn { "Shutdown completed with ${messageQueue.size()} message(s) still in the queue." } } - unloadQueue("sender is shutting down") } + private fun awaitExecutorDrain(deadlineNanos: Long) { + while (messageQueue.size() > 0 && System.nanoTime() < deadlineNanos && !executor.isTerminated) { + val remainingMs = remainingMs(deadlineNanos).coerceAtLeast(1) + executor.awaitTermination(minOf(remainingMs, 250), TimeUnit.MILLISECONDS) + } + } + + private fun remainingMs(deadlineNanos: Long): Long = + TimeUnit.NANOSECONDS.toMillis(deadlineNanos - System.nanoTime()).coerceAtLeast(0) + /** * Processes the message queue. * It will try to send the message from a queue to the destination with exponential backoff. @@ -135,15 +163,21 @@ open class QueuedAgentMessageSender( /** * Last attempt to send unsent messages, and register them as unsent if unsuccessful */ - private fun unloadQueue(reason: String) { + private fun unloadQueue(reason: String, deadlineNanos: Long = Long.MAX_VALUE) { if (messageQueue.size() == 0) return - logger.info { "Unloading a message queue as $reason, queue size: ${messageQueue.size()}" } - do { - val message = messageQueue.poll()?.also { (destination, message) -> - tryToSend(destination, message) || handleUnsent(destination, message, reason) + logger.info { "Unloading message queue ($reason), queue size: ${messageQueue.size()}" } + while (messageQueue.size() > 0) { + if (System.nanoTime() >= deadlineNanos) { + logger.warn { + "Shutdown flush timeout while unloading queue, ${messageQueue.size()} message(s) remain." + } + break } - } while (message != null) - logger.info { "Finished unloading a message queue." } + val queued = messageQueue.poll() ?: break + val (destination, message) = queued + tryToSend(destination, message) || handleUnsent(destination, message, reason) + } + logger.info { "Finished unloading message queue." } } /** diff --git a/lib-jvm-shared/gradlew.bat b/lib-jvm-shared/gradlew.bat index ac1b06f93..107acd32c 100644 --- a/lib-jvm-shared/gradlew.bat +++ b/lib-jvm-shared/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/test2code/src/main/kotlin/com/epam/drill/agent/test2code/Test2Code.kt b/test2code/src/main/kotlin/com/epam/drill/agent/test2code/Test2Code.kt index 73cd7aa75..4a9a9f621 100644 --- a/test2code/src/main/kotlin/com/epam/drill/agent/test2code/Test2Code.kt +++ b/test2code/src/main/kotlin/com/epam/drill/agent/test2code/Test2Code.kt @@ -37,6 +37,7 @@ import com.epam.drill.agent.test2code.classloading.ClassLoadersScanner import com.epam.drill.agent.test2code.classloading.ClassScanner import com.epam.drill.agent.test2code.classparsing.parseAstClass import com.epam.drill.agent.test2code.configuration.Test2CodeParameterDefinitions +import com.epam.drill.agent.common.lifecycle.AgentShutdownRegistry import com.epam.drill.agent.test2code.coverage.* private const val DRILL_TEST_ID_HEADER = "drill-test-id" @@ -92,7 +93,9 @@ class Test2Code( } if (coverageCollectionEnabled) { coverageSender.startSendingCoverage() - Runtime.getRuntime().addShutdownHook(Thread { coverageSender.stopSendingCoverage() }) + AgentShutdownRegistry.register("coverage-sender") { remainingMs -> + coverageSender.stopSendingCoverage(remainingMs) + } } else { logger.info { "Coverage collection is disabled" } } diff --git a/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/CoverageManager.kt b/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/CoverageManager.kt index e6d319e8d..f3e0caaeb 100644 --- a/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/CoverageManager.kt +++ b/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/CoverageManager.kt @@ -54,6 +54,10 @@ open class CoverageManager( return threadCoverageRecorder.pollRecorded() + globalCoverageRecorder.pollRecorded() } + override fun getUnreleased(): Sequence { + return threadCoverageRecorder.getUnreleased() + globalCoverageRecorder.getUnreleased() + } + } /** diff --git a/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/CoverageSender.kt b/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/CoverageSender.kt index 1fcd57077..b12095d6f 100644 --- a/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/CoverageSender.kt +++ b/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/CoverageSender.kt @@ -28,7 +28,7 @@ import java.util.concurrent.ConcurrentHashMap interface CoverageSender { fun startSendingCoverage() - fun stopSendingCoverage() + fun stopSendingCoverage(remainingMs: Long) } class IntervalCoverageSender( @@ -44,13 +44,21 @@ class IntervalCoverageSender( private val collectUnreleasedProbes: () -> Sequence = { emptySequence() }, private val classMethodsMetadata: ConcurrentHashMap ) : CoverageSender { - private val scheduledThreadPool = Executors.newSingleThreadScheduledExecutor() + private val scheduledThreadPool = Executors.newSingleThreadScheduledExecutor { runnable -> + Thread(runnable, "drill-coverage-sender").apply { isDaemon = true } + } private val destination = AgentMessageDestination("POST", "coverage") private val logger = KotlinLogging.logger {} override fun startSendingCoverage() { scheduledThreadPool.scheduleAtFixedRate( - Runnable { sendProbes(collectReleasedProbes()) }, + { + try { + sendProbes(collectReleasedProbes()) + } catch (t: Throwable) { + logger.error(t) { "Coverage sending job failed" } + } + }, 0, intervalMs, TimeUnit.MILLISECONDS @@ -58,14 +66,12 @@ class IntervalCoverageSender( logger.info { "Coverage sending job is started." } } - override fun stopSendingCoverage() { + override fun stopSendingCoverage(remainingMs: Long) { sendProbes(collectReleasedProbes()) sendProbes(collectUnreleasedProbes()) - sender.shutdown() scheduledThreadPool.shutdown() - if (!scheduledThreadPool.awaitTermination(1, TimeUnit.SECONDS)) { - logger.error("Failed to send some coverage data prior to shutdown") - scheduledThreadPool.shutdownNow(); + if (remainingMs > 0 && !scheduledThreadPool.awaitTermination(remainingMs, TimeUnit.MILLISECONDS)) { + logger.warn { "Coverage sending scheduler did not stop within ${remainingMs}ms; leaving it for JVM exit." } } logger.info { "Coverage sending job is stopped." } } diff --git a/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/GlobalCoverageRecorder.kt b/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/GlobalCoverageRecorder.kt index 123d1c018..a02697a80 100644 --- a/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/GlobalCoverageRecorder.kt +++ b/test2code/src/main/kotlin/com/epam/drill/agent/test2code/coverage/GlobalCoverageRecorder.kt @@ -50,4 +50,7 @@ class GlobalCoverageRecorder: ICoverageRecorder { } else emptySequence() } + + override fun getUnreleased(): Sequence = + globalExecData.values.asSequence().filter { it.probes.containCovered() } } From b44a935f4f63d5ee36a7935b3890fbb3aa83a9cc Mon Sep 17 00:00:00 2001 From: RomanDavlyatshin Date: Tue, 2 Jun 2026 21:22:40 +0400 Subject: [PATCH 3/4] fix: remove lib-jvm-shared from gitignore and add changes for previous commit --- .gitignore | 1 - .../transport/DrillDaemonThreadFactory.kt | 22 ++++++++++++ .../common/lifecycle/AgentShutdownRegistry.kt | 36 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/DrillDaemonThreadFactory.kt create mode 100644 lib-jvm-shared/common/src/commonMain/kotlin/com/epam/drill/agent/common/lifecycle/AgentShutdownRegistry.kt diff --git a/.gitignore b/.gitignore index 359547de6..04f872a31 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ build/ **/kotlin/kni/ kni-meta-info **/commonGenerated/ -/lib-jvm-shared/ diff --git a/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/DrillDaemonThreadFactory.kt b/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/DrillDaemonThreadFactory.kt new file mode 100644 index 000000000..50010d250 --- /dev/null +++ b/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/DrillDaemonThreadFactory.kt @@ -0,0 +1,22 @@ +/** + * Copyright 2020 - 2022 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.epam.drill.agent.transport + +import java.util.concurrent.ThreadFactory + +internal fun drillDaemonThreadFactory(threadName: String): ThreadFactory = ThreadFactory { runnable -> + Thread(runnable, threadName).apply { isDaemon = true } +} diff --git a/lib-jvm-shared/common/src/commonMain/kotlin/com/epam/drill/agent/common/lifecycle/AgentShutdownRegistry.kt b/lib-jvm-shared/common/src/commonMain/kotlin/com/epam/drill/agent/common/lifecycle/AgentShutdownRegistry.kt new file mode 100644 index 000000000..7fdea54ee --- /dev/null +++ b/lib-jvm-shared/common/src/commonMain/kotlin/com/epam/drill/agent/common/lifecycle/AgentShutdownRegistry.kt @@ -0,0 +1,36 @@ +/** + * Copyright 2020 - 2022 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.epam.drill.agent.common.lifecycle + +/** + * Registry for JVM shutdown tasks. Tasks run before the message transport is flushed. + * [remainingMs] is the time left within the global [shutdownFlushTimeoutMs] budget. + */ +object AgentShutdownRegistry { + private val tasks = linkedMapOf Unit>() + + fun register(name: String, action: (remainingMs: Long) -> Unit) { + tasks[name] = action + } + + fun tasks(): List = + tasks.map { (name, action) -> NamedShutdownTask(name, action) } +} + +data class NamedShutdownTask( + val name: String, + val action: (remainingMs: Long) -> Unit, +) From 0a1f9ad083df866ec533ecdd650de719fe9788c2 Mon Sep 17 00:00:00 2001 From: RomanDavlyatshin Date: Tue, 2 Jun 2026 21:24:55 +0400 Subject: [PATCH 4/4] fix: remove drill daemon thread factory --- .../transport/DrillDaemonThreadFactory.kt | 22 ------------------- .../transport/QueuedAgentMessageSender.kt | 7 +++--- 2 files changed, 3 insertions(+), 26 deletions(-) delete mode 100644 lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/DrillDaemonThreadFactory.kt diff --git a/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/DrillDaemonThreadFactory.kt b/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/DrillDaemonThreadFactory.kt deleted file mode 100644 index 50010d250..000000000 --- a/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/DrillDaemonThreadFactory.kt +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright 2020 - 2022 EPAM Systems - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.epam.drill.agent.transport - -import java.util.concurrent.ThreadFactory - -internal fun drillDaemonThreadFactory(threadName: String): ThreadFactory = ThreadFactory { runnable -> - Thread(runnable, threadName).apply { isDaemon = true } -} diff --git a/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/QueuedAgentMessageSender.kt b/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/QueuedAgentMessageSender.kt index 8393def72..a1ec948f1 100644 --- a/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/QueuedAgentMessageSender.kt +++ b/lib-jvm-shared/agent-transport/src/jvmMain/kotlin/com/epam/drill/agent/transport/QueuedAgentMessageSender.kt @@ -42,10 +42,9 @@ open class QueuedAgentMessageSender( private val maxRetries: Int = 5 ) : AgentMessageSender { private val logger = KotlinLogging.logger {} - private val executor: ExecutorService = Executors.newFixedThreadPool( - maxThreads, - drillDaemonThreadFactory("drill-message-sender"), - ) + private val executor: ExecutorService = Executors.newFixedThreadPool(maxThreads) { runnable -> + Thread(runnable, "drill-message-sender").apply { isDaemon = true } + } private val isRunning = AtomicBoolean(true)