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

Merged
vxfemboy merged 1 commit from feat/uring-gso-fate-tags into main 2026-07-03 13:39:32 +00:00
vxfemboy commented 2026-07-03 13:31:26 +00:00 (Migrated from github.com)

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

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](https://claude.com/claude-code)
Sign in to join this conversation.
No description provided.