Fix stream disposal: queueDisposal on last ACK for closed streams#29
Open
MKS2508 wants to merge 1 commit into
Open
Fix stream disposal: queueDisposal on last ACK for closed streams#29MKS2508 wants to merge 1 commit into
MKS2508 wants to merge 1 commit into
Conversation
collectClosedStreams marks streams closed_for_gc but never queues disposal. The only disposal path (connection.zig STREAM+FIN handler) is guarded by !closed_for_gc, so pre-marked streams are never disposed until connection close. Move the disposal check to onAck: when the last byte is acknowledged for a closed_for_gc stream (retransmit_count==0 && !hasUnackedData()), queue disposal directly. Same guard semantics as the existing disposal path, but at the right moment — after ACKs confirm delivery, not at FIN-receive time. Adds 4 lifecycle tests in stream.zig covering: - collectClosedStreams marking behavior - queueDisposal + drainDisposalQueue lifecycle - Deferred disposal when hasUnackedData - Integrated disposal guard with full ACK cycle Tests: 527/527 pass
Author
|
Opening this together with #30, which has the full context, impact analysis, and rationale. Happy to rework based on feedback. |
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
collectClosedStreamsmarks fully-closed bidi streams asclosed_for_gcbut never queues them for disposal. The only existing disposal path (the STREAM+FIN handler inconnection.zig) is guarded by!closed_for_gc, so once a stream is marked, it stays in theStreamsMap— retaining itswrite_buffer— until the connection closes.For long-lived connections serving many short-lived streams (e.g. WebTransport sessions pulling byte ranges), send buffers accumulate for the entire connection lifetime, even though every stream is fully acknowledged and could be reclaimed.
Root cause
collectClosedStreams(stream.zig) is a marking pass — it setsclosed_for_gc = trueand adjusts flow-control counters, but explicitly defers disposal (comment at stream.zig:1127-1129: "Delay disposal: keep the stream in the map so PTO can reset send_offset for retransmission under loss. Stream will be cleaned up when the connection closes.").drainDisposalQueueis the only mechanism that actually removes streams from the map and frees memory. The sole producer ofqueueDisposalcalls is the STREAM+FIN handler (connection.zig:1904), guarded by!strm.closed_for_gc. So ifcollectClosedStreamsmarks a stream before the STREAM+FIN path runs, disposal never fires for it.Fix
Move the disposal check into
onAckprocessing. When an ACK frame acknowledges the last un-ACKed byte of aclosed_for_gcstream, queue disposal directly:Applied at both ACK-processing call-sites in
connection.zig(~l1544 and ~l1677). Same guard semantics as the existing disposal path atconnection.zig:1904—retransmit_count == 0 and !hasUnackedData()— but evaluated at the right moment: after ACKs confirm delivery, not at FIN-receive time.Why this is safe (PTO / loss recovery)
The guard
retransmit_count == 0 and !s.send.hasUnackedData()means: the stream has no unacknowledged data AND no in-flight retransmissions. Under these conditions there is nothing for PTO to reset or retransmit — thesend_offsetis fully settled. The "Delay disposal" comment's concern (PTO resettingsend_offsetunder loss) does not apply when the send side is fully ACKed: there's no unacked data left to recover.The existing disposal path at
connection.zig:1904already relies on the same guard for the STREAM+FIN case. This PR applies it at the ACK moment for streams thatcollectClosedStreamshas already marked.Tests
Adds 4 lifecycle tests in
stream.zigcovering the disposal path that was previously untested (collectClosedStreams,drainDisposalQueue,queueDisposal,closed_for_gchad zero tests in the codebase):collectClosedStreams marks fully-closed bidi streams— verifiesclosed_for_gcmarking + open-stream counter decrement.queueDisposal + drainDisposalQueue lifecycle— verifies deferred removal (queue keeps entry, drain removes it + frees memory).collectClosedStreams defers disposal when hasUnackedData— verifies a marked stream with unacked data stays in the map.disposal guard: closed_for_gc + fully-acked -> disposed— integrated test of the full guard through a complete ACK cycle.All tests pass (527 total; +4 new).
Behavior change
Streams that are fully closed AND fully acknowledged are now disposed at ACK time instead of waiting for connection close. Streams with unacked data or in-flight retransmissions are unaffected (guard fails, disposal deferred as before).