Skip to content

Avoid race in duplex pipe for streaming calls#3572

Draft
oldergod wants to merge 1 commit intomasterfrom
bquenaudon.2026-04-16.lateflushes
Draft

Avoid race in duplex pipe for streaming calls#3572
oldergod wants to merge 1 commit intomasterfrom
bquenaudon.2026-04-16.lateflushes

Conversation

@oldergod
Copy link
Copy Markdown
Member

@oldergod oldergod commented Apr 16, 2026

Fixes #3370

From Claude

Root Cause

GrpcServerStreamingCall was implemented by delegating to GrpcStreamingCall, which uses a PipeDuplexRequestBody, an asynchronous pipe where the request body is written by a background coroutine on Dispatchers.IO. This means the request message and the HTTP/2 END_STREAM flag are sent asynchronously, after the HTTP call is already enqueued.
Many gRPC server implementations wait for the client's half-close (END_STREAM) before starting to stream responses. With the duplex pipe approach, there's a race between when the server receives END_STREAM and when it decides to time out the idle stream, causing the reported ~5-minute delay.

Fix

RealGrpcServerStreamingCall now has its own independent implementation using newRequestBody() (a non-duplex request body), identical to how RealGrpcCall works. The complete request message + END_STREAM is written synchronously in OkHttp's writeTo() call, ensuring the server receives the full request before any response reading begins.
The old delegation-based class is preserved as GrpcStreamingCallServerStreamingAdapter, still used by the test-double factory (GrpcServerStreamingCall { ... } in GrpcCalls.kt).

Supporting changes:

  • readFromResponseBodyCallback() in grpc.kt: added an overload accepting onResponseMetadata: (Map<String, String>) -> Unit so both RealGrpcStreamingCall and RealGrpcServerStreamingCall can use it
  • BlockingMessageSource: replaced grpcCall: RealGrpcStreamingCall with onResponseMetadata: (Map<String, String>) -> Unit for the same reason
  • GrpcClient.newServerStreamingCall() — now creates RealGrpcServerStreamingCall directly instead of wrapping a streaming call.

@oldergod oldergod force-pushed the bquenaudon.2026-04-16.lateflushes branch 2 times, most recently from 2160ea1 to ba9575f Compare April 16, 2026 09:57
@oldergod oldergod force-pushed the bquenaudon.2026-04-16.lateflushes branch from ba9575f to c11403d Compare April 16, 2026 10:09
@oldergod
Copy link
Copy Markdown
Member Author

Does it look legit to you @swankjesse ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GrpcStreamingCall client buffers packets and flushes only after timeout — not receiving events in real-time

1 participant