timer: sleep(duration) via min-heap of (deadline, pid)

Adds a BinaryHeap of timer entries on SchedulerState. sleep() inserts
an entry and parks; schedule_loop pops due entries each iteration and
unparks them. When the run queue is empty but timers are pending, the
OS thread sleeps until the soonest deadline.

Single-threaded only; thread::sleep is fine because no other thread
can wake us. The IO thread coming next will need a Condvar or pipe
wakeup to break this OS-sleep early.
This commit is contained in:
Claude
2026-05-22 05:22:55 +00:00
parent 6c48caecab
commit 2cf75febdc
4 changed files with 253 additions and 2 deletions

89
src/timer.rs Normal file
View File

@@ -0,0 +1,89 @@
//! Sleep timers.
//!
//! A min-heap of `(deadline, Pid)` entries lives on `SchedulerState`. When
//! an actor calls `sleep`, the runtime inserts the entry, marks the actor
//! parked, and yields. On every scheduler loop iteration the runtime pops
//! all entries whose deadline has passed and unparks them. When the run
//! queue is empty but the heap is not, the runtime sleeps the OS thread
//! until the soonest deadline, then re-checks.
//!
//! `BinaryHeap` is a max-heap, so entries are stored with their deadline
//! wrapped in `Reverse` to get min-heap behaviour.
//!
//! Stale pids (slot reused since the timer was inserted) are detected on
//! `due_pids` pop and silently dropped — same convention as the run queue.
use crate::pid::Pid;
use std::cmp::Reverse;
use std::collections::BinaryHeap;
use std::time::{Duration, Instant};
#[derive(PartialEq, Eq)]
pub struct Entry {
pub deadline: Instant,
pub pid: Pid,
}
impl Ord for Entry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// Only `deadline` matters for ordering; pid is a tiebreaker so the
// type is Ord, but the order among same-deadline entries is
// irrelevant.
self.deadline
.cmp(&other.deadline)
.then_with(|| self.pid.index().cmp(&other.pid.index()))
.then_with(|| self.pid.generation().cmp(&other.pid.generation()))
}
}
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>>,
}
impl Timers {
pub fn new() -> Self {
Self { heap: BinaryHeap::new() }
}
pub fn insert(&mut self, deadline: Instant, pid: Pid) {
self.heap.push(Reverse(Entry { deadline, pid }));
}
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<Instant> {
self.heap.peek().map(|r| r.0.deadline)
}
/// Pop and return every pid whose deadline is ≤ `now`.
pub fn pop_due(&mut self, now: Instant) -> Vec<Pid> {
let mut out = Vec::new();
while let Some(r) = self.heap.peek() {
if r.0.deadline <= now {
let e = self.heap.pop().unwrap().0;
out.push(e.pid);
} else {
break;
}
}
out
}
}
/// Wall-clock duration helper exposed for `sleep`.
pub fn deadline_from_now(duration: Duration) -> Instant {
Instant::now()
.checked_add(duration)
.unwrap_or_else(Instant::now)
}