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
+24 -18
View File
@@ -33,13 +33,15 @@ impl std::error::Error for LockTimeout {}
struct Wait {
pid: Pid,
seq: u64,
/// The wait's park-epoch (slot-word wait identity, see slot_state.rs).
/// Grants and timeouts wake via `unpark_at(pid, epoch)`; a stale entry
/// can neither be granted by mistake nor wake the wrong wait.
epoch: u32,
}
struct MutexState {
holder: Option<Pid>,
waiters: VecDeque<Wait>,
next_seq: u64,
default_timeout: Duration,
}
@@ -53,7 +55,6 @@ impl MutexCore {
state: StdMutex::new(MutexState {
holder: None,
waiters: VecDeque::new(),
next_seq: 0,
default_timeout,
}),
}
@@ -61,17 +62,17 @@ impl MutexCore {
}
impl TimerTarget for MutexCore {
fn on_timeout(&self, pid: Pid, wait_seq: u64) {
fn on_timeout(&self, pid: Pid, epoch: u32) {
let unpark = {
let mut st = self.state.lock().unwrap();
// Remove from waiters only if still there with matching seq.
// Remove from waiters only if still there with matching epoch.
// If the lock was already granted (holder == Some(pid)), the
// timer fired after the grant — treat as no-op; the actor
// will see `is_holder == true` and return Ok.
if st.holder == Some(pid) {
return;
}
let pos = st.waiters.iter().position(|w| w.pid == pid && w.seq == wait_seq);
let pos = st.waiters.iter().position(|w| w.pid == pid && w.epoch == epoch);
if pos.is_some() {
st.waiters.remove(pos.unwrap());
true
@@ -80,7 +81,7 @@ impl TimerTarget for MutexCore {
}
};
if unpark {
scheduler::unpark(pid);
scheduler::unpark_at(pid, epoch);
}
}
}
@@ -133,20 +134,25 @@ impl<T> Mutex<T> {
// Slow path: register as a waiter, set timeout, park.
let _np = scheduler::NoPreempt::enter();
let seq = {
let epoch = {
let mut st = self.core.state.lock().unwrap();
let seq = st.next_seq;
st.next_seq = st.next_seq.wrapping_add(1);
st.waiters.push_back(Wait { pid: me, seq });
seq
// begin_wait is lock-free — legal under the state lock; this
// makes the epoch atomic with the registration's visibility to
// grants and timeouts.
let epoch = scheduler::begin_wait();
st.waiters.push_back(Wait { pid: me, epoch });
epoch
};
let target: Arc<dyn TimerTarget> = self.core.clone();
let deadline = timer::deadline_from_now(timeout);
scheduler::insert_wait_timer(deadline, me, target, seq);
scheduler::insert_wait_timer(deadline, me, target, epoch);
scheduler::park_current();
// Resumed. Are we the holder?
// Resumed — precisely: only our grant or our timer can wake this
// wait (both epoch-stamped; a stop wake unwinds out of
// park_current). The one-shot interpretation below is therefore
// exhaustive. Are we the holder?
let is_holder = self.core.state.lock().unwrap().holder == Some(me);
if is_holder {
let value = self.value.lock().unwrap().take()
@@ -228,12 +234,12 @@ impl<T> Drop for MutexGuard<'_, T> {
let v = self.value.take().expect("MutexGuard: double drop");
*self.mutex.value.lock().unwrap() = Some(v);
let next_pid = {
let next = {
let mut st = self.mutex.core.state.lock().unwrap();
match st.waiters.pop_front() {
Some(w) => {
st.holder = Some(w.pid);
Some(w.pid)
Some((w.pid, w.epoch))
}
None => {
st.holder = None;
@@ -241,8 +247,8 @@ impl<T> Drop for MutexGuard<'_, T> {
}
}
};
if let Some(pid) = next_pid {
scheduler::unpark(pid);
if let Some((pid, epoch)) = next {
scheduler::unpark_at(pid, epoch);
}
}
}