Skip to content

Commit 19c55d8

Browse files
committed
refactor(model-server): implement health endpoint via base class
Moves the health endpoint over to the operative API (as it is mainly used for operations) and implements it using a generated base class instead of a manual declaration that can drift from the spec.
1 parent aefc479 commit 19c55d8

File tree

5 files changed

+143
-44
lines changed

5 files changed

+143
-44
lines changed

model-server-openapi/specifications/model-server-operative.yaml

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,81 @@ paths:
2424
operationId: getMetrics
2525
tags:
2626
- metrics
27+
responses:
28+
"200":
29+
$ref: '#/components/responses/MetricsResponse'
30+
31+
/health:
32+
get:
33+
operationId: getHealth
34+
tags:
35+
- health
2736
responses:
2837
"200":
2938
$ref: '#/components/responses/200'
39+
default:
40+
$ref: '#/components/responses/GeneralError'
3041

3142
components:
3243
responses:
33-
"200":
44+
MetricsResponse:
3445
description: OK
3546
content:
3647
text/plain:
3748
schema:
3849
type: string
50+
51+
GeneralError:
52+
description: Unexpected error
53+
content:
54+
application/problem+json:
55+
schema:
56+
$ref: '#/components/schemas/Problem'
57+
58+
schemas:
59+
# From https://opensource.zalando.com/restful-api-guidelines/models/problem-1.0.1.yaml
60+
Problem:
61+
type: object
62+
properties:
63+
type:
64+
type: string
65+
format: uri-reference
66+
description: >
67+
A URI reference that uniquely identifies the problem type only in the
68+
context of the provided API. Opposed to the specification in RFC-9457,
69+
it is neither recommended to be dereferenceable and point to a
70+
human-readable documentation nor globally unique for the problem type.
71+
default: 'about:blank'
72+
example: '/some/uri-reference'
73+
title:
74+
type: string
75+
description: >
76+
A short summary of the problem type. Written in English and readable
77+
for engineers, usually not suited for non technical stakeholders and
78+
not localized.
79+
example: some title for the error situation
80+
status:
81+
type: integer
82+
format: int32
83+
description: >
84+
The HTTP status code generated by the origin server for this occurrence
85+
of the problem.
86+
minimum: 100
87+
maximum: 600
88+
exclusiveMaximum: true
89+
detail:
90+
type: string
91+
description: >
92+
A human readable explanation specific to this occurrence of the
93+
problem that is helpful to locate the problem and give advice on how
94+
to proceed. Written in English and readable for engineers, usually not
95+
suited for non technical stakeholders and not localized.
96+
example: some description for the error situation
97+
instance:
98+
type: string
99+
format: uri-reference
100+
description: >
101+
A URI reference that identifies the specific occurrence of the problem,
102+
e.g. by adding a fragment identifier or sub-path to the problem type.
103+
May be used to locate the root of this problem in the source code.
104+
example: '/some/uri-reference#specific-occurrence-context'

model-server-openapi/specifications/model-server.yaml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,6 @@ paths:
5757
$ref: '#/components/responses/200'
5858
default:
5959
$ref: '#/components/responses/GeneralError'
60-
/health:
61-
get:
62-
operationId: getHealth
63-
responses:
64-
"200":
65-
$ref: '#/components/responses/200'
66-
default:
67-
$ref: '#/components/responses/GeneralError'
6860
/v2/repositories:
6961
get:
7062
operationId: getRepositories

