You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Stream gRPC client calls in both directions - #1642
Second step of R6a (#1561), after #1641 for the server: GrpcClientAdapter makes calls of every streaming type. Restlet can now be both ends of a streaming gRPC call.
Change
RestletClientStream sent the whole request and read the whole response, at most one message each way. The transport factory therefore refused every non-unary method with UNIMPLEMENTED. Now:
The exchange starts with the request headers. The request body is fed frame by frame from a bounded queue and ends at the half-close.
The body is written by the Netty client's existing background writer: on HTTP/2, a body of unknown size is written concurrently, and handle() returns at the response head.
So messages flow both ways over one stream, and no core change is needed.
Outbound readiness. Each frame written is reported to the SDK (onSentBytes), so isReady() and onReady follow the network, with the ready threshold at half the queue budget. A sender that ignores readiness and queues more than the budget fails with RESOURCE_EXHAUSTED, like the server's response queue (Host client-streaming and bidirectional gRPC methods #1641). A single message larger than the budget is still accepted.
Inbound demand.
The response is read in chunks as it arrives and handed to the SDK.
Bytes not yet consumed by its deframer, which only reads the messages the application requested, stay within the queue budget.
So a slow or paused reader stops the reads, and HTTP/2 flow control stops the server.
Trailers are read at the end of the body.
The SDK deframer bounds message size (RESOURCE_EXHAUSTED).
Configuration. New GrpcClientAdapter.setMaxQueuedBytes: per call and per direction, default 256 KiB, at least 16 KiB, mirroring GrpcServerAdapter.
Factory. Refuses only methods of type UNKNOWN.
Unchanged. Plaintext only, no retry, keepalive or connection controls; per-call authority overrides and compression stay refused.
changes.md, readme.md and the adapter Javadoc are updated.
Tests
The new GrpcClientStreamingTestCase runs every case twice: against grpc-java's own Netty server, with a fixed 64 KiB flow-control window so the peer can't absorb what the client holds, and against GrpcServerAdapter, so that both ends are Restlet.
Test
Checks
serverStreamingPreservesOrder
50 × 4 KiB responses, in order
clientStreamingSendsEveryMessage
50 × 4 KiB requests, one summary reply
bidirectionalCallsExchangeMessagesBothWays
Each reply received before the next request, 5 rounds on one stream
requestsFollowTheServerDemand
With a 32 KiB budget and the server requesting nothing, a client respecting readiness has sent under half of 4 MiB after 1 s. Once requested, all 1024 messages arrive
responsesFollowTheClientDemand
With the client requesting nothing, a server respecting readiness has produced under half of 4 MiB after 1 s. Once requested, all 1024 arrive
messagesLargerThanTheQueueBudgetStreamThrough
A 512 KiB message each way through a 16 KiB budget
cancellingAStreamReachesTheServerAndFreesTheSlot
Client cancel mid-stream reaches the server's onCancel and frees the only slot
anOpenStreamEndsAtItsDeadline
A never-half-closed bidirectional call ends with DEADLINE_EXCEEDED
The two "every message" tests send 50 messages, about 200 KiB: their stubs ignore readiness, so they must stay within the queue budget.
The demand tests stall on purpose before moving 4 MiB, so the Restlet peer gets a 30 s call limit instead of its 5 s default. The same change is applied to #1641's unrequestedMessagesPushBackOnTheClient, which hit the default on a loaded machine.
rejectsStreamingAndOutboundLimitBeforeSending became rejectsUnknownMethodTypesAndOutboundLimitBeforeSending. The other client tests are unchanged and pass, so unary behaviour is preserved, including cancellation before and after headers, deadlines, admission and the trailers-only path.
Not in this PR
Retiring the R0 manual-framing streaming fixtures in core (GrpcSdkTransportSpikeTestCase). They're test-only diagnostics, so they can go in a follow-up once R6a is complete.
Idle-disconnect behaviour of long-lived streams, and connection controls (GOAWAY/draining, PING/keepalive), which stay rejected.
RestletClientStream sent the whole request and read the whole response,
so the transport factory refused every non-unary method. Start the
HTTP/2 exchange with the request headers instead, and feed the request
body frame by frame from a bounded queue, ending at the half-close; the
Netty client already writes a body of unknown size in the background on
HTTP/2 and returns at the response head, so messages flow both ways.
Report written frames to the SDK, so readiness follows the network, and
fail a sender that queues past the budget with RESOURCE_EXHAUSTED. Read
the response as it arrives, keeping the bytes the deframer hasn't
consumed within the budget, so an application that requests nothing
pushes back on the server. Add GrpcClientAdapter.setMaxQueuedBytes,
mirroring the server adapter. Only methods of unknown type are refused.
Test every streaming type against grpc-java's Netty server and against
GrpcServerAdapter, with readiness, demand, large messages, cancellation
and deadlines. Give the server's push-back test room on loaded machines.
Part of #1561 (R6a).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Jerome Louvel <[email protected]>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Terminal responses can leave flow-control-blocked request writers and exchanges uncleared, while readiness accounting can reject valid large-message streams with RESOURCE_EXHAUSTED.
Extends the SDK-native gRPC client transport to support all streaming call types over Restlet HTTP/2, complementing the existing server-side streaming support.
Changes:
Streams requests and responses concurrently, with queue budgets tied to outbound readiness and inbound demand.
Adds configurable client queue limits and permits all known gRPC method types.
Adds interoperability tests against Netty and Restlet peers, and updates documentation.
File
Description
readme.md
Advertises client support for every streaming type.
Move the request queueing out of the stream sink into begin, send and
enqueue, and the exchange's request building and response handling
into newRequest and receive, to bring their complexity within
SonarCloud's limit; behaviour is unchanged. Use unnamed catch variables.
Responses now stream as they arrive, so the one message a server sends
before waiting may reach the application before a cancellation, as
with grpc-netty: cancellationAfterHeadersReleasesAdmissionAndClosesExactlyOnce
now allows at most one message instead of none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Jerome Louvel <[email protected]>
A server may answer and reset the stream before the client half-closes,
while the request is still written. The disconnect then failed calls
whose whole response had arrived, and a writer held by flow control
kept the exchange after the call released its slot. A disconnect now
only wakes the reader and the writer: a complete response is still
read, and a broken one fails the head or the body read. When the call
ends with its request unfinished, the queued frames are dropped and the
HTTP request is aborted, resetting the stream and freeing the writer.
The queue rejected the next large message of a sender respecting
readiness while the tail of the previous one was still queued. A
message may now start while the queue is within its budget, and grow up
to the message-size limit once started, so the queue stays within its
budget plus one message.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Jerome Louvel <[email protected]>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Peer-disconnect draining can incur substantial CPU and allocation overhead from one-byte reads, while client queue-overflow rejection and admission-slot cleanup lack regression coverage.
…eer is gone
A sender ignoring readiness towards a peer that requests nothing is now
tested against both peers: the call ends once with RESOURCE_EXHAUSTED
and frees its only slot.
Once the peer is gone, the response the connector buffered was read one
byte at a time while the inbound budget was full; it is now drained in
whole chunks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Active issues can reject valid large unknown-length requests with RESOURCE_EXHAUSTED and leave unfinished HTTP/2 exchanges retained until timeout after response completion.
No open issues 🎉
2 previously missed issues
In code that hasn't changed since last review
Unknown-length messages can exceed the budget before draining
For unknown-length marshaller streams, grpc-java emits the header and intermediate buffers with messages == 0, then counts the message on its final buffer, so messages > 0 is not a message-start marker. A single valid 100 KiB request through a 16 KiB budget can therefore reset messageStart to its own queued bytes and fail with RESOURCE_EXHAUSTED before the writer starts draining. Track admission at actual message boundaries independently of frame counts, and add a large unknown-length-marshaller regression.
Terminal completion can leave an unfinished exchange until timeout
requestEnded becomes true when the entity writer returns, but NettyClientCall.writeRequest() still waits for writability before sending LastHttpContent. If the last DATA write leaves the channel non-writable and the server completes its response, this guard skips aborting the unfinished exchange, retaining its writer and HTTP/2 stream until the connector timeout. Attempt abort on terminal completion even after the entity writer returns; the connector already ignores cancellation of completed exchanges.
…d exchange
grpc-java frames a message of unknown length before counting it, so
frame message counts do not mark where messages start: a single large
message could be refused with RESOURCE_EXHAUSTED before the writer
drained it. The queue now reads the 5-byte gRPC prefixes to find each
message start and its declared length.
The entity writer may return before the connector sends the last
content, so the exchange is now aborted whenever the call ends; the
connector ignores the abort of a completed exchange.
The queue wait moves out of the entity class, and the stalled-sender
tests wait for their counters to settle instead of sleeping.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Addressed the two "previously missed issues" from the latest Copilot review in 494bc9f:
Unknown-length messages exceeding the budget before draining. Frame message counts no longer mark message starts. The queue reads the 5-byte gRPC prefixes to find each message start, which must be within the budget, and its declared length, which must be within the message-size limit. New test largeMessagesOfUnknownLengthAreAccepted, against both peers, sends two 100 KiB messages through a 16 KiB queue with a marshaller of unknown length. Before the fix it failed against both with RESOURCE_EXHAUSTED: Request queue byte budget exceeded.
Terminal completion leaving an unfinished exchange. The exchange is now aborted whenever the call ends, even once the entity writer returned. NettyClientCall.cancel() ignores a released exchange, or one whose request was sent and response completed. There is no regression test for this one, as the non-writable channel before the last content is not reproducible on demand.
Also from SonarCloud: the queue wait moved out of the entity class (complexity), endRequest moved into State, and the stalled-sender tests wait for their counters to settle instead of sleeping. The remaining S110 (6 parents) comes from extending OutputRepresentation, as the server adapter already does.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second step of R6a (#1561), after #1641 for the server:
GrpcClientAdaptermakes calls of every streaming type. Restlet can now be both ends of a streaming gRPC call.Change
RestletClientStreamsent the whole request and read the whole response, at most one message each way. The transport factory therefore refused every non-unary method withUNIMPLEMENTED. Now:handle()returns at the response head.onSentBytes), soisReady()andonReadyfollow the network, with the ready threshold at half the queue budget. A sender that ignores readiness and queues more than the budget fails withRESOURCE_EXHAUSTED, like the server's response queue (Host client-streaming and bidirectional gRPC methods #1641). A single message larger than the budget is still accepted.RESOURCE_EXHAUSTED).GrpcClientAdapter.setMaxQueuedBytes: per call and per direction, default 256 KiB, at least 16 KiB, mirroringGrpcServerAdapter.UNKNOWN.changes.md,readme.mdand the adapter Javadoc are updated.Tests
The new
GrpcClientStreamingTestCaseruns every case twice: against grpc-java's own Netty server, with a fixed 64 KiB flow-control window so the peer can't absorb what the client holds, and againstGrpcServerAdapter, so that both ends are Restlet.serverStreamingPreservesOrderclientStreamingSendsEveryMessagebidirectionalCallsExchangeMessagesBothWaysrequestsFollowTheServerDemandresponsesFollowTheClientDemandmessagesLargerThanTheQueueBudgetStreamThroughcancellingAStreamReachesTheServerAndFreesTheSlotonCanceland frees the only slotanOpenStreamEndsAtItsDeadlineDEADLINE_EXCEEDEDThe two "every message" tests send 50 messages, about 200 KiB: their stubs ignore readiness, so they must stay within the queue budget.
The demand tests stall on purpose before moving 4 MiB, so the Restlet peer gets a 30 s call limit instead of its 5 s default. The same change is applied to #1641's
unrequestedMessagesPushBackOnTheClient, which hit the default on a loaded machine.mvn -o verify -pl org.restlet.ext.grpcpasses: 93 tests.rejectsStreamingAndOutboundLimitBeforeSendingbecamerejectsUnknownMethodTypesAndOutboundLimitBeforeSending. The other client tests are unchanged and pass, so unary behaviour is preserved, including cancellation before and after headers, deadlines, admission and the trailers-only path.Not in this PR
GrpcSdkTransportSpikeTestCase). They're test-only diagnostics, so they can go in a follow-up once R6a is complete.🤖 Generated with Claude Code