vpp_plugin/vppinfra/tw_timer.rs
1//! Timer wheels
2//!
3//! # Design Considerations
4//!
5//! This module implements a hierarchical timer wheel data structure optimised for efficient
6//! expiration of large numbers of timers in O(1) amortised time. Key design decisions:
7//!
8//! - Hierarchical levels: Multiple levels with decreasing resolution enable handling both
9//! near-term (high precision) and far-future (lower precision) timers efficiently without
10//! requiring a massive single-level array.
11//!
12//! - Doubly-linked lists per slot: Each slot in each level contains a doubly-linked list of
13//! timers, enabling O(1) stop (and removal) of arbitrary timers.
14//!
15//! - Head entries: Every slot is pre-initialised with a head entry, eliminating the need for
16//! special-case list manipulation.
17//!
18//! - Cascading: When a timer expires in a coarser level, it is re-added to finer levels (if
19//! available), until it is determined to the caller as having expired. This still allows
20//! efficient processing of many timers far in the future whilst still allowing them to have
21//! the fine expiration resolution.
22//!
23//! - Generic context: The context type `T` is associated with each timer, allowing arbitrary
24//! application data to be attached without additional allocation.
25
26use std::mem::MaybeUninit;
27
28use super::Vec;
29use slab::Slab;
30
31/// Add timer error
32#[non_exhaustive]
33#[derive(Debug)]
34pub enum StartTimerError<T> {
35 /// The timer is already expired
36 Expired(T),
37}
38
39const EMPTY_INDEX: u32 = u32::MAX;
40
41/// Timer handle
42///
43/// Used for stopping a running timer.
44#[derive(Debug)]
45// Copying a timer handle would make it easier to double-cancel timers, causing undesirable behaviour
46#[allow(missing_copy_implementations)]
47pub struct TimerHandle(u32);
48
49/// Timer entry data
50#[derive(Debug, Copy, Clone)]
51enum TimerEntryData<T> {
52 /// Value for the head of a list
53 Head,
54 /// An actual timer entry
55 Timer {
56 /// The absolute expiration time in ticks
57 expire_time: u64,
58 /// Application context for the timer entry
59 context: T,
60 },
61}
62
63/// A timer entry
64#[derive(Debug, Copy, Clone)]
65struct TimerEntry<T> {
66 /// Index of previous entry
67 next: u32,
68 /// Index of next entry
69 previous: u32,
70 /// Timer entry value
71 data: TimerEntryData<T>,
72}
73
74/// A list of timer entries for a slot
75#[derive(Debug)]
76struct EntryList {
77 /// Index to head of list
78 head: u32,
79}
80
81impl Default for EntryList {
82 fn default() -> Self {
83 Self { head: EMPTY_INDEX }
84 }
85}
86
87/// A level in the timer wheel
88#[derive(Debug)]
89struct Level<const NUM_SLOTS: usize> {
90 /// The slots, each containing a list of timers
91 slots: [EntryList; NUM_SLOTS],
92 /// Current slot index
93 position: usize,
94}
95
96/// Hierarchical timer wheel
97///
98/// This is a multi-level timer data structure where each level has lower resolution (is coarser)
99/// than the previous. It efficiently handles expiration of many timers by batching them into
100/// slots.
101///
102/// # Parameters
103///
104/// - `T`: The context type associated with each timer
105/// - `NUM_LEVELS`: The number of hierarchical levels (each adds a factor of NUM_SLOTS in range)
106/// - `NUM_SLOTS`: The number of slots per level
107///
108/// # Resolution and Range
109///
110/// - **Range per level**: Level 0 covers `[0, NUM_SLOTS]` ticks. Level 1 covers
111/// `[0, NUM_SLOTS²]` ticks at coarser granularity. In general, level `L` covers
112/// `[0, NUM_SLOTS^(L+1)]` ticks.
113///
114/// - **Total range**: With `NUM_LEVELS` levels, the wheel can represent timers up to
115/// approximately `NUM_SLOTS^NUM_LEVELS` ticks in the future (though timers beyond
116/// `NUM_SLOTS^NUM_LEVELS` are still supported, see below).
117///
118/// # Handling Timers Far in the Future
119///
120/// When a timer is scheduled with an expiration time far beyond what the wheel's levels can
121/// directly represent (delta ≥ `NUM_SLOTS^NUM_LEVELS`), it is placed in the **last slot of
122/// the highest level**. As time progresses and cascading occurs, such timers are progressively
123/// re-inserted at appropriate finer-resolution slots, eventually cascading down to level 0 for
124/// precise expiration. This approach trades initial placement overhead for the ability to handle
125/// arbitrarily distant future timers without requiring additional data structures.
126///
127/// # Compatibility with VPP C Timer Wheels
128///
129/// The functionality and interface to the timer wheel is intended to be similar to VPP C Timer
130/// Wheels, but isn't binary-compatible with VPP C Timer Wheels, since this would constrain the
131/// implementation too much and it's not envisaged to be likely that timer wheels would be used
132/// across API boundaries.
133#[derive(Debug)]
134pub struct TimerWheel<T, const NUM_LEVELS: usize, const NUM_SLOTS: usize> {
135 /// Timer pool
136 timers: Slab<TimerEntry<T>>,
137 /// The levels of the wheel
138 levels: [Level<NUM_SLOTS>; NUM_LEVELS],
139 /// Current time in ticks
140 current_time: u64,
141}
142
143// Manually implement Default rather than deriving it to avoid the `T: Default` constraint that
144// would otherwise be added
145impl<T, const NUM_LEVELS: usize, const NUM_SLOTS: usize> Default
146 for TimerWheel<T, NUM_LEVELS, NUM_SLOTS>
147{
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153impl<T, const NUM_LEVELS: usize, const NUM_SLOTS: usize> TimerWheel<T, NUM_LEVELS, NUM_SLOTS> {
154 /// Create a new timer wheel
155 pub fn new() -> Self {
156 let mut uninit_self = MaybeUninit::uninit();
157 Self::init(&mut uninit_self);
158
159 // SAFETY: `Self::init` fully initialised `uninit_self` before call.
160 unsafe { uninit_self.assume_init() }
161 }
162
163 /// Initialise a [`TimerWheel`] in uninitialized memory.
164 ///
165 /// This can be used to avoid excessive stage usage when `uninit_self` is located in
166 /// allocated memory, such as from a `Vec`, `Box` or `Rc`.
167 pub fn init(uninit_self: &mut MaybeUninit<Self>) -> &mut Self {
168 // SAFETY: `uninit_self` points to valid writable memory and we initialise each field before returning.
169 let init_self = unsafe {
170 let ptr = uninit_self.as_mut_ptr();
171 std::ptr::addr_of_mut!((*ptr).timers)
172 .write(Slab::with_capacity(NUM_LEVELS * NUM_SLOTS));
173 for level in 0..NUM_LEVELS {
174 let level_ptr =
175 (std::ptr::addr_of_mut!((*ptr).levels) as *mut Level<NUM_SLOTS>).add(level);
176 for slot in 0..NUM_SLOTS {
177 let slot_ptr =
178 (std::ptr::addr_of_mut!((*level_ptr).slots) as *mut EntryList).add(slot);
179 slot_ptr.write(Default::default());
180 }
181 std::ptr::addr_of_mut!((*level_ptr).position).write(0);
182 }
183 std::ptr::addr_of_mut!((*ptr).current_time).write(0);
184 uninit_self.assume_init_mut()
185 };
186
187 // Populate all slots with a head entry - this allows us to remove a timer later without
188 // needing to keep track of which level and slot the timer is added to and without having
189 // to brute-force it.
190 for level in 0..NUM_LEVELS {
191 for slot in 0..NUM_SLOTS {
192 init_self.levels[level].slots[slot].head = init_self.timers.insert(TimerEntry {
193 next: EMPTY_INDEX,
194 previous: EMPTY_INDEX,
195 data: TimerEntryData::Head,
196 }) as u32;
197 }
198 }
199
200 init_self
201 }
202
203 /// Start a timer which expires after the given interval
204 ///
205 /// The timer is one-shot, not periodic.
206 ///
207 /// `interval` is the interval past the last time timers were expired after which this timer
208 /// should expire. `context` is a context to associate with the timer
209 ///
210 /// This has a time complexity of O(1).
211 pub fn start_timer(&mut self, interval: u64, context: T) -> TimerHandle {
212 let expire_time = self.current_time.saturating_add(interval);
213
214 let handle = TimerHandle(self.timers.insert(TimerEntry {
215 next: EMPTY_INDEX,
216 previous: EMPTY_INDEX,
217 data: TimerEntryData::Timer {
218 expire_time,
219 context,
220 },
221 }) as u32);
222
223 self.start_timer_unchecked(handle.0);
224
225 handle
226 }
227
228 /// Start a timer which expires at an absolute time
229 ///
230 /// `expire_time` is the absolute time after which this timer should expire. `context` is a
231 /// context to associate with the timer
232 ///
233 /// This has a time complexity of O(1).
234 pub fn start_timer_absolute(
235 &mut self,
236 expire_time: u64,
237 context: T,
238 ) -> Result<TimerHandle, StartTimerError<T>> {
239 if expire_time <= self.current_time {
240 return Err(StartTimerError::Expired(context));
241 }
242
243 let handle = TimerHandle(self.timers.insert(TimerEntry {
244 next: EMPTY_INDEX,
245 previous: EMPTY_INDEX,
246 data: TimerEntryData::Timer {
247 expire_time,
248 context,
249 },
250 }) as u32);
251
252 self.start_timer_unchecked(handle.0);
253
254 Ok(handle)
255 }
256
257 /// Start a timer, not checking if it has already expired
258 fn start_timer_unchecked(&mut self, index: u32) {
259 let timer = &self.timers[index as usize];
260 let delta = match &timer.data {
261 TimerEntryData::Timer { expire_time, .. } => *expire_time - self.current_time,
262 TimerEntryData::Head => unreachable!(),
263 };
264 // In case it's too far in the future, put in the last level's last slot
265 let mut level = NUM_LEVELS - 1;
266 let mut slot = (self.levels[level].position + NUM_SLOTS - 1) % NUM_SLOTS;
267
268 for l in 0..NUM_LEVELS {
269 if delta < (NUM_SLOTS as u64).pow((l + 1) as u32) {
270 level = l;
271 slot = (self.levels[l].position
272 + (delta / (NUM_SLOTS as u64).pow(l as u32)) as usize)
273 .saturating_sub(1)
274 % NUM_SLOTS;
275 break;
276 }
277 }
278
279 let head = &mut self.timers[self.levels[level].slots[slot].head as usize];
280 let old_head_next = std::mem::replace(&mut head.next, index);
281 self.timers[index as usize].next = old_head_next;
282 self.timers[index as usize].previous = self.levels[level].slots[slot].head;
283 if old_head_next != EMPTY_INDEX {
284 self.timers[old_head_next as usize].previous = index;
285 }
286 }
287
288 /// Stop a running timer
289 ///
290 /// This has a time complexity of O(1).
291 pub fn stop_timer(&mut self, handle: TimerHandle) -> Option<T> {
292 let timer = self.timers.get(handle.0 as usize)?;
293 // Refuse to remove a special head entry - note that this shouldn't happen since head
294 // entries are allocated at init time (so reuse isn't a factor) and otherwise a TimerHandle
295 // cannot be constructed pointing to anything other than an a timer entry.
296 if matches!(timer.data, TimerEntryData::Head) {
297 return None;
298 }
299 let timer_next = timer.next;
300 let timer_previous = timer.previous;
301
302 // Unlink the timer from the doubly-linked list
303 // timer_previous is never EMPTY_INDEX here because the head of the lists are
304 // pre-populated with entries
305 self.timers[timer_previous as usize].next = timer_next;
306 if timer_next != EMPTY_INDEX {
307 self.timers[timer_next as usize].previous = timer_previous;
308 }
309
310 let timer = self.timers.remove(handle.0 as usize);
311 match timer.data {
312 TimerEntryData::Timer { context, .. } => Some(context),
313 // We should have returned early with the check for this above
314 TimerEntryData::Head => unreachable!(),
315 }
316 }
317
318 /// Process a level's current slot, expire timers, and cascade if needed
319 fn process_level(&mut self, level: usize) -> Vec<T> {
320 let mut expired_contexts = Vec::new();
321
322 if level >= NUM_LEVELS {
323 return expired_contexts;
324 }
325
326 // Extract timers from current slot
327 let slot = self.levels[level].position;
328 let head = &mut self.timers[self.levels[level].slots[slot].head as usize];
329 let mut timer_index = std::mem::replace(&mut head.next, EMPTY_INDEX);
330
331 // Process each timer
332 while timer_index != EMPTY_INDEX {
333 let timer = &self.timers[timer_index as usize];
334 let next_timer_index = timer.next;
335
336 match &timer.data {
337 TimerEntryData::Timer { expire_time, .. } => {
338 // Check that the timer has actually expired. There are two cases where it
339 // might not have:
340 // 1. Timer started in level > 0, with a lower resolution than for
341 // level == 0; and
342 // 2. Timer started in time not represented by the number of levels & slots,
343 // i.e. too far in the future.
344 // In both of these cases, if the timer hasn't yet expired we want to
345 // re-start the timer at the appropriate level & slot.
346 if *expire_time <= self.current_time {
347 let timer = self.timers.remove(timer_index as usize);
348 match timer.data {
349 TimerEntryData::Timer { context, .. } => {
350 expired_contexts.push(context);
351 }
352 TimerEntryData::Head => unreachable!(),
353 }
354 } else {
355 self.start_timer_unchecked(timer_index);
356 }
357 }
358 TimerEntryData::Head => unreachable!(),
359 }
360 timer_index = next_timer_index;
361 }
362
363 // Advance position
364 self.levels[level].position = (self.levels[level].position + 1) % NUM_SLOTS;
365
366 // Cascade to next level if this level wrapped
367 if self.levels[level].position == 0 {
368 let cascaded_expired_contexts = self.process_level(level + 1);
369 expired_contexts.extend(cascaded_expired_contexts);
370 }
371
372 expired_contexts
373 }
374
375 /// Advance time by one tick, expiring timers
376 fn tick(&mut self) -> Vec<T> {
377 self.current_time += 1;
378 self.process_level(0)
379 }
380
381 /// Advance time by multiple ticks, expiring timers
382 pub fn expire_timers(&mut self, ticks: u64) -> Vec<T> {
383 let mut expired_contexts = Vec::new();
384
385 for _ in 0..ticks {
386 let tick_expired_contexts = self.tick();
387 expired_contexts.extend(tick_expired_contexts);
388 }
389
390 expired_contexts
391 }
392
393 /// Get the next expiration time in ticks, or `None` if no unexpired, started timers
394 ///
395 /// This has worst-case time complexity O(n) where n is `NUM_LEVELS * NUM_SLOTS`.
396 pub fn next_expiration(&self) -> Option<u64> {
397 for level in 0..NUM_LEVELS {
398 let start_slot = self.levels[level].position;
399 for i in 0..NUM_SLOTS {
400 let slot = (start_slot + i) % NUM_SLOTS;
401 let head = &self.timers[self.levels[level].slots[slot].head as usize];
402 if head.next != EMPTY_INDEX {
403 let mut timer_index = head.next;
404 let mut min_expire_time: Option<u64> = None;
405 while timer_index != EMPTY_INDEX {
406 let timer = &self.timers[timer_index as usize];
407 timer_index = timer.next;
408 match &timer.data {
409 TimerEntryData::Timer { expire_time, .. } => {
410 if let Some(prev_min_expire_time) = min_expire_time {
411 min_expire_time = Some(prev_min_expire_time.min(*expire_time));
412 } else {
413 min_expire_time = Some(*expire_time);
414 }
415 }
416 // We should have skipped over the head already
417 TimerEntryData::Head => unreachable!(),
418 }
419 }
420 return min_expire_time;
421 }
422 }
423 }
424 None
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use crate::vppinfra::clib_mem_init;
431
432 use super::*;
433
434 #[test]
435 fn test_immediate_timer() {
436 clib_mem_init();
437
438 let mut wheel: TimerWheel<u8, 4, 256> = Default::default();
439 let e = wheel.start_timer_absolute(0, 1).expect_err("add timer");
440 assert!(
441 matches!(e, StartTimerError::Expired(1)),
442 "{:?} != AddTimerError::Expired",
443 e
444 );
445 }
446
447 #[test]
448 fn test_future_timer() {
449 clib_mem_init();
450
451 let mut wheel: TimerWheel<u8, 4, 256> = Default::default();
452 wheel.start_timer_absolute(5, 1).expect("add timer");
453 let contexts = wheel.expire_timers(4);
454 assert_eq!(contexts, []);
455 let contexts = wheel.expire_timers(1);
456 assert_eq!(contexts, [1]);
457 }
458
459 #[test]
460 fn test_multiple_timers() {
461 clib_mem_init();
462
463 let mut wheel: TimerWheel<u8, 4, 256> = Default::default();
464 wheel.start_timer_absolute(1, 1).expect("add timer");
465 wheel.start_timer(2, 2);
466 wheel.start_timer_absolute(3, 3).expect("add timer");
467 let contexts = wheel.expire_timers(1);
468 assert_eq!(contexts, [1]);
469 let contexts = wheel.expire_timers(2);
470 assert_eq!(contexts, [2, 3]);
471 }
472
473 #[test]
474 fn test_level_1() {
475 clib_mem_init();
476
477 let mut wheel: TimerWheel<u8, 4, 256> = Default::default();
478 // Add a timer that will be placed in level 1 (delta = 257 > 256)
479 wheel.start_timer(257, 1);
480 // Advance 256 ticks: this should trigger cascade and re-insert the timer
481 let contexts = wheel.expire_timers(256);
482 assert_eq!(contexts, []);
483 // Advance one more tick to expire the re-inserted timer
484 let contexts = wheel.expire_timers(1);
485 assert_eq!(contexts, [1]);
486 }
487
488 #[test]
489 fn test_next_expiration() {
490 clib_mem_init();
491
492 let mut wheel: TimerWheel<u8, 4, 256> = Default::default();
493 assert_eq!(wheel.next_expiration(), None);
494 wheel.start_timer(5, 1);
495 assert_eq!(wheel.next_expiration(), Some(5));
496 wheel.start_timer_absolute(3, 2).expect("add timer");
497 assert_eq!(wheel.next_expiration(), Some(3));
498 wheel.start_timer(10, 3);
499 assert_eq!(wheel.next_expiration(), Some(3));
500 let contexts = wheel.expire_timers(3); // expire at 3
501 assert_eq!(contexts, [2]);
502 assert_eq!(wheel.next_expiration(), Some(5));
503 }
504
505 #[test]
506 fn test_stop_timers() {
507 clib_mem_init();
508
509 let mut wheel: TimerWheel<u8, 4, 256> = Default::default();
510 let timer1 = wheel.start_timer(5, 1);
511 let timer2 = wheel.start_timer_absolute(3, 2).expect("add timer");
512 let timer3 = wheel.start_timer_absolute(5, 3).expect("add timer");
513 // Stop a timer that isn't the head of the list
514 assert_eq!(wheel.stop_timer(timer1), Some(1));
515 let contexts = wheel.expire_timers(3);
516 assert_eq!(contexts, [2]);
517 // Stop an already-expired timer
518 assert_eq!(wheel.stop_timer(timer2), None);
519 // Stop a timer that is head of the list
520 assert_eq!(wheel.stop_timer(timer3), Some(3));
521 // Advance 2 more ticks and don't expect any of the timers to fire
522 let contexts = wheel.expire_timers(2);
523 assert_eq!(contexts, []);
524 }
525
526 #[test]
527 fn test_timer_far_in_future() {
528 clib_mem_init();
529
530 let mut wheel: TimerWheel<u8, 2, 4> = Default::default();
531 // The maximum ticks represented by the slots is 4^2, so 17 is beyond that - we still
532 // expect it to expire at the correct time.
533 let timer1 = wheel.start_timer_absolute(17, 1).expect("add timer");
534 let contexts = wheel.expire_timers(16);
535 assert_eq!(contexts, []);
536 assert_eq!(wheel.stop_timer(timer1), Some(1));
537 // Add two, both far in the future, and expect them to expire at the correct time as well
538 // as the next expiration time to take the lower of the two.
539 wheel.start_timer(18, 2);
540 wheel.start_timer(17, 1);
541 assert_eq!(wheel.next_expiration(), Some(16 + 17));
542 let contexts = wheel.expire_timers(17);
543 assert_eq!(contexts, [1]);
544 }
545}