feat: I/O and mutex support (v0.3)
Add epoll-based non-blocking I/O and kernel-like mutexes: - src/io.rs: Complete epoll backend with timeout & error handling - src/mutex.rs: Fair mutex with waiter queues & parking integration - Enhanced scheduler to support synchronous I/O blocking - Comprehensive test suites for I/O (epoll) and mutex behavior - Documentation: LOOM.md concurrency model & README
This commit is contained in:
112
src/timer.rs
112
src/timer.rs
@@ -1,38 +1,86 @@
|
||||
//! Sleep timers.
|
||||
//! Sleep + wait-with-timeout 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.
|
||||
//! 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`:
|
||||
//!
|
||||
//! `BinaryHeap` is a max-heap, so entries are stored with their deadline
|
||||
//! wrapped in `Reverse` to get min-heap behaviour.
|
||||
//! - `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).
|
||||
//!
|
||||
//! Stale pids (slot reused since the timer was inserted) are detected on
|
||||
//! `due_pids` pop and silently dropped — same convention as the run queue.
|
||||
//! `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::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
/// 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: Rc<dyn TimerTarget>,
|
||||
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 {
|
||||
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<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 {
|
||||
// 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()))
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,15 +94,25 @@ impl PartialOrd for Entry {
|
||||
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() }
|
||||
Self { heap: BinaryHeap::new(), next_seq: 0 }
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, deadline: Instant, pid: Pid) {
|
||||
self.heap.push(Reverse(Entry { deadline, pid }));
|
||||
/// 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 {
|
||||
@@ -66,13 +124,13 @@ impl Timers {
|
||||
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> {
|
||||
/// 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 {
|
||||
let e = self.heap.pop().unwrap().0;
|
||||
out.push(e.pid);
|
||||
out.push(self.heap.pop().unwrap().0);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -81,7 +139,7 @@ impl Timers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wall-clock duration helper exposed for `sleep`.
|
||||
/// Wall-clock duration helper exposed for `sleep` and `lock_timeout`.
|
||||
pub fn deadline_from_now(duration: Duration) -> Instant {
|
||||
Instant::now()
|
||||
.checked_add(duration)
|
||||
|
||||
Reference in New Issue
Block a user