Skip to main content

subms_timer_wheel/features/
deadline_scheduler.rs

1//! Absolute-deadline scheduling layer on top of the base wheel.
2//! Callers schedule against wall-clock instants ("fire at t=...") and
3//! drive the scheduler with `poll()` calls. The wheel itself stays
4//! tick-counted; the layer translates between instant deltas and tick
5//! deltas via an injected `Clock` so the workload is deterministic
6//! under test.
7//!
8//! The clock abstraction (a trait, not a free-running `Instant`) is
9//! deliberate: time-based tests that sleep are flaky and slow; tests
10//! that mutate a `TestClock` finish in microseconds and are exact.
11//!
12//! Granularity is 1 ms per tick by default; a deadline of `now + 12
13//! ms` lands twelve ticks out. Sub-ms deadlines round up to one tick.
14
15use crate::TimerWheel;
16use std::sync::OnceLock;
17use std::time::{Duration, Instant};
18
19/// Source of monotonic time. The deadline scheduler measures "from
20/// now" deltas off this; production code injects [`MonotonicClock`]
21/// and tests inject [`TestClock`].
22pub trait Clock {
23    /// Elapsed monotonic nanoseconds since the clock's origin. The
24    /// origin doesn't matter; only deltas do.
25    fn now_nanos(&self) -> u64;
26}
27
28#[derive(Default)]
29pub struct MonotonicClock {
30    /// Set once on first read (or eagerly by `new`). Persisting the
31    /// origin is what keeps successive `now_nanos` reads monotonic;
32    /// re-sampling `Instant::now` as the origin each call would make
33    /// every read a fresh near-zero delta off an unrelated baseline.
34    origin: OnceLock<Instant>,
35}
36
37impl MonotonicClock {
38    pub fn new() -> Self {
39        let origin = OnceLock::new();
40        let _ = origin.set(Instant::now());
41        Self { origin }
42    }
43}
44
45impl Clock for MonotonicClock {
46    fn now_nanos(&self) -> u64 {
47        let origin = self.origin.get_or_init(Instant::now);
48        Instant::now().duration_since(*origin).as_nanos() as u64
49    }
50}
51
52/// Hand-stepped clock for deterministic tests. `advance(d)` moves
53/// time forward by `d`; the scheduler then catches up via `poll()`.
54pub struct TestClock {
55    now_nanos: std::cell::Cell<u64>,
56}
57
58impl Default for TestClock {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl TestClock {
65    pub fn new() -> Self {
66        Self {
67            now_nanos: std::cell::Cell::new(0),
68        }
69    }
70
71    pub fn advance(&self, d: Duration) {
72        self.now_nanos
73            .set(self.now_nanos.get().saturating_add(d.as_nanos() as u64));
74    }
75}
76
77impl Clock for TestClock {
78    fn now_nanos(&self) -> u64 {
79        self.now_nanos.get()
80    }
81}
82
83pub struct DeadlineScheduler<V, C: Clock> {
84    wheel: TimerWheel<V>,
85    clock: C,
86    tick_nanos: u64,
87    /// Nanos consumed by previous ticks. Lets `poll()` advance
88    /// `(elapsed - consumed) / tick_nanos` ticks atomically.
89    consumed_nanos: u64,
90}
91
92impl<V, C: Clock> DeadlineScheduler<V, C> {
93    /// Build a deadline scheduler with `num_slots` wheel slots and
94    /// `tick` resolution (rounded up to 1 ns minimum).
95    pub fn new(num_slots: usize, clock: C, tick: Duration) -> Self {
96        let tick_nanos = (tick.as_nanos() as u64).max(1);
97        Self {
98            wheel: TimerWheel::new(num_slots),
99            clock,
100            tick_nanos,
101            consumed_nanos: 0,
102        }
103    }
104
105    pub fn tick_nanos(&self) -> u64 {
106        self.tick_nanos
107    }
108
109    /// The injected clock. `TestClock::advance` takes `&self`, so a test or
110    /// a demo can step time through this without owning the clock twice.
111    pub fn clock(&self) -> &C {
112        &self.clock
113    }
114
115    pub fn pending(&self) -> usize {
116        self.wheel.pending()
117    }
118
119    pub fn is_empty(&self) -> bool {
120        self.wheel.is_empty()
121    }
122
123    /// Schedule `value` to fire after `delay`. Equivalent to
124    /// `schedule_at(now + delay, value)`.
125    pub fn schedule_after(&mut self, delay: Duration, value: V) -> u64 {
126        let ticks = self.nanos_to_ticks(delay.as_nanos() as u64);
127        self.wheel.schedule(ticks, value)
128    }
129
130    /// Schedule `value` to fire at absolute deadline `when_nanos`
131    /// (same epoch as `Clock::now_nanos`). If the deadline is in the
132    /// past, the timer is queued for the next tick.
133    pub fn schedule_at(&mut self, when_nanos: u64, value: V) -> u64 {
134        let now = self.clock.now_nanos();
135        let diff = when_nanos.saturating_sub(now);
136        let ticks = self.nanos_to_ticks(diff).max(1);
137        self.wheel.schedule(ticks, value)
138    }
139
140    pub fn cancel(&mut self, id: u64) -> bool {
141        self.wheel.cancel(id)
142    }
143
144    /// Push a pending timer out to a new deadline, keeping its id. This is
145    /// the idle-timeout pattern: one timer per session, bumped on every
146    /// inbound message rather than cancelled and re-armed.
147    pub fn reschedule_at(&mut self, id: u64, when_nanos: u64) -> bool {
148        let now = self.clock.now_nanos();
149        let diff = when_nanos.saturating_sub(now);
150        let ticks = self.nanos_to_ticks(diff).max(1);
151        self.wheel.reschedule(id, ticks)
152    }
153
154    pub fn reschedule_after(&mut self, id: u64, delay: Duration) -> bool {
155        let ticks = self.nanos_to_ticks(delay.as_nanos() as u64).max(1);
156        self.wheel.reschedule(id, ticks)
157    }
158
159    /// Hand back every armed timer without firing it. The shutdown path.
160    pub fn drain(&mut self) -> Vec<V> {
161        self.wheel.drain()
162    }
163
164    /// Advance the wheel by however many ticks the clock has accrued
165    /// since the last `poll`. Returns every fired value across the
166    /// catch-up batch. Idempotent if called twice with no clock
167    /// movement in between.
168    pub fn poll(&mut self) -> Vec<V> {
169        let now = self.clock.now_nanos();
170        let pending = now.saturating_sub(self.consumed_nanos);
171        let ticks = (pending / self.tick_nanos) as usize;
172        self.consumed_nanos = self
173            .consumed_nanos
174            .saturating_add(ticks as u64 * self.tick_nanos);
175        let mut fired = Vec::new();
176        for _ in 0..ticks {
177            fired.extend(self.wheel.tick());
178        }
179        fired
180    }
181
182    fn nanos_to_ticks(&self, nanos: u64) -> usize {
183        nanos.div_ceil(self.tick_nanos) as usize
184    }
185}
186
187#[cfg(test)]
188#[path = "deadline_scheduler_tests.rs"]
189mod tests;