Skip to content

Commit 9ff4825

Browse files
author
Oleksandr Dzhychko
committed
refactor(model-server): extract IRepositoriesManager interface to make route handlers testable
1 parent 8e04371 commit 9ff4825

File tree

11 files changed

+129
-50
lines changed

11 files changed

+129
-50
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import org.apache.commons.io.FileUtils
4848
import org.apache.ignite.Ignition
4949
import org.modelix.authorization.KeycloakUtils
5050
import org.modelix.authorization.installAuthentication
51+
import org.modelix.model.InMemoryModels
5152
import org.modelix.model.server.handlers.ContentExplorer
5253
import org.modelix.model.server.handlers.DeprecatedLightModelServer
5354
import org.modelix.model.server.handlers.HistoryHandler
@@ -150,8 +151,9 @@ object Main {
150151
i += 2
151152
}
152153
val localModelClient = LocalModelClient(storeClient)
154+
val inMemoryModels = InMemoryModels()
153155
val repositoriesManager = RepositoriesManager(localModelClient)
154-
val modelServer = KeyValueLikeModelServer(repositoriesManager)
156+
val modelServer = KeyValueLikeModelServer(repositoriesManager, storeClient, inMemoryModels)
155157
val sharedSecretFile = cmdLineArgs.secretFile
156158
if (sharedSecretFile.exists()) {
157159
modelServer.setSharedSecret(
@@ -162,7 +164,7 @@ object Main {
162164
val repositoryOverview = RepositoryOverview(repositoriesManager)
163165
val historyHandler = HistoryHandler(localModelClient, repositoriesManager)
164166
val contentExplorer = ContentExplorer(localModelClient, repositoriesManager)
165-
val modelReplicationServer = ModelReplicationServer(repositoriesManager)
167+
val modelReplicationServer = ModelReplicationServer(repositoriesManager, localModelClient, inMemoryModels)
166168
val metricsHandler = MetricsHandler()
167169

168170
val configureNetty: NettyApplicationEngine.Configuration.() -> Unit = {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import org.modelix.model.lazy.RepositoryId
5050
import org.modelix.model.server.templates.PageWithMenuBar
5151
import kotlin.collections.set
5252

53-
class ContentExplorer(private val client: IModelClient, private val repoManager: RepositoriesManager) {
53+
class ContentExplorer(private val client: IModelClient, private val repoManager: IRepositoriesManager) {
5454

5555
fun init(application: Application) {
5656
application.routing {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import org.modelix.model.server.templates.PageWithMenuBar
5555
import java.time.LocalDateTime
5656
import java.time.format.DateTimeFormatter
5757

58-
class HistoryHandler(val client: IModelClient, private val repositoriesManager: RepositoriesManager) {
58+
class HistoryHandler(val client: IModelClient, private val repositoriesManager: IRepositoriesManager) {
5959

6060
fun init(application: Application) {
6161
application.routing {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 org.modelix.model.lazy.BranchReference
20+
import org.modelix.model.lazy.CLVersion
21+
import org.modelix.model.lazy.RepositoryId
22+
23+
interface IRepositoriesManager {
24+
/**
25+
* Used to retrieve the server ID. If needed, the server ID is created and stored.
26+
*
27+
* If a server ID was not created yet, it is generated and saved in the database.
28+
* It gets stored under the current and all legacy database keys.
29+
*
30+
* If the server ID was created previously but is only stored under a legacy database key,
31+
* it also gets stored under the current and all legacy database keys.
32+
*/
33+
suspend fun maybeInitAndGetSeverId(): String
34+
fun getRepositories(): Set<RepositoryId>
35+
suspend fun createRepository(repositoryId: RepositoryId, userName: String?, useRoleIds: Boolean = true): CLVersion
36+
suspend fun removeRepository(repository: RepositoryId): Boolean
37+
38+
fun getBranches(repositoryId: RepositoryId): Set<BranchReference>
39+
40+
suspend fun removeBranches(repository: RepositoryId, branchNames: Set<String>)
41+
42+
/**
43+
* Same as [removeBranches] but blocking.
44+
* Caller is expected to execute it outside the request thread.
45+
*/
46+
fun removeBranchesBlocking(repository: RepositoryId, branchNames: Set<String>)
47+
suspend fun getVersion(branch: BranchReference): CLVersion?
48+
suspend fun getVersionHash(branch: BranchReference): String?
49+
suspend fun pollVersionHash(branch: BranchReference, lastKnown: String?): String
50+
suspend fun mergeChanges(branch: BranchReference, newVersionHash: String): String
51+
52+
/**
53+
* Same as [mergeChanges] but blocking.
54+
* Caller is expected to execute it outside the request thread.
55+
*/
56+
fun mergeChangesBlocking(branch: BranchReference, newVersionHash: String): String
57+
suspend fun computeDelta(versionHash: String, baseVersionHash: String?): ObjectData
58+
}
59+
60+
fun IRepositoriesManager.getBranchNames(repositoryId: RepositoryId): Set<String> {
61+
return getBranches(repositoryId).map { it.branchName }.toSet()
62+
}

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import org.modelix.authorization.checkPermission
4444
import org.modelix.authorization.getUserName
4545
import org.modelix.authorization.requiresPermission
4646
import org.modelix.authorization.toKeycloakScope
47+
import org.modelix.model.InMemoryModels
4748
import org.modelix.model.lazy.BranchReference
4849
import org.modelix.model.lazy.RepositoryId
4950
import org.modelix.model.persistent.HashUtil
@@ -67,16 +68,22 @@ private class NotFoundException(description: String?) : RuntimeException(descrip
6768

6869
typealias CallContext = PipelineContext<Unit, ApplicationCall>
6970

70-
class KeyValueLikeModelServer(val repositoriesManager: RepositoriesManager) {
71+
class KeyValueLikeModelServer(
72+
private val repositoriesManager: IRepositoriesManager,
73+
private val storeClient: IStoreClient,
74+
private val inMemoryModels: InMemoryModels,
75+
) {
76+
77+
constructor(repositoriesManager: RepositoriesManager) :
78+
this(repositoriesManager, repositoriesManager.client.store, InMemoryModels())
79+
7180
companion object {
7281
private val LOG = LoggerFactory.getLogger(KeyValueLikeModelServer::class.java)
7382
val HASH_PATTERN = Pattern.compile("[a-zA-Z0-9\\-_]{5}\\*[a-zA-Z0-9\\-_]{38}")
7483
const val PROTECTED_PREFIX = "$$$"
7584
val HEALTH_KEY = PROTECTED_PREFIX + "health2"
7685
}
7786

78-
val storeClient: IStoreClient get() = repositoriesManager.client.store
79-
8087
fun init(application: Application) {
8188
// Functionally, it does not matter if the server ID
8289
// is created eagerly on startup or lazily on the first request,
@@ -100,7 +107,7 @@ class KeyValueLikeModelServer(val repositoriesManager: RepositoriesManager) {
100107
?.getBranchReference(System.getenv("MODELIX_SERVER_MODELQL_WARMUP_BRANCH"))
101108
if (branchRef != null) {
102109
val version = repositoriesManager.getVersion(branchRef)
103-
if (repositoriesManager.inMemoryModels.getModel(version!!.getTree()).isActive) {
110+
if (inMemoryModels.getModel(version!!.getTree()).isActive) {
104111
call.respondText(
105112
status = HttpStatusCode.ServiceUnavailable,
106113
text = "Waiting for version $version to be loaded into memory",
@@ -346,7 +353,7 @@ class KeyValueLikeModelServer(val repositoriesManager: RepositoriesManager) {
346353

347354
HashUtil.checkObjectHashes(hashedObjects)
348355

349-
repositoriesManager.client.store.runTransactionSuspendable {
356+
storeClient.runTransactionSuspendable {
350357
storeClient.putAll(hashedObjects)
351358
storeClient.putAll(userDefinedEntries)
352359
for ((branch, value) in branchChanges) {

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import kotlinx.serialization.encodeToString
4646
import kotlinx.serialization.json.Json
4747
import org.modelix.api.public.Paths
4848
import org.modelix.authorization.getUserName
49+
import org.modelix.model.InMemoryModels
4950
import org.modelix.model.api.ITree
5051
import org.modelix.model.api.PBranch
5152
import org.modelix.model.api.TreePointer
@@ -69,15 +70,20 @@ import org.slf4j.LoggerFactory
6970
* Implements the endpoints used by the 'model-client', but compared to KeyValueLikeModelServer also understands what
7071
* client sends. This allows more validations and more responsibilities on the server side.
7172
*/
72-
class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
73-
constructor(modelClient: LocalModelClient) : this(RepositoriesManager(modelClient))
73+
class ModelReplicationServer(
74+
private val repositoriesManager: IRepositoriesManager,
75+
private val modelClient: LocalModelClient,
76+
private val inMemoryModels: InMemoryModels,
77+
) {
78+
constructor(repositoriesManager: RepositoriesManager) :
79+
this(repositoriesManager, repositoriesManager.client, InMemoryModels())
80+
81+
constructor(modelClient: LocalModelClient) : this(RepositoriesManager(modelClient), modelClient, InMemoryModels())
7482
constructor(storeClient: IStoreClient) : this(LocalModelClient(storeClient))
7583

7684
companion object {
7785
private val LOG = LoggerFactory.getLogger(ModelReplicationServer::class.java)
7886
}
79-
80-
private val modelClient: LocalModelClient get() = repositoriesManager.client
8187
private val storeClient: IStoreClient get() = modelClient.store
8288

8389
fun init(application: Application) {
@@ -268,16 +274,16 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
268274
LOG.trace("Running query on {} @ {}", branchRef, version)
269275
val initialTree = version!!.getTree()
270276
val branch = OTBranch(
271-
PBranch(initialTree, repositoriesManager.client.idGenerator),
272-
repositoriesManager.client.idGenerator,
273-
repositoriesManager.client.storeCache,
277+
PBranch(initialTree, modelClient.idGenerator),
278+
modelClient.idGenerator,
279+
modelClient.storeCache,
274280
)
275281

276282
ModelQLServer.handleCall(call, { writeAccess ->
277283
if (writeAccess) {
278284
branch.getRootNode() to branch.getArea()
279285
} else {
280-
val model = repositoriesManager.inMemoryModels.getModel(initialTree).await()
286+
val model = inMemoryModels.getModel(initialTree).await()
281287
model.getNode(ITree.ROOT_ID) to model.getArea()
282288
}
283289
}, {
@@ -286,7 +292,7 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
286292
val (ops, newTree) = branch.getPendingChanges()
287293
if (newTree != initialTree) {
288294
val newVersion = CLVersion.createRegularVersion(
289-
id = repositoriesManager.client.idGenerator.generate(),
295+
id = modelClient.idGenerator.generate(),
290296
author = getUserName(),
291297
tree = newTree as CLTree,
292298
baseVersion = version,
@@ -299,7 +305,7 @@ class ModelReplicationServer(val repositoriesManager: RepositoriesManager) {
299305

300306
post<Paths.postRepositoryVersionHashQuery> {
301307
val versionHash = call.parameters["versionHash"]!!
302-
val version = CLVersion.loadFromHash(versionHash, repositoriesManager.client.storeCache)
308+
val version = CLVersion.loadFromHash(versionHash, modelClient.storeCache)
303309
val initialTree = version.getTree()
304310
val branch = TreePointer(initialTree)
305311
ModelQLServer.handleCall(call, branch.getRootNode(), branch.getArea())

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

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import kotlinx.coroutines.launch
2525
import kotlinx.coroutines.runBlocking
2626
import kotlinx.datetime.Clock
2727
import org.apache.commons.collections4.map.LRUMap
28-
import org.modelix.model.InMemoryModels
2928
import org.modelix.model.ModelMigrations
3029
import org.modelix.model.VersionMerger
3130
import org.modelix.model.api.IBranch
@@ -51,7 +50,7 @@ import org.slf4j.LoggerFactory
5150
import java.lang.ref.SoftReference
5251
import java.util.UUID
5352

54-
class RepositoriesManager(val client: LocalModelClient) {
53+
class RepositoriesManager(val client: LocalModelClient) : IRepositoriesManager {
5554

5655
init {
5756
fun migrateLegacyRepositoriesList(infoBranch: IBranch) {
@@ -82,13 +81,6 @@ class RepositoriesManager(val client: LocalModelClient) {
8281

8382
private val store: IStoreClient get() = client.store
8483
private val objectStore: IDeserializingKeyValueStore get() = client.storeCache
85-
val inMemoryModels = InMemoryModels()
86-
87-
fun dispose() {
88-
// TODO find instance creations and add a dispose() call if needed. Whoever creates an instance is responsible
89-
// for its lifecycle.
90-
inMemoryModels.dispose()
91-
}
9284

9385
fun generateClientId(repositoryId: RepositoryId): Long {
9486
return client.store.generateId("$KEY_PREFIX:${repositoryId.id}:clientId")
@@ -103,7 +95,7 @@ class RepositoriesManager(val client: LocalModelClient) {
10395
* If the server ID was created previously but is only stored under a legacy database key,
10496
* it also gets stored under the current and all legacy database keys.
10597
*/
106-
suspend fun maybeInitAndGetSeverId(): String {
98+
override suspend fun maybeInitAndGetSeverId(): String {
10799
return store.runTransactionSuspendable {
108100
var serverId = store[SERVER_ID_KEY]
109101
if (serverId == null) {
@@ -118,7 +110,7 @@ class RepositoriesManager(val client: LocalModelClient) {
118110
}
119111
}
120112

121-
fun getRepositories(): Set<RepositoryId> {
113+
override fun getRepositories(): Set<RepositoryId> {
122114
val repositoriesList = store[REPOSITORIES_LIST_KEY]
123115
val emptyRepositoriesList = repositoriesList.isNullOrBlank()
124116
return if (emptyRepositoriesList) {
@@ -130,7 +122,7 @@ class RepositoriesManager(val client: LocalModelClient) {
130122

131123
private fun repositoryExists(repositoryId: RepositoryId) = getRepositories().contains(repositoryId)
132124

133-
suspend fun createRepository(repositoryId: RepositoryId, userName: String?, useRoleIds: Boolean = true): CLVersion {
125+
override suspend fun createRepository(repositoryId: RepositoryId, userName: String?, useRoleIds: Boolean): CLVersion {
134126
var initialVersion: CLVersion? = null
135127
store.runTransactionSuspendable {
136128
val masterBranch = repositoryId.getBranchReference()
@@ -155,7 +147,7 @@ class RepositoriesManager(val client: LocalModelClient) {
155147
return store[branchListKey(repositoryId)]?.lines()?.toSet() ?: emptySet()
156148
}
157149

158-
fun getBranches(repositoryId: RepositoryId): Set<BranchReference> {
150+
override fun getBranches(repositoryId: RepositoryId): Set<BranchReference> {
159151
return getBranchNames(repositoryId)
160152
.map { repositoryId.getBranchReference(it) }
161153
.sortedBy { it.branchName }
@@ -188,7 +180,7 @@ class RepositoriesManager(val client: LocalModelClient) {
188180
}
189181
}
190182

191-
suspend fun removeRepository(repository: RepositoryId): Boolean {
183+
override suspend fun removeRepository(repository: RepositoryId): Boolean {
192184
return store.runTransactionSuspendable {
193185
if (!repositoryExists(repository)) {
194186
return@runTransactionSuspendable false
@@ -206,7 +198,7 @@ class RepositoriesManager(val client: LocalModelClient) {
206198
}
207199
}
208200

209-
suspend fun removeBranches(repository: RepositoryId, branchNames: Set<String>) {
201+
override suspend fun removeBranches(repository: RepositoryId, branchNames: Set<String>) {
210202
return store.runTransactionSuspendable {
211203
removeBranchesBlocking(repository, branchNames)
212204
}
@@ -216,7 +208,7 @@ class RepositoriesManager(val client: LocalModelClient) {
216208
* Same as [removeBranches] but blocking.
217209
* Caller is expected to execute it outside the request thread.
218210
*/
219-
fun removeBranchesBlocking(repository: RepositoryId, branchNames: Set<String>) {
211+
override fun removeBranchesBlocking(repository: RepositoryId, branchNames: Set<String>) {
220212
if (branchNames.isEmpty()) return
221213
store.runTransaction {
222214
val key = branchListKey(repository)
@@ -229,7 +221,7 @@ class RepositoriesManager(val client: LocalModelClient) {
229221
}
230222
}
231223

232-
suspend fun mergeChanges(branch: BranchReference, newVersionHash: String): String {
224+
override suspend fun mergeChanges(branch: BranchReference, newVersionHash: String): String {
233225
return store.runTransactionSuspendable {
234226
mergeChangesBlocking(branch, newVersionHash)
235227
}
@@ -239,7 +231,7 @@ class RepositoriesManager(val client: LocalModelClient) {
239231
* Same as [mergeChanges] but blocking.
240232
* Caller is expected to execute it outside the request thread.
241233
*/
242-
fun mergeChangesBlocking(branch: BranchReference, newVersionHash: String): String {
234+
override fun mergeChangesBlocking(branch: BranchReference, newVersionHash: String): String {
243235
return store.runTransaction {
244236
val headHash = getVersionHashBlocking(branch)
245237
val mergedHash = if (headHash == null) {
@@ -262,11 +254,11 @@ class RepositoriesManager(val client: LocalModelClient) {
262254
}
263255
}
264256

265-
suspend fun getVersion(branch: BranchReference): CLVersion? {
257+
override suspend fun getVersion(branch: BranchReference): CLVersion? {
266258
return getVersionHash(branch)?.let { CLVersion.loadFromHash(it, client.storeCache) }
267259
}
268260

269-
suspend fun getVersionHash(branch: BranchReference): String? {
261+
override suspend fun getVersionHash(branch: BranchReference): String? {
270262
return store.runTransactionSuspendable {
271263
getVersionHashBlocking(branch)
272264
}
@@ -293,13 +285,13 @@ class RepositoriesManager(val client: LocalModelClient) {
293285
store.put(legacyBranchKey(branch), hash, false)
294286
}
295287

296-
suspend fun pollVersionHash(branch: BranchReference, lastKnown: String?): String {
288+
override suspend fun pollVersionHash(branch: BranchReference, lastKnown: String?): String {
297289
return pollEntry(client.store, branchKey(branch), lastKnown)
298290
?: throw IllegalStateException("No version found for branch '${branch.branchName}' in repository '${branch.repositoryId}'")
299291
}
300292

301293
private val versionDeltaCache = VersionDeltaCache(client.storeCache)
302-
suspend fun computeDelta(versionHash: String, baseVersionHash: String?): ObjectData {
294+
override suspend fun computeDelta(versionHash: String, baseVersionHash: String?): ObjectData {
303295
if (versionHash == baseVersionHash) return ObjectData.empty
304296
if (baseVersionHash == null) {
305297
// no need to cache anything if there is no delta computation happening

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import kotlinx.html.tr
2525
import org.modelix.api.html.Paths
2626
import org.modelix.model.server.templates.PageWithMenuBar
2727

28-
class RepositoryOverview(private val repoManager: RepositoriesManager) {
28+
class RepositoryOverview(private val repoManager: IRepositoriesManager) {
2929

3030
fun init(application: Application) {
3131
application.routing {

0 commit comments

Comments
 (0)