@@ -9,7 +9,6 @@ package kotlinx.rpc.grpc.internal
9
9
import cnames.structs.grpc_call
10
10
import kotlinx.atomicfu.atomic
11
11
import kotlinx.cinterop.*
12
- import kotlinx.coroutines.coroutineScope
13
12
import kotlinx.coroutines.suspendCancellableCoroutine
14
13
import libgrpcpp_c.*
15
14
import platform.posix.memset
@@ -19,9 +18,20 @@ import kotlin.coroutines.resumeWithException
19
18
import kotlin.experimental.ExperimentalNativeApi
20
19
import kotlin.native.ref.createCleaner
21
20
21
+ /* *
22
+ * A coroutine wrapper around the grpc completion_queue, which manages message operations.
23
+ * It is based on the "new" callback API; therefore, there are no kotlin-side threads required to poll
24
+ * the queue.
25
+ */
22
26
internal class CompletionQueue {
23
27
24
- // internal as it must be accessible from the SHUTDOWN_CB
28
+ private enum class State { OPEN , SHUTTING_DOWN , CLOSED }
29
+
30
+ private val state = atomic(State .OPEN )
31
+
32
+ // internal as it must be accessible from the SHUTDOWN_CB,
33
+ // but it shouldn't be used from outside this file.
34
+ @Suppress(" PropertyName" )
25
35
internal val _shutdownDone = kotlinx.coroutines.CompletableDeferred <Unit >()
26
36
27
37
// used for spinning lock. false means not used (available)
@@ -40,58 +50,94 @@ internal class CompletionQueue {
40
50
private val thisStableRefCleaner = createCleaner(thisStableRef) { it.dispose() }
41
51
private val shutdownFunctorCleaner = createCleaner(shutdownFunctor) { nativeHeap.free(it) }
42
52
43
- suspend fun runBatch (call : CPointer <grpc_call>, ops : CPointer <grpc_op>, nOps : ULong ) = coroutineScope {
44
- suspendCancellableCoroutine<Unit > { cont ->
53
+ init {
54
+ // Assert grpc_iomgr_run_in_background() to guarantee that the event manager provides
55
+ // IO threads and supports the callback API.
56
+ require(kgrpc_iomgr_run_in_background()) { " The gRPC iomgr is not running background threads, required for callback based APIs." }
57
+ }
58
+
59
+ suspend fun runBatch (call : CPointer <grpc_call>, ops : CPointer <grpc_op>, nOps : ULong ) =
60
+ suspendCancellableCoroutine< grpc_call_error> { cont ->
45
61
val tag = newCbTag(cont, OPS_COMPLETE_CB )
46
62
63
+ var err = grpc_call_error.GRPC_CALL_ERROR
47
64
// synchronizes access to grpc_call_start_batch
48
- while (! batchStartGuard.compareAndSet(expect = false , update = true )) {
49
- // could not be set to true (currently hold by different thread)
50
- }
65
+ withBatchStartLock {
66
+ if (state.value != State .OPEN ) {
67
+ deleteCbTag(tag)
68
+ cont.resume(grpc_call_error.GRPC_CALL_ERROR_COMPLETION_QUEUE_SHUTDOWN )
69
+ return @suspendCancellableCoroutine
70
+ }
51
71
52
- var err: UInt
53
- try {
54
72
err = grpc_call_start_batch(call, ops, nOps, tag, null )
55
- } finally {
56
- batchStartGuard.value = false
57
73
}
58
74
59
- if (err != 0u ) {
75
+ if (err != grpc_call_error.GRPC_CALL_OK ) {
76
+ // if the call was not successful, the callback will not be invoked.
60
77
deleteCbTag(tag)
61
- cont.resumeWithException( IllegalStateException ( " start_batch err= $err " ) )
78
+ cont.resume( err)
62
79
return @suspendCancellableCoroutine
63
80
}
64
81
65
82
66
83
cont.invokeOnCancellation {
67
- this // keep reference, otherwise the cleaners might get cleaned before batch finishes
68
- TODO (" Implement call operation cancellation" )
84
+ @Suppress(" UnusedExpression" )
85
+ // keep reference, otherwise the cleaners might get invoked before the batch finishes
86
+ this
87
+ // cancel the call if one of its batches is canceled.
88
+ // grpc_call_cancel is thread-safe and can be called several times.
89
+ // the callback is invoked anyway, so the tag doesn't get deleted here.
90
+ grpc_call_cancel(call, null )
69
91
}
70
92
}
71
- }
72
93
73
94
suspend fun shutdown () {
74
- if (_shutdownDone .isCompleted) return
95
+ if (! state.compareAndSet(State .OPEN , State .SHUTTING_DOWN )) {
96
+ // the first call to shutdown() makes transition and to SHUTTING_DOWN and
97
+ // initiates shut down. all other invocations await the shutdown.
98
+ _shutdownDone .await()
99
+ return
100
+ }
101
+
102
+ // wait until all batch operations since the state transitions were started.
103
+ // this is required to prevent batches from starting after shutdown was initialized
104
+ withBatchStartLock { }
105
+
75
106
grpc_completion_queue_shutdown(raw)
76
107
_shutdownDone .await()
108
+ state.value = State .CLOSED
109
+ }
110
+
111
+ private inline fun withBatchStartLock (block : () -> Unit ) {
112
+ try {
113
+ // spin until this thread occupies the guard
114
+ @Suppress(" ControlFlowWithEmptyBody" )
115
+ while (! batchStartGuard.compareAndSet(expect = false , update = true )) {
116
+ }
117
+ block()
118
+ } finally {
119
+ // set guard to "not occupied"
120
+ batchStartGuard.value = false
121
+ }
77
122
}
78
123
}
79
124
125
+ // kq stands for kompletion_queue lol
80
126
@CName(" kq_ops_complete_cb" )
81
127
private fun opsCompleteCb (functor : CPointer <grpc_completion_queue_functor>? , ok : Int ) {
82
128
val tag = functor!! .reinterpret< grpc_cb_tag> ()
83
- val cont = tag.pointed.user_data!! .asStableRef<Continuation <Unit >>().get()
129
+ val cont = tag.pointed.user_data!! .asStableRef<Continuation <grpc_call_error >>().get()
84
130
deleteCbTag(tag)
85
- if (ok != 0 ) cont.resume(Unit ) else cont.resumeWithException(IllegalStateException (" batch failed" ))
131
+ if (ok != 0 ) cont.resume(grpc_call_error.GRPC_CALL_OK )
132
+ else cont.resumeWithException(IllegalStateException (" batch failed" ))
86
133
}
87
134
88
135
@CName(" kq_shutdown_cb" )
89
136
private fun shutdownCb (functor : CPointer <grpc_completion_queue_functor>? , ok : Int ) {
90
137
val tag = functor!! .reinterpret< grpc_cb_tag> ()
91
138
val cq = tag.pointed.user_data!! .asStableRef<CompletionQueue >().get()
92
- check(ok != 0 ) { " CQ shutdown failed" }
93
- grpc_completion_queue_destroy(cq.raw)
94
139
cq._shutdownDone .complete(Unit )
140
+ grpc_completion_queue_destroy(cq.raw)
95
141
}
96
142
97
143
private val OPS_COMPLETE_CB = staticCFunction(::opsCompleteCb)
0 commit comments