Skip to content

Commit 62884ed

Browse files
author
Oleksandr Dzhychko
authored
Merge pull request #332 from modelix/MODELIX-608
MODELIX-608 Health check for warming up the MPS cache
2 parents af26e09 + 6faa0e2 commit 62884ed

File tree

3 files changed

+99
-10
lines changed

3 files changed

+99
-10
lines changed

docs/global/modules/core/pages/reference/component-mps-model-server-plugin.adoc

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,21 @@ https://api.modelix.org/3.12.0/mps-model-server-plugin/index.html[API doc^] | ht
99

1010
== Health checks
1111

12-
The plugin offers a set of health checks via HTTP on port 48305 and path `/health`.
12+
The plugin offers a set of health checks via HTTP GET on port `48305` and path `/health`.
13+
1314
Health checks can be enabled adding query parameters with the health check name and the value `true` to the request.
15+
Some health checks require further information that needs to be provided by query parameters.
16+
17+
.Example of combining health check
18+
[source,text]
19+
----
20+
http(s)://<host>:48305/health?indexer=true&loadModels=true&loadModelsModuleNamespacePrefix=foo.bar <1> <2>
21+
----
22+
<.> `indexer=true` enables <<indexer>>
23+
<.> `loadModels=true` enables <<loadModels>>
24+
* `loadModelsModuleNamespacePrefix` is a parameter related to `loadModels`
1425

