Skip to content

Commit ba8fca7

Browse files
authored
grpc: Status exception API refactoring (#536)
* grpc: Refactor Status and StatusException * grpc: Add documentation
1 parent fa02074 commit ba8fca7

File tree

9 files changed

+111
-32
lines changed

9 files changed

+111
-32
lines changed

grpc/grpc-client/src/commonMain/kotlin/kotlinx/rpc/grpc/client/internal/suspendClientCalls.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import kotlinx.rpc.grpc.GrpcMetadata
2222
import kotlinx.rpc.grpc.Status
2323
import kotlinx.rpc.grpc.StatusCode
2424
import kotlinx.rpc.grpc.StatusException
25+
import kotlinx.rpc.grpc.cause
2526
import kotlinx.rpc.grpc.client.ClientCallScope
2627
import kotlinx.rpc.grpc.client.GrpcCallOptions
2728
import kotlinx.rpc.grpc.client.GrpcClient
@@ -284,7 +285,7 @@ private class ClientCallScopeImpl<Request, Response>(
284285
onClose = { status: Status, trailers: GrpcMetadata ->
285286
var cause = when {
286287
status.statusCode == StatusCode.OK -> null
287-
status.getCause() is CancellationException -> status.getCause()
288+
status.cause is CancellationException -> status.cause
288289
else -> StatusException(status, trailers)
289290
}
290291

grpc/grpc-client/src/nativeMain/kotlin/kotlinx/rpc/grpc/client/GrpcCallCredentials.native.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import kotlinx.rpc.grpc.GrpcMetadata
1717
import kotlinx.rpc.grpc.StatusException
1818
import kotlinx.rpc.grpc.internal.destroyEntries
1919
import kotlinx.rpc.grpc.internal.toRaw
20+
import kotlinx.rpc.grpc.status
2021
import kotlinx.rpc.grpc.statusCode
2122
import libkgrpc.*
2223
import platform.posix.size_tVar
@@ -87,7 +88,7 @@ private fun getMetadataCallback(
8788
}
8889
notifyResult(metadata, grpc_status_code.GRPC_STATUS_OK, null)
8990
} catch (e: StatusException) {
90-
notifyResult(metadata, e.getStatus().statusCode.toRaw(), e.message)
91+
notifyResult(metadata, e.status.statusCode.toRaw(), e.message)
9192
} catch (e: CancellationException) {
9293
notifyResult(metadata, grpc_status_code.GRPC_STATUS_CANCELLED, e.message)
9394
throw e

grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
package kotlinx.rpc.grpc
88

9+
import kotlinx.rpc.internal.utils.InternalRpcApi
10+
911
/**
1012
* Defines the status of an operation by providing a standard [StatusCode] in conjunction with an
1113
* optional descriptive message.
@@ -26,14 +28,39 @@ package kotlinx.rpc.grpc
2628
* [doc/statuscodes.md](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md)
2729
*/
2830
public expect class Status {
29-
public fun getDescription(): String?
30-
public fun getCause(): Throwable?
31+
internal fun getDescription(): String?
32+
internal fun getCause(): Throwable?
3133
}
3234

35+
/**
36+
* Creates a [Status] with the specified [code], optional [description], and [cause].
37+
*/
3338
public expect fun Status(code: StatusCode, description: String? = null, cause: Throwable? = null): Status
3439

40+
/**
41+
* The status code of this status.
42+
*/
3543
public expect val Status.statusCode: StatusCode
3644

45+
/**
46+
* The description of this status, or null if not present.
47+
*/
48+
public val Status.description: String? get() = getDescription()
49+
50+
// this is currently @InternalRpcApi as it's behavior would be inconsistent between JVM and Native.
51+
@InternalRpcApi
52+
public val Status.cause: Throwable? get() = getCause()
53+
54+
/**
55+
* Converts this status to a [StatusException] with optional [trailers].
56+
*/
57+
public fun Status.asException(trailers: GrpcMetadata? = null): StatusException {
58+
return StatusException(this, trailers)
59+
}
60+
61+
/**
62+
* Standard gRPC status codes.
63+
*/
3764
public enum class StatusCode(public val value: Int) {
3865
OK(0),
3966
CANCELLED(1),
@@ -53,5 +80,23 @@ public enum class StatusCode(public val value: Int) {
5380
DATA_LOSS(15),
5481
UNAUTHENTICATED(16);
5582

83+
/**
84+
* The ASCII-encoded byte representation of the status code value.
85+
*/
5686
public val valueAscii: ByteArray = value.toString().encodeToByteArray()
87+
88+
/**
89+
* Converts this status code to a [Status] with an optional [description].
90+
*/
91+
public fun asStatus(description: String? = null): Status {
92+
return Status(this, description)
93+
}
94+
95+
/**
96+
* Converts this status code to a [StatusException] with optional [description] and [trailers].
97+
*/
98+
public fun asException(description: String? = null, trailers: GrpcMetadata? = null): StatusException {
99+
return StatusException(Status(this, description), trailers)
100+
}
57101
}
102+

grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusException.kt

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,56 @@
44

55
package kotlinx.rpc.grpc
66

7+
import kotlinx.rpc.internal.utils.InternalRpcApi
8+
79
/**
8-
* [Status] in Exception form, for propagating Status information via exceptions.
10+
* Exception used for propagating gRPC status information in non-OK results.
11+
*
12+
* This is the primary mechanism for reporting and handling errors in gRPC calls.
13+
* When a server encounters an error, it typically throws a [StatusException] with an appropriate
14+
* [StatusCode] to signal the failure to the client. Clients receive this exception when
15+
* remote calls fail with a non-OK status.
16+
*
17+
* The easiest way to construct a [StatusException] is to use the [StatusCode.asException] extension function:
18+
* ```
19+
* throw StatusCode.UNAUTHORIZED.asException("Authentication failed")
20+
* ```
21+
*
22+
* @see Status
23+
* @see StatusCode
924
*/
1025
public expect class StatusException : Exception {
1126
public constructor(status: Status)
1227
public constructor(status: Status, trailers: GrpcMetadata?)
1328

14-
public fun getStatus(): Status
15-
public fun getTrailers(): GrpcMetadata?
29+
internal fun getStatus(): Status
30+
internal fun getTrailers(): GrpcMetadata?
1631
}
1732

33+
/**
34+
* The status associated with this exception.
35+
*/
36+
public val StatusException.status: Status get() = getStatus()
37+
38+
/**
39+
* The trailing metadata associated with this exception, or null if not present.
40+
*/
41+
public val StatusException.trailers: GrpcMetadata? get() = getTrailers()
42+
43+
@InternalRpcApi
1844
public expect class StatusRuntimeException : RuntimeException {
19-
public constructor(status: Status)
20-
public constructor(status: Status, trailers: GrpcMetadata?)
45+
internal constructor(status: Status, trailers: GrpcMetadata?)
2146

22-
public fun getStatus(): Status
23-
public fun getTrailers(): GrpcMetadata?
47+
internal fun getStatus(): Status
48+
internal fun getTrailers(): GrpcMetadata?
2449
}
50+
51+
@InternalRpcApi
52+
public fun StatusRuntimeException(code: StatusCode, description: String? = null, trailers: GrpcMetadata? = null): StatusRuntimeException {
53+
return StatusRuntimeException(Status(code, description), trailers)
54+
}
55+
56+
@InternalRpcApi
57+
public val StatusRuntimeException.status: Status get() = getStatus()
58+
@InternalRpcApi
59+
public val StatusRuntimeException.trailers: GrpcMetadata? get() = getTrailers()

grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/suspendUtils.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.single
1212
import kotlinx.rpc.grpc.Status
1313
import kotlinx.rpc.grpc.StatusCode
1414
import kotlinx.rpc.grpc.StatusException
15+
import kotlinx.rpc.grpc.asException
1516
import kotlinx.rpc.internal.utils.InternalRpcApi
1617

1718
@InternalRpcApi
@@ -25,16 +26,12 @@ public fun <T> Flow<T>.singleOrStatusFlow(
2526
found = true
2627
emit(it)
2728
} else {
28-
throw StatusException(
29-
Status(StatusCode.INTERNAL, "Expected one $expected for $descriptor but received two")
30-
)
29+
throw StatusCode.INTERNAL.asException("Expected one $expected for $descriptor but received two")
3130
}
3231
}
3332

3433
if (!found) {
35-
throw StatusException(
36-
Status(StatusCode.INTERNAL, "Expected one $expected for $descriptor but received none")
37-
)
34+
throw StatusCode.INTERNAL.asException("Expected one $expected for $descriptor but received none")
3835
}
3936
}
4037

grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/test/proto/GrpcCallCredentialsTest.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ package kotlinx.rpc.grpc.test.proto
77
import kotlinx.coroutines.CompletableDeferred
88
import kotlinx.rpc.RpcServer
99
import kotlinx.rpc.grpc.GrpcMetadata
10-
import kotlinx.rpc.grpc.Status
1110
import kotlinx.rpc.grpc.StatusCode
12-
import kotlinx.rpc.grpc.StatusException
1311
import kotlinx.rpc.grpc.append
1412
import kotlinx.rpc.grpc.buildGrpcMetadata
1513
import kotlinx.rpc.grpc.client.GrpcCallCredentials
@@ -26,6 +24,7 @@ import kotlinx.rpc.grpc.test.SERVER_CERT_PEM
2624
import kotlinx.rpc.grpc.test.SERVER_KEY_PEM
2725
import kotlinx.rpc.grpc.test.assertGrpcFailure
2826
import kotlinx.rpc.grpc.test.invoke
27+
import kotlinx.rpc.grpc.asException
2928
import kotlinx.rpc.registerService
3029
import kotlinx.rpc.withService
3130
import kotlin.coroutines.cancellation.CancellationException
@@ -338,7 +337,7 @@ abstract class PlaintextCallCredentials : GrpcCallCredentials {
338337
}
339338

340339
class ThrowingCallCredentials(
341-
private val exception: Throwable = StatusException(Status(StatusCode.UNIMPLEMENTED, "This is my custom exception"))
340+
private val exception: Throwable = StatusCode.UNIMPLEMENTED.asException("This is my custom exception")
342341
) : PlaintextCallCredentials() {
343342
override suspend fun Context.getRequestMetadata(): GrpcMetadata {
344343
throw exception

grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusException.native.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,24 @@ public actual class StatusException : Exception {
1818
this.trailers = trailers
1919
}
2020

21-
public actual fun getStatus(): Status = status
21+
internal actual fun getStatus(): Status = status
2222

23-
public actual fun getTrailers(): GrpcMetadata? = trailers
23+
internal actual fun getTrailers(): GrpcMetadata? = trailers
2424
}
2525

2626
public actual class StatusRuntimeException : RuntimeException {
2727
private val status: Status
2828
private val trailers: GrpcMetadata?
2929

30-
public actual constructor(status: Status) : this(status, null)
31-
32-
public actual constructor(status: Status, trailers: GrpcMetadata?) : super(
30+
internal actual constructor(status: Status, trailers: GrpcMetadata?) : super(
3331
"${status.statusCode}: ${status.getDescription()}",
3432
status.getCause()
3533
) {
3634
this.status = status
3735
this.trailers = trailers
3836
}
3937

40-
public actual fun getStatus(): Status = status
38+
internal actual fun getStatus(): Status = status
4139

42-
public actual fun getTrailers(): GrpcMetadata? = trailers
40+
internal actual fun getTrailers(): GrpcMetadata? = trailers
4341
}

grpc/grpc-server/src/commonMain/kotlin/kotlinx/rpc/grpc/server/internal/suspendServerCalls.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import kotlinx.rpc.grpc.internal.singleOrStatusFlow
3030
import kotlinx.rpc.grpc.merge
3131
import kotlinx.rpc.grpc.server.ServerCallScope
3232
import kotlinx.rpc.grpc.server.ServerInterceptor
33+
import kotlinx.rpc.grpc.status
34+
import kotlinx.rpc.grpc.trailers
3335
import kotlinx.rpc.internal.utils.InternalRpcApi
3436
import kotlin.reflect.KType
3537
import kotlin.reflect.typeOf
@@ -206,17 +208,17 @@ private fun <Request, Response> CoroutineScope.serverCallListenerImpl(
206208
val closeStatus = when (failure) {
207209
null -> Status(StatusCode.OK)
208210
is CancellationException -> Status(StatusCode.CANCELLED, cause = failure)
209-
is StatusException -> failure.getStatus()
210-
is StatusRuntimeException -> failure.getStatus()
211+
is StatusException -> failure.status
212+
is StatusRuntimeException -> failure.status
211213
else -> Status(StatusCode.UNKNOWN, cause = failure)
212214
}
213215

214216
val trailers = serverCallScope.responseTrailers
215217

216218
// we merge the failure trailers with the user-defined trailers
217219
when (failure) {
218-
is StatusException -> failure.getTrailers()
219-
is StatusRuntimeException -> failure.getTrailers()
220+
is StatusException -> failure.trailers
221+
is StatusRuntimeException -> failure.trailers
220222
else -> null
221223
}?.let { trailers.merge(it) }
222224

grpc/grpc-server/src/nativeMain/kotlin/kotlinx/rpc/grpc/server/internal/NativeServerCall.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import kotlinx.rpc.grpc.internal.toGrpcByteBuffer
3636
import kotlinx.rpc.grpc.internal.toGrpcSlice
3737
import kotlinx.rpc.grpc.internal.toKotlin
3838
import kotlinx.rpc.grpc.internal.toRaw
39+
import kotlinx.rpc.grpc.status
3940
import kotlinx.rpc.grpc.statusCode
4041
import kotlinx.rpc.protobuf.input.stream.asInputStream
4142
import kotlinx.rpc.protobuf.input.stream.asSource
@@ -363,7 +364,7 @@ internal class NativeServerCall<Request, Response>(
363364
} catch (e: Throwable) {
364365
// TODO: Log internal error as warning
365366
val status = when (e) {
366-
is StatusException -> e.getStatus()
367+
is StatusException -> e.status
367368
else -> Status(
368369
StatusCode.INTERNAL,
369370
description = "Internal error, so canceling the stream",

0 commit comments

Comments
 (0)