dstar_gateway_core/session/
timer_wheel.rs

1//! Deterministic timer wheel keyed by `Instant`.
2//!
3//! Stores N named timers. Each timer has a target instant. The
4//! wheel tells the shell what its next wake-up time is via
5//! `next_deadline`. When time advances past a timer's target, the
6//! caller (the session machine) checks `is_expired(name, now)`.
7
8use std::collections::HashMap;
9use std::time::Instant;
10
11/// Named timer identifier — a `&'static str` keeps the wheel
12/// allocation-free for fixed sets of timers (keepalive,
13/// inactivity, connect-deadline, voice-inactivity, etc.).
14pub(crate) type TimerId = &'static str;
15
16/// Tiny timer wheel for the session machines.
17#[derive(Debug, Default)]
18pub(crate) struct TimerWheel {
19    timers: HashMap<TimerId, Instant>,
20}
21
22impl TimerWheel {
23    pub(crate) fn new() -> Self {
24        Self::default()
25    }
26
27    /// Set or reset a named timer.
28    pub(crate) fn set(&mut self, name: TimerId, deadline: Instant) {
29        let _previous = self.timers.insert(name, deadline);
30    }
31
32    /// Remove a named timer.
33    pub(crate) fn clear(&mut self, name: TimerId) {
34        let _previous = self.timers.remove(name);
35    }
36
37    /// True if `now >= timer.deadline`.
38    pub(crate) fn is_expired(&self, name: TimerId, now: Instant) -> bool {
39        self.timers.get(name).is_some_and(|&d| now >= d)
40    }
41
42    /// Earliest unexpired deadline across all timers, or `None` if
43    /// no timers are set.
44    pub(crate) fn next_deadline(&self) -> Option<Instant> {
45        self.timers.values().copied().min()
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use std::time::Duration;
53
54    #[test]
55    fn empty_wheel_has_no_deadline() {
56        let w = TimerWheel::new();
57        assert!(w.next_deadline().is_none());
58    }
59
60    #[test]
61    fn set_and_check_expiry() {
62        let mut w = TimerWheel::new();
63        let now = Instant::now();
64        w.set("keepalive", now + Duration::from_secs(1));
65        assert!(!w.is_expired("keepalive", now));
66        assert!(w.is_expired("keepalive", now + Duration::from_secs(2)));
67    }
68
69    #[test]
70    fn next_deadline_is_earliest() {
71        let mut w = TimerWheel::new();
72        let now = Instant::now();
73        w.set("a", now + Duration::from_secs(5));
74        w.set("b", now + Duration::from_secs(2));
75        w.set("c", now + Duration::from_secs(10));
76        assert_eq!(w.next_deadline(), Some(now + Duration::from_secs(2)));
77    }
78
79    #[test]
80    fn clear_removes_timer() {
81        let mut w = TimerWheel::new();
82        let now = Instant::now();
83        w.set("a", now + Duration::from_secs(1));
84        w.clear("a");
85        assert!(w.next_deadline().is_none());
86    }
87}