subms_timer_wheel/lib.rs
1//! Single-level hashed timer wheel. O(1) schedule and cancel.
2//!
3//! The wheel has `N` buckets. A scheduled timer at `delay` ticks goes into
4//! bucket `(now + delay) % N` with a `rounds` counter of how many full
5//! revolutions it must sit out first. On `tick()`, the hand walks one bucket
6//! forward: timers with `rounds == 0` fire (their values are returned); the
7//! rest have `rounds` decremented. Cancel drops the id from the index and
8//! flags the entry; the flagged entry is reclaimed on the next visit to its
9//! bucket.
10//!
11//! Tradeoff vs the hierarchical wheel: with a single level, a long delay
12//! causes many no-op revolutions. For workloads with delays bounded by
13//! `N` ticks, the single level is optimal.
14//!
15//! Thread safety: `TimerWheel` is single-threaded. Every method takes
16//! `&mut self`, so one caller owns the wheel and there is no interior
17//! synchronisation to pay for. It is `Send` when `V: Send` and can be moved
18//! to a ticker thread; to arm timers from several threads at once, enable the
19//! `concurrent` feature or hand work to the ticker thread through a queue.
20//!
21//! ```
22//! use subms_timer_wheel::TimerWheel;
23//! let mut w: TimerWheel<&'static str> = TimerWheel::new(256);
24//! let id = w.schedule(5, "hello");
25//! for _ in 0..4 { assert!(w.tick().is_empty()); }
26//! assert_eq!(w.tick(), vec!["hello"]);
27//! let _ = id; // returned id can be used to cancel before firing
28//! ```
29//!
30//! Full writeup, design notes and measured benchmarks:
31//! <https://www.submillisecond.com/cookbook/recipes/subms-timer-wheel>
32
33use std::collections::HashMap;
34
35pub mod error;
36pub use error::TimerError;
37
38/// Ceiling on a timer's rounds counter. Held at `i32::MAX` rather than
39/// `u32::MAX` so the Java port, whose counter is a signed `int`, refuses
40/// exactly the same delays this one does.
41const MAX_ROUNDS: u64 = i32::MAX as u64;
42
43pub struct TimerWheel<V> {
44 slots: Vec<Slot<V>>,
45 mask: usize,
46 hand: usize,
47 next_id: u64,
48 /// Live timers only: an id is removed here the moment it is cancelled or
49 /// fired, so `pending()` never counts a timer the caller has retired.
50 id_to_slot: HashMap<u64, usize>,
51}
52
53struct Slot<V> {
54 entries: Vec<Entry<V>>,
55}
56
57struct Entry<V> {
58 id: u64,
59 rounds: u32,
60 value: V,
61 cancelled: bool,
62}
63
64impl<V> TimerWheel<V> {
65 /// `num_slots` rounded up to a power of two.
66 pub fn new(num_slots: usize) -> Self {
67 let n = num_slots.max(2).next_power_of_two();
68 let mut slots = Vec::with_capacity(n);
69 for _ in 0..n {
70 slots.push(Slot {
71 entries: Vec::new(),
72 });
73 }
74 Self {
75 slots,
76 mask: n - 1,
77 hand: 0,
78 next_id: 1,
79 id_to_slot: HashMap::new(),
80 }
81 }
82
83 pub fn num_slots(&self) -> usize {
84 self.slots.len()
85 }
86
87 /// Largest delay the wheel can represent: a timer can sit out at most
88 /// `i32::MAX` revolutions of `N` slots. Held at the signed bound rather
89 /// than `u32::MAX` so the Java port refuses exactly the same delays.
90 pub fn max_delay(&self) -> u64 {
91 self.slots.len() as u64 * MAX_ROUNDS
92 }
93
94 /// Number of live (scheduled, not yet fired or cancelled) timers. A
95 /// correct wheel returns this to 0 once every scheduled timer has fired;
96 /// a leak would let it climb without bound.
97 pub fn pending(&self) -> usize {
98 self.id_to_slot.len()
99 }
100
101 pub fn is_empty(&self) -> bool {
102 self.id_to_slot.is_empty()
103 }
104
105 /// Entries physically held in one bucket, including cancelled ones not
106 /// yet swept. Reading the spread across buckets is how you catch a
107 /// workload whose delays all collide on one slot.
108 pub fn slot_len(&self, slot: usize) -> usize {
109 self.slots.get(slot).map_or(0, |s| s.entries.len())
110 }
111
112 /// Schedule `value` to fire in `delay_ticks`. Returns an id for cancel.
113 ///
114 /// A delay of 0 fires on the next tick, matching Netty's treatment of a
115 /// deadline already in the past. A delay past [`Self::max_delay`] is
116 /// clamped; use [`Self::try_schedule`] to have it refused instead.
117 pub fn schedule(&mut self, delay_ticks: usize, value: V) -> u64 {
118 let d = self.clamp_delay(delay_ticks);
119 let id = self.next_id;
120 self.next_id += 1;
121 self.insert(id, d, value);
122 id
123 }
124
125 /// Schedule `value`, refusing a delay the wheel cannot represent.
126 pub fn try_schedule(&mut self, delay_ticks: usize, value: V) -> Result<u64, TimerError> {
127 let max = self.max_delay();
128 if delay_ticks as u64 > max {
129 return Err(TimerError::DelayTooLong {
130 delay: delay_ticks as u64,
131 max,
132 });
133 }
134 Ok(self.schedule(delay_ticks, value))
135 }
136
137 /// Mark a scheduled timer cancelled. Returns `true` if it was pending.
138 pub fn cancel(&mut self, id: u64) -> bool {
139 let Some(slot) = self.id_to_slot.remove(&id) else {
140 return false;
141 };
142 // The entry keeps its seat until the hand reaches this bucket; only
143 // the index is updated eagerly, which is what keeps `pending()` exact.
144 for e in &mut self.slots[slot].entries {
145 if e.id == id && !e.cancelled {
146 e.cancelled = true;
147 return true;
148 }
149 }
150 false
151 }
152
153 /// Move a pending timer to a new delay, keeping its id. Returns `false`
154 /// if the id is not pending (already fired, already cancelled, unknown).
155 ///
156 /// Unlike cancel this removes the entry eagerly - leaving a flagged
157 /// entry behind would let one id sit in two buckets at once.
158 pub fn reschedule(&mut self, id: u64, delay_ticks: usize) -> bool {
159 let Some(slot) = self.id_to_slot.remove(&id) else {
160 return false;
161 };
162 let Some(pos) = self.slots[slot]
163 .entries
164 .iter()
165 .position(|e| e.id == id && !e.cancelled)
166 else {
167 return false;
168 };
169 let entry = self.slots[slot].entries.swap_remove(pos);
170 let d = self.clamp_delay(delay_ticks);
171 self.insert(id, d, entry.value);
172 true
173 }
174
175 /// Advance the hand one tick. Returns the values of all timers that
176 /// fired (rounds was 0 and not cancelled). Cancelled timers are dropped
177 /// silently. Other timers have their `rounds` decremented.
178 pub fn tick(&mut self) -> Vec<V> {
179 self.hand = (self.hand + 1) & self.mask;
180 let slot = self.hand;
181 let mut fired = Vec::new();
182 let entries = std::mem::take(&mut self.slots[slot].entries);
183 let mut survivors = Vec::new();
184 for mut e in entries {
185 if e.cancelled {
186 continue;
187 }
188 if e.rounds == 0 {
189 self.id_to_slot.remove(&e.id);
190 fired.push(e.value);
191 } else {
192 e.rounds -= 1;
193 survivors.push(e);
194 }
195 }
196 self.slots[slot].entries = survivors;
197 fired
198 }
199
200 /// Advance `ticks` ticks and return everything that fired across them,
201 /// in tick order. A ticker thread that woke late catches up here rather
202 /// than firing a whole revolution's timers on one bucket.
203 pub fn advance(&mut self, ticks: usize) -> Vec<V> {
204 let mut fired = Vec::new();
205 for _ in 0..ticks {
206 fired.append(&mut self.tick());
207 }
208 fired
209 }
210
211 /// Remove every pending timer and return its value. The hand stays where
212 /// it is. This is the shutdown path: Netty's `HashedWheelTimer::stop`
213 /// hands back the timeouts it never got to run, and so does this.
214 pub fn drain(&mut self) -> Vec<V> {
215 let mut out = Vec::with_capacity(self.id_to_slot.len());
216 for slot in &mut self.slots {
217 for e in std::mem::take(&mut slot.entries) {
218 if !e.cancelled {
219 out.push(e.value);
220 }
221 }
222 }
223 self.id_to_slot.clear();
224 out
225 }
226
227 /// Drop every pending timer and reset the hand. Ids already handed out
228 /// are never reused, so a late `cancel` on a cleared timer returns
229 /// `false` rather than hitting an unrelated timer.
230 pub fn clear(&mut self) {
231 for slot in &mut self.slots {
232 slot.entries.clear();
233 }
234 self.id_to_slot.clear();
235 self.hand = 0;
236 }
237
238 fn clamp_delay(&self, delay_ticks: usize) -> usize {
239 let max = self.max_delay().min(usize::MAX as u64) as usize;
240 delay_ticks.clamp(1, max)
241 }
242
243 fn insert(&mut self, id: u64, delay: usize, value: V) {
244 let n = self.slots.len();
245 let slot = self.hand.wrapping_add(delay) & self.mask;
246 // rounds = ceil(d/N) - 1, not floor(d/N). They agree everywhere except
247 // when d is an exact multiple of N, where the timer lands back on the
248 // bucket the hand has just left and waits a full revolution for the
249 // revisit. Charging a rounds counter for that revolution as well fires
250 // the timer a lap late.
251 let rounds = (delay.div_ceil(n) - 1) as u32;
252 self.slots[slot].entries.push(Entry {
253 id,
254 rounds,
255 value,
256 cancelled: false,
257 });
258 self.id_to_slot.insert(id, slot);
259 }
260}
261
262#[cfg(feature = "harness")]
263pub mod recipe;
264
265// Opt-in feature modules. Each is independent of the base wheel and
266// gated by its own Cargo feature; `cargo add subms-timer-wheel` alone
267// keeps the base zero-dep + std-only shape.
268#[cfg(any(
269 feature = "hierarchical",
270 feature = "concurrent",
271 feature = "deadline-scheduler",
272 feature = "cron",
273 feature = "metrics",
274))]
275pub mod features;
276
277#[cfg(feature = "concurrent")]
278pub use features::concurrent::ConcurrentTimerWheel;
279#[cfg(feature = "cron")]
280pub use features::cron::{CronError, CronSchedule, CronScheduler};
281#[cfg(feature = "deadline-scheduler")]
282pub use features::deadline_scheduler::{Clock, DeadlineScheduler, MonotonicClock, TestClock};
283#[cfg(feature = "hierarchical")]
284pub use features::hierarchical::HierarchicalTimerWheel;
285#[cfg(feature = "metrics")]
286pub use features::metrics::{MeteredTimerWheel, TimerMetrics};
287
288#[cfg(test)]
289#[path = "wheel_tests.rs"]
290mod wheel_tests;
291
292#[cfg(test)]
293#[path = "sample_app_tests.rs"]
294mod sample_app_tests;