Skip to main content

subms_timer_wheel/features/
hierarchical.rs

1//! Hierarchical timer wheel (HHW). Three levels, each a wheel of
2//! 64 slots: seconds, minutes (each slot = 64 ticks), hours (each
3//! slot = 64*64 ticks). A timer scheduled `d` ticks out lands on the
4//! coarsest wheel whose slot can hold it; on each tick of a higher
5//! wheel we cascade its expiring slot's entries down to the lower
6//! wheel re-binned at the residual offset.
7//!
8//! Capacity: 64 * 64 * 64 = 262_144 ticks per "day" (loose analogy).
9//! Long delays no longer cost a no-op revolution per `mask` ticks the
10//! way the base wheel does; they sit on the coarse wheel and get
11//! cascaded down only as their fire time approaches.
12//!
13//! Memory: 3 * 64 = 192 buckets total regardless of how many timers
14//! are scheduled - the buckets hold Vecs of entries, not a per-tick
15//! slot. Compare with the base single-level wheel which needs a slot
16//! count >= max-delay for O(1) firing.
17
18use crate::TimerError;
19
20const LEVELS: usize = 3;
21const SLOTS: usize = 64;
22const MASK: usize = SLOTS - 1;
23const LEVEL_SHIFT: [u32; LEVELS] = [0, 6, 12];
24const LEVEL_RANGE: [usize; LEVELS] = [
25    SLOTS,                 // level 0: 1..=64 ticks
26    SLOTS * SLOTS,         // level 1: 65..=4096 ticks
27    SLOTS * SLOTS * SLOTS, // level 2: 4097..=262_144 ticks
28];
29
30struct Entry<V> {
31    id: u64,
32    deadline: u64,
33    value: Option<V>,
34    cancelled: bool,
35}
36
37pub struct HierarchicalTimerWheel<V> {
38    /// Three wheels of 64 buckets each.
39    wheels: [[Vec<Entry<V>>; SLOTS]; LEVELS],
40    /// Monotonically increasing tick counter. The level-i slot for
41    /// `t` is `(t >> LEVEL_SHIFT[i]) & MASK`.
42    now: u64,
43    next_id: u64,
44    /// Counts cascade events (entries moved from a coarser wheel down
45    /// to a finer one). Useful for diagnostics; doubles as a stable
46    /// hook for the metrics feature.
47    cascades: u64,
48    /// Live entries. Tracked rather than derived because counting means
49    /// walking all 192 buckets, and callers poll this on every tick.
50    pending: usize,
51}
52
53impl<V> HierarchicalTimerWheel<V> {
54    pub fn new() -> Self {
55        // const-init a 3x64 array of empty Vecs. The repeat-with shape
56        // avoids requiring V: Clone.
57        let wheels = std::array::from_fn(|_| std::array::from_fn(|_| Vec::new()));
58        Self {
59            wheels,
60            now: 0,
61            next_id: 1,
62            cascades: 0,
63            pending: 0,
64        }
65    }
66
67    pub fn now(&self) -> u64 {
68        self.now
69    }
70
71    pub fn cascades(&self) -> u64 {
72        self.cascades
73    }
74
75    /// Live (scheduled, not yet fired or cancelled) timers.
76    pub fn pending(&self) -> usize {
77        self.pending
78    }
79
80    pub fn is_empty(&self) -> bool {
81        self.pending == 0
82    }
83
84    /// Max delay (in ticks) the wheel can place without overflowing the
85    /// coarsest level. Schedules beyond this cap are rejected by
86    /// [`Self::try_schedule`] and clamped by [`Self::schedule`].
87    pub const fn max_delay() -> usize {
88        LEVEL_RANGE[LEVELS - 1]
89    }
90
91    /// Schedule `value` to fire in `delay` ticks. Delays larger than
92    /// [`Self::max_delay`] are clamped to the cap; use
93    /// [`Self::try_schedule`] for explicit overflow handling.
94    pub fn schedule(&mut self, delay: u64, value: V) -> u64 {
95        let cap = Self::max_delay() as u64;
96        let d = delay.min(cap.saturating_sub(1));
97        self.try_schedule(d, value).expect("clamped delay fits")
98    }
99
100    pub fn try_schedule(&mut self, delay: u64, value: V) -> Result<u64, TimerError> {
101        let max = Self::max_delay() as u64;
102        if delay >= max {
103            return Err(TimerError::DelayTooLong { delay, max });
104        }
105        let id = self.next_id;
106        self.next_id += 1;
107        self.insert(id, self.now + delay, value);
108        Ok(id)
109    }
110
111    /// Mark `id` cancelled. Returns true if a pending entry was found.
112    /// O(n) over every bucket; the tradeoff vs the base wheel (which
113    /// keeps an id->slot map) is that the hierarchical wheel moves
114    /// entries on cascade, so an id->slot map would need to be patched
115    /// on every cascade. Linear sweep on cancel is the cheaper deal.
116    pub fn cancel(&mut self, id: u64) -> bool {
117        for lvl in 0..LEVELS {
118            for slot in 0..SLOTS {
119                for e in &mut self.wheels[lvl][slot] {
120                    if e.id == id && !e.cancelled {
121                        e.cancelled = true;
122                        e.value = None;
123                        self.pending -= 1;
124                        return true;
125                    }
126                }
127            }
128        }
129        false
130    }
131
132    /// Move a pending timer to a new delay, keeping its id. Pays the same
133    /// linear sweep as [`Self::cancel`], for the same reason.
134    pub fn reschedule(&mut self, id: u64, delay: u64) -> bool {
135        let cap = Self::max_delay() as u64;
136        let d = delay.min(cap.saturating_sub(1));
137        for lvl in 0..LEVELS {
138            for slot in 0..SLOTS {
139                let Some(pos) = self.wheels[lvl][slot]
140                    .iter()
141                    .position(|e| e.id == id && !e.cancelled)
142                else {
143                    continue;
144                };
145                let entry = self.wheels[lvl][slot].swap_remove(pos);
146                let Some(value) = entry.value else {
147                    return false;
148                };
149                self.pending -= 1;
150                self.insert(id, self.now + d, value);
151                return true;
152            }
153        }
154        false
155    }
156
157    /// Remove every pending timer and return its value; the tick counter
158    /// stays where it is.
159    pub fn drain(&mut self) -> Vec<V> {
160        let mut out = Vec::with_capacity(self.pending);
161        for lvl in 0..LEVELS {
162            for slot in 0..SLOTS {
163                for mut e in std::mem::take(&mut self.wheels[lvl][slot]) {
164                    if e.cancelled {
165                        continue;
166                    }
167                    if let Some(v) = e.value.take() {
168                        out.push(v);
169                    }
170                }
171            }
172        }
173        self.pending = 0;
174        out
175    }
176
177    /// Drop every pending timer and reset the tick counter.
178    pub fn clear(&mut self) {
179        for lvl in 0..LEVELS {
180            for slot in 0..SLOTS {
181                self.wheels[lvl][slot].clear();
182            }
183        }
184        self.pending = 0;
185        self.now = 0;
186    }
187
188    fn insert(&mut self, id: u64, deadline: u64, value: V) {
189        let entry = Entry {
190            id,
191            deadline,
192            value: Some(value),
193            cancelled: false,
194        };
195        let (lvl, slot) = self.bucket_for(deadline);
196        self.wheels[lvl][slot].push(entry);
197        self.pending += 1;
198    }
199
200    /// Advance one tick. Returns the values of all timers whose
201    /// deadline equals the new `now`. Cascade from coarser wheels
202    /// down to finer wheels as needed.
203    pub fn tick(&mut self) -> Vec<V> {
204        self.now += 1;
205        // Cascade higher levels whose slot is about to roll over.
206        // The slot index at level L wraps every `LEVEL_RANGE[L]`
207        // ticks; when the lower-level index wraps to 0, the next
208        // higher level's slot has new contents to push down.
209        // Walk highest-to-lowest so a level-2 entry cascading down
210        // to level 1 still has time to re-cascade to level 0 on the
211        // same tick when its deadline is now.
212        for lvl in (1..LEVELS).rev() {
213            let lower_period = 1u64 << LEVEL_SHIFT[lvl];
214            if self.now % lower_period == 0 {
215                let slot = ((self.now >> LEVEL_SHIFT[lvl]) as usize) & MASK;
216                // Move entries from wheels[lvl][slot] down to their
217                // correct lower-level slot now that we're closer in time.
218                let entries = std::mem::take(&mut self.wheels[lvl][slot]);
219                for e in entries {
220                    if e.cancelled {
221                        continue;
222                    }
223                    self.cascades += 1;
224                    let (new_lvl, new_slot) = self.bucket_for(e.deadline);
225                    self.wheels[new_lvl][new_slot].push(e);
226                }
227            }
228        }
229
230        let slot = (self.now as usize) & MASK;
231        let entries = std::mem::take(&mut self.wheels[0][slot]);
232        let mut fired = Vec::new();
233        for mut e in entries {
234            if e.cancelled {
235                continue;
236            }
237            if e.deadline != self.now {
238                // An entry rebinned into this level-0 slot whose deadline is
239                // still a revolution away. LEVEL_RANGE[0]=64 leaves no room
240                // for it today; re-binning rather than dropping keeps the
241                // wheel correct if the level spans are ever retuned.
242                let (lvl, slot) = self.bucket_for(e.deadline);
243                self.wheels[lvl][slot].push(e);
244                continue;
245            }
246            self.pending -= 1;
247            if let Some(v) = e.value.take() {
248                fired.push(v);
249            }
250        }
251        fired
252    }
253
254    /// Pick the coarsest level whose slot range contains `deadline -
255    /// now`, then the slot within that level.
256    fn bucket_for(&self, deadline: u64) -> (usize, usize) {
257        let diff = deadline.saturating_sub(self.now);
258        let lvl = if diff < LEVEL_RANGE[0] as u64 {
259            0
260        } else if diff < LEVEL_RANGE[1] as u64 {
261            1
262        } else {
263            2
264        };
265        let slot = ((deadline >> LEVEL_SHIFT[lvl]) as usize) & MASK;
266        (lvl, slot)
267    }
268}
269
270impl<V> Default for HierarchicalTimerWheel<V> {
271    fn default() -> Self {
272        Self::new()
273    }
274}
275
276#[cfg(test)]
277#[path = "hierarchical_tests.rs"]
278mod tests;