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:
+55
-67
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user