26+
[#indexer]
1527
=== indexer
1628

1729
The check fails, if the indexer is currently running for one of the opened projects.
@@ -30,3 +42,26 @@ Reports an unhealthy system whenever no project is loaded.
3042

3143
Reports an unhealthy system when no virtual folders are available.
3244
This might also be true in case a project without virtual folders is fully loaded.
45+
46+
[#loadModels]
47+
=== loadModels
48+
49+
Returns after trying to eagerly load a set of specified modules.
50+
This check can be used to avoid a slow first ModelQl query after launching an MPS instance running this plugin.
51+
52+
[NOTE]
53+
This health check has the side effect of loading the model data.
54+
It does not just report whether the model data is loaded or not.
55+
It always tries to load model data before returning a result.
56+
57+
Multiple `loadModelsModuleNamespacePrefix` parameters can be provided
58+
to specify the modules from which the models should be loaded.
59+
60+
.Usage example
61+
[source,text]
62+
----
63+
http(s)://<host>:48305/health?loadModels=true&loadModelsModuleNamespacePrefix=org.foo&loadModelsModuleNamespacePrefix=org.bar <.> <.> <.>
64+
----
65+
<.> `loadModels=true` enables <<loadModels>>
66+
<.> `loadModelsModuleNamespacePrefix=org.foo` specifies to load all models from modules starting with `org.foo`
67+
<.> `loadModelsModuleNamespacePrefix=org.bar` specifies to load all models from modules starting with `org.bar`

model-server-lib/src/main/kotlin/org/modelix/model/server/light/LightModelServer.kt

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import io.ktor.server.websocket.WebSockets
3535
import io.ktor.server.websocket.pingPeriod
3636
import io.ktor.server.websocket.timeout
3737
import io.ktor.server.websocket.webSocket
38+
import io.ktor.util.toMap
3839
import io.ktor.websocket.Frame
3940
import io.ktor.websocket.readText
4041
import io.ktor.websocket.send
@@ -208,22 +209,27 @@ class LightModelServer @JvmOverloads constructor(val port: Int, val rootNodeProv
208209
try {
209210
val allChecks = healthChecks.associateBy { it.id }.toMap()
210211
val enabledChecks = allChecks.filter { it.value.enabledByDefault }.keys.toMutableSet()
211-
212-
call.request.queryParameters.entries().forEach { entry ->
213-
entry.value.forEach { value ->
214-
if (!allChecks.containsKey(entry.key)) throw IllegalArgumentException("Unknown check: ${entry.key}")
215-
if (value.toBooleanStrict()) {
216-
enabledChecks.add(entry.key)
217-
} else {
218-
enabledChecks.remove(entry.key)
212+
val validParameterNames = (allChecks.keys + allChecks.flatMap { it.value.validParameterNames }).toSet()
213+
214+
val queryParameters = call.request.queryParameters
215+
val queryParametersMap = queryParameters.toMap()
216+
queryParameters.entries().forEach { entry ->
217+
require(validParameterNames.contains(entry.key)) { "Unknown check: ${entry.key}" }
218+
if (allChecks.containsKey(entry.key)) {
219+
entry.value.forEach { value ->
220+
if (value.toBooleanStrict()) {
221+
enabledChecks.add(entry.key)
222+
} else {
223+
enabledChecks.remove(entry.key)
224+
}
219225
}
220226
}
221227
}
222228
var isHealthy = true
223229
for (healthCheck in allChecks.values) {
224230
if (enabledChecks.contains(healthCheck.id)) {
225231
output.appendLine("--- running check '${healthCheck.id}' ---")
226-
val result = healthCheck.run(output)
232+
val result = healthCheck.run(output, queryParametersMap)
227233
output.appendLine()
228234
output.appendLine("-> " + if (result) "successful" else "failed")
229235
isHealthy = isHealthy && result
@@ -442,9 +448,11 @@ class LightModelServer @JvmOverloads constructor(val port: Int, val rootNodeProv
442448
}
443449

444450
interface IHealthCheck {
451+
val validParameterNames: Set<String> get() = emptySet()
445452
val id: String
446453
val enabledByDefault: Boolean
447454
fun run(output: StringBuilder): Boolean
455+
fun run(output: StringBuilder, parameters: Map<String, List<String>>): Boolean = run(output)
448456
}
449457
}
450458

mps-model-server-plugin/src/main/kotlin/org/modelix/model/server/mps/MPSModelServer.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class MPSModelServerForProject(private val project: Project) : Disposable {
3939
}
4040
}
4141

42+
const val LOAD_MODELS_MODULE_PARAMETER_NAME = "loadModelsModuleNamespacePrefix"
43+
4244
@Service(Service.Level.APP)
4345
class MPSModelServer : Disposable {
4446

@@ -128,6 +130,50 @@ class MPSModelServer : Disposable {
128130
return false
129131
}
130132
})
133+
.healthCheck(object : LightModelServer.IHealthCheck {
134+
// Usage example for this health check:
135+
// `/health?loadModels=true&loadModelsModuleNamespacePrefix=org.foo&loadModelsModuleNamespacePrefix=org.bar`
136+
// This should load all models from modules starting with either `org.foo` or `org.bar`.
137+
override val validParameterNames: Set<String> = setOf(LOAD_MODELS_MODULE_PARAMETER_NAME)
138+
override val id: String
139+
get() = "loadModels"
140+
override val enabledByDefault: Boolean
141+
get() = false
142+
143+
override fun run(output: StringBuilder): Boolean {
144+
throw UnsupportedOperationException("parameters required")
145+
}
146+
147+
override fun run(output: StringBuilder, parameters: Map<String, List<String>>): Boolean {
148+
val projects = getMPSProjects()
149+
val namespaces = parameters[LOAD_MODELS_MODULE_PARAMETER_NAME]?.toSet()
150+
if (namespaces == null) {
151+
output.append("parameter '$LOAD_MODELS_MODULE_PARAMETER_NAME' missing")
152+
return false
153+
}
154+
155+
val project = projects.firstOrNull()
156+
if (project == null) {
157+
output.append("no projects loaded")
158+
return false
159+
}
160+
val repository = project.repository
161+
repository.modelAccess.runReadAction {
162+
val modules = repository.modules.filter {
163+
val moduleName = it.moduleName ?: return@filter false
164+
namespaces.any { namespace -> moduleName.startsWith(namespace) }
165+
}
166+
val models = modules.flatMap { it.models }
167+
models.forEach {
168+
// This triggers the loading of the model data which would otherwise slow down the
169+
// first query.
170+
it.rootNodes
171+
}
172+
output.append("${models.size} models in ${modules.size} modules loaded")
173+
}
174+
return true
175+
}
176+
})
131177
.build()
132178
server!!.start()
133179
}

0 commit comments

Comments
 (0)