feat(uring): FEC-safe GSO batching via fate tags (#17) - #24
Merged
Conversation
Re-enable UDP GSO batching in the UringDriver without breaking FEC. GSO was
pinned to MAX_GSO_SEGMENTS_PER_SEND=1 because coalescing a RaptorQ object's
source symbol with its own repair into one UDP_SEGMENT super-skb makes a single
netem drop take both, defeating recovery (empirically: cap=8 -> ~95% delivery,
FAIL). Fix per docs/superpowers/specs/2026-07-02-uring-gso-fate-tags-design.md:
- Tag each TUN-egress datagram with its RaptorQ object id ("fate") across the
Dispatch::on_tun boundary: new `EgressDatagram { fate, bytes }` in yip-io::poll;
DataPlane fills fate = symbol.object_id (already known at encode time — no
yip-transport API change, no wire change). PollDriver ignores fate.
- UringDriver stages egress datagrams in `pending_gso` across TUN-read
completions within one poll_once, then `flush_pending_gso` forms batches taking
at most one datagram per distinct fate per pass, so a coalesced skb never holds
two symbols of the same object. `can_coalesce_gso_tagged` is the single
correctness choke point (rejects duplicate fate or mismatched length);
queue_udp_gso is generic over AsRef<[u8]> so tagged + untagged callers share it.
- MAX_GSO_SEGMENTS_PER_SEND 1 -> 32 (now a throughput/blast-radius knob, not a
correctness guard). ARQ-retransmit egress stays non-GSO (out of scope).
Tests: 3 pure choke-point tests (reject-duplicate-fate / accept-distinct /
reject-mismatched-length); same-object 5-datagram batch asserts
gso_submission_count == 0 (never coalesced); distinct-fate large batch asserts
chunking with a de-tautologized expected count (hardcoded literals, not the
function under test); fallback test rewritten around distinct fates.
Verified: build + clippy -D warnings + yip-io (19) + yipd (36) unit tests green;
arq_recovers_bulk_loss 99.3% (>=98%, ARQ retransmits 132>0) under uring with GSO
active — the FEC-regression gate; ping / ping-under-loss / L2 pass under poll and
uring. Single-stream iperf3 throughput unchanged (~310-322 Mbit/s both drivers —
FEC/CPU-bound, not syscall-bound; a dedicated bursty-workload bench is a follow-up).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Implements the signed-off design in
docs/superpowers/specs/2026-07-02-uring-gso-fate-tags-design.md. Closes #17.Problem
GSO was pinned to
MAX_GSO_SEGMENTS_PER_SEND = 1because coalescing a RaptorQ object's source symbol with its own repair into oneUDP_SEGMENTsuper-skb makes a single netem drop take both → FEC can't recover (empirically: cap=8 → ~95% delivery, FAIL). So GSO was silently disabled while still paying cmsg cost.Fix
EgressDatagram { fate: u16, bytes }inyip-io::poll;Dispatch::on_tunreturns&[EgressDatagram].DataPlanesetsfate = symbol.object_id(already known at encode time — noyip-transportAPI change, no wire change).PollDriverignoresfate.UringDriverstages egress datagrams inpending_gsoacross TUN-read completions in onepoll_once;flush_pending_gsoforms batches taking at most one datagram per distinct fate per skb, so a coalesced skb never holds two symbols of one object.can_coalesce_gso_taggedis the single unit-tested correctness choke point.queue_udp_gsois now generic overAsRef<[u8]>so tagged + untagged callers share it.MAX_GSO_SEGMENTS_PER_SEND1 → 32 (now a throughput/blast-radius knob, not a correctness guard). ARQ-retransmit egress stays non-GSO (out of scope, documented).Verification
arq_recovers_bulk_lossunderYIP_USE_URING=1with GSO active → 99.3% delivery (≥98%), ARQ retransmits 132 (>0). This is the test that failed at ~95% with the naive cap bump.gso_submission_count == 0(never coalesced); distinct-fate large batch asserts chunking with a de-tautologized expected count (hardcoded literals, not the function under test); fallback test rewritten around distinct fates. yip-io (19) + yipd (36) green.🤖 Generated with Claude Code