@@ -81,7 +81,7 @@ internal class NativeClientCall<Request, Response>(
81
81
private var listener: Listener <Response >? = null
82
82
private var halfClosed = false
83
83
private var cancelled = false
84
- private var closed = atomic(false )
84
+ private val closed = atomic(false )
85
85
86
86
// tracks how many operations are in flight (not yet completed by the listener).
87
87
// if 0 and we got a closeInfo (containing the status), there are no more ongoing operations.
@@ -133,7 +133,9 @@ internal class NativeClientCall<Request, Response>(
133
133
val lst = checkNotNull(listener) { internalError(" Not yet started" ) }
134
134
// allows the managed channel to join for the call to finish.
135
135
callJob.complete()
136
- lst.onClose(info.first, info.second)
136
+ safeUserCode(" Failed to call onClose." ) {
137
+ lst.onClose(info.first, info.second)
138
+ }
137
139
}
138
140
}
139
141
@@ -142,9 +144,8 @@ internal class NativeClientCall<Request, Response>(
142
144
* This is called as soon as the RECV_STATUS_ON_CLIENT batch (started with [startRecvStatus]) finished.
143
145
*/
144
146
private fun markClosePending (status : Status , trailers : GrpcTrailers ) {
145
- if (closeInfo.compareAndSet(null , Pair (status, trailers))) {
146
- tryToCloseCall()
147
- }
147
+ closeInfo.compareAndSet(null , Pair (status, trailers))
148
+ tryToCloseCall()
148
149
}
149
150
150
151
/* *
@@ -153,7 +154,9 @@ internal class NativeClientCall<Request, Response>(
153
154
*/
154
155
private fun turnReady () {
155
156
if (ready.compareAndSet(expect = false , update = true )) {
156
- listener?.onReady()
157
+ safeUserCode(" Failed to call onClose." ) {
158
+ listener?.onReady()
159
+ }
157
160
}
158
161
}
159
162
@@ -163,7 +166,6 @@ internal class NativeClientCall<Request, Response>(
163
166
headers : GrpcTrailers ,
164
167
) {
165
168
check(listener == null ) { internalError(" Already started" ) }
166
- check(! cancelled) { internalError(" Already cancelled." ) }
167
169
168
170
listener = responseListener
169
171
@@ -254,7 +256,8 @@ internal class NativeClientCall<Request, Response>(
254
256
is BatchResult .Submitted -> {
255
257
callResult.future.onComplete {
256
258
val details = statusDetails.toByteArray().toKString()
257
- val status = Status (statusCode.value.toKotlin(), details, null )
259
+ val kStatusCode = statusCode.value.toKotlin()
260
+ val status = Status (kStatusCode, details, null )
258
261
val trailers = GrpcTrailers ()
259
262
260
263
// cleanup
@@ -306,7 +309,9 @@ internal class NativeClientCall<Request, Response>(
306
309
grpc_metadata_array_destroy(meta.ptr)
307
310
arena.clear()
308
311
}) {
309
- // TODO: Send headers to listener
312
+ safeUserCode(" Failed to call onHeaders." ) {
313
+ listener?.onHeaders(GrpcTrailers ())
314
+ }
310
315
}
311
316
}
312
317
@@ -319,7 +324,10 @@ internal class NativeClientCall<Request, Response>(
319
324
// limit numMessages to prevent potential stack overflows
320
325
check(numMessages <= 16 ) { internalError(" numMessages must be <= 16" ) }
321
326
val listener = checkNotNull(listener) { internalError(" Not yet started" ) }
322
- check(! cancelled) { internalError(" Already cancelled" ) }
327
+ if (cancelled) {
328
+ // no need to send message if the call got already cancelled.
329
+ return
330
+ }
323
331
324
332
var remainingMessages = numMessages
325
333
@@ -342,7 +350,9 @@ internal class NativeClientCall<Request, Response>(
342
350
val buf = recvPtr.value ? : return @runBatch
343
351
val msg = methodDescriptor.getResponseMarshaller()
344
352
.parse(buf.toKotlin().asInputStream())
345
- listener.onMessage(msg)
353
+ safeUserCode(" Failed to call onClose." ) {
354
+ listener.onMessage(msg)
355
+ }
346
356
post()
347
357
}
348
358
}
@@ -353,8 +363,11 @@ internal class NativeClientCall<Request, Response>(
353
363
354
364
override fun cancel (message : String? , cause : Throwable ? ) {
355
365
cancelled = true
356
- val message = if (cause != null ) " $message : ${cause.message} " else message
357
- cancelInternal(grpc_status_code.GRPC_STATUS_CANCELLED , message ? : " Call cancelled" )
366
+ val status = Status (StatusCode .CANCELLED , message ? : " Call cancelled" , cause)
367
+ // user side cancellation must always win over any other status (even if the call is already completed).
368
+ // this will also preserve the cancellation cause, which cannot be passed to the grpc-core.
369
+ closeInfo.value = Pair (status, GrpcTrailers ())
370
+ cancelInternal(grpc_status_code.GRPC_STATUS_CANCELLED , message ? : " Call cancelled with cause: ${cause?.message} " )
358
371
}
359
372
360
373
private fun cancelInternal (statusCode : grpc_status_code, message : String ) {
@@ -366,7 +379,7 @@ internal class NativeClientCall<Request, Response>(
366
379
367
380
override fun halfClose () {
368
381
check(! halfClosed) { internalError(" Already half closed." ) }
369
- check( ! cancelled) { internalError( " Already cancelled. " ) }
382
+ if ( cancelled) return
370
383
halfClosed = true
371
384
372
385
val arena = Arena ()
@@ -384,9 +397,10 @@ internal class NativeClientCall<Request, Response>(
384
397
override fun sendMessage (message : Request ) {
385
398
checkNotNull(listener) { internalError(" Not yet started" ) }
386
399
check(! halfClosed) { internalError(" Already half closed." ) }
387
- check(! cancelled) { internalError(" Already cancelled." ) }
388
400
check(isReady()) { internalError(" Not yet ready." ) }
389
401
402
+ if (cancelled) return
403
+
390
404
// set ready false, as only one message can be sent at a time.
391
405
ready.value = false
392
406
@@ -408,6 +422,18 @@ internal class NativeClientCall<Request, Response>(
408
422
turnReady()
409
423
}
410
424
}
425
+
426
+ /* *
427
+ * Safely executes the provided block of user code, catching any thrown exceptions or errors.
428
+ * If an exception is caught, it cancels the operation with the specified message and cause.
429
+ */
430
+ private fun safeUserCode (cancelMsg : String , block : () -> Unit ) {
431
+ try {
432
+ block()
433
+ } catch (e: Throwable ) {
434
+ cancel(cancelMsg, e)
435
+ }
436
+ }
411
437
}
412
438
413
439
0 commit comments