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
+55 -67
View File
@@ -39,27 +39,22 @@ pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
parked_receiver: None,
senders: 1,
receiver_alive: true,
cur_wait: None,
next_wait_seq: 0,
timed_out: false,
}));
(Sender { inner: inner.clone() }, Receiver { inner })
}
struct Inner<T> {
queue: VecDeque<T>,
parked_receiver: Option<Pid>,
/// The parked receiver's `(pid, park-epoch)`. The epoch is the slot
/// word's runtime-wide wait identity (see slot_state.rs): wakers call
/// `unpark_at(pid, epoch)`, so an entry left over from an already-woken
/// wait — a `select` loser arm, a satisfied `recv_timeout`'s timer — is
/// inert: the wake fails the word's epoch CAS and no-ops. This replaces
/// the old per-channel `cur_wait`/`next_wait_seq`/`timed_out` trio: wait
/// identity now exists exactly once, in the slot word.
parked_receiver: Option<(Pid, u32)>,
senders: usize,
receiver_alive: bool,
/// The seq of the receiver's current *bounded* wait (`recv_timeout`), or
/// `None` outside one. Lets `on_timeout` tell "this wait" apart from "a
/// later wait on the same channel" — same convention as `Mutex`.
cur_wait: Option<u64>,
/// Monotonic source for `cur_wait` seqs.
next_wait_seq: u64,
/// Set by `on_timeout` when it cancels the current bounded wait; consumed
/// by the woken receiver to distinguish a timeout from a message wake.
timed_out: bool,
}
pub struct Sender<T> {
@@ -129,8 +124,8 @@ impl<T> Drop for Sender<T> {
None
}
};
if let Some(pid) = unpark {
crate::scheduler::unpark(pid);
if let Some((pid, epoch)) = unpark {
crate::scheduler::unpark_at(pid, epoch);
}
}
}
@@ -151,9 +146,9 @@ impl<T> Sender<T> {
g.queue.push_back(value);
g.parked_receiver.take()
};
if let Some(pid) = unpark {
if let Some((pid, epoch)) = unpark {
crate::te!(crate::trace::Event::Send { sender: crate::actor::current_pid().unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)), receiver: Some(pid) });
crate::scheduler::unpark(pid);
crate::scheduler::unpark_at(pid, epoch);
} else {
crate::te!(crate::trace::Event::Send { sender: crate::actor::current_pid().unwrap_or(crate::pid::Pid::new(u32::MAX, u32::MAX)), receiver: None });
}
@@ -178,7 +173,10 @@ impl<T> Receiver<T> {
g.parked_receiver.is_none(),
"channel has more than one receiver"
);
g.parked_receiver = Some(me);
// begin_wait is lock-free — legal under the Channel lock;
// registering in the same critical section makes the epoch
// atomic with the senders' view of the registration.
g.parked_receiver = Some((me, crate::scheduler::begin_wait()));
crate::te!(crate::trace::Event::RecvPark(me));
}
// Release the lock before parking — the unparker will need it.
@@ -192,14 +190,21 @@ impl<T> Receiver<T> {
/// `timeout` has elapsed, returning [`RecvTimeoutError::Timeout`].
///
/// Built on the same timer machinery as `Mutex::lock_timeout`: the wait
/// registers a `WaitTimeout` entry tagged with a per-wait seq; on expiry
/// the channel (as the [`TimerTarget`](crate::timer::TimerTarget)) checks
/// whether *this* wait is still parked and, only then, cancels it. A wake
/// that races the deadline resolves message-first: if a message is
/// available when the receiver runs, it is delivered even if the timer
/// had already fired. A satisfied or abandoned wait leaves its timer
/// entry to expire as a no-op (seq mismatch), per the no-cancellation
/// convention in `timer.rs`.
/// registers a `WaitTimeout` entry stamped with the wait's park-epoch;
/// on expiry the channel (as the
/// [`TimerTarget`](crate::timer::TimerTarget)) checks whether *this*
/// wait is still parked and, only then, cancels it. A wake that races
/// the deadline resolves message-first: if a message is available when
/// the receiver runs, it is delivered even if the timer had already
/// fired. A satisfied or abandoned wait leaves its timer entry to expire
/// as a no-op (registration gone; epoch consumed), per the
/// no-cancellation convention in `timer.rs`.
///
/// The wake is classified from state alone — wakes are precise (the only
/// stamped wakers of this wait are a send, the last-sender drop, and the
/// timer; a stop wake unwinds out of `park_current` and never reaches
/// the classification), so: message queued → `Ok`; `senders == 0` →
/// `Disconnected`; neither → it was the timer → `Timeout`.
///
/// `Duration::ZERO` is a valid timeout: it parks until the immediately-
/// due timer is drained, then reports `Timeout` unless a message was
@@ -212,7 +217,7 @@ impl<T> Receiver<T> {
.expect("recv_timeout() called outside an actor");
// Fast path + wait registration, one critical section.
let seq;
let epoch;
{
let mut g = self.inner.lock();
if let Some(v) = g.queue.pop_front() {
@@ -225,11 +230,8 @@ impl<T> Receiver<T> {
g.parked_receiver.is_none(),
"channel has more than one receiver"
);
seq = g.next_wait_seq;
g.next_wait_seq = g.next_wait_seq.wrapping_add(1);
g.cur_wait = Some(seq);
g.timed_out = false;
g.parked_receiver = Some(me);
epoch = crate::scheduler::begin_wait();
g.parked_receiver = Some((me, epoch));
crate::te!(crate::trace::Event::RecvPark(me));
}
@@ -239,34 +241,18 @@ impl<T> Receiver<T> {
// protocol makes the park below return immediately in that case.
let deadline = crate::timer::deadline_from_now(timeout);
let target: std::sync::Arc<dyn crate::timer::TimerTarget> = self.inner.clone();
crate::scheduler::insert_wait_timer(deadline, me, target, seq);
crate::scheduler::insert_wait_timer(deadline, me, target, epoch);
loop {
crate::scheduler::park_current();
crate::te!(crate::trace::Event::RecvWake(crate::actor::current_pid().unwrap()));
let mut g = self.inner.lock();
if let Some(v) = g.queue.pop_front() {
g.cur_wait = None;
g.timed_out = false;
return Ok(v);
}
if g.senders == 0 {
g.cur_wait = None;
g.timed_out = false;
return Err(RecvTimeoutError::Disconnected);
}
if g.timed_out {
g.cur_wait = None;
g.timed_out = false;
return Err(RecvTimeoutError::Timeout);
}
// Defensive: no current wake source reaches here (a send leaves a
// message, closure zeroes senders, the timer sets timed_out, and
// a stop unwinds out of park_current) — but if one ever does,
// re-register and keep waiting; cur_wait == Some(seq) keeps the
// already-armed timer valid for this wait.
g.parked_receiver = Some(me);
crate::scheduler::park_current();
crate::te!(crate::trace::Event::RecvWake(crate::actor::current_pid().unwrap()));
let mut g = self.inner.lock();
if let Some(v) = g.queue.pop_front() {
return Ok(v);
}
if g.senders == 0 {
return Err(RecvTimeoutError::Disconnected);
}
Err(RecvTimeoutError::Timeout)
}
/// Selective receive: remove and return the first queued message for which
@@ -300,7 +286,7 @@ impl<T> Receiver<T> {
g.parked_receiver.is_none(),
"channel has more than one receiver"
);
g.parked_receiver = Some(me);
g.parked_receiver = Some((me, crate::scheduler::begin_wait()));
crate::te!(crate::trace::Event::RecvPark(me));
}
// Release the lock before parking — the unparker will need it.
@@ -346,16 +332,18 @@ impl<T> Receiver<T> {
// ---------------------------------------------------------------------------
impl<T: Send + 'static> crate::timer::TimerTarget for RawMutex<Inner<T>> {
fn on_timeout(&self, pid: Pid, wait_seq: u64) {
// Cancel the wait only if THIS wait (seq match) is still parked. If a
// sender already took `parked_receiver`, the receiver is waking with
// a message — message wins, the timer no-ops. If `cur_wait` moved on,
// this entry is stale (the wait was satisfied or abandoned) — no-op.
fn on_timeout(&self, pid: Pid, epoch: u32) {
// Cancel the wait only if THIS wait (epoch match) is still
// registered. If a sender already took `parked_receiver`, the
// receiver is waking with a message — message wins, the timer
// no-ops. If a later wait by the same receiver is registered, the
// epoch mismatches — stale entry, no-op. (The unpark_at would fail
// its word CAS in either case anyway; checking under the lock keeps
// the registration bookkeeping exact.)
let unpark = {
let mut g = self.lock();
if g.cur_wait == Some(wait_seq) && g.parked_receiver == Some(pid) {
if g.parked_receiver == Some((pid, epoch)) {
g.parked_receiver = None;
g.timed_out = true;
true
} else {
false
@@ -364,7 +352,7 @@ impl<T: Send + 'static> crate::timer::TimerTarget for RawMutex<Inner<T>> {
// Unpark outside the channel lock — it may take the run-queue lock;
// legal under a Channel lock, but pointless to nest.
if unpark {
crate::scheduler::unpark(pid);
crate::scheduler::unpark_at(pid, epoch);
}
}
}