refactor(wakes): epoch-stamp every registration-based wake; retire per-primitive wait seqs

The slot epoch is THE wait identity, so the hand-rolled per-primitive copies
go away: channel loses cur_wait/next_wait_seq/timed_out, mutex loses Wait.seq
and next_seq, TimerTarget::on_timeout takes the epoch. Registrations become
(pid, epoch) — channel parked_receiver, mutex waiters, io fd waiters,
Blocking io completions, sleep timers, joiner lists — and their wakers move
to unpark_at. begin_wait is lock-free, so each primitive opens the wait
inside the same critical section that publishes the registration.

recv_timeout's wake-classification loop collapses: wakes are precise, so
queue → Ok, senders==0 → Disconnected, else Timeout — the 'Defensive'
re-register branch is now unreachable by protocol, not by audit. Same for
Mutex::lock_timeout's one-shot park.

End-state invariant, auditable in one sentence: the only wildcard wake is
request_stop, which is terminal.
This commit is contained in:
smarm
2026-06-10 07:15:29 +00:00
parent 4913835c02
commit 400854ac5d
7 changed files with 185 additions and 147 deletions
+11 -7
View File
@@ -84,6 +84,9 @@ use std::thread::JoinHandle as OsJoinHandle;
pub type IoResult = Result<Box<dyn Any + Send>, Box<dyn Any + Send>>;
struct Request {
/// The submitter's park-epoch — carried through to the `Blocking`
/// completion so the wake is epoch-matched.
epoch: u32,
pid: Pid,
/// The work to perform. Returns the wire-form result directly.
work: Box<dyn FnOnce() -> IoResult + Send>,
@@ -93,7 +96,7 @@ struct Request {
pub enum Completion {
/// A `block_on_io` closure has finished (Ok = return value, Err = panic
/// payload).
Blocking { pid: Pid, result: IoResult },
Blocking { pid: Pid, epoch: u32, result: IoResult },
/// An fd registered via `wait_readable`/`wait_writable` is ready. The
/// scheduler looks up the parked pid in `waiters`, unparks it, and
/// removes the entry. `pid` isn't in this variant because the epoll
@@ -131,7 +134,7 @@ pub struct IoThread {
/// One parked actor per registered fd. Populated by `wait_readable` /
/// `wait_writable` and drained by the scheduler when a `FdReady`
/// completion is processed.
pub waiters: HashMap<RawFd, Pid>,
pub waiters: HashMap<RawFd, (Pid, u32)>,
// ----- Threads -----
@@ -231,12 +234,12 @@ impl IoThread {
}
/// Hand a request to the pool. Increments `outstanding`.
pub fn submit(&mut self, pid: Pid, work: Box<dyn FnOnce() -> IoResult + Send>) {
pub fn submit(&mut self, pid: Pid, epoch: u32, work: Box<dyn FnOnce() -> IoResult + Send>) {
self.outstanding += 1;
// Send can only fail if the pool has hung up, which only happens
// on shutdown. submit during shutdown is a bug.
self.tx
.send(Request { pid, work })
.send(Request { pid, epoch, work })
.expect("io pool hung up unexpectedly");
}
@@ -265,6 +268,7 @@ impl IoThread {
&mut self,
fd: RawFd,
pid: Pid,
epoch: u32,
readable: bool,
writable: bool,
) -> io::Result<()> {
@@ -303,7 +307,7 @@ impl IoThread {
if r < 0 {
return Err(io::Error::last_os_error());
}
self.waiters.insert(fd, pid);
self.waiters.insert(fd, (pid, epoch));
Ok(())
}
@@ -369,7 +373,7 @@ fn pool_loop(
completions: Arc<Mutex<VecDeque<Completion>>>,
wake_write: RawFd,
) {
while let Ok(Request { pid, work }) = rx.recv() {
while let Ok(Request { pid, epoch, work }) = rx.recv() {
let result: IoResult = match panic::catch_unwind(panic::AssertUnwindSafe(work)) {
Ok(r) => r,
Err(payload) => Err(payload),
@@ -377,7 +381,7 @@ fn pool_loop(
completions
.lock()
.unwrap()
.push_back(Completion::Blocking { pid, result });
.push_back(Completion::Blocking { pid, epoch, result });
wake_scheduler(wake_write);
}
}