Skip to main content

dtact_util/timer/
native.rs

1//! Native timer backend: a hashed timing wheel (Varghese & Lauck), the
2//! standard O(1)-amortized-insert/fire structure for high-churn timer
3//! workloads — single global heap-based timers do not belong in this
4//! codebase.
5//!
6//! Layout: `WHEEL_SIZE` (256) buckets, each covering one `TICK` (1ms) of
7//! wall-clock time, so one full rotation spans `WHEEL_SIZE * TICK` = 256ms.
8//! A timer whose deadline falls further out than one rotation is placed in
9//! its target slot immediately with a `rounds` counter set to how many full
10//! rotations must elapse before it's live; each time the wheel revisits that
11//! slot it decrements `rounds` until it reaches zero, at which point the
12//! timer actually fires. This is the classic single-level hashed wheel with
13//! implicit "cascading via rounds" instead of a multi-tier hierarchy —
14//! O(1) insert, O(1) amortized fire, bounded per-tick work.
15//!
16//! **Zero-lock hot path.** Each bucket is a [`crate::lockfree::MpmcStack`]
17//! (lock-free Treiber stack) instead of a `Mutex<Vec<_>>` — 256-way
18//! sharding *and* no OS mutex/futex on either the insert or the per-tick
19//! drain. Per-timer completion state is plain atomics
20//! (`AtomicBool`/`AtomicI8` for the rounds counter) plus an
21//! [`crate::lockfree::AtomicWakerSlot`] instead of `Mutex<Option<Waker>>`.
22//! The worker thread idles via `thread::park()`/`Thread::unpark()`
23//! (a futex under the hood, not a `Condvar`) rather than blocking on a
24//! `Mutex`-guarded `Condvar::wait`.
25//!
26//! Cancellation is lazy: a dropped [`DtactSleep`] simply lets its
27//! `Arc<Node>` refcount hit zero without being scrubbed out of its bucket;
28//! the wheel drops the last reference silently when its round comes up
29//! (waking nobody, since nothing is listening), so no O(n) linear removal
30//! is ever needed on the hot cancel path.
31
32use crate::lockfree::{AtomicWakerSlot, MpmcStack};
33use std::future::Future;
34use std::pin::Pin;
35use std::sync::atomic::{AtomicI32, AtomicU64, AtomicUsize, Ordering};
36use std::sync::{Arc, OnceLock};
37use std::task::{Context, Poll};
38use std::thread::Thread;
39use std::time::{Duration, Instant};
40
41const WHEEL_BITS: u32 = 8;
42const WHEEL_SIZE: usize = 1 << WHEEL_BITS; // 256 slots
43const WHEEL_MASK: u64 = (WHEEL_SIZE as u64) - 1;
44const TICK: Duration = Duration::from_millis(1);
45/// Sentinel meaning "not yet fired" for `SleepState::done`, stored as a
46/// plain atomic rather than a bool so the fire path is a single
47/// store+wake with no separate flag/result pair to keep in sync.
48const PENDING: i32 = 0;
49const DONE: i32 = 1;
50
51#[repr(align(64))]
52struct SleepState {
53    /// PENDING (0) or DONE (1) — see constants above.
54    status: AtomicI32,
55    waker: AtomicWakerSlot,
56}
57
58#[repr(align(64))]
59struct Node {
60    state: Arc<SleepState>,
61    /// Remaining full rotations before this node is eligible to fire.
62    /// Decremented with `Relaxed` `fetch_sub` — only ever touched by the
63    /// single wheel worker thread, so no synchronization is needed beyond
64    /// what already orders bucket drains (each tick fully drains and
65    /// rebuilds its bucket via `MpmcStack::drain_all`/`push`).
66    rounds: AtomicUsize,
67}
68
69#[repr(align(64))]
70struct Wheel {
71    buckets: Box<[MpmcStack<Arc<Node>>]>,
72    /// Absolute tick counter, advanced only by the single worker thread.
73    current_tick: AtomicU64,
74    pending: AtomicUsize,
75    start: Instant,
76    worker: OnceLock<Thread>,
77}
78
79static WHEEL: OnceLock<Arc<Wheel>> = OnceLock::new();
80
81fn wheel() -> &'static Arc<Wheel> {
82    WHEEL.get_or_init(|| {
83        let mut buckets = Vec::with_capacity(WHEEL_SIZE);
84        for _ in 0..WHEEL_SIZE {
85            buckets.push(MpmcStack::new());
86        }
87        let w = Arc::new(Wheel {
88            buckets: buckets.into_boxed_slice(),
89            current_tick: AtomicU64::new(0),
90            pending: AtomicUsize::new(0),
91            start: Instant::now(),
92            worker: OnceLock::new(),
93        });
94        let worker_wheel = Arc::clone(&w);
95        let handle = std::thread::Builder::new()
96            .name("dtact-timer-wheel".into())
97            .spawn(move || worker_loop(&worker_wheel))
98            .expect("failed to spawn dtact-timer-wheel worker thread");
99        let _ = w.worker.set(handle.thread().clone());
100        w
101    })
102}
103
104fn worker_loop(w: &Arc<Wheel>) {
105    loop {
106        // Idle-park: no pending timers anywhere, avoid ticking for nothing.
107        // `park_timeout` also guards against a lost wakeup race between
108        // the emptiness check and the park call — worst case we wake up
109        // to nothing pending and loop back around within 50ms.
110        if w.pending.load(Ordering::Acquire) == 0 {
111            std::thread::park_timeout(Duration::from_millis(50));
112            continue;
113        }
114
115        let tick = w.current_tick.load(Ordering::Relaxed);
116        let tick_deadline = w.start + TICK * (tick as u32 + 1);
117        let now = Instant::now();
118        if tick_deadline > now {
119            std::thread::park_timeout(tick_deadline - now);
120            // Re-loop regardless of whether we were woken early by a new
121            // registration or by the timeout — the top-of-loop deadline
122            // check below re-derives whether it's actually time to tick.
123            if Instant::now() < tick_deadline {
124                continue;
125            }
126        }
127
128        let tick = w.current_tick.fetch_add(1, Ordering::Relaxed) + 1;
129        let slot = (tick & WHEEL_MASK) as usize;
130
131        let bucket = &w.buckets[slot];
132        if bucket.is_empty() {
133            continue;
134        }
135        let batch = bucket.drain_all();
136        let mut fired = 0usize;
137        for node in batch {
138            let rounds = node.rounds.load(Ordering::Relaxed);
139            if rounds == 0 {
140                node.state.status.store(DONE, Ordering::Release);
141                node.state.waker.take_and_wake();
142                fired += 1;
143            } else {
144                node.rounds.fetch_sub(1, Ordering::Relaxed);
145                bucket.push(node);
146            }
147        }
148        if fired > 0 {
149            w.pending.fetch_sub(fired, Ordering::AcqRel);
150        }
151    }
152}
153
154fn register(deadline: Instant) -> Arc<SleepState> {
155    let now = Instant::now();
156
157    // A deadline that's already passed (including `Duration::ZERO`
158    // sleeps) must fire immediately rather than going through the wheel:
159    // inserting into the *current* tick's bucket would place it in a slot
160    // the worker just finished draining, which then wouldn't be revisited
161    // for a full rotation (`WHEEL_SIZE` ticks, ~256ms) — a "zero-duration"
162    // sleep would then take up to a quarter-second instead of resolving
163    // on the very next poll. Short-circuiting here also avoids waking the
164    // wheel worker thread at all for a no-op sleep.
165    if deadline <= now {
166        return Arc::new(SleepState {
167            status: AtomicI32::new(DONE),
168            waker: AtomicWakerSlot::new(),
169        });
170    }
171
172    let w = wheel();
173    let state = Arc::new(SleepState {
174        status: AtomicI32::new(PENDING),
175        waker: AtomicWakerSlot::new(),
176    });
177
178    let remaining = deadline - now;
179    let ticks_from_now = remaining.as_nanos().div_ceil(TICK.as_nanos()) as u64;
180
181    let current_tick = w.current_tick.load(Ordering::Relaxed);
182    let target_tick = current_tick + ticks_from_now;
183    let slot = (target_tick & WHEEL_MASK) as usize;
184    let rounds = (ticks_from_now >> WHEEL_BITS) as usize;
185
186    let node = Arc::new(Node {
187        state: Arc::clone(&state),
188        rounds: AtomicUsize::new(rounds),
189    });
190
191    w.buckets[slot].push(node);
192    w.pending.fetch_add(1, Ordering::AcqRel);
193    if let Some(t) = w.worker.get() {
194        t.unpark();
195    }
196
197    state
198}
199
200/// A future that completes once, after the given [`Duration`] has elapsed.
201pub struct DtactSleep {
202    state: Arc<SleepState>,
203}
204
205impl DtactSleep {
206    /// Sleep for `duration` from now.
207    #[must_use]
208    pub fn new(duration: Duration) -> Self {
209        Self::until(Instant::now() + duration)
210    }
211
212    /// Sleep until the given `deadline` (already-elapsed deadlines
213    /// resolve on the very next wheel tick).
214    #[must_use]
215    pub fn until(deadline: Instant) -> Self {
216        Self {
217            state: register(deadline),
218        }
219    }
220}
221
222impl Future for DtactSleep {
223    type Output = ();
224
225    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
226        if self.state.status.load(Ordering::Acquire) == DONE {
227            return Poll::Ready(());
228        }
229        self.state.waker.register(cx.waker());
230        // Re-check after installing the waker to close the race where the
231        // wheel fired between the initial load and the waker install.
232        if self.state.status.load(Ordering::Acquire) == DONE {
233            return Poll::Ready(());
234        }
235        Poll::Pending
236    }
237}
238
239/// Convenience free function mirroring `tokio::time::sleep`.
240#[must_use]
241pub fn sleep(duration: Duration) -> DtactSleep {
242    DtactSleep::new(duration)
243}
244
245/// Configures how [`DtactInterval::tick`] catches up after missing one
246/// or more ticks.
247///
248/// A tick is "missed" when the interval isn't polled promptly enough —
249/// e.g. the caller was busy doing other work. Mirrors
250/// `tokio::time::MissedTickBehavior`.
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
252pub enum MissedTickBehavior {
253    /// Fire every missed tick back-to-back with no delay between them
254    /// until caught up, then resume the original cadence. `next` always
255    /// advances by exactly one `period` per `tick()` call, so a caller
256    /// that fell behind sees a burst of immediately-ready `tick()`
257    /// calls rather than any ticks being silently dropped. The default,
258    /// matching `tokio::time::MissedTickBehavior`'s own default.
259    #[default]
260    Burst,
261    /// Skip every missed tick: the next scheduled time jumps straight to
262    /// the next period boundary that's still in the future, rather than
263    /// firing once for each one that was missed.
264    Skip,
265    /// Reset the schedule relative to when the late tick actually fired,
266    /// not the original cadence — the next tick is `period` after *now*,
267    /// not after the missed tick's originally-scheduled time.
268    Delay,
269}
270
271/// A repeating timer. `tick()` is a plain async method mirroring
272/// `tokio::time::Interval::tick`.
273pub struct DtactInterval {
274    period: Duration,
275    next: Instant,
276    missed_tick_behavior: MissedTickBehavior,
277}
278
279impl DtactInterval {
280    /// Build an interval firing every `period`, starting one `period`
281    /// from now, with the default [`MissedTickBehavior::Burst`] catch-up
282    /// policy (change it via [`Self::set_missed_tick_behavior`]).
283    ///
284    /// # Panics
285    ///
286    /// Panics if `period` is zero — a zero-length period has no sensible
287    /// tick rate and almost always indicates a caller bug (e.g. an
288    /// unintended default `Duration::ZERO`).
289    #[must_use]
290    pub fn new(period: Duration) -> Self {
291        assert!(
292            period > Duration::ZERO,
293            "dtact-timer: interval period must be > 0"
294        );
295        Self {
296            period,
297            next: Instant::now() + period,
298            missed_tick_behavior: MissedTickBehavior::default(),
299        }
300    }
301
302    /// The current missed-tick catch-up policy.
303    #[must_use]
304    pub const fn missed_tick_behavior(&self) -> MissedTickBehavior {
305        self.missed_tick_behavior
306    }
307
308    /// Change the missed-tick catch-up policy. Takes effect starting
309    /// from the next [`Self::tick`] call — doesn't retroactively change
310    /// how the interval caught up (or didn't) from any prior lateness.
311    pub const fn set_missed_tick_behavior(&mut self, behavior: MissedTickBehavior) {
312        self.missed_tick_behavior = behavior;
313    }
314
315    /// Wait for the next tick, returning the `Instant` it fired at.
316    pub async fn tick(&mut self) -> Instant {
317        DtactSleep::until(self.next).await;
318        let fired_at = self.next;
319        match self.missed_tick_behavior {
320            MissedTickBehavior::Burst => {
321                self.next += self.period;
322            }
323            MissedTickBehavior::Skip => {
324                // Advance by whole periods to the next boundary still in
325                // the future, rather than firing once per missed tick.
326                let now = Instant::now();
327                let mut next = self.next + self.period;
328                while next <= now {
329                    next += self.period;
330                }
331                self.next = next;
332            }
333            MissedTickBehavior::Delay => {
334                self.next = Instant::now() + self.period;
335            }
336        }
337        fired_at
338    }
339}
340
341/// Convenience free function mirroring `tokio::time::interval`.
342#[must_use]
343pub fn interval(period: Duration) -> DtactInterval {
344    DtactInterval::new(period)
345}
346
347/// Error returned by [`DtactTimeout`] when the wrapped future does not
348/// complete before the deadline.
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub struct TimeoutError;
351
352impl std::fmt::Display for TimeoutError {
353    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354        write!(
355            f,
356            "dtact-timer: deadline elapsed before the future completed"
357        )
358    }
359}
360
361impl std::error::Error for TimeoutError {}
362
363/// Wraps a future with a deadline: resolves to `Ok(F::Output)` if the inner
364/// future completes first, or `Err(TimeoutError)` if the deadline elapses
365/// first.
366pub struct DtactTimeout<F> {
367    inner: Pin<Box<F>>,
368    sleep: DtactSleep,
369}
370
371impl<F> DtactTimeout<F> {
372    /// Wrap `inner`, racing it against a `duration`-long deadline.
373    pub fn new(duration: Duration, inner: F) -> Self {
374        Self {
375            inner: Box::pin(inner),
376            sleep: DtactSleep::new(duration),
377        }
378    }
379}
380
381impl<F: Future> Future for DtactTimeout<F> {
382    type Output = Result<F::Output, TimeoutError>;
383
384    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
385        let this = self.get_mut();
386        if let Poll::Ready(v) = this.inner.as_mut().poll(cx) {
387            return Poll::Ready(Ok(v));
388        }
389        match Pin::new(&mut this.sleep).poll(cx) {
390            Poll::Ready(()) => Poll::Ready(Err(TimeoutError)),
391            Poll::Pending => Poll::Pending,
392        }
393    }
394}
395
396/// Convenience free function mirroring `tokio::time::timeout`.
397pub fn timeout<F: Future>(duration: Duration, fut: F) -> DtactTimeout<F> {
398    DtactTimeout::new(duration, fut)
399}