diff --git a/CHANGELOG.md b/CHANGELOG.md index a19eaa17..a55b5ad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ ## Change Log +### 1.4.1 + +#### Changes + +* fix(ServerCalls): Fix regression in Status cause for exceptions thrown by implementations by @andrewparmet in https://github.com/grpc/grpc-kotlin/pull/456 +* Bazel: update to use guava 32.0.1 consistently. by @brettchabot in https://github.com/grpc/grpc-kotlin/pull/436 +* Bump composeVersion from 1.5.1 to 1.5.2 in /examples by @dependabot in https://github.com/grpc/grpc-kotlin/pull/438 +* Bump org.gradle.test-retry from 1.5.5 to 1.5.6 by @dependabot in https://github.com/grpc/grpc-kotlin/pull/439 +* Bump androidx.activity:activity-compose from 1.7.2 to 1.8.0 in /examples by @dependabot in https://github.com/grpc/grpc-kotlin/pull/441 +* Bump composeVersion from 1.5.2 to 1.5.3 in /examples by @dependabot in https://github.com/grpc/grpc-kotlin/pull/440 +* Bump org.jetbrains.dokka from 1.9.0 to 1.9.10 by @dependabot in https://github.com/grpc/grpc-kotlin/pull/446 +* Bump org.jlleitschuh.gradle.ktlint from 11.6.0 to 11.6.1 in /examples by @dependabot in https://github.com/grpc/grpc-kotlin/pull/443 +* Bump com.google.guava:guava from 32.1.2-jre to 32.1.3-jre by @dependabot in https://github.com/grpc/grpc-kotlin/pull/445 +* FIXED: Missing double quotes in compiler docs leading to error. by @prodbyola in https://github.com/grpc/grpc-kotlin/pull/453 +* Bump androidx.activity:activity-compose from 1.8.0 to 1.8.1 in /examples by @dependabot in https://github.com/grpc/grpc-kotlin/pull/457 +* Bump composeVersion from 1.5.3 to 1.5.4 in /examples by @dependabot in https://github.com/grpc/grpc-kotlin/pull/447 +* Bump jvm from 1.9.10 to 1.9.20 in /examples by @dependabot in https://github.com/grpc/grpc-kotlin/pull/450 +* Bump org.junit.jupiter:junit-jupiter-engine from 5.10.0 to 5.10.1 by @dependabot in https://github.com/grpc/grpc-kotlin/pull/454 + +#### New Contributors +* @brettchabot made their first contribution in https://github.com/grpc/grpc-kotlin/pull/436 +* @prodbyola made their first contribution in https://github.com/grpc/grpc-kotlin/pull/453 +* @andrewparmet made their first contribution in https://github.com/grpc/grpc-kotlin/pull/456 + +**Full Changelog**: https://github.com/grpc/grpc-kotlin/compare/v1.4.0...v1.4.1 + + ### 1.4.0 #### Changes diff --git a/build.gradle.kts b/build.gradle.kts index 5c63759a..c7692df7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,12 +5,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") version "1.8.0" apply false id("com.google.protobuf") version "0.9.4" apply false - id("org.gradle.test-retry") version "1.5.5" + id("org.gradle.test-retry") version "1.5.6" id("io.github.gradle-nexus.publish-plugin") version "1.3.0" } group = "io.grpc" -version = "1.4.0" // CURRENT_GRPC_KOTLIN_VERSION +version = "1.4.1" // CURRENT_GRPC_KOTLIN_VERSION ext["grpcVersion"] = "1.57.2" ext["protobufVersion"] = "3.24.1" diff --git a/compiler/README.md b/compiler/README.md index 05613a91..b2e67f90 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -178,7 +178,7 @@ that just runs that `jar`, for example create a file named `protoc-gen-grpc-kotl #!/usr/bin/env sh DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -java -jar $DIR/protoc-gen-grpc-kotlin-SOME_VERSION-jdk8.jar "$@ +java -jar $DIR/protoc-gen-grpc-kotlin-SOME_VERSION-jdk8.jar "$@" ``` Then make that file executable: diff --git a/compiler/build.gradle.kts b/compiler/build.gradle.kts index 275a4e83..afb3ba40 100644 --- a/compiler/build.gradle.kts +++ b/compiler/build.gradle.kts @@ -18,16 +18,16 @@ dependencies { // Misc implementation(kotlin("reflect")) - implementation("com.squareup:kotlinpoet:1.14.2") + implementation("com.squareup:kotlinpoet:1.15.1") implementation("com.google.truth:truth:1.1.5") // Testing testImplementation("junit:junit:4.13.2") - testImplementation("com.google.guava:guava:32.1.2-jre") + testImplementation("com.google.guava:guava:32.1.3-jre") testImplementation("com.google.jimfs:jimfs:1.3.0") testImplementation("com.google.protobuf:protobuf-gradle-plugin:0.9.4") testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") - testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.1") testImplementation("org.mockito:mockito-core:4.11.0") } diff --git a/examples/android/build.gradle.kts b/examples/android/build.gradle.kts index 15ba0c7e..0d5a33dc 100644 --- a/examples/android/build.gradle.kts +++ b/examples/android/build.gradle.kts @@ -3,12 +3,12 @@ plugins { kotlin("android") } -val composeVersion = "1.5.1" -val composeCompilerVersion = "1.5.3" +val composeVersion = "1.5.4" +val composeCompilerVersion = "1.5.4" dependencies { implementation(project(":stub-android")) - implementation("androidx.activity:activity-compose:1.7.2") + implementation("androidx.activity:activity-compose:1.8.1") implementation("androidx.compose.foundation:foundation-layout:$composeVersion") implementation("androidx.compose.material:material:$composeVersion") implementation("androidx.compose.runtime:runtime:$composeVersion") diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index 5a79c0a3..1406b701 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -1,13 +1,13 @@ plugins { id("com.android.application") version "8.1.1" apply false id("com.google.protobuf") version "0.9.4" apply false - kotlin("jvm") version "1.9.10" apply false - id("org.jlleitschuh.gradle.ktlint") version "11.6.0" apply false + kotlin("jvm") version "1.9.20" apply false + id("org.jlleitschuh.gradle.ktlint") version "11.6.1" apply false } // todo: move to subprojects, but how? ext["grpcVersion"] = "1.57.2" -ext["grpcKotlinVersion"] = "1.4.0" // CURRENT_GRPC_KOTLIN_VERSION +ext["grpcKotlinVersion"] = "1.4.1" // CURRENT_GRPC_KOTLIN_VERSION ext["protobufVersion"] = "3.24.1" ext["coroutinesVersion"] = "1.7.3" diff --git a/stub/build.gradle.kts b/stub/build.gradle.kts index 1e4ea4d6..03ba82a3 100644 --- a/stub/build.gradle.kts +++ b/stub/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.dokka.gradle.DokkaTask import java.net.URL plugins { - id("org.jetbrains.dokka") version "1.9.0" + id("org.jetbrains.dokka") version "1.9.10" } repositories { @@ -27,7 +27,7 @@ dependencies { // Testing testImplementation("junit:junit:4.13.2") - testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.1") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:${rootProject.ext["coroutinesVersion"]}") testImplementation("com.google.truth.extensions:truth-proto-extension:1.1.5") testImplementation("io.grpc:grpc-protobuf:${rootProject.ext["grpcVersion"]}") diff --git a/stub/src/main/java/io/grpc/kotlin/ServerCalls.kt b/stub/src/main/java/io/grpc/kotlin/ServerCalls.kt index 404990da..d2b532b4 100644 --- a/stub/src/main/java/io/grpc/kotlin/ServerCalls.kt +++ b/stub/src/main/java/io/grpc/kotlin/ServerCalls.kt @@ -26,13 +26,13 @@ import io.grpc.ServerCallHandler import io.grpc.ServerMethodDefinition import io.grpc.Status import io.grpc.StatusException +import io.grpc.StatusRuntimeException import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.onFailure import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -40,7 +40,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.channels.onFailure import io.grpc.Metadata as GrpcMetadata /** @@ -262,6 +261,7 @@ object ServerCalls { val closeStatus = when (failure) { null -> Status.OK is CancellationException -> Status.CANCELLED.withCause(failure) + is StatusException, is StatusRuntimeException -> Status.fromThrowable(failure) else -> Status.fromThrowable(failure).withCause(failure) } val trailers = failure?.let { Status.trailersFromThrowable(it) } ?: GrpcMetadata() diff --git a/stub/src/test/java/io/grpc/kotlin/ServerCallsTest.kt b/stub/src/test/java/io/grpc/kotlin/ServerCallsTest.kt index 4996c2aa..9ed61a66 100644 --- a/stub/src/test/java/io/grpc/kotlin/ServerCallsTest.kt +++ b/stub/src/test/java/io/grpc/kotlin/ServerCallsTest.kt @@ -20,12 +20,10 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.ProtoTruth.assertThat import io.grpc.* import io.grpc.examples.helloworld.GreeterGrpc -import io.grpc.examples.helloworld.GreeterGrpcKt import io.grpc.examples.helloworld.HelloReply import io.grpc.examples.helloworld.HelloRequest import io.grpc.examples.helloworld.GreeterGrpcKt.GreeterCoroutineStub import io.grpc.examples.helloworld.GreeterGrpcKt.GreeterCoroutineImplBase -import io.grpc.stub.StreamObserver import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -1015,7 +1013,8 @@ class ServerCallsTest : AbstractCallsTest() { } @Test - fun testStatusExceptionPropagatesStack() = runBlocking { + fun testPropagateStackTraceForStatusException() = runBlocking { + val thrownStatusCause = CompletableDeferred() val serverImpl = object : GreeterCoroutineImplBase() { override suspend fun sayHello(request: HelloRequest): HelloReply { @@ -1023,7 +1022,9 @@ class ServerCallsTest : AbstractCallsTest() { } private fun internalServerCall(): Nothing { - throw StatusException(Status.INTERNAL) + val exception = Exception("causal exception") + thrownStatusCause.complete(exception) + throw Status.INTERNAL.withCause(exception).asException() } } @@ -1059,7 +1060,163 @@ class ServerCallsTest : AbstractCallsTest() { assertThat(clientException.status.code).isEqualTo(Status.Code.INTERNAL) val statusCause = receivedStatusCause.await() // but the exception should propagate to server interceptors, with stack trace intact - assertThat(statusCause).isNotNull() + assertThat(statusCause).isEqualTo(thrownStatusCause.await()) + assertThat(statusCause!!.stackTraceToString()).contains("internalServerCall") + } + + @Test + fun testPropagateStackTraceForStatusRuntimeException() = runBlocking { + val thrownStatusCause = CompletableDeferred() + + val serverImpl = object : GreeterCoroutineImplBase() { + override suspend fun sayHello(request: HelloRequest): HelloReply { + internalServerCall() + } + + private fun internalServerCall(): Nothing { + val exception = Exception("causal exception") + thrownStatusCause.complete(exception) + throw Status.INTERNAL.withCause(exception).asRuntimeException() + } + } + + val receivedStatusCause = CompletableDeferred() + + val interceptor = object : ServerInterceptor { + override fun interceptCall( + call: ServerCall, + requestHeaders: Metadata, + next: ServerCallHandler + ): ServerCall.Listener = + next.startCall( + object : ForwardingServerCall.SimpleForwardingServerCall(call) { + override fun close(status: Status, trailers: Metadata) { + receivedStatusCause.complete(status.cause) + super.close(status, trailers) + } + }, + requestHeaders + ) + } + + val channel = makeChannel(serverImpl, interceptor) + + val stub = GreeterGrpc.newBlockingStub(channel) + val clientException = assertThrows { + stub.sayHello(helloRequest("")) + } + + // the exception should not propagate to the client + assertThat(clientException.cause).isNull() + + assertThat(clientException.status.code).isEqualTo(Status.Code.INTERNAL) + val statusCause = receivedStatusCause.await() + // but the exception should propagate to server interceptors, with stack trace intact + assertThat(statusCause).isEqualTo(thrownStatusCause.await()) + assertThat(statusCause!!.stackTraceToString()).contains("internalServerCall") + } + + @Test + fun testPropagateStackTraceForNonStatusException() = runBlocking { + val thrownStatusCause = CompletableDeferred() + + val serverImpl = object : GreeterCoroutineImplBase() { + override suspend fun sayHello(request: HelloRequest): HelloReply { + internalServerCall() + } + + private fun internalServerCall(): Nothing { + val exception = Exception("causal exception") + thrownStatusCause.complete(exception) + throw exception + } + } + + val receivedStatusCause = CompletableDeferred() + + val interceptor = object : ServerInterceptor { + override fun interceptCall( + call: ServerCall, + requestHeaders: Metadata, + next: ServerCallHandler + ): ServerCall.Listener = + next.startCall( + object : ForwardingServerCall.SimpleForwardingServerCall(call) { + override fun close(status: Status, trailers: Metadata) { + receivedStatusCause.complete(status.cause) + super.close(status, trailers) + } + }, + requestHeaders + ) + } + + val channel = makeChannel(serverImpl, interceptor) + + val stub = GreeterGrpc.newBlockingStub(channel) + val clientException = assertThrows { + stub.sayHello(helloRequest("")) + } + + // the exception should not propagate to the client + assertThat(clientException.cause).isNull() + + assertThat(clientException.status.code).isEqualTo(Status.Code.UNKNOWN) + val statusCause = receivedStatusCause.await() + // but the exception should propagate to server interceptors, with stack trace intact + assertThat(statusCause).isEqualTo(thrownStatusCause.await()) + assertThat(statusCause!!.stackTraceToString()).contains("internalServerCall") + } + + @Test + fun testPropagateStackTraceForNonStatusExceptionWithStatusExceptionCause() = runBlocking { + val thrownStatusCause = CompletableDeferred() + + val serverImpl = object : GreeterCoroutineImplBase() { + override suspend fun sayHello(request: HelloRequest): HelloReply { + internalServerCall() + } + + private fun internalServerCall(): Nothing { + val exception = Exception("causal exception", Status.INTERNAL.asException()) + thrownStatusCause.complete(exception) + throw exception + } + } + + val receivedStatusCause = CompletableDeferred() + + val interceptor = object : ServerInterceptor { + override fun interceptCall( + call: ServerCall, + requestHeaders: Metadata, + next: ServerCallHandler + ): ServerCall.Listener = + next.startCall( + object : ForwardingServerCall.SimpleForwardingServerCall(call) { + override fun close(status: Status, trailers: Metadata) { + receivedStatusCause.complete(status.cause) + super.close(status, trailers) + } + }, + requestHeaders + ) + } + + val channel = makeChannel(serverImpl, interceptor) + + val stub = GreeterGrpc.newBlockingStub(channel) + val clientException = assertThrows { + stub.sayHello(helloRequest("")) + } + + // the exception should not propagate to the client + assertThat(clientException.cause).isNull() + + assertThat(clientException.status.code).isEqualTo(Status.Code.INTERNAL) + val statusCause = receivedStatusCause.await() + // but the exception should propagate to server interceptors, with stack trace intact + assertThat(statusCause).isEqualTo(thrownStatusCause.await()) assertThat(statusCause!!.stackTraceToString()).contains("internalServerCall") }