From 62c924c4ab2f84ab08f257e72789701afcd81925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Correia?= Date: Tue, 23 Sep 2025 08:38:01 +0100 Subject: [PATCH] Add liveness and readiness probes --- .../resources/static-content/openapi.yml | 35 +++++++++++++++++++ src/jvmMain/kotlin/Module.kt | 2 ++ .../kotlin/controller/health/HealthModule.kt | 25 +++++++++++++ .../ValidationServiceFactoryImpl.kt | 23 ++++++------ .../validation/ValidationServiceStatus.kt | 14 ++++++++ 5 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 src/jvmMain/kotlin/controller/health/HealthModule.kt create mode 100644 src/jvmMain/kotlin/controller/validation/ValidationServiceStatus.kt diff --git a/src/commonMain/resources/static-content/openapi.yml b/src/commonMain/resources/static-content/openapi.yml index c6f993c..347b121 100644 --- a/src/commonMain/resources/static-content/openapi.yml +++ b/src/commonMain/resources/static-content/openapi.yml @@ -184,6 +184,41 @@ paths: application/json: schema: $ref: "#/components/schemas/packageServerStatus" + /health/ready: + get: + tags: + - Health + description: "Readiness probe. Returns 200 OK if all presets have been loaded and the validation service is ready, otherwise returns 503." + operationId: healthReady + responses: + "200": + description: OK + content: + text/plain: + schema: + type: string + example: OK + "503": + description: Service Unavailable + content: + text/plain: + schema: + type: string + example: ValidationService not ready + /health/live: + get: + tags: + - Health + description: "Liveness probe. Always returns OK if the service is running." + operationId: healthLive + responses: + "200": + description: OK + content: + text/plain: + schema: + type: string + example: OK components: diff --git a/src/jvmMain/kotlin/Module.kt b/src/jvmMain/kotlin/Module.kt index 0ed4a07..32d8216 100644 --- a/src/jvmMain/kotlin/Module.kt +++ b/src/jvmMain/kotlin/Module.kt @@ -1,5 +1,6 @@ import io.ktor.serialization.gson.* import controller.debug.debugModule +import controller.health.healthModule import controller.ig.igModule import controller.terminology.terminologyModule import controller.uptime.uptimeModule @@ -96,6 +97,7 @@ fun Application.setup() { validationModule() terminologyModule() uptimeModule() + healthModule() get("/") { call.respondText( diff --git a/src/jvmMain/kotlin/controller/health/HealthModule.kt b/src/jvmMain/kotlin/controller/health/HealthModule.kt new file mode 100644 index 0000000..33829c8 --- /dev/null +++ b/src/jvmMain/kotlin/controller/health/HealthModule.kt @@ -0,0 +1,25 @@ +package controller.health + +import controller.validation.ValidationServiceStatus +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.http.* + +fun Route.healthModule() { + route("/health/ready") { + get { + if (ValidationServiceStatus.isReady()) { + call.respond(HttpStatusCode.OK, "OK") + } else { + call.respond(HttpStatusCode.ServiceUnavailable, "ValidationService not ready") + } + } + } + + route("/health/live") { + get { + call.respond(HttpStatusCode.OK, "OK") + } + } +} diff --git a/src/jvmMain/kotlin/controller/validation/ValidationServiceFactoryImpl.kt b/src/jvmMain/kotlin/controller/validation/ValidationServiceFactoryImpl.kt index 8bad155..e451c1f 100644 --- a/src/jvmMain/kotlin/controller/validation/ValidationServiceFactoryImpl.kt +++ b/src/jvmMain/kotlin/controller/validation/ValidationServiceFactoryImpl.kt @@ -30,18 +30,19 @@ class ValidationServiceFactoryImpl : ValidationServiceFactory { val validationService = ValidationService(sessionCache); thread { - presets.forEach { - if (it.key != "CUSTOM") { - println("Loading preset: " + it.key) - try { - validationService.putBaseEngine(it.key, it.validationContext) - } catch (e: Exception) { - println("Error loading preset: " + it.key) - e.printStackTrace() + presets.forEach { + if (it.key != "CUSTOM") { + println("Loading preset: " + it.key) + try { + validationService.putBaseEngine(it.key, it.validationContext) + } catch (e: Exception) { + println("Error loading preset: " + it.key) + e.printStackTrace() + } + println("Preset loaded: " + it.key); } - println("Preset loaded: " + it.key); } - } + ValidationServiceStatus.setService(validationService) } return validationService } @@ -71,7 +72,7 @@ class ValidationServiceFactoryImpl : ValidationServiceFactory { return Collections.unmodifiableList(presets) } - override fun getValidationService() : ValidationService { + override fun getValidationService(): ValidationService { if (java.lang.Runtime.getRuntime().freeMemory() < validationServiceConfig.engineReloadThreshold) { println( "Free memory ${ diff --git a/src/jvmMain/kotlin/controller/validation/ValidationServiceStatus.kt b/src/jvmMain/kotlin/controller/validation/ValidationServiceStatus.kt new file mode 100644 index 0000000..d17bb53 --- /dev/null +++ b/src/jvmMain/kotlin/controller/validation/ValidationServiceStatus.kt @@ -0,0 +1,14 @@ +package controller.validation + +import org.hl7.fhir.validation.service.ValidationService +import java.util.concurrent.atomic.AtomicReference + +object ValidationServiceStatus { + private val serviceRef = AtomicReference(null) + + fun setService(service: ValidationService) { + serviceRef.set(service) + } + + fun isReady(): Boolean = serviceRef.get() != null +}