Skip to content

feat(network): add opt-in io_uring UDP backend - #158

Draft
gd-0 wants to merge 6 commits into
mainfrom
gd/udp-io-uring
Draft

gd-0 wants to merge 6 commits into
mainfrom
gd/udp-io-uring

Conversation

@gd-0

@gd-0 gd-0 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Adds an opt-in Linux io_uring backend for reliable UDP through UdpConfig::io. Both peers can choose their backend independently; the wire protocol is unchanged. The default remains syscall UDP, and TCP is unchanged.

The backend uses one ring per socket, multishot recvmsg with provided buffers, and pooled owned send buffers. Compatible runs retain GSO across mixed-peer batches, with datagram fallback when offload fails. Polling has explicit receive limits; thread handoff and teardown synchronously cancel kernel requests before releasing their buffers. Send slots remain individually boxed so borrowing one slot cannot invalidate kernel pointers into another. Default capacity reserves about 6 MiB per socket. Linux 6.0+ and permission to use io_uring are required; setup failures are reported through the existing socket-creation path.

The audit found and fixed two issues: send backpressure discarded the pending ACK flag, and ACK processing could replenish an intended bounded receive drain. ACKs now remain pending until accepted, and every receive pass is capped. Both UDP backends emit ACKs between receive batches, preventing slow callbacks from deferring them until the receive queue drains. Regression coverage verifies ACK retry after saturation, sustained duplex delivery with one send slot and one receive buffer, and sustained large-message delivery on both backends.

Five alternating measured loopback rounds at afcad76 against main e59fe83, after discarded warmup runs. These measurements precede the subsequent plumbing and syscall ACK-scheduling changes and have not been rerun for the current head:

Workload Main io_uring Result
2 KiB burst throughput 442 MiB/s 773 MiB/s +74.9%; median latency 4.8 → 2.4 µs
64 KiB broadcast throughput 1,398 MiB/s 7,461 MiB/s 5.34×; p99 346 → 70 µs
2 MiB burst throughput 7,285 MiB/s 7,315 MiB/s +0.4%; 3.0% below the branch's syscall control
2 MiB broadcast median latency 1.33 ms 3.23 ms Regression; CPU cost also increases

The broadcast gains include improved GSO grouping, so these measure the complete backend rather than io_uring alone. These are local loopback results; physical NIC/WAN performance and non-Linux builds remain unverified.

Validation: just fmt, just clippy, and cargo test --workspace --all-features --locked passed in hosted CI at fd57087 with default parallelism (365 tests; four existing ignored doc tests). The ACK regression was reproduced before its fix. Coverage includes the shared UDP suite on both backends, mixed peers, loss/reconnect, DCache, sustained large messages, queue exhaustion, GSO fallback, GRO validation, IPv4/IPv6, buffer-ring tail wraparound, and thread movement.

Follow-up validation: just fmt, just clippy, and the full flux-network suite passed (116 tests, run serially after a parallel TCP test encountered AddrInUse). The newly shared sustained test reproduced syscall ACK starvation before the fix and passes for both backends afterward. An isolated Miri reproducer rejects the unboxed vector indexing pattern under Stacked Borrows and passes with boxed slots; this is not a full-backend Miri run.

Compatibility: adding the public UdpConfig::io field requires updates to exhaustive struct literals. Literals using struct update syntax keep compiling. Account for this source change in the next release; this PR does not bump the workspace version. Uses the workspace's existing io-uring dependency.

Follow-up syscall measurements (2026-09-09): main e59fe83, pre-ACK-fix 89b648c, and current implementation ab2d295. Separate saved binaries, five measured rounds after one discarded round per size/revision, rotating revision order; no tests/builds ran concurrently. Ryzen 9 9950X, Linux 7.1.5, release/native, IPv4 loopback, the same pinned cores and requested 16 MiB socket buffers. FLUX_BENCH_TRANSPORT=udp FLUX_BENCH_SCALE=16 FLUX_BENCH_SIZE=2k or 64k. Entries are medians of per-run statistics, not pooled percentiles.

Syscall workload Main MiB/s Before MiB/s Current MiB/s Change vs before Before → current p50 / p99 µs Before → current CPU ns/B
paced/2k 20 20 20 +0.0% 4.7 / 5.5 → 4.3 / 5.5 95.477 → 95.498
paced/64k 625 625 625 +0.0% 9.1 / 10.6 → 8.6 / 10.0 2.977 → 2.976
burst/2k 443 445 434 -2.5% 4.8 / 5.8 → 4.4 / 5.0 2.382 → 2.367
burst/64k 7,002 6,933 7,104 +2.5% 9.0 / 11.8 → 8.4 / 11.0 0.201 → 0.194
bcast/2k 565 566 564 -0.4% 17.1 / 95.2 → 17.0 / 103.1 1.755 → 1.751
bcast/64k 1,412 1,406 1,378 -2.0% 211.3 / 343.7 → 210.5 / 344.0 0.758 → 0.711

The ACK fix has a measured throughput cost of 2.5% for 2 KiB bursts and 2.0% for 64 KiB broadcasts in this run; 64 KiB bursts improve 2.5%. Paced throughput is unchanged. These are syscall comparisons, not refreshed io_uring speedup claims. The older io_uring table above remains attributed to its measured revision.

Each receive batch now scans all peers owned by the driver, filtering by socket, and emits at most one pending ACK per matching peer. The scan is O(total driver peers) per batch; the broadcast benchmark covers eight peers, not hundreds.

Default-parallel local validation passes all 116 network tests. The prior CI failure was a sustained-message sender disconnect, not a deadline expiry. Constraining local socket buffers to 212,992 bytes reproduced that disconnect; restoring large buffers on the same two CPUs passed the unchanged test on both backends. CI now sets rmem_max and wmem_max to 16 MiB so Linux can honor the test's requested socket capacity. Message counts, deadlines, assertions, and default test parallelism are unchanged. Hosted CI at fd57087 passes the full workspace suite with default parallelism: 365 passed, four existing ignored doc tests. Both shared sustained-message variants pass; lint and semver checks are green. An additional local run with CI compiler flags (RUSTFLAGS="") and two CPUs also passes all 116 network tests with default parallelism.

@gd-0
gd-0 requested a review from a team September 8, 2026 19:14
@gd-0
gd-0 marked this pull request as draft September 8, 2026 21:15
gd-0 added 3 commits September 8, 2026 22:21
Drop the redundant Box around pooled send slots (the Vec is never resized),
add a UdpSocket::ring accessor, skip the extra per-socket ACK scan on idle
io_uring polls, and dedupe the bench size filter.
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