Skip to content

Commit 4ecbbc9

Browse files
committed
feat(model-server): new environment variables for warming up the ModelQL cache
It ensures that the latest model version on a branch is loaded into memory so that also the first ModelQL query is fast.
1 parent 2456877 commit 4ecbbc9

File tree

5 files changed

+68
-12
lines changed

5 files changed

+68
-12
lines changed

model-datastructure/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ kotlin {
5555
// implementation("org.json:json:20230618")
5656
implementation(libs.trove4j)
5757
implementation(libs.apache.commons.collections)
58+
implementation(libs.kotlin.coroutines.core)
5859
//
5960
// implementation("com.google.oauth-client:google-oauth-client:1.34.1")
6061
// implementation("com.google.oauth-client:google-oauth-client-jetty:1.34.1")

model-datastructure/src/jvmMain/kotlin/org/modelix/model/InMemoryModel.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ package org.modelix.model
1818

1919
import gnu.trove.map.TLongObjectMap
2020
import gnu.trove.map.hash.TLongObjectHashMap
21+
import kotlinx.coroutines.CoroutineScope
22+
import kotlinx.coroutines.Dispatchers
23+
import kotlinx.coroutines.Job
24+
import kotlinx.coroutines.launch
2125
import org.modelix.model.api.ConceptReference
2226
import org.modelix.model.api.IBranch
2327
import org.modelix.model.api.IConcept
@@ -44,6 +48,42 @@ import kotlin.time.DurationUnit
4448

4549
private val LOG = mu.KotlinLogging.logger { }
4650

51+
class InMemoryModelLoader(val model: IncrementalInMemoryModel) {
52+
private val coroutineScope = CoroutineScope(Dispatchers.IO)
53+
private var modelLoadingJob: Job? = null
54+
55+
/**
56+
* Should be called repeatedly by a readiness probe until it returns true.
57+
*
58+
* @return true if the model is done loading
59+
*/
60+
@Synchronized
61+
fun loadModelAsync(tree: CLTree): Boolean {
62+
if (model.getLoadedModel()?.loadedMapRef?.getHash() == tree.nodesMap!!.hash) return true
63+
if (modelLoadingJob?.isActive != true) {
64+
modelLoadingJob = coroutineScope.launch {
65+
try {
66+
model.getModel(tree)
67+
} catch (ex: Throwable) {
68+
LOG.error(ex) { "Failed loading model ${tree.hash}" }
69+
}
70+
}
71+
}
72+
return false
73+
}
74+
}
75+
76+
class InMemoryModels {
77+
private val models = HashMap<String, InMemoryModelLoader>()
78+
79+
@Synchronized
80+
fun getModel(id: String) = models.getOrPut(id) { InMemoryModelLoader(IncrementalInMemoryModel()) }
81+
82+
fun getModel(tree: CLTree) = getModel(tree.getId()).model.getModel(tree)
83+
84+
fun loadModelAsync(tree: CLTree) = getModel(tree.getId()).loadModelAsync(tree)
85+
}
86+
4787
class IncrementalInMemoryModel {
4888
private var lastModel: InMemoryModel? = null
4989

@@ -58,6 +98,8 @@ class IncrementalInMemoryModel {
5898
lastModel = newModel
5999
return newModel
60100
}
101+
102+
fun getLoadedModel() = lastModel
61103
}
62104

63105
class InMemoryModel private constructor(

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import org.modelix.authorization.getUserName
4444
import org.modelix.authorization.requiresPermission
4545
import org.modelix.authorization.toKeycloakScope
4646
import org.modelix.model.lazy.BranchReference
47+
import org.modelix.model.lazy.RepositoryId
4748
import org.modelix.model.persistent.HashUtil
4849
import org.modelix.model.server.store.IStoreClient
4950
import org.modelix.model.server.store.pollEntry
@@ -93,6 +94,20 @@ class KeyValueLikeModelServer(val repositoriesManager: RepositoriesManager) {
9394
private fun Application.modelServerModule() {
9495
routing {
9596
get<Paths.getHealth> {
97+
// eagerly load model into memory to speed up ModelQL queries
98+
val branchRef = System.getenv("MODELIX_SERVER_MODELQL_WARMUP_REPOSITORY")?.let { RepositoryId(it) }
99+
?.getBranchReference(System.getenv("MODELIX_SERVER_MODELQL_WARMUP_BRANCH"))
100+
if (branchRef != null) {
101+
val version = repositoriesManager.getVersion(branchRef)
102+
if (!repositoriesManager.inMemoryModels.loadModelAsync(version!!.getTree())) {
103+
call.respondText(
104+
status = HttpStatusCode.ServiceUnavailable,
105+
text = "Waiting for version $version to be loaded into memory",
106+
)
107+
return@get
108+
}
109+
}
110+
96111
if (isHealthy()) {
97112
call.respondText(text = "healthy", contentType = ContentType.Text.Plain, status = HttpStatusCode.OK)
98113
} else {

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

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import kotlinx.serialization.encodeToString
4747
import kotlinx.serialization.json.Json
4848
import org.modelix.api.public.Paths
4949
import org.modelix.authorization.getUserName
50-
import org.modelix.model.IncrementalInMemoryModel
5150
import org.modelix.model.api.ITree
5251
import org.modelix.model.api.PBranch
5352
import org.modelix.model.api.TreePointer
@@ -266,25 +265,22 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
266265
TODO()
267266
}
268267

269-
val inMemoryModel = IncrementalInMemoryModel()
270-
post<Paths.postRepositoryBranchQuery> {
271-
fun ApplicationCall.repositoryId() = RepositoryId(parameters["repository"]!!)
272-
fun PipelineContext<Unit, ApplicationCall>.repositoryId() = call.repositoryId()
273-
274-
fun ApplicationCall.branchRef() = repositoryId().getBranchReference(parameters["branch"]!!)
275-
fun PipelineContext<Unit, ApplicationCall>.branchRef() = call.branchRef()
276-
277-
val branchRef = branchRef()
268+
post<Paths.postRepositoryBranchQuery> { parameters ->
269+
val branchRef = RepositoryId(parameters.repository).getBranchReference(parameters.branch)
278270
val version = repositoriesManager.getVersion(branchRef)
279271
LOG.trace("Running query on {} @ {}", branchRef, version)
280272
val initialTree = version!!.getTree()
281-
val branch = OTBranch(PBranch(initialTree, repositoriesManager.client.idGenerator), repositoriesManager.client.idGenerator, repositoriesManager.client.storeCache)
273+
val branch = OTBranch(
274+
PBranch(initialTree, repositoriesManager.client.idGenerator),
275+
repositoriesManager.client.idGenerator,
276+
repositoriesManager.client.storeCache,
277+
)
282278

283279
ModelQLServer.handleCall(call, { writeAccess ->
284280
if (writeAccess) {
285281
branch.getRootNode() to branch.getArea()
286282
} else {
287-
val model = inMemoryModel.getModel(initialTree)
283+
val model = repositoriesManager.inMemoryModels.getModel(initialTree)
288284
model.getNode(ITree.ROOT_ID) to model.getArea()
289285
}
290286
}, {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.map
2121
import kotlinx.datetime.Clock
2222
import org.apache.commons.collections4.map.LRUMap
2323
import org.modelix.model.IKeyValueStore
24+
import org.modelix.model.InMemoryModels
2425
import org.modelix.model.VersionMerger
2526
import org.modelix.model.api.IBranch
2627
import org.modelix.model.api.IReadTransaction
@@ -51,6 +52,7 @@ class RepositoriesManager(val client: LocalModelClient) {
5152
private val store: IStoreClient get() = client.store
5253
private val kvStore: IKeyValueStore get() = client.asyncStore
5354
private val objectStore: IDeserializingKeyValueStore get() = client.storeCache
55+
val inMemoryModels = InMemoryModels()
5456

5557
fun generateClientId(repositoryId: RepositoryId): Long {
5658
return client.store.generateId("$KEY_PREFIX:${repositoryId.id}:clientId")

0 commit comments

Comments
 (0)