Skip to content

Commit f3b4e9f

Browse files
authored
Merge pull request #1235 from modelix/MODELIX-1050-Deadlock-in-model-server
fix(model-server): deadlock caused by non-existing lock ordering
2 parents 3556438 + cfebf70 commit f3b4e9f

37 files changed

+1533
-624
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import org.modelix.model.server.handlers.ui.RepositoryOverview
5858
import org.modelix.model.server.store.IgniteStoreClient
5959
import org.modelix.model.server.store.InMemoryStoreClient
6060
import org.modelix.model.server.store.IsolatingStore
61+
import org.modelix.model.server.store.RequiresTransaction
6162
import org.modelix.model.server.store.forGlobalRepository
6263
import org.modelix.model.server.store.loadDump
6364
import org.modelix.model.server.store.writeDump
@@ -149,9 +150,12 @@ object Main {
149150
}
150151
var i = 0
151152
val globalStoreClient = storeClient.forGlobalRepository()
152-
while (i < cmdLineArgs.setValues.size) {
153-
globalStoreClient.put(cmdLineArgs.setValues[i], cmdLineArgs.setValues[i + 1])
154-
i += 2
153+
@OptIn(RequiresTransaction::class)
154+
globalStoreClient.getTransactionManager().runWrite {
155+
while (i < cmdLineArgs.setValues.size) {
156+
globalStoreClient.put(cmdLineArgs.setValues[i], cmdLineArgs.setValues[i + 1])
157+
i += 2
158+
}
155159
}
156160
val repositoriesManager = RepositoriesManager(storeClient)
157161
val modelServer = KeyValueLikeModelServer(repositoriesManager)

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import io.ktor.server.application.call
77
import io.ktor.server.response.respondText
88
import io.ktor.util.pipeline.PipelineContext
99
import org.modelix.model.server.handlers.KeyValueLikeModelServer.Companion.PROTECTED_PREFIX
10+
import org.modelix.model.server.store.RequiresTransaction
1011
import org.modelix.model.server.store.StoreManager
1112

1213
class HealthApiImpl(
@@ -25,9 +26,12 @@ class HealthApiImpl(
2526

2627
private fun isHealthy(): Boolean {
2728
val store = stores.getGlobalStoreClient()
28-
val value = toLong(store[HEALTH_KEY]) + 1
29-
store.put(HEALTH_KEY, java.lang.Long.toString(value))
30-
return toLong(store[HEALTH_KEY]) >= value
29+
@OptIn(RequiresTransaction::class)
30+
return store.getTransactionManager().runWrite {
31+
val value = toLong(store[HEALTH_KEY]) + 1
32+
store.put(HEALTH_KEY, java.lang.Long.toString(value))
33+
toLong(store[HEALTH_KEY]) >= value
34+
}
3135
}
3236

3337
private fun toLong(value: String?): Long {

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

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import org.modelix.model.lazy.CLVersion
66
import org.modelix.model.lazy.IDeserializingKeyValueStore
77
import org.modelix.model.lazy.RepositoryId
88
import org.modelix.model.server.store.IStoreClient
9+
import org.modelix.model.server.store.ITransactionManager
10+
import org.modelix.model.server.store.RequiresTransaction
911
import org.modelix.model.server.store.StoreManager
1012

1113
interface IRepositoriesManager {
@@ -18,31 +20,38 @@ interface IRepositoriesManager {
1820
* If the server ID was created previously but is only stored under a legacy database key,
1921
* it also gets stored under the current and all legacy database keys.
2022
*/
21-
suspend fun maybeInitAndGetSeverId(): String
23+
@RequiresTransaction
24+
fun maybeInitAndGetSeverId(): String
25+
26+
@RequiresTransaction
2227
fun getRepositories(): Set<RepositoryId>
23-
suspend fun createRepository(repositoryId: RepositoryId, userName: String?, useRoleIds: Boolean = true, legacyGlobalStorage: Boolean = false): CLVersion
24-
suspend fun removeRepository(repository: RepositoryId): Boolean
2528

26-
fun getBranches(repositoryId: RepositoryId): Set<BranchReference>
29+
@RequiresTransaction
30+
fun createRepository(repositoryId: RepositoryId, userName: String?, useRoleIds: Boolean = true, legacyGlobalStorage: Boolean = false): CLVersion
2731

28-
suspend fun removeBranches(repository: RepositoryId, branchNames: Set<String>)
32+
@RequiresTransaction
33+
fun removeRepository(repository: RepositoryId): Boolean
34+
35+
@RequiresTransaction
36+
fun getBranches(repositoryId: RepositoryId): Set<BranchReference>
2937

3038
/**
3139
* Same as [removeBranches] but blocking.
3240
* Caller is expected to execute it outside the request thread.
3341
*/
34-
fun removeBranchesBlocking(repository: RepositoryId, branchNames: Set<String>)
35-
suspend fun getVersion(branch: BranchReference): CLVersion?
36-
suspend fun getVersion(repository: RepositoryId, versionHash: String): CLVersion?
37-
suspend fun getVersionHash(branch: BranchReference): String?
42+
@RequiresTransaction
43+
fun removeBranches(repository: RepositoryId, branchNames: Set<String>)
44+
45+
@RequiresTransaction
46+
fun getVersion(branch: BranchReference): CLVersion?
47+
fun getVersion(repository: RepositoryId, versionHash: String): CLVersion?
48+
49+
@RequiresTransaction
50+
fun getVersionHash(branch: BranchReference): String?
3851
suspend fun pollVersionHash(branch: BranchReference, lastKnown: String?): String
39-
suspend fun mergeChanges(branch: BranchReference, newVersionHash: String): String
4052

41-
/**
42-
* Same as [mergeChanges] but blocking.
43-
* Caller is expected to execute it outside the request thread.
44-
*/
45-
fun mergeChangesBlocking(branch: BranchReference, newVersionHash: String): String
53+
@RequiresTransaction
54+
fun mergeChanges(branch: BranchReference, newVersionHash: String): String
4655
suspend fun computeDelta(repository: RepositoryId?, versionHash: String, baseVersionHash: String?): ObjectData
4756

4857
/**
@@ -54,14 +63,16 @@ interface IRepositoriesManager {
5463
fun isIsolated(repository: RepositoryId): Boolean?
5564

5665
fun getStoreManager(): StoreManager
66+
fun getTransactionManager(): ITransactionManager
5767
}
5868

69+
@RequiresTransaction
5970
fun IRepositoriesManager.getBranchNames(repositoryId: RepositoryId): Set<String> {
6071
return getBranches(repositoryId).map { it.branchName }.toSet()
6172
}
6273

63-
fun IRepositoriesManager.getStoreClient(repository: RepositoryId?): IStoreClient {
64-
return getStoreManager().getStoreClient(repository?.takeIf { isIsolated(it) ?: false })
74+
fun IRepositoriesManager.getStoreClient(repository: RepositoryId?, immutable: Boolean): IStoreClient {
75+
return getStoreManager().getStoreClient(repository?.takeIf { isIsolated(it) ?: false }, immutable)
6576
}
6677

6778
fun IRepositoriesManager.getAsyncStore(repository: RepositoryId?): IAsyncObjectStore {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import io.ktor.server.routing.routing
99
import io.ktor.util.pipeline.PipelineContext
1010
import org.modelix.authorization.getUserName
1111
import org.modelix.authorization.requiresLogin
12+
import org.modelix.model.server.store.RequiresTransaction
13+
import org.modelix.model.server.store.runReadIO
1214

1315
/**
1416
* Implementation of the REST API that is responsible for handling client and server IDs.
@@ -24,7 +26,11 @@ class IdsApiImpl(
2426
//
2527
// Functionally, it does not matter if the server ID is created eagerly or lazily,
2628
// as long as the same server ID is returned from the same server.
27-
val serverId = repositoriesManager.maybeInitAndGetSeverId()
29+
val serverId =
30+
@OptIn(RequiresTransaction::class)
31+
repositoriesManager.getTransactionManager().runReadIO {
32+
repositoriesManager.maybeInitAndGetSeverId()
33+
}
2834
call.respondText(serverId)
2935
}
3036

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

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import io.ktor.server.resources.put
1313
import io.ktor.server.response.respondText
1414
import io.ktor.server.routing.routing
1515
import io.ktor.util.pipeline.PipelineContext
16-
import kotlinx.coroutines.runBlocking
1716
import kotlinx.html.br
1817
import kotlinx.html.div
1918
import kotlinx.html.h1
@@ -29,9 +28,11 @@ import org.modelix.model.lazy.BranchReference
2928
import org.modelix.model.persistent.HashUtil
3029
import org.modelix.model.server.ModelServerPermissionSchema
3130
import org.modelix.model.server.store.ObjectInRepository
31+
import org.modelix.model.server.store.RequiresTransaction
3232
import org.modelix.model.server.store.StoreManager
3333
import org.modelix.model.server.store.pollEntry
34-
import org.modelix.model.server.store.runTransactionSuspendable
34+
import org.modelix.model.server.store.runReadIO
35+
import org.modelix.model.server.store.runWriteIO
3536
import org.modelix.model.server.templates.PageWithMenuBar
3637
import java.io.IOException
3738
import java.util.*
@@ -61,7 +62,8 @@ class KeyValueLikeModelServer(
6162
// request to initialize it lazily, would make the code less robust.
6263
// Each change in the logic of RepositoriesManager#maybeInitAndGetSeverId would need
6364
// the special conditions in the affected requests to be updated.
64-
runBlocking { repositoriesManager.maybeInitAndGetSeverId() }
65+
@OptIn(RequiresTransaction::class)
66+
repositoriesManager.getTransactionManager().runWrite { repositoriesManager.maybeInitAndGetSeverId() }
6567
application.apply {
6668
modelServerModule()
6769
}
@@ -89,7 +91,8 @@ class KeyValueLikeModelServer(
8991
get<Paths.getKeyGet> {
9092
val key = call.parameters["key"]!!
9193
checkKeyPermission(key, EPermissionType.READ)
92-
val value = stores.getGlobalKeyValueStore()[key]
94+
@OptIn(RequiresTransaction::class)
95+
val value = runRead { stores.getGlobalStoreClient()[key] }
9396
respondValue(key, value)
9497
}
9598
get<Paths.pollKeyGet> {
@@ -106,21 +109,25 @@ class KeyValueLikeModelServer(
106109
post<Paths.counterKeyPost> {
107110
val key = call.parameters["key"]!!
108111
checkKeyPermission(key, EPermissionType.WRITE)
109-
val value = stores.getGlobalStoreClient().generateId(key)
112+
val value = stores.getGlobalStoreClient(false).generateId(key)
110113
call.respondText(text = value.toString())
111114
}
112115

113116
get<Paths.getRecursivelyKeyGet> {
114117
val key = call.parameters["key"]!!
115118
checkKeyPermission(key, EPermissionType.READ)
116-
call.respondText(collect(key, this).toString(2), contentType = ContentType.Application.Json)
119+
@OptIn(RequiresTransaction::class)
120+
call.respondText(runRead { collect(key, this) }.toString(2), contentType = ContentType.Application.Json)
117121
}
118122

119123
put<Paths.putKeyPut> {
120124
val key = call.parameters["key"]!!
121125
val value = call.receiveText()
122126
try {
123-
putEntries(mapOf(key to value))
127+
@OptIn(RequiresTransaction::class)
128+
runWrite {
129+
putEntries(mapOf(key to value))
130+
}
124131
call.respondText("OK")
125132
} catch (e: NotFoundException) {
126133
throw HttpException(HttpStatusCode.NotFound, title = "Not found", details = e.message, cause = e)
@@ -139,7 +146,10 @@ class KeyValueLikeModelServer(
139146
}
140147
entries = sortByDependency(entries)
141148
try {
142-
putEntries(entries)
149+
@OptIn(RequiresTransaction::class)
150+
runWrite {
151+
putEntries(entries)
152+
}
143153
call.respondText(entries.size.toString() + " entries written")
144154
} catch (e: NotFoundException) {
145155
throw HttpException(HttpStatusCode.NotFound, title = "Not found", details = e.message, cause = e)
@@ -158,7 +168,8 @@ class KeyValueLikeModelServer(
158168
checkKeyPermission(key, EPermissionType.READ)
159169
keys.add(key)
160170
}
161-
val values = stores.getGlobalStoreClient().getAll(keys)
171+
@OptIn(RequiresTransaction::class)
172+
val values = runRead { stores.getGlobalStoreClient(false).getAll(keys) }
162173
for (i in keys.indices) {
163174
val respEntry = JSONObject()
164175
respEntry.put("key", keys[i])
@@ -199,6 +210,7 @@ class KeyValueLikeModelServer(
199210
return sorted
200211
}
201212

213+
@RequiresTransaction
202214
fun collect(rootKey: String, callContext: CallContext?): JSONArray {
203215
val result = JSONArray()
204216
val processed: MutableSet<String> = HashSet()
@@ -210,7 +222,7 @@ class KeyValueLikeModelServer(
210222
if (callContext != null) {
211223
keys.forEach { callContext.checkKeyPermission(it, EPermissionType.READ) }
212224
}
213-
val values = stores.getGlobalStoreClient().getAll(keys)
225+
val values = stores.getGlobalStoreClient(false).getAll(keys)
214226
for (i in keys.indices) {
215227
val key = keys[i]
216228
val value = values[i]
@@ -240,7 +252,8 @@ class KeyValueLikeModelServer(
240252
return result
241253
}
242254

243-
private suspend fun CallContext.putEntries(newEntries: Map<String, String?>) {
255+
@RequiresTransaction
256+
private fun CallContext.putEntries(newEntries: Map<String, String?>) {
244257
val referencedKeys: MutableSet<String> = HashSet()
245258
for ((key, value) in newEntries) {
246259
checkKeyPermission(key, EPermissionType.WRITE)
@@ -300,17 +313,15 @@ class KeyValueLikeModelServer(
300313
// We could try to move the objects later, but since this API is deprecated, it's not worth the effort.
301314
}
302315

303-
stores.getGlobalStoreClient().runTransactionSuspendable {
304-
stores.genericStore.putAll(hashedObjects.mapKeys { ObjectInRepository.global(it.key) })
305-
stores.genericStore.putAll(userDefinedEntries.mapKeys { ObjectInRepository.global(it.key) })
306-
for ((branch, value) in branchChanges) {
307-
if (value == null) {
308-
checkPermission(ModelServerPermissionSchema.branch(branch).delete)
309-
repositoriesManager.removeBranchesBlocking(branch.repositoryId, setOf(branch.branchName))
310-
} else {
311-
checkPermission(ModelServerPermissionSchema.branch(branch).push)
312-
repositoriesManager.mergeChangesBlocking(branch, value)
313-
}
316+
stores.genericStore.putAll(hashedObjects.mapKeys { ObjectInRepository.global(it.key) })
317+
stores.genericStore.putAll(userDefinedEntries.mapKeys { ObjectInRepository.global(it.key) })
318+
for ((branch, value) in branchChanges) {
319+
if (value == null) {
320+
checkPermission(ModelServerPermissionSchema.branch(branch).delete)
321+
repositoriesManager.removeBranches(branch.repositoryId, setOf(branch.branchName))
322+
} else {
323+
checkPermission(ModelServerPermissionSchema.branch(branch).push)
324+
repositoriesManager.mergeChanges(branch, value)
314325
}
315326
}
316327
}
@@ -363,4 +374,12 @@ class KeyValueLikeModelServer(
363374
else -> unknown()
364375
}
365376
}
377+
378+
private suspend fun <R> runRead(body: () -> R): R {
379+
return repositoriesManager.getTransactionManager().runReadIO(body)
380+
}
381+
382+
private suspend fun <R> runWrite(body: () -> R): R {
383+
return repositoriesManager.getTransactionManager().runWriteIO(body)
384+
}
366385
}

0 commit comments

Comments
 (0)