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
+33 -8
View File
@@ -96,7 +96,10 @@ impl JoinHandle {
Some(cold.outcome.take().expect("Done slot must have outcome"))
}
crate::slot_state::Status::Live => {
cold.waiters.push(me);
// begin_wait is lock-free, legal under the cold lock;
// registering under it makes the epoch atomic with
// the check-Done-or-register linearization point.
cold.waiters.push((me, begin_wait()));
None
}
}
@@ -219,11 +222,30 @@ pub fn park_current() {
}
pub fn unpark(pid: Pid) {
// The whole protocol lives on the slot's packed word (gen + state in one
// CAS) — see Slot::unpark_action. No runtime lock unless we enqueue.
// The whole protocol lives on the slot's packed word (gen + epoch +
// state in one CAS) — see slot_state.rs. No runtime lock unless we
// enqueue. WILDCARD form: reserved for terminal wakes (request_stop);
// every registration-based waker must use `unpark_at`.
let _ = try_with_runtime(|inner| inner.unpark(pid));
}
/// Epoch-matched unpark: wake `pid` only if its current wait is still the
/// one this waker registered for. The form every registration-based waker
/// (channel senders, mutex grants, wait-timers, io completions, joiner
/// wakes) must use — see slot_state.rs for the consuming-wake rules.
pub(crate) fn unpark_at(pid: Pid, epoch: u32) {
let _ = try_with_runtime(|inner| inner.unpark_at(pid, epoch));
}
/// Open a new wait for the CURRENT actor: bump its park-epoch and return
/// it. Call once per wait, before registering `(pid, epoch)` with any
/// waker. Lock-free (one CAS on the own slot word), so it is legal under
/// any lock, including a Channel-class lock.
pub(crate) fn begin_wait() -> u32 {
let me = current_pid().expect("begin_wait() called outside an actor");
with_runtime(|inner| inner.begin_wait(me))
}
/// Request cooperative cancellation of `pid`.
///
/// Sets the actor's stop flag and wakes it so it observes the flag promptly:
@@ -281,8 +303,9 @@ impl Drop for NoPreempt {
pub fn sleep(duration: std::time::Duration) {
let me = current_pid().expect("sleep() called outside an actor");
let _np = NoPreempt::enter();
let epoch = begin_wait();
let deadline = crate::timer::deadline_from_now(duration);
with_runtime(|inner| inner.timers.lock().unwrap().insert_sleep(deadline, me));
with_runtime(|inner| inner.timers.lock().unwrap().insert_sleep(deadline, me, epoch));
park_current();
}
@@ -290,13 +313,13 @@ pub fn insert_wait_timer(
deadline: std::time::Instant,
pid: Pid,
target: std::sync::Arc<dyn crate::timer::TimerTarget>,
wait_seq: u64,
epoch: u32,
) {
with_runtime(|inner| {
inner.timers.lock().unwrap().insert(
deadline,
pid,
crate::timer::Reason::WaitTimeout { target, wait_seq },
crate::timer::Reason::WaitTimeout { target, epoch },
);
});
}
@@ -317,9 +340,10 @@ where
});
{
let _np = NoPreempt::enter();
let epoch = begin_wait();
with_runtime(|inner| {
let mut io = inner.io.lock().unwrap();
io.as_mut().expect("io thread not started").submit(me, work);
io.as_mut().expect("io thread not started").submit(me, epoch, work);
});
park_current();
}
@@ -351,9 +375,10 @@ pub fn wait_writable(fd: std::os::fd::RawFd) -> std::io::Result<()> {
fn wait_fd(fd: std::os::fd::RawFd, readable: bool, writable: bool) -> std::io::Result<()> {
let me = current_pid().expect("wait_*() called outside an actor");
let _np = NoPreempt::enter();
let epoch = begin_wait();
with_runtime(|inner| {
let mut io = inner.io.lock().unwrap();
io.as_mut().expect("io thread not started").epoll_register(fd, me, readable, writable)
io.as_mut().expect("io thread not started").epoll_register(fd, me, epoch, readable, writable)
})?;
park_current();
Ok(())