Skip to content

feat(uring): FEC-safe GSO batching via fate tags (#17) - #24

Merged
vxfemboy merged 1 commit into
mainfrom
feat/uring-gso-fate-tags
Jul 3, 2026
Merged

feat(uring): FEC-safe GSO batching via fate tags (#17)#24
vxfemboy merged 1 commit into
mainfrom
feat/uring-gso-fate-tags

Conversation

@vxfemboy

@vxfemboy vxfemboy commented Jul 3, 2026

Copy link
Copy Markdown
Member

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 = 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 → FEC can't recover (empirically: cap=8 → ~95% delivery, FAIL). So GSO was silently disabled while still paying cmsg cost.

Fix

  • Fate tags across the trait boundary. New EgressDatagram { fate: u16, bytes } in yip-io::poll; Dispatch::on_tun returns &[EgressDatagram]. DataPlane sets fate = symbol.object_id (already known at encode time — no yip-transport API change, no wire change). PollDriver ignores fate.
  • Accumulator + choke point. UringDriver stages egress datagrams in pending_gso across TUN-read completions in one poll_once; flush_pending_gso forms 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_tagged is the single unit-tested correctness choke point. queue_udp_gso is now 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, documented).

Verification

  • FEC-regression gate: arq_recovers_bulk_loss under YIP_USE_URING=1 with GSO active → 99.3% delivery (≥98%), ARQ retransmits 132 (>0). This is the test that failed at ~95% with the naive cap bump.
  • Unit: 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. yip-io (19) + yipd (36) green.
  • netns: ping / ping-under-loss / L2 pass under poll and uring.
  • Throughput: single-stream iperf3 unchanged (~310–322 Mbit/s both drivers) — that path is FEC/CPU-bound, not syscall-bound, so no single-stream gain or regression; a dedicated bursty-workload throughput bench is a documented follow-up.

🤖 Generated with Claude Code

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>
@vxfemboy
vxfemboy merged commit 7c343fc into main Jul 3, 2026
8 checks passed
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.

io_uring: re-enable GSO batching (MAX_GSO_SEGMENTS_PER_SEND=1 makes UDP_SEGMENT a no-op)

1 participant