diff --git a/crates/punktfunk-core/src/transport/udp/mod.rs b/crates/punktfunk-core/src/transport/udp/mod.rs index c7d6d427..cb58ba85 100644 --- a/crates/punktfunk-core/src/transport/udp/mod.rs +++ b/crates/punktfunk-core/src/transport/udp/mod.rs @@ -45,12 +45,20 @@ const RECV_BUF: usize = MAX_DATAGRAM_BYTES + 1; /// died at full rate over WiFi). Same lossy-drop contract as `WouldBlock`; FEC + the next frame /// recover. Asynchronous network-path blips (`ENETUNREACH`/`EHOSTUNREACH`/`ENETDOWN`/`EHOSTDOWN`) /// are droppable for the same reason a stale ICMP is. +/// - Windows `WSAENOBUFS` (10055): the exact analogue of unix `ENOBUFS` — a high-bitrate keyframe +/// burst (one `WSASendMsg` USO super-buffer is up to ~512 segments ≈ 700 KB) momentarily exhausts +/// the socket send buffer / AFD non-paged pool, and Winsock reports `WSAENOBUFS`, which Rust maps +/// to `ErrorKind::Uncategorized` (so the `WouldBlock` arm misses it, exactly like unix `ENOBUFS`). +/// Without treating it as transient a Windows host tears the whole session down under load +/// (observed live: `native::stream` "send failed — stopping stream" on a paced video burst). Same +/// lossy-drop contract; FEC + the next frame recover. The `WSAENET*`/`WSAEHOST*` family is the +/// Windows counterpart of the droppable unix network-path blips above. fn is_transient_io(e: &std::io::Error) -> bool { use std::io::ErrorKind::{ConnectionRefused, ConnectionReset, WouldBlock}; if matches!(e.kind(), WouldBlock | ConnectionRefused | ConnectionReset) { return true; } - // `ENOBUFS` & friends have no stable `ErrorKind`, so match the raw errno (unix only). + // `ENOBUFS` & friends have no stable `ErrorKind`, so match the raw errno. #[cfg(unix)] { matches!( @@ -62,7 +70,20 @@ fn is_transient_io(e: &std::io::Error) -> bool { | Some(libc::EHOSTDOWN) ) } - #[cfg(not(unix))] + // Windows Winsock codes (WSAE*), raw like the sibling `uso_unsupported`. WSAEWOULDBLOCK (10035) + // already maps to `ErrorKind::WouldBlock` above, so it isn't repeated here. + #[cfg(windows)] + { + matches!( + e.raw_os_error(), + Some(10055) // WSAENOBUFS — tx queue / send buffer full (the dominant high-bitrate drop) + | Some(10051) // WSAENETUNREACH + | Some(10065) // WSAEHOSTUNREACH + | Some(10050) // WSAENETDOWN + | Some(10064) // WSAEHOSTDOWN + ) + } + #[cfg(not(any(unix, windows)))] { false } @@ -324,6 +345,53 @@ mod tests { } } + /// The raw-errno tx-queue-full / network-blip codes have no stable `ErrorKind` (they surface as + /// `Uncategorized`), so they only get caught by the platform `raw_os_error()` arms. A burst that + /// momentarily exhausts the send buffer must stay a lossy drop, never a teardown — this is the + /// regression guard for the Windows `WSAENOBUFS` (10055) session crash and the unix `ENOBUFS` + /// wlan-driver case. Gated per platform because a code is only classified on its own OS. + #[test] + fn transient_io_covers_raw_tx_queue_and_path_codes() { + use std::io::Error; + + #[cfg(unix)] + { + for code in [ + libc::ENOBUFS, + libc::ENETUNREACH, + libc::EHOSTUNREACH, + libc::ENETDOWN, + libc::EHOSTDOWN, + ] { + assert!( + is_transient_io(&Error::from_raw_os_error(code)), + "unix errno {code} should be transient" + ); + } + // A genuine failure with no stable ErrorKind must still tear down. + assert!( + !is_transient_io(&Error::from_raw_os_error(libc::EACCES)), + "EACCES must stay fatal" + ); + } + + #[cfg(windows)] + { + // WSAENOBUFS / WSAENETUNREACH / WSAEHOSTUNREACH / WSAENETDOWN / WSAEHOSTDOWN. + for code in [10055, 10051, 10065, 10050, 10064] { + assert!( + is_transient_io(&Error::from_raw_os_error(code)), + "WSA code {code} should be transient" + ); + } + // WSAEACCES (10013) — a real failure that must stay fatal. + assert!( + !is_transient_io(&Error::from_raw_os_error(10013)), + "WSAEACCES must stay fatal" + ); + } + } + /// `send_batch` delivers a whole frame's worth of packets over real loopback UDP — exercising /// the `sendmmsg` path on Linux (the scalar-loop default elsewhere). 100 × 200 B = 20 KB fits /// the socket buffer, so loopback is lossless and every packet must arrive intact + in order.