diff --git a/build.gradle.kts b/build.gradle.kts index 516f8a6fe..575dbbf0d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,6 @@ plugins { alias(libs.plugins.serialization) apply false alias(libs.plugins.kotlinx.rpc) apply false alias(libs.plugins.atomicfu) apply false - alias(libs.plugins.conventions.kover) alias(libs.plugins.conventions.root) } diff --git a/detekt/config.yaml b/detekt/config.yaml index 2553f4594..1821e9d5d 100644 --- a/detekt/config.yaml +++ b/detekt/config.yaml @@ -219,7 +219,7 @@ empty-blocks: EmptyForBlock: active: true EmptyFunctionBlock: - active: true + active: false ignoreOverridden: false EmptyIfBlock: active: true @@ -288,7 +288,7 @@ exceptions: ThrowingNewInstanceOfSameException: active: true TooGenericExceptionCaught: - active: true + active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] exceptionNames: - 'ArrayIndexOutOfBoundsException' diff --git a/docs/pages/kotlinx-rpc/topics/platforms.topic b/docs/pages/kotlinx-rpc/topics/platforms.topic index 8470157bc..9b6cd14c8 100644 --- a/docs/pages/kotlinx-rpc/topics/platforms.topic +++ b/docs/pages/kotlinx-rpc/topics/platforms.topic @@ -73,7 +73,7 @@ core jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • wasmWasi
  • node
  • +
  • wasmJs
  • browser
  • node
  • wasmWasi
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm32
  • watchosArm64
  • watchosDeviceArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -81,7 +81,7 @@ utils jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • wasmWasi
  • node
  • +
  • wasmJs
  • browser
  • node
  • wasmWasi
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm32
  • watchosArm64
  • watchosDeviceArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -89,7 +89,7 @@ krpc-client jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -97,7 +97,7 @@ krpc-core jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -105,7 +105,7 @@ krpc-logging jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -113,7 +113,7 @@ krpc-server jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -129,7 +129,7 @@ krpc-ktor-client jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -137,7 +137,7 @@ krpc-ktor-core jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -145,7 +145,7 @@ krpc-ktor-server jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -153,7 +153,7 @@ krpc-serialization-cbor jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -161,7 +161,7 @@ krpc-serialization-core jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -169,7 +169,7 @@ krpc-serialization-json jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • @@ -177,7 +177,7 @@ krpc-serialization-protobuf jvm
  • browser
  • node
  • -
  • wasmJs
  • browser
  • d8
  • node
  • +
  • wasmJs
  • browser
  • node
  • apple
  • ios
  • iosArm64
  • iosSimulatorArm64
  • iosX64
  • macos
  • macosArm64
  • macosX64
  • watchos
  • watchosArm64
  • watchosSimulatorArm64
  • watchosX64
  • tvos
  • tvosArm64
  • tvosSimulatorArm64
  • tvosX64
  • linux
  • linuxArm64
  • linuxX64
  • windows
  • mingwX64
  • diff --git a/gradle-conventions/src/main/kotlin/conventions-common.gradle.kts b/gradle-conventions/src/main/kotlin/conventions-common.gradle.kts index 42638476a..78b01a3fc 100644 --- a/gradle-conventions/src/main/kotlin/conventions-common.gradle.kts +++ b/gradle-conventions/src/main/kotlin/conventions-common.gradle.kts @@ -51,11 +51,3 @@ afterEvaluate { }) } } - -apply(plugin = "org.jetbrains.kotlinx.kover") - -val thisProject = project - -rootProject.configurations.matching { it.name == "kover" }.all { - rootProject.dependencies.add("kover", thisProject) -} diff --git a/gradle-conventions/src/main/kotlin/util/apiValidation.kt b/gradle-conventions/src/main/kotlin/util/apiValidation.kt index 5bbf2f661..83b56a064 100644 --- a/gradle-conventions/src/main/kotlin/util/apiValidation.kt +++ b/gradle-conventions/src/main/kotlin/util/apiValidation.kt @@ -11,6 +11,7 @@ import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation private val excludedProjects = setOf( "krpc-test", "krpc-compatibility-tests", + "krpc-protocol-compatibility-tests", "compiler-plugin-tests", ) diff --git a/gradle-conventions/src/main/kotlin/util/kover.kt b/gradle-conventions/src/main/kotlin/util/kover.kt new file mode 100644 index 000000000..ec0d7b5ee --- /dev/null +++ b/gradle-conventions/src/main/kotlin/util/kover.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package util + +import org.gradle.api.Project + +@Suppress("unused") +fun Project.applyKover() { + plugins.apply("org.jetbrains.kotlinx.kover") + + rootProject.configurations.matching { it.name == "kover" }.all { + rootProject.dependencies.add("kover", this@applyKover) + } +} diff --git a/gradle-conventions/src/main/kotlin/util/targets/wasm.kt b/gradle-conventions/src/main/kotlin/util/targets/wasm.kt index 34b3cb171..30e52376a 100644 --- a/gradle-conventions/src/main/kotlin/util/targets/wasm.kt +++ b/gradle-conventions/src/main/kotlin/util/targets/wasm.kt @@ -27,7 +27,8 @@ fun KmpConfig.configureWasm() { browser() nodejs() if (wasmJsD8) { - d8() + // this platform needs some care KRPC-210 +// d8() } binaries.library() diff --git a/kotlin-js-store/package-lock.json b/kotlin-js-store/package-lock.json index b0ddf4130..11922e6e9 100644 --- a/kotlin-js-store/package-lock.json +++ b/kotlin-js-store/package-lock.json @@ -22,6 +22,8 @@ "packages/kotlinx-rpc-krpc-krpc-server-test", "packages/kotlinx-rpc-krpc-krpc-test", "packages/kotlinx-rpc-krpc-krpc-test-test", + "packages/kotlinx-rpc-tests-test-utils", + "packages/kotlinx-rpc-tests-test-utils-test", "packages/kotlinx-rpc-krpc-krpc-ktor-krpc-ktor-client", "packages/kotlinx-rpc-krpc-krpc-ktor-krpc-ktor-client-test", "packages/kotlinx-rpc-krpc-krpc-ktor-krpc-ktor-core", @@ -2597,6 +2599,14 @@ "resolved": "packages/kotlinx-rpc-krpc-krpc-test-test", "link": true }, + "node_modules/kotlinx-rpc-tests-test-utils": { + "resolved": "packages/kotlinx-rpc-tests-test-utils", + "link": true + }, + "node_modules/kotlinx-rpc-tests-test-utils-test": { + "resolved": "packages/kotlinx-rpc-tests-test-utils-test", + "link": true + }, "node_modules/kotlinx-rpc-utils": { "resolved": "packages/kotlinx-rpc-utils", "link": true @@ -4929,6 +4939,29 @@ "webpack-cli": "6.0.1" } }, + "packages/kotlinx-rpc-tests-test-utils": { + "version": "0.10.0-SNAPSHOT", + "devDependencies": {} + }, + "packages/kotlinx-rpc-tests-test-utils-test": { + "version": "0.10.0-SNAPSHOT", + "dependencies": { + "puppeteer": "24.9.0" + }, + "devDependencies": { + "karma": "6.4.4", + "karma-chrome-launcher": "3.2.0", + "karma-mocha": "2.0.1", + "karma-sourcemap-loader": "0.4.0", + "karma-webpack": "5.0.1", + "kotlin-web-helpers": "2.1.0", + "mocha": "11.7.1", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "webpack": "5.100.2", + "webpack-cli": "6.0.1" + } + }, "packages/kotlinx-rpc-utils": { "version": "0.10.0-SNAPSHOT", "devDependencies": {} diff --git a/kotlin-js-store/wasm/package-lock.json b/kotlin-js-store/wasm/package-lock.json index 22a23110d..784ed37d2 100644 --- a/kotlin-js-store/wasm/package-lock.json +++ b/kotlin-js-store/wasm/package-lock.json @@ -22,6 +22,8 @@ "packages/kotlinx-rpc-krpc-krpc-server-test", "packages/kotlinx-rpc-krpc-krpc-test", "packages/kotlinx-rpc-krpc-krpc-test-test", + "packages/kotlinx-rpc-tests-test-utils", + "packages/kotlinx-rpc-tests-test-utils-test", "packages/kotlinx-rpc-krpc-krpc-ktor-krpc-ktor-client", "packages/kotlinx-rpc-krpc-krpc-ktor-krpc-ktor-client-test", "packages/kotlinx-rpc-krpc-krpc-ktor-krpc-ktor-core", @@ -2597,6 +2599,14 @@ "resolved": "packages/kotlinx-rpc-krpc-krpc-test-test", "link": true }, + "node_modules/kotlinx-rpc-tests-test-utils": { + "resolved": "packages/kotlinx-rpc-tests-test-utils", + "link": true + }, + "node_modules/kotlinx-rpc-tests-test-utils-test": { + "resolved": "packages/kotlinx-rpc-tests-test-utils-test", + "link": true + }, "node_modules/kotlinx-rpc-utils": { "resolved": "packages/kotlinx-rpc-utils", "link": true @@ -4916,6 +4926,28 @@ "webpack-cli": "6.0.1" } }, + "packages/kotlinx-rpc-tests-test-utils": { + "version": "0.10.0-SNAPSHOT", + "devDependencies": {} + }, + "packages/kotlinx-rpc-tests-test-utils-test": { + "version": "0.10.0-SNAPSHOT", + "dependencies": { + "puppeteer": "24.9.0" + }, + "devDependencies": { + "karma": "6.4.4", + "karma-chrome-launcher": "3.2.0", + "karma-mocha": "2.0.1", + "karma-sourcemap-loader": "0.4.0", + "karma-webpack": "5.0.1", + "kotlin-web-helpers": "2.1.0", + "mocha": "11.7.1", + "source-map-loader": "5.0.0", + "webpack": "5.100.2", + "webpack-cli": "6.0.1" + } + }, "packages/kotlinx-rpc-utils": { "version": "0.10.0-SNAPSHOT", "devDependencies": {} diff --git a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/KrpcClient.kt b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/KrpcClient.kt index 562249e6b..57c28d844 100644 --- a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/KrpcClient.kt +++ b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/KrpcClient.kt @@ -5,12 +5,27 @@ package kotlinx.rpc.krpc.client import kotlinx.atomicfu.atomic -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.rpc.RpcCall @@ -21,7 +36,8 @@ import kotlinx.rpc.descriptor.RpcInvokator import kotlinx.rpc.internal.utils.InternalRpcApi import kotlinx.rpc.internal.utils.getOrNull import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap -import kotlinx.rpc.krpc.* +import kotlinx.rpc.krpc.KrpcConfig +import kotlinx.rpc.krpc.KrpcTransport import kotlinx.rpc.krpc.client.internal.ClientStreamContext import kotlinx.rpc.krpc.client.internal.ClientStreamSerializer import kotlinx.rpc.krpc.client.internal.KrpcClientConnector @@ -35,7 +51,6 @@ import kotlinx.serialization.StringFormat import kotlinx.serialization.modules.SerializersModule import kotlin.collections.first import kotlin.concurrent.Volatile -import kotlin.coroutines.cancellation.CancellationException import kotlin.properties.Delegates /** @@ -157,7 +172,7 @@ public abstract class KrpcClient : RpcClient, KrpcEndpoint { private val connector by lazy { checkTransportReadiness() - KrpcClientConnector(config.serialFormatInitializer.build(), transport, config.waitForServices) + KrpcClientConnector(config.serialFormatInitializer.build(), transport, config.connector) } private var connectionId: Long? = null @@ -221,9 +236,11 @@ public abstract class KrpcClient : RpcClient, KrpcEndpoint { message.errorMessage } - serverSupportedPlugins.completeExceptionally( - IllegalStateException("Server failed to process protocol message: ${message.failedMessage}") - ) + if (!serverSupportedPlugins.isCompleted) { + serverSupportedPlugins.completeExceptionally( + IllegalStateException("Server failed to process protocol message: ${message.failedMessage}") + ) + } } } } @@ -295,15 +312,15 @@ public abstract class KrpcClient : RpcClient, KrpcEndpoint { sendCancellation(CancellationType.REQUEST, call.descriptor.fqName, callId) - connector.unsubscribeFromMessages(call.descriptor.fqName, callId) { - cancellingRequests.remove(callId) - } + connector.unsubscribeFromMessages(call.descriptor.fqName, callId) + cancellingRequests.remove(callId) } throw e } finally { channel.close() requestChannels.remove(callId) + connector.unsubscribeFromMessages(call.descriptor.fqName, callId) } } } @@ -341,17 +358,8 @@ public abstract class KrpcClient : RpcClient, KrpcEndpoint { } is KrpcCallMessage.CallException -> { - val cause = runCatching { - message.cause.deserialize() - } - - val result = if (cause.isFailure) { - cause.exceptionOrNull()!! - } else { - cause.getOrNull()!! - } - - channel.close(result) + val cause = message.cause.deserialize() + channel.close(cause) } is KrpcCallMessage.CallSuccess, is KrpcCallMessage.StreamMessage -> { @@ -456,8 +464,23 @@ public abstract class KrpcClient : RpcClient, KrpcEndpoint { serviceTypeString = serviceTypeString, ) } catch (e: CancellationException) { + currentCoroutineContext().ensureActive() + + val wrapped = ManualCancellationException(e) + val serializedReason = serializeException(wrapped) + val message = KrpcCallMessage.StreamCancel( + callId = outgoingStream.callId, + serviceType = serviceTypeString, + streamId = outgoingStream.streamId, + cause = serializedReason, + connectionId = outgoingStream.connectionId, + serviceId = outgoingStream.serviceId, + ) + sender.sendMessage(message) + + // stop the flow and its coroutine, other flows are not affected throw e - } catch (@Suppress("detekt.TooGenericExceptionCaught") cause: Throwable) { + } catch (cause: Throwable) { val serializedReason = serializeException(cause) val message = KrpcCallMessage.StreamCancel( callId = outgoingStream.callId, diff --git a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/KrpcClientConnector.kt b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/KrpcClientConnector.kt index ebee09b5c..f0fd313b2 100644 --- a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/KrpcClientConnector.kt +++ b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/KrpcClientConnector.kt @@ -1,43 +1,27 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.krpc.client.internal +import kotlinx.rpc.krpc.KrpcConfig import kotlinx.rpc.krpc.KrpcTransport import kotlinx.rpc.krpc.internal.* import kotlinx.serialization.SerialFormat -internal sealed interface CallSubscriptionId { - data class Service( - val serviceTypeString: String, - val callId: String, - ) : CallSubscriptionId - - data object Protocol : CallSubscriptionId - - data object Generic : CallSubscriptionId -} - internal class KrpcClientConnector private constructor( - private val connector: KrpcConnector + private val connector: KrpcConnector ) : KrpcMessageSender by connector { constructor( serialFormat: SerialFormat, transport: KrpcTransport, - waitForServices: Boolean = false, + config: KrpcConfig.Connector, ) : this( - KrpcConnector(serialFormat, transport, waitForServices, isServer = false) { - when (this) { - is KrpcCallMessage -> CallSubscriptionId.Service(serviceType, callId) - is KrpcProtocolMessage -> CallSubscriptionId.Protocol - is KrpcGenericMessage -> CallSubscriptionId.Generic - } - } + KrpcConnector(serialFormat, transport, config, isServer = false) ) - fun unsubscribeFromMessages(serviceTypeString: String, callId: String, callback: () -> Unit = {}) { - connector.unsubscribeFromMessages(CallSubscriptionId.Service(serviceTypeString, callId), callback) + suspend fun unsubscribeFromMessages(serviceTypeString: String, callId: String) { + connector.unsubscribeFromMessages(HandlerKey.ServiceCall(serviceTypeString, callId)) } suspend fun subscribeToCallResponse( @@ -45,21 +29,20 @@ internal class KrpcClientConnector private constructor( callId: String, subscription: suspend (KrpcCallMessage) -> Unit, ) { - connector.subscribeToMessages(CallSubscriptionId.Service(serviceTypeString, callId)) { - subscription(it as KrpcCallMessage) + connector.subscribeToMessages(HandlerKey.ServiceCall(serviceTypeString, callId)) { + subscription(it) } } suspend fun subscribeToProtocolMessages(subscription: suspend (KrpcProtocolMessage) -> Unit) { - connector.subscribeToMessages(CallSubscriptionId.Protocol) { - subscription(it as KrpcProtocolMessage) + connector.subscribeToMessages(HandlerKey.Protocol) { + subscription(it) } } - @Suppress("unused") suspend fun subscribeToGenericMessages(subscription: suspend (KrpcGenericMessage) -> Unit) { - connector.subscribeToMessages(CallSubscriptionId.Generic) { - subscription(it as KrpcGenericMessage) + connector.subscribeToMessages(HandlerKey.Generic) { + subscription(it) } } } diff --git a/krpc/krpc-core/api/krpc-core.api b/krpc/krpc-core/api/krpc-core.api index 7b7696b17..c3f4c2cbf 100644 --- a/krpc/krpc-core/api/krpc-core.api +++ b/krpc/krpc-core/api/krpc-core.api @@ -1,19 +1,34 @@ public abstract interface class kotlinx/rpc/krpc/KrpcConfig { + public abstract fun getConnector ()Lkotlinx/rpc/krpc/KrpcConfig$Connector; public abstract fun getSerialFormatInitializer ()Lkotlinx/rpc/krpc/serialization/KrpcSerialFormatBuilder; public abstract fun getWaitForServices ()Z } public final class kotlinx/rpc/krpc/KrpcConfig$Client : kotlinx/rpc/krpc/KrpcConfig { + public fun getConnector ()Lkotlinx/rpc/krpc/KrpcConfig$Connector; public fun getSerialFormatInitializer ()Lkotlinx/rpc/krpc/serialization/KrpcSerialFormatBuilder; public fun getWaitForServices ()Z } +public final class kotlinx/rpc/krpc/KrpcConfig$Connector { + public final fun getCallTimeout-UwyO8pc ()J + public final fun getPerCallBufferSize ()I + public final fun getWaitTimeout-UwyO8pc ()J +} + +public final class kotlinx/rpc/krpc/KrpcConfig$DefaultImpls { + public static fun getWaitForServices (Lkotlinx/rpc/krpc/KrpcConfig;)Z +} + public final class kotlinx/rpc/krpc/KrpcConfig$Server : kotlinx/rpc/krpc/KrpcConfig { + public fun getConnector ()Lkotlinx/rpc/krpc/KrpcConfig$Connector; public fun getSerialFormatInitializer ()Lkotlinx/rpc/krpc/serialization/KrpcSerialFormatBuilder; public fun getWaitForServices ()Z } public abstract class kotlinx/rpc/krpc/KrpcConfigBuilder { + public final fun buildConnector ()Lkotlinx/rpc/krpc/KrpcConfig$Connector; + public final fun connector (Lkotlin/jvm/functions/Function1;)V public final fun getWaitForServices ()Z protected final fun rpcSerialFormat ()Lkotlinx/rpc/krpc/serialization/KrpcSerialFormatBuilder; public final fun serialization (Lkotlin/jvm/functions/Function1;)V @@ -25,6 +40,17 @@ public final class kotlinx/rpc/krpc/KrpcConfigBuilder$Client : kotlinx/rpc/krpc/ public final fun build ()Lkotlinx/rpc/krpc/KrpcConfig$Client; } +public final class kotlinx/rpc/krpc/KrpcConfigBuilder$Connector { + public fun ()V + public final fun dontWait-UwyO8pc ()J + public final fun getCallTimeout-UwyO8pc ()J + public final fun getPerCallBufferSize ()I + public final fun getWaitTimeout-UwyO8pc ()J + public final fun setCallTimeout-LRDsOJo (J)V + public final fun setPerCallBufferSize (I)V + public final fun setWaitTimeout-LRDsOJo (J)V +} + public final class kotlinx/rpc/krpc/KrpcConfigBuilder$Server : kotlinx/rpc/krpc/KrpcConfigBuilder { public fun ()V public final fun build ()Lkotlinx/rpc/krpc/KrpcConfig$Server; diff --git a/krpc/krpc-core/api/krpc-core.klib.api b/krpc/krpc-core/api/krpc-core.klib.api index 3193d1b57..ac0c04c74 100644 --- a/krpc/krpc-core/api/krpc-core.klib.api +++ b/krpc/krpc-core/api/krpc-core.klib.api @@ -12,23 +12,34 @@ abstract interface kotlinx.rpc.krpc/KrpcTransport : kotlinx.coroutines/Coroutine } sealed interface kotlinx.rpc.krpc/KrpcConfig { // kotlinx.rpc.krpc/KrpcConfig|null[0] + abstract val connector // kotlinx.rpc.krpc/KrpcConfig.connector|{}connector[0] + abstract fun (): kotlinx.rpc.krpc/KrpcConfig.Connector // kotlinx.rpc.krpc/KrpcConfig.connector.|(){}[0] abstract val serialFormatInitializer // kotlinx.rpc.krpc/KrpcConfig.serialFormatInitializer|{}serialFormatInitializer[0] abstract fun (): kotlinx.rpc.krpc.serialization/KrpcSerialFormatBuilder<*, *> // kotlinx.rpc.krpc/KrpcConfig.serialFormatInitializer.|(){}[0] - abstract val waitForServices // kotlinx.rpc.krpc/KrpcConfig.waitForServices|{}waitForServices[0] - abstract fun (): kotlin/Boolean // kotlinx.rpc.krpc/KrpcConfig.waitForServices.|(){}[0] + open val waitForServices // kotlinx.rpc.krpc/KrpcConfig.waitForServices|{}waitForServices[0] + open fun (): kotlin/Boolean // kotlinx.rpc.krpc/KrpcConfig.waitForServices.|(){}[0] final class Client : kotlinx.rpc.krpc/KrpcConfig { // kotlinx.rpc.krpc/KrpcConfig.Client|null[0] + final val connector // kotlinx.rpc.krpc/KrpcConfig.Client.connector|{}connector[0] + final fun (): kotlinx.rpc.krpc/KrpcConfig.Connector // kotlinx.rpc.krpc/KrpcConfig.Client.connector.|(){}[0] final val serialFormatInitializer // kotlinx.rpc.krpc/KrpcConfig.Client.serialFormatInitializer|{}serialFormatInitializer[0] final fun (): kotlinx.rpc.krpc.serialization/KrpcSerialFormatBuilder<*, *> // kotlinx.rpc.krpc/KrpcConfig.Client.serialFormatInitializer.|(){}[0] - final val waitForServices // kotlinx.rpc.krpc/KrpcConfig.Client.waitForServices|{}waitForServices[0] - final fun (): kotlin/Boolean // kotlinx.rpc.krpc/KrpcConfig.Client.waitForServices.|(){}[0] + } + + final class Connector { // kotlinx.rpc.krpc/KrpcConfig.Connector|null[0] + final val callTimeout // kotlinx.rpc.krpc/KrpcConfig.Connector.callTimeout|{}callTimeout[0] + final fun (): kotlin.time/Duration // kotlinx.rpc.krpc/KrpcConfig.Connector.callTimeout.|(){}[0] + final val perCallBufferSize // kotlinx.rpc.krpc/KrpcConfig.Connector.perCallBufferSize|{}perCallBufferSize[0] + final fun (): kotlin/Int // kotlinx.rpc.krpc/KrpcConfig.Connector.perCallBufferSize.|(){}[0] + final val waitTimeout // kotlinx.rpc.krpc/KrpcConfig.Connector.waitTimeout|{}waitTimeout[0] + final fun (): kotlin.time/Duration // kotlinx.rpc.krpc/KrpcConfig.Connector.waitTimeout.|(){}[0] } final class Server : kotlinx.rpc.krpc/KrpcConfig { // kotlinx.rpc.krpc/KrpcConfig.Server|null[0] + final val connector // kotlinx.rpc.krpc/KrpcConfig.Server.connector|{}connector[0] + final fun (): kotlinx.rpc.krpc/KrpcConfig.Connector // kotlinx.rpc.krpc/KrpcConfig.Server.connector.|(){}[0] final val serialFormatInitializer // kotlinx.rpc.krpc/KrpcConfig.Server.serialFormatInitializer|{}serialFormatInitializer[0] final fun (): kotlinx.rpc.krpc.serialization/KrpcSerialFormatBuilder<*, *> // kotlinx.rpc.krpc/KrpcConfig.Server.serialFormatInitializer.|(){}[0] - final val waitForServices // kotlinx.rpc.krpc/KrpcConfig.Server.waitForServices|{}waitForServices[0] - final fun (): kotlin/Boolean // kotlinx.rpc.krpc/KrpcConfig.Server.waitForServices.|(){}[0] } } @@ -53,6 +64,8 @@ sealed class kotlinx.rpc.krpc/KrpcConfigBuilder { // kotlinx.rpc.krpc/KrpcConfig final fun (): kotlin/Boolean // kotlinx.rpc.krpc/KrpcConfigBuilder.waitForServices.|(){}[0] final fun (kotlin/Boolean) // kotlinx.rpc.krpc/KrpcConfigBuilder.waitForServices.|(kotlin.Boolean){}[0] + final fun buildConnector(): kotlinx.rpc.krpc/KrpcConfig.Connector // kotlinx.rpc.krpc/KrpcConfigBuilder.buildConnector|buildConnector(){}[0] + final fun connector(kotlin/Function1) // kotlinx.rpc.krpc/KrpcConfigBuilder.connector|connector(kotlin.Function1){}[0] final fun rpcSerialFormat(): kotlinx.rpc.krpc.serialization/KrpcSerialFormatBuilder<*, *> // kotlinx.rpc.krpc/KrpcConfigBuilder.rpcSerialFormat|rpcSerialFormat(){}[0] final fun serialization(kotlin/Function1) // kotlinx.rpc.krpc/KrpcConfigBuilder.serialization|serialization(kotlin.Function1){}[0] @@ -62,6 +75,22 @@ sealed class kotlinx.rpc.krpc/KrpcConfigBuilder { // kotlinx.rpc.krpc/KrpcConfig final fun build(): kotlinx.rpc.krpc/KrpcConfig.Client // kotlinx.rpc.krpc/KrpcConfigBuilder.Client.build|build(){}[0] } + final class Connector { // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector|null[0] + constructor () // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.|(){}[0] + + final var callTimeout // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.callTimeout|{}callTimeout[0] + final fun (): kotlin.time/Duration // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.callTimeout.|(){}[0] + final fun (kotlin.time/Duration) // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.callTimeout.|(kotlin.time.Duration){}[0] + final var perCallBufferSize // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.perCallBufferSize|{}perCallBufferSize[0] + final fun (): kotlin/Int // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.perCallBufferSize.|(){}[0] + final fun (kotlin/Int) // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.perCallBufferSize.|(kotlin.Int){}[0] + final var waitTimeout // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.waitTimeout|{}waitTimeout[0] + final fun (): kotlin.time/Duration // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.waitTimeout.|(){}[0] + final fun (kotlin.time/Duration) // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.waitTimeout.|(kotlin.time.Duration){}[0] + + final fun dontWait(): kotlin.time/Duration // kotlinx.rpc.krpc/KrpcConfigBuilder.Connector.dontWait|dontWait(){}[0] + } + final class Server : kotlinx.rpc.krpc/KrpcConfigBuilder { // kotlinx.rpc.krpc/KrpcConfigBuilder.Server|null[0] constructor () // kotlinx.rpc.krpc/KrpcConfigBuilder.Server.|(){}[0] diff --git a/krpc/krpc-core/build.gradle.kts b/krpc/krpc-core/build.gradle.kts index d2ea2a8d0..36889112f 100644 --- a/krpc/krpc-core/build.gradle.kts +++ b/krpc/krpc-core/build.gradle.kts @@ -2,6 +2,9 @@ * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmTest + plugins { alias(libs.plugins.conventions.kmp) alias(libs.plugins.serialization) @@ -24,5 +27,28 @@ kotlin { implementation(libs.kotlin.reflect) } } + + commonTest { + dependencies { + implementation(projects.tests.testUtils) + + implementation(libs.kotlin.test) + implementation(libs.coroutines.test) + implementation(libs.serialization.json) + } + } + + jvmTest { + dependencies { + implementation(libs.coroutines.debug) + implementation(libs.lincheck) + implementation(libs.logback.classic) + } + } } } + +tasks.withType { + // lincheck agent + jvmArgs("-XX:+EnableDynamicAgentLoading") +} diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcConfig.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcConfig.kt index 566afada2..3ba88ceae 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcConfig.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcConfig.kt @@ -7,6 +7,8 @@ package kotlinx.rpc.krpc import kotlinx.rpc.krpc.serialization.KrpcSerialFormat import kotlinx.rpc.krpc.serialization.KrpcSerialFormatBuilder import kotlinx.rpc.krpc.serialization.KrpcSerialFormatConfiguration +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds /** * Builder for [KrpcConfig]. Provides DSL to configure parameters for KrpcClient and/or KrpcServer. @@ -32,11 +34,60 @@ public sealed class KrpcConfigBuilder protected constructor() { } /** - * A flag indicating whether a client or a server should wait for subscribers - * if no service is available to process a message immediately. - * If `false`, the endpoint that sent the unprocessed message will receive a call exception - * saying there were no services to process the message. + * DSL for connector configuration. + * + * Connector is responsible for handling all messages transferring. + * Example usage: + * ```kotlin + * connector { + * waitTimeout = 10.seconds + * callTimeout = 10.seconds + * perCallBufferSize = 1000 + * } + * ``` */ + public fun connector(builder: Connector.() -> Unit) { + connector.builder() + } + + /** + * Configuration for RPC connector - a handler for all messages transferring. + */ + public class Connector { + /** + * A flag indicating how long a client or a server should wait for subscribers + * if no service is available to process a message immediately. + * If negative ([dontWait]) or when timeout is exceeded, + * the endpoint that sent the unprocessed message will receive a call exception + * saying there were no services to process the message. + */ + public var waitTimeout: Duration = Duration.INFINITE + + /** + * A flag indicating that a client or a server should not wait for subscribers + * + * @see Connector.waitTimeout + */ + public fun dontWait(): Duration = (-1).seconds + + /** + * A timeout for a call. + * If a call is not completed in this time, it will be cancelled with a call exception. + */ + public var callTimeout: Duration = Duration.INFINITE + + /** + * A buffer size for a single call. + * + * The default value is 1, + * meaning that only after one message is handled - the next one will be sent. + * + * This buffer also applies to how many messages are cached with [waitTimeout] + */ + public var perCallBufferSize: Int = 1 + } + + @Deprecated("Use connector { } instead", level = DeprecationLevel.ERROR) public var waitForServices: Boolean = true /** @@ -46,7 +97,7 @@ public sealed class KrpcConfigBuilder protected constructor() { public fun build(): KrpcConfig.Client { return KrpcConfig.Client( serialFormatInitializer = rpcSerialFormat(), - waitForServices = waitForServices, + connector = buildConnector(), ) } } @@ -58,7 +109,7 @@ public sealed class KrpcConfigBuilder protected constructor() { public fun build(): KrpcConfig.Server { return KrpcConfig.Server( serialFormatInitializer = rpcSerialFormat(), - waitForServices = waitForServices, + connector = buildConnector(), ) } } @@ -73,6 +124,12 @@ public sealed class KrpcConfigBuilder protected constructor() { private var serialFormatInitializer: KrpcSerialFormatBuilder<*, *>? = null + private val connector = Connector() + + public fun buildConnector(): KrpcConfig.Connector { + return KrpcConfig.Connector(connector.waitTimeout, connector.callTimeout, connector.perCallBufferSize) + } + private val configuration = object : KrpcSerialFormatConfiguration { override fun register(rpcSerialFormatInitializer: KrpcSerialFormatBuilder.Binary<*, *>) { serialFormatInitializer = rpcSerialFormatInitializer @@ -101,9 +158,36 @@ public sealed interface KrpcConfig { public val serialFormatInitializer: KrpcSerialFormatBuilder<*, *> /** - * @see KrpcConfigBuilder.waitForServices + * @see KrpcConfigBuilder.connector */ - public val waitForServices: Boolean + public val connector: Connector + + @Deprecated("Use connector instead", level = DeprecationLevel.ERROR) + public val waitForServices: Boolean get() = true + + /** + * @see KrpcConfigBuilder.connector + */ + public class Connector internal constructor( + /** + * @see KrpcConfigBuilder.Connector.waitTimeout + */ + public val waitTimeout: Duration, + + /** + * @see KrpcConfigBuilder.Connector.callTimeout + */ + public val callTimeout: Duration, + + /** + * @see KrpcConfigBuilder.Connector.perCallBufferSize + */ + public val perCallBufferSize: Int, + ) { + init { + require(perCallBufferSize != 0) { "perCallBufferSize must not be zero" } + } + } /** * @see [KrpcConfig] @@ -113,10 +197,8 @@ public sealed interface KrpcConfig { * @see KrpcConfigBuilder.serialization */ override val serialFormatInitializer: KrpcSerialFormatBuilder<*, *>, - /** - * @see KrpcConfigBuilder.waitForServices - */ - override val waitForServices: Boolean, + + override val connector: Connector, ) : KrpcConfig /** @@ -127,10 +209,11 @@ public sealed interface KrpcConfig { * @see KrpcConfigBuilder.serialization */ override val serialFormatInitializer: KrpcSerialFormatBuilder<*, *>, + /** - * @see KrpcConfigBuilder.waitForServices + * @see KrpcConfigBuilder.connector */ - override val waitForServices: Boolean, + override val connector: Connector, ) : KrpcConfig } diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcTransport.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcTransport.kt index 563913f19..a70b816b6 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcTransport.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcTransport.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.krpc diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/BufferResult.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/BufferResult.kt new file mode 100644 index 000000000..9e4398df5 --- /dev/null +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/BufferResult.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.internal + +internal sealed interface BufferResult { + class Success(val message: T) : BufferResult + class Failure : BufferResult + class Closed(val cause: Throwable?) : BufferResult +} + +internal fun BufferResult.getOrNull(): T? { + return if (this is BufferResult.Success) message else null +} + +internal inline fun BufferResult.onFailure(body: () -> Unit): BufferResult { + if (this is BufferResult.Failure) { + body() + } + + return this +} + +internal inline fun BufferResult.onClosed(body: (Throwable?) -> Unit): BufferResult { + if (this is BufferResult.Closed) { + body(cause) + } + + return this +} + +internal inline val BufferResult<*>.isFailure: Boolean + get() = this is BufferResult.Failure + +internal inline val BufferResult<*>.isSuccess: Boolean + get() = this is BufferResult.Success + +internal inline val BufferResult<*>.isClosed: Boolean + get() = this is BufferResult.Closed diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.kt index 29f3dba64..10666295c 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.kt @@ -4,6 +4,7 @@ package kotlinx.rpc.krpc.internal +import kotlinx.coroutines.CancellationException import kotlinx.rpc.internal.rpcInternalTypeName import kotlinx.rpc.internal.utils.InternalRpcApi @@ -19,8 +20,46 @@ public fun serializeException(cause: Throwable): SerializedException { internal expect fun Throwable.stackElements(): List +internal expect fun SerializedException.deserializeUnsafe(): Throwable + +internal fun SerializedException.nonJvmManualCancellationExceptionDeserialize(): ManualCancellationException? { + if (className == ManualCancellationException::class.rpcInternalTypeName) { + val cancellation = cause?.deserializeUnsafe() + ?: error("ManualCancellationException must have a cause") + + return ManualCancellationException( + CancellationException( + message = cancellation.message, + cause = cancellation.cause, + ) + ) + } + + return null +} + +@InternalRpcApi +public fun SerializedException.deserialize(): Throwable { + val cause = runCatching { + deserializeUnsafe() + } + + val result = if (cause.isFailure) { + cause.exceptionOrNull()!! + } else { + val ex = cause.getOrNull()!! + if (ex is ManualCancellationException) { + ex.cause + } else { + ex + } + } + + return result +} + @InternalRpcApi -public expect fun SerializedException.deserialize(): Throwable +public class ManualCancellationException(override val cause: CancellationException): RuntimeException() internal expect class DeserializedException( toStringMessage: String, @@ -31,3 +70,8 @@ internal expect class DeserializedException( ) : Throwable { override val message: String } + +internal fun illegalStateException(message: String, cause: Throwable? = null): IllegalStateException = when (cause) { + null -> IllegalStateException(message) + else -> IllegalStateException(message, cause) +} diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcConnector.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcConnector.kt index bf4808223..a86001ea6 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcConnector.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcConnector.kt @@ -4,63 +4,86 @@ package kotlinx.rpc.krpc.internal -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedSendChannelException import kotlinx.coroutines.delay +import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.rpc.internal.internalRpcError import kotlinx.rpc.internal.utils.InternalRpcApi import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap +import kotlinx.rpc.krpc.KrpcConfig import kotlinx.rpc.krpc.KrpcTransport import kotlinx.rpc.krpc.KrpcTransportMessage import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLoggerContainer import kotlinx.rpc.krpc.receiveCatching import kotlinx.serialization.* -import kotlin.time.Duration.Companion.seconds @InternalRpcApi -public interface KrpcMessageSender : CoroutineScope { +public interface KrpcMessageSender { + public val transportScope: CoroutineScope + public suspend fun sendMessage(message: KrpcMessage) + + public fun drainSendQueueAndClose(message: String) } -private typealias KrpcMessageHandler = suspend (KrpcMessage) -> Unit +internal typealias KrpcMessageSubscription = suspend (Message) -> Unit /** * Represents a connector for remote procedure call (RPC) communication. * This class is responsible for sending and receiving [KrpcMessage] over a specified transport. * - * @param SubscriptionKey the type of the subscription key used for message subscriptions. - * @param serialFormat the serial format used for encoding and decoding [KrpcMessage]. - * @param transport the transport used for sending and receiving encoded RPC messages. - * @param waitForSubscribers a flag indicating whether the connector should wait for subscribers - * if no service is available to process the message immediately. - * If false, the endpoint that sent the message will receive a [KrpcCallMessage.CallException] - * that says that there were no services to process its message. - * @param isServer flag indication whether this is a server or a client. - * @param getKey a lambda function that returns the subscription key for a given [KrpcCallMessage]. * DO NOT use actual dumper in production! */ @InternalRpcApi -public class KrpcConnector( +public class KrpcConnector( private val serialFormat: SerialFormat, private val transport: KrpcTransport, - private val waitForSubscribers: Boolean = true, + private val config: KrpcConfig.Connector, isServer: Boolean, - private val getKey: KrpcMessage.() -> SubscriptionKey, -) : KrpcMessageSender, CoroutineScope by transport { +) : KrpcMessageSender { + override val transportScope: CoroutineScope = transport + private val role = if (isServer) SERVER_ROLE else CLIENT_ROLE - private val logger = RpcInternalCommonLogger.logger(rpcInternalObjectId(role)) + private val logger = RpcInternalCommonLogger.logger("$role Connector") + + private val receiveHandlers = RpcInternalConcurrentHashMap, KrpcReceiveHandler>() + private val keyLocks = RpcInternalConcurrentHashMap, Mutex>() + private val serviceSubscriptions = + RpcInternalConcurrentHashMap>() - private val waiting = RpcInternalConcurrentHashMap>() - private val subscriptions = RpcInternalConcurrentHashMap() - private val processWaitersLocks = RpcInternalConcurrentHashMap() + private val sendHandlers = RpcInternalConcurrentHashMap, KrpcSendHandler>() + private val sendChannel = Channel(Channel.UNLIMITED) + + private var receiveBufferSize: Int? = null + private var sendBufferSize: Int? = null + + private var peerSupportsBackPressure = false private val dumpLogger by lazy { RpcInternalDumpLoggerContainer.provide() } + // prevent errors ping-pong + private suspend fun sendMessageIgnoreClosure(message: KrpcMessage) { + try { + sendMessage(message) + } catch (_: ClosedSendChannelException) { + // ignore + } + } + override suspend fun sendMessage(message: KrpcMessage) { + if (message is KrpcProtocolMessage.Handshake) { + message.pluginParams[KrpcPluginKey.WINDOW_UPDATE] = "${config.perCallBufferSize}" + } + val transportMessage = when (serialFormat) { is StringFormat -> { KrpcTransportMessage.StringMessage(serialFormat.encodeToString(message)) @@ -79,177 +102,442 @@ public class KrpcConnector( dumpLogger.dump(role, SEND_PHASE) { transportMessage.dump() } } - transport.send(transportMessage) + sendTransportMessage(message.handlerKey(), transportMessage) + } + + private suspend fun sendTransportMessage(key: HandlerKey<*>, message: KrpcTransportMessage) { + sendHandlers.computeIfAbsent(key) { + KrpcSendHandler(sendChannel).also { + // - fallback to unlimited buffer if no buffer size is specified + // this is for backwards compatibility + // - use unlimited size for protocol messages + val initialSize = when (key) { + HandlerKey.Protocol, HandlerKey.Generic -> -1 + else -> sendBufferSize ?: -1 + } + + it.updateWindowSize(initialSize) + } + }.sendMessage(message) } - public fun unsubscribeFromMessages(key: SubscriptionKey, callback: () -> Unit = {}) { - launch(CoroutineName("krpc-connector-unsubscribe-$key")) { - delay(15.seconds) - subscriptions.remove(key) - processWaitersLocks.remove(key) - }.invokeOnCompletion { + public fun unsubscribeFromMessagesAsync(key: HandlerKey<*>, callback: suspend () -> Unit = {}) { + if (!keyLocks.containsKey(key)) { + println("Key $key is not registered") + return + } + + transportScope.launch(CoroutineName("krpc-connector-unsubscribe-$key")) { + unsubscribeFromMessages(key) + callback() } } - public suspend fun subscribeToMessages(key: SubscriptionKey, handler: KrpcMessageHandler) { - subscriptions[key] = handler - processWaiters(key, handler) + public suspend fun unsubscribeFromMessages(key: HandlerKey<*>) { + withLockForKey(key, createKey = false) { + if (key is HandlerKey.Service) { + receiveHandlers.withKeys { keys -> + keys + .filter { it is HandlerKey.ServiceCall && it.serviceType == key.serviceType } + .forEach { + cleanForKey(it) + } + } + + serviceSubscriptions.remove(key) + } else { + cleanForKey(key) + } + } + } + + private fun cleanForKey(key: HandlerKey<*>) { + sendHandlers.remove(key)?.close(null) + receiveHandlers.remove(key)?.close(key, null) + keyLocks.remove(key) + } + + public suspend fun subscribeToMessages( + key: HandlerKey, + subscription: KrpcMessageSubscription, + ) { + withLockForKey(key, createKey = true) { + if (key is HandlerKey.Service) { + serviceSubscriptions.computeIfAbsent(key) { + @Suppress("UNCHECKED_CAST") + subscription as KrpcMessageSubscription + } + + receiveHandlers.withKeys { keys -> + keys + .filter { it is HandlerKey.ServiceCall && it.serviceType == key.serviceType } + .forEach { + @Suppress("UNCHECKED_CAST") + subscribeWithActingHandlerPerTrack(it as HandlerKey, subscription) + } + } + } else { + subscribeWithActingHandlerPerTrack(key, subscription) + } + } + } + + // per track: + // - call id is a track + // - generic is a track + // - protocol is a track + // - service is **not** a track + private fun subscribeWithActingHandlerPerTrack( + key: HandlerKey, + subscription: KrpcMessageSubscription, + ) { + val storingHandler = handlerFor(key) + + if (storingHandler !is KrpcStoringReceiveHandler) { + internalRpcError("Already subscribed to messages with key $key") + } + + @Suppress("UNCHECKED_CAST") + val handler = KrpcActingReceiveHandler( + callHandler = subscription as KrpcMessageSubscription, + storingHandler = storingHandler, + key = key, + sender = this, + timeout = config.callTimeout, + broadcastUpdates = peerSupportsBackPressure, + ) + + receiveHandlers[key] = handler + } + + private fun handlerFor(key: HandlerKey): KrpcReceiveHandler { + if (key is HandlerKey.Service) { + internalRpcError("Wrong key type for a handler: $key") + } + + return receiveHandlers.computeIfAbsent(key) { + val storing = KrpcStoringReceiveHandler( + buffer = KrpcReceiveBuffer { + // - fallback to unlimited buffer if no buffer size is specified + // this is for backwards compatibility + // - use unlimited size for protocol messages + when (key) { + HandlerKey.Protocol, HandlerKey.Generic -> Channel.UNLIMITED + else -> receiveBufferSize ?: Channel.UNLIMITED + } + }, + sender = this, + ) + + if (key is HandlerKey.ServiceCall && role == SERVER_ROLE) { + val serviceKey = HandlerKey.Service(key.serviceType) + val subscription = serviceSubscriptions[serviceKey] + if (subscription != null) { + @Suppress("UNCHECKED_CAST") + KrpcActingReceiveHandler( + callHandler = subscription as KrpcMessageSubscription, + storingHandler = storing, + key = key, + sender = this, + timeout = config.callTimeout, + broadcastUpdates = peerSupportsBackPressure, + ) + } else { + storing + } + } else { + storing + } + } + } + + @Suppress("JoinDeclarationAndAssignment") + private val sendJob: Job + + override fun drainSendQueueAndClose(message: String) { + // don't close receive handlers, as + // we don't want to send 'unprocessed messages' messages + // because peer is closing entirely anyway + // + // don't close send handlers, as we want to process them all + + // close for receiving new messages + sendChannel.close() + + sendJob.invokeOnCompletion { + transportScope.cancel(message) + } } init { - launch(CoroutineName("krpc-connector-receive-loop")) { + sendJob = transportScope.launch(CoroutineName("krpc-connector-send-loop")) { + while (true) { + transport.send(sendChannel.receiveCatching().getOrNull() ?: break) + } + } + + transportScope.launch(CoroutineName("krpc-connector-receive-loop")) { while (true) { processMessage(transport.receiveCatching().getOrNull() ?: break) } } + + transportScope.coroutineContext.job.invokeOnCompletion { + receiveHandlers.clear() + keyLocks.clear() + serviceSubscriptions.clear() + sendHandlers.clear() + } } - private suspend fun processMessage(transportMessage: KrpcTransportMessage) { - val message: KrpcMessage = when { - serialFormat is StringFormat && transportMessage is KrpcTransportMessage.StringMessage -> { - serialFormat.decodeFromString(transportMessage.value) - } + private fun decodeMessage(transportMessage: KrpcTransportMessage): KrpcMessage? { + try { + return when { + serialFormat is StringFormat && transportMessage is KrpcTransportMessage.StringMessage -> { + // otherwise KrpcMessage? is inferred which leads to decode exception + serialFormat.decodeFromString(transportMessage.value) + } - serialFormat is BinaryFormat && transportMessage is KrpcTransportMessage.BinaryMessage -> { - serialFormat.decodeFromByteArray(transportMessage.value) - } + serialFormat is BinaryFormat && transportMessage is KrpcTransportMessage.BinaryMessage -> { + // otherwise KrpcMessage? is inferred which leads to decode exception + serialFormat.decodeFromByteArray(transportMessage.value) + } - else -> { - return + else -> { + logger.error { + "Unsupported serialization format: ${serialFormat::class} for ${transportMessage::class}" + } + + return null + } } + } catch (e: SerializationException) { + logger.error(e) { "Failed to decode transport message" } + logger.debug { "Failed message: ${transportMessage.dump()}" } + } catch (e: IllegalArgumentException) { + logger.error(e) { "Decoded message is not a valid ${KrpcMessage::class}" } + logger.debug { "Invalid message: ${transportMessage.dump()}" } } + return null + } + + private suspend fun processMessage(transportMessage: KrpcTransportMessage) { + val message = decodeMessage(transportMessage) ?: return + if (dumpLogger.isEnabled) { dumpLogger.dump(role, RECEIVE_PHASE) { transportMessage.dump() } } - processMessage(message) + processMessage(message, message.handlerKey()) } - private suspend fun processMessage(message: KrpcMessage) = withLockForKey(message.getKey()) { - when (message) { - is KrpcCallMessage -> processServiceMessage(message) - is KrpcProtocolMessage, is KrpcGenericMessage -> processNonServiceMessage(message) + private suspend fun processMessage(message: KrpcMessage, key: HandlerKey<*>) { + when { + message is KrpcCallMessage -> { + @Suppress("UNCHECKED_CAST") + processServiceMessage(message, key as HandlerKey) + } + + message is KrpcGenericMessage && message.pluginParams.orEmpty().contains(KrpcPluginKey.WINDOW_UPDATE) -> { + processWindow(message) + } + + message is KrpcProtocolMessage || message is KrpcGenericMessage -> { + processNonServiceMessage(message, key) + } + + else -> { + logger.error { "Received message of unknown processing: $message" } + } } } - private suspend fun processNonServiceMessage(message: KrpcMessage) { - when (val result = tryHandle(message)) { - is HandlerResult.Failure -> { - val failure = KrpcProtocolMessage.Failure( - connectionId = message.connectionId, - errorMessage = "Failed to process ${message::class.simpleName}, error: ${result.cause?.message}", - failedMessage = message, - ) + private suspend fun processWindow(message: KrpcGenericMessage) { + when (val result = decodeWindow(message)) { + is WindowResult.Success -> { + // must be 'key' from 'result' + sendHandlers[result.key]?.updateWindowSize(result.update) + } - sendMessage(failure) + is WindowResult.Failure -> { + sendMessageIgnoreClosure( + KrpcProtocolMessage.Failure( + errorMessage = result.message, + connectionId = message.connectionId, + failedMessage = message, + ) + ) } + } + } - HandlerResult.NoSubscription -> { - waiting.computeIfAbsent(message.getKey()) { mutableListOf() }.add(message) + private suspend fun processNonServiceMessage(message: KrpcMessage, key: HandlerKey<*>) { + // should be the first message we receive + if (message is KrpcProtocolMessage.Handshake) { + if (message.supportedPlugins.contains(KrpcPlugin.BACKPRESSURE)) { + peerSupportsBackPressure = true + + // If it is a new version peer, it will be set to the buffer size from the config + receiveBufferSize = config.perCallBufferSize + + val windowParam = message.pluginParams[KrpcPluginKey.WINDOW_UPDATE] + ?.toIntOrNull() + + // -1 for an unlimited buffer size for old peers + sendBufferSize = windowParam ?: -1 + } else { + // UNLIMITED for backwards compatibility + receiveBufferSize = Channel.UNLIMITED + // -1 for an unlimited buffer size for old peers for backwards compatibility + sendBufferSize = -1 } - HandlerResult.Success -> {} // ok + // update this if present, as this is the only sender that has -1 even for new clients + sendHandlers[HandlerKey.Protocol]?.updateWindowSize(sendBufferSize ?: -1) } - } - private suspend fun processServiceMessage(message: KrpcCallMessage) { - val result = tryHandle(message) + withLockForKey(key, createKey = true) { + val handler = handlerFor(key) - // todo better exception processing probably - if (result != HandlerResult.Success) { - if (waitForSubscribers) { - waiting.computeIfAbsent(message.getKey()) { mutableListOf() }.add(message) + handler.handle(message) { cause -> + if (message.isException) { + return@handle + } - val reason = when (result) { - is HandlerResult.Failure -> { - "Unhandled exception while processing ${result.cause?.message}" - } + val failure = KrpcProtocolMessage.Failure( + connectionId = message.connectionId, + errorMessage = "Failed to process $key, error: ${cause?.message}", + failedMessage = message, + ) - is HandlerResult.NoSubscription -> { - "No service with key '${message.getKey()}' and '${message.serviceType}' type was registered. " + - "Available: keys: [${subscriptions.keys.joinToString()}]" - } + // too late for exceptions + sendMessageIgnoreClosure(failure) + } + }?.onFailure { + if (message.isException) { + return@onFailure + } - else -> { - "Unknown" - } + val failure = KrpcProtocolMessage.Failure( + connectionId = message.connectionId, + errorMessage = "Message limit of ${config.perCallBufferSize} is exceeded for $key.", + failedMessage = message, + ) + + // too late for exceptions + sendMessageIgnoreClosure(failure) + }?.onClosed { + // do nothing; it's a service message, meaning that the service is dead + } + } + + private suspend fun processServiceMessage(message: KrpcCallMessage, key: HandlerKey) { + val (handler, result) = withLockForKey(key, createKey = true) { + val handler = receiveHandlers[key] + ?: if (config.waitTimeout.isPositive()) { + handlerFor(key) + } else { + val errorMessage = "No registered service of ${message.serviceType} service type " + + "was able to process message ($key) at the moment. " + + "Available: keys: [${receiveHandlers.keys.joinToString()}]." + + sendCallException(message, illegalStateException(errorMessage)) + return } - logger.warn((result as? HandlerResult.Failure)?.cause) { - "No registered service of ${message.serviceType} service type " + - "was able to process message ($message) at the moment. Waiting for new services. " + - "Reason: $reason" + val result = handler.handle(message) { initialCause -> + if (message.isException) { + return@handle } - return + val cause = illegalStateException( + message = "Failed to process call ${message.callId} for service ${message.serviceType}", + cause = initialCause, + ) + + sendCallException(message, cause) } - val initialCause = (result as? HandlerResult.Failure)?.cause + handler to result + } ?: error("unreachable, as we create a lock for the key") - val cause = IllegalStateException( - "Failed to process call ${message.callId} for service ${message.serviceType}, " + - "${subscriptions.values.size} attempts failed", - initialCause, - ) + result.onFailure { + if (message.isException) { + return@onFailure + } - val callException = KrpcCallMessage.CallException( - callId = message.callId, - serviceType = message.serviceType, - cause = serializeException(cause), - connectionId = message.connectionId, - serviceId = message.serviceId, + sendCallException( + message = message, + cause = illegalStateException("Message limit of ${config.perCallBufferSize} is exceeded for $key"), ) + }.onClosed { + if (message.isException) { + return@onClosed + } - sendMessage(callException) + sendCallException( + message = message, + cause = illegalStateException("Service $key is dead"), + ) } - } - private suspend fun tryHandle( - message: KrpcMessage, - handler: KrpcMessageHandler? = null, - ): HandlerResult { - val key = message.getKey() - val subscriber = handler ?: subscriptions[key] ?: return HandlerResult.NoSubscription + if (handler is KrpcStoringReceiveHandler && result.isSuccess) { + transportScope.launch(CoroutineName("krpc-connector-discard-if-unprocessed-$key")) { + delay(config.waitTimeout) - val result = runCatching { - subscriber(message) - } + withLockForKey(key, createKey = false) { + if (handler.processingStarted || receiveHandlers[key] != handler) { + return@launch + } - return when { - result.isFailure -> { - val exception = result.exceptionOrNull() - if (exception !is CancellationException) { - logger.error(exception) { "Failed to handle message with key $key" } + receiveHandlers.remove(key) + keyLocks.remove(key) } - HandlerResult.Failure(exception) - } - else -> { - HandlerResult.Success + handler.close( + key = key, + e = illegalStateException( + "Waiting limit of ${config.waitTimeout} " + + "is exceeded for unprocessed messages with $key" + ), + ) } } } - private suspend fun processWaiters(key: SubscriptionKey, handler: KrpcMessageHandler) { - withLockForKey(key) { - if (waiting.values.isEmpty()) return + private suspend fun sendCallException(message: KrpcCallMessage, cause: Throwable) { + val callException = KrpcCallMessage.CallException( + callId = message.callId, + serviceType = message.serviceType, + cause = serializeException(cause), + connectionId = message.connectionId, + serviceId = message.serviceId, + ) - val iterator = waiting[key]?.iterator() ?: return - while (iterator.hasNext()) { - val message = iterator.next() + sendMessage(callException) + } - val tryHandle = tryHandle(message, handler) - if (tryHandle == HandlerResult.Success) { - iterator.remove() - } - } + private suspend inline fun withLockForKey( + key: HandlerKey<*>, + createKey: Boolean, + action: () -> T, + ): T? { + val lockKey = if (key is HandlerKey.ServiceCall && role == SERVER_ROLE) { + HandlerKey.Service(key.serviceType) + } else { + key + } + + val mutex = if (createKey) { + keyLocks.computeIfAbsent(lockKey) { Mutex() } + } else { + keyLocks[lockKey] } - } - private suspend inline fun withLockForKey(key: SubscriptionKey, action: () -> T): T = - processWaitersLocks.computeIfAbsent(key) { Mutex() }.withLock(action = action) + return mutex?.withLock(action = action) + } internal companion object { const val SEND_PHASE = "Send" @@ -268,10 +556,8 @@ public class KrpcConnector( } } -private sealed interface HandlerResult { - data object Success : HandlerResult - - data object NoSubscription : HandlerResult - - data class Failure(val cause: Throwable?) : HandlerResult -} +internal val KrpcMessage.isException + get() = when (this) { + is KrpcCallMessage.CallException, is KrpcProtocolMessage.Failure -> true + else -> false + } diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcEndpoint.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcEndpoint.kt index 3289f0a24..2eb14c35b 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcEndpoint.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcEndpoint.kt @@ -1,11 +1,11 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.krpc.internal import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.ClosedSendChannelException import kotlinx.coroutines.launch import kotlinx.rpc.internal.utils.InternalRpcApi @@ -25,13 +25,15 @@ public interface KrpcEndpoint { ) { if (!supportedPlugins.contains(KrpcPlugin.CANCELLATION)) { if (closeTransportAfterSending) { - sender.cancel("Transport finished") + sender.drainSendQueueAndClose("Transport finished") } return } - val sendJob = sender.launch(CoroutineName("krpc-endpoint-cancellation-$serviceId-$cancellationId")) { + val sendJob = sender.transportScope.launch( + CoroutineName("krpc-endpoint-cancellation-$serviceId-$cancellationId"), + ) { val message = KrpcGenericMessage( connectionId = null, pluginParams = listOfNotNull( @@ -42,12 +44,16 @@ public interface KrpcEndpoint { ).toMap() ) - sender.sendMessage(message) + try { + sender.sendMessage(message) + } catch (_: ClosedSendChannelException) { + // ignore, call was already closed + } } if (closeTransportAfterSending) { sendJob.invokeOnCompletion { - sender.cancel("Transport finished") + sender.drainSendQueueAndClose("Transport finished") } } } diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcMessage.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcMessage.kt index 64b678038..6d7fdeba2 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcMessage.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcMessage.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.krpc.internal @@ -59,7 +59,7 @@ public sealed interface KrpcProtocolMessage : KrpcMessage { public data class Handshake( val supportedPlugins: Set, override val connectionId: Long? = null, - override val pluginParams: Map = emptyMap(), + override val pluginParams: MutableMap = mutableMapOf(), ) : KrpcProtocolMessage @InternalRpcApi diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPlugin.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPlugin.kt index f4d4d7a82..e0e54beff 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPlugin.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPlugin.kt @@ -58,6 +58,11 @@ public enum class KrpcPlugin( * Clients don't require cancellation acknowledgement from the peer server. */ NO_ACK_CANCELLATION(4, KrpcVersion.V_0_8_0), + + /** + * Backpressure mechanism. + */ + BACKPRESSURE(5, KrpcVersion.V_0_10_0), ; @InternalRpcApi diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPluginKey.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPluginKey.kt index 8401c6c43..66e59a796 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPluginKey.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPluginKey.kt @@ -51,6 +51,16 @@ public enum class KrpcPluginKey( * Marks a request as a one doesn't suspend and returns a flow. */ NON_SUSPENDING_SERVER_FLOW_MARKER(5, KrpcPlugin.NON_SUSPENDING_SERVER_FLOWS), + + /** + * Represents the update to the window size from the peer endpoint. + */ + WINDOW_UPDATE(6, KrpcPlugin.BACKPRESSURE), + + /** + * Represents the call id of the current [WINDOW_UPDATE]. + */ + WINDOW_KEY(7, KrpcPlugin.BACKPRESSURE), ; init { diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcReceiveBuffer.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcReceiveBuffer.kt new file mode 100644 index 000000000..85d1856a6 --- /dev/null +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcReceiveBuffer.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.internal + +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.channels.Channel + +internal class KrpcReceiveBuffer( + private val bufferSize: () -> Int, +) { + data class MessageRequest( + val message: KrpcMessage, + val onMessageFailure: suspend (Throwable?) -> Unit, + ) + + private val _inBuffer = atomic(0) + val inBuffer get() = _inBuffer.value + + private val _channelSize by lazy { bufferSize() } + val channel by lazy { + Channel(capacity = _channelSize) + } + + val window get() = _channelSize - _inBuffer.value + + suspend fun receiveCatching(): BufferResult { + val result = channel.receiveCatching() + if (result.isSuccess) { + _inBuffer.decrementAndGet() + } + + return when { + result.isSuccess -> BufferResult.Success(result.getOrThrow()) + result.isClosed -> BufferResult.Closed(result.exceptionOrNull()) + result.isFailure -> BufferResult.Failure() + else -> error("Unreachable") + } + } + + fun trySend(message: MessageRequest): BufferResult { + val result = channel.trySend(message) + if (result.isSuccess) { + _inBuffer.incrementAndGet() + } + + return when { + result.isSuccess -> BufferResult.Success(Unit) + result.isClosed -> BufferResult.Closed(result.exceptionOrNull()) + result.isFailure -> BufferResult.Failure() + else -> error("Unreachable") + } + } + + fun close(cause: Throwable?) { + channel.close(cause) + channel.cancel(CancellationException(null, cause)) + + _inBuffer.getAndSet(_channelSize) + } +} diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcReceiveHandler.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcReceiveHandler.kt new file mode 100644 index 000000000..6e9c6ac3e --- /dev/null +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcReceiveHandler.kt @@ -0,0 +1,266 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.internal + +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.channels.ClosedSendChannelException +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.rpc.internal.utils.InternalRpcApi +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.time.Duration + +@InternalRpcApi +public sealed interface HandlerKey { + @InternalRpcApi + public data object Protocol : HandlerKey + + @InternalRpcApi + public data object Generic : HandlerKey + + @InternalRpcApi + public data class Service(val serviceType: String) : HandlerKey + + @InternalRpcApi + public data class ServiceCall(val serviceType: String, val callId: String) : HandlerKey +} + +internal fun Message.handlerKey(): HandlerKey { + @Suppress("UNCHECKED_CAST") + return when (this) { + is KrpcCallMessage -> HandlerKey.ServiceCall(serviceType, callId) + is KrpcProtocolMessage -> HandlerKey.Protocol + is KrpcGenericMessage -> HandlerKey.Generic + else -> error("unreachable") + } as HandlerKey +} + +internal sealed interface KrpcReceiveHandler { + fun handle( + message: KrpcMessage, + onMessageFailure: suspend (Throwable?) -> Unit, + ): BufferResult + + fun close(key: HandlerKey<*>, e: Throwable?) +} + +internal class KrpcStoringReceiveHandler( + private val buffer: KrpcReceiveBuffer, + private val sender: KrpcMessageSender, +) : KrpcReceiveHandler { + val inBuffer get() = buffer.inBuffer + private val closed = atomic(false) + private val scope get() = sender.transportScope + + override fun handle( + message: KrpcMessage, + onMessageFailure: suspend (Throwable?) -> Unit, + ): BufferResult { + if (closed.value) { + return BufferResult.Closed(illegalStateException("KrpcStoringReceiveHandler closed")) + } + + return buffer.trySend(KrpcReceiveBuffer.MessageRequest(message, onMessageFailure)) + } + + override fun close(key: HandlerKey<*>, e: Throwable?) { + if (!closed.compareAndSet(expect = false, update = true)) { + return + } + + // close for sending + buffer.channel.close(e) + + scope.launch(CoroutineName("krpc-connector-message-buffer-close-$key")) { + val allLeft = mutableListOf() + + while (true) { + val result = buffer.channel.tryReceive() + if (result.isClosed || result.isFailure) { + break + } + + allLeft.add(result.getOrThrow().message) + } + + val undelivered = allLeft + .filterIsInstance() // ignore protocol messages + + if (undelivered.isEmpty()) { + buffer.close(e) + return@launch + } + + undelivered.groupBy { it.callId }.forEach { (callId, messages) -> + val message = messages.first() + + val cause = illegalStateException( + message = "Buffer closed for $callId for service ${message.serviceType}, " + + "${messages.size} messages were unprocessed", + cause = e, + ) + + val callException = KrpcCallMessage.CallException( + callId = message.callId, + serviceType = message.serviceType, + cause = serializeException(cause), + connectionId = message.connectionId, + serviceId = message.serviceId, + ) + + this@KrpcStoringReceiveHandler.sender.sendMessage(callException) + } + + buffer.close(e) + } + } + + private val _processingStarted = atomic(false) + val processingStarted get() = _processingStarted.value + + suspend fun receiveCatching(): BufferResult { + if (closed.value) { + return BufferResult.Closed(illegalStateException("KrpcStoringReceiveHandler closed")) + } + + _processingStarted.getAndSet(true) + return buffer.receiveCatching() + } +} + +internal class KrpcActingReceiveHandler( + private val callHandler: KrpcMessageSubscription, + private val storingHandler: KrpcStoringReceiveHandler, + private val sender: KrpcMessageSender, + key: HandlerKey<*>, + timeout: Duration, + broadcastUpdates: Boolean, +) : KrpcReceiveHandler { + private val job = sender.transportScope.launch( + context = CoroutineName("krpc-connector-message-handler-$key"), + start = CoroutineStart.LAZY, + ) { + while (true) { + val (message, onMessageFailure) = storingHandler.receiveCatching().getOrNull() ?: break + + val result = if (timeout == Duration.INFINITE) { + tryHandle(message) + } else { + withTimeoutOrNull(timeout) { + tryHandle(message) + } ?: HandlerResult.Failure( + illegalStateException("Timeout while processing message") + ) + } + + if (result is HandlerResult.Failure) { + onMessageFailure(result.cause) + } + + if (message !is KrpcCallMessage) continue + + if (broadcastUpdates) { + broadcastWindowUpdate(1, message.connectionId, message.serviceType, message.callId) + } + } + } + + init { + if (storingHandler.inBuffer > 0) { + job.start() + } + } + + override fun handle( + message: KrpcMessage, + onMessageFailure: suspend (Throwable?) -> Unit, + ): BufferResult { + job.start() + return storingHandler.handle(message, onMessageFailure) + } + + override fun close(key: HandlerKey<*>, e: Throwable?) { + if (e != null) { + if (e is CancellationException) { + job.cancel(e) + } else { + job.cancel(CancellationException(null, e)) + } + } else { + job.cancel() + } + + storingHandler.close(key, e) + } + + private suspend fun tryHandle(message: KrpcMessage): HandlerResult { + try { + callHandler(message) + + return HandlerResult.Success + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + return HandlerResult.Failure(e) + } + } + + internal suspend fun broadcastWindowUpdate(update: Int, connectionId: Long?, serviceType: String, callId: String) { + try { + sender.sendMessage( + KrpcGenericMessage( + connectionId = connectionId, + pluginParams = mutableMapOf( + KrpcPluginKey.WINDOW_UPDATE to "$update", + KrpcPluginKey.WINDOW_KEY to "$serviceType/$callId", + ), + ) + ) + } catch (_: ClosedSendChannelException) { + // ignore, connection is closed, no more channel updates are needed + } + } + + private sealed interface HandlerResult { + data object Success : HandlerResult + + data class Failure(val cause: Throwable?) : HandlerResult + } +} + +internal sealed interface WindowResult { + data class Success(val update: Int, val key: HandlerKey.ServiceCall) : WindowResult + + data class Failure(val message: String) : WindowResult +} + +internal fun decodeWindow(message: KrpcGenericMessage): WindowResult { + val windowParam = message.pluginParams?.get(KrpcPluginKey.WINDOW_UPDATE) + + if (windowParam.isNullOrEmpty()) { + return WindowResult.Failure("Window param must be of the form /") + } + + val updateParam = windowParam.toIntOrNull() + ?: return WindowResult.Failure( + "Window param must be of the form / and in form of two longs" + ) + + val windowKey = message.pluginParams[KrpcPluginKey.WINDOW_KEY]?.split("/").orEmpty() + val serviceType = windowKey.getOrNull(0) + val callId = windowKey.getOrNull(1) + + if (windowKey.size != 2 || serviceType == null || callId == null) { + return WindowResult.Failure("Window key must be of the form /") + } + + val subscriptionKey = HandlerKey.ServiceCall(serviceType, callId) + + return WindowResult.Success(updateParam, subscriptionKey) +} diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcSendHandler.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcSendHandler.kt new file mode 100644 index 000000000..ad55a5434 --- /dev/null +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcSendHandler.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.internal + +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.locks.ReentrantLock +import kotlinx.atomicfu.locks.withLock +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.channels.ClosedSendChannelException +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.onClosed +import kotlinx.coroutines.channels.onSuccess +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.rpc.krpc.KrpcTransportMessage +import kotlin.coroutines.resume + +internal class KrpcSendHandler( + private val sendChannel: SendChannel, +) { + private val closed = atomic(false) + + internal var window = -1 + private set + + /** + * Could've been just `atomic`, but when we send the message, we need to: + * - Check size + * - Send Message + * - Update window size if sent + * + * As a one atomic operation. + * + * We are an execution resume guarantee as [kotlinx.coroutines.channels.Channel.trySend] doesn't wait + * + * TODO contention is very high here, we need to optimize it. + * KRPC-211 Optimize KrpcSendHandler + */ + private val windowLock = ReentrantLock() + private val continuationsLock = ReentrantLock() + + // internal for tests + @Suppress("PropertyName", "detekt.VariableNaming") + internal val __continuations = mutableListOf>() + + fun updateWindowSize(update: Int) { + if (closed.value) { + return + } + + windowLock.withLock { + window = if (window == -1) { + update + } else { + window + update + } + + if (window <= 0) { + return + } + + continuationsLock.withLock { + @Suppress("unused") + for (i in 0 until window) { + __continuations.removeFirstOrNull()?.resume(Unit) + } + } + } + } + + @Suppress("detekt.ThrowsCount") + suspend fun sendMessage(message: KrpcTransportMessage) { + if (closed.value) { + throw ClosedSendChannelException("KrpcSendHandler closed") + } + + while (true) { + val currentWindow = window + if (currentWindow == -1) { + sendChannel.trySend(message).onClosed { + throw it ?: ClosedSendChannelException("KrpcSendHandler closed") + } + + break + } else { + if (currentWindow == 0) { + suspendCancellableCoroutine { cont -> + windowLock.withLock { + if (window > 0) { + cont.resume(Unit) + return@withLock + } + + continuationsLock.withLock { + __continuations.add(cont) + } + } + } + + continue + } + + val sent = windowLock.withLock { + val currentWindow = window + + if (currentWindow == 0) { + return@withLock false + } + + if (closed.value) { + throw ClosedSendChannelException("KrpcSendHandler closed") + } + + val result = sendChannel.trySend(message) + .onSuccess { + // don't resume other continuations, as we decrement + // no if check as we have lock + window = currentWindow - 1 + } + .onClosed { + throw it ?: ClosedSendChannelException("KrpcSendHandler closed") + } + + result.isSuccess + } + + if (sent) { + break + } + } + } + } + + fun close(e: Throwable? = null) { + if (closed.compareAndSet(expect = false, update = true)) { + continuationsLock.withLock { + __continuations.forEach { it.cancel(e) } + } + } + } +} diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcVersion.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcVersion.kt index fb310ec5e..d3eb84fd8 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcVersion.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcVersion.kt @@ -11,4 +11,5 @@ internal enum class KrpcVersion { V_0_1_0_BETA, V_0_6_0, V_0_8_0, + V_0_10_0, } diff --git a/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcConnectorTest.kt b/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcConnectorTest.kt new file mode 100644 index 000000000..1c83e90fe --- /dev/null +++ b/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcConnectorTest.kt @@ -0,0 +1,397 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package kotlinx.rpc.krpc + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.rpc.krpc.internal.HandlerKey +import kotlinx.rpc.krpc.internal.KrpcCallMessage +import kotlinx.rpc.krpc.internal.KrpcConnector +import kotlinx.rpc.krpc.internal.KrpcGenericMessage +import kotlinx.rpc.krpc.internal.KrpcMessage +import kotlinx.rpc.krpc.internal.KrpcPlugin +import kotlinx.rpc.krpc.internal.KrpcPluginKey +import kotlinx.rpc.krpc.internal.KrpcProtocolMessage +import kotlinx.rpc.krpc.internal.deserialize +import kotlinx.rpc.test.runTestWithCoroutinesProbes +import kotlinx.serialization.json.Json +import kotlin.coroutines.CoroutineContext +import kotlin.js.JsName +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue +import kotlin.test.fail +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class KrpcConnectorTest : KrpcConnectorBaseTest() { + @Test + @JsName("service_call_success_is_delivered_to_a_client") + fun `service call success is delivered to a client`() = runTest { client, server -> + // Enable backpressure to use configured perCallBufferSize deterministically + val clientPExceptionsChannel = Channel(Channel.UNLIMITED) + val serverPExceptionsChannel = Channel(Channel.UNLIMITED) + val hs = handshakeBoth(client, server, clientPExceptionsChannel, serverPExceptionsChannel) + + val clientInbox = Channel(10) + + client.subscribeToMessages(HandlerKey.ServiceCall("svc", "1")) { + clientInbox.send(it) + } + + server.subscribeToMessages(HandlerKey.Service("svc")) { message -> + if (message is KrpcCallMessage.CallDataString) { + server.sendMessage( + KrpcCallMessage.CallSuccessString( + callId = message.callId, + serviceType = message.serviceType, + data = "${message.data} : OK", + connectionId = message.connectionId, + serviceId = message.serviceId, + ) + ) + } + } + + client.sendMessage("ping".asCallMessage("svc", "1")) + + val reply = clientInbox.receive() + val success = assertIs(reply) + assertEquals("ping : OK", success.data) + + hs.assertAllShookHands() + + assertTrue(clientPExceptionsChannel.isEmpty) + assertTrue(serverPExceptionsChannel.isEmpty) + } + + @Test + @JsName("server_exception_propagates_as_a_call_exception") + fun `server exception propagates as a call exception`() = runTest { client, server -> + val clientPExceptionsChannel = Channel(Channel.UNLIMITED) + val serverPExceptionsChannel = Channel(Channel.UNLIMITED) + val hs = handshakeBoth(client, server, clientPExceptionsChannel, serverPExceptionsChannel) + + val clientInbox = Channel(10) + client.subscribeToMessages(HandlerKey.ServiceCall("svc", "2")) { + clientInbox.send(it) + } + + server.subscribeToMessages(HandlerKey.Service("svc")) { + throw IllegalArgumentException("boom") + } + + server.subscribeToMessages(HandlerKey.Generic) { + throw IllegalArgumentException("boom") + } + + client.sendMessage("req".asCallMessage("svc", "2")) + + val msg = clientInbox.receive() + val ex = assertIs(msg) + val t = ex.cause.deserialize() + assertTrue(t.message!!.contains("Failed to process call"), t.toString()) + + client.sendMessage("req".asGenericMessage()) + val protocolMessage = clientPExceptionsChannel.receive() + val protocolException = assertIs(protocolMessage) + assertEquals("req".asGenericMessage(), protocolException.failedMessage) + + hs.assertAllShookHands() + } + + @Test + @JsName("server_timeout_sends_a_call_exception") + fun `server timeout sends a call exception`() = runTest(callTimeout = 200.milliseconds) { client, server -> + val clientPExceptionsChannel = Channel(Channel.UNLIMITED) + val serverPExceptionsChannel = Channel(Channel.UNLIMITED) + val hs = handshakeBoth(client, server, clientPExceptionsChannel, serverPExceptionsChannel) + + val clientInbox = Channel(10) + client.subscribeToMessages(HandlerKey.ServiceCall("svc", "3")) { + clientInbox.send(it) + } + + server.subscribeToMessages(HandlerKey.Service("svc")) { + // Simulate long processing exceeding callTimeout + delay(500.milliseconds) + } + + client.sendMessage("req".asCallMessage("svc", "3")) + + val msg = clientInbox.receive() + + val ex = assertIs(msg) + val t = ex.cause.deserialize() + assertTrue(t.message!!.contains("Failed to process call"), t.toString()) + assertEquals(t.cause?.message?.contains("Timeout while processing message"), true, t.toString()) + + hs.assertAllShookHands() + assertTrue(clientPExceptionsChannel.isEmpty) + assertTrue(serverPExceptionsChannel.isEmpty) + } + + @Test + @JsName("buffer_overflow_does_not_result_in_a_call_exception") + fun `buffer overflow does not result in a call exception`() = runTest(perCallBufferSize = 1) { client, server -> + val clientPExceptionsChannel = Channel(Channel.UNLIMITED) + val serverPExceptionsChannel = Channel(Channel.UNLIMITED) + val hs = handshakeBoth(client, server, clientPExceptionsChannel, serverPExceptionsChannel) + + val clientInbox = Channel(10) + client.subscribeToMessages(HandlerKey.ServiceCall("svc", "4")) { + if (it is KrpcCallMessage.CallException) { + val ex = it.cause.deserialize() + + coroutineContext.cancel(CancellationException("Unexpected failure from the server", ex)) + return@subscribeToMessages + } + + clientInbox.send(it) + } + + val serverInbox = Channel(10) + server.subscribeToMessages(HandlerKey.Service("svc")) { + if (it is KrpcCallMessage.CallException) { + val ex = it.cause.deserialize() + + coroutineContext.cancel(CancellationException("Unexpected failure from the client", ex)) + return@subscribeToMessages + } + + serverInbox.send(it) + } + + client.sendMessage("m1".asCallMessage("svc", "4")) + client.sendMessage("m2".asCallMessage("svc", "4")) + + val msg = serverInbox.receive() + assertEquals("m1", (msg as KrpcCallMessage.CallDataString).data) + val msg2 = serverInbox.receive() + assertEquals("m2", (msg2 as KrpcCallMessage.CallDataString).data) + + hs.assertAllShookHands() + assertTrue(clientInbox.isEmpty) + assertTrue(clientPExceptionsChannel.isEmpty) + assertTrue(serverPExceptionsChannel.isEmpty) + } + + @Test + @JsName("wait_timeout_exceeds_sends_a_call_exception") + fun `wait timeout exceeds sends a call exception`() = runTest( + waitTimeout = 300.milliseconds, + perCallBufferSize = 2, + ) { client, server -> + val clientPExceptionsChannel = Channel(Channel.UNLIMITED) + val serverPExceptionsChannel = Channel(Channel.UNLIMITED) + val hs = handshakeBoth(client, server, clientPExceptionsChannel, serverPExceptionsChannel) + + val clientInbox = Channel(10) + client.subscribeToMessages(HandlerKey.ServiceCall("svc", "5")) { + clientInbox.send(it) + } + + // Send one message that will stay unprocessed until waitTimeout triggers discard + client.sendMessage("stay".asCallMessage("svc", "5")) + client.sendMessage("stay2".asCallMessage("svc", "5")) + + val msg = clientInbox.receive() + val ex = assertIs(msg) + val top = ex.cause.deserialize() + // top-level message comes from buffer close; nested cause contains a wait-timeout message + assertEquals(top.message?.contains("2 messages were unprocessed"), true, top.toString()) + assertEquals(top.cause?.message?.contains("Waiting limit of"), true, top.toString()) + + hs.assertAllShookHands() + } + + @Test + @JsName("one_item_buffer_size_works_with_a_stream_of_messages") + fun `one item buffer size works with a stream of messages`() = runTest(perCallBufferSize = 1) { client, server -> + val clientPExceptionsChannel = Channel(Channel.UNLIMITED) + val serverPExceptionsChannel = Channel(Channel.UNLIMITED) + val hs = handshakeBoth(client, server, clientPExceptionsChannel, serverPExceptionsChannel) + + val clientInbox = Channel(10) + client.subscribeToMessages(HandlerKey.ServiceCall("svc", "6")) { + clientInbox.send(it) + } + + server.subscribeToMessages(HandlerKey.Service("svc")) { message -> + server.sendMessage(message) + } + + launch { + repeat(100) { + client.sendMessage("ping".asCallMessage("svc", "6")) + } + } + + repeat(100) { + assertEquals("ping", (clientInbox.receive() as KrpcCallMessage.CallDataString).data) + } + + hs.assertAllShookHands() + } + + private class HsResult { + val clientShookHands = CompletableDeferred() + val serverShookHands = CompletableDeferred() + + fun assertAllShookHands() { + assertTrue(clientShookHands.isCompleted) + assertTrue(serverShookHands.isCompleted) + } + + suspend fun await() { + clientShookHands.await() + serverShookHands.await() + } + } + + private suspend fun handshakeBoth( + client: KrpcConnector, + server: KrpcConnector, + clientExceptionsChannel: Channel, + serverExceptionsChannel: Channel, + ): HsResult { + val hs = KrpcProtocolMessage.Handshake(KrpcPlugin.ALL) + val hsResult = HsResult() + + client.subscribeToMessages(HandlerKey.Protocol) { + if (it is KrpcProtocolMessage.Failure) { + if (!hsResult.clientShookHands.isCompleted) { + fail("Handshake must be first message, but got: $it") + } + + clientExceptionsChannel.send(it) + } + assertEquals(hs, it) + hsResult.clientShookHands.complete(Unit) + } + + server.subscribeToMessages(HandlerKey.Protocol) { + if (it is KrpcProtocolMessage.Failure) { + if (!hsResult.serverShookHands.isCompleted) { + fail("Handshake must be first message, but got: $it") + } + + serverExceptionsChannel.send(it) + } + assertEquals(hs, it) + hsResult.serverShookHands.complete(Unit) + } + + client.sendMessage(hs) + server.sendMessage(hs) + + hsResult.await() + + return hsResult + } +} + +abstract class KrpcConnectorBaseTest { + protected fun String.asCallMessage( + serviceId: String, + callId: String, + ) = KrpcCallMessage.CallDataString( + connectionId = null, + callId = callId, + serviceType = serviceId, + data = this, + callableName = "", + callType = KrpcCallMessage.CallType.Method, + ) + + protected fun String.asGenericMessage() = KrpcGenericMessage( + connectionId = null, + pluginParams = mapOf(KrpcPluginKey.GENERIC_MESSAGE_TYPE to this), + ) + + protected fun runTest( + testTimeout: Duration = 15.seconds, + waitTimeout: Duration = 1.seconds, + callTimeout: Duration = 1.seconds, + perCallBufferSize: Int = 100, + body: suspend TestScope.(clientConnector: KrpcConnector, serverConnector: KrpcConnector) -> Unit, + ) = runTestWithCoroutinesProbes(timeout = testTimeout) { + val connectorConfig = KrpcConfig.Connector(waitTimeout, callTimeout, perCallBufferSize) + + val transport = LocalTransport(coroutineContext) + + val clientConnector = KrpcConnector( + serialFormat = Json, + transport = transport.client, + config = connectorConfig, + isServer = false, + ) + + val serverConnector = KrpcConnector( + serialFormat = Json, + transport = transport.server, + config = connectorConfig, + isServer = true, + ) + + try { + body(clientConnector, serverConnector) + } finally { + transport.coroutineContext.job.cancelAndJoin() + } + } +} + +private class LocalTransport( + parentContext: CoroutineContext? = null, +) : CoroutineScope { + override val coroutineContext = SupervisorJob(parentContext?.get(Job)) + + private val clientIncoming = Channel() + private val serverIncoming = Channel() + + val client: KrpcTransport = object : KrpcTransport { + override val coroutineContext: CoroutineContext = Job(this@LocalTransport.coroutineContext.job).let { + if (parentContext != null) parentContext + it else it + } + + override suspend fun send(message: KrpcTransportMessage) { + serverIncoming.send(message) + } + + override suspend fun receive(): KrpcTransportMessage { + return clientIncoming.receive() + } + } + + val server: KrpcTransport = object : KrpcTransport { + override val coroutineContext: CoroutineContext = Job(this@LocalTransport.coroutineContext.job).let { + if (parentContext != null) parentContext + it else it + } + + override suspend fun send(message: KrpcTransportMessage) { + clientIncoming.send(message) + } + + override suspend fun receive(): KrpcTransportMessage { + return serverIncoming.receive() + } + } +} diff --git a/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveBufferTest.kt b/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveBufferTest.kt new file mode 100644 index 000000000..ac0b9a0e4 --- /dev/null +++ b/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveBufferTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc + +import kotlinx.coroutines.test.TestScope +import kotlinx.rpc.krpc.internal.BufferResult +import kotlinx.rpc.krpc.internal.KrpcGenericMessage +import kotlinx.rpc.krpc.internal.KrpcPluginKey +import kotlinx.rpc.krpc.internal.KrpcReceiveBuffer +import kotlinx.rpc.krpc.internal.isClosed +import kotlinx.rpc.krpc.internal.isFailure +import kotlinx.rpc.krpc.internal.isSuccess +import kotlinx.rpc.test.runTestWithCoroutinesProbes +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +internal class KrpcReceiveBufferTest : KrpcReceiveBufferBaseTest() { + @Test + fun zeroBufferSize() = runTest(bufferSize = 0) { buffer -> + val result = buffer.trySend("hello") + + assertTrue { result.isFailure } + assertEquals(0, buffer.window) + assertEquals(0, buffer.inBuffer) + } + + @Test + fun oneBufferSize() = runTest(bufferSize = 1) { buffer -> + assertEquals(1, buffer.window) + val result = buffer.trySend("hello") + + assertTrue { result.isSuccess } + assertEquals(0, buffer.window) + assertEquals(1, buffer.inBuffer) + + val secondResult = buffer.trySend("world") + assertTrue { secondResult.isFailure } + } + + @Test + fun closeBuffer() = runTest(bufferSize = 3) { buffer -> + assertEquals(3, buffer.window) + buffer.trySend("hello") + buffer.trySend("world") + buffer.close(null) + assertEquals(3, buffer.inBuffer) + assertEquals(0, buffer.window) + val result = buffer.trySend("test") + assertTrue { result.isClosed } + } +} + +internal abstract class KrpcReceiveBufferBaseTest { + protected fun KrpcReceiveBuffer.trySend( + message: String, + ): BufferResult { + return trySend( + KrpcReceiveBuffer.MessageRequest( + message = KrpcGenericMessage(null, mapOf(KrpcPluginKey.WINDOW_KEY to message)), + onMessageFailure = { }, + ) + ) + } + + protected fun runTest( + bufferSize: Int, + timeout: Duration = 10.seconds, + body: suspend TestScope.(KrpcReceiveBuffer) -> Unit, + ) = runTestWithCoroutinesProbes(timeout = timeout) { + val buffer = KrpcReceiveBuffer( + bufferSize = { bufferSize }, + ) + + try { + body(buffer) + } finally { + buffer.close(null) + } + } +} diff --git a/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.kt b/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.kt new file mode 100644 index 000000000..0806ba99f --- /dev/null +++ b/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.kt @@ -0,0 +1,294 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc + +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.job +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield +import kotlinx.rpc.krpc.internal.HandlerKey +import kotlinx.rpc.krpc.internal.KrpcActingReceiveHandler +import kotlinx.rpc.krpc.internal.KrpcCallMessage +import kotlinx.rpc.krpc.internal.KrpcCallMessage.CallType +import kotlinx.rpc.krpc.internal.KrpcGenericMessage +import kotlinx.rpc.krpc.internal.KrpcMessage +import kotlinx.rpc.krpc.internal.KrpcMessageSender +import kotlinx.rpc.krpc.internal.KrpcMessageSubscription +import kotlinx.rpc.krpc.internal.KrpcPluginKey +import kotlinx.rpc.krpc.internal.KrpcReceiveBuffer +import kotlinx.rpc.krpc.internal.KrpcSendHandler +import kotlinx.rpc.krpc.internal.KrpcStoringReceiveHandler +import kotlinx.rpc.krpc.internal.WindowResult +import kotlinx.rpc.krpc.internal.decodeWindow +import kotlinx.rpc.krpc.internal.deserialize +import kotlinx.rpc.krpc.internal.isFailure +import kotlinx.rpc.krpc.internal.isSuccess +import kotlinx.rpc.krpc.internal.onClosed +import kotlinx.rpc.krpc.internal.onFailure +import kotlinx.rpc.test.runTestWithCoroutinesProbes +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +internal class KrpcReceiveHandlerTest : KrpcReceiveHandlerBaseTest() { + @Test + fun zeroBufferSizeStoring() = runStoringTest(bufferSize = 0) { + val result = storing.handle("test".asMessage()) {} + + assertTrue { result.isFailure } + } + + @Test + fun windowUpdate() = runActingTest( + callTimeOut = 10.seconds, + bufferSize = 0, + callHandler = { }, + ) { acting -> + acting.broadcastWindowUpdate(1, null, "service", "callId") + + val windowResult = decodeWindow(channel.receive() as KrpcGenericMessage) + + assertTrue(windowResult is WindowResult.Success, windowResult.toString()) + assertEquals(1, windowResult.update) + assertEquals("service", windowResult.key.serviceType) + assertEquals("callId", windowResult.key.callId) + + acting.broadcastWindowUpdate(-1, null, "service", "callId") + val windowResult2 = decodeWindow(channel.receive() as KrpcGenericMessage) + assertEquals(-1, (windowResult2 as WindowResult.Success).update) + + acting.broadcastWindowUpdate(Int.MAX_VALUE, null, "service", "callId") + val windowResult3 = decodeWindow(channel.receive() as KrpcGenericMessage) + assertEquals(Int.MAX_VALUE, (windowResult3 as WindowResult.Success).update) + + acting.broadcastWindowUpdate(Int.MIN_VALUE, null, "service", "callId") + val windowResult4 = decodeWindow(channel.receive() as KrpcGenericMessage) + assertEquals(Int.MIN_VALUE, (windowResult4 as WindowResult.Success).update) + } + + @Test + fun oneBufferSizeStoring() = runStoringTest(bufferSize = 1) { + val result = storing.handle("test".asMessage()) {} + + assertTrue { result.isSuccess } + + val result2 = storing.handle("test".asMessage()) {} + + assertTrue { result2.isFailure } + } + + @Test + fun multipleWindowSizeStoring() = runStoringTest(bufferSize = 3) { + storing.handle("test1".asCallMessage("1")) {} + storing.handle("test2".asCallMessage("1")) {} + storing.handle("test3".asCallMessage("2")) {} + + storing.close(HandlerKey.Generic, null) + + val message1 = (channel.receive() as KrpcCallMessage.CallException).cause.deserialize().message.orEmpty() + assertTrue(message1.contains("2 messages were unprocessed"), message1) + + val message2 = (channel.receive() as KrpcCallMessage.CallException).cause.deserialize().message.orEmpty() + assertTrue(message2.contains("1 messages were unprocessed"), message2) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun stressActing() { + val actorJob = Job() + val collected = mutableListOf() + val bufferSize = stressBufferSize + + runActingTest( + callTimeOut = 10.seconds, + bufferSize = bufferSize, + callHandler = { collected.add(it) }, + timeout = 360.seconds, + ) { acting -> + val sendChannel = Channel(Channel.UNLIMITED) + val sender = KrpcSendHandler(sendChannel) + sender.updateWindowSize(bufferSize) + + val windowJob = launch { + while (true) { + val window = when (val message = channel.receive()) { + is KrpcCallMessage.CallException -> fail( + "Unexpected call exception", + message.cause.deserialize() + ) + + is KrpcGenericMessage -> decodeWindow(message) + else -> fail("Unexpected message: $message") + } + + sender.updateWindowSize((window as WindowResult.Success).update) + } + } + + val senderJob = launch { + while (true) { + val message = sendChannel.receive() as KrpcTransportMessage.StringMessage + + acting.handle(message.value.asCallMessage("1")) { + fail( + "Unexpected onMessageFailure call, " + + "window: ${sender.window}, collected: ${collected.size}\"", + it + ) + }.onFailure { + fail( + "Unexpected onFailure call, " + + "window: ${sender.window}, collected: ${collected.size}" + ) + }.onClosed { + fail( + "Unexpected onClosed call, " + + "window: ${sender.window}, collected: ${collected.size}\"", + it + ) + } + } + } + + val counter = Counter() + val printJob = launch { + while (true) { + withContext(Dispatchers.Default) { + delay(5.seconds) + } + println("Collected: ${collected.size}, launches: ${counter.launches.value}, total: ${counter.total.value}") + } + } + + val iterations = stressIterations + List(iterations) { + launch { + repeat(100) { + sender.sendMessage(KrpcTransportMessage.StringMessage("Hello")) + counter.total.incrementAndGet() + } + counter.launches.incrementAndGet() + } + }.joinAll() + + while (!buffer.channel.isEmpty && sender.window != bufferSize) { + yield() + } + + assertEquals(iterations * 100, collected.size) + actorJob.cancelAndJoin() + senderJob.cancelAndJoin() + windowJob.cancelAndJoin() + printJob.cancelAndJoin() + } + } +} + +internal abstract class KrpcReceiveHandlerBaseTest { + class Counter { + val launches = atomic(0) + val total = atomic(0) + } + + protected fun String.asMessage() = KrpcGenericMessage( + connectionId = null, + pluginParams = mapOf(KrpcPluginKey.WINDOW_KEY to this) + ) + + protected fun String.asCallMessage(callId: String) = KrpcCallMessage.CallDataString( + connectionId = null, + callId = callId, + serviceType = "service", + data = this, + callableName = "", + callType = CallType.Method, + ) + + class TestConfig( + val storing: KrpcStoringReceiveHandler, + val buffer: KrpcReceiveBuffer, + val channel: Channel, + val sender: KrpcMessageSender, + val testScope: TestScope, + ) : CoroutineScope by testScope + + protected fun runActingTest( + callTimeOut: Duration, + bufferSize: Int, + callHandler: KrpcMessageSubscription, + timeout: Duration = 30.seconds, + body: suspend TestConfig.(acting: KrpcActingReceiveHandler) -> Unit, + ): TestResult { + return runStoringTest(bufferSize, timeout) { + val acting = KrpcActingReceiveHandler( + callHandler = callHandler, + storingHandler = storing, + sender = sender, + key = HandlerKey.Generic, + timeout = callTimeOut, + broadcastUpdates = true, + ) + + body(this, acting) + + acting.close(HandlerKey.Generic, null) + } + } + + protected fun runStoringTest( + bufferSize: Int, + timeout: Duration = 30.seconds, + body: suspend TestConfig.() -> Unit, + ) = runTestWithCoroutinesProbes(timeout = timeout) { + val buffer = KrpcReceiveBuffer( + bufferSize = { bufferSize }, + ) + + val channel = Channel(Channel.UNLIMITED) + + val senderJob = Job(this@runTestWithCoroutinesProbes.coroutineContext.job) + val sender = object : KrpcMessageSender { + override suspend fun sendMessage(message: KrpcMessage) { + channel.send(message) + } + + override val transportScope: CoroutineScope = CoroutineScope(senderJob) + + override fun drainSendQueueAndClose(message: String) { + TODO("Not yet implemented") + } + } + + val handler = KrpcStoringReceiveHandler(buffer, sender) + val config = TestConfig(handler, buffer, channel, sender, this) + + try { + body(config) + } finally { + handler.close(HandlerKey.Generic, null) + buffer.close(null) + channel.cancel() + channel.close() + senderJob.cancelAndJoin() + } + } +} + +internal expect val stressIterations: Int +internal expect val stressBufferSize: Int diff --git a/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcSendHandlerTest.kt b/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcSendHandlerTest.kt new file mode 100644 index 000000000..759c276e4 --- /dev/null +++ b/krpc/krpc-core/src/commonTest/kotlin/kotlinx/rpc/krpc/KrpcSendHandlerTest.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.coroutines.yield +import kotlinx.rpc.krpc.internal.KrpcSendHandler +import kotlinx.rpc.test.runTestWithCoroutinesProbes +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +internal class KrpcSendHandlerTest : KrpcSendHandlerBaseTest() { + @Test + fun zeroWindowSize() = runTest { _, handler -> + handler.updateWindowSize(0) + + val job = launch { + var cancelled = false + + try { + handler.sendMessage("Hello".asMessage()) + } catch (e: CancellationException) { + cancelled = e.message?.contains("Test finished") ?: false + if (!cancelled) { + fail("Unexpected cancellation exception", e) + } + + throw e + } + + if (!cancelled) { + fail("Expected cancellation exception") + } + } + + handler.awaitSenders(1) + + handler.close(CancellationException("Test finished")) + job.join() + } + + @Test + fun oneWindowSize() = runTest { channel, handler -> + handler.updateWindowSize(0) + + val job = launch { + handler.sendMessage("Hello".asMessage()) + } + + handler.awaitSenders(1) + + handler.updateWindowSize(1) + + job.join() + + assertEquals("Hello", channel.receive().value) + } + + @Test + fun multipleWindowSize() = runTest { channel, handler -> + handler.updateWindowSize(0) + + val jobs = List(3) { + launch { + handler.sendMessage("Hello".asMessage()) + } + } + + handler.awaitSenders(3) + + handler.updateWindowSize(1) + + assertEquals("Hello", channel.receive().value) + + handler.awaitSenders(2) + + handler.updateWindowSize(2) + + assertEquals("Hello", channel.receive().value) + assertEquals("Hello", channel.receive().value) + + jobs.joinAll() + + handler.updateWindowSize(1) + handler.sendMessage("Hello".asMessage()) + assertEquals("Hello", channel.receive().value) + } +} + +internal abstract class KrpcSendHandlerBaseTest { + protected suspend fun KrpcSendHandler.awaitSenders(num: Int) { + withTimeoutOrNull(10.seconds) { + while (true) { + if (__continuations.size == num) { + break + } + + yield() + } + } ?: fail("Timeout while waiting for continuation, current size: ${__continuations.size}") + } + + protected fun runTest( + timeout: Duration = 10.seconds, + body: suspend TestScope.(Channel, KrpcSendHandler) -> Unit, + ) = runTestWithCoroutinesProbes(timeout = timeout) { + val channel = Channel( + capacity = Channel.UNLIMITED, + ) + + val handler = KrpcSendHandler(channel) + try { + body(channel, handler) + } finally { + handler.close() + channel.cancel() + channel.close() + } + } +} + +val KrpcTransportMessage.value get() = (this as KrpcTransportMessage.StringMessage).value +fun String.asMessage() = KrpcTransportMessage.StringMessage(this) diff --git a/krpc/krpc-core/src/jsMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.js.kt b/krpc/krpc-core/src/jsMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.js.kt index a3a1017b6..030017bfa 100644 --- a/krpc/krpc-core/src/jsMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.js.kt +++ b/krpc/krpc-core/src/jsMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.js.kt @@ -1,13 +1,11 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ @file:Suppress("detekt.MatchingDeclarationName") package kotlinx.rpc.krpc.internal -import kotlinx.rpc.internal.utils.InternalRpcApi - internal actual class DeserializedException actual constructor( private val toStringMessage: String, actual override val message: String, @@ -24,7 +22,7 @@ internal actual class DeserializedException actual constructor( internal actual fun Throwable.stackElements(): List = emptyList() -@InternalRpcApi -public actual fun SerializedException.deserialize(): Throwable { - return DeserializedException(toStringMessage, message, stacktrace, cause, className) +internal actual fun SerializedException.deserializeUnsafe(): Throwable { + return nonJvmManualCancellationExceptionDeserialize() + ?: DeserializedException(toStringMessage, message, stacktrace, cause, className) } diff --git a/krpc/krpc-core/src/jsTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.js.kt b/krpc/krpc-core/src/jsTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.js.kt new file mode 100644 index 000000000..8b39cd68f --- /dev/null +++ b/krpc/krpc-core/src/jsTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.js.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc + +internal actual val stressIterations: Int = 3000 +internal actual val stressBufferSize: Int = 1500 diff --git a/krpc/krpc-core/src/jvmMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.jvm.kt b/krpc/krpc-core/src/jvmMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.jvm.kt index 53ccb4640..9c2341324 100644 --- a/krpc/krpc-core/src/jvmMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.jvm.kt +++ b/krpc/krpc-core/src/jvmMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.jvm.kt @@ -1,10 +1,9 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.krpc.internal -import kotlinx.rpc.internal.utils.InternalRpcApi import java.lang.reflect.Constructor import java.lang.reflect.Modifier @@ -37,8 +36,7 @@ internal actual fun Throwable.stackElements(): List = stackTrace.m ) } -@InternalRpcApi -public actual fun SerializedException.deserialize(): Throwable { +internal actual fun SerializedException.deserializeUnsafe(): Throwable { try { val clazz = Class.forName(className) val fieldsCount = clazz.fieldsCountOrDefault(throwableFields) diff --git a/krpc/krpc-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.jvm.kt b/krpc/krpc-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.jvm.kt new file mode 100644 index 000000000..e348e5539 --- /dev/null +++ b/krpc/krpc-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.jvm.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc + +internal actual val stressIterations: Int = 10_000 +internal actual val stressBufferSize: Int = 500 diff --git a/krpc/krpc-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/KrpcSendHandlerLincheckTest.kt b/krpc/krpc-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/KrpcSendHandlerLincheckTest.kt new file mode 100644 index 000000000..5418efe79 --- /dev/null +++ b/krpc/krpc-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/KrpcSendHandlerLincheckTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedSendChannelException +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.rpc.krpc.internal.KrpcSendHandler +import org.jetbrains.lincheck.datastructures.IntGen +import org.jetbrains.lincheck.datastructures.ModelCheckingOptions +import org.jetbrains.lincheck.datastructures.Operation +import org.jetbrains.lincheck.datastructures.Param +import org.jetbrains.lincheck.datastructures.StressOptions +import kotlin.coroutines.resume +import kotlin.test.Ignore +import kotlin.test.Test + +@Suppress("unused") +@Param(name = "update", gen = IntGen::class, conf = "1:3") +class KrpcSendHandlerLincheckTest { + private val channel = Channel(Channel.UNLIMITED) + private val handler = KrpcSendHandler(channel) + + @Operation + fun updateWindow( + @Param(name = "update") update: Int, + ) = handler.updateWindowSize(update) + + @Operation + suspend fun send(message: String): Unit = handler.sendMessage(message.asMessage()) + + @Operation + fun close() = handler.close() + + @Operation + suspend fun receive(): String = channel.receive().value + + @Test + fun modelTest() = ModelCheckingOptions() + .actorsBefore(2) + .threads(5) + .actorsPerThread(4) + .actorsAfter(2) + .iterations(100) + .invocationsPerIteration(100) +// .checkObstructionFreedom(true) // todo we are not lock free + .minimizeFailedScenario(true) + .sequentialSpecification(SequentialKrpcSendHandler::class.java) + .check(this::class) + + @Test + @Ignore + fun stressTest() = StressOptions() + .actorsBefore(2) + .threads(3) + .actorsPerThread(6) + .actorsAfter(2) + .iterations(10) // 10 is a magic number here, 11 hangs + .invocationsPerIteration(1000) + .minimizeFailedScenario(true) + .sequentialSpecification(SequentialKrpcSendHandler::class.java) + .check(this::class) +} + +@Suppress("unused") +class SequentialKrpcSendHandler { + private var closed = false + private val channel = Channel(Channel.UNLIMITED) + private var window: Int = -1 + private val continuations = mutableListOf>() + + fun updateWindow(update: Int) { + if (closed) { + return + } + + window = if (window == -1) update else window + update + + continuations.forEach { it.resume(Unit) } + continuations.clear() + } + + suspend fun send(message: String) { + if (closed) { + throw ClosedSendChannelException("KrpcSendHandler closed") + } + + val window = window + if (window == -1) { + channel.send(message) + return + } + + while (true) { + if (window == 0) { + suspendCancellableCoroutine { cont -> + continuations.add(cont) + } + } else { + channel.send(message) + updateWindow(-1) + break + } + } + } + + fun close() { + closed = true + } + + suspend fun receive(): String { + return channel.receive() + } +} diff --git a/krpc/krpc-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/KrpcSendHandlerStressTest.kt b/krpc/krpc-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/KrpcSendHandlerStressTest.kt new file mode 100644 index 000000000..13b2d5e42 --- /dev/null +++ b/krpc/krpc-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/KrpcSendHandlerStressTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc + +import kotlinx.coroutines.ExecutorCoroutineDispatcher +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import java.time.Instant +import java.util.concurrent.Executors +import kotlin.random.Random +import kotlin.test.Test +import kotlin.time.Duration.Companion.seconds + +internal class KrpcSendHandlerStressTest : KrpcSendHandlerBaseTest() { + @Test + fun stressNoWindow() = runTest { channel, handler -> + executorDispatcher().use { dispatcher -> + repeat(100_000) { + launch(dispatcher) { + handler.sendMessage("Hello".asMessage()) + } + } + + launch { + repeat(100_000) { + channel.receive() + } + }.join() + } + } + + @Test + fun stressOpenWindow() = runTest { channel, handler -> + executorDispatcher().use { dispatcher -> + handler.updateWindowSize(100_000) + + repeat(100_000) { + launch(dispatcher) { + handler.sendMessage("Hello".asMessage()) + } + } + + launch { + repeat(100_000) { + channel.receive() + } + }.join() + } + } + + @Test + fun stressOpenWindowWithWait() = runTest { channel, handler -> + executorDispatcher().use { dispatcher -> + handler.updateWindowSize(0) + + repeat(100_000) { + launch(dispatcher) { + handler.sendMessage("Hello".asMessage()) + } + } + + val collector = launch { + repeat(100_000) { + channel.receive() + } + } + + handler.awaitSenders(100_000) + + handler.updateWindowSize(100_000) + + collector.join() + } + } + + @Test + fun stressRandomWindow() = runTest(timeout = 30.seconds) { channel, handler -> + executorDispatcher().use { dispatcher -> + handler.updateWindowSize(0) + + repeat(100_000) { + launch(dispatcher) { + handler.sendMessage("Hello".asMessage()) + } + } + + val collector = launch { + repeat(100_000) { + channel.receive() + } + } + + handler.awaitSenders(100_000) + + var processed = 0 + val random = Random(Instant.now().toEpochMilli()) + while (processed < 100_000) { + val nextUpdate = random.nextInt(500, 2000) + handler.updateWindowSize(nextUpdate) + handler.awaitSenders((100_000 - processed - nextUpdate).coerceAtLeast(0)) + println("Processed $processed") + processed += nextUpdate + } + + collector.join() + } + } + + private fun executorDispatcher(): ExecutorCoroutineDispatcher = Executors + .newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2) + .asCoroutineDispatcher() +} diff --git a/krpc/krpc-core/src/nativeMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.native.kt b/krpc/krpc-core/src/nativeMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.native.kt index 989c31df2..07abac13c 100644 --- a/krpc/krpc-core/src/nativeMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.native.kt +++ b/krpc/krpc-core/src/nativeMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.native.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ @file:Suppress("detekt.MatchingDeclarationName") @@ -21,6 +21,7 @@ internal actual class DeserializedException actual constructor( internal actual fun Throwable.stackElements(): List = emptyList() -public actual fun SerializedException.deserialize(): Throwable { - return DeserializedException(toStringMessage, message, stacktrace, cause, className) +internal actual fun SerializedException.deserializeUnsafe(): Throwable { + return nonJvmManualCancellationExceptionDeserialize() + ?: DeserializedException(toStringMessage, message, stacktrace, cause, className) } diff --git a/krpc/krpc-core/src/nativeTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.native.kt b/krpc/krpc-core/src/nativeTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.native.kt new file mode 100644 index 000000000..8b39cd68f --- /dev/null +++ b/krpc/krpc-core/src/nativeTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.native.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc + +internal actual val stressIterations: Int = 3000 +internal actual val stressBufferSize: Int = 1500 diff --git a/krpc/krpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.wasm.kt b/krpc/krpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.wasm.kt index a3a1017b6..8acc01ea0 100644 --- a/krpc/krpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.wasm.kt +++ b/krpc/krpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/krpc/internal/ExceptionUtils.wasm.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ @file:Suppress("detekt.MatchingDeclarationName") @@ -24,7 +24,7 @@ internal actual class DeserializedException actual constructor( internal actual fun Throwable.stackElements(): List = emptyList() -@InternalRpcApi -public actual fun SerializedException.deserialize(): Throwable { - return DeserializedException(toStringMessage, message, stacktrace, cause, className) +internal actual fun SerializedException.deserializeUnsafe(): Throwable { + return nonJvmManualCancellationExceptionDeserialize() + ?: DeserializedException(toStringMessage, message, stacktrace, cause, className) } diff --git a/krpc/krpc-core/src/wasmJsTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.wasmJs.kt b/krpc/krpc-core/src/wasmJsTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.wasmJs.kt new file mode 100644 index 000000000..8b39cd68f --- /dev/null +++ b/krpc/krpc-core/src/wasmJsTest/kotlin/kotlinx/rpc/krpc/KrpcReceiveHandlerTest.wasmJs.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc + +internal actual val stressIterations: Int = 3000 +internal actual val stressBufferSize: Int = 1500 diff --git a/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts b/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts index 6e27be3eb..a05708828 100644 --- a/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts +++ b/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts @@ -27,6 +27,7 @@ kotlin { implementation(projects.krpc.krpcKtor.krpcKtorServer) implementation(projects.krpc.krpcKtor.krpcKtorClient) implementation(projects.krpc.krpcLogging) + implementation(projects.tests.testUtils) implementation(libs.kotlin.test) implementation(libs.ktor.server.netty) diff --git a/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/ktor/KtorTransportTest.kt b/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/ktor/KtorTransportTest.kt index 9eb506b42..680c6cfc5 100644 --- a/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/ktor/KtorTransportTest.kt +++ b/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/ktor/KtorTransportTest.kt @@ -18,21 +18,20 @@ import io.ktor.server.routing.* import io.ktor.server.testing.* import kotlinx.coroutines.* import kotlinx.coroutines.debug.DebugProbes -import kotlinx.coroutines.test.runTest import kotlinx.rpc.annotations.Rpc import kotlinx.rpc.krpc.client.KrpcClient -import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLogger +import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLoggerContainer +import kotlinx.rpc.krpc.internal.logging.dumpLogger import kotlinx.rpc.krpc.ktor.client.installKrpc import kotlinx.rpc.krpc.ktor.client.rpc import kotlinx.rpc.krpc.ktor.client.rpcConfig import kotlinx.rpc.krpc.ktor.server.Krpc import kotlinx.rpc.krpc.ktor.server.rpc import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.test.runTestWithCoroutinesProbes import kotlinx.rpc.withService import org.junit.Assert.assertEquals -import org.junit.platform.commons.logging.Logger -import org.junit.platform.commons.logging.LoggerFactory import java.net.ServerSocket import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -85,8 +84,6 @@ class KtorTransportTest { ignoreUnknownKeys = true } } - - waitForServices = true } registerService { NewServiceImpl(call) } @@ -138,7 +135,7 @@ class KtorTransportTest { @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) @Test @Ignore("Wait for Ktor fix (https://github.com/ktorio/ktor/pull/4927) or apply workaround if rejected") - fun testEndpointsTerminateWhenWsDoes() = runTest(timeout = 15.seconds) { + fun testEndpointsTerminateWhenWsDoes() = runTestWithCoroutinesProbes(timeout = 15.seconds) { DebugProbes.install() val logger = setupLogger() @@ -247,17 +244,10 @@ class KtorTransportTest { return port } - private fun setupLogger(): Logger { - val logger = LoggerFactory.getLogger(KtorTransportTest::class.java) - - RpcInternalDumpLoggerContainer.set(object : RpcInternalDumpLogger { + private fun setupLogger(): RpcInternalCommonLogger { + val logger = RpcInternalCommonLogger.logger(KtorTransportTest::class) - override val isEnabled: Boolean = true - - override fun dump(vararg tags: String, message: () -> String) { - logger.info { "[${tags.joinToString()}] ${message()}" } - } - }) + RpcInternalDumpLoggerContainer.set(logger.dumpLogger()) return logger } diff --git a/krpc/krpc-logging/build.gradle.kts b/krpc/krpc-logging/build.gradle.kts index 11ec6d04e..be0f4ee9c 100644 --- a/krpc/krpc-logging/build.gradle.kts +++ b/krpc/krpc-logging/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ plugins { @@ -15,5 +15,11 @@ kotlin { implementation(projects.utils) } } + + jvmMain { + dependencies { + implementation(libs.slf4j.api) + } + } } } diff --git a/krpc/krpc-logging/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/logging/RpcInternalDumpLogger.kt b/krpc/krpc-logging/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/logging/RpcInternalDumpLogger.kt index 1405167fe..9dcec545b 100644 --- a/krpc/krpc-logging/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/logging/RpcInternalDumpLogger.kt +++ b/krpc/krpc-logging/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/logging/RpcInternalDumpLogger.kt @@ -13,6 +13,17 @@ public interface RpcInternalDumpLogger { public fun dump(vararg tags: String, message: () -> String) } +@InternalRpcApi +public fun RpcInternalCommonLogger.dumpLogger(): RpcInternalDumpLogger { + return object : RpcInternalDumpLogger { + override val isEnabled: Boolean = true + + override fun dump(vararg tags: String, message: () -> String) { + this@dumpLogger.info { "${tags.joinToString(" ") { "[$it]" }} ${message()}" } + } + } +} + @InternalRpcApi public object RpcInternalDumpLoggerContainer { private var logger: RpcInternalDumpLogger? = null diff --git a/krpc/krpc-serialization/krpc-serialization-core/api/krpc-serialization-core.api b/krpc/krpc-serialization/krpc-serialization-core/api/krpc-serialization-core.api index 78a34cca3..0382934af 100644 --- a/krpc/krpc-serialization/krpc-serialization-core/api/krpc-serialization-core.api +++ b/krpc/krpc-serialization/krpc-serialization-core/api/krpc-serialization-core.api @@ -1,7 +1,6 @@ public abstract interface class kotlinx/rpc/krpc/serialization/KrpcSerialFormat { public abstract fun applySerializersModule (Ljava/lang/Object;Lkotlinx/serialization/modules/SerializersModule;)V public abstract fun withBuilder (Lkotlinx/serialization/SerialFormat;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/SerialFormat; - public static synthetic fun withBuilder$default (Lkotlinx/rpc/krpc/serialization/KrpcSerialFormat;Lkotlinx/serialization/SerialFormat;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/SerialFormat; } public final class kotlinx/rpc/krpc/serialization/KrpcSerialFormat$DefaultImpls { diff --git a/krpc/krpc-server/api/krpc-server.api b/krpc/krpc-server/api/krpc-server.api index 7d1f9c37c..d5d349f34 100644 --- a/krpc/krpc-server/api/krpc-server.api +++ b/krpc/krpc-server/api/krpc-server.api @@ -3,7 +3,7 @@ public abstract class kotlinx/rpc/krpc/server/KrpcServer : kotlinx/rpc/RpcServer public final fun awaitCompletion (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun close (Ljava/lang/String;)V public static synthetic fun close$default (Lkotlinx/rpc/krpc/server/KrpcServer;Ljava/lang/String;ILjava/lang/Object;)V - public fun deregisterService (Lkotlin/reflect/KClass;)V + public final fun deregisterService (Lkotlin/reflect/KClass;)V public final fun registerService (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function0;)V } diff --git a/krpc/krpc-server/api/krpc-server.klib.api b/krpc/krpc-server/api/krpc-server.klib.api index e7f08f0ed..05eca67ef 100644 --- a/krpc/krpc-server/api/krpc-server.klib.api +++ b/krpc/krpc-server/api/krpc-server.klib.api @@ -9,8 +9,8 @@ abstract class kotlinx.rpc.krpc.server/KrpcServer : kotlinx.rpc.krpc.internal/KrpcEndpoint, kotlinx.rpc/RpcServer { // kotlinx.rpc.krpc.server/KrpcServer|null[0] constructor (kotlinx.rpc.krpc/KrpcConfig.Server, kotlinx.rpc.krpc/KrpcTransport) // kotlinx.rpc.krpc.server/KrpcServer.|(kotlinx.rpc.krpc.KrpcConfig.Server;kotlinx.rpc.krpc.KrpcTransport){}[0] + final fun <#A1: kotlin/Any> deregisterService(kotlin.reflect/KClass<#A1>) // kotlinx.rpc.krpc.server/KrpcServer.deregisterService|deregisterService(kotlin.reflect.KClass<0:0>){0§}[0] final fun <#A1: kotlin/Any> registerService(kotlin.reflect/KClass<#A1>, kotlin/Function0<#A1>) // kotlinx.rpc.krpc.server/KrpcServer.registerService|registerService(kotlin.reflect.KClass<0:0>;kotlin.Function0<0:0>){0§}[0] final fun close(kotlin/String? = ...) // kotlinx.rpc.krpc.server/KrpcServer.close|close(kotlin.String?){}[0] final suspend fun awaitCompletion() // kotlinx.rpc.krpc.server/KrpcServer.awaitCompletion|awaitCompletion(){}[0] - open fun <#A1: kotlin/Any> deregisterService(kotlin.reflect/KClass<#A1>) // kotlinx.rpc.krpc.server/KrpcServer.deregisterService|deregisterService(kotlin.reflect.KClass<0:0>){0§}[0] } diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/KrpcServer.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/KrpcServer.kt index 1e45590a6..24545fac6 100644 --- a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/KrpcServer.kt +++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/KrpcServer.kt @@ -70,7 +70,7 @@ public abstract class KrpcServer( KrpcServerConnector( serialFormat = config.serialFormatInitializer.build(), transport = transport, - waitForServices = config.waitForServices, + config = config.connector, ) } @@ -133,7 +133,7 @@ public abstract class KrpcServer( } } - override fun <@Rpc Service : Any> deregisterService(serviceKClass: KClass) { + final override fun <@Rpc Service : Any> deregisterService(serviceKClass: KClass) { connector.unsubscribeFromServiceMessages(serviceDescriptorOf(serviceKClass).fqName) rpcServices.remove(serviceDescriptorOf(serviceKClass).fqName) } @@ -175,7 +175,7 @@ public abstract class KrpcServer( else -> { logger.warn { "Unsupported ${KrpcPluginKey.CANCELLATION_TYPE} $type for server, " + - "only 'endpoint' type may be sent by a server" + "only 'endpoint' type may be sent by a client" } } } diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerConnector.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerConnector.kt index 886441247..3c669be4f 100644 --- a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerConnector.kt +++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerConnector.kt @@ -1,61 +1,51 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.krpc.server.internal +import kotlinx.rpc.krpc.KrpcConfig import kotlinx.rpc.krpc.KrpcTransport import kotlinx.rpc.krpc.internal.* import kotlinx.serialization.SerialFormat -internal sealed interface MessageKey { - data class Service(val serviceTypeString: String): MessageKey - - data object Protocol: MessageKey - - data object Generic: MessageKey -} - internal class KrpcServerConnector private constructor( - private val connector: KrpcConnector, -): KrpcMessageSender by connector { + private val connector: KrpcConnector, +) : KrpcMessageSender by connector { constructor( serialFormat: SerialFormat, transport: KrpcTransport, - waitForServices: Boolean = false, + config: KrpcConfig.Connector, ) : this( - KrpcConnector(serialFormat, transport, waitForServices, isServer = true) { - when (this) { - is KrpcCallMessage -> MessageKey.Service(serviceType) - is KrpcProtocolMessage -> MessageKey.Protocol - is KrpcGenericMessage -> MessageKey.Generic - } - } + KrpcConnector(serialFormat, transport, config, isServer = true) ) - fun unsubscribeFromServiceMessages(serviceTypeString: String, callback: () -> Unit = {}) { - connector.unsubscribeFromMessages(MessageKey.Service(serviceTypeString), callback) + fun unsubscribeFromServiceMessages(serviceTypeString: String, callback: suspend () -> Unit = {}) { + connector.unsubscribeFromMessagesAsync(HandlerKey.Service(serviceTypeString), callback) + } + + suspend fun unsubscribeFromCallMessages(serviceTypeString: String, callId: String) { + connector.unsubscribeFromMessages(HandlerKey.ServiceCall(serviceTypeString, callId)) } suspend fun subscribeToServiceMessages( serviceTypeString: String, subscription: suspend (KrpcCallMessage) -> Unit, ) { - connector.subscribeToMessages(MessageKey.Service(serviceTypeString)) { - subscription(it as KrpcCallMessage) + connector.subscribeToMessages(HandlerKey.Service(serviceTypeString)) { + subscription(it) } } suspend fun subscribeToProtocolMessages(subscription: suspend (KrpcProtocolMessage) -> Unit) { - connector.subscribeToMessages(MessageKey.Protocol) { - subscription(it as KrpcProtocolMessage) + connector.subscribeToMessages(HandlerKey.Protocol) { + subscription(it) } } - @Suppress("unused") suspend fun subscribeToGenericMessages(subscription: suspend (KrpcGenericMessage) -> Unit) { - connector.subscribeToMessages(MessageKey.Generic) { - subscription(it as KrpcGenericMessage) + connector.subscribeToMessages(HandlerKey.Generic) { + subscription(it) } } } diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerService.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerService.kt index 9e0afcf5d..2afac9ebc 100644 --- a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerService.kt +++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerService.kt @@ -41,25 +41,26 @@ internal class KrpcServerService<@Rpc T : Any>( val exception = result.exceptionOrNull() ?: error("Expected exception value") - cancelRequest( + closeReceiving( callId = message.callId, message = "Cancelled after failed to process message: $message, error message: ${exception.message}", cause = exception, ) if (exception is CancellationException) { - return + currentCoroutineContext().ensureActive() } - val error = serializeException(exception) + val serialized = serializeException(exception) val errorMessage = KrpcCallMessage.CallException( callId = message.callId, serviceType = message.serviceType, - cause = error, + cause = serialized, connectionId = message.connectionId, ) connector.sendMessage(errorMessage) + unsubscribeFromCallMessages(message.callId) } } @@ -72,7 +73,7 @@ internal class KrpcServerService<@Rpc T : Any>( } is KrpcCallMessage.CallException -> { - cancelRequest( + closeReceiving( callId = message.callId, message = "Cancelled after KrpcCallMessage.CallException received", cause = message.cause.deserialize(), @@ -181,24 +182,32 @@ internal class KrpcServerService<@Rpc T : Any>( sendMessages(serialFormat, returnSerializer, value, callData) } } catch (cause: CancellationException) { - throw cause - } catch (@Suppress("detekt.TooGenericExceptionCaught") cause: Throwable) { + currentCoroutineContext().ensureActive() + + val wrapped = ManualCancellationException(cause) + + failure = wrapped + } catch (cause: Throwable) { failure = cause + } finally { + if (failure != null) { + val serializedCause = serializeException(failure) + val exceptionMessage = KrpcCallMessage.CallException( + callId = callId, + serviceType = descriptor.fqName, + cause = serializedCause, + connectionId = callData.connectionId, + serviceId = callData.serviceId, + ) - val serializedCause = serializeException(cause) - KrpcCallMessage.CallException( - callId = callId, - serviceType = descriptor.fqName, - cause = serializedCause, - connectionId = callData.connectionId, - serviceId = callData.serviceId, - ).also { connector.sendMessage(it) } - } + connector.sendMessage(exceptionMessage) + + closeReceiving(callId, "Server request failed", failure, fromJob = true) + } else { + closeReceiving(callId, fromJob = true) + } - if (failure != null) { - cancelRequest(callId, "Server request failed", failure, fromJob = true) - } else { - cancelRequest(callId, fromJob = true) + unsubscribeFromCallMessages(callId) } } @@ -207,6 +216,10 @@ internal class KrpcServerService<@Rpc T : Any>( requestJob.start() } + suspend fun unsubscribeFromCallMessages(callId: String) { + connector.unsubscribeFromCallMessages(descriptor.fqName, callId) + } + private suspend fun sendMessages( serialFormat: SerialFormat, returnSerializer: KSerializer, @@ -296,7 +309,7 @@ internal class KrpcServerService<@Rpc T : Any>( ) } catch (cause: CancellationException) { throw cause - } catch (@Suppress("detekt.TooGenericExceptionCaught") cause: Throwable) { + } catch (cause: Throwable) { val serializedCause = serializeException(cause) connector.sendMessage( KrpcCallMessage.StreamCancel( @@ -311,7 +324,7 @@ internal class KrpcServerService<@Rpc T : Any>( } } - suspend fun cancelRequest( + suspend fun closeReceiving( callId: String, message: String? = null, cause: Throwable? = null, @@ -325,7 +338,7 @@ internal class KrpcServerService<@Rpc T : Any>( message: String? = null, cause: Throwable? = null, ) { - cancelRequest(callId, message, cause, fromJob = false) + closeReceiving(callId, message, cause, fromJob = false) if (!supportedPlugins.contains(KrpcPlugin.NO_ACK_CANCELLATION)) { connector.sendMessage( @@ -339,6 +352,8 @@ internal class KrpcServerService<@Rpc T : Any>( ) ) } + + unsubscribeFromCallMessages(callId) } private val serverStreamContext: ServerStreamContext = ServerStreamContext() diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/ServerStreamContext.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/ServerStreamContext.kt index 0311dd4fa..7d99e1ff9 100644 --- a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/ServerStreamContext.kt +++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/ServerStreamContext.kt @@ -55,7 +55,7 @@ internal class ServerStreamContext { fun prepareClientStream(streamId: String, elementKind: KSerializer): Flow { val callId = currentCallId.get() ?: error("No call id") - val channel = Channel(Channel.UNLIMITED) + val channel = Channel() @Suppress("UNCHECKED_CAST") val map = streams.computeIfAbsent(callId) { RpcInternalConcurrentHashMap() } diff --git a/krpc/krpc-test/build.gradle.kts b/krpc/krpc-test/build.gradle.kts index 288bf56dd..758e6b45f 100644 --- a/krpc/krpc-test/build.gradle.kts +++ b/krpc/krpc-test/build.gradle.kts @@ -6,7 +6,6 @@ import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmTest -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.nio.file.Files plugins { @@ -38,6 +37,8 @@ kotlin { api(projects.krpc.krpcClient) api(projects.krpc.krpcLogging) + implementation(projects.tests.testUtils) + implementation(projects.krpc.krpcSerialization.krpcSerializationJson) implementation(libs.serialization.core) @@ -71,6 +72,7 @@ kotlin { implementation(libs.slf4j.api) implementation(libs.logback.classic) implementation(libs.coroutines.debug) + implementation(libs.lincheck) } } } @@ -79,7 +81,15 @@ kotlin { } tasks.withType { + // lincheck agent + jvmArgs("-XX:+EnableDynamicAgentLoading") environment("LIBRARY_VERSION", libs.versions.kotlinx.rpc.get()) + + if (project.hasProperty("stressTests") && project.property("stressTests") == "true") { + include("kotlinx/rpc/krpc/test/stress/**") + } else { + exclude("kotlinx/rpc/krpc/test/stress/**") + } } val resourcesPath = projectDir.resolve("src/jvmTest/resources") diff --git a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestService.kt b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestService.kt index b3e803c98..1c46e57c9 100644 --- a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestService.kt +++ b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestService.kt @@ -48,6 +48,7 @@ interface KrpcTestService { fun nonSuspendBidirectional(flow: Flow): Flow fun nonSuspendBidirectionalPayload(payloadWithStream: PayloadWithStream): Flow + fun slowConsumer(): Flow suspend fun empty() suspend fun returnType(): String suspend fun simpleWithParams(name: String): String diff --git a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.kt b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.kt index 5042bb308..bd1b386bc 100644 --- a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.kt +++ b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.kt @@ -6,7 +6,7 @@ package kotlinx.rpc.krpc.test import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.test.TestScope +import kotlinx.rpc.test.runThreadIfPossible import kotlinx.serialization.Serializable import kotlin.coroutines.resumeWithException import kotlin.test.assertContentEquals @@ -42,6 +42,15 @@ class KrpcTestServiceBackend : KrpcTestService { return payloadWithStream.stream.map { it.length } } + override fun slowConsumer(): Flow { + return flow { + repeat(10) { + delay(100) + emit(it) + } + } + } + @Suppress("detekt.EmptyFunctionBlock") override suspend fun empty() {} @@ -98,7 +107,9 @@ class KrpcTestServiceBackend : KrpcTestService { return arg1 } - override suspend fun returnTestClassThatThrowsWhileDeserialization(value: Int): TestClassThatThrowsWhileDeserialization { + override suspend fun returnTestClassThatThrowsWhileDeserialization( + value: Int, + ): TestClassThatThrowsWhileDeserialization { return TestClassThatThrowsWhileDeserialization(value) } @@ -147,12 +158,19 @@ class KrpcTestServiceBackend : KrpcTestService { return arg1.count() } + @OptIn(DelicateCoroutinesApi::class) + @Suppress("detekt.GlobalCoroutineUsage") override suspend fun incomingStreamSyncCollectMultiple( arg1: Flow, arg2: Flow, arg3: Flow, ): Int { - return arg1.count() + arg2.count() + arg3.count() + // buffer of size 1 may cause lock here without multiple coroutines + return listOf( + GlobalScope.async { arg1.count() }, + GlobalScope.async { arg2.count() }, + GlobalScope.async { arg3.count() }, + ).awaitAll().sum() } override fun outgoingStream(): Flow { @@ -250,7 +268,3 @@ class KrpcTestServiceBackend : KrpcTestService { } } } - -internal expect fun runThreadIfPossible(runner: () -> Unit) - -internal expect fun TestScope.debugCoroutines() diff --git a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTransportTestBase.kt b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTransportTestBase.kt index d343c376f..581439b04 100644 --- a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTransportTestBase.kt +++ b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTransportTestBase.kt @@ -11,13 +11,15 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.TestScope import kotlinx.rpc.krpc.KrpcTransport import kotlinx.rpc.krpc.rpcClientConfig import kotlinx.rpc.krpc.rpcServerConfig import kotlinx.rpc.krpc.serialization.KrpcSerialFormatConfiguration import kotlinx.rpc.krpc.server.KrpcServer import kotlinx.rpc.registerService +import kotlinx.rpc.test.runTestWithCoroutinesProbes import kotlinx.rpc.withService import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException @@ -29,6 +31,7 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.modules.SerializersModule import kotlin.coroutines.cancellation.CancellationException import kotlin.test.* +import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds internal object LocalDateSerializer : KSerializer { @@ -283,6 +286,23 @@ abstract class KrpcTransportTestBase { assertEquals(9, result) } + @Test + fun slowConsumer() = runTest { + val foreverAwait = CompletableDeferred() + val collectFirst = CompletableDeferred() + val slow = launch { + client.slowConsumer().collect { + collectFirst.complete(Unit) + foreverAwait.await() + } + } + + collectFirst.await() + val fast = client.simpleWithParams("test") + assertEquals(fast, "tset") + slow.cancelAndJoin() + } + @Test fun outgoingStream() = runTest { val result = client.outgoingStream() @@ -311,9 +331,16 @@ abstract class KrpcTransportTestBase { } @Test - fun RPC_should_be_able_to_receive_100_000_ints_in_reasonable_time() = runTest(timeout = JS_EXTENDED_TIMEOUT) { + fun RPC_should_be_able_to_receive_100_000_ints_in_reasonable_time() = runTest(timeout = EXTENDED_TIMEOUT) { val n = 100_000 - assertEquals(client.getNInts(n).last(), n) + var counter = 0 + val last = client.getNInts(n).onEach { + counter++ + if (counter % 1000 == 0) { + println("Iteration: $counter") + } + }.last() + assertEquals(n, last) } @Test @@ -330,7 +357,7 @@ abstract class KrpcTransportTestBase { } @Test - @Suppress("detekt.TooGenericExceptionCaught", "detekt.ThrowsCount") + @Suppress("detekt.ThrowsCount") fun testExceptionSerializationAndPropagating() = runTest { try { client.throwsIllegalArgument("me") @@ -472,8 +499,12 @@ abstract class KrpcTransportTestBase { } } } + + private fun runTest(timeout: Duration = 10.seconds, testBody: suspend TestScope.() -> Unit): TestResult { + return runTestWithCoroutinesProbes(timeout = timeout, body = testBody) + } } -private val JS_EXTENDED_TIMEOUT = if (isJs) 300.seconds else 60.seconds +private val EXTENDED_TIMEOUT = if (isJs) 500.seconds else 200.seconds internal expect val isJs: Boolean diff --git a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/BackPressureTest.kt b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/BackPressureTest.kt new file mode 100644 index 000000000..4297462c6 --- /dev/null +++ b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/BackPressureTest.kt @@ -0,0 +1,200 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.async +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.job +import kotlinx.coroutines.test.TestScope +import kotlinx.rpc.annotations.Rpc +import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLoggerContainer +import kotlinx.rpc.krpc.rpcClientConfig +import kotlinx.rpc.krpc.rpcServerConfig +import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.registerService +import kotlinx.rpc.test.WaitCounter +import kotlinx.rpc.test.runTestWithCoroutinesProbes +import kotlinx.rpc.withService +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Rpc +interface BackPressure { + suspend fun plain() + + fun serverStream(num: Int): Flow + + suspend fun clientStream(flow: Flow) +} + +class BackPressureImpl : BackPressure { + val plainCounter = WaitCounter() + val serverStreamCounter = WaitCounter() + val clientStreamCounter = WaitCounter() + val entered = CompletableDeferred() + val fence = CompletableDeferred() + + override suspend fun plain() { + plainCounter.increment() + } + + override fun serverStream(num: Int): Flow { + return flow { + repeat(num) { + serverStreamCounter.increment() + emit(it) + } + } + } + + val consumed = mutableListOf() + override suspend fun clientStream(flow: Flow) { + flow.collect { + if (it == 0) { + entered.complete(Unit) + fence.await() + } + consumed.add(it) + } + } +} + +class BackPressureTest : BackPressureTestBase() { + @Test + fun buffer_size_1_server() = runServerTest(perCallBufferSize = 1) + + @Test + fun buffer_size_10_server() = runServerTest(perCallBufferSize = 10) + + @Test + fun buffer_size_1_client() = runClientTest(perCallBufferSize = 1) + + @Test + fun buffer_size_10_client() = runClientTest(perCallBufferSize = 10) +} + +// `+2` explanation: +// 1. the first element is sent and processed by the client +// 2. the second element is sent and is suspended on the client +// because the processing of the first element is not finished yet +// 3. the third element is n from the flow and suspended on the server side +abstract class BackPressureTestBase { + protected fun runServerTest( + perCallBufferSize: Int, + timeout: Duration = 30.seconds, + ) = runTest(perCallBufferSize, timeout) { service, impl -> + var counter = 0 + val flowList = async { + service.serverStream(1000).map { + if (it == 0) { + impl.entered.complete(Unit) + impl.fence.await() + } + counter++ + if (counter % 10 == 0) { + println("Iteration: $counter") + } + }.toList() + } + + impl.entered.await() + impl.serverStreamCounter.await(perCallBufferSize + 2) + + repeat(1000) { + service.plain() + } + + impl.plainCounter.await(1000) + + assertEquals(perCallBufferSize + 2, impl.serverStreamCounter.value) + impl.fence.complete(Unit) + impl.serverStreamCounter.await(1000) + assertEquals(1000, flowList.await().size) + } + + protected fun runClientTest( + perCallBufferSize: Int, + timeout: Duration = 30.seconds, + ) = runTest(perCallBufferSize, timeout) { service, impl -> + var counter = 0 + val flowList = async { + service.clientStream(flow { + repeat(1000) { + impl.clientStreamCounter.increment() + emit(it) + counter++ + if (counter % 10 == 0) { + println("Iteration: $counter") + } + } + }) + } + + impl.entered.await() + impl.clientStreamCounter.await(perCallBufferSize + 2) + + repeat(1000) { + service.plain() + } + + impl.plainCounter.await(1000) + + assertEquals(0, impl.consumed.size) + assertEquals(perCallBufferSize + 2, impl.clientStreamCounter.value) + impl.fence.complete(Unit) + impl.clientStreamCounter.await(1000) + flowList.await() + assertEquals(1000, impl.consumed.size) + } + + protected fun runTest( + perCallBufferSize: Int, + timeout: Duration = 10.seconds, + body: suspend TestScope.(BackPressure, BackPressureImpl) -> Unit, + ) = runTestWithCoroutinesProbes(timeout = timeout) { + val transport = LocalTransport(coroutineContext, recordTimestamps = false) + val clientConfig = rpcClientConfig { + serialization { + json() + } + + connector { + this.perCallBufferSize = perCallBufferSize + } + } + val client = KrpcTestClient(clientConfig, transport.client) + val serverConfig = rpcServerConfig { + serialization { + json() + } + + connector { + this.perCallBufferSize = perCallBufferSize + } + } + val server = KrpcTestServer(serverConfig, transport.server) + val impl = BackPressureImpl() + server.registerService { impl } + val service = client.withService() + + try { + body(service, impl) + } finally { + RpcInternalDumpLoggerContainer.set(null) + client.close() + server.close() + client.awaitCompletion() + server.awaitCompletion() + transport.coroutineContext.job.cancelAndJoin() + } + } +} diff --git a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/CoroutineContextPropagationTest.kt b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/CoroutineContextPropagationTest.kt index a0a3704a5..aade86b3a 100644 --- a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/CoroutineContextPropagationTest.kt +++ b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/CoroutineContextPropagationTest.kt @@ -4,16 +4,19 @@ package kotlinx.rpc.krpc.test +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.job import kotlinx.coroutines.withContext import kotlinx.rpc.krpc.rpcClientConfig import kotlinx.rpc.krpc.rpcServerConfig import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.test.runTestWithCoroutinesProbes import kotlinx.rpc.withService import kotlin.coroutines.CoroutineContext import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.seconds class CoroutineContextPropagationTest { private val rpcServerConfig = rpcServerConfig { @@ -36,7 +39,7 @@ class CoroutineContextPropagationTest { } @Test - fun test() = runTest { + fun test() = runTestWithCoroutinesProbes(timeout = 60.seconds) { var actualContext: CoroutineElement? = null val transport = LocalTransport(CoroutineElement("transport")) val server = KrpcTestServer(rpcServerConfig, transport.server) @@ -51,9 +54,17 @@ class CoroutineContextPropagationTest { } } } - withContext(CoroutineElement("client")) { - client.withService(Echo::class).echo("request") + try { + withContext(CoroutineElement("client")) { + client.withService(Echo::class).echo("request") + } + assertEquals(CoroutineElement("transport"), actualContext) + } finally { + server.close() + client.close() + server.awaitCompletion() + client.awaitCompletion() + transport.coroutineContext.job.cancelAndJoin() } - assertEquals(CoroutineElement("transport"), actualContext) } } diff --git a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/LocalTransport.kt b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/LocalTransport.kt index 5f9073d46..a91134667 100644 --- a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/LocalTransport.kt +++ b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/LocalTransport.kt @@ -18,6 +18,7 @@ import kotlin.time.ExperimentalTime class LocalTransport( parentContext: CoroutineContext? = null, + private val recordTimestamps: Boolean = true, ) : CoroutineScope { override val coroutineContext = SupervisorJob(parentContext?.get(Job)) @@ -34,7 +35,9 @@ class LocalTransport( @OptIn(ExperimentalTime::class) override suspend fun send(message: KrpcTransportMessage) { - lastMessageSentOnClient.getAndSet(Clock.System.now().toEpochMilliseconds()) + if (recordTimestamps) { + lastMessageSentOnClient.getAndSet(Clock.System.now().toEpochMilliseconds()) + } serverIncoming.send(message) } @@ -50,7 +53,9 @@ class LocalTransport( @OptIn(ExperimentalTime::class) override suspend fun send(message: KrpcTransportMessage) { - lastMessageSentOnServer.getAndSet(Clock.System.now().toEpochMilliseconds()) + if (recordTimestamps) { + lastMessageSentOnServer.getAndSet(Clock.System.now().toEpochMilliseconds()) + } clientIncoming.send(message) } diff --git a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/ProtocolTestBase.kt b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/ProtocolTestBase.kt index f714c20d7..bf50b6303 100644 --- a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/ProtocolTestBase.kt +++ b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/ProtocolTestBase.kt @@ -21,8 +21,10 @@ import kotlinx.rpc.krpc.rpcServerConfig import kotlinx.rpc.krpc.serialization.json.json import kotlinx.rpc.krpc.server.KrpcServer import kotlinx.rpc.registerService +import kotlinx.rpc.test.runTestWithCoroutinesProbes import kotlinx.rpc.withService import kotlinx.serialization.BinaryFormat +import kotlin.time.Duration.Companion.seconds abstract class ProtocolTestBase { protected fun runTest( @@ -38,10 +40,14 @@ abstract class ProtocolTestBase { }, block: suspend TestBody.() -> Unit, ): TestResult { - return kotlinx.coroutines.test.runTest { - val finished = TestBody(clientConfig, serverConfig, this).apply { block() } + return runTestWithCoroutinesProbes(timeout = 60.seconds) { + val finished = TestBody(clientConfig, serverConfig, this) - finished.transport.coroutineContext.job.cancelAndJoin() + try { + finished.block() + } finally { + finished.transport.coroutineContext.job.cancelAndJoin() + } } } diff --git a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.kt b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.kt index 6e29b8f46..8897ef491 100644 --- a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.kt +++ b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.kt @@ -16,16 +16,17 @@ import kotlinx.rpc.krpc.KrpcConfigBuilder import kotlinx.rpc.krpc.KrpcTransport import kotlinx.rpc.krpc.client.KrpcClient import kotlinx.rpc.krpc.internal.KrpcProtocolMessage -import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLogger import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLoggerContainer import kotlinx.rpc.krpc.rpcClientConfig import kotlinx.rpc.krpc.rpcServerConfig import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.test.runTestWithCoroutinesProbes import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertTrue +import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @Rpc @@ -61,12 +62,20 @@ class TransportTest { serialization { json() } + + connector { + waitTimeout = Duration.INFINITE + } } private val serverConfig = rpcServerConfig { serialization { json() } + + connector { + waitTimeout = Duration.INFINITE + } } private fun clientOf(localTransport: LocalTransport): RpcClient { @@ -81,12 +90,12 @@ class TransportTest { return KrpcTestServer(serverConfig, localTransport.server) } - private fun runTest(block: suspend TestScope.(logs: List) -> Unit): TestResult = - kotlinx.coroutines.test.runTest(timeout = 20.seconds) { - debugCoroutines() - - val logger = RpcInternalCommonLogger.logger("TransportTest") - + private fun runTest( + timeout: Duration = 120.seconds, + times: Int = testIterations, + block: suspend TestScope.(logs: List) -> Unit, + ): TestResult = runTestWithCoroutinesProbes(timeout = timeout) { + repeat(times) { val logs = mutableListOf() val logsChannel = Channel(Channel.UNLIMITED) @@ -102,16 +111,18 @@ class TransportTest { override fun dump(vararg tags: String, message: () -> String) { val message = "${tags.joinToString(" ") { "[$it]" }} ${message()}" logsChannel.trySend(message) - logger.info { message } } }) - block(logs) - - RpcInternalDumpLoggerContainer.set(null) - logsJob.cancelAndJoin() - logsChannel.close() + try { + block(logs) + } finally { + RpcInternalDumpLoggerContainer.set(null) + logsJob.cancelAndJoin() + logsChannel.close() + } } + } @Test fun testUsingWrongService() = runTest { @@ -129,7 +140,9 @@ class TransportTest { json() } - waitForServices = false + connector { + waitTimeout = dontWait() + } } server.registerService { EchoImpl() } @@ -201,7 +214,7 @@ class TransportTest { } @Test - fun testLateConnectWithManyCallsAndClients() = runTest { + fun testLateConnectWithManyCallsAndClients() = runTest(timeout = 240.seconds) { val transports = LocalTransport() val client = clientOf(transports) @@ -266,7 +279,7 @@ class TransportTest { private val configInitialized = atomic(0) @Test - fun transportInitializedOnlyOnce() = runTest { logs -> + fun transportInitializedOnlyOnce() = runTest(times = 1) { logs -> val localTransport = LocalTransport() val client = object : KrpcClient() { override suspend fun initializeTransport(): KrpcTransport { @@ -303,3 +316,5 @@ class TransportTest { return instances } } + +internal expect val testIterations: Int diff --git a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationService.kt b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationService.kt index 9ffe82c24..0e32e05c7 100644 --- a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationService.kt +++ b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationService.kt @@ -4,47 +4,58 @@ package kotlinx.rpc.krpc.test.cancellation -import kotlinx.atomicfu.atomic import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.rpc.annotations.Rpc +import kotlinx.rpc.test.WaitCounter @Rpc interface CancellationService { suspend fun longRequest() - suspend fun serverDelay(millis: Long) - suspend fun callException() + suspend fun serverCancellation() + fun incomingStream(): Flow + fun cancellationInIncomingStream(): Flow + + suspend fun cancellationInOutgoingStream(stream: Flow, cancelled: Flow) + suspend fun outgoingStream(stream: Flow) suspend fun outgoingStreamAsync(stream: Flow) suspend fun outgoingStreamWithDelayedResponse(stream: Flow) - suspend fun outgoingStreamWithException(stream: Flow) - fun nonSuspendable(): Flow } class CancellationServiceImpl : CancellationService { - val delayCounter = atomic(0) + val waitCounter = WaitCounter() + val successCounter = WaitCounter() + val cancellationsCounter = WaitCounter() + val consumedIncomingValues = mutableListOf() val firstIncomingConsumed = CompletableDeferred() val consumedAll = CompletableDeferred() val fence = CompletableDeferred() override suspend fun longRequest() { - firstIncomingConsumed.complete(0) - fence.await() + try { + firstIncomingConsumed.complete(0) + waitCounter.increment() + fence.await() + successCounter.increment() + } catch (e: CancellationException) { + cancellationsCounter.increment() + throw e + } } - override suspend fun serverDelay(millis: Long) { - delay(millis) - delayCounter.incrementAndGet() + override suspend fun serverCancellation() { + throw CancellationException("serverCancellation") } override suspend fun callException() { @@ -55,6 +66,34 @@ class CancellationServiceImpl : CancellationService { return resumableFlow(fence) } + override fun cancellationInIncomingStream(): Flow { + return flow { + emit(1) + throw CancellationException("cancellationInIncomingStream") + } + } + + override suspend fun cancellationInOutgoingStream(stream: Flow, cancelled: Flow) { + supervisorScope { + launch { + consume(stream) + } + + launch { + try { + cancelled.collect { + if (it == 0) { + firstIncomingConsumed.complete(it) + } + } + } catch (e: CancellationException) { + cancellationsCounter.increment() + throw e + } + } + } + } + override suspend fun outgoingStream(stream: Flow) { consume(stream) } @@ -69,18 +108,14 @@ class CancellationServiceImpl : CancellationService { } override suspend fun outgoingStreamWithDelayedResponse(stream: Flow) { - consume(stream) - - unskippableDelay(10000) - } - - override suspend fun outgoingStreamWithException(stream: Flow) { - consume(stream) - - unskippableDelay(300) + try { + consume(stream) - // it will not cancel launch collector - error("exception in request") + fence.await() + } catch (e: CancellationException) { + cancellationsCounter.increment() + throw e + } } private suspend fun consume(stream: Flow) { diff --git a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationTest.kt b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationTest.kt index dcf356156..0669d0082 100644 --- a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationTest.kt +++ b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationTest.kt @@ -11,36 +11,51 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.toList import kotlinx.rpc.withService -import kotlin.test.* +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail class CancellationTest { @Test fun testCancelRequestScope() = runCancellationTest { val cancellingRequestJob = launch { - service.serverDelay(100) + service.longRequest() } val aliveRequestJob = launch { - service.serverDelay(300) + service.longRequest() } + serverInstance().waitCounter.await(2) cancellingRequestJob.cancelAndJoin() + serverInstance().cancellationsCounter.await(1) + serverInstance().fence.complete(Unit) aliveRequestJob.join() assertFalse(aliveRequestJob.isCancelled, "Expected aliveRequestJob not to be cancelled") assertTrue(cancellingRequestJob.isCancelled, "Expected cancellingRequestJob to be cancelled") - assertEquals(1, serverInstances.single().delayCounter.value, "Expected one request to be cancelled") + assertEquals(1, serverInstance().successCounter.value, "Expected one request to be cancelled") checkAlive() stopAllAndJoin() + + assertEquals(1, serverInstance().successCounter.value, "Expected one request to succeed") + assertEquals(1, serverInstance().cancellationsCounter.value, "Expected one request to be cancelled") } @Test fun testCallException() = runCancellationTest { val requestJob = launch { - service.serverDelay(300) + service.longRequest() } + serverInstance().firstIncomingConsumed.await() + val exceptionRequestJob = launch { try { service.callException() @@ -52,73 +67,149 @@ class CancellationTest { } exceptionRequestJob.join() + serverInstance().fence.complete(Unit) requestJob.join() assertFalse(requestJob.isCancelled, "Expected requestJob not to be cancelled") assertTrue(exceptionRequestJob.isCancelled, "Expected exception in callException call") - assertEquals(1, serverInstances.single().delayCounter.value, "Error should not cancel parallel request") + assertEquals(1, serverInstance().successCounter.value, "Error should not cancel parallel request") + + checkAlive() + stopAllAndJoin() + + assertEquals(0, serverInstance().cancellationsCounter.value, "Expected no requests to be cancelled") + } + + @Test + fun testServerRequestCancellation() = runCancellationTest { + supervisorScope { + val requestJob = launch { + service.serverCancellation() + } + + requestJob.join() + + assertTrue(requestJob.isCancelled, "Expected requestJob to be cancelled") + } + + checkAlive() + stopAllAndJoin() + } + + @Test + fun testCancellationInServerStream() = runCancellationTest { + supervisorScope { + var ex: CancellationException? = null + val requestJob = launch { + try { + service.cancellationInIncomingStream().toList() + } catch (e: CancellationException) { + ex = e + throw e + } + } + + requestJob.join() + + assertTrue(requestJob.isCancelled, "Expected requestJob to be cancelled") + assertNotNull(ex, "Expected requestJob to be cancelled with a CancellationException") + } checkAlive() stopAllAndJoin() } + @Test + fun testCancellationInClientStream() = runCancellationTest { + supervisorScope { + val requestJob = launch { + service.cancellationInOutgoingStream( + stream = flow { + emit(42) + emit(43) + }, + cancelled = flow { + emit(1) + serverInstance().firstIncomingConsumed.await() + throw CancellationException("cancellationInClientStream") + }, + ) + } + + requestJob.join() + serverInstance().consumedAll.await() + + assertFalse(requestJob.isCancelled, "Expected requestJob not to be cancelled") + assertContentEquals(listOf(42, 43), serverInstance().consumedIncomingValues) + } + + checkAlive() + stopAllAndJoin() + + assertEquals(1, serverInstance().cancellationsCounter.value, "Expected 1 request to be cancelled") + } + @Test fun testCancelClient() = runCancellationTest { val firstRequestJob = launch { - service.serverDelay(300) + service.longRequest() } val secondService = client.withService() val secondRequestJob = launch { - secondService.serverDelay(300) + secondService.longRequest() } - unskippableDelay(150) // wait for requests to reach server + serverInstance().waitCounter.await(2) client.close() + client.awaitCompletion() + server.awaitCompletion() firstRequestJob.join() secondRequestJob.join() + serverInstance().cancellationsCounter.await(2) assertTrue(firstRequestJob.isCancelled, "Expected firstRequestJob to be cancelled") assertTrue(secondRequestJob.isCancelled, "Expected secondRequestJob to be cancelled") - assertEquals(0, serverInstances.sumOf { it.delayCounter.value }, "Expected no requests to succeed") - - client.awaitCompletion() - server.awaitCompletion() + assertEquals(0, serverInstances.sumOf { it.successCounter.value }, "Expected no requests to succeed") checkAlive(clientAlive = false, serverAlive = false) stopAllAndJoin() + + assertEquals(2, serverInstance().cancellationsCounter.value, "Expected 2 requests to be cancelled") } @Test fun testCancelServer() = runCancellationTest { val firstRequestJob = launch { - service.serverDelay(300) + service.longRequest() } val secondService = client.withService() val secondRequestJob = launch { - secondService.serverDelay(300) + secondService.longRequest() } - unskippableDelay(150) // wait for requests to reach server + serverInstance().waitCounter.await(2) // wait for requests to reach server server.close() + server.awaitCompletion() + client.awaitCompletion() firstRequestJob.join() secondRequestJob.join() + serverInstance().cancellationsCounter.await(2) assertTrue(firstRequestJob.isCancelled, "Expected firstRequestJob to be cancelled") assertTrue(secondRequestJob.isCancelled, "Expected secondRequestJob to be cancelled") - assertEquals(0, serverInstances.sumOf { it.delayCounter.value }, "Expected no requests to succeed") - - client.awaitCompletion() - server.awaitCompletion() + assertEquals(0, serverInstances.sumOf { it.successCounter.value }, "Expected no requests to succeed") checkAlive(clientAlive = false, serverAlive = false) stopAllAndJoin() + + assertEquals(2, serverInstance().cancellationsCounter.value, "Expected 2 requests to be cancelled") } @Test @@ -191,6 +282,8 @@ class CancellationTest { // close by request cancel and not scope closure serverInstance().consumedAll.await() + serverInstance().cancellationsCounter.await(1) + assertContentEquals(listOf(0), serverInstance().consumedIncomingValues) stopAllAndJoin() @@ -215,6 +308,8 @@ class CancellationTest { // close by request cancel and not scope closure serverInstance().consumedAll.await() + serverInstance().cancellationsCounter.await(1) + val result = flow.toList() assertContentEquals(listOf(0), serverInstance().consumedIncomingValues) @@ -277,6 +372,8 @@ class CancellationTest { } stopAllAndJoin() + + assertEquals(1, serverInstance().cancellationsCounter.value, "Expected 1 request to be cancelled") } @Test diff --git a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationToolkit.kt b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationToolkit.kt index 3dd848f09..2002ef9dc 100644 --- a/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationToolkit.kt +++ b/krpc/krpc-test/src/commonTest/kotlin/kotlinx/rpc/krpc/test/cancellation/CancellationToolkit.kt @@ -6,27 +6,28 @@ package kotlinx.rpc.krpc.test.cancellation import kotlinx.coroutines.* import kotlinx.coroutines.test.TestResult -import kotlinx.coroutines.test.runTest import kotlinx.rpc.krpc.KrpcConfigBuilder import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger -import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLogger import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLoggerContainer +import kotlinx.rpc.krpc.internal.logging.dumpLogger import kotlinx.rpc.krpc.rpcClientConfig import kotlinx.rpc.krpc.rpcServerConfig import kotlinx.rpc.krpc.serialization.json.json import kotlinx.rpc.krpc.test.KrpcTestClient import kotlinx.rpc.krpc.test.KrpcTestServer import kotlinx.rpc.krpc.test.LocalTransport -import kotlinx.rpc.krpc.test.debugCoroutines import kotlinx.rpc.registerService +import kotlinx.rpc.test.runTestWithCoroutinesProbes import kotlinx.rpc.withService import kotlin.time.Duration.Companion.seconds fun runCancellationTest(body: suspend CancellationToolkit.() -> Unit): TestResult { - return runTest(timeout = 15.seconds) { - debugCoroutines() - CancellationToolkit(this).apply { - body() + return runTestWithCoroutinesProbes(timeout = 15.seconds) { + val toolkit = CancellationToolkit(this) + try { + body(toolkit) + } finally { + toolkit.close() } } } @@ -35,16 +36,10 @@ class CancellationToolkit(scope: CoroutineScope) : CoroutineScope by scope { private val logger = RpcInternalCommonLogger.logger("CancellationTest") init { - RpcInternalDumpLoggerContainer.set(object : RpcInternalDumpLogger { - override val isEnabled: Boolean = true - - override fun dump(vararg tags: String, message: () -> String) { - logger.info { "${tags.joinToString(" ") { "[$it]" }} ${message()}" } - } - }) + RpcInternalDumpLoggerContainer.set(logger.dumpLogger()) } - private val serializationConfig: KrpcConfigBuilder.() -> Unit = { + private val configBuilder: KrpcConfigBuilder.() -> Unit = { serialization { json() } @@ -54,7 +49,7 @@ class CancellationToolkit(scope: CoroutineScope) : CoroutineScope by scope { val client by lazy { KrpcTestClient(rpcClientConfig { - serializationConfig() + configBuilder() }, transport.client) } @@ -64,7 +59,7 @@ class CancellationToolkit(scope: CoroutineScope) : CoroutineScope by scope { private val firstServerInstance = CompletableDeferred() suspend fun serverInstance(): CancellationServiceImpl = firstServerInstance.await() - val server = KrpcTestServer(rpcServerConfig { serializationConfig() }, transport.server).apply { + val server = KrpcTestServer(rpcServerConfig { configBuilder() }, transport.server).apply { registerService { CancellationServiceImpl().also { impl -> if (!firstServerInstance.isCompleted) { @@ -75,12 +70,13 @@ class CancellationToolkit(scope: CoroutineScope) : CoroutineScope by scope { } } } -} -/** - * [runTest] can skip delays, and sometimes it prevents from writing a test - * running delay on [Dispatchers.Default] fixes delay, for questions refer to [runTest] documentation. - */ -suspend fun unskippableDelay(millis: Long) { - withContext(Dispatchers.Default) { delay(millis) } + suspend fun close() { + RpcInternalDumpLoggerContainer.set(null) + client.close() + server.close() + client.awaitCompletion() + server.awaitCompletion() + transport.coroutineContext.job.cancelAndJoin() + } } diff --git a/krpc/krpc-test/src/jsMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.js.kt b/krpc/krpc-test/src/jsMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.js.kt deleted file mode 100644 index e1cc13031..000000000 --- a/krpc/krpc-test/src/jsMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.js.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.krpc.test - -import kotlinx.coroutines.test.TestScope - -actual inline fun runThreadIfPossible(runner: () -> Unit) { - runner() -} - -@Suppress("detekt.EmptyFunctionBlock") -internal actual fun TestScope.debugCoroutines() { -} diff --git a/krpc/krpc-test/src/jsTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.js.kt b/krpc/krpc-test/src/jsTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.js.kt new file mode 100644 index 000000000..483c79d1e --- /dev/null +++ b/krpc/krpc-test/src/jsTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.js.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test + +internal actual val testIterations: Int = 100 diff --git a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/BaseServiceTest.kt b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/BaseServiceTest.kt new file mode 100644 index 000000000..5bea7a1f9 --- /dev/null +++ b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/BaseServiceTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancelAndJoin +import kotlinx.rpc.krpc.rpcClientConfig +import kotlinx.rpc.krpc.rpcServerConfig +import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.registerService +import kotlinx.rpc.withService +import kotlin.coroutines.CoroutineContext + +abstract class BaseServiceTest { + class Env( + val service: TestService, + val impl: TestServiceImpl, + val transport: LocalTransport, + testScope: CoroutineScope, + ) : CoroutineScope by testScope + + protected suspend fun CoroutineScope.runServiceTest( + parentContext: CoroutineContext, + perCallBufferSize: Int = 100, + body: suspend Env.() -> Unit, + ) { + val transport = LocalTransport(parentContext, recordTimestamps = false) + + val clientConfig = rpcClientConfig { + serialization { + json() + } + + connector { + this.perCallBufferSize = perCallBufferSize + } + } + + val serverConfig = rpcServerConfig { + serialization { + json() + } + + connector { + this.perCallBufferSize = perCallBufferSize + } + } + + val client = KrpcTestClient(clientConfig, transport.client) + val service = client.withService() + + val server = KrpcTestServer(serverConfig, transport.server) + val impl = TestServiceImpl() + server.registerService { impl } + + val env = Env(service, impl, transport, this) + try { + body(env) + } finally { + client.close() + server.close() + client.awaitCompletion() + server.awaitCompletion() + transport.coroutineContext.cancelAndJoin() + } + } +} diff --git a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/TestLogAppender.kt b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/TestLogAppender.kt new file mode 100644 index 000000000..077399cf7 --- /dev/null +++ b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/TestLogAppender.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase + +class TestLogAppender : AppenderBase() { + init { + start() + } + + val events = mutableListOf() + val errors get() = events.filter { it.level == Level.ERROR } + val warnings get() = events.filter { it.level == Level.WARN } + + override fun append(eventObject: ILoggingEvent) { + events.add(eventObject) + } +} diff --git a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/TestService.kt b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/TestService.kt new file mode 100644 index 000000000..d9d3de809 --- /dev/null +++ b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/TestService.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test + +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.toList +import kotlinx.rpc.annotations.Rpc + +@Rpc +interface TestService { + suspend fun unary(n: Int): Int + fun serverStreaming(num: Int): Flow + suspend fun clientStreaming(n: Flow): Int + fun bidiStreaming(flow: Flow): Flow +} + +class TestServiceImpl : TestService { + val unaryInvocations = atomic(0) + val serverStreamingInvocations = atomic(0) + val clientStreamingInvocations = atomic(0) + val bidiStreamingInvocations = atomic(0) + + override suspend fun unary(n: Int): Int { + unaryInvocations.incrementAndGet() + return n + } + + override fun serverStreaming(num: Int): Flow { + serverStreamingInvocations.incrementAndGet() + return (1..num).asFlow() + } + + override suspend fun clientStreaming(n: Flow): Int { + clientStreamingInvocations.incrementAndGet() + return n.toList().sum() + } + + override fun bidiStreaming(flow: Flow): Flow { + bidiStreamingInvocations.incrementAndGet() + return flow + } +} diff --git a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.jvm.kt b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.jvm.kt new file mode 100644 index 000000000..ed9cd5e35 --- /dev/null +++ b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.jvm.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test + +internal actual val testIterations: Int = 1000 diff --git a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/ApiTestContext.kt b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/ApiTestContext.kt index 2a770ce88..8371f8bdc 100644 --- a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/ApiTestContext.kt +++ b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/ApiTestContext.kt @@ -1,11 +1,10 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.krpc.test.api -import kotlinx.rpc.krpc.test.api.ApiVersioningTest.Companion.CURRENT_CLASS_DUMPS_DIR -import kotlinx.rpc.krpc.test.api.ApiVersioningTest.Companion.LATEST_CLASS_DUMPS_DIR +import kotlinx.rpc.krpc.test.api.ApiVersioningTest.Companion.CLASS_DUMPS_DIR import kotlinx.rpc.krpc.test.api.util.StringGoldContent import kotlinx.rpc.krpc.test.api.util.checkGold import kotlin.reflect.KClass @@ -27,8 +26,8 @@ class ApiTestContext { sampled.add(clazz) val log = checkGold( - latestDir = LATEST_CLASS_DUMPS_DIR, - currentDir = CURRENT_CLASS_DUMPS_DIR, + latestDir = CLASS_DUMPS_DIR, + currentDir = CLASS_DUMPS_DIR, filename = clazz.simpleName!!, content = StringGoldContent(currentContent), parseGoldFile = { StringGoldContent(it) }, diff --git a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/ApiVersioningTest.kt b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/ApiVersioningTest.kt index c99def244..a8559793e 100644 --- a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/ApiVersioningTest.kt +++ b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/ApiVersioningTest.kt @@ -1,25 +1,17 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.krpc.test.api -import kotlinx.coroutines.flow.toList import kotlinx.rpc.krpc.internal.CancellationType import kotlinx.rpc.krpc.internal.KrpcMessage import kotlinx.rpc.krpc.internal.KrpcPlugin import kotlinx.rpc.krpc.internal.KrpcPluginKey import kotlinx.rpc.krpc.test.api.util.GoldUtils.NewLine -import kotlinx.rpc.krpc.test.plainFlow -import org.jetbrains.krpc.test.api.util.SamplingData import org.junit.Test import java.nio.file.Path import kotlin.io.path.Path -import kotlin.io.path.isDirectory -import kotlin.io.path.listDirectoryEntries -import kotlin.io.path.name -import kotlin.test.Ignore -import kotlin.test.assertEquals import kotlin.test.fail class ApiVersioningTest { @@ -45,82 +37,10 @@ class ApiVersioningTest { testEnum() } - @Test - fun testEchoSampling() = wireSamplingTest("echo") { - sample { - val response = echo("Hello", SamplingData("data")) - assertEquals(SamplingData("data"), response) - } - } - - @Test - @Ignore("Flow sampling tests are too unstable. Ignored until better fix") - fun testClientStreamSampling() = wireSamplingTest("clientStream") { - sample { - val response = clientStream(plainFlow { it }).joinToString() - val expected = List(5) { it }.joinToString() - - assertEquals(expected, response) - } - } - - @Test - @Ignore("Flow sampling tests are too unstable. Ignored until better fix") - fun testServerStreamSampling() = wireSamplingTest("serverStream") { - sample { - val response = serverFlow().toList().joinToString() - val expected = List(5) { SamplingData("data") }.joinToString() - - assertEquals(expected, response) - } - } - - @Test - fun testCallExceptionSampling() = wireSamplingTest("callException") { - // ignore protobuf here, as it's hard to properly sample stacktrace - // in Json we can just cut it out - sample(SamplingFormat.Json) { - try { - callException() - fail("Expected exception to be thrown") - } catch (e: IllegalStateException) { - assertEquals("Server exception", e.message) - } - } - } - companion object { - val LIBRARY_VERSION_DIR = System.getenv("LIBRARY_VERSION")?.versionToDirName() - ?: error("Expected LIBRARY_VERSION env variable") + val CLASS_DUMPS_DIR: Path = Path("src/jvmTest/resources/class_dumps/") - val CURRENT_CLASS_DUMPS_DIR: Path = Path("src/jvmTest/resources/class_dumps/") - .resolve(LIBRARY_VERSION_DIR) - - val LATEST_CLASS_DUMPS_DIR: Path = Path("src/jvmTest/resources/class_dumps/") - .latestVersionOrCurrent() - - val WIRE_DUMPS_DIR: Path = Path("src/jvmTest/resources/wire_dumps/") val INDEXED_ENUM_DUMPS_DIR: Path = Path("src/jvmTest/resources/indexed_enum_dumps/") - - private fun String.versionToDirName(): String { - return replace('.', '_').replace('-', '_').substringBefore("-") - } - - fun Path.latestVersionOrCurrent(): Path { - return listDirectoryEntries() - .filter { it.isDirectory() } - .sortedWith { a, b -> - val aBeta = a.name.contains("beta") - val bBeta = b.name.contains("beta") - when { - aBeta && bBeta -> a.compareTo(b) - aBeta -> -1 - bBeta -> 1 - else -> a.name.substringBefore("-").compareTo(b.name.substringBefore("-")) - } - }.lastOrNull() - ?: resolve(LIBRARY_VERSION_DIR) - } } } diff --git a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/WireSamplingTestScope.kt b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/WireSamplingTestScope.kt deleted file mode 100644 index 8e0210f83..000000000 --- a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/api/WireSamplingTestScope.kt +++ /dev/null @@ -1,422 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.krpc.test.api - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.job -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.TestResult -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import kotlinx.rpc.annotations.Rpc -import kotlinx.rpc.internal.utils.hex.rpcInternalHexToReadableBinary -import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap -import kotlinx.rpc.krpc.KrpcTransportMessage -import kotlinx.rpc.krpc.client.KrpcClient -import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger -import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLogger -import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLoggerContainer -import kotlinx.rpc.krpc.rpcClientConfig -import kotlinx.rpc.krpc.rpcServerConfig -import kotlinx.rpc.krpc.serialization.KrpcSerialFormatConfiguration -import kotlinx.rpc.krpc.serialization.json.json -import kotlinx.rpc.krpc.serialization.protobuf.protobuf -import kotlinx.rpc.krpc.test.KrpcTestClient -import kotlinx.rpc.krpc.test.KrpcTestServer -import kotlinx.rpc.krpc.test.LocalTransport -import kotlinx.rpc.krpc.test.api.ApiVersioningTest.Companion.latestVersionOrCurrent -import kotlinx.rpc.krpc.test.api.util.GoldComparable -import kotlinx.rpc.krpc.test.api.util.GoldComparisonResult -import kotlinx.rpc.krpc.test.api.util.GoldUtils -import kotlinx.rpc.krpc.test.api.util.checkGold -import kotlinx.rpc.krpc.test.debugCoroutines -import kotlinx.rpc.registerService -import kotlinx.rpc.withService -import kotlinx.serialization.ExperimentalSerializationApi -import org.jetbrains.krpc.test.api.util.SamplingService -import org.jetbrains.krpc.test.api.util.SamplingServiceImpl -import java.nio.file.Files -import java.nio.file.Path -import kotlin.io.path.name -import kotlin.reflect.full.callSuspend -import kotlin.reflect.full.memberFunctions -import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.isAccessible -import kotlin.time.Clock -import kotlin.time.Duration.Companion.seconds -import kotlin.time.ExperimentalTime - -@Suppress("RedundantUnitReturnType") -fun wireSamplingTest(name: String, sampling: suspend WireSamplingTestScope.() -> Unit): TestResult { - return runTest(timeout = 15.seconds) { - debugCoroutines() - - WireSamplingTestScope(name, this).apply { - sampling() - - runSimulatorTests() - } - } -} - -class WireSamplingTestScope(private val sampleName: String, scope: TestScope) : CoroutineScope by scope { - private val logger = RpcInternalCommonLogger.logger("[WireTest] [$sampleName]") - private var clientSampling: (suspend SamplingService.() -> Unit)? = null - - suspend fun sample( - vararg formats: SamplingFormat = SamplingFormat.ALL, - block: suspend SamplingService.() -> Unit, - ) { - if (clientSampling != null) { - error("Please, add only one sampling per test") - } - clientSampling = block - - val fails = mutableListOf() - formats.forEach { format -> - val finishedToolkit = WireToolkit(this, format).apply { - initClient() - - server // init server - - service.block() - - @Suppress("UNCHECKED_CAST") - val requests = KrpcClient::class.memberProperties - .single { it.name == "requestChannels" } - .apply { - isAccessible = true - } - .get(client) as RpcInternalConcurrentHashMap> - - while (requests.values.isNotEmpty()) { - delay(100) - } - - stop() - } - - val log = checkGold( - latestDir = LATEST_WIRE_DUMPS_DIR, - currentDir = CURRENT_WIRE_DUMPS_DIR, - filename = "${sampleName}_${format.name.lowercase()}", - content = WireContent(finishedToolkit.logs, format.commentBinaryOutput), - parseGoldFile = { WireContent.fromText(it) }, - ) - - if (log != null) { - fails.add("Connection sample updated for '$sampleName' sample with $format format. $log") - } - } - - fails.failIfAnyCauses() - } - - private suspend fun WireToolkit.initClient() { - KrpcClient::class.memberFunctions.single { it.name == "initializeTransport" }.apply { - isAccessible = true - callSuspend(client) - } - } - - var skipOldServerTests: Boolean = true - - suspend fun runSimulatorTests() { - ApiVersioningTest.WIRE_DUMPS_DIR.listFiles().forEach { dir -> - runTestForVersionDirectory(dir) - } - } - - private suspend fun runTestForVersionDirectory(dir: Path) { - dir.listFiles().filter { - it.name.run { - startsWith(sampleName) && endsWith(GoldUtils.GOLD_EXTENSION) - } - }.forEach { path -> - val file = path.toFile() - val formatName = file.name - .removePrefix("${sampleName}_") - .removeSuffix(".${GoldUtils.GOLD_EXTENSION}") - - val format = SamplingFormat.valueOf(formatName.replaceFirstChar { it.uppercaseChar() }) - runTestOnSample(dir.name, format, DumpLog.fromText(file.readLines(Charsets.UTF_8))) - } - } - - private suspend fun runTestOnSample(version: String, format: SamplingFormat, dump: List) { - runOldClientTest(version, format, dump) - - if (!skipOldServerTests || version == ApiVersioningTest.LIBRARY_VERSION_DIR) { - runOldServerTest(version, format, dump) - } - } - - private suspend fun runOldClientTest(version: String, format: SamplingFormat, dump: List) { - val oldClientToolkit = WireToolkit(this, format, logger) - logger.info { "Running wire test: old client (version: $version) with current server on $format format" } - - oldClientToolkit.initClient() - oldClientToolkit.server // init server - for ((role, _, message) in dump.filter { it.phase == Phase.Send }) { - when (role) { - Role.Client -> { - oldClientToolkit.transport.client.send(message.toTransportMessage(format)) - } - - Role.Server -> { - // wait and discard message as we can not make safe assessments on it's contents - oldClientToolkit.transport.client.receive() - } - } - } - - // if logs are over, and we've reached this line, test is considered successful - oldClientToolkit.stop() - } - - private suspend fun runOldServerTest(version: String, format: SamplingFormat, dump: List) { - val oldServerToolkit = WireToolkit(this, format, logger) - logger.info { "Running wire test: old server (version: $version) with current client on $format format" } - - val clientJob = oldServerToolkit.transport.launch { - val runClient = clientSampling - ?: error("Client sampling is absent for $sampleName test") - - oldServerToolkit.service.runClient() - } - - for ((role, _, message) in dump.filter { it.phase == Phase.Send }) { - when (role) { - Role.Client -> { - // wait and discard message as we can not make safe assessments on it's contents - oldServerToolkit.transport.server.receive() - } - - Role.Server -> { - oldServerToolkit.transport.server.send(message.toTransportMessage(format)) - } - } - } - clientJob.join() - // if logs are over, and we've reached this line, test is considered successful - oldServerToolkit.stop() - } - - @OptIn(ExperimentalStdlibApi::class) - private fun String.toTransportMessage(format: SamplingFormat): KrpcTransportMessage { - return when (format) { - SamplingFormat.Json -> KrpcTransportMessage.StringMessage(this) - SamplingFormat.Protobuf -> KrpcTransportMessage.BinaryMessage(hexToByteArray()) - } - } - - private fun Path.listFiles(): List { - return Files.newDirectoryStream(this).use { it.toList() } - } - - companion object { - private val CURRENT_WIRE_DUMPS_DIR = ApiVersioningTest.WIRE_DUMPS_DIR - .resolve(ApiVersioningTest.LIBRARY_VERSION_DIR) - - private val LATEST_WIRE_DUMPS_DIR = ApiVersioningTest.WIRE_DUMPS_DIR - .latestVersionOrCurrent() - } -} - -private class WireToolkit(scope: CoroutineScope, format: SamplingFormat, val logger: RpcInternalCommonLogger? = null) { - val transport = LocalTransport(scope.coroutineContext.job) - - val client by lazy { - KrpcTestClient(rpcClientConfig { - serialization { - format.init(this) - } - }, transport.client) - } - - val service: SamplingService by lazy { - client.withService().withConsistentServiceId() - } - - val server by lazy { - KrpcTestServer(rpcServerConfig { serialization { format.init(this) } }, transport.server).apply { - registerService { SamplingServiceImpl() } - } - } - - val logs = mutableListOf() - - val dumpLogger = object : RpcInternalDumpLogger { - override val isEnabled: Boolean = true - - override fun dump(vararg tags: String, message: () -> String) { - if (logger != null) { - val log = when (format) { - SamplingFormat.Json -> message() - SamplingFormat.Protobuf -> message().rpcInternalHexToReadableBinary() - } - - logger.info { "DumpLog: ${tags.joinToString(" ") { "[$it]" }} $log" } - } else { - logs.add(DumpLog(Role.fromText(tags[0]), Phase.fromText(tags[1]), message())) - } - } - } - - suspend fun stop() { - @OptIn(ExperimentalTime::class) - while (true) { - val now = Clock.System.now().toEpochMilliseconds() - if (now - transport.lastMessageSentOnClient.value > 400 && - now - transport.lastMessageSentOnServer.value > 400 - ) { - break - } - - delay(100) - } - - RpcInternalDumpLoggerContainer.set(null) - - client.close() - server.close() - client.awaitCompletion() - server.awaitCompletion() - transport.coroutineContext.job.cancelAndJoin() - } - - init { - RpcInternalDumpLoggerContainer.set(dumpLogger) - } - - private inline fun <@Rpc reified Service : Any> Service.withConsistentServiceId(): Service = apply { - val clazz = this::class.java - val prop = clazz - .declaredFields - .single { it.name == "__rpc_stub_id" } - .apply { isAccessible = true } - - prop.set(this, 1L) - } -} - -@OptIn(ExperimentalSerializationApi::class) -enum class SamplingFormat(val commentBinaryOutput: Boolean, val init: KrpcSerialFormatConfiguration.() -> Unit) { - Json(false, { - json() - }), - - Protobuf(true, { - protobuf() - }), - ; - - companion object { - val ALL: Array = SamplingFormat.entries.toTypedArray() - } -} - -data class DumpLog( - val role: Role, - val phase: Phase, - val log: String, -) { - companion object { - fun fromText(lines: List): List { - return lines - .map { it.trim() } - .filter { !it.startsWith("//") && it.isNotBlank() } - .map { line -> - val (prefix, log) = line.split("$", limit = 2).map { it.trim() } - val (role, phase) = prefix.split(" ") - - DumpLog(Role.fromText(role), Phase.fromText(phase), log) - } - } - } -} - -enum class Role { - Server, Client; - - companion object { - fun fromText(text: String): Role { - return text.trimStart('[').trimEnd(']').let { - if (it == "Server") Server else Client - } - } - } -} - -enum class Phase { - Send, Receive; - - companion object { - fun fromText(text: String): Phase { - return text.trimStart('[').trimEnd(']').let { - if (it == "Send") Send else Receive - } - } - } -} - -private class WireContent( - rawLogs: List, - private val commentBinaryOutput: Boolean, -) : GoldComparable { - // too vague to trace, sensitive to code changes that do not affect actual structure - @Suppress("RegExpRedundantEscape") - private val stackTraceRegExp = Regex("\"stacktrace\":\\[.*?\\]") - - private fun String.removeExceptionStackTraceData(): String { - return replace(stackTraceRegExp, "\"stacktrace\":[]") - } - - private val logs = rawLogs.map { - it.copy(log = it.log.removeExceptionStackTraceData()) - } - - private val transformed = logs.filter { - it.phase == Phase.Send // receive phases are not needed to replay a connection - } - - override fun compare(other: WireContent): GoldComparisonResult { - return if (transformed == other.transformed) { - GoldComparisonResult.Ok - } else { - GoldComparisonResult.Failure( - "Wire comparison failed:\n" + - "Gold:\n ${other.dump()}\n\n" + - "Actual:\n ${dump()}" - ) - } - } - - override fun dump(): String { - return logs.joinToString(GoldUtils.NewLine) { dump -> - val base = "[${dump.role}] [${dump.phase}] $ ${dump.log}" - - if (commentBinaryOutput) { - val decodedBytes = dump.log.rpcInternalHexToReadableBinary() - - "// decoded: $decodedBytes" + GoldUtils.NewLine + base - } else { - base - } - } - } - - companion object { - fun fromText(text: String): WireContent { - return WireContent( - rawLogs = DumpLog.fromText(text.split('\r', '\n')), - commentBinaryOutput = false, - ) - } - } -} diff --git a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/lincheck/LincheckTest.kt b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/lincheck/LincheckTest.kt new file mode 100644 index 000000000..f0724022b --- /dev/null +++ b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/lincheck/LincheckTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.lincheck + +import ch.qos.logback.classic.Logger +import kotlinx.coroutines.ExecutorCoroutineDispatcher +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.rpc.krpc.test.BaseServiceTest +import kotlinx.rpc.krpc.test.TestLogAppender +import org.jetbrains.lincheck.Lincheck +import org.jetbrains.lincheck.datastructures.CTestConfiguration +import org.slf4j.LoggerFactory +import java.util.concurrent.Executors +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail + +class LincheckTest : BaseLincheckTest() { + @Test + @Ignore("Has some lincheck issues, waiting for a fix from the team") + fun simpleConcurrentRequests() = runTest { testLog -> + launch { + assertEquals(1, service.unary(1)) + } + + launch { + assertEquals(2, service.unary(2)) + } + + assertTrue(testLog.warnings.isEmpty()) + assertTrue(testLog.errors.isEmpty()) + } +} + +abstract class BaseLincheckTest : BaseServiceTest() { + fun createDispatcher(nThreads: Int): ExecutorCoroutineDispatcher = Executors + .newFixedThreadPool(nThreads) + .asCoroutineDispatcher() + + protected fun runTest( + shouldFail: Boolean = false, + invocations: Int = CTestConfiguration.DEFAULT_INVOCATIONS, + nThreads: Int = Runtime.getRuntime().availableProcessors(), + perCallBufferSize: Int = 100, + block: suspend Env.(TestLogAppender) -> Unit, + ) { + val root = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger + val testAppender = root.getAppender("TEST") as TestLogAppender + testAppender.events.clear() + + val result = runCatching { + Lincheck.runConcurrentTest(invocations) { + createDispatcher(nThreads).use { dispatcher -> + runBlocking(dispatcher) { + runServiceTest(coroutineContext, perCallBufferSize) { + block(testAppender) + } + } + } + } + } + + testAppender.events.clear() + + if (result.isFailure != shouldFail) { + val exceptionOrNull = result.exceptionOrNull() + val message = if (shouldFail) { + "Should've failed but succeeded" + } else { + "Should've succeeded but failed" + } + + fail(message, exceptionOrNull) + } + } +} diff --git a/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/stress/StressTest.kt b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/stress/StressTest.kt new file mode 100644 index 000000000..a92d9abda --- /dev/null +++ b/krpc/krpc-test/src/jvmTest/kotlin/kotlinx/rpc/krpc/test/stress/StressTest.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.stress + +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLoggerContainer +import kotlinx.rpc.krpc.test.BaseServiceTest +import kotlinx.rpc.test.runTestWithCoroutinesProbes +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +class StressTest : BaseStressTest() { + // ~30 sec, 300_000 messages + @Test + fun `unary, buffer 100, launches 3_000 x 100`() = testUnary(100, 300.seconds, 3_000, 100) + + // ~10 sec, 100_000 messages + @Test + fun `unary, buffer 1, launches 10_000 x 10`() = testUnary(1, 300.seconds, 10_000, 10) + + // ~51 min, 500_000 messages + @Test + fun `unary, 1000 buffer, launches 50_000 x 10`() = testUnary(1000, 400.seconds, 50_000, 10) + + + // ~15 sec, 4_000_000 messages + @Test + fun `server streaming, buffer 30 buf, launches 200 x 10`() = testServerStreaming(30, 300.seconds, 200, 10) + + // ~19 sec, 4_000_000 messages + @Test + fun `server streaming, buffer 1, launches 200 x 10`() = testServerStreaming(1, 300.seconds, 200, 10) + + // ~14 sec, 4_000_000 messages + @Test + fun `server streaming, buffer 2000, launches 200 x 10`() = testServerStreaming(2000, 400.seconds, 200, 10) + + + // ~15 sec, 4_000_000 messages + @Test + fun `client streaming, buffer 30, launches 200 x 10`() = testClientStreaming(30, 300.seconds, 200, 10) + + // ~19 sec, 4_000_000 messages + @Test + fun `client streaming, buffer 1, launches 200 x 10`() = testClientStreaming(1, 300.seconds, 200, 10) + + // ~15 sec, 4_000_000 messages + @Test + fun `client streaming, buffer 2000, launches 200 x 10`() = testClientStreaming(2000, 400.seconds, 200, 10) + + + // ~23 sec, 4_500_000 messages + @Test + fun `bidi streaming, buffer 30, launches 150 x 10`() = testBidiStreaming(30, 300.seconds, 150, 10) + + // ~24 sec, 4_500_000 messages + @Test + fun `bidi streaming, buffer 1, launches 150 x 10`() = testBidiStreaming(1, 300.seconds, 150, 10) + + // ~20 sec, 4_500_000 messages + @Test + fun `bidi streaming, buffer 2000, launches 150 x 10`() = testBidiStreaming(2000, 300.seconds, 150, 10) +} + + +abstract class BaseStressTest : BaseServiceTest() { + // (launches * iterationsPerLaunch) ^ 2 * 2 messages + protected fun testBidiStreaming( + perCallBufferSize: Int, + timeout: Duration, + launches: Int, + iterationsPerLaunch: Int, + ) = runTest(perCallBufferSize, timeout) { counter -> + List(launches) { id -> + val i = id + 1 + launch { + repeat(iterationsPerLaunch) { iter -> + val j = iter + 1 + assertEquals( + expected = (1 + i * j) * (i * j) / 2, + actual = service.bidiStreaming((1..i * j).asFlow()).toList().sum(), + ) + counter.total.incrementAndGet() + } + counter.launches.incrementAndGet() + } + }.joinAll() + + assertEquals(launches * iterationsPerLaunch, impl.bidiStreamingInvocations.value) + } + + // (launches * iterationsPerLaunch) ^ 2 messages + protected fun testClientStreaming( + perCallBufferSize: Int, + timeout: Duration, + launches: Int, + iterationsPerLaunch: Int, + ) = runTest(perCallBufferSize, timeout) { counter -> + List(launches) { id -> + val i = id + 1 + launch { + repeat(iterationsPerLaunch) { iter -> + val j = iter + 1 + assertEquals( + expected = (1 + i * j) * (i * j) / 2, + actual = service.clientStreaming((1..i * j).asFlow()), + ) + counter.total.incrementAndGet() + } + counter.launches.incrementAndGet() + } + }.joinAll() + + assertEquals(launches * iterationsPerLaunch, impl.clientStreamingInvocations.value) + } + + // (launches * iterationsPerLaunch) ^ 2 messages + protected fun testServerStreaming( + perCallBufferSize: Int, + timeout: Duration, + launches: Int, + iterationsPerLaunch: Int, + ) = runTest(perCallBufferSize, timeout) { counter -> + List(launches) { id -> + val i = id + 1 + launch { + repeat(iterationsPerLaunch) { iter -> + val j = iter + 1 + assertEquals( + expected = (1 + i * j) * (i * j) / 2, + actual = service.serverStreaming(i * j).toList().sum(), + message = "i=$i, j=$j", + ) + counter.total.incrementAndGet() + } + counter.launches.incrementAndGet() + } + }.joinAll() + + assertEquals(launches * iterationsPerLaunch, impl.serverStreamingInvocations.value) + } + + // (launches * iterationsPerLaunch) messages + protected fun testUnary( + perCallBufferSize: Int, + timeout: Duration, + launches: Int, + iterationsPerLaunch: Int, + ) = runTest(perCallBufferSize, timeout) { counter -> + List(launches) { id -> + launch { + repeat(iterationsPerLaunch) { iter -> + assertEquals(id * iter, service.unary(id * iter)) + counter.total.incrementAndGet() + } + counter.launches.incrementAndGet() + } + }.joinAll() + + assertEquals(launches * iterationsPerLaunch, impl.unaryInvocations.value) + } + + class Counter { + val launches = atomic(0) + val total = atomic(0) + } + + private fun runTest( + perCallBufferSize: Int = 100, + timeout: Duration = 300.seconds, + body: suspend Env.(Counter) -> Unit, + ) = runTestWithCoroutinesProbes(timeout = timeout) { + RpcInternalDumpLoggerContainer.set(null) + runServiceTest(coroutineContext, perCallBufferSize) { + val counter = Counter() + val counterJob = launch { + while (true) { + withContext(Dispatchers.Default) { + delay(5.seconds) + println("Launches: ${counter.launches.value}, total: ${counter.total.value}") + } + } + } + + body(this, counter) + + counterJob.cancelAndJoin() + } + } +} diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcPlugin.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcPlugin.gold deleted file mode 100644 index a3d03ab4a..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcPlugin.gold +++ /dev/null @@ -1,4 +0,0 @@ -kotlinx.rpc.krpc.internal.KrpcPlugin - UNKNOWN - HANDSHAKE - CANCELLATION \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcPluginKey.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcPluginKey.gold deleted file mode 100644 index 34f58e6fb..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcPluginKey.gold +++ /dev/null @@ -1,6 +0,0 @@ -kotlinx.rpc.krpc.internal.KrpcPluginKey - UNKNOWN - GENERIC_MESSAGE_TYPE - CANCELLATION_TYPE - CANCELLATION_ID - CLIENT_SERVICE_ID \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallData.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallData.gold deleted file mode 100644 index d4581352a..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallData.gold +++ /dev/null @@ -1,18 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.CallData [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallData] - - callType: org.jetbrains.krpc.internal.transport.RPCMessage.CallType - - Nullable - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallType - - callableName: kotlin.String - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - org.jetbrains.krpc.internal.transport.RPCMessage.CallDataBinary - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallDataBinary - org.jetbrains.krpc.RPCMessage.CallData - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallDataString \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallDataBinary.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallDataBinary.gold deleted file mode 100644 index b0edd5b46..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallDataBinary.gold +++ /dev/null @@ -1,15 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.CallDataBinary [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallDataBinary] - - callId: kotlin.String - - callType: org.jetbrains.krpc.internal.transport.RPCMessage.CallType - - Nullable - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallType - - method: kotlin.String - - Declared name: callableName - - connectionId: kotlin.Long - - Nullable - - data: kotlin.ByteArray - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallDataString.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallDataString.gold deleted file mode 100644 index b9ba5971b..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallDataString.gold +++ /dev/null @@ -1,15 +0,0 @@ -org.jetbrains.krpc.RPCMessage.CallData [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallDataString] - - callId: kotlin.String - - callType: org.jetbrains.krpc.internal.transport.RPCMessage.CallType - - Nullable - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallType - - method: kotlin.String - - Declared name: callableName - - connectionId: kotlin.Long - - Nullable - - data: kotlin.String - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallException.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallException.gold deleted file mode 100644 index 3b7dfc0e7..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallException.gold +++ /dev/null @@ -1,11 +0,0 @@ -org.jetbrains.krpc.RPCMessage.CallException [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallException] - - callId: kotlin.String - - cause: org.jetbrains.krpc.SerializedException - - Declared name: kotlinx.rpc.krpc.internal.SerializedException - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallResult.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallResult.gold deleted file mode 100644 index 2e7672b9e..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallResult.gold +++ /dev/null @@ -1,14 +0,0 @@ -org.jetbrains.krpc.RPCMessage.CallResult [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallResult] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - org.jetbrains.krpc.RPCMessage.CallException - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallException - org.jetbrains.krpc.internal.transport.RPCMessage.CallSuccess - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccess \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallSuccess.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallSuccess.gold deleted file mode 100644 index f34c74a72..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallSuccess.gold +++ /dev/null @@ -1,14 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.CallSuccess [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccess] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - org.jetbrains.krpc.internal.transport.RPCMessage.CallSuccessBinary - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccessBinary - org.jetbrains.krpc.RPCMessage.CallSuccess - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccessString \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallSuccessBinary.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallSuccessBinary.gold deleted file mode 100644 index e8cd58905..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallSuccessBinary.gold +++ /dev/null @@ -1,10 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.CallSuccessBinary [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccessBinary] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - data: kotlin.ByteArray - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallSuccessString.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallSuccessString.gold deleted file mode 100644 index 34c961851..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallSuccessString.gold +++ /dev/null @@ -1,10 +0,0 @@ -org.jetbrains.krpc.RPCMessage.CallSuccess [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccessString] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - data: kotlin.String - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallType.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallType.gold deleted file mode 100644 index 84b5971b3..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/CallType.gold +++ /dev/null @@ -1,3 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.CallType [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallType] - Method - Field \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/Failure.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/Failure.gold deleted file mode 100644 index f01da216f..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/Failure.gold +++ /dev/null @@ -1,8 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Failure [Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage.Failure] - - connectionId: kotlin.Long - - Nullable - - errorMessage: kotlin.String - - failedMessage: org.jetbrains.krpc.internal.transport.RPCMessage - - Nullable - - Declared name: kotlinx.rpc.krpc.internal.KrpcMessage - - pluginParams: kotlin.collections.Map \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/Handshake.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/Handshake.gold deleted file mode 100644 index 2f5e2be80..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/Handshake.gold +++ /dev/null @@ -1,5 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake [Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage.Handshake] - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - supportedPlugins: kotlin.collections.Set \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcCallMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcCallMessage.gold deleted file mode 100644 index 0273b93a3..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcCallMessage.gold +++ /dev/null @@ -1,20 +0,0 @@ -org.jetbrains.krpc.RPCMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage] - - callId: kotlin.String - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - org.jetbrains.krpc.internal.transport.RPCMessage.CallData - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallData - org.jetbrains.krpc.RPCMessage.CallResult - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallResult - org.jetbrains.krpc.RPCMessage.StreamCancel - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamCancel - org.jetbrains.krpc.RPCMessage.StreamFinished - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamFinished - org.jetbrains.krpc.internal.transport.RPCMessage.StreamMessage - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessage \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcGenericMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcGenericMessage.gold deleted file mode 100644 index b27a3f009..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcGenericMessage.gold +++ /dev/null @@ -1,5 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCGenericMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcGenericMessage] - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcMessage.gold deleted file mode 100644 index 6924a1c42..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcMessage.gold +++ /dev/null @@ -1,12 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcMessage] - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - org.jetbrains.krpc.RPCMessage - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage - org.jetbrains.krpc.internal.transport.RPCGenericMessage - - Declared name: kotlinx.rpc.krpc.internal.KrpcGenericMessage - org.jetbrains.krpc.internal.transport.RPCProtocolMessage - - Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcPlugin.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcPlugin.gold deleted file mode 100644 index d46db17a9..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcPlugin.gold +++ /dev/null @@ -1,5 +0,0 @@ -kotlinx.rpc.krpc.internal.KrpcPlugin - UNKNOWN - HANDSHAKE - CANCELLATION - NON_SUSPENDING_SERVER_FLOWS \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcPluginKey.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcPluginKey.gold deleted file mode 100644 index 137d218c5..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcPluginKey.gold +++ /dev/null @@ -1,7 +0,0 @@ -kotlinx.rpc.krpc.internal.KrpcPluginKey - UNKNOWN - GENERIC_MESSAGE_TYPE - CANCELLATION_TYPE - CANCELLATION_ID - CLIENT_SERVICE_ID - NON_SUSPENDING_SERVER_FLOW_MARKER \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcProtocolMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcProtocolMessage.gold deleted file mode 100644 index 7e80aac18..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/KrpcProtocolMessage.gold +++ /dev/null @@ -1,9 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCProtocolMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage] - - pluginParams: kotlin.collections.Map - - connectionId: kotlin.Long - - Nullable - - org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Failure - - Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage.Failure - org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake - - Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage.Handshake \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/SerializedException.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/SerializedException.gold deleted file mode 100644 index 427993cbb..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/SerializedException.gold +++ /dev/null @@ -1,8 +0,0 @@ -org.jetbrains.krpc.SerializedException [Declared name: kotlinx.rpc.krpc.internal.SerializedException] - - cause: org.jetbrains.krpc.SerializedException - - Nullable - - Declared name: kotlinx.rpc.krpc.internal.SerializedException - - className: kotlin.String - - message: kotlin.String - - stacktrace: kotlin.collections.List - - toStringMessage: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StackElement.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StackElement.gold deleted file mode 100644 index d67b95977..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StackElement.gold +++ /dev/null @@ -1,6 +0,0 @@ -org.jetbrains.krpc.StackElement [Declared name: kotlinx.rpc.krpc.internal.StackElement] - - clazz: kotlin.String - - fileName: kotlin.String - - Nullable - - lineNumber: kotlin.Int - - method: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamCancel.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamCancel.gold deleted file mode 100644 index 8e6bd5e5e..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamCancel.gold +++ /dev/null @@ -1,13 +0,0 @@ -org.jetbrains.krpc.RPCMessage.StreamCancel [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamCancel] - - callId: kotlin.String - - cause: org.jetbrains.krpc.SerializedException - - Declared name: kotlinx.rpc.krpc.internal.SerializedException - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - flowId: kotlin.String - - Declared name: streamId \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamFinished.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamFinished.gold deleted file mode 100644 index a260d7d75..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamFinished.gold +++ /dev/null @@ -1,11 +0,0 @@ -org.jetbrains.krpc.RPCMessage.StreamFinished [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamFinished] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - flowId: kotlin.String - - Declared name: streamId \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamMessage.gold deleted file mode 100644 index e07485913..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamMessage.gold +++ /dev/null @@ -1,15 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.StreamMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessage] - - streamId: kotlin.String - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - org.jetbrains.krpc.internal.transport.RPCMessage.StreamMessageBinary - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessageBinary - org.jetbrains.krpc.RPCMessage.StreamMessage - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessageString \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamMessageBinary.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamMessageBinary.gold deleted file mode 100644 index 6da67510f..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamMessageBinary.gold +++ /dev/null @@ -1,12 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.StreamMessageBinary [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessageBinary] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - data: kotlin.ByteArray - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - flowId: kotlin.String - - Declared name: streamId \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamMessageString.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamMessageString.gold deleted file mode 100644 index 3316fc14e..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_6_0/StreamMessageString.gold +++ /dev/null @@ -1,12 +0,0 @@ -org.jetbrains.krpc.RPCMessage.StreamMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessageString] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - data: kotlin.String - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - flowId: kotlin.String - - Declared name: streamId \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallData.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallData.gold deleted file mode 100644 index d4581352a..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallData.gold +++ /dev/null @@ -1,18 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.CallData [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallData] - - callType: org.jetbrains.krpc.internal.transport.RPCMessage.CallType - - Nullable - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallType - - callableName: kotlin.String - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - org.jetbrains.krpc.internal.transport.RPCMessage.CallDataBinary - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallDataBinary - org.jetbrains.krpc.RPCMessage.CallData - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallDataString \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallDataBinary.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallDataBinary.gold deleted file mode 100644 index b0edd5b46..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallDataBinary.gold +++ /dev/null @@ -1,15 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.CallDataBinary [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallDataBinary] - - callId: kotlin.String - - callType: org.jetbrains.krpc.internal.transport.RPCMessage.CallType - - Nullable - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallType - - method: kotlin.String - - Declared name: callableName - - connectionId: kotlin.Long - - Nullable - - data: kotlin.ByteArray - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallDataString.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallDataString.gold deleted file mode 100644 index b9ba5971b..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallDataString.gold +++ /dev/null @@ -1,15 +0,0 @@ -org.jetbrains.krpc.RPCMessage.CallData [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallDataString] - - callId: kotlin.String - - callType: org.jetbrains.krpc.internal.transport.RPCMessage.CallType - - Nullable - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallType - - method: kotlin.String - - Declared name: callableName - - connectionId: kotlin.Long - - Nullable - - data: kotlin.String - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallException.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallException.gold deleted file mode 100644 index 3b7dfc0e7..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallException.gold +++ /dev/null @@ -1,11 +0,0 @@ -org.jetbrains.krpc.RPCMessage.CallException [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallException] - - callId: kotlin.String - - cause: org.jetbrains.krpc.SerializedException - - Declared name: kotlinx.rpc.krpc.internal.SerializedException - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallResult.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallResult.gold deleted file mode 100644 index 2e7672b9e..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallResult.gold +++ /dev/null @@ -1,14 +0,0 @@ -org.jetbrains.krpc.RPCMessage.CallResult [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallResult] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - org.jetbrains.krpc.RPCMessage.CallException - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallException - org.jetbrains.krpc.internal.transport.RPCMessage.CallSuccess - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccess \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallSuccess.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallSuccess.gold deleted file mode 100644 index f34c74a72..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallSuccess.gold +++ /dev/null @@ -1,14 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.CallSuccess [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccess] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - org.jetbrains.krpc.internal.transport.RPCMessage.CallSuccessBinary - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccessBinary - org.jetbrains.krpc.RPCMessage.CallSuccess - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccessString \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallSuccessBinary.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallSuccessBinary.gold deleted file mode 100644 index e8cd58905..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallSuccessBinary.gold +++ /dev/null @@ -1,10 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.CallSuccessBinary [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccessBinary] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - data: kotlin.ByteArray - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallSuccessString.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallSuccessString.gold deleted file mode 100644 index 34c961851..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallSuccessString.gold +++ /dev/null @@ -1,10 +0,0 @@ -org.jetbrains.krpc.RPCMessage.CallSuccess [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallSuccessString] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - data: kotlin.String - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallType.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallType.gold deleted file mode 100644 index 84b5971b3..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/CallType.gold +++ /dev/null @@ -1,3 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.CallType [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallType] - Method - Field \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/Failure.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/Failure.gold deleted file mode 100644 index f01da216f..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/Failure.gold +++ /dev/null @@ -1,8 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Failure [Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage.Failure] - - connectionId: kotlin.Long - - Nullable - - errorMessage: kotlin.String - - failedMessage: org.jetbrains.krpc.internal.transport.RPCMessage - - Nullable - - Declared name: kotlinx.rpc.krpc.internal.KrpcMessage - - pluginParams: kotlin.collections.Map \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/Handshake.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/Handshake.gold deleted file mode 100644 index 2f5e2be80..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/Handshake.gold +++ /dev/null @@ -1,5 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake [Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage.Handshake] - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - supportedPlugins: kotlin.collections.Set \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcCallMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcCallMessage.gold deleted file mode 100644 index 0273b93a3..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcCallMessage.gold +++ /dev/null @@ -1,20 +0,0 @@ -org.jetbrains.krpc.RPCMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage] - - callId: kotlin.String - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - org.jetbrains.krpc.internal.transport.RPCMessage.CallData - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallData - org.jetbrains.krpc.RPCMessage.CallResult - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.CallResult - org.jetbrains.krpc.RPCMessage.StreamCancel - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamCancel - org.jetbrains.krpc.RPCMessage.StreamFinished - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamFinished - org.jetbrains.krpc.internal.transport.RPCMessage.StreamMessage - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessage \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcGenericMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcGenericMessage.gold deleted file mode 100644 index b27a3f009..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcGenericMessage.gold +++ /dev/null @@ -1,5 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCGenericMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcGenericMessage] - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcMessage.gold deleted file mode 100644 index 6924a1c42..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcMessage.gold +++ /dev/null @@ -1,12 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcMessage] - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - org.jetbrains.krpc.RPCMessage - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage - org.jetbrains.krpc.internal.transport.RPCGenericMessage - - Declared name: kotlinx.rpc.krpc.internal.KrpcGenericMessage - org.jetbrains.krpc.internal.transport.RPCProtocolMessage - - Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcProtocolMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcProtocolMessage.gold deleted file mode 100644 index 7e80aac18..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcProtocolMessage.gold +++ /dev/null @@ -1,9 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCProtocolMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage] - - pluginParams: kotlin.collections.Map - - connectionId: kotlin.Long - - Nullable - - org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Failure - - Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage.Failure - org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake - - Declared name: kotlinx.rpc.krpc.internal.KrpcProtocolMessage.Handshake \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/SerializedException.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/SerializedException.gold deleted file mode 100644 index 427993cbb..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/SerializedException.gold +++ /dev/null @@ -1,8 +0,0 @@ -org.jetbrains.krpc.SerializedException [Declared name: kotlinx.rpc.krpc.internal.SerializedException] - - cause: org.jetbrains.krpc.SerializedException - - Nullable - - Declared name: kotlinx.rpc.krpc.internal.SerializedException - - className: kotlin.String - - message: kotlin.String - - stacktrace: kotlin.collections.List - - toStringMessage: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StackElement.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StackElement.gold deleted file mode 100644 index d67b95977..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StackElement.gold +++ /dev/null @@ -1,6 +0,0 @@ -org.jetbrains.krpc.StackElement [Declared name: kotlinx.rpc.krpc.internal.StackElement] - - clazz: kotlin.String - - fileName: kotlin.String - - Nullable - - lineNumber: kotlin.Int - - method: kotlin.String \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamCancel.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamCancel.gold deleted file mode 100644 index 8e6bd5e5e..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamCancel.gold +++ /dev/null @@ -1,13 +0,0 @@ -org.jetbrains.krpc.RPCMessage.StreamCancel [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamCancel] - - callId: kotlin.String - - cause: org.jetbrains.krpc.SerializedException - - Declared name: kotlinx.rpc.krpc.internal.SerializedException - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - flowId: kotlin.String - - Declared name: streamId \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamFinished.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamFinished.gold deleted file mode 100644 index a260d7d75..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamFinished.gold +++ /dev/null @@ -1,11 +0,0 @@ -org.jetbrains.krpc.RPCMessage.StreamFinished [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamFinished] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - flowId: kotlin.String - - Declared name: streamId \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamMessage.gold deleted file mode 100644 index e07485913..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamMessage.gold +++ /dev/null @@ -1,15 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.StreamMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessage] - - streamId: kotlin.String - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - org.jetbrains.krpc.internal.transport.RPCMessage.StreamMessageBinary - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessageBinary - org.jetbrains.krpc.RPCMessage.StreamMessage - - Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessageString \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamMessageBinary.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamMessageBinary.gold deleted file mode 100644 index 6da67510f..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamMessageBinary.gold +++ /dev/null @@ -1,12 +0,0 @@ -org.jetbrains.krpc.internal.transport.RPCMessage.StreamMessageBinary [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessageBinary] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - data: kotlin.ByteArray - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - flowId: kotlin.String - - Declared name: streamId \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamMessageString.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamMessageString.gold deleted file mode 100644 index 3316fc14e..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/StreamMessageString.gold +++ /dev/null @@ -1,12 +0,0 @@ -org.jetbrains.krpc.RPCMessage.StreamMessage [Declared name: kotlinx.rpc.krpc.internal.KrpcCallMessage.StreamMessageString] - - callId: kotlin.String - - connectionId: kotlin.Long - - Nullable - - data: kotlin.String - - pluginParams: kotlin.collections.Map - - Nullable - - serviceId: kotlin.Long - - Nullable - - serviceType: kotlin.String - - flowId: kotlin.String - - Declared name: streamId \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallData.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/CallData.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallData.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/CallData.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallDataBinary.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/CallDataBinary.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallDataBinary.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/CallDataBinary.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallDataString.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/CallDataString.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallDataString.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/CallDataString.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallException.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/CallException.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallException.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/CallException.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallResult.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/CallResult.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallResult.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/CallResult.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallSuccess.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/CallSuccess.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallSuccess.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/CallSuccess.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallSuccessBinary.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/CallSuccessBinary.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallSuccessBinary.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/CallSuccessBinary.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallSuccessString.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/CallSuccessString.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallSuccessString.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/CallSuccessString.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallType.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/CallType.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/CallType.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/CallType.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/Failure.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/Failure.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/Failure.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/Failure.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/Handshake.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/Handshake.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/Handshake.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/Handshake.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcCallMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcCallMessage.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcCallMessage.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcCallMessage.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcGenericMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcGenericMessage.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcGenericMessage.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcGenericMessage.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcMessage.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcMessage.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcMessage.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcPlugin.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcPlugin.gold similarity index 74% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcPlugin.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcPlugin.gold index 0735462c8..63dd1498a 100644 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcPlugin.gold +++ b/krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcPlugin.gold @@ -3,4 +3,5 @@ kotlinx.rpc.krpc.internal.KrpcPlugin HANDSHAKE CANCELLATION NON_SUSPENDING_SERVER_FLOWS - NO_ACK_CANCELLATION \ No newline at end of file + NO_ACK_CANCELLATION + BACKPRESSURE \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcPluginKey.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcPluginKey.gold similarity index 67% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcPluginKey.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcPluginKey.gold index 137d218c5..f4be9b92d 100644 --- a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_8_0/KrpcPluginKey.gold +++ b/krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcPluginKey.gold @@ -4,4 +4,6 @@ kotlinx.rpc.krpc.internal.KrpcPluginKey CANCELLATION_TYPE CANCELLATION_ID CLIENT_SERVICE_ID - NON_SUSPENDING_SERVER_FLOW_MARKER \ No newline at end of file + NON_SUSPENDING_SERVER_FLOW_MARKER + WINDOW_UPDATE + WINDOW_KEY \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcProtocolMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcProtocolMessage.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/KrpcProtocolMessage.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/KrpcProtocolMessage.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/SerializedException.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/SerializedException.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/SerializedException.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/SerializedException.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StackElement.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/StackElement.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StackElement.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/StackElement.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StreamCancel.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/StreamCancel.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StreamCancel.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/StreamCancel.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StreamFinished.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/StreamFinished.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StreamFinished.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/StreamFinished.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StreamMessage.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/StreamMessage.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StreamMessage.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/StreamMessage.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StreamMessageBinary.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/StreamMessageBinary.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StreamMessageBinary.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/StreamMessageBinary.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StreamMessageString.gold b/krpc/krpc-test/src/jvmTest/resources/class_dumps/StreamMessageString.gold similarity index 100% rename from krpc/krpc-test/src/jvmTest/resources/class_dumps/0_5_0/StreamMessageString.gold rename to krpc/krpc-test/src/jvmTest/resources/class_dumps/StreamMessageString.gold diff --git a/krpc/krpc-test/src/jvmTest/resources/indexed_enum_dumps/KrpcPlugin.gold b/krpc/krpc-test/src/jvmTest/resources/indexed_enum_dumps/KrpcPlugin.gold index 1317fc12a..8c182300a 100644 --- a/krpc/krpc-test/src/jvmTest/resources/indexed_enum_dumps/KrpcPlugin.gold +++ b/krpc/krpc-test/src/jvmTest/resources/indexed_enum_dumps/KrpcPlugin.gold @@ -2,4 +2,5 @@ UNKNOWN - 0 HANDSHAKE - 1 CANCELLATION - 2 NON_SUSPENDING_SERVER_FLOWS - 3 -NO_ACK_CANCELLATION - 4 \ No newline at end of file +NO_ACK_CANCELLATION - 4 +BACKPRESSURE - 5 \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/indexed_enum_dumps/KrpcPluginKey.gold b/krpc/krpc-test/src/jvmTest/resources/indexed_enum_dumps/KrpcPluginKey.gold index edc818e2f..4c8e5b2a7 100644 --- a/krpc/krpc-test/src/jvmTest/resources/indexed_enum_dumps/KrpcPluginKey.gold +++ b/krpc/krpc-test/src/jvmTest/resources/indexed_enum_dumps/KrpcPluginKey.gold @@ -3,4 +3,6 @@ GENERIC_MESSAGE_TYPE - 1 CANCELLATION_TYPE - 2 CANCELLATION_ID - 3 CLIENT_SERVICE_ID - 4 -NON_SUSPENDING_SERVER_FLOW_MARKER - 5 \ No newline at end of file +NON_SUSPENDING_SERVER_FLOW_MARKER - 5 +WINDOW_UPDATE - 6 +WINDOW_KEY - 7 \ No newline at end of file diff --git a/krpc/krpc-test/src/jvmTest/resources/logback.xml b/krpc/krpc-test/src/jvmTest/resources/logback.xml index 4a4570334..1bc8cc3da 100644 --- a/krpc/krpc-test/src/jvmTest/resources/logback.xml +++ b/krpc/krpc-test/src/jvmTest/resources/logback.xml @@ -1,5 +1,5 @@ @@ -8,9 +8,13 @@ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - + + + + + - \ No newline at end of file + diff --git a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_4_0/callException_json.gold b/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_4_0/callException_json.gold deleted file mode 100644 index 286a96bf7..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_4_0/callException_json.gold +++ /dev/null @@ -1,9 +0,0 @@ -[Client] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766]} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766]} -[Server] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766],"connectionId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766],"connectionId":1} -[Client] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`callException$rpcMethod`:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"callException","callType":"Method","data":"{}","connectionId":1,"serviceId":1} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`callException$rpcMethod`:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"callException","callType":"Method","data":"{}","connectionId":1,"serviceId":1} -[Server] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallException","callId":"1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`callException$rpcMethod`:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","cause":{"toStringMessage":"java.lang.IllegalStateException: Server exception","message":"Server exception","stacktrace":[],"cause":{"toStringMessage":"java.lang.IllegalStateException: Server exception","message":"Server exception","stacktrace":[],"cause":null,"className":"java.lang.IllegalStateException"},"className":"java.lang.IllegalStateException"},"connectionId":1,"serviceId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallException","callId":"1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`callException$rpcMethod`:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","cause":{"toStringMessage":"java.lang.IllegalStateException: Server exception","message":"Server exception","stacktrace":[],"cause":{"toStringMessage":"java.lang.IllegalStateException: Server exception","message":"Server exception","stacktrace":[],"cause":null,"className":"java.lang.IllegalStateException"},"className":"java.lang.IllegalStateException"},"connectionId":1,"serviceId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCGenericMessage","connectionId":null,"pluginParams":{"-32767":"cancellation","-32766":"CANCELLATION_ACK","-32765":"1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`callException$rpcMethod`:1"}} diff --git a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_4_0/echo_json.gold b/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_4_0/echo_json.gold deleted file mode 100644 index 3c8637539..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_4_0/echo_json.gold +++ /dev/null @@ -1,9 +0,0 @@ -[Client] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766]} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766]} -[Server] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766],"connectionId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766],"connectionId":1} -[Client] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`echo$rpcMethod`:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"echo","callType":"Method","data":"{\"arg1\":\"Hello\",\"data\":{\"data\":\"data\"}}","connectionId":1,"serviceId":1} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`echo$rpcMethod`:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"echo","callType":"Method","data":"{\"arg1\":\"Hello\",\"data\":{\"data\":\"data\"}}","connectionId":1,"serviceId":1} -[Server] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallSuccess","callId":"1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`echo$rpcMethod`:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","data":"{\"data\":\"data\"}","connectionId":1,"serviceId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallSuccess","callId":"1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`echo$rpcMethod`:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","data":"{\"data\":\"data\"}","connectionId":1,"serviceId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCGenericMessage","connectionId":null,"pluginParams":{"-32767":"cancellation","-32766":"CANCELLATION_ACK","-32765":"1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`echo$rpcMethod`:1"}} diff --git a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_4_0/echo_protobuf.gold b/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_4_0/echo_protobuf.gold deleted file mode 100644 index 3d481f5b1..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_4_0/echo_protobuf.gold +++ /dev/null @@ -1,18 +0,0 @@ -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake???????????????????????? -[Client] [Send] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b651216088180feffffffffffff01088280feffffffffffff01 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake???????????????????????? -[Server] [Receive] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b651216088180feffffffffffff01088280feffffffffffff01 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake?????????????????????????? -[Server] [Send] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b651218088180feffffffffffff01088280feffffffffffff011001 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake?????????????????????????? -[Client] [Receive] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b651218088180feffffffffffff01088280feffffffffffff011001 -// decoded: ??org.jetbrains.krpc.internal.transport.RPCMessage.CallDataBinary????W1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`echo$rpcMethod`:1?0org.jetbrains.krpc.test.api.util.SamplingService??echo ?*???Hello????data0?8? -[Client] [Send] $ 0a3f6f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c4461746142696e61727912a8010a57313a6f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963652e60247270635365727669636553747562602e606563686f247270634d6574686f64603a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a046563686f20002a0f0a0548656c6c6f12060a046461746130013801 -// decoded: ??org.jetbrains.krpc.internal.transport.RPCMessage.CallDataBinary????W1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`echo$rpcMethod`:1?0org.jetbrains.krpc.test.api.util.SamplingService??echo ?*???Hello????data0?8? -[Server] [Receive] $ 0a3f6f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c4461746142696e61727912a8010a57313a6f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963652e60247270635365727669636553747562602e606563686f247270634d6574686f64603a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a046563686f20002a0f0a0548656c6c6f12060a046461746130013801 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCMessage.CallSuccessBinary????W1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`echo$rpcMethod`:1?0org.jetbrains.krpc.test.api.util.SamplingService????data ?(? -[Server] [Send] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c5375636365737342696e6172791297010a57313a6f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963652e60247270635365727669636553747562602e606563686f247270634d6574686f64603a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a060a046461746120012801 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCMessage.CallSuccessBinary????W1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`echo$rpcMethod`:1?0org.jetbrains.krpc.test.api.util.SamplingService????data ?(? -[Client] [Receive] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c5375636365737342696e6172791297010a57313a6f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963652e60247270635365727669636553747562602e606563686f247270634d6574686f64603a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a060a046461746120012801 -// decoded: ?7org.jetbrains.krpc.internal.transport.RPCGenericMessage??????????????????cancellation???????????????CANCELLATION_ACK?d????????????W1:org.jetbrains.krpc.test.api.util.SamplingService.`$rpcServiceStub`.`echo$rpcMethod`:1 -[Client] [Receive] $ 0a376f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504347656e657269634d65737361676512a0011219088180feffffffffffff01120c63616e63656c6c6174696f6e121d088280feffffffffffff01121043414e43454c4c4154494f4e5f41434b1264088380feffffffffffff011257313a6f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963652e60247270635365727669636553747562602e606563686f247270634d6574686f64603a31 diff --git a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_6_0/callException_json.gold b/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_6_0/callException_json.gold deleted file mode 100644 index 3d7fa87f4..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_6_0/callException_json.gold +++ /dev/null @@ -1,9 +0,0 @@ -[Client] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765]} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765]} -[Server] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765],"connectionId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765],"connectionId":1} -[Client] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:callException:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"callException","callType":"Method","data":"{}","connectionId":1,"serviceId":1} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:callException:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"callException","callType":"Method","data":"{}","connectionId":1,"serviceId":1} -[Server] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallException","callId":"1:callException:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","cause":{"toStringMessage":"java.lang.IllegalStateException: Server exception","message":"Server exception","stacktrace":[],"cause":{"toStringMessage":"java.lang.IllegalStateException: Server exception","message":"Server exception","stacktrace":[],"cause":null,"className":"java.lang.IllegalStateException"},"className":"java.lang.IllegalStateException"},"connectionId":1,"serviceId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallException","callId":"1:callException:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","cause":{"toStringMessage":"java.lang.IllegalStateException: Server exception","message":"Server exception","stacktrace":[],"cause":{"toStringMessage":"java.lang.IllegalStateException: Server exception","message":"Server exception","stacktrace":[],"cause":null,"className":"java.lang.IllegalStateException"},"className":"java.lang.IllegalStateException"},"connectionId":1,"serviceId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCGenericMessage","connectionId":null,"pluginParams":{"-32767":"cancellation","-32766":"CANCELLATION_ACK","-32765":"1:callException:1"}} diff --git a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_6_0/echo_json.gold b/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_6_0/echo_json.gold deleted file mode 100644 index e8d34906e..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_6_0/echo_json.gold +++ /dev/null @@ -1,9 +0,0 @@ -[Client] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765]} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765]} -[Server] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765],"connectionId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765],"connectionId":1} -[Client] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:echo:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"echo","callType":"Method","data":"{\"arg1\":\"Hello\",\"data\":{\"data\":\"data\"}}","connectionId":1,"serviceId":1} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:echo:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"echo","callType":"Method","data":"{\"arg1\":\"Hello\",\"data\":{\"data\":\"data\"}}","connectionId":1,"serviceId":1} -[Server] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallSuccess","callId":"1:echo:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","data":"{\"data\":\"data\"}","connectionId":1,"serviceId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallSuccess","callId":"1:echo:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","data":"{\"data\":\"data\"}","connectionId":1,"serviceId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCGenericMessage","connectionId":null,"pluginParams":{"-32767":"cancellation","-32766":"CANCELLATION_ACK","-32765":"1:echo:1"}} diff --git a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_6_0/echo_protobuf.gold b/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_6_0/echo_protobuf.gold deleted file mode 100644 index 78db12d34..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_6_0/echo_protobuf.gold +++ /dev/null @@ -1,18 +0,0 @@ -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake?!????????????????????????????????? -[Client] [Send] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b651221088180feffffffffffff01088280feffffffffffff01088380feffffffffffff01 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake?!????????????????????????????????? -[Server] [Receive] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b651221088180feffffffffffff01088280feffffffffffff01088380feffffffffffff01 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake?#??????????????????????????????????? -[Server] [Send] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b651223088180feffffffffffff01088280feffffffffffff01088380feffffffffffff011001 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake?#??????????????????????????????????? -[Client] [Receive] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b651223088180feffffffffffff01088280feffffffffffff01088380feffffffffffff011001 -// decoded: ??org.jetbrains.krpc.internal.transport.RPCMessage.CallDataBinary?Y??1:echo:1?0org.jetbrains.krpc.test.api.util.SamplingService??echo ?*???Hello????data0?8? -[Client] [Send] $ 0a3f6f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c4461746142696e61727912590a08313a6563686f3a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a046563686f20002a0f0a0548656c6c6f12060a046461746130013801 -// decoded: ??org.jetbrains.krpc.internal.transport.RPCMessage.CallDataBinary?Y??1:echo:1?0org.jetbrains.krpc.test.api.util.SamplingService??echo ?*???Hello????data0?8? -[Server] [Receive] $ 0a3f6f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c4461746142696e61727912590a08313a6563686f3a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a046563686f20002a0f0a0548656c6c6f12060a046461746130013801 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCMessage.CallSuccessBinary?H??1:echo:1?0org.jetbrains.krpc.test.api.util.SamplingService????data ?(? -[Server] [Send] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c5375636365737342696e61727912480a08313a6563686f3a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a060a046461746120012801 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCMessage.CallSuccessBinary?H??1:echo:1?0org.jetbrains.krpc.test.api.util.SamplingService????data ?(? -[Client] [Receive] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c5375636365737342696e61727912480a08313a6563686f3a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a060a046461746120012801 -// decoded: ?7org.jetbrains.krpc.internal.transport.RPCGenericMessage?Q???????????????cancellation???????????????CANCELLATION_ACK???????????????1:echo:1 -[Client] [Receive] $ 0a376f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504347656e657269634d65737361676512511219088180feffffffffffff01120c63616e63656c6c6174696f6e121d088280feffffffffffff01121043414e43454c4c4154494f4e5f41434b1215088380feffffffffffff011208313a6563686f3a31 diff --git a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_8_0/callException_json.gold b/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_8_0/callException_json.gold deleted file mode 100644 index c9c382ff5..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_8_0/callException_json.gold +++ /dev/null @@ -1,8 +0,0 @@ -[Client] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765,-32764]} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765,-32764]} -[Server] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765,-32764],"connectionId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765,-32764],"connectionId":1} -[Client] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:callException:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"callException","callType":"Method","data":"{}","connectionId":1,"serviceId":1,"pluginParams":{"-32763":""}} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:callException:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"callException","callType":"Method","data":"{}","connectionId":1,"serviceId":1,"pluginParams":{"-32763":""}} -[Server] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallException","callId":"1:callException:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","cause":{"toStringMessage":"java.lang.IllegalStateException: Server exception","message":"Server exception","stacktrace":[],"cause":null,"className":"java.lang.IllegalStateException"},"connectionId":1,"serviceId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallException","callId":"1:callException:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","cause":{"toStringMessage":"java.lang.IllegalStateException: Server exception","message":"Server exception","stacktrace":[],"cause":null,"className":"java.lang.IllegalStateException"},"connectionId":1,"serviceId":1} diff --git a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_8_0/echo_json.gold b/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_8_0/echo_json.gold deleted file mode 100644 index 7f68cc990..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_8_0/echo_json.gold +++ /dev/null @@ -1,10 +0,0 @@ -[Client] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765,-32764]} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765,-32764]} -[Server] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765,-32764],"connectionId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake","supportedPlugins":[-32767,-32766,-32765,-32764],"connectionId":1} -[Client] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:echo:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"echo","callType":"Method","data":"{\"arg1\":\"Hello\",\"data\":{\"data\":\"data\"}}","connectionId":1,"serviceId":1,"pluginParams":{"-32763":""}} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallData","callId":"1:echo:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","method":"echo","callType":"Method","data":"{\"arg1\":\"Hello\",\"data\":{\"data\":\"data\"}}","connectionId":1,"serviceId":1,"pluginParams":{"-32763":""}} -[Server] [Send] $ {"type":"org.jetbrains.krpc.RPCMessage.CallSuccess","callId":"1:echo:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","data":"{\"data\":\"data\"}","connectionId":1,"serviceId":1} -[Client] [Receive] $ {"type":"org.jetbrains.krpc.RPCMessage.CallSuccess","callId":"1:echo:1","serviceType":"org.jetbrains.krpc.test.api.util.SamplingService","data":"{\"data\":\"data\"}","connectionId":1,"serviceId":1} -[Client] [Send] $ {"type":"org.jetbrains.krpc.internal.transport.RPCGenericMessage","connectionId":null,"pluginParams":{"-32767":"cancellation","-32766":"REQUEST","-32764":"org.jetbrains.krpc.test.api.util.SamplingService","-32765":"1:echo:1"}} -[Server] [Receive] $ {"type":"org.jetbrains.krpc.internal.transport.RPCGenericMessage","connectionId":null,"pluginParams":{"-32767":"cancellation","-32766":"REQUEST","-32764":"org.jetbrains.krpc.test.api.util.SamplingService","-32765":"1:echo:1"}} diff --git a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_8_0/echo_protobuf.gold b/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_8_0/echo_protobuf.gold deleted file mode 100644 index bc4821607..000000000 --- a/krpc/krpc-test/src/jvmTest/resources/wire_dumps/0_8_0/echo_protobuf.gold +++ /dev/null @@ -1,20 +0,0 @@ -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake?,???????????????????????????????????????????? -[Client] [Send] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b65122c088180feffffffffffff01088280feffffffffffff01088380feffffffffffff01088480feffffffffffff01 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake?,???????????????????????????????????????????? -[Server] [Receive] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b65122c088180feffffffffffff01088280feffffffffffff01088380feffffffffffff01088480feffffffffffff01 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake?.?????????????????????????????????????????????? -[Server] [Send] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b65122e088180feffffffffffff01088280feffffffffffff01088380feffffffffffff01088480feffffffffffff011001 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCProtocolMessage.Handshake?.?????????????????????????????????????????????? -[Client] [Receive] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504350726f746f636f6c4d6573736167652e48616e647368616b65122e088180feffffffffffff01088280feffffffffffff01088380feffffffffffff01088480feffffffffffff011001 -// decoded: ??org.jetbrains.krpc.internal.transport.RPCMessage.CallDataBinary?h??1:echo:1?0org.jetbrains.krpc.test.api.util.SamplingService??echo ?*???Hello????data0?8?B?????????????? -[Client] [Send] $ 0a3f6f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c4461746142696e61727912680a08313a6563686f3a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a046563686f20002a0f0a0548656c6c6f12060a046461746130013801420d088580feffffffffffff011200 -// decoded: ??org.jetbrains.krpc.internal.transport.RPCMessage.CallDataBinary?h??1:echo:1?0org.jetbrains.krpc.test.api.util.SamplingService??echo ?*???Hello????data0?8?B?????????????? -[Server] [Receive] $ 0a3f6f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c4461746142696e61727912680a08313a6563686f3a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a046563686f20002a0f0a0548656c6c6f12060a046461746130013801420d088580feffffffffffff011200 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCMessage.CallSuccessBinary?H??1:echo:1?0org.jetbrains.krpc.test.api.util.SamplingService????data ?(? -[Server] [Send] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c5375636365737342696e61727912480a08313a6563686f3a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a060a046461746120012801 -// decoded: ?Borg.jetbrains.krpc.internal.transport.RPCMessage.CallSuccessBinary?H??1:echo:1?0org.jetbrains.krpc.test.api.util.SamplingService????data ?(? -[Client] [Receive] $ 0a426f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e5250434d6573736167652e43616c6c5375636365737342696e61727912480a08313a6563686f3a3112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651a060a046461746120012801 -// decoded: ?7org.jetbrains.krpc.internal.transport.RPCGenericMessage??????????????????cancellation???????????????REQUEST?=????????????0org.jetbrains.krpc.test.api.util.SamplingService???????????????1:echo:1 -[Client] [Send] $ 0a376f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504347656e657269634d6573736167651287011219088180feffffffffffff01120c63616e63656c6c6174696f6e1214088280feffffffffffff01120752455155455354123d088480feffffffffffff0112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651215088380feffffffffffff011208313a6563686f3a31 -// decoded: ?7org.jetbrains.krpc.internal.transport.RPCGenericMessage??????????????????cancellation???????????????REQUEST?=????????????0org.jetbrains.krpc.test.api.util.SamplingService???????????????1:echo:1 -[Server] [Receive] $ 0a376f72672e6a6574627261696e732e6b7270632e696e7465726e616c2e7472616e73706f72742e52504347656e657269634d6573736167651287011219088180feffffffffffff01120c63616e63656c6c6174696f6e1214088280feffffffffffff01120752455155455354123d088480feffffffffffff0112306f72672e6a6574627261696e732e6b7270632e746573742e6170692e7574696c2e53616d706c696e67536572766963651215088380feffffffffffff011208313a6563686f3a31 diff --git a/krpc/krpc-test/src/nativeTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.native.kt b/krpc/krpc-test/src/nativeTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.native.kt new file mode 100644 index 000000000..ed9cd5e35 --- /dev/null +++ b/krpc/krpc-test/src/nativeTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.native.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test + +internal actual val testIterations: Int = 1000 diff --git a/krpc/krpc-test/src/wasmJsMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.wasmJs.kt b/krpc/krpc-test/src/wasmJsMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.wasmJs.kt deleted file mode 100644 index 84b7631cb..000000000 --- a/krpc/krpc-test/src/wasmJsMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.wasmJs.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.krpc.test - -import kotlinx.coroutines.test.TestScope - -actual inline fun runThreadIfPossible(runner: () -> Unit) { - runner() -} - -internal actual fun TestScope.debugCoroutines() { -} diff --git a/krpc/krpc-test/src/wasmJsTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.wasmJs.kt b/krpc/krpc-test/src/wasmJsTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.wasmJs.kt new file mode 100644 index 000000000..483c79d1e --- /dev/null +++ b/krpc/krpc-test/src/wasmJsTest/kotlin/kotlinx/rpc/krpc/test/TransportTest.wasmJs.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test + +internal actual val testIterations: Int = 100 diff --git a/settings.gradle.kts b/settings.gradle.kts index 9c8dee01a..4cc0f76ff 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -52,7 +52,9 @@ includePublic(":krpc:krpc-ktor:krpc-ktor-server") includePublic(":krpc:krpc-ktor:krpc-ktor-client") include(":tests") +include(":tests:test-utils") include(":tests:krpc-compatibility-tests") +include(":tests:krpc-protocol-compatibility-tests") val kotlinMasterBuild = providers.gradleProperty("kotlinx.rpc.kotlinMasterBuild").orNull == "true" diff --git a/tests/krpc-compatibility-tests/build.gradle.kts b/tests/krpc-compatibility-tests/build.gradle.kts index 8b57ad1e6..f8f856898 100644 --- a/tests/krpc-compatibility-tests/build.gradle.kts +++ b/tests/krpc-compatibility-tests/build.gradle.kts @@ -67,6 +67,8 @@ dependencies { testImplementation(libs.slf4j.api) testImplementation(libs.logback.classic) testImplementation(libs.coroutines.debug) + + testImplementation(projects.tests.testUtils) } kotlin { diff --git a/tests/krpc-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/compatibility/KrpcCompatibilityTests.kt b/tests/krpc-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/compatibility/KrpcCompatibilityTests.kt index 9101b2b94..f8dec7c71 100644 --- a/tests/krpc-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/compatibility/KrpcCompatibilityTests.kt +++ b/tests/krpc-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/compatibility/KrpcCompatibilityTests.kt @@ -4,14 +4,17 @@ package kotlinx.rpc.krpc.compatibility -import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.job import kotlinx.rpc.krpc.rpcClientConfig import kotlinx.rpc.krpc.rpcServerConfig import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.test.runTestWithCoroutinesProbes import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory import java.net.URLClassLoader import java.util.stream.Stream +import kotlin.time.Duration.Companion.seconds class KrpcCompatibilityTests { private class ClientServer(private val clientClassLoader: URLClassLoader, val serverClassLoader: URLClassLoader) : @@ -62,13 +65,21 @@ class KrpcCompatibilityTests { private fun compatibilityTests(clientServer: ClientServer): Stream { return clientServer.client.getAllTests().map { (name, test) -> DynamicTest.dynamicTest(name) { - runTest { + runTestWithCoroutinesProbes(timeout = 60.seconds) { val localTransport = LocalTransport() val server = KrpcTestServer(rpcServerConfig, localTransport.server) val client = KrpcTestClient(rpcClientConfig, localTransport.client) clientServer.server.serveAllInterfaces(server) - test(client) + try { + test(client) + } finally { + server.close() + client.close() + server.awaitCompletion() + client.awaitCompletion() + localTransport.coroutineContext.job.cancelAndJoin() + } } } }.stream() diff --git a/tests/krpc-protocol-compatibility-tests/build.gradle.kts b/tests/krpc-protocol-compatibility-tests/build.gradle.kts new file mode 100644 index 000000000..5b72247b3 --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/build.gradle.kts @@ -0,0 +1,95 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PropertyName") + +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + +plugins { + alias(libs.plugins.conventions.jvm) + alias(libs.plugins.serialization) + alias(libs.plugins.kotlinx.rpc) + alias(libs.plugins.atomicfu) +} + +val main: SourceSet by sourceSets.getting +val test: SourceSet by sourceSets.getting + +val compatibilityTestSourcesDir: File = project.layout.buildDirectory.dir("compatibilityTestSources").get().asFile + +fun versioned(name: String): Configuration { + val configuration = configurations.create(name) { + isCanBeConsumed = true + isCanBeResolved = true + isTransitive = true + } + + val sourceSet = sourceSets.create(name) { + compileClasspath += main.output + runtimeClasspath += main.output + + compileClasspath += configuration + runtimeClasspath += configuration + } + + val copySourceSetTestResources by tasks.register("copy_${name}_ToTestResources") { + dependsOn(sourceSet.output) + from(sourceSet.output) + into(compatibilityTestSourcesDir.resolve(name)) + } + + tasks.processTestResources.configure { + dependsOn(copySourceSetTestResources) + } + + return configuration +} + +val v0_9 = versioned("v0_9") +val v0_8 = versioned("v0_8") + +test.resources { + srcDir(compatibilityTestSourcesDir) +} + +fun DependencyHandlerScope.versioned(configuration: Configuration, version: String) { + add(configuration.name, "org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:$version") + add(configuration.name, "org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:$version") + add(configuration.name, "org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:$version") + add(configuration.name, libs.atomicfu) + add(configuration.name, projects.tests.testUtils) +} + +dependencies { + api(libs.atomicfu) + api(projects.tests.testUtils) + implementation(libs.serialization.core) + implementation(libs.coroutines.core) + implementation(libs.kotlin.reflect) + + versioned(v0_9, "0.9.1") + versioned(v0_8, "0.8.1") + + // current version is in test source set + testImplementation(projects.krpc.krpcCore) + testImplementation(projects.krpc.krpcServer) + testImplementation(projects.krpc.krpcClient) + testImplementation(projects.krpc.krpcSerialization.krpcSerializationJson) + testImplementation(projects.tests.testUtils) + + testImplementation(libs.coroutines.test) + testImplementation(libs.kotlin.test.junit5) + + testImplementation(libs.slf4j.api) + testImplementation(libs.logback.classic) + testImplementation(libs.coroutines.debug) +} + +kotlin { + explicitApi = ExplicitApiMode.Disabled +} + +tasks.test { + useJUnitPlatform() +} diff --git a/tests/krpc-protocol-compatibility-tests/src/main/kotlin/kotlinx/rpc/krpc/test/compat/TestApi.kt b/tests/krpc-protocol-compatibility-tests/src/main/kotlin/kotlinx/rpc/krpc/test/compat/TestApi.kt new file mode 100644 index 000000000..3a1b30c2c --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/main/kotlin/kotlinx/rpc/krpc/test/compat/TestApi.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.rpc.test.WaitCounter + +interface CompatTransport : CoroutineScope { + suspend fun send(message: String) + suspend fun receive(): String +} + +class TestConfig( + val perCallBufferSize: Int, +) + +interface CompatService { + suspend fun unary(n: Int): Int + fun serverStreaming(num: Int): Flow + suspend fun clientStreaming(n: Flow): Int + fun bidiStreaming(flow: Flow): Flow + + suspend fun requestCancellation() + fun serverStreamCancellation(): Flow + suspend fun clientStreamCancellation(n: Flow) + + fun fastServerProduce(n: Int): Flow +} + +interface CompatServiceImpl { + val exitMethod: WaitCounter + val cancelled: WaitCounter + val entered: CompletableDeferred + val fence: CompletableDeferred +} + +interface Starter { + suspend fun startClient(transport: CompatTransport, config: TestConfig): CompatService + suspend fun stopClient() + suspend fun startServer(transport: CompatTransport, config: TestConfig): CompatServiceImpl + suspend fun stopServer() +} diff --git a/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/KrpcProtocolCompatibilityTests.kt b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/KrpcProtocolCompatibilityTests.kt new file mode 100644 index 000000000..96c0e2210 --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/KrpcProtocolCompatibilityTests.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat + +import kotlinx.coroutines.async +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import org.junit.jupiter.api.TestFactory +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.seconds + +class KrpcProtocolCompatibilityTests : KrpcProtocolCompatibilityTestsBase() { + @TestFactory + fun unaryCalls() = matrixTest { service, _ -> + assertEquals(1, service.unary(1)) + + List(100) { + launch { + assertEquals(it + 1, service.unary(it + 1)) + } + }.joinAll() + + assertNoErrorsInLogs() + } + + @TestFactory + fun serverStreamCalls() = matrixTest { service, _ -> + assertEquals(1, service.serverStreaming(1).toList().sum()) + + List(100) { + launch { + assertEquals((it + 1) * (it + 2) / 2, service.serverStreaming(it + 1).toList().sum()) + } + }.joinAll() + + assertNoErrorsInLogs() + } + + @TestFactory + fun clientStreamCalls() = matrixTest { service, _ -> + assertEquals(1, service.clientStreaming((1..1).asFlow())) + + List(100) { + launch { + assertEquals( + (it + 1) * (it + 2) / 2, + service.clientStreaming((1..it + 1).asFlow()), + ) + } + }.joinAll() + + assertNoErrorsInLogs() + } + + @TestFactory + fun bidiStreamCalls() = matrixTest { service, _ -> + assertEquals( + 1, + service.bidiStreaming((1..1).asFlow()).toList().sum() + ) + + List(100) { + launch { + assertEquals( + (it + 1) * (it + 2) / 2, + service.bidiStreaming((1..it + 1).asFlow()).toList().sum(), + ) + } + }.joinAll() + + assertNoErrorsInLogs() + } + + @TestFactory + fun requestCancellation() = matrixTest { service, impl -> + val job = launch { + service.requestCancellation() + } + + impl.entered.await() + job.cancelAndJoin() + impl.cancelled.await(1) + assertEquals(0, impl.exitMethod.value) + + val followup = launch { + service.requestCancellation() + } + impl.fence.complete(Unit) + followup.join() + assertEquals(1, impl.exitMethod.value) + + assertNoErrorsInLogs() + assertEquals(1, impl.cancelled.value) + } + + @TestFactory + fun serverStreamCancellation() = matrixTest { service, impl -> + val job = launch { + service.serverStreamCancellation().collect {} + } + + impl.entered.await() + job.cancelAndJoin() + impl.cancelled.await(1) + assertEquals(0, impl.exitMethod.value) + + val followup = async { + service.serverStreamCancellation().toList() + } + impl.fence.complete(Unit) + assertEquals(listOf(1, 2), followup.await()) + + assertNoErrorsInLogs() + assertEquals(1, impl.cancelled.value) + } + + @TestFactory + fun clientStreamCancellation() = matrixTest { service, impl -> + val job = launch { + service.clientStreamCancellation(flow { + emit(1) + impl.fence.await() + }) + } + + impl.entered.await() + job.cancelAndJoin() + impl.cancelled.await(1) + + assertNoErrorsInLogs() + } + + @TestFactory + fun fastProducer() = matrixTest(timeout = 60.seconds) { service, impl -> + val async = async { + service.fastServerProduce(1000).map { + // long produce + impl.entered.complete(Unit) + impl.fence.await() + it * it + }.toList() + } + + impl.entered.await() + repeat(10_000) { + assertEquals(1, service.unary(1)) + assertEquals(55, service.serverStreaming(10).toList().sum()) + } + + impl.fence.complete(Unit) + assertEquals(List(1000) { it * it },async.await()) + } +} diff --git a/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/KrpcProtocolCompatibilityTestsBase.kt b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/KrpcProtocolCompatibilityTestsBase.kt new file mode 100644 index 000000000..649a7ee44 --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/KrpcProtocolCompatibilityTestsBase.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat + +import ch.qos.logback.classic.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.debug.DebugProbes +import kotlinx.coroutines.test.TestScope +import kotlinx.rpc.test.runTestWithCoroutinesProbes +import org.junit.jupiter.api.DynamicTest +import org.slf4j.LoggerFactory +import java.net.URLClassLoader +import java.util.stream.Stream +import kotlin.test.assertTrue +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Suppress("EnumEntryName", "detekt.EnumNaming") +enum class Versions { + v0_9, + v0_8, + ; +} + +enum class Role { + Server, Client; +} + +class VersionRolePair( + val version: Versions, + val role: Role, +) + +@Suppress("unused") +val Versions.client get() = VersionRolePair(this, Role.Client) +@Suppress("unused") +val Versions.server get() = VersionRolePair(this, Role.Server) + +abstract class KrpcProtocolCompatibilityTestsBase { + class LoadedStarter(val version: Versions, val classLoader: URLClassLoader) { + val starter = classLoader + .loadClass("kotlinx.rpc.krpc.test.compat.service.TestStarter") + .getDeclaredConstructor() + .newInstance() as Starter + + suspend fun close() { + classLoader.close() + starter.stopClient() + starter.stopServer() + } + } + + private fun prepareStarters(exclude: List): List { + return Versions.entries.filter { it !in exclude }.map { version -> + val versionResourcePath = javaClass.classLoader.getResource(version.name)!! + val versionClassLoader = URLClassLoader(arrayOf(versionResourcePath), javaClass.classLoader) + + LoadedStarter(version, versionClassLoader) + } + } + + class TestEnv( + val old: Starter, + val new: Starter, + val appender: TestLogAppender, + val testScope: TestScope, + ) : CoroutineScope by testScope { + fun assertNoErrorsInLogs() { + assertTrue { appender.errors.isEmpty() } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun runTest( + role: Role, + exclude: List, + timeout: Duration = 10.seconds, + body: suspend TestEnv.() -> Unit, + ): Stream { + return prepareStarters(exclude).map { + DynamicTest.dynamicTest("$role ${it.version}") { + runTestWithCoroutinesProbes(timeout = timeout) { + DebugProbes.withDebugProbes { + val root = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger + val testAppender = root.getAppender("TEST") as TestLogAppender + testAppender.events.clear() + try { + val env = TestEnv(it.starter, it.starter, testAppender, this) + body(env) + } finally { + testAppender.events.clear() + it.close() + } + } + } + } + }.stream() + } + + private fun testTransport() = LocalTransport() + + private fun testOldClientWithNewServer( + perCallBufferSize: Int = 1, + timeout: Duration = 10.seconds, + exclude: List, + body: suspend TestEnv.(CompatService, CompatServiceImpl) -> Unit, + ) = runTest(Role.Client, exclude, timeout) { + val transport = testTransport() + val config = TestConfig(perCallBufferSize) + val service = old.startClient(transport.client, config) + val impl = new.startServer(transport.server, config) + body(service, impl) + } + + private fun testOldServersWithNewClient( + perCallBufferSize: Int = 1, + timeout: Duration = 10.seconds, + exclude: List, + body: suspend TestEnv.(CompatService, CompatServiceImpl) -> Unit, + ) = runTest(Role.Server, exclude, timeout) { + val transport = testTransport() + val config = TestConfig(perCallBufferSize) + val service = new.startClient(transport.client, config) + val impl = old.startServer(transport.server, config) + body(service, impl) + } + + protected fun matrixTest( + perCallBufferSize: Int = 1, + timeout: Duration = 10.seconds, + exclude: List = emptyList(), + body: suspend TestEnv.(CompatService, CompatServiceImpl) -> Unit, + ): Stream { + val clientExclude = exclude.filter { it.role == Role.Client }.map { it.version } + val serverExclude = exclude.filter { it.role == Role.Server }.map { it.version } + return Stream.concat( + testOldClientWithNewServer(perCallBufferSize, timeout, clientExclude, body), + testOldServersWithNewClient(perCallBufferSize, timeout, serverExclude, body), + ) + } +} diff --git a/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/LocalTransport.kt b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/LocalTransport.kt new file mode 100644 index 000000000..a27339c83 --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/LocalTransport.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.job +import kotlin.coroutines.CoroutineContext + +class LocalTransport(parentScope: CoroutineScope? = null) : CoroutineScope { + override val coroutineContext = parentScope?.run { SupervisorJob(coroutineContext.job) } + ?: SupervisorJob() + + private val clientIncoming = Channel() + private val serverIncoming = Channel() + + val client: CompatTransport = object : CompatTransport { + override val coroutineContext: CoroutineContext = Job(this@LocalTransport.coroutineContext.job) + + override suspend fun send(message: String) { + serverIncoming.send(message) + } + + override suspend fun receive(): String { + return clientIncoming.receive() + } + } + + val server: CompatTransport = object : CompatTransport { + override val coroutineContext: CoroutineContext = Job(this@LocalTransport.coroutineContext) + + override suspend fun send(message: String) { + clientIncoming.send(message) + } + + override suspend fun receive(): String { + return serverIncoming.receive() + } + } +} diff --git a/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/TestLogAppender.kt b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/TestLogAppender.kt new file mode 100644 index 000000000..809e3b51d --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/TestLogAppender.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase + +class TestLogAppender : AppenderBase() { + init { + start() + } + + val events = mutableListOf() + val errors get() = events.filter { it.level == Level.ERROR } + + override fun append(eventObject: ILoggingEvent) { + events.add(eventObject) + } +} diff --git a/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/service/TestService.kt b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/service/TestService.kt new file mode 100644 index 000000000..de6b12b38 --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/service/TestService.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat.service + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList +import kotlinx.rpc.annotations.Rpc +import kotlinx.rpc.krpc.test.compat.CompatServiceImpl +import kotlinx.rpc.test.WaitCounter +import kotlin.coroutines.cancellation.CancellationException + +@Rpc +interface TestService { + suspend fun unary(n: Int): Int + fun serverStreaming(num: Int): Flow + suspend fun clientStreaming(n: Flow): Int + fun bidiStreaming(flow: Flow): Flow + suspend fun requestCancellation() + fun serverStreamCancellation(): Flow + suspend fun clientStreamCancellation(n: Flow) + + fun fastServerProduce(n: Int): Flow +} + +class TestServiceImpl : TestService, CompatServiceImpl { + override suspend fun unary(n: Int): Int { + return n + } + + override fun serverStreaming(num: Int): Flow { + return (1..num).asFlow() + } + + override suspend fun clientStreaming(n: Flow): Int { + return n.toList().sum() + } + + override fun bidiStreaming(flow: Flow): Flow { + return flow + } + + override val exitMethod: WaitCounter = WaitCounter() + override val cancelled: WaitCounter = WaitCounter() + + override val entered: CompletableDeferred = CompletableDeferred() + override val fence: CompletableDeferred = CompletableDeferred() + + override suspend fun requestCancellation() { + try { + entered.complete(Unit) + fence.await() + exitMethod.increment() + } catch (e: CancellationException) { + cancelled.increment() + throw e + } + } + + override fun serverStreamCancellation(): Flow { + return flow { + try { + emit(1) + entered.complete(Unit) + fence.await() + emit(2) + } catch (e: CancellationException) { + cancelled.increment() + throw e + } + } + } + + override suspend fun clientStreamCancellation(n: Flow) { + try { + n.collect { + if (it != 0) { + entered.complete(Unit) + } + } + } catch (e: CancellationException) { + cancelled.increment() + throw e + } + } + + override fun fastServerProduce(n: Int): Flow { + return flow { + repeat(n) { + emit(it) + } + } + } +} diff --git a/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/service/TestStarter.kt b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/service/TestStarter.kt new file mode 100644 index 000000000..f411b07cf --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/test/kotlin/kotlinx/rpc/krpc/test/compat/service/TestStarter.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat.service + +import kotlinx.coroutines.flow.Flow +import kotlinx.rpc.krpc.KrpcTransport +import kotlinx.rpc.krpc.KrpcTransportMessage +import kotlinx.rpc.krpc.client.InitializedKrpcClient +import kotlinx.rpc.krpc.client.KrpcClient +import kotlinx.rpc.krpc.rpcClientConfig +import kotlinx.rpc.krpc.rpcServerConfig +import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.krpc.server.KrpcServer +import kotlinx.rpc.krpc.test.compat.CompatService +import kotlinx.rpc.krpc.test.compat.CompatServiceImpl +import kotlinx.rpc.krpc.test.compat.CompatTransport +import kotlinx.rpc.krpc.test.compat.Starter +import kotlinx.rpc.krpc.test.compat.TestConfig +import kotlinx.rpc.registerService +import kotlinx.rpc.withService +import kotlin.coroutines.CoroutineContext + +fun CompatTransport.toKrpc(): KrpcTransport { + return object : KrpcTransport { + override suspend fun send(message: KrpcTransportMessage) { + this@toKrpc.send((message as KrpcTransportMessage.StringMessage).value) + } + + override suspend fun receive(): KrpcTransportMessage { + return KrpcTransportMessage.StringMessage(this@toKrpc.receive()) + } + + override val coroutineContext: CoroutineContext = this@toKrpc.coroutineContext + } +} + +@Suppress("unused") +class TestStarter : Starter { + private var client: KrpcClient? = null + private var server: KrpcServer? = null + + override suspend fun startClient(transport: CompatTransport, config: TestConfig): CompatService { + val transport = transport.toKrpc() + val clientConfig = rpcClientConfig { + serialization { + json() + } + + connector { + perCallBufferSize = config.perCallBufferSize + } + } + + client = object : InitializedKrpcClient(clientConfig, transport) {} + val service = client!!.withService() + return object : CompatService { + override suspend fun unary(n: Int): Int { + return service.unary(n) + } + + override fun serverStreaming(num: Int): Flow { + return service.serverStreaming(num) + } + + override suspend fun clientStreaming(n: Flow): Int { + return service.clientStreaming(n) + } + + override fun bidiStreaming(flow: Flow): Flow { + return service.bidiStreaming(flow) + } + + override suspend fun requestCancellation() { + return service.requestCancellation() + } + + override fun serverStreamCancellation(): Flow { + return service.serverStreamCancellation() + } + + override suspend fun clientStreamCancellation(n: Flow) { + return service.clientStreamCancellation(n) + } + + override fun fastServerProduce(n: Int): Flow { + return service.fastServerProduce(n) + } + } + } + + override suspend fun stopClient() { + client?.close() + client?.awaitCompletion() + } + + override suspend fun startServer(transport: CompatTransport, config: TestConfig): CompatServiceImpl { + val transport = transport.toKrpc() + val serverConfig = rpcServerConfig { + serialization { + json() + } + + connector { + perCallBufferSize = config.perCallBufferSize + } + } + + server = object : KrpcServer(serverConfig, transport) {} + val impl = TestServiceImpl() + server?.registerService { impl } + return impl + } + + override suspend fun stopServer() { + server?.close() + server?.awaitCompletion() + } +} diff --git a/tests/krpc-protocol-compatibility-tests/src/test/resources/logback.xml b/tests/krpc-protocol-compatibility-tests/src/test/resources/logback.xml new file mode 100644 index 000000000..b95d91b44 --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/test/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/tests/krpc-protocol-compatibility-tests/src/v0_8/kotlin/kotlinx/rpc/krpc/test/compat/service/TestService.kt b/tests/krpc-protocol-compatibility-tests/src/v0_8/kotlin/kotlinx/rpc/krpc/test/compat/service/TestService.kt new file mode 100644 index 000000000..4bd533560 --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/v0_8/kotlin/kotlinx/rpc/krpc/test/compat/service/TestService.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat.service + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList +import kotlinx.rpc.annotations.Rpc +import kotlinx.rpc.krpc.test.compat.CompatServiceImpl +import kotlinx.rpc.test.WaitCounter +import kotlin.coroutines.cancellation.CancellationException + +@Rpc +interface TestService { + suspend fun unary(n: Int): Int + fun serverStreaming(num: Int): Flow + suspend fun clientStreaming(n: Flow): Int + fun bidiStreaming(flow: Flow): Flow + + suspend fun requestCancellation() + fun serverStreamCancellation(): Flow + suspend fun clientStreamCancellation(n: Flow) + + fun fastServerProduce(n: Int): Flow +} + +class TestServiceImpl : TestService, CompatServiceImpl { + override suspend fun unary(n: Int): Int { + return n + } + + override fun serverStreaming(num: Int): Flow { + return (1..num).asFlow() + } + + override suspend fun clientStreaming(n: Flow): Int { + return n.toList().sum() + } + + override fun bidiStreaming(flow: Flow): Flow { + return flow + } + + override val exitMethod: WaitCounter = WaitCounter() + override val cancelled: WaitCounter = WaitCounter() + + override val entered: CompletableDeferred = CompletableDeferred() + override val fence: CompletableDeferred = CompletableDeferred() + + override suspend fun requestCancellation() { + try { + entered.complete(Unit) + fence.await() + exitMethod.increment() + } catch (e: CancellationException) { + cancelled.increment() + throw e + } + } + + override fun serverStreamCancellation(): Flow { + return flow { + try { + emit(1) + entered.complete(Unit) + fence.await() + emit(2) + } catch (e: CancellationException) { + cancelled.increment() + throw e + } + } + } + + override suspend fun clientStreamCancellation(n: Flow) { + try { + n.collect { + if (it != 0) { + entered.complete(Unit) + } + } + } catch (e: CancellationException) { + cancelled.increment() + throw e + } + } + + override fun fastServerProduce(n: Int): Flow { + return flow { + repeat(n) { + emit(it) + } + } + } +} diff --git a/tests/krpc-protocol-compatibility-tests/src/v0_8/kotlin/kotlinx/rpc/krpc/test/compat/service/TestStarter.kt b/tests/krpc-protocol-compatibility-tests/src/v0_8/kotlin/kotlinx/rpc/krpc/test/compat/service/TestStarter.kt new file mode 100644 index 000000000..c18878a74 --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/v0_8/kotlin/kotlinx/rpc/krpc/test/compat/service/TestStarter.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat.service + +import kotlinx.coroutines.flow.Flow +import kotlinx.rpc.krpc.KrpcTransport +import kotlinx.rpc.krpc.KrpcTransportMessage +import kotlinx.rpc.krpc.client.InitializedKrpcClient +import kotlinx.rpc.krpc.client.KrpcClient +import kotlinx.rpc.krpc.rpcClientConfig +import kotlinx.rpc.krpc.rpcServerConfig +import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.krpc.server.KrpcServer +import kotlinx.rpc.krpc.test.compat.CompatService +import kotlinx.rpc.krpc.test.compat.CompatServiceImpl +import kotlinx.rpc.krpc.test.compat.CompatTransport +import kotlinx.rpc.krpc.test.compat.Starter +import kotlinx.rpc.krpc.test.compat.TestConfig +import kotlinx.rpc.registerService +import kotlinx.rpc.withService +import kotlin.coroutines.CoroutineContext + +fun CompatTransport.toKrpc(): KrpcTransport { + return object : KrpcTransport { + override suspend fun send(message: KrpcTransportMessage) { + this@toKrpc.send((message as KrpcTransportMessage.StringMessage).value) + } + + override suspend fun receive(): KrpcTransportMessage { + return KrpcTransportMessage.StringMessage(this@toKrpc.receive()) + } + + override val coroutineContext: CoroutineContext = this@toKrpc.coroutineContext + } +} + +@Suppress("unused") +class TestStarter : Starter { + private var client: KrpcClient? = null + private var server: KrpcServer? = null + + override suspend fun startClient(transport: CompatTransport, config: TestConfig): CompatService { + val transport = transport.toKrpc() + val clientConfig = rpcClientConfig { + serialization { + json() + } + } + + client = object : InitializedKrpcClient(clientConfig, transport) {} + val service = client!!.withService() + return object : CompatService { + override suspend fun unary(n: Int): Int { + return service.unary(n) + } + + override fun serverStreaming(num: Int): Flow { + return service.serverStreaming(num) + } + + override suspend fun clientStreaming(n: Flow): Int { + return service.clientStreaming(n) + } + + override fun bidiStreaming(flow: Flow): Flow { + return service.bidiStreaming(flow) + } + + override suspend fun requestCancellation() { + return service.requestCancellation() + } + + override fun serverStreamCancellation(): Flow { + return service.serverStreamCancellation() + } + + override suspend fun clientStreamCancellation(n: Flow) { + return service.clientStreamCancellation(n) + } + + override fun fastServerProduce(n: Int): Flow { + return service.fastServerProduce(n) + } + } + } + + override suspend fun stopClient() { + client?.close() + client?.awaitCompletion() + } + + override suspend fun startServer(transport: CompatTransport, config: TestConfig): CompatServiceImpl { + val transport = transport.toKrpc() + val serverConfig = rpcServerConfig { + serialization { + json() + } + } + + server = object : KrpcServer(serverConfig, transport) {} + val impl = TestServiceImpl() + server?.registerService { impl } + return impl + } + + override suspend fun stopServer() { + server?.close() + server?.awaitCompletion() + } +} diff --git a/tests/krpc-protocol-compatibility-tests/src/v0_9/kotlin/kotlinx/rpc/krpc/test/compat/service/TestService.kt b/tests/krpc-protocol-compatibility-tests/src/v0_9/kotlin/kotlinx/rpc/krpc/test/compat/service/TestService.kt new file mode 100644 index 000000000..4bd533560 --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/v0_9/kotlin/kotlinx/rpc/krpc/test/compat/service/TestService.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat.service + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList +import kotlinx.rpc.annotations.Rpc +import kotlinx.rpc.krpc.test.compat.CompatServiceImpl +import kotlinx.rpc.test.WaitCounter +import kotlin.coroutines.cancellation.CancellationException + +@Rpc +interface TestService { + suspend fun unary(n: Int): Int + fun serverStreaming(num: Int): Flow + suspend fun clientStreaming(n: Flow): Int + fun bidiStreaming(flow: Flow): Flow + + suspend fun requestCancellation() + fun serverStreamCancellation(): Flow + suspend fun clientStreamCancellation(n: Flow) + + fun fastServerProduce(n: Int): Flow +} + +class TestServiceImpl : TestService, CompatServiceImpl { + override suspend fun unary(n: Int): Int { + return n + } + + override fun serverStreaming(num: Int): Flow { + return (1..num).asFlow() + } + + override suspend fun clientStreaming(n: Flow): Int { + return n.toList().sum() + } + + override fun bidiStreaming(flow: Flow): Flow { + return flow + } + + override val exitMethod: WaitCounter = WaitCounter() + override val cancelled: WaitCounter = WaitCounter() + + override val entered: CompletableDeferred = CompletableDeferred() + override val fence: CompletableDeferred = CompletableDeferred() + + override suspend fun requestCancellation() { + try { + entered.complete(Unit) + fence.await() + exitMethod.increment() + } catch (e: CancellationException) { + cancelled.increment() + throw e + } + } + + override fun serverStreamCancellation(): Flow { + return flow { + try { + emit(1) + entered.complete(Unit) + fence.await() + emit(2) + } catch (e: CancellationException) { + cancelled.increment() + throw e + } + } + } + + override suspend fun clientStreamCancellation(n: Flow) { + try { + n.collect { + if (it != 0) { + entered.complete(Unit) + } + } + } catch (e: CancellationException) { + cancelled.increment() + throw e + } + } + + override fun fastServerProduce(n: Int): Flow { + return flow { + repeat(n) { + emit(it) + } + } + } +} diff --git a/tests/krpc-protocol-compatibility-tests/src/v0_9/kotlin/kotlinx/rpc/krpc/test/compat/service/TestStarter.kt b/tests/krpc-protocol-compatibility-tests/src/v0_9/kotlin/kotlinx/rpc/krpc/test/compat/service/TestStarter.kt new file mode 100644 index 000000000..c18878a74 --- /dev/null +++ b/tests/krpc-protocol-compatibility-tests/src/v0_9/kotlin/kotlinx/rpc/krpc/test/compat/service/TestStarter.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.krpc.test.compat.service + +import kotlinx.coroutines.flow.Flow +import kotlinx.rpc.krpc.KrpcTransport +import kotlinx.rpc.krpc.KrpcTransportMessage +import kotlinx.rpc.krpc.client.InitializedKrpcClient +import kotlinx.rpc.krpc.client.KrpcClient +import kotlinx.rpc.krpc.rpcClientConfig +import kotlinx.rpc.krpc.rpcServerConfig +import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.krpc.server.KrpcServer +import kotlinx.rpc.krpc.test.compat.CompatService +import kotlinx.rpc.krpc.test.compat.CompatServiceImpl +import kotlinx.rpc.krpc.test.compat.CompatTransport +import kotlinx.rpc.krpc.test.compat.Starter +import kotlinx.rpc.krpc.test.compat.TestConfig +import kotlinx.rpc.registerService +import kotlinx.rpc.withService +import kotlin.coroutines.CoroutineContext + +fun CompatTransport.toKrpc(): KrpcTransport { + return object : KrpcTransport { + override suspend fun send(message: KrpcTransportMessage) { + this@toKrpc.send((message as KrpcTransportMessage.StringMessage).value) + } + + override suspend fun receive(): KrpcTransportMessage { + return KrpcTransportMessage.StringMessage(this@toKrpc.receive()) + } + + override val coroutineContext: CoroutineContext = this@toKrpc.coroutineContext + } +} + +@Suppress("unused") +class TestStarter : Starter { + private var client: KrpcClient? = null + private var server: KrpcServer? = null + + override suspend fun startClient(transport: CompatTransport, config: TestConfig): CompatService { + val transport = transport.toKrpc() + val clientConfig = rpcClientConfig { + serialization { + json() + } + } + + client = object : InitializedKrpcClient(clientConfig, transport) {} + val service = client!!.withService() + return object : CompatService { + override suspend fun unary(n: Int): Int { + return service.unary(n) + } + + override fun serverStreaming(num: Int): Flow { + return service.serverStreaming(num) + } + + override suspend fun clientStreaming(n: Flow): Int { + return service.clientStreaming(n) + } + + override fun bidiStreaming(flow: Flow): Flow { + return service.bidiStreaming(flow) + } + + override suspend fun requestCancellation() { + return service.requestCancellation() + } + + override fun serverStreamCancellation(): Flow { + return service.serverStreamCancellation() + } + + override suspend fun clientStreamCancellation(n: Flow) { + return service.clientStreamCancellation(n) + } + + override fun fastServerProduce(n: Int): Flow { + return service.fastServerProduce(n) + } + } + } + + override suspend fun stopClient() { + client?.close() + client?.awaitCompletion() + } + + override suspend fun startServer(transport: CompatTransport, config: TestConfig): CompatServiceImpl { + val transport = transport.toKrpc() + val serverConfig = rpcServerConfig { + serialization { + json() + } + } + + server = object : KrpcServer(serverConfig, transport) {} + val impl = TestServiceImpl() + server?.registerService { impl } + return impl + } + + override suspend fun stopServer() { + server?.close() + server?.awaitCompletion() + } +} diff --git a/tests/test-utils/build.gradle.kts b/tests/test-utils/build.gradle.kts new file mode 100644 index 000000000..861604f02 --- /dev/null +++ b/tests/test-utils/build.gradle.kts @@ -0,0 +1,29 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + +plugins { + alias(libs.plugins.conventions.kmp) + alias(libs.plugins.atomicfu) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(libs.coroutines.core) + implementation(libs.coroutines.test) + } + } + + jvmMain { + dependencies { + implementation(libs.coroutines.debug) + } + } + } + + explicitApi = ExplicitApiMode.Disabled +} diff --git a/tests/test-utils/src/commonMain/kotlin/kotlinx/rpc/test/runTest.kt b/tests/test-utils/src/commonMain/kotlin/kotlinx/rpc/test/runTest.kt new file mode 100644 index 000000000..ecee5d1ae --- /dev/null +++ b/tests/test-utils/src/commonMain/kotlin/kotlinx/rpc/test/runTest.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.TestScope +import kotlin.time.Duration + +fun runTestWithCoroutinesProbes( + timeout: Duration, + body: suspend TestScope.() -> Unit, +): TestResult { + return withDebugProbes { + kotlinx.coroutines.test.runTest(timeout = timeout, testBody = body) + } +} + +expect fun withDebugProbes(body: () -> T): T diff --git a/tests/test-utils/src/commonMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.kt b/tests/test-utils/src/commonMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.kt new file mode 100644 index 000000000..125833423 --- /dev/null +++ b/tests/test-utils/src/commonMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +expect fun runThreadIfPossible(runner: () -> Unit) diff --git a/tests/test-utils/src/commonMain/kotlin/kotlinx/rpc/test/waitCounter.kt b/tests/test-utils/src/commonMain/kotlin/kotlinx/rpc/test/waitCounter.kt new file mode 100644 index 000000000..06668ebc1 --- /dev/null +++ b/tests/test-utils/src/commonMain/kotlin/kotlinx/rpc/test/waitCounter.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.locks.ReentrantLock +import kotlinx.atomicfu.locks.withLock +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.collections.orEmpty +import kotlin.collections.plus +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume + +class WaitCounter { + val value: Int get() = counter.value + private val counter = atomic(0) + private val lock = ReentrantLock() + private val waiters = mutableMapOf>>() + + fun increment() { + lock.withLock { + val current = counter.incrementAndGet() + waiters[current]?.forEach { it.resume(Unit) } + } + } + + suspend fun await(value: Int) = suspendCancellableCoroutine { + lock.withLock { + if (counter.value == value) { + it.resume(Unit) + } else { + waiters[value] = waiters[value].orEmpty() + it + } + } + } +} diff --git a/tests/test-utils/src/jsMain/kotlin/kotlinx/rpc/test/runTest.js.kt b/tests/test-utils/src/jsMain/kotlin/kotlinx/rpc/test/runTest.js.kt new file mode 100644 index 000000000..40ec1a7b4 --- /dev/null +++ b/tests/test-utils/src/jsMain/kotlin/kotlinx/rpc/test/runTest.js.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +actual inline fun withDebugProbes(body: () -> T): T = body() diff --git a/tests/test-utils/src/jsMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.js.kt b/tests/test-utils/src/jsMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.js.kt new file mode 100644 index 000000000..903dbc590 --- /dev/null +++ b/tests/test-utils/src/jsMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.js.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +actual inline fun runThreadIfPossible(runner: () -> Unit) = runner() diff --git a/krpc/krpc-test/src/jvmMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.jvm.kt b/tests/test-utils/src/jvmMain/kotlin/kotlinx/rpc/test/runTest.jvm.kt similarity index 53% rename from krpc/krpc-test/src/jvmMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.jvm.kt rename to tests/test-utils/src/jvmMain/kotlin/kotlinx/rpc/test/runTest.jvm.kt index 864ee2d54..07518536c 100644 --- a/krpc/krpc-test/src/jvmMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.jvm.kt +++ b/tests/test-utils/src/jvmMain/kotlin/kotlinx/rpc/test/runTest.jvm.kt @@ -2,17 +2,16 @@ * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.rpc.krpc.test +package kotlinx.rpc.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.debug.DebugProbes -import kotlinx.coroutines.test.TestScope - -actual fun runThreadIfPossible(runner: () -> Unit) { - Thread(runner).start() -} @OptIn(ExperimentalCoroutinesApi::class) -internal actual fun TestScope.debugCoroutines() { - DebugProbes.install() +actual fun withDebugProbes(body: () -> T): T { + var result: T? = null + DebugProbes.withDebugProbes { + result = body() + } + return result!! } diff --git a/tests/test-utils/src/jvmMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.jvm.kt b/tests/test-utils/src/jvmMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.jvm.kt new file mode 100644 index 000000000..7532298c5 --- /dev/null +++ b/tests/test-utils/src/jvmMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.jvm.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +actual fun runThreadIfPossible(runner: () -> Unit) { + Thread(runner).start() +} diff --git a/tests/test-utils/src/nativeMain/kotlin/kotlinx/rpc/test/runTest.native.kt b/tests/test-utils/src/nativeMain/kotlin/kotlinx/rpc/test/runTest.native.kt new file mode 100644 index 000000000..40ec1a7b4 --- /dev/null +++ b/tests/test-utils/src/nativeMain/kotlin/kotlinx/rpc/test/runTest.native.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +actual inline fun withDebugProbes(body: () -> T): T = body() diff --git a/krpc/krpc-test/src/nativeMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.native.kt b/tests/test-utils/src/nativeMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.native.kt similarity index 69% rename from krpc/krpc-test/src/nativeMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.native.kt rename to tests/test-utils/src/nativeMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.native.kt index 4d4073c72..7dd6ceb64 100644 --- a/krpc/krpc-test/src/nativeMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.native.kt +++ b/tests/test-utils/src/nativeMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.native.kt @@ -2,9 +2,8 @@ * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.rpc.krpc.test +package kotlinx.rpc.test -import kotlinx.coroutines.test.TestScope import kotlin.native.concurrent.ObsoleteWorkersApi import kotlin.native.concurrent.Worker @@ -12,7 +11,3 @@ import kotlin.native.concurrent.Worker actual fun runThreadIfPossible(runner: () -> Unit) { Worker.start(errorReporting = true).executeAfter(0L, runner) } - -@Suppress("detekt.EmptyFunctionBlock") -internal actual fun TestScope.debugCoroutines() { -} diff --git a/tests/test-utils/src/wasmJsMain/kotlin/kotlinx/rpc/test/runTest.wasmJs.kt b/tests/test-utils/src/wasmJsMain/kotlin/kotlinx/rpc/test/runTest.wasmJs.kt new file mode 100644 index 000000000..40ec1a7b4 --- /dev/null +++ b/tests/test-utils/src/wasmJsMain/kotlin/kotlinx/rpc/test/runTest.wasmJs.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +actual inline fun withDebugProbes(body: () -> T): T = body() diff --git a/tests/test-utils/src/wasmJsMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.wasmJs.kt b/tests/test-utils/src/wasmJsMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.wasmJs.kt new file mode 100644 index 000000000..903dbc590 --- /dev/null +++ b/tests/test-utils/src/wasmJsMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.wasmJs.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +actual inline fun runThreadIfPossible(runner: () -> Unit) = runner() diff --git a/tests/test-utils/src/wasmWasiMain/kotlin/kotlinx/rpc/test/runTest.wasmWasi.kt b/tests/test-utils/src/wasmWasiMain/kotlin/kotlinx/rpc/test/runTest.wasmWasi.kt new file mode 100644 index 000000000..40ec1a7b4 --- /dev/null +++ b/tests/test-utils/src/wasmWasiMain/kotlin/kotlinx/rpc/test/runTest.wasmWasi.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +actual inline fun withDebugProbes(body: () -> T): T = body() diff --git a/tests/test-utils/src/wasmWasiMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.wasmWasi.kt b/tests/test-utils/src/wasmWasiMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.wasmWasi.kt new file mode 100644 index 000000000..903dbc590 --- /dev/null +++ b/tests/test-utils/src/wasmWasiMain/kotlin/kotlinx/rpc/test/runThreadIfPossible.wasmWasi.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.test + +actual inline fun runThreadIfPossible(runner: () -> Unit) = runner() diff --git a/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/map/RpcInternalConcurrentHashMap.kt b/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/map/RpcInternalConcurrentHashMap.kt index dd3556ddc..d53fac2dc 100644 --- a/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/map/RpcInternalConcurrentHashMap.kt +++ b/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/map/RpcInternalConcurrentHashMap.kt @@ -32,6 +32,9 @@ public interface RpcInternalConcurrentHashMap { public val values: Collection + // ConcurrentModificationException safe + public fun withKeys(block: (Set) -> T): T + public data class Entry( val key: K, val value: V, diff --git a/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/map/SynchronizedHashMap.kt b/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/map/SynchronizedHashMap.kt index d855b8a6a..a9c42087e 100644 --- a/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/map/SynchronizedHashMap.kt +++ b/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/map/SynchronizedHashMap.kt @@ -54,4 +54,10 @@ internal class SynchronizedHashMap : RpcInternalConcurrentHashM override val values: Collection get() = synchronized(this) { map.values } + + override fun withKeys(block: (Set) -> T): T { + synchronized(this) { + return block(map.keys) + } + } } diff --git a/utils/src/jvmMain/kotlin/kotlinx/rpc/internal/utils/map/ConcurrentHashMap.jvm.kt b/utils/src/jvmMain/kotlin/kotlinx/rpc/internal/utils/map/ConcurrentHashMap.jvm.kt index 897bdc9a9..f61c4d6ac 100644 --- a/utils/src/jvmMain/kotlin/kotlinx/rpc/internal/utils/map/ConcurrentHashMap.jvm.kt +++ b/utils/src/jvmMain/kotlin/kotlinx/rpc/internal/utils/map/ConcurrentHashMap.jvm.kt @@ -52,4 +52,8 @@ private class ConcurrentHashMapJvm(initialSize: Int) : RpcInter override val values: Collection get() = map.values + + override fun withKeys(block: (Set) -> T): T { + return block(map.keys) + } } diff --git a/versions-root/libs.versions.toml b/versions-root/libs.versions.toml index 359e23181..0a50dde47 100644 --- a/versions-root/libs.versions.toml +++ b/versions-root/libs.versions.toml @@ -28,6 +28,7 @@ kover = "0.9.1" develocity = "3.19.2" common-custom-user-data = "2.3" compat-patrouille = "0.0.1" +lincheck = "3.2" [libraries] # kotlinx.rpc – references to the included builds @@ -103,6 +104,7 @@ intellij-util = { module = "com.jetbrains.intellij.platform:util", version.ref = atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } develocity = { module = "com.gradle:develocity-gradle-plugin", version.ref = "develocity" } common-custom-user-data = { module ="com.gradle:common-custom-user-data-gradle-plugin", version.ref = "common-custom-user-data" } +lincheck = { module = "org.jetbrains.lincheck:lincheck", version.ref = "lincheck" } # gradle plugins as lib deps detekt-gradle-plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt-gradle-plugin" }