Skip to main content

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}