From 407ee76eb52633bbee99f4ca5f29c4f41245e805 Mon Sep 17 00:00:00 2001 From: alfredo-mfaria Date: Tue, 20 Jun 2023 16:15:11 +0200 Subject: [PATCH] CID:1287 Adds heartbeat and graceful shutdown --- CODEOWNERS | 2 +- build.gradle.kts | 10 ++-- .../adapter/feign/FeignRunStatusProvider.kt | 13 ++++ .../connector/adapter/feign/VsmClient.kt | 11 ++++ .../feign/data/UpdateRunStateRequest.kt | 23 +++++++ .../connector/domain/RunStatusProvider.kt | 7 +++ .../connector/runner/InitialStateRunner.kt | 5 +- .../connector/runner/ShutdownService.kt | 36 +++++++++++ .../connector/scheduler/HeartbeatScheduler.kt | 17 ++++++ .../scheduler/ScheduleRepositories.kt | 5 +- .../shared/cache/AssignmentCache.kt | 24 ++++++++ .../connector/runner/ShutdownServiceTest.kt | 60 +++++++++++++++++++ 12 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/FeignRunStatusProvider.kt create mode 100644 src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/data/UpdateRunStateRequest.kt create mode 100644 src/main/kotlin/net/leanix/vsm/githubbroker/connector/domain/RunStatusProvider.kt create mode 100644 src/main/kotlin/net/leanix/vsm/githubbroker/connector/runner/ShutdownService.kt create mode 100644 src/main/kotlin/net/leanix/vsm/githubbroker/connector/scheduler/HeartbeatScheduler.kt create mode 100644 src/main/kotlin/net/leanix/vsm/githubbroker/shared/cache/AssignmentCache.kt create mode 100644 src/test/kotlin/net/leanix/vsm/githubbroker/connector/runner/ShutdownServiceTest.kt diff --git a/CODEOWNERS b/CODEOWNERS index 6e19cf7..434ebeb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,4 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, # they will be requested for review when someone opens a pull request. -* @alfredo-mfaria @aravindmetku @geoandri @henriqamaral @mohamedlajmileanix \ No newline at end of file +* @leanix/team-cider \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 9edcc1a..3c4cc8c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,13 +1,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id("org.springframework.boot") version "3.0.5" + id("org.springframework.boot") version "3.1.0" id("io.spring.dependency-management") version "1.1.0" - id("io.gitlab.arturbosch.detekt") version "1.21.0" + id("io.gitlab.arturbosch.detekt") version "1.23.0" id("com.expediagroup.graphql") version "6.3.1" id("org.cyclonedx.bom") version "1.7.2" - kotlin("jvm") version "1.6.21" - kotlin("plugin.spring") version "1.6.21" + kotlin("jvm") version "1.8.21" + kotlin("plugin.spring") version "1.8.21" jacoco } @@ -45,7 +45,7 @@ dependencies { dependencyManagement { imports { - mavenBom("org.springframework.cloud:spring-cloud-dependencies:2022.0.1") + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2022.0.3") } dependencies { dependency("com.google.guava:guava:30.0-jre") diff --git a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/FeignRunStatusProvider.kt b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/FeignRunStatusProvider.kt new file mode 100644 index 0000000..5891f9a --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/FeignRunStatusProvider.kt @@ -0,0 +1,13 @@ +package net.leanix.vsm.githubbroker.connector.adapter.feign + +import net.leanix.vsm.githubbroker.connector.adapter.feign.data.UpdateRunStateRequest +import net.leanix.vsm.githubbroker.connector.domain.RunStatusProvider +import org.springframework.stereotype.Component + +@Component +class FeignRunStatusProvider(private val vsmClient: VsmClient) : RunStatusProvider { + + override fun updateRunStatus(runId: String, runState: UpdateRunStateRequest) { + vsmClient.updateRunState(runId, runState) + } +} diff --git a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/VsmClient.kt b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/VsmClient.kt index 4f45576..006519d 100644 --- a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/VsmClient.kt +++ b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/VsmClient.kt @@ -6,11 +6,13 @@ import net.leanix.vsm.githubbroker.connector.adapter.feign.data.DoraRequest import net.leanix.vsm.githubbroker.connector.adapter.feign.data.LanguageRequest import net.leanix.vsm.githubbroker.connector.adapter.feign.data.ServiceRequest import net.leanix.vsm.githubbroker.connector.adapter.feign.data.TopicRequest +import net.leanix.vsm.githubbroker.connector.adapter.feign.data.UpdateRunStateRequest import net.leanix.vsm.githubbroker.shared.Constants.EVENT_TYPE_HEADER import net.leanix.vsm.githubbroker.shared.auth.adapter.feign.config.MtmFeignClientConfiguration import org.springframework.cloud.openfeign.FeignClient import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestParam @@ -47,4 +49,13 @@ interface VsmClient { @RequestParam("integrationName") integrationName: String, @RequestParam("configSetName") configSetName: String ): AssignmentResponse + + @GetMapping("/health/heartbeat") + fun heartbeat(@RequestParam("runId") runId: String): String + + @PutMapping("/run/status") + fun updateRunState( + @RequestParam("runId") runId: String, + @RequestBody runState: UpdateRunStateRequest, + ) } diff --git a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/data/UpdateRunStateRequest.kt b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/data/UpdateRunStateRequest.kt new file mode 100644 index 0000000..66c94b0 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/adapter/feign/data/UpdateRunStateRequest.kt @@ -0,0 +1,23 @@ +package net.leanix.vsm.githubbroker.connector.adapter.feign.data + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class UpdateRunStateRequest( + val state: RunState, + val workspaceId: String? = null, + val connector: String? = null, + val orgName: String? = null, + val message: String? = null, + val region: String? = null +) + +enum class RunState { + QUEUED, + DELETED, + RUNNING, + FINISHED, + FAILED, + FINISHED_FOR_LIVE, + LIVE +} diff --git a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/domain/RunStatusProvider.kt b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/domain/RunStatusProvider.kt new file mode 100644 index 0000000..f2a3d77 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/domain/RunStatusProvider.kt @@ -0,0 +1,7 @@ +package net.leanix.vsm.githubbroker.connector.domain + +import net.leanix.vsm.githubbroker.connector.adapter.feign.data.UpdateRunStateRequest + +interface RunStatusProvider { + fun updateRunStatus(runId: String, runState: UpdateRunStateRequest) +} diff --git a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/runner/InitialStateRunner.kt b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/runner/InitialStateRunner.kt index 517f0b3..e952c91 100644 --- a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/runner/InitialStateRunner.kt +++ b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/runner/InitialStateRunner.kt @@ -9,6 +9,7 @@ import net.leanix.vsm.githubbroker.connector.domain.CommandProvider import net.leanix.vsm.githubbroker.logs.application.LoggingService import net.leanix.vsm.githubbroker.logs.domain.LogStatus import net.leanix.vsm.githubbroker.logs.domain.StatusLog +import net.leanix.vsm.githubbroker.shared.cache.AssignmentCache import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.boot.ApplicationArguments @@ -54,7 +55,9 @@ class InitialStateRunner( } private fun getAssignments(): List? { kotlin.runCatching { - return assignmentService.getAssignments() + val assignments = assignmentService.getAssignments() + AssignmentCache.addAll(assignments) + return assignments }.onFailure { logger.error("Failed to get initial state. No assignment found for this workspace id") } diff --git a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/runner/ShutdownService.kt b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/runner/ShutdownService.kt new file mode 100644 index 0000000..ec83711 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/runner/ShutdownService.kt @@ -0,0 +1,36 @@ +package net.leanix.vsm.githubbroker.connector.runner + +import jakarta.annotation.PreDestroy +import net.leanix.vsm.githubbroker.connector.adapter.feign.FeignRunStatusProvider +import net.leanix.vsm.githubbroker.connector.adapter.feign.data.RunState +import net.leanix.vsm.githubbroker.connector.adapter.feign.data.UpdateRunStateRequest +import net.leanix.vsm.githubbroker.shared.Constants.GITHUB_ENTERPRISE_CONNECTOR +import net.leanix.vsm.githubbroker.shared.cache.AssignmentCache +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class ShutdownService(private val runStatusProvider: FeignRunStatusProvider) { + + private val logger = LoggerFactory.getLogger(ShutdownService::class.java) + + @PreDestroy + fun onDestroy() { + if (AssignmentCache.getAll().isEmpty()) { + logger.info("Shutting down github broker before receiving any assignment") + } else { + AssignmentCache.getAll().values.forEach { assignment -> + runStatusProvider.updateRunStatus( + assignment.runId.toString(), + UpdateRunStateRequest( + state = RunState.FINISHED, + workspaceId = assignment.workspaceId.toString(), + connector = GITHUB_ENTERPRISE_CONNECTOR, + orgName = assignment.organizationName, + message = "Gracefully stopped github enterprise" + ) + ) + } + } + } +} diff --git a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/scheduler/HeartbeatScheduler.kt b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/scheduler/HeartbeatScheduler.kt new file mode 100644 index 0000000..b544952 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/scheduler/HeartbeatScheduler.kt @@ -0,0 +1,17 @@ +package net.leanix.vsm.githubbroker.connector.scheduler + +import net.leanix.vsm.githubbroker.connector.adapter.feign.VsmClient +import net.leanix.vsm.githubbroker.shared.cache.AssignmentCache +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class HeartbeatScheduler( + private val vsmClient: VsmClient +) { + + @Scheduled(fixedRate = 300000) // 5 minutes + fun heartbeat() { + AssignmentCache.getAll().values.forEach { assigment -> vsmClient.heartbeat(assigment.runId.toString()) } + } +} diff --git a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/scheduler/ScheduleRepositories.kt b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/scheduler/ScheduleRepositories.kt index d78a440..92a0f53 100644 --- a/src/main/kotlin/net/leanix/vsm/githubbroker/connector/scheduler/ScheduleRepositories.kt +++ b/src/main/kotlin/net/leanix/vsm/githubbroker/connector/scheduler/ScheduleRepositories.kt @@ -5,6 +5,7 @@ import net.leanix.vsm.githubbroker.connector.application.RepositoriesService import net.leanix.vsm.githubbroker.connector.domain.Assignment import net.leanix.vsm.githubbroker.connector.domain.CommandEventAction import net.leanix.vsm.githubbroker.connector.domain.CommandProvider +import net.leanix.vsm.githubbroker.shared.cache.AssignmentCache import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.scheduling.annotation.Scheduled @@ -34,7 +35,9 @@ class ScheduleRepositories( private fun getAssignments(): List? { return kotlin.runCatching { - return assignmentService.getAssignments() + val assignments = assignmentService.getAssignments() + AssignmentCache.addAll(assignments) + return assignments }.getOrElse { logger.error("Failed to get initial state. No assignment found for this workspace id") null diff --git a/src/main/kotlin/net/leanix/vsm/githubbroker/shared/cache/AssignmentCache.kt b/src/main/kotlin/net/leanix/vsm/githubbroker/shared/cache/AssignmentCache.kt new file mode 100644 index 0000000..8ac5cb3 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/githubbroker/shared/cache/AssignmentCache.kt @@ -0,0 +1,24 @@ +package net.leanix.vsm.githubbroker.shared.cache + +import net.leanix.vsm.githubbroker.connector.domain.Assignment + +object AssignmentCache { + + private val assigmentCache: MutableMap = mutableMapOf() + + fun addAll(newAssignments: List) { + newAssignments.forEach { assignment -> assigmentCache[assignment.organizationName] = assignment } + } + + fun get(key: String): Assignment? { + return assigmentCache[key] + } + + fun getAll(): Map { + return assigmentCache + } + + fun deleteAll() { + assigmentCache.clear() + } +} diff --git a/src/test/kotlin/net/leanix/vsm/githubbroker/connector/runner/ShutdownServiceTest.kt b/src/test/kotlin/net/leanix/vsm/githubbroker/connector/runner/ShutdownServiceTest.kt new file mode 100644 index 0000000..62cb760 --- /dev/null +++ b/src/test/kotlin/net/leanix/vsm/githubbroker/connector/runner/ShutdownServiceTest.kt @@ -0,0 +1,60 @@ +package net.leanix.vsm.githubbroker.connector.runner + +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import net.leanix.vsm.githubbroker.connector.adapter.feign.FeignRunStatusProvider +import net.leanix.vsm.githubbroker.connector.adapter.feign.data.RunState.FINISHED +import net.leanix.vsm.githubbroker.connector.adapter.feign.data.UpdateRunStateRequest +import net.leanix.vsm.githubbroker.connector.domain.Assignment +import net.leanix.vsm.githubbroker.shared.Constants.GITHUB_ENTERPRISE_CONNECTOR +import net.leanix.vsm.githubbroker.shared.cache.AssignmentCache +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.* + +class ShutdownServiceTest { + private val runStatusProvider: FeignRunStatusProvider = mockk(relaxed = true) + private val shutdownService = ShutdownService(runStatusProvider) + + @BeforeEach + fun setup() { + clearAllMocks() + AssignmentCache.deleteAll() + } + + @Test + fun `test onDestroy with empty assignment cache`() { + shutdownService.onDestroy() + + // Assert no interactions with the run status provider + verify(exactly = 0) { runStatusProvider.updateRunStatus(any(), any()) } + } + + @Test + fun `test onDestroy with assignments`() { + val assignment = Assignment(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), null, "mock-org") + AssignmentCache.addAll(listOf(assignment)) + + val runStateSlot = slot() + + every { runStatusProvider.updateRunStatus(any(), capture(runStateSlot)) } answers { } + + shutdownService.onDestroy() + + verify { + runStatusProvider.updateRunStatus( + eq(assignment.runId.toString()), + match { + it.state == FINISHED && + it.workspaceId == assignment.workspaceId.toString() && + it.connector == GITHUB_ENTERPRISE_CONNECTOR && + it.orgName == assignment.organizationName && + it.message == "Gracefully stopped github enterprise" + } + ) + } + } +}