loopd: read on a socket whose peer closed, instead of failing on macOS

macOS refuses every socket option with EINVAL once the peer has closed
(XNU sosetoptlock, bsd/kern/uipc_socket.c), even with unread data still
buffered. loopd set a read timeout before each read, so on macOS every
frame or response that arrived just before the peer closed was lost:
BrokerPort reported the broker unavailable with "os error 22", and the
llama client failed the same way. Twelve loopd test binaries failed on
macOS; Linux never refuses, so the gate on Talos did not see it.

Both places now go through socket::set_read_timeout, which on Apple
targets takes that one refusal as success: a socket shut in both
directions returns its data or the end at once and cannot block. A
zero timeout is still an error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 18:27:31 -07:00
co-authored by Claude Opus 5.5
parent d21baa2954
commit 00a85c124d
5 changed files with 71 additions and 4 deletions
+47
View File
@@ -0,0 +1,47 @@
//! Setting a read timeout on a Unix socket whose peer has already gone.
use std::io::{ErrorKind, Read, Write};
use std::os::unix::net::UnixStream;
use std::time::Duration;
use loopd::socket::set_read_timeout;
#[test]
fn a_peer_that_wrote_and_closed_still_delivers_what_it_wrote() {
let (mut ours, mut theirs) = UnixStream::pair().unwrap();
theirs.write_all(b"frame").unwrap();
drop(theirs);
set_read_timeout(&ours, Duration::from_secs(5)).unwrap();
let mut got = Vec::new();
ours.read_to_end(&mut got).unwrap();
assert_eq!(got, b"frame");
}
#[test]
fn a_peer_that_closed_without_writing_reads_as_the_end() {
let (mut ours, theirs) = UnixStream::pair().unwrap();
drop(theirs);
set_read_timeout(&ours, Duration::from_secs(5)).unwrap();
let mut byte = [0u8; 1];
assert_eq!(ours.read(&mut byte).unwrap(), 0);
}
#[test]
fn a_live_peer_gets_the_timeout() {
let (mut ours, _theirs) = UnixStream::pair().unwrap();
set_read_timeout(&ours, Duration::from_millis(20)).unwrap();
let mut byte = [0u8; 1];
let err = ours.read(&mut byte).unwrap_err();
assert!(
matches!(err.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut),
"{err:?}"
);
}
#[test]
fn a_zero_timeout_is_still_refused() {
let (ours, theirs) = UnixStream::pair().unwrap();
drop(theirs);
let err = set_read_timeout(&ours, Duration::ZERO).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}