From 81e7030cccd520fc9a33524564851345b6f05d02 Mon Sep 17 00:00:00 2001 From: iryabov Date: Tue, 29 Sep 2026 16:17:55 +0200 Subject: [PATCH 1/3] feat: add check for requiring test project id parameter --- .../epam/drill/agent/configuration/ParameterDefinitions.kt | 5 ++++- .../com/epam/drill/agent/test/session/SessionController.kt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) 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 0e35df84..25736c08 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 @@ -17,10 +17,12 @@ package com.epam.drill.agent.configuration import com.epam.drill.agent.common.configuration.AgentParameterDefinition import com.epam.drill.agent.common.configuration.AgentParameterDefinitionCollection +import com.epam.drill.agent.common.configuration.AgentParameters import com.epam.drill.agent.common.configuration.NullableAgentParameterDefinition import com.epam.drill.agent.konform.validation.jsonschema.minLength object ParameterDefinitions: AgentParameterDefinitionCollection() { + val testTracingEnabled: (AgentParameters) -> Boolean = { config -> config[CapabilityParameterDefinitions.TEST_TRACING_ENABLED] } val API_URL = AgentParameterDefinition.forString( name = "apiUrl", @@ -65,9 +67,10 @@ object ParameterDefinitions: AgentParameterDefinitionCollection() { val JS_AGENT_BUILD_VERSION = NullableAgentParameterDefinition.forString(name = "jsAgentBuildVersion").register() val JS_AGENT_ID = NullableAgentParameterDefinition.forString(name = "jsAgentId").register() - val TEST_PROJECT_ID = AgentParameterDefinition.forString( + val TEST_PROJECT_ID = NullableAgentParameterDefinition.forString( name = "testProjectId", description = "Unique arbitrary string identifying your test project. Example: my-test-project", + requiredIf = testTracingEnabled, validator = { identifier() minLength(3) 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 73021d81..070e49fc 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 @@ -85,7 +85,7 @@ actual object SessionController { SessionPayload( id = sessionId, groupId = Configuration.parameters[DefaultParameterDefinitions.GROUP_ID], - testProjectId = Configuration.parameters[ParameterDefinitions.TEST_PROJECT_ID], + testProjectId = Configuration.parameters[ParameterDefinitions.TEST_PROJECT_ID] ?: error("Test project ID is not set"), testTaskId = Configuration.parameters[ParameterDefinitions.TEST_TASK_ID], startedAt = System.currentTimeMillis().toIsoTimeFormat(), builds = builds From f48f97265448aac09eead8aad69bb7a90297b050 Mon Sep 17 00:00:00 2001 From: iryabov Date: Tue, 29 Sep 2026 17:19:54 +0200 Subject: [PATCH 2/3] feat: add test session heartbeat sending --- .../agent/test/session/SessionController.kt | 26 +++++- .../transport/TestSessionHeartbeatPayload.kt | 33 +++++++ .../transport/TestSessionHeartbeatSender.kt | 91 +++++++++++++++++++ .../IntervalTestSessionHeartbeatSenderTest.kt | 77 ++++++++++++++++ 4 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/TestSessionHeartbeatPayload.kt create mode 100644 java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/TestSessionHeartbeatSender.kt create mode 100644 java-agent/src/jvmTest/kotlin/com/epam/drill/agent/transport/IntervalTestSessionHeartbeatSenderTest.kt 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 070e49fc..03889e06 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 @@ -32,10 +32,9 @@ 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.test.execution.TestMethodInfo -import com.epam.drill.agent.test.sending.AddTestDefinitionsPayload -import com.epam.drill.agent.test.sending.AddTestLaunchesPayload -import com.epam.drill.agent.test.session.SessionController.getSessionId import com.epam.drill.agent.transport.DataIngestMessageSender +import com.epam.drill.agent.transport.IntervalTestSessionHeartbeatSender +import com.epam.drill.agent.transport.directMessageSender import mu.KotlinLogging import java.time.Instant import java.time.ZoneId @@ -73,6 +72,8 @@ actual object SessionController { testInfoSender.stopSendingTests(remainingMs) } + startHeartbeatReporting() + val builds = takeIf { Configuration.parameters[ParameterDefinitions.RECOMMENDED_TESTS_TARGET_APP_ID].isNotEmpty() }?.let { SingleSessionBuildPayload( @@ -85,7 +86,8 @@ actual object SessionController { SessionPayload( id = sessionId, groupId = Configuration.parameters[DefaultParameterDefinitions.GROUP_ID], - testProjectId = Configuration.parameters[ParameterDefinitions.TEST_PROJECT_ID] ?: error("Test project ID is not set"), + testProjectId = Configuration.parameters[ParameterDefinitions.TEST_PROJECT_ID] + ?: error("Test project ID is not set"), testTaskId = Configuration.parameters[ParameterDefinitions.TEST_TASK_ID], startedAt = System.currentTimeMillis().toIsoTimeFormat(), builds = builds @@ -98,6 +100,22 @@ actual object SessionController { private fun isTestTracingEnabled(): Boolean = Configuration.parameters[TEST_TRACING_ENABLED] private fun isTestLaunchMetadataSendingEnabled(): Boolean = isTestTracingEnabled() && Configuration.parameters[ParameterDefinitions.TEST_TRACING_PER_TEST_LAUNCH_ENABLED] + + private fun startHeartbeatReporting() { + if (!Configuration.parameters[ParameterDefinitions.HEARTBEAT_ENABLED]) return + val heartbeatSender = IntervalTestSessionHeartbeatSender( + sender = directMessageSender(), + intervalMs = Configuration.parameters[ParameterDefinitions.HEARTBEAT_INTERVAL], + groupId = Configuration.parameters[DefaultParameterDefinitions.GROUP_ID], + testProjectId = Configuration.parameters[ParameterDefinitions.TEST_PROJECT_ID] + ?: error("Test project ID is not set"), + testSessionId = sessionId + ) + heartbeatSender.startSendingHeartbeat() + AgentShutdownRegistry.register("session-heartbeat-sender") { remainingMs -> + heartbeatSender.stopSendingHeartbeat(remainingMs) + } + } } private fun List.toTestLaunchPayloads(): List = map { info -> diff --git a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/TestSessionHeartbeatPayload.kt b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/TestSessionHeartbeatPayload.kt new file mode 100644 index 00000000..5723560c --- /dev/null +++ b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/TestSessionHeartbeatPayload.kt @@ -0,0 +1,33 @@ +/** + * 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 com.epam.drill.agent.common.transport.AgentMessage +import kotlinx.serialization.Serializable + +@Serializable +class TestSessionHeartbeatPayload( + val groupId: String, + val testProjectId: String, + val testSessionId: String, + val status: TestSessionStatus, +) : AgentMessage() + +@Serializable +enum class TestSessionStatus { + RUNNING, + FINISHED +} diff --git a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/TestSessionHeartbeatSender.kt b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/TestSessionHeartbeatSender.kt new file mode 100644 index 00000000..8edcb8a0 --- /dev/null +++ b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/TestSessionHeartbeatSender.kt @@ -0,0 +1,91 @@ +/** + * 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 com.epam.drill.agent.common.transport.AgentMessageDestination +import com.epam.drill.agent.common.transport.AgentMessageSender +import mu.KotlinLogging +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +interface TestSessionHeartbeatSender { + fun startSendingHeartbeat() + fun stopSendingHeartbeat(remainingMs: Long) +} + +/** + * Periodically reports the test session heartbeat status to the Backend. + * + * While the session is active it sends [TestSessionStatus.RUNNING] on a fixed + * interval. On graceful shutdown it stops the scheduler and sends a final + * [TestSessionStatus.FINISHED] status. All requests are sent synchronously + * through the provided [sender] (a DIRECT sender, never the queued pipeline). + */ +class IntervalTestSessionHeartbeatSender( + private val sender: AgentMessageSender, + private val intervalMs: Long, + private val groupId: String, + private val testProjectId: String, + private val testSessionId: String, +) : TestSessionHeartbeatSender { + private val logger = KotlinLogging.logger {} + private val destination = AgentMessageDestination("PUT", "sessions/heartbeat") + private val scheduledThreadPool = Executors.newSingleThreadScheduledExecutor { runnable -> + Thread(runnable, "drill-session-heartbeat-sender").apply { isDaemon = true } + } + + override fun startSendingHeartbeat() { + scheduledThreadPool.scheduleAtFixedRate( + { + try { + sendStatus(TestSessionStatus.RUNNING) + } catch (t: Throwable) { + logger.error(t) { "Test session heartbeat status sending job failed" } + } + }, + intervalMs, + intervalMs, + TimeUnit.MILLISECONDS + ) + logger.info { "Test session heartbeat status sending job is started." } + } + + override fun stopSendingHeartbeat(remainingMs: Long) { + scheduledThreadPool.shutdown() + if (remainingMs > 0 && !scheduledThreadPool.awaitTermination(remainingMs, TimeUnit.MILLISECONDS)) { + logger.warn { "Test session heartbeat sending scheduler did not stop within ${remainingMs}ms; leaving it for JVM exit." } + } + try { + sendStatus(TestSessionStatus.FINISHED) + } catch (t: Throwable) { + logger.error(t) { "Failed to send SHUTDOWN test session heartbeat status" } + } + logger.info { "Test session heartbeat status sending job is stopped." } + } + + private fun sendStatus(status: TestSessionStatus) { + sender.send( + destination, + TestSessionHeartbeatPayload( + groupId = groupId, + testProjectId = testProjectId, + testSessionId = testSessionId, + status = status + ), + TestSessionHeartbeatPayload.serializer() + ) + } +} diff --git a/java-agent/src/jvmTest/kotlin/com/epam/drill/agent/transport/IntervalTestSessionHeartbeatSenderTest.kt b/java-agent/src/jvmTest/kotlin/com/epam/drill/agent/transport/IntervalTestSessionHeartbeatSenderTest.kt new file mode 100644 index 00000000..244f965f --- /dev/null +++ b/java-agent/src/jvmTest/kotlin/com/epam/drill/agent/transport/IntervalTestSessionHeartbeatSenderTest.kt @@ -0,0 +1,77 @@ +/** + * 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.Collections +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import com.epam.drill.agent.common.transport.AgentMessageDestination +import com.epam.drill.agent.common.transport.AgentMessageSender +import kotlinx.serialization.KSerializer + +class IntervalTestSessionHeartbeatSenderTest { + + private class RecordingSender : AgentMessageSender { + val sent = Collections.synchronizedList(mutableListOf>()) + override fun send(destination: AgentMessageDestination, message: T, serializer: KSerializer) { + sent.add(destination to message) + } + } + + private fun newSender(recorder: RecordingSender, intervalMs: Long = 50L) = IntervalTestSessionHeartbeatSender( + sender = recorder, + intervalMs = intervalMs, + groupId = "someGroupId", + testProjectId = "someTestProjectId", + testSessionId = "someTestSessionId" + ) + + @Test + fun `should periodically send RUNNING status to sessions heartbeat`() { + val recorder = RecordingSender() + val sender = newSender(recorder, intervalMs = 50L) + + sender.startSendingHeartbeat() + Thread.sleep(200) + sender.stopSendingHeartbeat(1000) + + val running = recorder.sent.map { it.second }.filterIsInstance() + .filter { it.status == TestSessionStatus.RUNNING } + assertTrue(running.size >= 2, "Expected multiple RUNNING statuses, got ${running.size}") + + val destination = recorder.sent.first().first + assertEquals("PUT", destination.type) + assertEquals("sessions/heartbeat", destination.target) + + val payload = running.first() + assertEquals("someGroupId", payload.groupId) + assertEquals("someTestProjectId", payload.testProjectId) + assertEquals("someTestSessionId", payload.testSessionId) + } + + @Test + fun `should send SHUTDOWN status on stop`() { + val recorder = RecordingSender() + val sender = newSender(recorder, intervalMs = 10_000L) + + sender.startSendingHeartbeat() + sender.stopSendingHeartbeat(1000) + + val statuses = recorder.sent.map { it.second }.filterIsInstance().map { it.status } + assertEquals(TestSessionStatus.FINISHED, statuses.last()) + } +} From b5186310aa918afb6235cf16f9c64b047e8fd4e2 Mon Sep 17 00:00:00 2001 From: iryabov Date: Fri, 30 Oct 2026 09:38:26 +0100 Subject: [PATCH 3/3] feat: add commitSha and buildVersion to AgentHeartbeatPayload and related classes --- .../com/epam/drill/agent/transport/AgentHeartbeatPayload.kt | 2 ++ .../com/epam/drill/agent/transport/AgentHeartbeatSender.kt | 4 ++++ .../com/epam/drill/agent/transport/JvmModuleMessageSender.kt | 4 +++- .../drill/agent/transport/IntervalAgentHeartbeatSenderTest.kt | 4 +++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/AgentHeartbeatPayload.kt b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/AgentHeartbeatPayload.kt index 5e64b935..20b19989 100644 --- a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/AgentHeartbeatPayload.kt +++ b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/AgentHeartbeatPayload.kt @@ -23,6 +23,8 @@ class AgentHeartbeatPayload( val groupId: String, val appId: String, val instanceId: String, + val commitSha: String?, + val buildVersion: String?, val status: AgentHeartbeatStatus, ) : AgentMessage() diff --git a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/AgentHeartbeatSender.kt b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/AgentHeartbeatSender.kt index c202112c..68e4615d 100644 --- a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/AgentHeartbeatSender.kt +++ b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/AgentHeartbeatSender.kt @@ -40,6 +40,8 @@ class IntervalAgentHeartbeatSender( private val groupId: String, private val appId: String, private val instanceId: String, + private val commitSha: String?, + private val buildVersion: String?, ) : AgentHeartbeatSender { private val logger = KotlinLogging.logger {} private val destination = AgentMessageDestination("PUT", "instances/heartbeat") @@ -83,6 +85,8 @@ class IntervalAgentHeartbeatSender( groupId = groupId, appId = appId, instanceId = instanceId, + commitSha = commitSha, + buildVersion = buildVersion, status = status ), AgentHeartbeatPayload.serializer() diff --git a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/JvmModuleMessageSender.kt b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/JvmModuleMessageSender.kt index 17a5d4be..c604230c 100644 --- a/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/JvmModuleMessageSender.kt +++ b/java-agent/src/jvmMain/kotlin/com/epam/drill/agent/transport/JvmModuleMessageSender.kt @@ -38,7 +38,9 @@ actual object JvmModuleMessageSender : AgentMessageSender by DataIngestMessageSe intervalMs = Configuration.parameters[ParameterDefinitions.HEARTBEAT_INTERVAL], groupId = Configuration.agentMetadata.groupId, appId = Configuration.agentMetadata.appId, - instanceId = Configuration.agentMetadata.instanceId + instanceId = Configuration.agentMetadata.instanceId, + commitSha = Configuration.agentMetadata.commitSha, + buildVersion = Configuration.agentMetadata.buildVersion ) } diff --git a/java-agent/src/jvmTest/kotlin/com/epam/drill/agent/transport/IntervalAgentHeartbeatSenderTest.kt b/java-agent/src/jvmTest/kotlin/com/epam/drill/agent/transport/IntervalAgentHeartbeatSenderTest.kt index 575cbe89..a3ee8982 100644 --- a/java-agent/src/jvmTest/kotlin/com/epam/drill/agent/transport/IntervalAgentHeartbeatSenderTest.kt +++ b/java-agent/src/jvmTest/kotlin/com/epam/drill/agent/transport/IntervalAgentHeartbeatSenderTest.kt @@ -37,7 +37,9 @@ class IntervalAgentHeartbeatSenderTest { intervalMs = intervalMs, groupId = "someGroupId", appId = "someAppId", - instanceId = "someInstanceId" + instanceId = "someInstanceId", + commitSha = null, + buildVersion = null ) @Test