diff --git a/model-server/src/main/kotlin/org/modelix/model/server/Main.kt b/model-server/src/main/kotlin/org/modelix/model/server/Main.kt index 0a0b1a4842..9b6c9f592c 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/Main.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/Main.kt @@ -58,6 +58,7 @@ import org.modelix.model.server.handlers.ui.RepositoryOverview import org.modelix.model.server.store.IgniteStoreClient import org.modelix.model.server.store.InMemoryStoreClient import org.modelix.model.server.store.IsolatingStore +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.store.forGlobalRepository import org.modelix.model.server.store.loadDump import org.modelix.model.server.store.writeDump @@ -149,9 +150,12 @@ object Main { } var i = 0 val globalStoreClient = storeClient.forGlobalRepository() - while (i < cmdLineArgs.setValues.size) { - globalStoreClient.put(cmdLineArgs.setValues[i], cmdLineArgs.setValues[i + 1]) - i += 2 + @OptIn(RequiresTransaction::class) + globalStoreClient.getTransactionManager().runWrite { + while (i < cmdLineArgs.setValues.size) { + globalStoreClient.put(cmdLineArgs.setValues[i], cmdLineArgs.setValues[i + 1]) + i += 2 + } } val repositoriesManager = RepositoriesManager(storeClient) val modelServer = KeyValueLikeModelServer(repositoriesManager) diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/HealthApiImpl.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/HealthApiImpl.kt index 0e7ee44afb..d988fab50a 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/handlers/HealthApiImpl.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/HealthApiImpl.kt @@ -7,6 +7,7 @@ import io.ktor.server.application.call import io.ktor.server.response.respondText import io.ktor.util.pipeline.PipelineContext import org.modelix.model.server.handlers.KeyValueLikeModelServer.Companion.PROTECTED_PREFIX +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.store.StoreManager class HealthApiImpl( @@ -25,6 +26,7 @@ class HealthApiImpl( private fun isHealthy(): Boolean { val store = stores.getGlobalStoreClient() + @OptIn(RequiresTransaction::class) return store.getTransactionManager().runWrite { val value = toLong(store[HEALTH_KEY]) + 1 store.put(HEALTH_KEY, java.lang.Long.toString(value)) diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/IRepositoriesManager.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/IRepositoriesManager.kt index 9ef7ed81f9..f11e0226ca 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/handlers/IRepositoriesManager.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/IRepositoriesManager.kt @@ -7,6 +7,7 @@ import org.modelix.model.lazy.IDeserializingKeyValueStore import org.modelix.model.lazy.RepositoryId import org.modelix.model.server.store.IStoreClient import org.modelix.model.server.store.ITransactionManager +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.store.StoreManager interface IRepositoriesManager { @@ -19,29 +20,38 @@ interface IRepositoriesManager { * If the server ID was created previously but is only stored under a legacy database key, * it also gets stored under the current and all legacy database keys. */ + @RequiresTransaction fun maybeInitAndGetSeverId(): String + + @RequiresTransaction fun getRepositories(): Set + + @RequiresTransaction fun createRepository(repositoryId: RepositoryId, userName: String?, useRoleIds: Boolean = true, legacyGlobalStorage: Boolean = false): CLVersion + + @RequiresTransaction fun removeRepository(repository: RepositoryId): Boolean + @RequiresTransaction fun getBranches(repositoryId: RepositoryId): Set /** * Same as [removeBranches] but blocking. * Caller is expected to execute it outside the request thread. */ + @RequiresTransaction fun removeBranches(repository: RepositoryId, branchNames: Set) + + @RequiresTransaction fun getVersion(branch: BranchReference): CLVersion? fun getVersion(repository: RepositoryId, versionHash: String): CLVersion? + + @RequiresTransaction fun getVersionHash(branch: BranchReference): String? suspend fun pollVersionHash(branch: BranchReference, lastKnown: String?): String - fun mergeChanges(branch: BranchReference, newVersionHash: String): String - /** - * Same as [mergeChanges] but blocking. - * Caller is expected to execute it outside the request thread. - */ - fun mergeChangesBlocking(branch: BranchReference, newVersionHash: String): String + @RequiresTransaction + fun mergeChanges(branch: BranchReference, newVersionHash: String): String suspend fun computeDelta(repository: RepositoryId?, versionHash: String, baseVersionHash: String?): ObjectData /** @@ -56,6 +66,7 @@ interface IRepositoriesManager { fun getTransactionManager(): ITransactionManager } +@RequiresTransaction fun IRepositoriesManager.getBranchNames(repositoryId: RepositoryId): Set { return getBranches(repositoryId).map { it.branchName }.toSet() } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/IdsApiImpl.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/IdsApiImpl.kt index ffe473482f..27c1eee672 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/handlers/IdsApiImpl.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/IdsApiImpl.kt @@ -9,6 +9,8 @@ import io.ktor.server.routing.routing import io.ktor.util.pipeline.PipelineContext import org.modelix.authorization.getUserName import org.modelix.authorization.requiresLogin +import org.modelix.model.server.store.RequiresTransaction +import org.modelix.model.server.store.runReadIO /** * Implementation of the REST API that is responsible for handling client and server IDs. @@ -24,7 +26,11 @@ class IdsApiImpl( // // Functionally, it does not matter if the server ID is created eagerly or lazily, // as long as the same server ID is returned from the same server. - val serverId = repositoriesManager.maybeInitAndGetSeverId() + val serverId = + @OptIn(RequiresTransaction::class) + repositoriesManager.getTransactionManager().runReadIO { + repositoriesManager.maybeInitAndGetSeverId() + } call.respondText(serverId) } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/KeyValueLikeModelServer.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/KeyValueLikeModelServer.kt index 0916fd45a2..3cecbdd4b4 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/handlers/KeyValueLikeModelServer.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/KeyValueLikeModelServer.kt @@ -28,6 +28,7 @@ import org.modelix.model.lazy.BranchReference import org.modelix.model.persistent.HashUtil import org.modelix.model.server.ModelServerPermissionSchema import org.modelix.model.server.store.ObjectInRepository +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.store.StoreManager import org.modelix.model.server.store.pollEntry import org.modelix.model.server.store.runReadIO @@ -61,6 +62,7 @@ class KeyValueLikeModelServer( // request to initialize it lazily, would make the code less robust. // Each change in the logic of RepositoriesManager#maybeInitAndGetSeverId would need // the special conditions in the affected requests to be updated. + @OptIn(RequiresTransaction::class) repositoriesManager.getTransactionManager().runWrite { repositoriesManager.maybeInitAndGetSeverId() } application.apply { modelServerModule() @@ -89,6 +91,7 @@ class KeyValueLikeModelServer( get { val key = call.parameters["key"]!! checkKeyPermission(key, EPermissionType.READ) + @OptIn(RequiresTransaction::class) val value = runRead { stores.getGlobalStoreClient()[key] } respondValue(key, value) } @@ -113,6 +116,7 @@ class KeyValueLikeModelServer( get { val key = call.parameters["key"]!! checkKeyPermission(key, EPermissionType.READ) + @OptIn(RequiresTransaction::class) call.respondText(runRead { collect(key, this) }.toString(2), contentType = ContentType.Application.Json) } @@ -120,6 +124,7 @@ class KeyValueLikeModelServer( val key = call.parameters["key"]!! val value = call.receiveText() try { + @OptIn(RequiresTransaction::class) runWrite { putEntries(mapOf(key to value)) } @@ -141,6 +146,7 @@ class KeyValueLikeModelServer( } entries = sortByDependency(entries) try { + @OptIn(RequiresTransaction::class) runWrite { putEntries(entries) } @@ -162,6 +168,7 @@ class KeyValueLikeModelServer( checkKeyPermission(key, EPermissionType.READ) keys.add(key) } + @OptIn(RequiresTransaction::class) val values = runRead { stores.getGlobalStoreClient(false).getAll(keys) } for (i in keys.indices) { val respEntry = JSONObject() @@ -203,6 +210,7 @@ class KeyValueLikeModelServer( return sorted } + @RequiresTransaction fun collect(rootKey: String, callContext: CallContext?): JSONArray { val result = JSONArray() val processed: MutableSet = HashSet() @@ -244,6 +252,7 @@ class KeyValueLikeModelServer( return result } + @RequiresTransaction private fun CallContext.putEntries(newEntries: Map) { val referencedKeys: MutableSet = HashSet() for ((key, value) in newEntries) { @@ -312,7 +321,7 @@ class KeyValueLikeModelServer( repositoriesManager.removeBranches(branch.repositoryId, setOf(branch.branchName)) } else { checkPermission(ModelServerPermissionSchema.branch(branch).push) - repositoriesManager.mergeChangesBlocking(branch, value) + repositoriesManager.mergeChanges(branch, value) } } } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/ModelReplicationServer.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/ModelReplicationServer.kt index 03c2a265de..149a9102c8 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/handlers/ModelReplicationServer.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/ModelReplicationServer.kt @@ -48,6 +48,7 @@ import org.modelix.model.server.api.v2.ImmutableObjectsStream import org.modelix.model.server.api.v2.VersionDelta import org.modelix.model.server.api.v2.VersionDeltaStream import org.modelix.model.server.api.v2.VersionDeltaStreamV2 +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.store.StoreManager import org.modelix.model.server.store.runReadIO import org.modelix.model.server.store.runWriteIO @@ -91,6 +92,7 @@ class ModelReplicationServer( override suspend fun PipelineContext.getRepositories() { call.respondText( + @OptIn(RequiresTransaction::class) runRead { repositoriesManager.getRepositories() } .filter { call.hasPermission(ModelServerPermissionSchema.repository(it).list) } .joinToString("\n") { it.id }, @@ -99,6 +101,7 @@ class ModelReplicationServer( override suspend fun PipelineContext.getRepositoryBranches(repository: String) { call.respondText( + @OptIn(RequiresTransaction::class) runRead { repositoriesManager.getBranchNames(repositoryId(repository)) } .filter { call.hasPermission(ModelServerPermissionSchema.repository(repository).branch(it).list) } .joinToString("\n"), @@ -112,6 +115,8 @@ class ModelReplicationServer( ) { checkPermission(ModelServerPermissionSchema.repository(repository).branch(branch).pull) val branchRef = repositoryId(repository).getBranchReference(branch) + + @OptIn(RequiresTransaction::class) val versionHash = runRead { repositoriesManager.getVersionHash(branchRef) ?: throw BranchNotFoundException(branchRef) } @@ -125,7 +130,11 @@ class ModelReplicationServer( ) { checkPermission(ModelServerPermissionSchema.repository(repository).branch(branch).pull) val branchRef = repositoryId(repository).getBranchReference(branch) - val versionHash = runRead { repositoriesManager.getVersionHash(branchRef) ?: throw BranchNotFoundException(branchRef) } + + @OptIn(RequiresTransaction::class) + val versionHash = runRead { + repositoriesManager.getVersionHash(branchRef) ?: throw BranchNotFoundException(branchRef) + } call.respond(BranchV1(branch, versionHash)) } @@ -141,6 +150,7 @@ class ModelReplicationServer( checkPermission(ModelServerPermissionSchema.repository(repositoryId).branch(branch).delete) + @OptIn(RequiresTransaction::class) runWrite { if (!repositoriesManager.getBranchNames(repositoryId).contains(branch)) { throw BranchNotFoundException(branch, repositoryId.id) @@ -158,7 +168,11 @@ class ModelReplicationServer( ) { checkPermission(ModelServerPermissionSchema.repository(repository).branch(branch).pull) val branchRef = repositoryId(repository).getBranchReference(branch) - val versionHash = runRead { repositoriesManager.getVersionHash(branchRef) ?: throw BranchNotFoundException(branchRef) } + + @OptIn(RequiresTransaction::class) + val versionHash = runRead { + repositoriesManager.getVersionHash(branchRef) ?: throw BranchNotFoundException(branchRef) + } call.respondText(versionHash) } @@ -168,6 +182,7 @@ class ModelReplicationServer( legacyGlobalStorage: Boolean?, ) { checkPermission(ModelServerPermissionSchema.repository(repository).create) + @OptIn(RequiresTransaction::class) val initialVersion = runWrite { repositoriesManager.createRepository( repositoryId(repository), @@ -182,6 +197,7 @@ class ModelReplicationServer( override suspend fun PipelineContext.deleteRepository(repository: String) { checkPermission(ModelServerPermissionSchema.repository(repository).delete) + @OptIn(RequiresTransaction::class) val foundAndDeleted = runWrite { repositoriesManager.removeRepository(repositoryId(repository)) } @@ -200,7 +216,9 @@ class ModelReplicationServer( val branchRef = repositoryId(repository).getBranchReference(branch) val deltaFromClient = call.receive() deltaFromClient.checkObjectHashes() + @OptIn(RequiresTransaction::class) // no transactions required for immutable store repositoriesManager.getStoreClient(RepositoryId(repository), true).putAll(deltaFromClient.getAllObjects()) + @OptIn(RequiresTransaction::class) val mergedHash = runWrite { repositoriesManager.mergeChanges(branchRef, deltaFromClient.versionHash) } @@ -228,6 +246,7 @@ class ModelReplicationServer( } val objects = withContext(Dispatchers.IO) { + @OptIn(RequiresTransaction::class) // no transactions required for immutable store repositoriesManager.getStoreClient(RepositoryId(repository), true).getAll(keys) } @@ -271,6 +290,7 @@ class ModelReplicationServer( ) { val branchRef = repositoryId(repository).getBranchReference(branchName) checkPermission(ModelServerPermissionSchema.branch(branchRef).query) + @OptIn(RequiresTransaction::class) val version = runRead { repositoriesManager.getVersion(branchRef) ?: throw BranchNotFoundException(branchRef) } LOG.trace("Running query on {} @ {}", branchRef, version) val initialTree = version.getTree() @@ -301,6 +321,7 @@ class ModelReplicationServer( baseVersion = version, operations = ops.map { it.getOriginalOp() }.toTypedArray(), ) + @OptIn(RequiresTransaction::class) runWrite { repositoriesManager.mergeChanges(branchRef, newVersion.getContentHash()) } @@ -343,6 +364,7 @@ class ModelReplicationServer( } withContext(Dispatchers.IO) { + @OptIn(RequiresTransaction::class) // no transactions required for immutable store repositoriesManager.getStoreClient(RepositoryId(repository), true).putAll(entries, true) } call.respondText("${entries.size} objects received") @@ -354,6 +376,7 @@ class ModelReplicationServer( lastKnown: String?, ) { checkPermission(ModelServerPermissionSchema.legacyGlobalObjects.read) + @OptIn(RequiresTransaction::class) if (runRead { stores.getGlobalStoreClient()[versionHash] } == null) { throw VersionNotFoundException(versionHash) } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/RepositoriesManager.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/RepositoriesManager.kt index dd303cb9c4..366ed77f2c 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/handlers/RepositoriesManager.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/RepositoriesManager.kt @@ -35,6 +35,7 @@ import org.modelix.model.server.api.v2.toMap import org.modelix.model.server.store.IRepositoryAwareStore import org.modelix.model.server.store.ITransactionManager import org.modelix.model.server.store.ObjectInRepository +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.store.StoreManager import org.modelix.model.server.store.assertWrite import org.modelix.model.server.store.pollEntry @@ -49,11 +50,10 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { constructor(store: IRepositoryAwareStore) : this(StoreManager(store)) init { + @RequiresTransaction fun migrateLegacyRepositoriesList(infoBranch: IBranch) { val legacyRepositories = listLegacyRepositories(infoBranch).groupBy { it.repositoryId } if (legacyRepositories.isNotEmpty()) { - // To not use `runTransactionSuspendable` like everywhere else, - // because this is blocking initialization code anyways. ensureRepositoriesAreInList(legacyRepositories.keys) for ((legacyRepository, legacyBranches) in legacyRepositories) { ensureBranchesAreInList(legacyRepository, legacyBranches.map { it.branchName }.toSet()) @@ -62,6 +62,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { } fun doMigrations() { + @OptIn(RequiresTransaction::class) stores.getTransactionManager().runWrite { val repositoryId = RepositoryId("info") val v1BranchKey = repositoryId.getBranchReference().getKey() @@ -96,6 +97,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { * If the server ID was created previously but is only stored under a legacy database key, * it also gets stored under the current and all legacy database keys. */ + @RequiresTransaction override fun maybeInitAndGetSeverId(): String { val store = stores.getGlobalStoreClient() var serverId = store[SERVER_ID_KEY] @@ -110,10 +112,12 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { return serverId } + @RequiresTransaction override fun getRepositories(): Set { return getRepositories(false) + getRepositories(true) } + @RequiresTransaction fun getRepositories(isolated: Boolean): Set { val repositoriesList = stores.genericStore[ObjectInRepository.global(repositoriesListKey(isolated))] val emptyRepositoriesList = repositoriesList.isNullOrBlank() @@ -131,6 +135,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { override fun isIsolated(repository: RepositoryId): Boolean? { // The repository might not exist, but new repositories will be created with isolated storage. // If a repository is not part of the legacy ones it's considered isolated. + @OptIn(RequiresTransaction::class) return stores.getTransactionManager().runRead { if (getRepositories(true).contains(repository)) return@runRead true if (getRepositories(false).contains(repository)) return@runRead false @@ -138,8 +143,10 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { } } + @OptIn(RequiresTransaction::class) private fun repositoryExists(repositoryId: RepositoryId) = getRepositories().contains(repositoryId) + @RequiresTransaction override fun createRepository( repositoryId: RepositoryId, userName: String?, @@ -170,10 +177,12 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { return initialVersion } + @RequiresTransaction fun getBranchNames(repositoryId: RepositoryId): Set { return stores.genericStore[branchListKey(repositoryId)]?.ifEmpty { null }?.lines()?.toSet().orEmpty() } + @RequiresTransaction override fun getBranches(repositoryId: RepositoryId): Set { return getBranchNames(repositoryId) .map { repositoryId.getBranchReference(it) } @@ -181,9 +190,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { .toSet() } - /** - * Must be executed inside a transaction - */ + @RequiresTransaction private fun ensureRepositoriesAreInList(repositoryIds: Set) { if (repositoryIds.isEmpty()) return val missingRepositories = repositoryIds - getRepositories() @@ -194,9 +201,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { } } - /** - * Must be executed inside a transaction - */ + @RequiresTransaction private fun ensureBranchesAreInList(repository: RepositoryId, branchNames: Set) { if (branchNames.isEmpty()) return val key = branchListKey(repository) @@ -207,14 +212,13 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { } } - /** - * Must be executed inside a transaction - */ + @RequiresTransaction private fun ensureBranchInList(branch: BranchReference) { ensureRepositoriesAreInList(setOf(branch.repositoryId)) ensureBranchesAreInList(branch.repositoryId, setOf(branch.branchName)) } + @RequiresTransaction override fun removeRepository(repository: RepositoryId): Boolean { val genericStore = stores.genericStore @@ -234,10 +238,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { return true } - /** - * Same as [removeBranches] but blocking. - * Caller is expected to execute it outside the request thread. - */ + @RequiresTransaction override fun removeBranches(repository: RepositoryId, branchNames: Set) { if (branchNames.isEmpty()) return val key = branchListKey(repository) @@ -249,15 +250,8 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { } } + @RequiresTransaction override fun mergeChanges(branch: BranchReference, newVersionHash: String): String { - return mergeChangesBlocking(branch, newVersionHash) - } - - /** - * Same as [mergeChanges] but blocking. - * Caller is expected to execute it outside the request thread. - */ - override fun mergeChangesBlocking(branch: BranchReference, newVersionHash: String): String { val headHash = getVersionHash(branch) val mergedHash = if (headHash == null) { newVersionHash @@ -278,6 +272,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { return mergedHash } + @RequiresTransaction override fun getVersion(branch: BranchReference): CLVersion? { return getVersionHash(branch)?.let { getVersion(branch.repositoryId, it) } } @@ -287,10 +282,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { return CLVersion.tryLoadFromHash(versionHash, legacyObjectStore) } - /** - * Same as [getVersionHash] but blocking. - * Caller is expected to execute it outside the request thread. - */ + @RequiresTransaction override fun getVersionHash(branch: BranchReference): String? { val isolated = isIsolated(branch.repositoryId) if (isolated == null) { @@ -304,6 +296,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager { } } + @RequiresTransaction private fun putVersionHash(branch: BranchReference, hash: String?) { val isolated = isIsolated(branch.repositoryId) ?: false stores.genericStore.put(branchKey(branch, isolated), hash, false) diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/ContentExplorer.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/ContentExplorer.kt index 488e10aaee..c4fd9d0a40 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/ContentExplorer.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/ContentExplorer.kt @@ -52,13 +52,13 @@ import org.modelix.model.api.PNodeAdapter import org.modelix.model.api.TreePointer import org.modelix.model.lazy.BranchReference import org.modelix.model.lazy.CLTree -import org.modelix.model.lazy.CLVersion import org.modelix.model.lazy.RepositoryId import org.modelix.model.server.ModelServerPermissionSchema import org.modelix.model.server.handlers.IRepositoriesManager import org.modelix.model.server.handlers.NodeNotFoundException -import org.modelix.model.server.handlers.getLegacyObjectStore +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.store.StoreManager +import org.modelix.model.server.store.runReadIO import org.modelix.model.server.templates.PageWithMenuBar class ContentExplorer(private val repoManager: IRepositoriesManager) { @@ -84,12 +84,15 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) { } call.checkPermission(ModelServerPermissionSchema.repository(repository).branch(branch).pull) - val latestVersion = repoManager.getVersion(BranchReference(RepositoryId(repository), branch)) + @OptIn(RequiresTransaction::class) + val latestVersion = repoManager.getTransactionManager().runReadIO { + repoManager.getVersionHash(BranchReference(RepositoryId(repository), branch)) + } if (latestVersion == null) { call.respondText("unable to find latest version", status = HttpStatusCode.InternalServerError) return@get } else { - call.respondRedirect("../../../versions/${latestVersion.getContentHash()}/") + call.respondRedirect("../../../versions/$latestVersion/") } } get("/content/repositories/{repository}/versions/{versionHash}") { @@ -111,7 +114,14 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) { it.toLongOrNull() ?: return@get call.respondText("Invalid expandTo value. Provide a node id.", status = HttpStatusCode.BadRequest) } - val tree = CLVersion.loadFromHash(versionHash, repoManager.getLegacyObjectStore(repositoryId)).getTree() + val version = repoManager.getTransactionManager().runReadIO { + repoManager.getVersion(repositoryId, versionHash) + } + if (version == null) { + call.respondText("version $versionHash not found", status = HttpStatusCode.NotFound) + return@get + } + val tree = version.getTree() val rootNode = PNodeAdapter(ITree.ROOT_ID, TreePointer(tree)) val expandedNodes = expandTo?.let { nodeId -> getAncestorsAndSelf(nodeId, tree) }.orEmpty() @@ -152,7 +162,14 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) { val expandedNodes = call.receive() - val tree = CLVersion.loadFromHash(versionHash, stores.getLegacyObjectStore(repositoryId)).getTree() + val version = repoManager.getTransactionManager().runReadIO { + repoManager.getVersion(repositoryId, versionHash) + } + if (version == null) { + call.respondText("version $versionHash not found", status = HttpStatusCode.NotFound) + return@post + } + val tree = version.getTree() val rootNode = PNodeAdapter(ITree.ROOT_ID, TreePointer(tree)) var expandedNodeIds = expandedNodes.expandedNodeIds @@ -180,12 +197,13 @@ class ContentExplorer(private val repoManager: IRepositoriesManager) { call.checkPermission(ModelServerPermissionSchema.repository(repositoryId).objects.read) - val version = try { - CLVersion.loadFromHash(versionHash, stores.getLegacyObjectStore(RepositoryId(repositoryId))) - } catch (ex: RuntimeException) { - return@get call.respondText("version not found", status = HttpStatusCode.NotFound) + val version = repoManager.getTransactionManager().runReadIO { + repoManager.getVersion(RepositoryId(repositoryId), versionHash) + } + if (version == null) { + call.respondText("version $versionHash not found", status = HttpStatusCode.NotFound) + return@get } - val node = PNodeAdapter(id, TreePointer(version.getTree())).takeIf { it.isValid } if (node != null) { diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/DiffView.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/DiffView.kt index 8c008c19e9..d1b9f28b7c 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/DiffView.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/DiffView.kt @@ -44,6 +44,7 @@ import org.modelix.model.server.ModelServerPermissionSchema import org.modelix.model.server.handlers.HttpException import org.modelix.model.server.handlers.RepositoriesManager import org.modelix.model.server.handlers.VersionNotFoundException +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.templates.PageWithMenuBar import org.modelix.streams.getSynchronous @@ -70,10 +71,11 @@ class DiffView(private val repositoryManager: RepositoriesManager) { application.routing { requiresLogin { get("/diff") { - val visibleRepositories = repositoryManager.getRepositories().filter { - call.hasPermission(ModelServerPermissionSchema.repository(it).list) - } + @OptIn(RequiresTransaction::class) call.respondHtmlTemplate(PageWithMenuBar("diff", "..")) { + val visibleRepositories = repositoryManager.getRepositories().filter { + call.hasPermission(ModelServerPermissionSchema.repository(it).list) + } bodyContent { buildDiffInputPage(visibleRepositories) } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/HistoryHandler.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/HistoryHandler.kt index 78059cb68b..edffda64ef 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/HistoryHandler.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/HistoryHandler.kt @@ -53,7 +53,10 @@ import org.modelix.model.server.ModelServerPermissionSchema import org.modelix.model.server.handlers.BranchNotFoundException import org.modelix.model.server.handlers.IRepositoriesManager import org.modelix.model.server.handlers.getLegacyObjectStore +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.store.StoreManager +import org.modelix.model.server.store.runReadIO +import org.modelix.model.server.store.runWriteIO import org.modelix.model.server.templates.PageWithMenuBar import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -75,7 +78,11 @@ class HistoryHandler(private val repositoriesManager: IRepositoriesManager) { val params = call.request.queryParameters val limit = toInt(params["limit"], 500) val skip = toInt(params["skip"], 0) - val latestVersion = repositoriesManager.getVersion(branch) + val latestVersion = + @OptIn(RequiresTransaction::class) + repositoriesManager.getTransactionManager().runReadIO { + repositoriesManager.getVersion(branch) + } checkNotNull(latestVersion) { "Branch not found: $branch" } call.respondHtmlTemplate(PageWithMenuBar("repos/", "../../..")) { headContent { @@ -105,7 +112,10 @@ class HistoryHandler(private val repositoriesManager: IRepositoriesManager) { val fromVersion = params["from"]!! val toVersion = params["to"]!! val user = getUserName() - revert(branch, fromVersion, toVersion, user) + @OptIn(RequiresTransaction::class) + repositoriesManager.getTransactionManager().runWriteIO { + revert(branch, fromVersion, toVersion, user) + } call.respondRedirect(".") } } @@ -118,7 +128,8 @@ class HistoryHandler(private val repositoriesManager: IRepositoriesManager) { } } - private suspend fun revert(repositoryAndBranch: BranchReference, from: String?, to: String?, author: String?) { + @RequiresTransaction + private fun revert(repositoryAndBranch: BranchReference, from: String?, to: String?, author: String?) { val version = repositoriesManager.getVersion(repositoryAndBranch) ?: throw BranchNotFoundException(repositoryAndBranch) val branch = OTBranch(PBranch(version.getTree(), stores.idGenerator), stores.idGenerator, repositoriesManager.getLegacyObjectStore(repositoryAndBranch.repositoryId)) branch.runWriteT { t -> diff --git a/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/RepositoryOverview.kt b/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/RepositoryOverview.kt index 0bce40a287..972215e340 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/RepositoryOverview.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/handlers/ui/RepositoryOverview.kt @@ -1,22 +1,31 @@ package org.modelix.model.server.handlers.ui +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.content.TextContent import io.ktor.http.encodeURLPathPart +import io.ktor.http.withCharset import io.ktor.server.application.Application import io.ktor.server.application.ApplicationCall import io.ktor.server.application.call -import io.ktor.server.html.respondHtmlTemplate +import io.ktor.server.html.Template +import io.ktor.server.response.respond import io.ktor.server.routing.get import io.ktor.server.routing.routing +import io.ktor.utils.io.charsets.Charsets import kotlinx.html.FlowContent import kotlinx.html.FlowOrInteractiveOrPhrasingContent +import kotlinx.html.HTML import kotlinx.html.a import kotlinx.html.button import kotlinx.html.h1 +import kotlinx.html.html import kotlinx.html.i import kotlinx.html.onClick import kotlinx.html.p import kotlinx.html.script import kotlinx.html.span +import kotlinx.html.stream.createHTML import kotlinx.html.table import kotlinx.html.tbody import kotlinx.html.td @@ -30,15 +39,34 @@ import org.modelix.authorization.requiresLogin import org.modelix.model.lazy.RepositoryId import org.modelix.model.server.ModelServerPermissionSchema import org.modelix.model.server.handlers.IRepositoriesManager +import org.modelix.model.server.store.ITransactionManager +import org.modelix.model.server.store.RequiresTransaction +import org.modelix.model.server.store.runReadIO import org.modelix.model.server.templates.PageWithMenuBar +suspend fun > ApplicationCall.respondHtmlTemplateInTransaction( + transactionManager: ITransactionManager, + template: TTemplate, + status: HttpStatusCode = HttpStatusCode.OK, + body: TTemplate.() -> Unit, +) { + val html = transactionManager.runReadIO { + template.body() + createHTML().html { + with(template) { apply() } + } + } + respond(TextContent(html, ContentType.Text.Html.withCharset(Charsets.UTF_8), status)) +} + class RepositoryOverview(private val repoManager: IRepositoriesManager) { fun init(application: Application) { application.routing { requiresLogin { get("/repos") { - call.respondHtmlTemplate(PageWithMenuBar("repos/", "..")) { + @OptIn(RequiresTransaction::class) + call.respondHtmlTemplateInTransaction(repoManager.getTransactionManager(), PageWithMenuBar("repos/", "..")) { headContent { title("Repositories") script(type = "text/javascript") { @@ -68,6 +96,7 @@ class RepositoryOverview(private val repoManager: IRepositoriesManager) { } } + @RequiresTransaction private fun FlowContent.buildMainPage(call: ApplicationCall) { h1 { +"Choose Repository" } val repositories = repoManager.getRepositories().filter { call.hasPermission(ModelServerPermissionSchema.repository(it).list) } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/store/DumpStore.kt b/model-server/src/main/kotlin/org/modelix/model/server/store/DumpStore.kt index ab5b0c7fb7..632f29fa72 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/store/DumpStore.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/store/DumpStore.kt @@ -24,7 +24,10 @@ fun IsolatingStore.loadDump(file: File): Int { ObjectInRepository.global(it.key) } } - putAll(entries, silent = true) + @OptIn(RequiresTransaction::class) + getTransactionManager().runWrite { + putAll(entries, silent = true) + } } return n } @@ -32,17 +35,20 @@ fun IsolatingStore.loadDump(file: File): Int { @Synchronized @Throws(IOException::class) fun IsolatingStore.writeDump(file: File) { - file.writer().use { writer -> - for ((key, value) in getAll()) { - if (value == null) continue - writer.append(key.key) - if (!key.isGlobal()) { - writer.append(REPOSITORY_SEPARATOR) - writer.append(key.getRepositoryId()) + @OptIn(RequiresTransaction::class) + getTransactionManager().runRead { + file.writer().use { writer -> + for ((key, value) in getAll()) { + if (value == null) continue + writer.append(key.key) + if (!key.isGlobal()) { + writer.append(REPOSITORY_SEPARATOR) + writer.append(key.getRepositoryId()) + } + writer.append("#") + writer.append(value) + writer.append("\n") } - writer.append("#") - writer.append(value) - writer.append("\n") } } } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/store/IGenericStoreClient.kt b/model-server/src/main/kotlin/org/modelix/model/server/store/IGenericStoreClient.kt index 0ea28f1f33..442c8f7422 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/store/IGenericStoreClient.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/store/IGenericStoreClient.kt @@ -16,6 +16,7 @@ interface IRepositoryAwareStore : IsolatingStore { * * Callers need to ensure that the repository is not usable anymore before calling this method. */ + @RequiresTransaction fun removeRepositoryObjects(repositoryId: RepositoryId) { val keysToDelete = getAll().asSequence() .map { it.key } @@ -26,15 +27,28 @@ interface IRepositoryAwareStore : IsolatingStore { } interface IGenericStoreClient : AutoCloseable { + @RequiresTransaction operator fun get(key: KeyT): String? = getAll(listOf(key)).first() + + @RequiresTransaction fun getAll(keys: List): List { val entries = getAll(keys.toSet()) return keys.map { entries[it] } } + + @RequiresTransaction fun getIfCached(key: KeyT): String? + + @RequiresTransaction fun getAll(keys: Set): Map + + @RequiresTransaction fun getAll(): Map + + @RequiresTransaction fun put(key: KeyT, value: String?, silent: Boolean = false) = putAll(mapOf(key to value)) + + @RequiresTransaction fun putAll(entries: Map, silent: Boolean = false) fun listen(key: KeyT, listener: IGenericKeyListener) fun removeListener(key: KeyT, listener: IGenericKeyListener) diff --git a/model-server/src/main/kotlin/org/modelix/model/server/store/IImmutableStore.kt b/model-server/src/main/kotlin/org/modelix/model/server/store/IImmutableStore.kt index a559169d16..ac21f13fa9 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/store/IImmutableStore.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/store/IImmutableStore.kt @@ -15,18 +15,22 @@ interface IImmutableStore { fun IImmutableStore.asGenericStore() = ImmutableStoreAsGenericStore(this) class ImmutableStoreAsGenericStore(val store: IImmutableStore) : IGenericStoreClient { + @RequiresTransaction override fun getAll(keys: Set): Map { return store.getAll(keys) } + @RequiresTransaction override fun getAll(): Map { throw UnsupportedOperationException() } + @RequiresTransaction override fun getIfCached(key: KeyT): String? { return store.getIfCached(key) } + @RequiresTransaction override fun putAll(entries: Map, silent: Boolean) { store.addAll(entries.mapValues { requireNotNull(it.value) { "Deleting entries not allowed: $it" } }) } @@ -44,7 +48,7 @@ class ImmutableStoreAsGenericStore(val store: IImmutableStore) : IGe } override fun getTransactionManager(): ITransactionManager { - throw UnsupportedOperationException() + return NoTransactionManager() } override fun getImmutableStore(): IImmutableStore { diff --git a/model-server/src/main/kotlin/org/modelix/model/server/store/IStoreClient.kt b/model-server/src/main/kotlin/org/modelix/model/server/store/IStoreClient.kt index aafa4380e4..dc52578b0b 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/store/IStoreClient.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/store/IStoreClient.kt @@ -1,28 +1,14 @@ package org.modelix.model.server.store -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import org.modelix.model.IGenericKeyListener import kotlin.time.Duration.Companion.seconds interface IStoreClient : IGenericStoreClient -suspend fun StoreManager.runTransactionSuspendable(body: () -> T): T { - return genericStore.runTransactionSuspendable(body) -} - -suspend fun IsolatingStore.runTransactionSuspendable(body: () -> T): T { - return withContext(Dispatchers.IO) { runWriteTransaction(body) } -} - -suspend fun IStoreClient.runTransactionSuspendable(body: () -> T): T { - return withContext(Dispatchers.IO) { runWriteTransaction(body) } -} - suspend fun pollEntry(storeClient: IsolatingStore, key: ObjectInRepository, lastKnownValue: String?): String? { var result: String? = null coroutineScope { @@ -53,6 +39,7 @@ suspend fun pollEntry(storeClient: IsolatingStore, key: ObjectInRepository, last // known value. // Registering the listener without needing it is less // likely to happen. + @OptIn(RequiresTransaction::class) val value = storeClient.runReadTransaction { storeClient[key] } if (value != lastKnownValue) { callHandler(value) @@ -65,7 +52,10 @@ suspend fun pollEntry(storeClient: IsolatingStore, key: ObjectInRepository, last } finally { storeClient.removeListener(key, listener) } - if (!handlerCalled) result = storeClient.runReadTransaction { storeClient[key] } + if (!handlerCalled) { + @OptIn(RequiresTransaction::class) + result = storeClient.runReadTransaction { storeClient[key] } + } } return result } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/store/IgniteStoreClient.kt b/model-server/src/main/kotlin/org/modelix/model/server/store/IgniteStoreClient.kt index 76514eac8d..de3642b3fc 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/store/IgniteStoreClient.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/store/IgniteStoreClient.kt @@ -95,21 +95,25 @@ class IgniteStoreClient(jdbcProperties: Properties? = null, private val inmemory SqlUtils(dataSource.connection).ensureSchemaInitialization() } + @RequiresTransaction override fun getIfCached(key: ObjectInRepository): String? { localLocks.assertRead() return cache.localPeek(key, CachePeekMode.ONHEAP, CachePeekMode.OFFHEAP) } + @RequiresTransaction override fun getAll(keys: Set): Map { localLocks.assertRead() return cache.getAll(keys) } + @RequiresTransaction override fun getAll(): Map { localLocks.assertRead() return cache.associate { it.key to it.value } } + @RequiresTransaction override fun removeRepositoryObjects(repositoryId: RepositoryId) { localLocks.assertWrite() if (!inmemory) { @@ -149,6 +153,7 @@ class IgniteStoreClient(jdbcProperties: Properties? = null, private val inmemory } } + @RequiresTransaction override fun putAll(entries: Map, silent: Boolean) { localLocks.assertWrite() @@ -322,7 +327,8 @@ class ChangeNotifier(val store: IsolatingStore) { private var lastNotifiedValue: String? = null fun notifyIfChanged() { - val value = store.get(key) + @OptIn(RequiresTransaction::class) + val value = store.runReadTransaction { store.get(key) } if (value == lastNotifiedValue) return lastNotifiedValue = value diff --git a/model-server/src/main/kotlin/org/modelix/model/server/store/InMemoryStoreClient.kt b/model-server/src/main/kotlin/org/modelix/model/server/store/InMemoryStoreClient.kt index 8bb26387e3..bcb1520bc8 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/store/InMemoryStoreClient.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/store/InMemoryStoreClient.kt @@ -29,31 +29,37 @@ class InMemoryStoreClient : IsolatingStore, ITransactionManager, IRepositoryAwar private val pendingChangeMessages = PendingChangeMessages(changeNotifier::notifyListeners) private val locks = TransactionLocks() + @RequiresTransaction override fun get(key: ObjectInRepository): String? { locks.assertRead() return if (transactionValues?.contains(key) == true) transactionValues!![key] else values[key] } + @RequiresTransaction override fun getIfCached(key: ObjectInRepository): String? { locks.assertRead() return get(key) } + @RequiresTransaction override fun getAll(keys: List): List { locks.assertRead() return keys.map { get(it) } } + @RequiresTransaction override fun getAll(): Map { locks.assertRead() return values + (transactionValues ?: emptyMap()) } + @RequiresTransaction override fun getAll(keys: Set): Map { locks.assertRead() return keys.associateWith { get(it) } } + @RequiresTransaction override fun put(key: ObjectInRepository, value: String?, silent: Boolean) { locks.assertWrite() (transactionValues ?: values)[key] = value @@ -62,6 +68,7 @@ class InMemoryStoreClient : IsolatingStore, ITransactionManager, IRepositoryAwar } } + @RequiresTransaction override fun putAll(entries: Map, silent: Boolean) { locks.assertWrite() for ((key, value) in entries) { @@ -79,6 +86,7 @@ class InMemoryStoreClient : IsolatingStore, ITransactionManager, IRepositoryAwar override fun generateId(key: ObjectInRepository): Long { // This is an atomic operation that doesn't require the caller to start a transaction + @OptIn(RequiresTransaction::class) return runWriteTransaction { val id = generateId(get(key)) put(key, id.toString(), false) @@ -132,6 +140,7 @@ class InMemoryStoreClient : IsolatingStore, ITransactionManager, IRepositoryAwar return object : IImmutableStore { override fun getAll(keys: Set): Map { keys.forEach { require(HashUtil.isSha256(it.key)) { "Not an immutable object: $it" } } + @OptIn(RequiresTransaction::class) return runRead { this@InMemoryStoreClient.getAll(keys) }.mapValues { val value = it.value if (value == null) throw ObjectValueNotFoundException(it.key.key) @@ -144,11 +153,13 @@ class InMemoryStoreClient : IsolatingStore, ITransactionManager, IRepositoryAwar require(HashUtil.isSha256(it.key.key)) { "Not an immutable object: $it" } HashUtil.checkObjectHash(it.key.key, it.value) } + @OptIn(RequiresTransaction::class) runWrite { this@InMemoryStoreClient.putAll(entries) } } override fun getIfCached(key: ObjectInRepository): String? { require(HashUtil.isSha256(key.key)) { "Not an immutable object: $key" } + @OptIn(RequiresTransaction::class) return runRead { this@InMemoryStoreClient.getIfCached(key) } } } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAdapter.kt b/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAdapter.kt index 0dba81c86c..4e358e6a47 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAdapter.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAdapter.kt @@ -17,15 +17,18 @@ abstract class StoreClientAdapter(val client: IsolatingStore) : IStoreClient { } } + @RequiresTransaction override fun get(key: String): String? { return getAll(setOf(key))[key] } + @RequiresTransaction override fun getAll(keys: List): List { val map = getAll(keys.toSet()) return keys.map { map[it] } } + @RequiresTransaction override fun getIfCached(key: String): String? { val fromRepository = client.getIfCached(key.withRepoScope()) if (fromRepository != null) return fromRepository @@ -34,6 +37,7 @@ abstract class StoreClientAdapter(val client: IsolatingStore) : IStoreClient { return client.getIfCached(ObjectInRepository.global(key)) } + @RequiresTransaction override fun getAll(keys: Set): Map { val fromRepository = client.getAll(keys.map { it.withRepoScope() }.toSet()).mapKeys { it.key.key } if (getRepositoryId() == null) return fromRepository @@ -51,15 +55,18 @@ abstract class StoreClientAdapter(val client: IsolatingStore) : IStoreClient { return fromRepository + fromGlobal } + @RequiresTransaction override fun getAll(): Map { throw UnsupportedOperationException() // return client.getAll().filterKeys { it.repositoryId == null || it.repositoryId == repositoryId?.id }.mapKeys { it.key.key } } + @RequiresTransaction override fun put(key: String, value: String?, silent: Boolean) { client.put(key.withRepoScope(), value, silent) } + @RequiresTransaction override fun putAll(entries: Map, silent: Boolean) { client.putAll(entries.mapKeys { it.key.withRepoScope() }, silent) } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAsAsyncStore.kt b/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAsAsyncStore.kt index 5f61f50665..bfe4c1ac32 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAsAsyncStore.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAsAsyncStore.kt @@ -28,10 +28,12 @@ class StoreClientAsAsyncStore(val store: IStoreClient) : IAsyncObjectStore { } override fun getIfCached(key: ObjectHash): T? { + @OptIn(RequiresTransaction::class) // store is immutable and doesn't require transactions return store.getIfCached(key.hash)?.let { key.deserializer(it) } } override fun get(key: ObjectHash): Maybe { + @OptIn(RequiresTransaction::class) // store is immutable and doesn't require transactions val value = store.get(key.hash) ?: return maybeOfEmpty() return key.deserializer(value).toMaybe() } @@ -39,6 +41,8 @@ class StoreClientAsAsyncStore(val store: IStoreClient) : IAsyncObjectStore { override fun getAllAsStream(keys: Observable>): Observable, Any?>> { val keysList = keys.toList().getSynchronous() val keysMap = keysList.associateBy { it.hash } + + @OptIn(RequiresTransaction::class) // store is immutable and doesn't require transactions val serializedValues = store.getAll(keysMap.keys) return serializedValues.map { val ref = keysMap[it.key]!! @@ -48,6 +52,8 @@ class StoreClientAsAsyncStore(val store: IStoreClient) : IAsyncObjectStore { override fun getAllAsMap(keys: List>): Single, Any?>> { val keysMap = keys.associateBy { it.hash } + + @OptIn(RequiresTransaction::class) // store is immutable and doesn't require transactions val serializedValues = store.getAll(keysMap.keys) return serializedValues.map { val ref = keysMap[it.key]!! @@ -56,6 +62,7 @@ class StoreClientAsAsyncStore(val store: IStoreClient) : IAsyncObjectStore { } override fun putAll(entries: Map, IKVValue>): Completable { + @OptIn(RequiresTransaction::class) // store is immutable and doesn't require transactions store.putAll(entries.entries.associate { it.key.hash to it.value.serialize() }) return completableOfEmpty() } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAsKeyValueStore.kt b/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAsKeyValueStore.kt index c0752c1172..73f8b39fe8 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAsKeyValueStore.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/store/StoreClientAsKeyValueStore.kt @@ -7,6 +7,7 @@ import org.modelix.model.IKeyValueStore class StoreClientAsKeyValueStore(val store: IStoreClient) : IKeyValueStore { override fun get(key: String): String? { + @OptIn(RequiresTransaction::class) // store is immutable and doesn't require transactions return store[key] } @@ -15,11 +16,14 @@ class StoreClientAsKeyValueStore(val store: IStoreClient) : IKeyValueStore { } override fun put(key: String, value: String?) { + @OptIn(RequiresTransaction::class) // store is immutable and doesn't require transactions store.put(key, value) } override fun getAll(keys: Iterable): Map { val keyList = IterableUtils.toList(keys) + + @OptIn(RequiresTransaction::class) // store is immutable and doesn't require transactions val values = store.getAll(keyList) val result: MutableMap = LinkedHashMap() for (i in keyList.indices) { @@ -29,6 +33,7 @@ class StoreClientAsKeyValueStore(val store: IStoreClient) : IKeyValueStore { } override fun putAll(entries: Map) { + @OptIn(RequiresTransaction::class) // store is immutable and doesn't require transactions store.putAll(entries) } diff --git a/model-server/src/main/kotlin/org/modelix/model/server/store/TransactionLocks.kt b/model-server/src/main/kotlin/org/modelix/model/server/store/TransactionLocks.kt index ade11f06e3..528a680c39 100644 --- a/model-server/src/main/kotlin/org/modelix/model/server/store/TransactionLocks.kt +++ b/model-server/src/main/kotlin/org/modelix/model/server/store/TransactionLocks.kt @@ -5,6 +5,19 @@ import kotlinx.coroutines.withContext import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.withLock +/** + * Each call of runRead/runWrite can be annotated with @OptIn(RequiresTransaction::class) + * All other usages should propagate this annotation. + * + * Unfortunately, there is no way to automatically opt in the body of runRead/runWrite. + * Checked exceptions in Java would allow stopping the propagation based on the execution flow, + * but Kotlin doesn't have checked exceptions. + * It's still better having to annotate everything instead of noticing missing transactions only at runtime. + */ +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION) +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +annotation class RequiresTransaction + interface ITransactionManager { fun canRead(): Boolean fun canWrite(): Boolean @@ -12,6 +25,20 @@ interface ITransactionManager { fun runWrite(body: () -> R): R } +class NoTransactionManager : ITransactionManager { + override fun canRead(): Boolean = true + + override fun canWrite(): Boolean = true + + override fun runRead(body: () -> R): R { + return body() + } + + override fun runWrite(body: () -> R): R { + return body() + } +} + suspend fun ITransactionManager.runWriteIO(body: () -> R): R { return withContext(Dispatchers.IO) { runWrite(body) @@ -24,16 +51,17 @@ suspend fun ITransactionManager.runReadIO(body: () -> R): R { } } +@RequiresTransaction fun ITransactionManager.assertRead() { - if (!canRead()) throw MissingReadTransactionException() + if (!canRead()) throw MissingTransactionException() } +@RequiresTransaction fun ITransactionManager.assertWrite() { if (!canWrite()) throw MissingWriteTransactionException() } -abstract class MissingTransactionException(message: String) : RuntimeException(message) -class MissingReadTransactionException() : MissingTransactionException("Read transaction required") +open class MissingTransactionException(message: String = "Transaction required") : Exception(message) class MissingWriteTransactionException() : MissingTransactionException("Write transaction required") /** diff --git a/model-server/src/test/java/org/modelix/model/server/RestModelServerTest.kt b/model-server/src/test/java/org/modelix/model/server/RestModelServerTest.kt deleted file mode 100644 index a4556b84cd..0000000000 --- a/model-server/src/test/java/org/modelix/model/server/RestModelServerTest.kt +++ /dev/null @@ -1,161 +0,0 @@ -package org.modelix.model.server - -import org.junit.Assert -import org.junit.Test -import org.modelix.model.server.handlers.KeyValueLikeModelServer -import org.modelix.model.server.handlers.RepositoriesManager -import org.modelix.model.server.store.InMemoryStoreClient -import org.modelix.model.server.store.forGlobalRepository - -class RestModelServerTest { - @Test - fun testCollectUnexistingKey() { - val rms = KeyValueLikeModelServer(RepositoriesManager(InMemoryStoreClient())) - val result = rms.collect("unexistingKey", null) - Assert.assertEquals(1, result.length().toLong()) - Assert.assertEquals(HashSet(mutableListOf("key")), result.getJSONObject(0).keySet()) - Assert.assertEquals("unexistingKey", result.getJSONObject(0)["key"]) - } - - @Test - fun testCollectExistingKeyNotHash() { - val genericStore = InMemoryStoreClient() - val storeClient = genericStore.forGlobalRepository() - storeClient.put("existingKey", "foo", false) - val rms = KeyValueLikeModelServer(RepositoriesManager(genericStore)) - val result = rms.collect("existingKey", null) - Assert.assertEquals(1, result.length().toLong()) - Assert.assertEquals( - HashSet(mutableListOf("key", "value")), - result.getJSONObject(0).keySet(), - ) - Assert.assertEquals("existingKey", result.getJSONObject(0)["key"]) - Assert.assertEquals("foo", result.getJSONObject(0)["value"]) - } - - @Test - fun testCollectExistingKeyHash() { - val genericStore = InMemoryStoreClient() - val storeClient = genericStore.forGlobalRepository() - storeClient.put("existingKey", "hash-*0123456789-0123456789-0123456789-00001", false) - storeClient.put("hash-*0123456789-0123456789-0123456789-00001", "bar", false) - val rms = - KeyValueLikeModelServer( - RepositoriesManager(genericStore), - ) - val result = rms.collect("existingKey", null) - Assert.assertEquals(2, result.length().toLong()) - - var obj = result.getJSONObject(0) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("existingKey", obj["key"]) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["value"]) - - obj = result.getJSONObject(1) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["key"]) - Assert.assertEquals("bar", obj["value"]) - } - - @Test - fun testCollectExistingKeyHashChained() { - val genericStore = InMemoryStoreClient() - val storeClient = genericStore.forGlobalRepository() - storeClient.put("root", "hash-*0123456789-0123456789-0123456789-00001", false) - storeClient.put( - "hash-*0123456789-0123456789-0123456789-00001", - "hash-*0123456789-0123456789-0123456789-00002", - false, - ) - storeClient.put( - "hash-*0123456789-0123456789-0123456789-00002", - "hash-*0123456789-0123456789-0123456789-00003", - false, - ) - storeClient.put( - "hash-*0123456789-0123456789-0123456789-00003", - "hash-*0123456789-0123456789-0123456789-00004", - false, - ) - storeClient.put("hash-*0123456789-0123456789-0123456789-00004", "end", false) - val rms = - KeyValueLikeModelServer( - RepositoriesManager(genericStore), - ) - val result = rms.collect("root", null) - Assert.assertEquals(5, result.length().toLong()) - - var obj = result.getJSONObject(0) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("root", obj["key"]) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["value"]) - - obj = result.getJSONObject(1) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["key"]) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00002", obj["value"]) - - obj = result.getJSONObject(2) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00002", obj["key"]) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00003", obj["value"]) - - obj = result.getJSONObject(3) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00003", obj["key"]) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00004", obj["value"]) - - obj = result.getJSONObject(4) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00004", obj["key"]) - Assert.assertEquals("end", obj["value"]) - } - - @Test - fun testCollectExistingKeyHashChainedWithRepetitions() { - val genericStore = InMemoryStoreClient() - val storeClient = genericStore.forGlobalRepository() - storeClient.put("root", "hash-*0123456789-0123456789-0123456789-00001", false) - storeClient.put( - "hash-*0123456789-0123456789-0123456789-00001", - "hash-*0123456789-0123456789-0123456789-00002", - false, - ) - storeClient.put( - "hash-*0123456789-0123456789-0123456789-00002", - "hash-*0123456789-0123456789-0123456789-00003", - false, - ) - storeClient.put( - "hash-*0123456789-0123456789-0123456789-00003", - "hash-*0123456789-0123456789-0123456789-00001", - false, - ) - val rms = - KeyValueLikeModelServer( - RepositoriesManager(genericStore), - ) - val result = rms.collect("root", null) - Assert.assertEquals(4, result.length().toLong()) - - var obj = result.getJSONObject(0) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("root", obj["key"]) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["value"]) - - obj = result.getJSONObject(1) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["key"]) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00002", obj["value"]) - - obj = result.getJSONObject(2) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00002", obj["key"]) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00003", obj["value"]) - - obj = result.getJSONObject(3) - Assert.assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00003", obj["key"]) - Assert.assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["value"]) - } -} diff --git a/model-server/src/test/kotlin/org/modelix/model/server/IgniteStoreClientParallelTransactionsTest.kt b/model-server/src/test/kotlin/org/modelix/model/server/IgniteStoreClientParallelTransactionsTest.kt index bf99ae8dc3..fc822ac6cc 100644 --- a/model-server/src/test/kotlin/org/modelix/model/server/IgniteStoreClientParallelTransactionsTest.kt +++ b/model-server/src/test/kotlin/org/modelix/model/server/IgniteStoreClientParallelTransactionsTest.kt @@ -7,6 +7,7 @@ import org.modelix.model.IKeyListener import org.modelix.model.server.store.IStoreClient import org.modelix.model.server.store.IgniteStoreClient import org.modelix.model.server.store.InMemoryStoreClient +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.store.forGlobalRepository import org.slf4j.LoggerFactory import java.util.concurrent.CompletableFuture @@ -38,12 +39,14 @@ abstract class StoreClientParallelTransactionsTest(val store: IStoreClient) { "key2", object : IKeyListener { override fun changed(key: String, value: String?) { - notifiedValueFuture.complete(store[key]!!) + @OptIn(RequiresTransaction::class) + notifiedValueFuture.complete(store.runReadTransaction { store[key] }!!) } }, ) launch(Dispatchers.IO) { + @OptIn(RequiresTransaction::class) store.runWriteTransaction { store.put("key1", "valueA") threadBlocker.reachPointInTime(1) @@ -57,6 +60,7 @@ abstract class StoreClientParallelTransactionsTest(val store: IStoreClient) { } launch(Dispatchers.IO) { + @OptIn(RequiresTransaction::class) store.runWriteTransaction { threadBlocker.sleepUntilPointInTime(1) store.put("key2", "valueB") @@ -84,6 +88,7 @@ abstract class StoreClientParallelTransactionsTest(val store: IStoreClient) { println("(1) launched") threadBlocker.reachPointInTime(1) threadBlocker.sleepUntilPointInTime(2) + @OptIn(RequiresTransaction::class) store.runWriteTransaction { println("(1) inside transaction") @@ -103,6 +108,7 @@ abstract class StoreClientParallelTransactionsTest(val store: IStoreClient) { println("(2) launched") threadBlocker.sleepUntilPointInTime(1) threadBlocker.reachPointInTime(2) + @OptIn(RequiresTransaction::class) store.runWriteTransaction { println("(2) inside transaction") diff --git a/model-server/src/test/kotlin/org/modelix/model/server/ModelClientTest.kt b/model-server/src/test/kotlin/org/modelix/model/server/ModelClientTest.kt index 71f426f640..9d752cae02 100644 --- a/model-server/src/test/kotlin/org/modelix/model/server/ModelClientTest.kt +++ b/model-server/src/test/kotlin/org/modelix/model/server/ModelClientTest.kt @@ -5,7 +5,6 @@ import io.ktor.server.testing.testApplication import kotlinx.coroutines.delay import kotlinx.coroutines.withTimeout import mu.KotlinLogging -import org.modelix.authorization.installAuthentication import org.modelix.model.IKeyListener import org.modelix.model.client.RestWebModelClient import org.modelix.model.server.handlers.KeyValueLikeModelServer diff --git a/model-server/src/test/kotlin/org/modelix/model/server/RestModelServerTest.kt b/model-server/src/test/kotlin/org/modelix/model/server/RestModelServerTest.kt new file mode 100644 index 0000000000..dc200e76db --- /dev/null +++ b/model-server/src/test/kotlin/org/modelix/model/server/RestModelServerTest.kt @@ -0,0 +1,183 @@ +package org.modelix.model.server + +import org.modelix.model.server.handlers.KeyValueLikeModelServer +import org.modelix.model.server.handlers.RepositoriesManager +import org.modelix.model.server.store.InMemoryStoreClient +import org.modelix.model.server.store.RequiresTransaction +import org.modelix.model.server.store.forGlobalRepository +import kotlin.test.Test +import kotlin.test.assertEquals + +class RestModelServerTest { + @Test + fun testCollectUnexistingKey() { + val store = InMemoryStoreClient() + val rms = KeyValueLikeModelServer(RepositoriesManager(store)) + + @OptIn(RequiresTransaction::class) + val result = store.runRead { rms.collect("unexistingKey", null) } + assertEquals(1, result.length().toLong()) + assertEquals(HashSet(mutableListOf("key")), result.getJSONObject(0).keySet()) + assertEquals("unexistingKey", result.getJSONObject(0)["key"]) + } + + @Test + fun testCollectExistingKeyNotHash() { + val genericStore = InMemoryStoreClient() + val storeClient = genericStore.forGlobalRepository() + @OptIn(RequiresTransaction::class) + genericStore.runWriteTransaction { storeClient.put("existingKey", "foo", false) } + val rms = KeyValueLikeModelServer(RepositoriesManager(genericStore)) + + @OptIn(RequiresTransaction::class) + val result = genericStore.runRead { rms.collect("existingKey", null) } + assertEquals(1, result.length().toLong()) + assertEquals( + HashSet(mutableListOf("key", "value")), + result.getJSONObject(0).keySet(), + ) + assertEquals("existingKey", result.getJSONObject(0)["key"]) + assertEquals("foo", result.getJSONObject(0)["value"]) + } + + @Test + fun testCollectExistingKeyHash() { + val genericStore = InMemoryStoreClient() + val storeClient = genericStore.forGlobalRepository() + @OptIn(RequiresTransaction::class) + genericStore.runWrite { + storeClient.put("existingKey", "hash-*0123456789-0123456789-0123456789-00001", false) + storeClient.put("hash-*0123456789-0123456789-0123456789-00001", "bar", false) + } + val rms = + KeyValueLikeModelServer( + RepositoriesManager(genericStore), + ) + + @OptIn(RequiresTransaction::class) + val result = genericStore.runRead { rms.collect("existingKey", null) } + assertEquals(2, result.length().toLong()) + + var obj = result.getJSONObject(0) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("existingKey", obj["key"]) + assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["value"]) + + obj = result.getJSONObject(1) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["key"]) + assertEquals("bar", obj["value"]) + } + + @Test + fun testCollectExistingKeyHashChained() { + val genericStore = InMemoryStoreClient() + val storeClient = genericStore.forGlobalRepository() + @OptIn(RequiresTransaction::class) + genericStore.runWrite { + storeClient.put("root", "hash-*0123456789-0123456789-0123456789-00001", false) + storeClient.put( + "hash-*0123456789-0123456789-0123456789-00001", + "hash-*0123456789-0123456789-0123456789-00002", + false, + ) + storeClient.put( + "hash-*0123456789-0123456789-0123456789-00002", + "hash-*0123456789-0123456789-0123456789-00003", + false, + ) + storeClient.put( + "hash-*0123456789-0123456789-0123456789-00003", + "hash-*0123456789-0123456789-0123456789-00004", + false, + ) + storeClient.put("hash-*0123456789-0123456789-0123456789-00004", "end", false) + } + val rms = + KeyValueLikeModelServer( + RepositoriesManager(genericStore), + ) + + @OptIn(RequiresTransaction::class) + val result = genericStore.runRead { rms.collect("root", null) } + assertEquals(5, result.length().toLong()) + + var obj = result.getJSONObject(0) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("root", obj["key"]) + assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["value"]) + + obj = result.getJSONObject(1) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["key"]) + assertEquals("hash-*0123456789-0123456789-0123456789-00002", obj["value"]) + + obj = result.getJSONObject(2) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("hash-*0123456789-0123456789-0123456789-00002", obj["key"]) + assertEquals("hash-*0123456789-0123456789-0123456789-00003", obj["value"]) + + obj = result.getJSONObject(3) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("hash-*0123456789-0123456789-0123456789-00003", obj["key"]) + assertEquals("hash-*0123456789-0123456789-0123456789-00004", obj["value"]) + + obj = result.getJSONObject(4) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("hash-*0123456789-0123456789-0123456789-00004", obj["key"]) + assertEquals("end", obj["value"]) + } + + @Test + fun testCollectExistingKeyHashChainedWithRepetitions() { + val genericStore = InMemoryStoreClient() + val storeClient = genericStore.forGlobalRepository() + @OptIn(RequiresTransaction::class) + genericStore.runWrite { + storeClient.put("root", "hash-*0123456789-0123456789-0123456789-00001", false) + storeClient.put( + "hash-*0123456789-0123456789-0123456789-00001", + "hash-*0123456789-0123456789-0123456789-00002", + false, + ) + storeClient.put( + "hash-*0123456789-0123456789-0123456789-00002", + "hash-*0123456789-0123456789-0123456789-00003", + false, + ) + storeClient.put( + "hash-*0123456789-0123456789-0123456789-00003", + "hash-*0123456789-0123456789-0123456789-00001", + false, + ) + } + val rms = + KeyValueLikeModelServer( + RepositoriesManager(genericStore), + ) + + @OptIn(RequiresTransaction::class) + val result = genericStore.runRead { rms.collect("root", null) } + assertEquals(4, result.length().toLong()) + + var obj = result.getJSONObject(0) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("root", obj["key"]) + assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["value"]) + + obj = result.getJSONObject(1) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["key"]) + assertEquals("hash-*0123456789-0123456789-0123456789-00002", obj["value"]) + + obj = result.getJSONObject(2) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("hash-*0123456789-0123456789-0123456789-00002", obj["key"]) + assertEquals("hash-*0123456789-0123456789-0123456789-00003", obj["value"]) + + obj = result.getJSONObject(3) + assertEquals(HashSet(mutableListOf("key", "value")), obj.keySet()) + assertEquals("hash-*0123456789-0123456789-0123456789-00003", obj["key"]) + assertEquals("hash-*0123456789-0123456789-0123456789-00001", obj["value"]) + } +} diff --git a/model-server/src/test/kotlin/org/modelix/model/server/StoreClientTest.kt b/model-server/src/test/kotlin/org/modelix/model/server/StoreClientTest.kt index 9e8e83f489..976663149e 100644 --- a/model-server/src/test/kotlin/org/modelix/model/server/StoreClientTest.kt +++ b/model-server/src/test/kotlin/org/modelix/model/server/StoreClientTest.kt @@ -8,6 +8,7 @@ import org.modelix.model.server.store.IStoreClient import org.modelix.model.server.store.IgniteStoreClient import org.modelix.model.server.store.InMemoryStoreClient import org.modelix.model.server.store.MissingWriteTransactionException +import org.modelix.model.server.store.RequiresTransaction import org.modelix.model.server.store.forGlobalRepository import java.util.Collections import kotlin.random.Random @@ -28,6 +29,7 @@ abstract class StoreClientTest(val store: IStoreClient) { @Test fun `transaction can be started from inside a transaction`() { + @OptIn(RequiresTransaction::class) store.runWriteTransaction { store.runWriteTransaction { store.put("abc", "def") @@ -40,6 +42,7 @@ abstract class StoreClientTest(val store: IStoreClient) { val key = "ljnrdlfkesmgf" val value = "izujztdrsew" assertFailsWith { + @OptIn(RequiresTransaction::class) store.put(key, value) } } @@ -50,6 +53,7 @@ abstract class StoreClientTest(val store: IStoreClient) { repeat(2) { val rand = Random(it) launch { + @OptIn(RequiresTransaction::class) store.runWriteTransaction { repeat(10) { val value = rand.nextInt().toString() @@ -69,15 +73,19 @@ abstract class StoreClientTest(val store: IStoreClient) { val value1 = "a" val value2 = "b" + @OptIn(RequiresTransaction::class) store.runWriteTransaction { store.put(key, value1) } + @OptIn(RequiresTransaction::class) assertEquals(value1, store.runReadTransaction { store.get(key) }) assertFailsWith(NullPointerException::class) { + @OptIn(RequiresTransaction::class) store.runWriteTransaction { store.put(key, value2) assertEquals(value2, store.get(key)) throw NullPointerException() } } + @OptIn(RequiresTransaction::class) assertEquals(value1, store.runReadTransaction { store.get(key) }) // failed transaction should be rolled back } @@ -94,12 +102,15 @@ abstract class StoreClientTest(val store: IStoreClient) { object : IKeyListener { override fun changed(key: String, value: String?) { valuesSeenByListener += value - valuesSeenByListener += store.get(key) + @OptIn(RequiresTransaction::class) + valuesSeenByListener += store.runReadTransaction { store.get(key) } } }, ) + @OptIn(RequiresTransaction::class) store.runWriteTransaction { store.put(key, value1) } + @OptIn(RequiresTransaction::class) assertEquals(value1, store.runReadTransaction { store.get(key) }) assertEquals(setOf(value1), valuesSeenByListener) @@ -108,6 +119,7 @@ abstract class StoreClientTest(val store: IStoreClient) { coroutineScope { launch { assertFailsWith(NullPointerException::class) { + @OptIn(RequiresTransaction::class) store.runWriteTransaction { assertEquals(value1, store.get(key)) store.put(key, value2, silent = false) @@ -118,6 +130,7 @@ abstract class StoreClientTest(val store: IStoreClient) { } launch { + @OptIn(RequiresTransaction::class) store.runWriteTransaction { assertEquals(value1, store.get(key)) store.put(key, value3, silent = false) diff --git a/model-server/src/test/kotlin/org/modelix/model/server/StoreClientWithStatistics.kt b/model-server/src/test/kotlin/org/modelix/model/server/StoreClientWithStatistics.kt index f22ddba708..b17d5f0ad1 100644 --- a/model-server/src/test/kotlin/org/modelix/model/server/StoreClientWithStatistics.kt +++ b/model-server/src/test/kotlin/org/modelix/model/server/StoreClientWithStatistics.kt @@ -2,6 +2,7 @@ package org.modelix.model.server import org.modelix.model.server.store.IRepositoryAwareStore import org.modelix.model.server.store.ObjectInRepository +import org.modelix.model.server.store.RequiresTransaction import java.util.concurrent.atomic.AtomicLong class StoreClientWithStatistics(val store: IRepositoryAwareStore) : IRepositoryAwareStore by store { @@ -9,21 +10,25 @@ class StoreClientWithStatistics(val store: IRepositoryAwareStore) : IRepositoryA fun getTotalRequests() = totalRequests.get() + @RequiresTransaction override fun get(key: ObjectInRepository): String? { totalRequests.incrementAndGet() return store.get(key) } + @RequiresTransaction override fun getAll(keys: List): List { totalRequests.incrementAndGet() return store.getAll(keys) } + @RequiresTransaction override fun getAll(keys: Set): Map { totalRequests.incrementAndGet() return store.getAll(keys) } + @RequiresTransaction override fun getAll(): Map { totalRequests.incrementAndGet() return store.getAll() diff --git a/model-server/src/test/kotlin/org/modelix/model/server/handlers/ModelReplicationServerTest.kt b/model-server/src/test/kotlin/org/modelix/model/server/handlers/ModelReplicationServerTest.kt index 31e21ab47a..b1b0d63bc9 100644 --- a/model-server/src/test/kotlin/org/modelix/model/server/handlers/ModelReplicationServerTest.kt +++ b/model-server/src/test/kotlin/org/modelix/model/server/handlers/ModelReplicationServerTest.kt @@ -51,6 +51,7 @@ import org.modelix.model.server.api.v2.VersionDeltaStreamV2 import org.modelix.model.server.installDefaultServerPlugins import org.modelix.model.server.runWithNettyServer import org.modelix.model.server.store.InMemoryStoreClient +import org.modelix.model.server.store.RequiresTransaction import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals @@ -146,6 +147,7 @@ class ModelReplicationServerTest { val repositoryId = RepositoryId("repo1") runWithTestModelServer { _, fixture -> + @OptIn(RequiresTransaction::class) fixture.repositoriesManager.getTransactionManager().runWrite { fixture.repositoriesManager.createRepository(repositoryId, null) } @@ -194,6 +196,7 @@ class ModelReplicationServerTest { val defaultBranchRef = repositoryId.getBranchReference("master") runWithTestModelServer { _, fixture -> + @OptIn(RequiresTransaction::class) fixture.repositoriesManager.getTransactionManager().runWrite { fixture.repositoriesManager.createRepository(repositoryId, null) fixture.repositoriesManager.mergeChanges( @@ -209,6 +212,7 @@ class ModelReplicationServerTest { } assertEquals(HttpStatusCode.NoContent, response.status) + @OptIn(RequiresTransaction::class) val branchNames = fixture.repositoriesManager.getTransactionManager().runRead { fixture.repositoriesManager.getBranchNames(repositoryId) } @@ -238,6 +242,7 @@ class ModelReplicationServerTest { modelReplicationServer, ), ) { _, _ -> + @OptIn(RequiresTransaction::class) repositoriesManager.getTransactionManager().runWrite { repositoriesManager.createRepository(repositoryId, null) } @@ -277,6 +282,7 @@ class ModelReplicationServerTest { ) } } + @OptIn(RequiresTransaction::class) repositoriesManager.getTransactionManager().runWrite { repositoriesManager.createRepository(repositoryId, null) } @@ -346,6 +352,7 @@ class ModelReplicationServerTest { val errorMessage = "expected test failure" val faultyRepositoriesManager = object : IRepositoriesManager by repositoriesManager { + @RequiresTransaction override fun getRepositories(): Set { error(errorMessage) } @@ -387,6 +394,7 @@ class ModelReplicationServerTest { val repositoryId = RepositoryId("repo1") runWithTestModelServer { _, fixture -> + @OptIn(RequiresTransaction::class) fixture.repositoriesManager.getTransactionManager().runWrite { fixture.repositoriesManager.createRepository(repositoryId, null) } @@ -410,6 +418,7 @@ class ModelReplicationServerTest { ContentType.parse("application/x.modelix.branch+json;version=1").withCharset(Charsets.UTF_8) runWithTestModelServer { _, fixture -> + @OptIn(RequiresTransaction::class) fixture.repositoriesManager.getTransactionManager().runWrite { fixture.repositoriesManager.createRepository(repositoryId, null) } diff --git a/model-server/src/test/kotlin/org/modelix/model/server/handlers/RepositoriesManagerTest.kt b/model-server/src/test/kotlin/org/modelix/model/server/handlers/RepositoriesManagerTest.kt index b746849c21..827c92385d 100644 --- a/model-server/src/test/kotlin/org/modelix/model/server/handlers/RepositoriesManagerTest.kt +++ b/model-server/src/test/kotlin/org/modelix/model/server/handlers/RepositoriesManagerTest.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.test.runTest import org.modelix.model.lazy.RepositoryId import org.modelix.model.server.store.IRepositoryAwareStore import org.modelix.model.server.store.InMemoryStoreClient +import org.modelix.model.server.store.RequiresTransaction import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals @@ -16,6 +17,7 @@ class RepositoriesManagerTest { val store = spyk(InMemoryStoreClient()) private val repoManager = RepositoriesManager(store) + @RequiresTransaction private fun initRepository(repoId: RepositoryId) { repoManager.createRepository(repoId, "testUser", useRoleIds = true, legacyGlobalStorage = false) } @@ -28,10 +30,12 @@ class RepositoriesManagerTest { @Test fun `deleting default branch works`() = runTest { val repoId = RepositoryId("branch-removal") + @OptIn(RequiresTransaction::class) repoManager.getTransactionManager().runWrite { initRepository(repoId) repoManager.removeBranches(repoId, setOf("master")) } + @OptIn(RequiresTransaction::class) val branches = repoManager.getTransactionManager().runRead { repoManager.getBranches(repoId) } assertTrue { branches.none { it.branchName == "master" } } } @@ -39,17 +43,22 @@ class RepositoriesManagerTest { @Test fun `repository data is removed when removing repository`() = runTest { val repoId = RepositoryId("abc") + @OptIn(RequiresTransaction::class) repoManager.getTransactionManager().runWrite { initRepository(repoId) repoManager.removeRepository(repoId) } - verify(exactly = 1) { store.removeRepositoryObjects(repoId) } + @OptIn(RequiresTransaction::class) + store.runWriteTransaction { + verify(exactly = 1) { store.removeRepositoryObjects(repoId) } + } } @Test fun `data of other repositories remains intact when removing a repository`() = runTest { val existingRepo = RepositoryId("existing") val toBeDeletedRepo = RepositoryId("tobedeleted") + @OptIn(RequiresTransaction::class) repoManager.getTransactionManager().runWrite { initRepository(existingRepo) initRepository(toBeDeletedRepo) diff --git a/model-server/src/test/kotlin/org/modelix/model/server/store/IgnitePostgresRepositoryRemovalTest.kt b/model-server/src/test/kotlin/org/modelix/model/server/store/IgnitePostgresRepositoryRemovalTest.kt index a3915ec38e..d56331f1bc 100644 --- a/model-server/src/test/kotlin/org/modelix/model/server/store/IgnitePostgresRepositoryRemovalTest.kt +++ b/model-server/src/test/kotlin/org/modelix/model/server/store/IgnitePostgresRepositoryRemovalTest.kt @@ -51,10 +51,13 @@ class IgnitePostgresRepositoryRemovalTest { ObjectInRepository(toDelete.id, "key0") to "value0", ObjectInRepository(toDelete.id, "key1") to "value1", ) + @OptIn(RequiresTransaction::class) store.runWrite { store.putAll(entries) } + @OptIn(RequiresTransaction::class) store.runWrite { store.removeRepositoryObjects(toDelete) } + @OptIn(RequiresTransaction::class) assertTrue { store.runRead { store.getAll(entries.keys) }.isEmpty() } } @@ -67,6 +70,7 @@ class IgnitePostgresRepositoryRemovalTest { check(it.updateCount == 1) } + @OptIn(RequiresTransaction::class) store.getTransactionManager().runWrite { store.removeRepositoryObjects(toDelete) } dbConnection.prepareStatement("SELECT * FROM modelix.model WHERE repository = ?").use { @@ -88,6 +92,7 @@ class IgnitePostgresRepositoryRemovalTest { check(it.updateCount == 1) } + @OptIn(RequiresTransaction::class) store.runWrite { store.removeRepositoryObjects(toDelete) } dbConnection.prepareStatement("SELECT * FROM modelix.model WHERE repository = ?").use { @@ -103,21 +108,26 @@ class IgnitePostgresRepositoryRemovalTest { ObjectInRepository(existing.id, "key0") to "value0", ObjectInRepository(existing.id, "key1") to "value1", ) + @OptIn(RequiresTransaction::class) store.runWrite { store.putAll(existingEntries) } val toDeleteEntries = mapOf( ObjectInRepository(toDelete.id, "key0") to "value0", ObjectInRepository(toDelete.id, "key1") to "value1", ) + @OptIn(RequiresTransaction::class) store.runWrite { store.putAll(toDeleteEntries) } + @OptIn(RequiresTransaction::class) store.runWrite { store.removeRepositoryObjects(toDelete) } + @OptIn(RequiresTransaction::class) assertEquals(existingEntries, store.runRead { store.getAll() }) } @Test fun `removing a non-existing repository does not throw`() { assertDoesNotThrow { + @OptIn(RequiresTransaction::class) store.runWrite { store.removeRepositoryObjects(RepositoryId("invalid")) } } } diff --git a/model-server/src/test/kotlin/org/modelix/model/server/store/IgnitePostgresTest.kt b/model-server/src/test/kotlin/org/modelix/model/server/store/IgnitePostgresTest.kt index 128239756a..fcbcf1cae4 100644 --- a/model-server/src/test/kotlin/org/modelix/model/server/store/IgnitePostgresTest.kt +++ b/model-server/src/test/kotlin/org/modelix/model/server/store/IgnitePostgresTest.kt @@ -43,6 +43,7 @@ class IgnitePostgresTest { @Test fun `can get values for multiple keys when Ignite has not cached the keys yet`() { + @OptIn(RequiresTransaction::class) store.runRead { // The actual keys are irrelevant for this test. // A fresh client will have no keys cached. @@ -57,6 +58,7 @@ class IgnitePostgresTest { @Test fun `store immutable object in repository`() { + @OptIn(RequiresTransaction::class) store.runWrite { val value = "immutable value " + System.nanoTime() val hash = HashUtil.sha256(value) @@ -73,6 +75,7 @@ class IgnitePostgresTest { @Test fun `store immutable object in global storage`() { + @OptIn(RequiresTransaction::class) store.runWrite { val value = "immutable value " + System.nanoTime() val hash = HashUtil.sha256(value) @@ -86,6 +89,7 @@ class IgnitePostgresTest { @Test fun `store mutable object in repository`() { + @OptIn(RequiresTransaction::class) store.runWrite { val value = "mutable value " + System.nanoTime() val hash = "mutable key " + System.nanoTime() @@ -102,6 +106,7 @@ class IgnitePostgresTest { @Test fun `store mutable object in global storage`() { + @OptIn(RequiresTransaction::class) store.runWrite { val value = "mutable value " + System.nanoTime() val hash = "mutable key " + System.nanoTime() @@ -115,6 +120,7 @@ class IgnitePostgresTest { @Test fun `read mutable legacy entry`() { + @OptIn(RequiresTransaction::class) store.runRead { val key = ObjectInRepository.global(":v2:repositories") assertEquals("courses", store.get(key)) @@ -123,6 +129,7 @@ class IgnitePostgresTest { @Test fun `overwrite mutable legacy entry`() { + @OptIn(RequiresTransaction::class) store.runWrite { val key = ObjectInRepository.global(":v2:repositories:courses:branches") assertEquals("master", store.get(key)) @@ -133,6 +140,7 @@ class IgnitePostgresTest { @Test fun `delete overwritten mutable legacy entry`() { + @OptIn(RequiresTransaction::class) store.runWrite { val key = ObjectInRepository.global(":v2:repositories:courses:branches:master") assertEquals("7fQeo*xrdfZuHZtaKhbp0OosarV5tVR8N3pW8JPkl7ZE", store.get(key)) @@ -145,6 +153,7 @@ class IgnitePostgresTest { @Test fun `read immutable legacy entry`() { + @OptIn(RequiresTransaction::class) store.runRead { val key = ObjectInRepository.global("2YAqw*tJWjb2t2RMO8ypIvHn8QpHjwmIUdhdFygV9lUE") assertEquals("S/5/0/W7HBQ*K6ZbUt76ti7r2pTscLVsWd8oiqIOIsKTpQB8Aw", store.get(key)) diff --git a/model-server/src/test/kotlin/org/modelix/model/server/store/InMemoryIsolatingStoreTest.kt b/model-server/src/test/kotlin/org/modelix/model/server/store/InMemoryIsolatingStoreTest.kt index f57a7395f5..63e2e50e9d 100644 --- a/model-server/src/test/kotlin/org/modelix/model/server/store/InMemoryIsolatingStoreTest.kt +++ b/model-server/src/test/kotlin/org/modelix/model/server/store/InMemoryIsolatingStoreTest.kt @@ -21,12 +21,14 @@ class InMemoryIsolatingStoreTest { ObjectInRepository(repoId.id, "key0") to "value0", ObjectInRepository(repoId.id, "key1") to "value1", ) + @OptIn(RequiresTransaction::class) store.runWrite { store.putAll(entries) store.removeRepositoryObjects(repoId) } + @OptIn(RequiresTransaction::class) assertTrue { store.runRead { store.getAll() }.isEmpty() } } @@ -37,6 +39,7 @@ class InMemoryIsolatingStoreTest { ObjectInRepository(existingId.id, "key0") to "value0", ObjectInRepository(existingId.id, "key1") to "value1", ) + @OptIn(RequiresTransaction::class) store.runWrite { store.putAll(existingEntries) } val toDeleteId = RepositoryId("toDelete") @@ -44,17 +47,20 @@ class InMemoryIsolatingStoreTest { ObjectInRepository(toDeleteId.id, "key0") to "value0", ObjectInRepository(toDeleteId.id, "key1") to "value1", ) + @OptIn(RequiresTransaction::class) store.runWrite { store.putAll(toDeleteEntries) store.removeRepositoryObjects(toDeleteId) } + @OptIn(RequiresTransaction::class) assertEquals(existingEntries, store.runRead { store.getAll() }) } @Test fun `removing a non-existing repository does not throw`() { assertDoesNotThrow { + @OptIn(RequiresTransaction::class) store.runWrite { store.removeRepositoryObjects(RepositoryId("invalid")) } } }