From 9cfe8bb36091caff052dc9557cc629f62d2cdd61 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Wed, 6 Sep 2023 12:48:33 +0200 Subject: [PATCH 1/2] fix(model-client): rethrow CancellationException CancellationException is a special exception that is expected when the coroutines scope is cancelled. --- .../kotlin/org/modelix/model/client2/ReplicatedModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/model-client/src/commonMain/kotlin/org/modelix/model/client2/ReplicatedModel.kt b/model-client/src/commonMain/kotlin/org/modelix/model/client2/ReplicatedModel.kt index 8b3f8a9955..f17801a8ab 100644 --- a/model-client/src/commonMain/kotlin/org/modelix/model/client2/ReplicatedModel.kt +++ b/model-client/src/commonMain/kotlin/org/modelix/model/client2/ReplicatedModel.kt @@ -17,6 +17,7 @@ import org.modelix.model.lazy.CLTree import org.modelix.model.lazy.CLVersion import org.modelix.model.operations.OTBranch import org.modelix.model.server.api.ModelQuery +import kotlin.coroutines.cancellation.CancellationException class ReplicatedModel(val client: IModelClientV2, val branchRef: BranchReference, val query: ModelQuery? = null) { private val scope = CoroutineScope(Dispatchers.Default) @@ -62,6 +63,9 @@ class ReplicatedModel(val client: IModelClientV2, val branchRef: BranchReference } as CLVersion remoteVersionReceived(newRemoteVersion) nextDelayMs = 0 + } catch (ex: CancellationException) { + LOG.debug { "Stop to poll branch $branchRef after disposing." } + throw ex } catch (ex: Throwable) { LOG.error(ex) { "Failed to poll branch $branchRef" } nextDelayMs = (nextDelayMs * 3 / 2).coerceIn(1000, 30000) From 7ef24f94bd89e6eefe23129b224f18f5854536f0 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Wed, 6 Sep 2023 13:02:25 +0200 Subject: [PATCH 2/2] feat(model-client): have IModelClientV2 implement Closeable This allows for the cleanup of internal resources like HttpClient on demand. --- gradle/libs.versions.toml | 1 + model-client/build.gradle.kts | 15 ++----- .../modelix/model/client2/IModelClientV2.kt | 5 ++- .../modelix/model/client2/ModelClientV2.kt | 4 ++ .../model/client2/ModelClientV2Test.kt | 45 +++++++++++++++++++ 5 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 model-client/src/commonTest/kotlin/org/modelix/model/client2/ModelClientV2Test.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c107003800..0ffbfed557 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,6 +55,7 @@ ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", v ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" } ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" } ktor-client-js = { group = "io.ktor", name = "ktor-client-js", version.ref = "ktor" } diff --git a/model-client/build.gradle.kts b/model-client/build.gradle.kts index ff30e3ad0e..8d29b83acf 100644 --- a/model-client/build.gradle.kts +++ b/model-client/build.gradle.kts @@ -55,8 +55,9 @@ kotlin { } val commonTest by getting { dependencies { - kotlin("test-common") - kotlin("test-annotations-common") + implementation(libs.ktor.client.mock) + implementation(libs.kotlin.coroutines.test) + implementation(kotlin("test")) } } val jvmMain by getting { @@ -81,11 +82,6 @@ kotlin { implementation(libs.ktor.serialization.json) } } - val jvmTest by getting { - dependencies { - implementation(kotlin("test")) - } - } val jsMain by getting { dependencies { implementation(kotlin("stdlib-js")) @@ -94,11 +90,6 @@ kotlin { implementation(npm("js-base64", "^3.4.5")) } } - val jsTest by getting { - dependencies { - implementation(kotlin("test-js")) - } - } } } diff --git a/model-client/src/commonMain/kotlin/org/modelix/model/client2/IModelClientV2.kt b/model-client/src/commonMain/kotlin/org/modelix/model/client2/IModelClientV2.kt index fe2679864b..b9ff6c2203 100644 --- a/model-client/src/commonMain/kotlin/org/modelix/model/client2/IModelClientV2.kt +++ b/model-client/src/commonMain/kotlin/org/modelix/model/client2/IModelClientV2.kt @@ -13,13 +13,14 @@ */ package org.modelix.model.client2 +import io.ktor.utils.io.core.Closeable import org.modelix.model.IVersion import org.modelix.model.api.IIdGenerator import org.modelix.model.lazy.BranchReference import org.modelix.model.lazy.RepositoryId import org.modelix.model.server.api.ModelQuery -interface IModelClientV2 { +interface IModelClientV2 : Closeable { fun getClientId(): Int fun getIdGenerator(): IIdGenerator fun getUserId(): String? @@ -44,4 +45,6 @@ interface IModelClientV2 { suspend fun poll(branch: BranchReference, lastKnownVersion: IVersion?): IVersion suspend fun poll(branch: BranchReference, lastKnownVersion: IVersion?, filter: ModelQuery): IVersion + + override fun close() } diff --git a/model-client/src/commonMain/kotlin/org/modelix/model/client2/ModelClientV2.kt b/model-client/src/commonMain/kotlin/org/modelix/model/client2/ModelClientV2.kt index a77d9d94e1..255e2ecd75 100644 --- a/model-client/src/commonMain/kotlin/org/modelix/model/client2/ModelClientV2.kt +++ b/model-client/src/commonMain/kotlin/org/modelix/model/client2/ModelClientV2.kt @@ -190,6 +190,10 @@ class ModelClientV2( TODO("Not yet implemented") } + override fun close() { + httpClient.close() + } + private fun createVersion(baseVersion: CLVersion?, delta: VersionDelta): CLVersion { return if (baseVersion == null) { CLVersion( diff --git a/model-client/src/commonTest/kotlin/org/modelix/model/client2/ModelClientV2Test.kt b/model-client/src/commonTest/kotlin/org/modelix/model/client2/ModelClientV2Test.kt new file mode 100644 index 0000000000..93bfcd8b9a --- /dev/null +++ b/model-client/src/commonTest/kotlin/org/modelix/model/client2/ModelClientV2Test.kt @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.modelix.model.client2 + +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respondError +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFailsWith + +class ModelClientV2Test { + + @Test + fun disposeClient() = runTest { + val url = "http://localhost/v2" + val mockEngine = MockEngine { + respondError(HttpStatusCode.NotFound) + } + val httpClient = HttpClient(mockEngine) + val modelClient = ModelClientV2.builder() + .client(httpClient) + .url(url) + .build() + modelClient.close() + assertFailsWith("Parent job is Completed") { + modelClient.init() + } + } +}