fix(io): retry interrupted receives
Real signal delivery on Linux (observed via x86_64-musl workspace testing) can interrupt blocking and nonblocking recv/recvmsg calls with EINTR. Several production and test receive paths treated that as a fatal error rather than a transient interruption: - Client::open's blocking receive loop and Client::recv_once/ recv_available's blocking receive returned ClientError::Socket on Interrupted instead of retrying within their existing timeout budget. - AsyncClient::poll_open and poll_recv's nonblocking recvmsg path (Linux ancillary) failed the same way instead of retrying through the existing readiness/work-budget machinery. - The server's ancillary receive loop could terminate the listener on an interrupted nonblocking recvmsg. - The client's MSG_ERRQUEUE TX-timestamp drain turned an interrupted read of optional metadata into a fatal ClientError::Socket. - Fake-server test helpers panicked or misclassified EINTR as a timeout instead of retrying it. Interrupted is now retried transparently everywhere a receive can observe it, while preserving each path's existing timing contract: - Blocking Open reuses its already-absolute per-attempt deadline, so interruptions resume within the same attempt without retransmitting or extending the deadline. - recv_once/recv_available compute one absolute deadline per logical receive (when a timeout is configured) so repeated interruptions cannot extend it past the configured timeout, and restore the socket's configured read timeout afterward so a later call in the same recv_available budget is unaffected. - AsyncClient's poll_open already bounds its retry loop with OPEN_POLL_WORK_BUDGET; poll_recv gains an equivalent RECV_POLL_WORK_BUDGET, since unlike WouldBlock, Interrupted does not clear Tokio's readiness state, so an unbounded retry loop was otherwise possible under sustained signal delivery. - The server's receive loop retries Interrupted the same way it already retries WouldBlock, yielding to the runtime after a bounded number of consecutive interruptions for the same reason. - The error-queue drain treats an interrupted read as no record obtained this drain (deferring to the next natural drain point) rather than a socket error, matching its existing EWOULDBLOCK handling. Deterministic regression tests use a small test-only injection hook (mirroring existing send-injection test hooks) rather than real signal timing, covering: Open completing despite interruptions with no retransmit, Open's deadline not extending under repeated interruptions, recv_once classifying a reply normally despite interruptions, recv_once's timeout not extending under repeated interruptions, and the AsyncClient equivalents for open and recv. No protocol, session, or timeout-classification behavior changes; no new dependencies; send-side EINTR handling is unchanged and out of scope.
This commit is contained in:
10 files changed
+448
-32
No files matched your search
@@ -4,7 +4,7 @@
|
||||
mod in_tree_server;
|
||||
|
||||
use std::{
|
||||
io::Read,
|
||||
io::{ErrorKind, Read},
|
||||
net::{SocketAddr, UdpSocket},
|
||||
process::{Command, Output, Stdio},
|
||||
sync::mpsc,
|
||||
@@ -1177,18 +1177,35 @@ fn start_open_failure_server_inner(
|
||||
FakeServer { addr, done }
|
||||
}
|
||||
|
||||
/// A fake server's `recv_from`, retrying an interrupted syscall
|
||||
/// (`EINTR`/`ErrorKind::Interrupted`) instead of panicking on it: real
|
||||
/// signal delivery (e.g. process-group signals during CI) can interrupt a
|
||||
/// blocking receive here without there being anything actually wrong.
|
||||
fn recv_request(socket: &UdpSocket) -> (Vec<u8>, SocketAddr) {
|
||||
let mut buf = [0_u8; 2048];
|
||||
let (size, peer) = socket.recv_from(&mut buf).unwrap();
|
||||
(buf[..size].to_vec(), peer)
|
||||
loop {
|
||||
match socket.recv_from(&mut buf) {
|
||||
Ok((size, peer)) => return (buf[..size].to_vec(), peer),
|
||||
Err(err) if err.kind() == ErrorKind::Interrupted => continue,
|
||||
Err(err) => panic!("fake server recv_from failed: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`recv_request`], but treats any non-interrupting error (including a
|
||||
/// configured read timeout) as "nothing more to receive" rather than a
|
||||
/// failure, since callers use this to detect the peer going quiet. An
|
||||
/// interrupted syscall carries no such information and is retried instead of
|
||||
/// being misclassified as quiet.
|
||||
fn recv_request_timeout(socket: &UdpSocket) -> Option<(Vec<u8>, SocketAddr)> {
|
||||
let mut buf = [0_u8; 2048];
|
||||
socket
|
||||
.recv_from(&mut buf)
|
||||
.ok()
|
||||
.map(|(size, peer)| (buf[..size].to_vec(), peer))
|
||||
loop {
|
||||
match socket.recv_from(&mut buf) {
|
||||
Ok((size, peer)) => return Some((buf[..size].to_vec(), peer)),
|
||||
Err(err) if err.kind() == ErrorKind::Interrupted => continue,
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_reply(flags: u8, token: u64, params: &Params) -> Vec<u8> {
|
||||
|
||||
Reference in new issue
Block a user