model-server/src/main/kotlin/org/modelix/model/server/Main.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import org.modelix.authorization.installAuthentication
6161
import org.modelix.model.InMemoryModels
6262
import org.modelix.model.server.handlers.ContentExplorer
6363
import org.modelix.model.server.handlers.DeprecatedLightModelServer
64+
import org.modelix.model.server.handlers.HealthApiImpl
6465
import org.modelix.model.server.handlers.HistoryHandler
6566
import org.modelix.model.server.handlers.HttpException
6667
import org.modelix.model.server.handlers.IdsApiImpl
@@ -160,14 +161,15 @@ object Main {
160161
}
161162
}
162163
var i = 0
164+
val globalStoreClient = storeClient.forGlobalRepository()
163165
while (i < cmdLineArgs.setValues.size) {
164-
storeClient.forGlobalRepository().put(cmdLineArgs.setValues[i], cmdLineArgs.setValues[i + 1])
166+
globalStoreClient.put(cmdLineArgs.setValues[i], cmdLineArgs.setValues[i + 1])
165167
i += 2
166168
}
167169
val localModelClient = LocalModelClient(storeClient.forContextRepository())
168170
val inMemoryModels = InMemoryModels()
169171
val repositoriesManager = RepositoriesManager(localModelClient)
170-
val modelServer = KeyValueLikeModelServer(repositoriesManager, storeClient.forGlobalRepository(), inMemoryModels)
172+
val modelServer = KeyValueLikeModelServer(repositoriesManager, globalStoreClient, inMemoryModels)
171173
val sharedSecretFile = cmdLineArgs.secretFile
172174
if (sharedSecretFile.exists()) {
173175
modelServer.setSharedSecret(
@@ -220,6 +222,7 @@ object Main {
220222
metricsApi.init(this)
221223
routing {
222224
IdsApiImpl(repositoriesManager, localModelClient).installRoutes(this)
225+
HealthApiImpl(repositoriesManager, globalStoreClient, inMemoryModels).installRoutes(this)
223226

224227
staticResources("/public", "public")
225228
get("/") {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2024.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.modelix.model.server.handlers
18+
19+
import io.ktor.http.ContentType
20+
import io.ktor.http.HttpStatusCode
21+
import io.ktor.server.application.ApplicationCall
22+
import io.ktor.server.application.call
23+
import io.ktor.server.response.respondText
24+
import io.ktor.util.pipeline.PipelineContext
25+
import org.modelix.api.operative.HealthApi
26+
import org.modelix.model.InMemoryModels
27+
import org.modelix.model.lazy.RepositoryId
28+
import org.modelix.model.server.handlers.KeyValueLikeModelServer.Companion.PROTECTED_PREFIX
29+
import org.modelix.model.server.store.IStoreClient
30+
31+
class HealthApiImpl(
32+
private val repositoriesManager: RepositoriesManager,
33+
private val storeClient: IStoreClient,
34+
private val inMemoryModels: InMemoryModels,
35+
) : HealthApi() {
36+
override suspend fun PipelineContext<Unit, ApplicationCall>.getHealth() {
37+
// eagerly load model into memory to speed up ModelQL queries
38+
val branchRef = System.getenv("MODELIX_SERVER_MODELQL_WARMUP_REPOSITORY")?.let { RepositoryId(it) }
39+
?.getBranchReference(System.getenv("MODELIX_SERVER_MODELQL_WARMUP_BRANCH"))
40+
if (branchRef != null) {
41+
val version = repositoriesManager.getVersion(branchRef)
42+
if (inMemoryModels.getModel(version!!.getTree()).isActive) {
43+
throw HttpException(
44+
HttpStatusCode.ServiceUnavailable,
45+
details = "Waiting for version $version to be loaded into memory",
46+
)
47+
}
48+
}
49+
50+
if (isHealthy()) {
51+
call.respondText(text = "healthy", contentType = ContentType.Text.Plain, status = HttpStatusCode.OK)
52+
} else {
53+
throw HttpException(HttpStatusCode.InternalServerError, details = "not healthy")
54+
}
55+
}
56+
57+
private fun isHealthy(): Boolean {
58+
val value = toLong(storeClient[HEALTH_KEY]) + 1
59+
storeClient.put(HEALTH_KEY, java.lang.Long.toString(value))
60+
return toLong(storeClient[HEALTH_KEY]) >= value
61+
}
62+
63+
private fun toLong(value: String?): Long {
64+
return if (value.isNullOrEmpty()) 0 else value.toLong()
65+
}
66+
67+
companion object {
68+
private const val HEALTH_KEY = PROTECTED_PREFIX + "health2"
69+
}
70+
}

model-server/src/main/kotlin/org/modelix/model/server/handlers/KeyValueLikeModelServer.kt

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import org.modelix.authorization.requiresPermission
4646
import org.modelix.authorization.toKeycloakScope
4747
import org.modelix.model.InMemoryModels
4848
import org.modelix.model.lazy.BranchReference
49-
import org.modelix.model.lazy.RepositoryId
5049
import org.modelix.model.persistent.HashUtil
5150
import org.modelix.model.server.store.ContextScopedStoreClient
5251
import org.modelix.model.server.store.IStoreClient
@@ -62,10 +61,6 @@ import java.util.regex.Pattern
6261
val PERMISSION_MODEL_SERVER = "model-server".asResource()
6362
val MODEL_SERVER_ENTRY = KeycloakResourceType("model-server-entry", KeycloakScope.READ_WRITE_DELETE)
6463

65-
private fun toLong(value: String?): Long {
66-
return if (value.isNullOrEmpty()) 0 else value.toLong()
67-
}
68-
6964
private class NotFoundException(description: String?) : RuntimeException(description)
7065

7166
typealias CallContext = PipelineContext<Unit, ApplicationCall>
@@ -85,8 +80,7 @@ class KeyValueLikeModelServer(
8580

8681
companion object {
8782
private val HASH_PATTERN: Pattern = Pattern.compile("[a-zA-Z0-9\\-_]{5}\\*[a-zA-Z0-9\\-_]{38}")
88-
private const val PROTECTED_PREFIX = "$$$"
89-
private const val HEALTH_KEY = PROTECTED_PREFIX + "health2"
83+
const val PROTECTED_PREFIX = "$$$"
9084
}
9185

9286
fun init(application: Application) {
@@ -106,26 +100,6 @@ class KeyValueLikeModelServer(
106100

107101
private fun Application.modelServerModule() {
108102
routing {
109-
get<Paths.getHealth> {
110-
// eagerly load model into memory to speed up ModelQL queries
111-
val branchRef = System.getenv("MODELIX_SERVER_MODELQL_WARMUP_REPOSITORY")?.let { RepositoryId(it) }
112-
?.getBranchReference(System.getenv("MODELIX_SERVER_MODELQL_WARMUP_BRANCH"))
113-
if (branchRef != null) {
114-
val version = repositoriesManager.getVersion(branchRef)
115-
if (inMemoryModels.getModel(version!!.getTree()).isActive) {
116-
throw HttpException(
117-
HttpStatusCode.ServiceUnavailable,
118-
details = "Waiting for version $version to be loaded into memory",
119-
)
120-
}
121-
}
122-
123-
if (isHealthy()) {
124-
call.respondText(text = "healthy", contentType = ContentType.Text.Plain, status = HttpStatusCode.OK)
125-
} else {
126-
throw HttpException(HttpStatusCode.InternalServerError, details = "not healthy")
127-
}
128-
}
129103
get<Paths.getHeaders> {
130104
val headers = call.request.headers.entries().flatMap { e -> e.value.map { e.key to it } }
131105
call.respondHtmlTemplate(PageWithMenuBar("headers", ".")) {
@@ -405,10 +379,4 @@ class KeyValueLikeModelServer(
405379
}
406380
call.checkPermission(MODEL_SERVER_ENTRY.createInstance(key), type.toKeycloakScope())
407381
}
408-
409-
private fun isHealthy(): Boolean {
410-
val value = toLong(storeClient[HEALTH_KEY]) + 1
411-
storeClient.put(HEALTH_KEY, java.lang.Long.toString(value))
412-
return toLong(storeClient[HEALTH_KEY]) >= value
413-
}
414382
}

0 commit comments

Comments
 (0)