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
@@ -883,6 +883,53 @@ fn receive_retries_false_readiness_and_shares_packet_classification() {
|
||||
assert_eq!(server.finish().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn receive_retries_interrupted_syscall_and_shares_packet_classification() {
|
||||
let server = start_server(move |socket, tx| {
|
||||
let (params, peer) = open_session(&socket, &tx, None);
|
||||
let (probe_packet, _) = recv_packet(&socket, &tx);
|
||||
let sequence = echo_request_sequence(&probe_packet, None);
|
||||
socket
|
||||
.send_to(
|
||||
&echo_reply(¶ms, sequence, TOKEN, flags::FLAG_REPLY, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
runtime().block_on(async {
|
||||
let mut client = opened_client(server.addr, 0).await;
|
||||
client.send_probe().await.unwrap();
|
||||
client.test_hooks.inject_recv_interrupted.set(2);
|
||||
|
||||
assert!(matches!(
|
||||
client.recv().await.unwrap().as_slice(),
|
||||
[ClientEvent::EchoReply { seq: 0, .. }]
|
||||
));
|
||||
});
|
||||
assert_eq!(server.finish().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_retries_interrupted_syscall_within_the_same_attempt() {
|
||||
let server = start_open_server(None, 0);
|
||||
runtime().block_on(async {
|
||||
let mut client = AsyncClient::connect(config(server.addr, None, 0))
|
||||
.await
|
||||
.unwrap();
|
||||
client.test_hooks.inject_recv_interrupted.set(3);
|
||||
|
||||
assert!(matches!(
|
||||
client.open().await,
|
||||
Ok(OpenOutcome::Started { .. })
|
||||
));
|
||||
// Only one open request reached the server: interruptions were
|
||||
// absorbed inside the single open attempt, with no retransmit.
|
||||
assert_eq!(client.test_hooks.send_attempts.get(), 1);
|
||||
});
|
||||
server.finish();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_close_dscp_failure_preserves_events_and_logical_ownership() {
|
||||
let server = start_peer_close_server();
|
||||
|
||||
Reference in new issue
Block a user