Skip to content

fix(tcp): drain the OOO queue iteratively, not by re-entering the FSM - #49

Open
Yossioren wants to merge 1 commit into
ccie18643:masterfrom
Yossioren:fix/ooo-queue-drain-without-recursion
Open

Yossioren wants to merge 1 commit into
ccie18643:masterfrom
Yossioren:fix/ooo-queue-drain-without-recursion

Conversation

@Yossioren

Copy link
Copy Markdown

After a gap-fill, '_phase5_consume_segment_and_postprocess' fed the next queued out-of-order segment back into 'session.tcp_fsm()' from inside the dispatch that was still processing the current one. Each queued segment therefore nested one more FSM dispatch (~10 interpreter frames), so a run of a few hundred OOO segments raised RecursionError and killed the packet-handler thread. Seen in practice under a speedtest through a VPN tunnel, where reordering produced long OOO runs.

The drain site now appends the released segment to a per-session '_ooo_ready' deque, and the outermost 'tcp_fsm()' call processes that deque in a loop under the (re-entrant) FSM lock once its own dispatch is done; nested calls see '_ooo_draining' and leave the next segment to the loop, so the depth stays at two however long the run is. Segments are still processed in sequence order, each through the full 'tcp_fsm()' path. Nothing in the dispatch ran after the old nested call ('_process_ack_packet' returns right after it), so the order of effects is unchanged.

Reference: RFC 9293 §3.10.7.4 (segments with higher beginning sequence numbers SHOULD be held for later processing, SHLD-31).

New integration test
'test__data_transfer_out_of_order__long_queue_drains_without_recursion' (300 queued segments, then the gap-fill) raised RecursionError before the fix and passes after it.

make test: 13845 passing, 11 skipped. make lint clean.

After a gap-fill, '_phase5_consume_segment_and_postprocess' fed the
next queued out-of-order segment back into 'session.tcp_fsm()' from
inside the dispatch that was still processing the current one. Each
queued segment therefore nested one more FSM dispatch (~10 interpreter
frames), so a run of a few hundred OOO segments raised RecursionError
and killed the packet-handler thread. Seen in practice under a
speedtest through a VPN tunnel, where reordering produced long OOO runs.

The drain site now appends the released segment to a per-session
'_ooo_ready' deque, and the outermost 'tcp_fsm()' call processes that
deque in a loop under the (re-entrant) FSM lock once its own dispatch
is done; nested calls see '_ooo_draining' and leave the next segment
to the loop, so the depth stays at two however long the run is.
Segments are still processed in sequence order, each through the full
'tcp_fsm()' path. Nothing in the dispatch ran after the old nested
call ('_process_ack_packet' returns right after it), so the order of
effects is unchanged.

Reference: RFC 9293 §3.10.7.4 (segments with higher beginning sequence
numbers SHOULD be held for later processing, SHLD-31).

New integration test
'test__data_transfer_out_of_order__long_queue_drains_without_recursion'
(300 queued segments, then the gap-fill) raised RecursionError before
the fix and passes after it.

make test: 13845 passing, 11 skipped. make lint clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant