Skip to content

Commit c8c62c1

Browse files
committed
grpc-native: Write docs
Signed-off-by: Johannes Zottele <[email protected]>
1 parent 192ddcf commit c8c62c1

File tree

5 files changed

+137
-15
lines changed

5 files changed

+137
-15
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import kotlinx.rpc.internal.utils.InternalRpcApi
1616
* Callback execution:
1717
* - On JVM it is guaranteed that callbacks aren't executed concurrently.
1818
* - On Native, it is only guaranteed that `onClose` is called after all other callbacks finished.
19+
*
20+
* Sending message readiness:
21+
* - On JVM, it is possible to call [sendMessage] multiple times, without checking [isReady].
22+
* Internally, it buffers the messages.
23+
* - On Native, you can only call [sendMessage] when [isReady] returns true. There is no buffering; therefore,
24+
* only one message can be sent at a time.
1925
*/
2026
@InternalRpcApi
2127
public expect abstract class ClientCall<Request, Response> {
Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import kotlinx.rpc.grpc.internal.*
1414
import kotlin.test.Test
1515
import kotlin.test.assertEquals
1616

17-
class CancellationClientTest {
17+
/**
18+
* Tests for JVM and Native clients.
19+
*/
20+
class RawClientTest {
1821

1922
@Test
2023
fun unaryEchoTest() = runTest(
@@ -39,8 +42,8 @@ class CancellationClientTest {
3942
}
4043

4144
@Test
42-
fun clientStreamingTest() = runTest(
43-
methodName = "ServerStreamingEcho",
45+
fun clientStreamingEchoTest() = runTest(
46+
methodName = "ClientStreamingEcho",
4447
type = MethodType.CLIENT_STREAMING,
4548
) { channel, descriptor ->
4649
val response = clientStreamingRpc(channel, descriptor, flow {
@@ -50,10 +53,29 @@ class CancellationClientTest {
5053
emit(EchoRequest { message = "Eccchhooo" })
5154
}
5255
})
53-
val expected = "Eccchhooo,Eccchhooo,Eccchhooo,Eccchhooo,Eccchhooo"
56+
val expected = "Eccchhooo, Eccchhooo, Eccchhooo, Eccchhooo, Eccchhooo"
5457
assertEquals(expected, response.message)
5558
}
5659

60+
@Test
61+
fun bidirectionalStreamingEchoTest() = runTest(
62+
methodName = "BidirectionalStreamingEcho",
63+
type = MethodType.BIDI_STREAMING,
64+
) { channel, descriptor ->
65+
val response = bidirectionalStreamingRpc(channel, descriptor, flow {
66+
repeat(5) {
67+
emit(EchoRequest { message = "Eccchhooo" })
68+
}
69+
})
70+
71+
var i = 0
72+
response.collect {
73+
i++
74+
assertEquals("Eccchhooo", it.message)
75+
}
76+
assertEquals(5, i)
77+
}
78+
5779
fun runTest(
5880
methodName: String,
5981
type: MethodType,

grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/CompletionQueue.kt

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,36 @@ import platform.posix.memset
1515
import kotlin.experimental.ExperimentalNativeApi
1616
import kotlin.native.ref.createCleaner
1717

18+
/**
19+
* The result of a batch operation (see [CompletionQueue.runBatch]).
20+
*/
1821
internal sealed interface BatchResult {
22+
/**
23+
* Happens when a batch was submitted and...
24+
* - the queue is closed
25+
* - the queue is in the process of a force shutdown
26+
* - the queue is in the process of a normal shutdown, and the batch is a new `RECV_STATUS_ON_CLIENT` batch.
27+
*/
1928
object CQShutdown : BatchResult
29+
30+
/**
31+
* Happens when the batch couldn't be submitted for some reason.
32+
*/
2033
data class CallError(val error: grpc_call_error) : BatchResult
34+
35+
/**
36+
* Happens when the batch was successfully submitted.
37+
* The [future] will be completed with `true` if the batch was successful, `false` otherwise.
38+
* In the case of `false`, the status of the `RECV_STATUS_ON_CLIENT` batch will provide the error details.
39+
*/
2140
data class Called(val future: CallbackFuture<Boolean>) : BatchResult
2241
}
2342

2443
/**
25-
* A coroutine wrapper around the grpc completion_queue, which manages message operations.
44+
* The Kotlin wrapper for the native grpc_completion_queue.
2645
* It is based on the "new" callback API; therefore, there are no kotlin-side threads required to poll
2746
* the queue.
47+
* Users can attach to the returned [CallbackFuture] if the batch was successfully submitted (see [BatchResult]).
2848
*/
2949
internal class CompletionQueue {
3050

@@ -44,11 +64,13 @@ internal class CompletionQueue {
4464
@Suppress("PropertyName")
4565
internal val _shutdownDone = CallbackFuture<Unit>()
4666

47-
// used for spinning lock. false means not used (available)
67+
// used to synchronize the start of a new batch operation.
4868
private val batchStartGuard = SynchronizedObject()
4969

70+
// a stable reference of this used as user_data in the shutdown callback.
5071
private val thisStableRef = StableRef.create(this)
5172

73+
// the shutdown functor/tag called when the queue is shut down.
5274
private val shutdownFunctor = nativeHeap.alloc<kgrpc_cb_tag> {
5375
functor.functor_run = SHUTDOWN_CB
5476
user_data = thisStableRef.asCPointer()
@@ -69,6 +91,10 @@ internal class CompletionQueue {
6991
require(kgrpc_iomgr_run_in_background()) { "The gRPC iomgr is not running background threads, required for callback based APIs." }
7092
}
7193

94+
/**
95+
* Submits a batch operation to the queue.
96+
* See [BatchResult] for possible outcomes.
97+
*/
7298
fun runBatch(call: NativeClientCall<*, *>, ops: CPointer<grpc_op>, nOps: ULong): BatchResult {
7399
val completion = CallbackFuture<Boolean>()
74100
val tag = newCbTag(completion, OPS_COMPLETE_CB)
@@ -102,7 +128,11 @@ internal class CompletionQueue {
102128
return BatchResult.Called(completion)
103129
}
104130

105-
// must not be canceled as it cleans resources and sets the state to CLOSED
131+
/**
132+
* Shuts down the queue.
133+
* The method returns immediately, but the queue will be shut down asynchronously.
134+
* The returned [CallbackFuture] will be completed with `Unit` when the queue is shut down.
135+
*/
106136
fun shutdown(force: Boolean = false): CallbackFuture<Unit> {
107137
if (force) {
108138
forceShutdown = true

grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/NativeClientCall.kt

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ internal class NativeClientCall<Request, Response>(
3434
}
3535

3636
init {
37+
// cancel the call if the job is canceled.
3738
callJob.invokeOnCompletion {
3839
when (it) {
3940
is CancellationException -> {
@@ -63,6 +64,9 @@ internal class NativeClientCall<Request, Response>(
6364
// if null, the call is still in progress. otherwise, the call can be closed as soon as inFlight is 0.
6465
private val closeInfo = atomic<Pair<Status, GrpcTrailers>?>(null)
6566

67+
// we currently don't buffer messages, so after one `sendMessage` call, ready turns false.
68+
private val ready = atomic(true)
69+
6670
/**
6771
* Increments the [inFlight] counter by one.
6872
* This should be called before starting a batch.
@@ -113,6 +117,16 @@ internal class NativeClientCall<Request, Response>(
113117
}
114118
}
115119

120+
/**
121+
* Sets the [ready] flag to true and calls the listener's onReady callback.
122+
* This is called as soon as the RECV_MESSAGE batch is finished (or failed).
123+
*/
124+
private fun turnReady() {
125+
if (ready.compareAndSet(expect = false, update = true)) {
126+
listener?.onReady()
127+
}
128+
}
129+
116130

117131
override fun start(
118132
responseListener: Listener<Response>,
@@ -124,13 +138,19 @@ internal class NativeClientCall<Request, Response>(
124138
listener = responseListener
125139

126140
// start receiving the status from the completion queue,
127-
// which is bound to the lifecycle of the call.
128-
val success = initializeCallOnCQ()
141+
// which is bound to the lifetime of the call.
142+
val success = startRecvStatus()
129143
if (!success) return
130144

145+
// send and receive initial headers to/from the server
131146
sendAndReceiveInitialMetadata()
132147
}
133148

149+
/**
150+
* Submits a batch operation to the [CompletionQueue] and handle the returned [BatchResult].
151+
* If the batch was successfully submitted, [onSuccess] is called.
152+
* In any case, [cleanup] is called.
153+
*/
134154
private fun runBatch(
135155
ops: CPointer<grpc_op>,
136156
nOps: ULong,
@@ -175,12 +195,19 @@ internal class NativeClientCall<Request, Response>(
175195
}
176196
}
177197

198+
/**
199+
* Starts a batch operation to receive the status from the completion queue.
200+
* This operation is bound to the lifetime of the call, so it will finish once all other operations are done.
201+
* If this operation fails, it will call [markClosePending] with the corresponding error, as the entire call
202+
* si considered failed.
203+
*
204+
* @return true if the batch was successfully submitted, false otherwise.
205+
* In this case, the call is considered failed.
206+
*/
178207
@OptIn(ExperimentalStdlibApi::class)
179-
private fun initializeCallOnCQ(): Boolean {
208+
private fun startRecvStatus(): Boolean {
180209
checkNotNull(listener) { "Not yet started" }
181210
val arena = Arena()
182-
// this must not be canceled as it sets the call status.
183-
// if the client itself got canceled, this will return fast.
184211
val statusCode = arena.alloc<grpc_status_code.Var>()
185212
val statusDetails = arena.alloc<grpc_slice>()
186213
val errorStr = arena.alloc<CPointerVar<ByteVar>>()
@@ -253,12 +280,19 @@ internal class NativeClientCall<Request, Response>(
253280
}
254281
}
255282

283+
/**
284+
* Requests [numMessages] messages from the server.
285+
* This must only be called again after [numMessages] were received in the [Listener.onMessage] callback.
286+
*/
256287
override fun request(numMessages: Int) {
257288
check(numMessages > 0) { "numMessages must be > 0" }
258289
val listener = checkNotNull(listener) { "Not yet started" }
259290
check(!cancelled) { "Already cancelled" }
260291

261292
var remainingMessages = numMessages
293+
294+
// we need to request only one message at a time, so we use a recursive function that
295+
// requests one message and then calls itself again.
262296
fun post() {
263297
if (remainingMessages-- <= 0) return
264298

@@ -272,13 +306,16 @@ internal class NativeClientCall<Request, Response>(
272306
if (recvPtr.value != null) grpc_byte_buffer_destroy(recvPtr.value)
273307
arena.clear()
274308
}) {
275-
val buf = recvPtr.value ?: return@runBatch // EOS
309+
// if the call was successful, but no message was received, we reached the end-of-stream.
310+
val buf = recvPtr.value ?: return@runBatch
276311
val msg = methodDescriptor.getResponseMarshaller()
277312
.parse(buf.toKotlin().asInputStream())
278313
listener.onMessage(msg)
279-
post() // post next only now
314+
post()
280315
}
281316
}
317+
318+
// start requesting messages
282319
post()
283320
}
284321

@@ -310,19 +347,31 @@ internal class NativeClientCall<Request, Response>(
310347
}
311348
}
312349

350+
override fun isReady(): Boolean = ready.value
351+
313352
override fun sendMessage(message: Request) {
314353
checkNotNull(listener) { "Not yet started" }
315354
check(!halfClosed) { "Already half closed." }
316355
check(!cancelled) { "Already cancelled." }
356+
check(isReady()) { "Not yet ready." }
357+
358+
// set ready false, as only one message can be sent at a time.
359+
ready.value = false
317360

318361
val arena = Arena()
319362
val inputStream = methodDescriptor.getRequestMarshaller().stream(message)
320363
val byteBuffer = inputStream.asSource().toGrpcByteBuffer()
364+
321365
val op = arena.alloc<grpc_op> {
322366
op = GRPC_OP_SEND_MESSAGE
323367
data.send_message.send_message = byteBuffer
324368
}
369+
325370
runBatch(op.ptr, 1u, cleanup = {
371+
// no mater what happens, we need to set ready to true again.
372+
turnReady()
373+
374+
// actual cleanup
326375
grpc_byte_buffer_destroy(byteBuffer)
327376
arena.clear()
328377
}) {

grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/internal/NativeManagedChannel.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import kotlin.experimental.ExperimentalNativeApi
1919
import kotlin.native.ref.createCleaner
2020
import kotlin.time.Duration
2121

22+
/**
23+
* Wrapper for [grpc_channel_credentials].
24+
*/
2225
internal sealed class GrpcCredentials(
2326
internal val raw: CPointer<grpc_channel_credentials>,
2427
) {
@@ -27,12 +30,21 @@ internal sealed class GrpcCredentials(
2730
}
2831
}
2932

33+
/**
34+
* Insecure credentials.
35+
*/
3036
internal class GrpcInsecureCredentials() :
3137
GrpcCredentials(grpc_insecure_credentials_create() ?: error("Failed to create credentials"))
3238

33-
39+
// default propagation mask for all calls.
3440
private const val GRPC_PROPAGATE_DEFAULTS = 0x0000FFFFu
3541

42+
/**
43+
* Native implementation of [ManagedChannel].
44+
*
45+
* @param target The target address to connect to.
46+
* @param credentials The credentials to use for the connection.
47+
*/
3648
internal class NativeManagedChannel(
3749
target: String,
3850
// we must store them, otherwise the credentials are getting released
@@ -89,11 +101,14 @@ internal class NativeManagedChannel(
89101
return
90102
}
91103
if (force) {
104+
// cancel all jobs, such that the shutdown is completing faster (not immediate).
92105
// TODO: replace jobs by custom pendingCallClass.
93106
callJobSupervisor.cancelChildren(CancellationException("Channel is shutting down"))
94107
}
95108

96109
// wait for the completion queue to shut down.
110+
// the completion queue will be shut down after all requests are completed.
111+
// therefore, we don't have to wait for the callJobs to be completed.
97112
cq.shutdown(force).onComplete {
98113
if (isTerminatedInternal.complete(Unit)) {
99114
// release the grpc runtime, so it might call grpc_shutdown()

0 commit comments

Comments
 (0)