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;