Repository navigation
Keep gRPC marshaller streams readable when closed before they are read - #54
Merged
merlimat merged 1 commit intoOct 13, 2026
Merged
Conversation
Since 0.9.0, closing the InputStream returned by the generated Marshaller.stream() empties it and hands its array back (or releases the buffers of its gathered values), even if nobody has read it. Proxies that forward the stream they receive, like BookKeeper's stream-storage proxy, break over the in-process transport: gRPC closes the stream once onMessage() returns, and the next hop reads that same stream later. A stream now gives up its array or buffers only once it has been read to the end. DrainableByteArrayInputStream ignores an earlier close. An early close of GatheringInputStream copies what is left into an array of its own and releases the buffers, so nothing leaks if the stream is never read; its methods are synchronized, as the close can overlap a read on another thread.
This was referenced Oct 13, 2026
Merged
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Problem
Since 0.9.0, closing the
InputStreamreturned by a LightProto gRPCMarshaller.stream()destroys its content even if nobody has read it.DrainableByteArrayInputStream.close()empties the stream and returns its array to the per-thread pool (#27).GatheringInputStream.close()empties it and releases its ByteBufs (#49).This breaks proxies that forward the stream they receive unchanged, like Apache BookKeeper's stream-storage proxy. gRPC's
ClientCallImplcloses each message as soon asonMessage()returns. Over the in-process transport, the next hop gets that same stream object and reads it later on its own executor, so it sees 0 bytes. BookKeeper'sTestStorageContainerStoreImplgets empty responses, andProxyPingPongServiceTesthangs. Up to 0.7.x,close()was a no-op.Example
Current generated code:
Change
A stream only gives up its array or buffers once it has been read to the end. Closing it any earlier keeps the content readable, and a later reader's close does the recycling. Further closes after that stay no-ops.
DrainableByteArrayInputStream: an early close does nothing, and the array is left to the garbage collector.GatheringInputStream: an early close copies what is left into an array the stream owns. It then releases the retained ByteBufs and gives the thread's array back, so nothing leaks if the stream is never read again. Later reads come from the copy. The methods aresynchronized, because the proxy's close can run on one thread while the peer reads on another. Without that, the proxy test below corrupts every message with a value of 16 KiB or more.The steady-state path is unchanged: when Netty drains a stream and then closes it, the array is recycled with no extra allocation or copy. One behaviour change: a send that fails before gRPC reads the stream (it exceeds the size limit, or a queued call is cancelled) no longer returns its small-message array, because that close looks the same as a proxy's.
Testing
GrpcProxyTest: an in-process backend, an identity-marshaller proxy modelled on BookKeeper'sProxyCall, and an in-process client, with nodirectExecutor()anywhere. It covers unary and server-streaming calls with 100 B, 16 KiB and 2 MiB payloads, and checks both what the backend received and what the client got. All 6 cases fail on 0.9.2 and pass with this change.GrpcMarshallerEarlyCloseTest, on both the small-message and gathering paths:parse()the same stream;mvn verifyfrom the root passes, including all gRPC and leak tests.ProxyPingPongServiceTest5/5,DirectPingPongServiceTest5/5,TestStorageContainerStoreImpl20/20.