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.
158 lines
5.7 KiB
Rust
158 lines
5.7 KiB
Rust
//! Sleep + wait-with-timeout timers.
|
|
//!
|
|
//! A min-heap of `(deadline, seq, reason)` entries lives on `SchedulerState`.
|
|
//! When an actor sleeps or starts a bounded wait (e.g. `mutex.lock()` with a
|
|
//! timeout), the runtime inserts an entry, marks the actor parked, and yields.
|
|
//! On every scheduler loop iteration the runtime pops all entries whose
|
|
//! deadline has passed and dispatches each according to its `Reason`:
|
|
//!
|
|
//! - `Sleep`: unpark the actor.
|
|
//! - `WaitTimeout`: call `on_timeout` on the registered target. The target
|
|
//! (e.g. a `Mutex`) decides whether the actor was actually still waiting
|
|
//! (timer fires first → unpark with error) or had already been granted
|
|
//! what it was waiting for (lock granted first → no-op).
|
|
//!
|
|
//! `BinaryHeap` is a max-heap; entries are wrapped in `Reverse` to get
|
|
//! min-heap behaviour.
|
|
//!
|
|
//! No cancellation. When a non-timer wakeup happens (e.g. lock granted
|
|
//! before timeout), the timer entry is left in the heap. It will be popped
|
|
//! eventually and the dispatch will observe "actor is no longer parked /
|
|
//! the wait's epoch was consumed" and no-op. Cost is ~32 bytes per stale
|
|
//! entry plus a few cycles on pop; acceptable given the upper bound is "one
|
|
//! entry per parked actor".
|
|
//!
|
|
//! Stale pids (slot reused since the timer was inserted) are filtered on
|
|
//! pop by the scheduler — same convention as the run queue.
|
|
|
|
use crate::pid::Pid;
|
|
use std::cmp::Reverse;
|
|
use std::collections::BinaryHeap;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// What to do when a timer entry's deadline arrives.
|
|
///
|
|
/// Held inside `Entry`, dispatched by the scheduler in `pop_due`.
|
|
pub enum Reason {
|
|
/// `sleep(d)`. Wake `pid` via the epoch-matched unpark: if anything
|
|
/// else (necessarily a terminal wake) already consumed the wait, the
|
|
/// entry is stale and no-ops at the CAS.
|
|
Sleep { epoch: u32 },
|
|
/// A bounded wait (`Mutex::lock_timeout`, `Receiver::recv_timeout`,
|
|
/// `select_timeout`). On expiry the scheduler calls
|
|
/// `target.on_timeout(pid, epoch)`. The target then decides whether
|
|
/// `pid` was actually still waiting (registration still present under
|
|
/// its lock), and if so takes the registration and unparks via
|
|
/// `unpark_at`. The epoch is the slot-word park-epoch — the runtime-wide
|
|
/// wait identity — so a stale entry is doubly inert: the registration
|
|
/// check misses, and even a racing unpark fails the word's epoch CAS.
|
|
WaitTimeout {
|
|
target: Arc<dyn TimerTarget>,
|
|
epoch: u32,
|
|
},
|
|
}
|
|
|
|
/// Callback the scheduler invokes when a `WaitTimeout` entry pops.
|
|
///
|
|
/// Implementors: do not touch `SchedulerState` other than via the public
|
|
/// `unpark` / channel APIs. The scheduler is mid-iteration when this fires.
|
|
pub trait TimerTarget: Send + Sync {
|
|
fn on_timeout(&self, pid: Pid, epoch: u32);
|
|
}
|
|
|
|
pub struct Entry {
|
|
pub deadline: Instant,
|
|
/// Insertion order, used purely as a tiebreaker so `Entry: Ord` works
|
|
/// without having to compare the `Reason` payload (which contains an
|
|
/// `Rc<dyn TimerTarget>` and isn't `Ord`).
|
|
seq: u64,
|
|
pub pid: Pid,
|
|
pub reason: Reason,
|
|
}
|
|
|
|
impl PartialEq for Entry {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.deadline == other.deadline && self.seq == other.seq
|
|
}
|
|
}
|
|
impl Eq for Entry {}
|
|
|
|
impl Ord for Entry {
|
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
// Earlier deadline first; ties broken by insertion order so the
|
|
// ordering is total. `Reason` and `Pid` deliberately don't
|
|
// participate.
|
|
self.deadline.cmp(&other.deadline).then_with(|| self.seq.cmp(&other.seq))
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for Entry {
|
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct Timers {
|
|
/// Reverse-wrapped so the smallest deadline is at the top.
|
|
heap: BinaryHeap<Reverse<Entry>>,
|
|
/// Monotonic counter for the tiebreaker `seq` field.
|
|
next_seq: u64,
|
|
}
|
|
|
|
impl Timers {
|
|
pub fn new() -> Self {
|
|
Self { heap: BinaryHeap::new(), next_seq: 0 }
|
|
}
|
|
|
|
/// Insert a `Sleep` timer. Convenience for the common case.
|
|
pub fn insert_sleep(&mut self, deadline: Instant, pid: Pid, epoch: u32) {
|
|
self.insert(deadline, pid, Reason::Sleep { epoch });
|
|
}
|
|
|
|
/// Insert an arbitrary timer entry.
|
|
pub fn insert(&mut self, deadline: Instant, pid: Pid, reason: Reason) {
|
|
let seq = self.next_seq;
|
|
self.next_seq = self.next_seq.wrapping_add(1);
|
|
self.heap.push(Reverse(Entry { deadline, seq, pid, reason }));
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.heap.is_empty()
|
|
}
|
|
|
|
/// Drop all pending entries. Called by the scheduler when it has decided
|
|
/// no actor is live: any remaining timer is orphaned and exists only to be
|
|
/// discarded so it can't keep the runtime alive.
|
|
pub fn clear(&mut self) {
|
|
self.heap.clear();
|
|
}
|
|
|
|
/// Soonest pending deadline, or `None` if the heap is empty.
|
|
pub fn peek_deadline(&self) -> Option<Instant> {
|
|
self.heap.peek().map(|r| r.0.deadline)
|
|
}
|
|
|
|
/// Pop every entry whose deadline is ≤ `now`, in deadline order.
|
|
/// The scheduler dispatches each entry by inspecting `entry.reason`.
|
|
pub fn pop_due(&mut self, now: Instant) -> Vec<Entry> {
|
|
let mut out = Vec::new();
|
|
while let Some(r) = self.heap.peek() {
|
|
if r.0.deadline <= now {
|
|
out.push(self.heap.pop().unwrap().0);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
}
|
|
|
|
/// Wall-clock duration helper exposed for `sleep` and `lock_timeout`.
|
|
pub fn deadline_from_now(duration: Duration) -> Instant {
|
|
Instant::now()
|
|
.checked_add(duration)
|
|
.unwrap_or_else(Instant::now)
|
|
}
|