//! 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 / //! wait_seq is stale" 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 { /// `loom::sleep(d)`. Unpark `pid` unconditionally (modulo the usual /// "still parked?" check the scheduler applies). Sleep, /// A bounded wait — currently only `Mutex::lock_timeout`. On expiry the /// scheduler calls `target.on_timeout(pid, wait_seq)`. The target then /// decides whether `pid` was actually still waiting, and if so unparks /// it with whatever error the wait was bounded for. `wait_seq` lets the /// target tell apart "this wait" from "a later wait by the same actor /// on the same target". WaitTimeout { target: Arc, wait_seq: u64, }, } /// 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, wait_seq: u64); } 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` 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 { Some(self.cmp(other)) } } #[derive(Default)] pub struct Timers { /// Reverse-wrapped so the smallest deadline is at the top. heap: BinaryHeap>, /// 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) { self.insert(deadline, pid, Reason::Sleep); } /// 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() } /// Soonest pending deadline, or `None` if the heap is empty. pub fn peek_deadline(&self) -> Option { 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 { 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) }