Spawn bidi transport send so request initiates before first message()#4
Merged
Spawn bidi transport send so request initiates before first message()#4
Conversation
call_bidi_stream previously stored the transport.send() future unpolled in RecvState::Pending, awaiting it lazily on the first message() call. For transports with a background driver (HttpClient -> hyper pool) this is fine. For SharedHttp2Connection, the send() future IS the work: it contains poll_ready -> connect -> handshake -> start streaming the ChannelBody. Nothing happens until that future is polled. So the half-duplex pattern (send all, close_send, then read) would buffer sends into the 32-deep ChannelBody mpsc with nobody draining it. Send #33 deadlocks. Even under 32 sends the behaviour is wrong: the server receives nothing until the client starts reading. Fix: tokio::spawn the send future so the request initiates immediately and ChannelBody gets polled as sends happen. RecvState::Pending holds the JoinHandle; message() awaits it as before. Preserves the original lazy-HEADERS-await behaviour (servers that wait for the first request before sending HEADERS still work). Dropping the handle on deadline timeout or BidiStream drop detaches the task, but the dropped tx closes the body stream, which ends the request, which completes the task naturally. No explicit abort needed. Integration test in tests/streaming sends 40 messages half-duplex over SharedHttp2Connection; timeout-wrapped so a regression fails fast. Fixes #2
46448ba to
286f99f
Compare
rpb-ant
approved these changes
Mar 17, 2026
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Fixes #2.
call_bidi_streampreviously stored thetransport.send()future unpolled inRecvState::Pending, awaiting it lazily on the firstmessage()call. ForSharedHttp2Connection, that future contains the actual poll_ready -> connect -> handshake -> stream-body work, so nothing happened until the caller started reading. The half-duplex pattern (send all, close_send, then read) would buffer into the 32-deepChannelBodympsc with nobody draining it and deadlock on the 33rd send.Changes
RecvState::PendingholdsJoinHandle<Result<Response<B>, ConnectError>>instead ofBoxFuturecall_bidi_streamspawns the send future instead of boxing itmessage()awaits the handle; JoinError (task panic) maps toConnectError::internaltests/streaming: 40 sends half-duplex overSharedHttp2Connection, timeout-wrapped so a regression fails fast rather than hanging the suitePreserved behaviour
Cleanup on drop
Dropping the BidiStream before
message()detaches the spawned task. The droppedtxcloses the body stream, which ends the request, which completes the task naturally - so no explicit abort is needed. If the task was mid-connect/handshake it completes that work before discovering an empty body; wasteful but not leaky.