Skip to main content

nexus_timer/
wheel.rs

1//! Timer wheel — the main data structure.
2//!
3//! `TimerWheel<T, S>` is a multi-level, no-cascade timer wheel. Entries are
4//! placed into a level based on how far in the future their deadline is.
5//! Once placed, an entry never moves — poll checks `deadline_ticks <= now`
6//! per entry.
7
8use std::marker::PhantomData;
9use std::mem;
10use std::time::{Duration, Instant};
11
12use nexus_slab::{Full, bounded, unbounded};
13
14use crate::entry::{EntryPtr, WheelEntry, entry_ref};
15use crate::handle::TimerHandle;
16use crate::level::Level;
17use crate::store::{BoundedStore, SlabStore};
18
19// =============================================================================
20// WheelBuilder (typestate)
21// =============================================================================
22
23/// Builder for configuring a timer wheel.
24///
25/// Defaults match the Linux kernel timer wheel (1ms tick, 64 slots/level,
26/// 8x multiplier, 7 levels → ~4.7 hour range).
27///
28/// # Examples
29///
30/// ```
31/// use std::time::{Duration, Instant};
32/// use nexus_timer::{Wheel, WheelBuilder};
33///
34/// let now = Instant::now();
35///
36/// // All defaults
37/// let wheel: Wheel<u64> = WheelBuilder::default().unbounded(4096).build(now);
38///
39/// // Custom config
40/// let wheel: Wheel<u64> = WheelBuilder::default()
41///     .tick_duration(Duration::from_micros(100))
42///     .slots_per_level(32)
43///     .unbounded(4096)
44///     .build(now);
45/// ```
46#[derive(Debug, Clone, Copy)]
47pub struct WheelBuilder {
48    tick_duration: Duration,
49    slots_per_level: usize,
50    clk_shift: u32,
51    num_levels: usize,
52}
53
54impl Default for WheelBuilder {
55    fn default() -> Self {
56        WheelBuilder {
57            tick_duration: Duration::from_millis(1),
58            slots_per_level: 64,
59            clk_shift: 3,
60            num_levels: 7,
61        }
62    }
63}
64
65impl WheelBuilder {
66    /// Creates a new builder with default configuration.
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Sets the tick duration. Default: 1ms.
72    pub fn tick_duration(mut self, d: Duration) -> Self {
73        self.tick_duration = d;
74        self
75    }
76
77    /// Sets the number of slots per level. Must be a power of 2. Default: 64.
78    pub fn slots_per_level(mut self, n: usize) -> Self {
79        self.slots_per_level = n;
80        self
81    }
82
83    /// Sets the bit shift between levels (multiplier = 2^clk_shift). Default: 3 (8x).
84    pub fn clk_shift(mut self, s: u32) -> Self {
85        self.clk_shift = s;
86        self
87    }
88
89    /// Sets the number of levels. Default: 7.
90    pub fn num_levels(mut self, n: usize) -> Self {
91        self.num_levels = n;
92        self
93    }
94
95    /// Transitions to an unbounded wheel builder.
96    ///
97    /// `chunk_capacity` is the slab chunk size (entries per chunk). The slab
98    /// grows by adding new chunks as needed.
99    pub fn unbounded(self, chunk_capacity: usize) -> UnboundedWheelBuilder {
100        UnboundedWheelBuilder {
101            config: self,
102            chunk_capacity,
103        }
104    }
105
106    /// Transitions to a bounded wheel builder.
107    ///
108    /// `capacity` is the maximum number of concurrent timers.
109    pub fn bounded(self, capacity: usize) -> BoundedWheelBuilder {
110        BoundedWheelBuilder {
111            config: self,
112            capacity,
113        }
114    }
115
116    fn validate(&self) {
117        assert!(
118            self.slots_per_level.is_power_of_two(),
119            "slots_per_level must be a power of 2, got {}",
120            self.slots_per_level
121        );
122        assert!(
123            self.slots_per_level <= 64,
124            "slots_per_level must be <= 64 (u64 bitmask), got {}",
125            self.slots_per_level
126        );
127        assert!(self.num_levels > 0, "num_levels must be > 0");
128        assert!(
129            self.num_levels <= 8,
130            "num_levels must be <= 8 (u8 bitmask), got {}",
131            self.num_levels
132        );
133        assert!(self.clk_shift > 0, "clk_shift must be > 0");
134        assert!(
135            !self.tick_duration.is_zero(),
136            "tick_duration must be non-zero"
137        );
138        let max_shift = (self.num_levels - 1) as u64 * self.clk_shift as u64;
139        assert!(
140            max_shift < 64,
141            "(num_levels - 1) * clk_shift must be < 64, got {}",
142            max_shift
143        );
144        let slots_log2 = self.slots_per_level.trailing_zeros() as u64;
145        assert!(
146            slots_log2 + max_shift < 64,
147            "slots_per_level << max_shift would overflow u64"
148        );
149    }
150
151    fn tick_ns(&self) -> u64 {
152        self.tick_duration.as_nanos() as u64
153    }
154}
155
156/// Terminal builder for an unbounded timer wheel.
157///
158/// Created via [`WheelBuilder::unbounded`]. The only method is `.build()`.
159#[derive(Debug)]
160pub struct UnboundedWheelBuilder {
161    config: WheelBuilder,
162    chunk_capacity: usize,
163}
164
165impl UnboundedWheelBuilder {
166    /// Builds the unbounded timer wheel.
167    ///
168    /// # Panics
169    ///
170    /// Panics if the configuration is invalid (non-power-of-2 slots, zero
171    /// levels, zero clk_shift, or zero tick duration).
172    pub fn build<T: 'static>(self, now: Instant) -> Wheel<T> {
173        self.config.validate();
174        let slab = unbounded::Slab::with_chunk_capacity(self.chunk_capacity);
175        let levels = build_levels::<T>(&self.config);
176        TimerWheel {
177            slab,
178            num_levels: self.config.num_levels,
179            levels,
180            current_ticks: 0,
181            tick_ns: self.config.tick_ns(),
182            epoch: now,
183            active_levels: 0,
184            len: 0,
185            _marker: PhantomData,
186        }
187    }
188}
189
190/// Terminal builder for a bounded timer wheel.
191///
192/// Created via [`WheelBuilder::bounded`]. The only method is `.build()`.
193#[derive(Debug)]
194pub struct BoundedWheelBuilder {
195    config: WheelBuilder,
196    capacity: usize,
197}
198
199impl BoundedWheelBuilder {
200    /// Builds the bounded timer wheel.
201    ///
202    /// # Panics
203    ///
204    /// Panics if the configuration is invalid (non-power-of-2 slots, zero
205    /// levels, zero clk_shift, or zero tick duration).
206    pub fn build<T: 'static>(self, now: Instant) -> BoundedWheel<T> {
207        self.config.validate();
208        let slab = bounded::Slab::with_capacity(self.capacity);
209        let levels = build_levels::<T>(&self.config);
210        TimerWheel {
211            slab,
212            num_levels: self.config.num_levels,
213            levels,
214            current_ticks: 0,
215            tick_ns: self.config.tick_ns(),
216            epoch: now,
217            active_levels: 0,
218            len: 0,
219            _marker: PhantomData,
220        }
221    }
222}
223
224// =============================================================================
225// TimerWheel
226// =============================================================================
227
228/// A multi-level, no-cascade timer wheel.
229///
230/// Generic over:
231/// - `T` — the user payload stored with each timer.
232/// - `S` — the slab storage backend. Defaults to `unbounded::Slab`.
233///
234/// # Thread Safety
235///
236/// `Send` but `!Sync`. Can be moved to a thread at setup but must not
237/// be shared. All internal raw pointers point into owned allocations
238/// (slab chunks, level slot arrays) — moving the wheel moves the heap
239/// data with it.
240pub struct TimerWheel<
241    T: 'static,
242    S: SlabStore<Item = WheelEntry<T>> = unbounded::Slab<WheelEntry<T>>,
243> {
244    slab: S,
245    levels: Vec<Level<T>>,
246    num_levels: usize,
247    active_levels: u8,
248    current_ticks: u64,
249    tick_ns: u64,
250    epoch: Instant,
251    len: usize,
252    _marker: PhantomData<*const ()>, // !Send (overridden below), !Sync
253}
254
255// SAFETY: TimerWheel<T, S> exclusively owns all memory behind its raw pointers.
256//
257// Pointer inventory and ownership:
258// - Slot `entry_head`/`entry_tail` — point into slab-owned memory (SlotCell
259//   in a slab chunk). Slab chunks are Vec<SlotCell<T>> heap allocations.
260// - DLL links (`WheelEntry::prev`, `WheelEntry::next`) — point to other
261//   SlotCells in the same slab.
262// - `Level::slots` — `Box<[WheelSlot<T>]>`, owned by the level.
263//
264// All pointed-to memory lives inside owned collections (Vec, Box<[T]>).
265// When TimerWheel is moved, the heap allocations stay at their addresses —
266// the internal pointers remain valid. No thread-local state. No shared
267// ownership.
268//
269// T: Send is required because timer values cross the thread boundary with
270// the wheel.
271//
272// S is NOT required to be Send. Slab types are !Send (raw pointers, Cell)
273// but the wheel exclusively owns its slab — no shared access, no aliasing.
274// Moving the wheel moves the slab; heap allocations stay at their addresses
275// so internal pointers remain valid.
276//
277// Outstanding TimerHandle<T> values are !Send and cannot follow the wheel
278// across threads. They become inert — consuming them requires &mut
279// TimerWheel which the original thread no longer has. The debug_assert in
280// TimerHandle::drop catches this as a programming error. Worst case is a
281// slot leak (refcount stuck at 1), not unsoundness.
282#[allow(clippy::non_send_fields_in_send_ty)]
283unsafe impl<T: Send + 'static, S: SlabStore<Item = WheelEntry<T>>> Send for TimerWheel<T, S> {}
284
285/// A timer wheel backed by a fixed-capacity slab.
286pub type BoundedWheel<T> = TimerWheel<T, bounded::Slab<WheelEntry<T>>>;
287
288/// A timer wheel backed by a growable slab.
289pub type Wheel<T> = TimerWheel<T, unbounded::Slab<WheelEntry<T>>>;
290
291// =============================================================================
292// Construction
293// =============================================================================
294
295impl<T: 'static> Wheel<T> {
296    /// Creates an unbounded timer wheel with default configuration.
297    ///
298    /// For custom configuration, use [`WheelBuilder`].
299    pub fn unbounded(chunk_capacity: usize, now: Instant) -> Self {
300        WheelBuilder::default().unbounded(chunk_capacity).build(now)
301    }
302}
303
304impl<T: 'static> BoundedWheel<T> {
305    /// Creates a bounded timer wheel with default configuration.
306    ///
307    /// For custom configuration, use [`WheelBuilder`].
308    pub fn bounded(capacity: usize, now: Instant) -> Self {
309        WheelBuilder::default().bounded(capacity).build(now)
310    }
311}
312
313fn build_levels<T: 'static>(config: &WheelBuilder) -> Vec<Level<T>> {
314    (0..config.num_levels)
315        .map(|i| Level::new(config.slots_per_level, i, config.clk_shift))
316        .collect()
317}
318
319// =============================================================================
320// Schedule
321// =============================================================================
322
323impl<T: 'static, S: SlabStore<Item = WheelEntry<T>>> TimerWheel<T, S> {
324    /// Schedules a timer and returns a handle for cancellation.
325    ///
326    /// The handle must be consumed via [`cancel`](Self::cancel) or
327    /// [`free`](Self::free). Dropping it is a programming error.
328    ///
329    /// # Panics
330    ///
331    /// Panics if the backing slab is at capacity (bounded slabs only).
332    /// This is a capacity planning error — size your wheel for peak load.
333    pub fn schedule(&mut self, deadline: Instant, value: T) -> TimerHandle<T> {
334        let deadline_ticks = self.instant_to_ticks(deadline);
335        let entry = WheelEntry::new(deadline_ticks, value, 2);
336        let slot = self.slab.alloc(entry);
337        let ptr = slot.into_ptr();
338        self.insert_entry(ptr, deadline_ticks);
339        self.len += 1;
340        TimerHandle::new(ptr)
341    }
342
343    /// Schedules a fire-and-forget timer (no handle returned).
344    ///
345    /// The timer will fire during poll and the value will be collected.
346    /// Cannot be cancelled.
347    ///
348    /// # Panics
349    ///
350    /// Panics if the backing slab is at capacity (bounded slabs only).
351    /// This is a capacity planning error — size your wheel for peak load.
352    pub fn schedule_forget(&mut self, deadline: Instant, value: T) {
353        let deadline_ticks = self.instant_to_ticks(deadline);
354        let entry = WheelEntry::new(deadline_ticks, value, 1);
355        let slot = self.slab.alloc(entry);
356        let ptr = slot.into_ptr();
357        self.insert_entry(ptr, deadline_ticks);
358        self.len += 1;
359    }
360}
361
362// =============================================================================
363// Schedule — fallible (bounded slabs only)
364// =============================================================================
365
366impl<T: 'static, S: BoundedStore<Item = WheelEntry<T>>> TimerWheel<T, S> {
367    /// Attempts to schedule a timer, returning a handle on success.
368    ///
369    /// Returns `Err(Full(value))` if the slab is at capacity. Use this
370    /// when you need graceful error handling. For the common case where
371    /// capacity exhaustion is fatal, use [`schedule`](Self::schedule).
372    pub fn try_schedule(&mut self, deadline: Instant, value: T) -> Result<TimerHandle<T>, Full<T>> {
373        let deadline_ticks = self.instant_to_ticks(deadline);
374        let entry = WheelEntry::new(deadline_ticks, value, 2);
375        match self.slab.try_alloc(entry) {
376            Ok(slot) => {
377                let ptr = slot.into_ptr();
378                self.insert_entry(ptr, deadline_ticks);
379                self.len += 1;
380                Ok(TimerHandle::new(ptr))
381            }
382            Err(full) => {
383                // Extract the user's T from the WheelEntry wrapper
384                // SAFETY: we just constructed this entry, take_value is valid
385                let wheel_entry = full.into_inner();
386                let value = unsafe { wheel_entry.take_value() }
387                    .expect("entry was just constructed with Some(value)");
388                Err(Full(value))
389            }
390        }
391    }
392
393    /// Attempts to schedule a fire-and-forget timer.
394    ///
395    /// Returns `Err(Full(value))` if the slab is at capacity. Use this
396    /// when you need graceful error handling. For the common case where
397    /// capacity exhaustion is fatal, use [`schedule_forget`](Self::schedule_forget).
398    pub fn try_schedule_forget(&mut self, deadline: Instant, value: T) -> Result<(), Full<T>> {
399        let deadline_ticks = self.instant_to_ticks(deadline);
400        let entry = WheelEntry::new(deadline_ticks, value, 1);
401        match self.slab.try_alloc(entry) {
402            Ok(slot) => {
403                let ptr = slot.into_ptr();
404                self.insert_entry(ptr, deadline_ticks);
405                self.len += 1;
406                Ok(())
407            }
408            Err(full) => {
409                let wheel_entry = full.into_inner();
410                let value = unsafe { wheel_entry.take_value() }
411                    .expect("entry was just constructed with Some(value)");
412                Err(Full(value))
413            }
414        }
415    }
416}
417
418// =============================================================================
419// Cancel / Free / Poll / Query — generic over any store
420// =============================================================================
421
422impl<T: 'static, S: SlabStore<Item = WheelEntry<T>>> TimerWheel<T, S> {
423    /// Cancels a timer and returns its value.
424    ///
425    /// - If the timer is still active: unlinks from the wheel, extracts value,
426    ///   frees the slab entry. Returns `Some(T)`.
427    /// - If the timer already fired (zombie handle): frees the slab entry.
428    ///   Returns `None`.
429    ///
430    /// Consumes the handle (no Drop runs).
431    pub fn cancel(&mut self, handle: TimerHandle<T>) -> Option<T> {
432        let ptr = handle.ptr;
433        // Consume handle without running Drop
434        mem::forget(handle);
435
436        // SAFETY: handle guarantees ptr is valid and allocated from our slab.
437        let entry = unsafe { entry_ref(ptr) };
438        let refs = entry.refs();
439
440        if refs == 2 {
441            // Active timer with handle — unlink, extract, free
442            let value = unsafe { entry.take_value() };
443            self.remove_entry(ptr);
444            self.len -= 1;
445            // SAFETY: ptr was allocated from our slab, entry is now spent
446            unsafe { self.slab.free_ptr(ptr) };
447            value
448        } else {
449            // refs == 1 means the wheel already fired this (zombie).
450            // The fire path decremented 2→1 and left the entry for us to free.
451            debug_assert_eq!(refs, 1, "unexpected refcount {refs} in cancel");
452            // SAFETY: ptr was allocated from our slab
453            unsafe { self.slab.free_ptr(ptr) };
454            None
455        }
456    }
457
458    /// Releases a timer handle without cancelling.
459    ///
460    /// - If the timer is still active: converts to fire-and-forget (refs 2→1).
461    ///   Timer stays in the wheel and will fire normally during poll.
462    /// - If the timer already fired (zombie): frees the slab entry (refs 1→0).
463    ///
464    /// Consumes the handle (no Drop runs).
465    pub fn free(&mut self, handle: TimerHandle<T>) {
466        let ptr = handle.ptr;
467        mem::forget(handle);
468
469        // SAFETY: handle guarantees ptr is valid
470        let entry = unsafe { entry_ref(ptr) };
471        let new_refs = entry.dec_refs();
472
473        if new_refs == 0 {
474            // Was a zombie (fired already, refs was 1) — free the entry
475            // SAFETY: ptr was allocated from our slab
476            unsafe { self.slab.free_ptr(ptr) };
477        }
478        // new_refs == 1: timer is now fire-and-forget, stays in wheel
479    }
480
481    /// Reschedules an active timer to a new deadline.
482    ///
483    /// Moves the entry from its current slot to the correct slot for
484    /// `new_deadline` without extracting or reconstructing the value.
485    ///
486    /// # Panics
487    ///
488    /// Panics if the timer has already fired (zombie handle). Only active
489    /// timers (refs == 2) can be rescheduled.
490    ///
491    /// Consumes and returns a new handle (same entry, new position).
492    pub fn reschedule(&mut self, handle: TimerHandle<T>, new_deadline: Instant) -> TimerHandle<T> {
493        let ptr = handle.ptr;
494        mem::forget(handle);
495
496        // SAFETY: handle guarantees ptr is valid
497        let entry = unsafe { entry_ref(ptr) };
498        assert_eq!(entry.refs(), 2, "cannot reschedule a fired timer");
499
500        // Remove from current position
501        self.remove_entry(ptr);
502
503        // Update deadline and reinsert
504        let new_ticks = self.instant_to_ticks(new_deadline);
505        entry.set_deadline_ticks(new_ticks);
506        self.insert_entry(ptr, new_ticks);
507
508        TimerHandle::new(ptr)
509    }
510
511    /// Fires all expired timers, collecting their values into `buf`.
512    ///
513    /// Returns the number of timers fired.
514    pub fn poll(&mut self, now: Instant, buf: &mut Vec<T>) -> usize {
515        self.poll_with_limit(now, usize::MAX, buf)
516    }
517
518    /// Fires expired timers up to `limit`, collecting values into `buf`.
519    ///
520    /// Resumable: if the limit is hit, the next call continues where this one
521    /// left off (as long as `now` hasn't changed).
522    ///
523    /// Returns the number of timers fired in this call.
524    pub fn poll_with_limit(&mut self, now: Instant, limit: usize, buf: &mut Vec<T>) -> usize {
525        let now_ticks = self.instant_to_ticks(now);
526        self.current_ticks = now_ticks;
527
528        let mut fired = 0;
529        let mut mask = self.active_levels;
530
531        while mask != 0 && fired < limit {
532            let lvl_idx = mask.trailing_zeros() as usize;
533            mask &= mask - 1; // clear lowest set bit
534            fired += self.poll_level(lvl_idx, now_ticks, limit - fired, buf);
535        }
536        fired
537    }
538
539    /// Returns the `Instant` of the next timer that will fire, or `None` if empty.
540    ///
541    /// Walks only active (non-empty) slots. O(active_slots) in the worst case,
542    /// but typically very fast because most slots are empty.
543    pub fn next_deadline(&self) -> Option<Instant> {
544        let mut min_ticks: Option<u64> = None;
545
546        let mut lvl_mask = self.active_levels;
547        while lvl_mask != 0 {
548            let lvl_idx = lvl_mask.trailing_zeros() as usize;
549            lvl_mask &= lvl_mask - 1;
550
551            let level = &self.levels[lvl_idx];
552            let mut slot_mask = level.active_slots();
553            while slot_mask != 0 {
554                let slot_idx = slot_mask.trailing_zeros() as usize;
555                slot_mask &= slot_mask - 1;
556
557                let slot = level.slot(slot_idx);
558                let mut entry_ptr = slot.entry_head();
559
560                while !entry_ptr.is_null() {
561                    // SAFETY: entry_ptr is in this slot's DLL
562                    let entry = unsafe { entry_ref(entry_ptr) };
563                    let dt = entry.deadline_ticks();
564                    min_ticks = Some(min_ticks.map_or(dt, |current| current.min(dt)));
565                    entry_ptr = entry.next();
566                }
567            }
568        }
569
570        min_ticks.map(|t| self.ticks_to_instant(t))
571    }
572
573    /// Returns the number of timers currently in the wheel.
574    #[inline]
575    pub fn len(&self) -> usize {
576        self.len
577    }
578
579    /// Returns true if the wheel contains no timers.
580    #[inline]
581    pub fn is_empty(&self) -> bool {
582        self.len == 0
583    }
584
585    // =========================================================================
586    // Internal: tick conversion
587    // =========================================================================
588
589    #[inline]
590    fn instant_to_ticks(&self, instant: Instant) -> u64 {
591        // Saturate at 0 for instants before epoch
592        let dur = instant.saturating_duration_since(self.epoch);
593        dur.as_nanos() as u64 / self.tick_ns
594    }
595
596    #[inline]
597    fn ticks_to_instant(&self, ticks: u64) -> Instant {
598        self.epoch + Duration::from_nanos(ticks * self.tick_ns)
599    }
600
601    // =========================================================================
602    // Internal: level selection
603    // =========================================================================
604
605    /// Selects the appropriate level for a deadline.
606    ///
607    /// Walks levels from finest to coarsest, picking the first level whose
608    /// range can represent the delta. Clamps to the highest level if the
609    /// deadline exceeds the wheel's total range.
610    #[inline]
611    fn select_level(&self, deadline_ticks: u64) -> usize {
612        let delta = deadline_ticks.saturating_sub(self.current_ticks);
613
614        for (i, level) in self.levels.iter().enumerate() {
615            if delta < level.range() {
616                return i;
617            }
618        }
619
620        // Beyond max range — clamp to highest level
621        self.num_levels - 1
622    }
623
624    // =========================================================================
625    // Internal: entry insertion into a level's slot
626    // =========================================================================
627
628    /// Inserts an entry into the appropriate level and slot.
629    ///
630    /// Records the level and slot index on the entry so `remove_entry` can
631    /// find it without recomputing (which would be unsound after time advances).
632    #[inline]
633    #[allow(clippy::needless_pass_by_ref_mut)]
634    fn insert_entry(&mut self, entry_ptr: EntryPtr<T>, deadline_ticks: u64) {
635        let lvl_idx = self.select_level(deadline_ticks);
636        let slot_idx = self.levels[lvl_idx].slot_index(deadline_ticks);
637
638        // Record location on the entry for O(1) lookup at cancel time.
639        // SAFETY: entry_ptr is valid (just allocated)
640        let entry = unsafe { entry_ref(entry_ptr) };
641        entry.set_location(lvl_idx as u8, slot_idx as u16);
642
643        // SAFETY: entry_ptr is valid (just allocated), not in any DLL yet
644        unsafe { self.levels[lvl_idx].slot(slot_idx).push_entry(entry_ptr) };
645
646        // Activate slot and level (idempotent — OR is a no-op if already set)
647        self.levels[lvl_idx].activate_slot(slot_idx);
648        self.active_levels |= 1 << lvl_idx;
649    }
650
651    /// Removes an entry from its level's slot DLL.
652    ///
653    /// Reads the stored level and slot index from the entry (set at insertion
654    /// time). Does NOT recompute from delta — that would be unsound after
655    /// `current_ticks` advances.
656    #[inline]
657    #[allow(clippy::needless_pass_by_ref_mut)]
658    fn remove_entry(&mut self, entry_ptr: EntryPtr<T>) {
659        // SAFETY: entry_ptr is valid (caller guarantee)
660        let entry = unsafe { entry_ref(entry_ptr) };
661
662        let lvl_idx = entry.level() as usize;
663        let slot_idx = entry.slot_idx() as usize;
664
665        // SAFETY: entry_ptr is in this slot's DLL (invariant from insert_entry)
666        unsafe { self.levels[lvl_idx].slot(slot_idx).remove_entry(entry_ptr) };
667
668        if self.levels[lvl_idx].slot(slot_idx).is_empty() {
669            self.levels[lvl_idx].deactivate_slot(slot_idx);
670            if !self.levels[lvl_idx].is_active() {
671                self.active_levels &= !(1 << lvl_idx);
672            }
673        }
674    }
675
676    // =========================================================================
677    // Internal: fire an entry
678    // =========================================================================
679
680    /// Fires a single entry: extracts value, decrements refcount, possibly frees.
681    ///
682    /// Returns `Some(T)` if the value was still present (not already cancelled).
683    #[inline]
684    fn fire_entry(&mut self, entry_ptr: EntryPtr<T>) -> Option<T> {
685        // SAFETY: entry_ptr is valid (we're walking the DLL)
686        let entry = unsafe { entry_ref(entry_ptr) };
687
688        // Extract value
689        // SAFETY: single-threaded
690        let value = unsafe { entry.take_value() };
691
692        let new_refs = entry.dec_refs();
693        if new_refs == 0 {
694            // Fire-and-forget (was refs=1) — free the slab entry immediately
695            // SAFETY: entry_ptr was allocated from our slab
696            unsafe { self.slab.free_ptr(entry_ptr) };
697        }
698        // new_refs == 1: handle exists (was refs=2), entry becomes zombie.
699        // Handle holder will free via cancel() or free().
700
701        self.len -= 1;
702        value
703    }
704
705    // =========================================================================
706    // Internal: poll a single level
707    // =========================================================================
708
709    /// Polls a single level for expired entries up to `limit`.
710    ///
711    fn poll_level(
712        &mut self,
713        lvl_idx: usize,
714        now_ticks: u64,
715        limit: usize,
716        buf: &mut Vec<T>,
717    ) -> usize {
718        let mut fired = 0;
719        let mut mask = self.levels[lvl_idx].active_slots();
720
721        while mask != 0 && fired < limit {
722            let slot_idx = mask.trailing_zeros() as usize;
723            mask &= mask - 1;
724
725            let slot_ptr = self.levels[lvl_idx].slot(slot_idx) as *const crate::level::WheelSlot<T>;
726            // SAFETY: slot_ptr points into self.levels[lvl_idx].slots
727            // (Box<[WheelSlot<T>]>), a stable heap allocation. fire_entry
728            // only mutates self.slab and self.len, not self.levels.
729            let slot = unsafe { &*slot_ptr };
730            let mut entry_ptr = slot.entry_head();
731
732            while !entry_ptr.is_null() && fired < limit {
733                // SAFETY: entry_ptr is in this slot's DLL
734                let entry = unsafe { entry_ref(entry_ptr) };
735                let next_entry = entry.next();
736
737                if entry.deadline_ticks() <= now_ticks {
738                    unsafe { slot.remove_entry(entry_ptr) };
739
740                    if let Some(value) = self.fire_entry(entry_ptr) {
741                        buf.push(value);
742                    }
743                    fired += 1;
744                }
745
746                entry_ptr = next_entry;
747            }
748
749            if slot.is_empty() {
750                self.levels[lvl_idx].deactivate_slot(slot_idx);
751            }
752        }
753
754        // Deactivate level if all slots drained
755        if !self.levels[lvl_idx].is_active() {
756            self.active_levels &= !(1 << lvl_idx);
757        }
758
759        fired
760    }
761}
762
763// =============================================================================
764// Drop
765// =============================================================================
766
767impl<T: 'static, S: SlabStore<Item = WheelEntry<T>>> Drop for TimerWheel<T, S> {
768    fn drop(&mut self) {
769        // Walk active levels and slots via bitmasks, free every entry.
770        let mut lvl_mask = self.active_levels;
771        while lvl_mask != 0 {
772            let lvl_idx = lvl_mask.trailing_zeros() as usize;
773            lvl_mask &= lvl_mask - 1;
774
775            let level = &self.levels[lvl_idx];
776            let mut slot_mask = level.active_slots();
777            while slot_mask != 0 {
778                let slot_idx = slot_mask.trailing_zeros() as usize;
779                slot_mask &= slot_mask - 1;
780
781                let slot = level.slot(slot_idx);
782                let mut entry_ptr = slot.entry_head();
783                while !entry_ptr.is_null() {
784                    // SAFETY: entry_ptr is in this slot's DLL
785                    let entry = unsafe { entry_ref(entry_ptr) };
786                    let next_entry = entry.next();
787
788                    // SAFETY: entry_ptr was allocated from our slab
789                    unsafe { self.slab.free(nexus_slab::RawSlot::from_ptr(entry_ptr)) };
790
791                    entry_ptr = next_entry;
792                }
793            }
794        }
795    }
796}
797
798// =============================================================================
799// Tests
800// =============================================================================
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805    use std::time::{Duration, Instant};
806
807    fn ms(millis: u64) -> Duration {
808        Duration::from_millis(millis)
809    }
810
811    // -------------------------------------------------------------------------
812    // Thread safety
813    // -------------------------------------------------------------------------
814
815    fn _assert_send<T: Send>() {}
816
817    #[test]
818    fn wheel_is_send() {
819        _assert_send::<Wheel<u64>>();
820        _assert_send::<BoundedWheel<u64>>();
821    }
822
823    // -------------------------------------------------------------------------
824    // Construction
825    // -------------------------------------------------------------------------
826
827    #[test]
828    fn default_config() {
829        let now = Instant::now();
830        let wheel: Wheel<u64> = Wheel::unbounded(1024, now);
831        assert!(wheel.is_empty());
832        assert_eq!(wheel.len(), 0);
833    }
834
835    #[test]
836    fn bounded_construction() {
837        let now = Instant::now();
838        let wheel: BoundedWheel<u64> = BoundedWheel::bounded(128, now);
839        assert!(wheel.is_empty());
840    }
841
842    #[test]
843    #[should_panic(expected = "slots_per_level must be a power of 2")]
844    fn invalid_config_non_power_of_two() {
845        let now = Instant::now();
846        WheelBuilder::default()
847            .slots_per_level(65)
848            .unbounded(1024)
849            .build::<u64>(now);
850    }
851
852    // -------------------------------------------------------------------------
853    // Schedule + Cancel
854    // -------------------------------------------------------------------------
855
856    #[test]
857    fn schedule_and_cancel() {
858        let now = Instant::now();
859        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
860
861        let h = wheel.schedule(now + ms(50), 42);
862        assert_eq!(wheel.len(), 1);
863
864        let val = wheel.cancel(h);
865        assert_eq!(val, Some(42));
866        assert_eq!(wheel.len(), 0);
867    }
868
869    #[test]
870    fn schedule_forget_fires() {
871        let now = Instant::now();
872        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
873
874        wheel.schedule_forget(now + ms(10), 99);
875        assert_eq!(wheel.len(), 1);
876
877        let mut buf = Vec::new();
878        let fired = wheel.poll(now + ms(20), &mut buf);
879        assert_eq!(fired, 1);
880        assert_eq!(buf, vec![99]);
881        assert_eq!(wheel.len(), 0);
882    }
883
884    #[test]
885    fn cancel_after_fire_returns_none() {
886        let now = Instant::now();
887        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
888
889        let h = wheel.schedule(now + ms(10), 42);
890
891        let mut buf = Vec::new();
892        wheel.poll(now + ms(20), &mut buf);
893        assert_eq!(buf, vec![42]);
894
895        // Handle is now a zombie
896        let val = wheel.cancel(h);
897        assert_eq!(val, None);
898    }
899
900    #[test]
901    fn free_active_timer_becomes_fire_and_forget() {
902        let now = Instant::now();
903        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
904
905        let h = wheel.schedule(now + ms(10), 42);
906        wheel.free(h); // releases handle, timer stays
907        assert_eq!(wheel.len(), 1);
908
909        let mut buf = Vec::new();
910        wheel.poll(now + ms(20), &mut buf);
911        assert_eq!(buf, vec![42]);
912        assert_eq!(wheel.len(), 0);
913    }
914
915    #[test]
916    fn free_zombie_handle() {
917        let now = Instant::now();
918        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
919
920        let h = wheel.schedule(now + ms(10), 42);
921
922        let mut buf = Vec::new();
923        wheel.poll(now + ms(20), &mut buf);
924
925        // Handle is zombie, free should clean up
926        wheel.free(h);
927    }
928
929    // -------------------------------------------------------------------------
930    // Bounded wheel
931    // -------------------------------------------------------------------------
932
933    #[test]
934    fn bounded_full() {
935        let now = Instant::now();
936        let mut wheel: BoundedWheel<u64> = BoundedWheel::bounded(2, now);
937
938        let h1 = wheel.try_schedule(now + ms(10), 1).unwrap();
939        let h2 = wheel.try_schedule(now + ms(20), 2).unwrap();
940
941        let err = wheel.try_schedule(now + ms(30), 3);
942        assert!(err.is_err());
943        let recovered = err.unwrap_err().into_inner();
944        assert_eq!(recovered, 3);
945
946        // Cancel one, should have room
947        wheel.cancel(h1);
948        let h3 = wheel.try_schedule(now + ms(30), 3).unwrap();
949
950        // Clean up handles
951        wheel.free(h2);
952        wheel.free(h3);
953    }
954
955    #[test]
956    fn bounded_schedule_forget_full() {
957        let now = Instant::now();
958        let mut wheel: BoundedWheel<u64> = BoundedWheel::bounded(1, now);
959
960        wheel.try_schedule_forget(now + ms(10), 1).unwrap();
961        let err = wheel.try_schedule_forget(now + ms(20), 2);
962        assert!(err.is_err());
963    }
964
965    // -------------------------------------------------------------------------
966    // Poll
967    // -------------------------------------------------------------------------
968
969    #[test]
970    fn poll_respects_deadline() {
971        let now = Instant::now();
972        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
973
974        wheel.schedule_forget(now + ms(10), 1);
975        wheel.schedule_forget(now + ms(50), 2);
976        wheel.schedule_forget(now + ms(100), 3);
977
978        let mut buf = Vec::new();
979
980        // At 20ms: only timer 1 should fire
981        let fired = wheel.poll(now + ms(20), &mut buf);
982        assert_eq!(fired, 1);
983        assert_eq!(buf, vec![1]);
984        assert_eq!(wheel.len(), 2);
985
986        // At 60ms: timer 2 fires
987        buf.clear();
988        let fired = wheel.poll(now + ms(60), &mut buf);
989        assert_eq!(fired, 1);
990        assert_eq!(buf, vec![2]);
991
992        // At 200ms: timer 3 fires
993        buf.clear();
994        let fired = wheel.poll(now + ms(200), &mut buf);
995        assert_eq!(fired, 1);
996        assert_eq!(buf, vec![3]);
997
998        assert!(wheel.is_empty());
999    }
1000
1001    #[test]
1002    fn poll_with_limit() {
1003        let now = Instant::now();
1004        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1005
1006        for i in 0..10 {
1007            wheel.schedule_forget(now + ms(1), i);
1008        }
1009
1010        let mut buf = Vec::new();
1011
1012        // Fire 3 at a time
1013        let fired = wheel.poll_with_limit(now + ms(5), 3, &mut buf);
1014        assert_eq!(fired, 3);
1015        assert_eq!(wheel.len(), 7);
1016
1017        let fired = wheel.poll_with_limit(now + ms(5), 3, &mut buf);
1018        assert_eq!(fired, 3);
1019        assert_eq!(wheel.len(), 4);
1020
1021        // Fire remaining
1022        let fired = wheel.poll(now + ms(5), &mut buf);
1023        assert_eq!(fired, 4);
1024        assert!(wheel.is_empty());
1025        assert_eq!(buf.len(), 10);
1026    }
1027
1028    // -------------------------------------------------------------------------
1029    // Multi-level
1030    // -------------------------------------------------------------------------
1031
1032    #[test]
1033    fn timers_across_levels() {
1034        let now = Instant::now();
1035        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1036
1037        // Level 0: 0-63ms
1038        wheel.schedule_forget(now + ms(5), 0);
1039        // Level 1: 64-511ms
1040        wheel.schedule_forget(now + ms(200), 1);
1041        // Level 2: 512-4095ms
1042        wheel.schedule_forget(now + ms(1000), 2);
1043
1044        let mut buf = Vec::new();
1045
1046        wheel.poll(now + ms(10), &mut buf);
1047        assert_eq!(buf, vec![0]);
1048
1049        buf.clear();
1050        wheel.poll(now + ms(250), &mut buf);
1051        assert_eq!(buf, vec![1]);
1052
1053        buf.clear();
1054        wheel.poll(now + ms(1500), &mut buf);
1055        assert_eq!(buf, vec![2]);
1056
1057        assert!(wheel.is_empty());
1058    }
1059
1060    // -------------------------------------------------------------------------
1061    // next_deadline
1062    // -------------------------------------------------------------------------
1063
1064    #[test]
1065    fn next_deadline_empty() {
1066        let now = Instant::now();
1067        let wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1068        assert!(wheel.next_deadline().is_none());
1069    }
1070
1071    #[test]
1072    fn next_deadline_returns_earliest() {
1073        let now = Instant::now();
1074        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1075
1076        wheel.schedule_forget(now + ms(100), 1);
1077        wheel.schedule_forget(now + ms(50), 2);
1078        wheel.schedule_forget(now + ms(200), 3);
1079
1080        let next = wheel.next_deadline().unwrap();
1081        // Should be close to now + 50ms (within tick granularity)
1082        let delta = next.duration_since(now);
1083        assert!(delta >= ms(49) && delta <= ms(51));
1084    }
1085
1086    // -------------------------------------------------------------------------
1087    // Deadline in the past
1088    // -------------------------------------------------------------------------
1089
1090    #[test]
1091    fn deadline_in_the_past_fires_immediately() {
1092        let now = Instant::now();
1093        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1094
1095        // Schedule at epoch (which is "now" at construction)
1096        wheel.schedule_forget(now, 42);
1097
1098        let mut buf = Vec::new();
1099        let fired = wheel.poll(now + ms(1), &mut buf);
1100        assert_eq!(fired, 1);
1101        assert_eq!(buf, vec![42]);
1102    }
1103
1104    // -------------------------------------------------------------------------
1105    // Deadline beyond max range — clamped
1106    // -------------------------------------------------------------------------
1107
1108    #[test]
1109    fn deadline_beyond_max_range_clamped() {
1110        let now = Instant::now();
1111        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1112
1113        // Way in the future — should clamp to highest level
1114        let h = wheel.schedule(now + Duration::from_secs(100_000), 99);
1115        assert_eq!(wheel.len(), 1);
1116
1117        // Won't fire at any reasonable time but will fire when enough ticks pass
1118        let mut buf = Vec::new();
1119        wheel.poll(now + Duration::from_secs(100_001), &mut buf);
1120        assert_eq!(buf, vec![99]);
1121
1122        // Note: handle was already consumed by the poll (fire-and-forget path won't
1123        // apply since refs=2). Actually the handle still exists. Let's clean up.
1124        // The timer already fired, so cancel returns None.
1125        // Actually buf got the value, which means it fired. But handle still needs cleanup.
1126        // We already pushed the value so we need to handle the zombie.
1127        // Wait — we used schedule (refs=2), poll fired it (refs 2→1 zombie), handle `h` exists.
1128        // Actually we consumed it with the poll — no we didn't, we still have `h`.
1129
1130        // h is a zombie handle now
1131        let val = wheel.cancel(h);
1132        assert_eq!(val, None);
1133    }
1134
1135    // -------------------------------------------------------------------------
1136    // Drop
1137    // -------------------------------------------------------------------------
1138
1139    #[test]
1140    fn drop_cleans_up_active_entries() {
1141        let now = Instant::now();
1142        let mut wheel: Wheel<String> = Wheel::unbounded(1024, now);
1143
1144        for i in 0..100 {
1145            wheel.schedule_forget(now + ms(i * 10), format!("timer-{i}"));
1146        }
1147
1148        assert_eq!(wheel.len(), 100);
1149        // Drop should free all entries without leaking
1150        drop(wheel);
1151    }
1152
1153    #[test]
1154    fn drop_with_outstanding_handles() {
1155        let now = Instant::now();
1156        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1157
1158        // Schedule but DON'T cancel — just free the handles
1159        let h1 = wheel.schedule(now + ms(10), 1);
1160        let h2 = wheel.schedule(now + ms(20), 2);
1161
1162        // Free the handles (convert to fire-and-forget) so they don't debug_assert
1163        wheel.free(h1);
1164        wheel.free(h2);
1165
1166        // Drop the wheel — should clean up the entries
1167        drop(wheel);
1168    }
1169
1170    // -------------------------------------------------------------------------
1171    // Level selection
1172    // -------------------------------------------------------------------------
1173
1174    #[test]
1175    fn level_selection_boundaries() {
1176        let now = Instant::now();
1177        let wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1178
1179        // Level 0: delta < 64
1180        assert_eq!(wheel.select_level(0), 0);
1181        assert_eq!(wheel.select_level(63), 0);
1182
1183        // Level 1: 64 <= delta < 512
1184        assert_eq!(wheel.select_level(64), 1);
1185        assert_eq!(wheel.select_level(511), 1);
1186
1187        // Level 2: 512 <= delta < 4096
1188        assert_eq!(wheel.select_level(512), 2);
1189    }
1190
1191    // -------------------------------------------------------------------------
1192    // Bug fix validation: cancel after time advance
1193    // -------------------------------------------------------------------------
1194
1195    #[test]
1196    fn cancel_after_time_advance() {
1197        // The critical bug: schedule at T+500ms (level 2, delta=500 ticks),
1198        // poll at T+400ms (no fire, but current_ticks advances to 400),
1199        // cancel at T+400ms. Old code would recompute delta = 500-400 = 100
1200        // → level 1. But the entry is in level 2. Stored location fixes this.
1201        let now = Instant::now();
1202        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1203
1204        let h = wheel.schedule(now + ms(500), 42);
1205        assert_eq!(wheel.len(), 1);
1206
1207        // Advance time — timer doesn't fire (deadline is 500ms)
1208        let mut buf = Vec::new();
1209        let fired = wheel.poll(now + ms(400), &mut buf);
1210        assert_eq!(fired, 0);
1211        assert!(buf.is_empty());
1212
1213        // Cancel after time advance — must find the entry in the correct slot
1214        let val = wheel.cancel(h);
1215        assert_eq!(val, Some(42));
1216        assert_eq!(wheel.len(), 0);
1217    }
1218
1219    // -------------------------------------------------------------------------
1220    // Same-slot entries
1221    // -------------------------------------------------------------------------
1222
1223    #[test]
1224    fn multiple_entries_same_slot() {
1225        let now = Instant::now();
1226        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1227
1228        // All 5 timers at the same deadline → same slot
1229        let mut handles = Vec::new();
1230        for i in 0..5 {
1231            handles.push(wheel.schedule(now + ms(10), i));
1232        }
1233        assert_eq!(wheel.len(), 5);
1234
1235        // Cancel the middle ones
1236        let v2 = wheel.cancel(handles.remove(2));
1237        assert_eq!(v2, Some(2));
1238        let v0 = wheel.cancel(handles.remove(0));
1239        assert_eq!(v0, Some(0));
1240        assert_eq!(wheel.len(), 3);
1241
1242        // Poll — remaining 3 should fire
1243        let mut buf = Vec::new();
1244        let fired = wheel.poll(now + ms(20), &mut buf);
1245        assert_eq!(fired, 3);
1246
1247        // Clean up zombie handles
1248        for h in handles {
1249            let val = wheel.cancel(h);
1250            assert_eq!(val, None); // already fired
1251        }
1252    }
1253
1254    // -------------------------------------------------------------------------
1255    // Level boundary
1256    // -------------------------------------------------------------------------
1257
1258    #[test]
1259    fn entry_at_level_boundary() {
1260        // Default config: level 0 range = 64 ticks (64ms).
1261        // A deadline at exactly tick 64 should go to level 1, not level 0.
1262        let now = Instant::now();
1263        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1264
1265        let h = wheel.schedule(now + ms(64), 99);
1266        assert_eq!(wheel.len(), 1);
1267
1268        // Should NOT fire at 63ms
1269        let mut buf = Vec::new();
1270        let fired = wheel.poll(now + ms(63), &mut buf);
1271        assert_eq!(fired, 0);
1272
1273        // Should fire at 64ms
1274        let fired = wheel.poll(now + ms(65), &mut buf);
1275        assert_eq!(fired, 1);
1276        assert_eq!(buf, vec![99]);
1277
1278        // Clean up zombie handle
1279        wheel.cancel(h);
1280    }
1281
1282    // -------------------------------------------------------------------------
1283    // Bookmark/resumption with mixed expiry
1284    // -------------------------------------------------------------------------
1285
1286    #[test]
1287    fn poll_with_limit_mixed_expiry() {
1288        let now = Instant::now();
1289        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1290
1291        // 3 expired at poll time, 2 not
1292        wheel.schedule_forget(now + ms(5), 1);
1293        wheel.schedule_forget(now + ms(5), 2);
1294        wheel.schedule_forget(now + ms(5), 3);
1295        wheel.schedule_forget(now + ms(500), 4); // not expired
1296        wheel.schedule_forget(now + ms(500), 5); // not expired
1297        assert_eq!(wheel.len(), 5);
1298
1299        let mut buf = Vec::new();
1300
1301        // Fire 2 of the 3 expired
1302        let fired = wheel.poll_with_limit(now + ms(10), 2, &mut buf);
1303        assert_eq!(fired, 2);
1304        assert_eq!(wheel.len(), 3);
1305
1306        // Fire remaining expired (1 more)
1307        let fired = wheel.poll_with_limit(now + ms(10), 5, &mut buf);
1308        assert_eq!(fired, 1);
1309        assert_eq!(wheel.len(), 2);
1310
1311        // The 2 unexpired should still be there
1312        assert_eq!(buf.len(), 3);
1313    }
1314
1315    // -------------------------------------------------------------------------
1316    // Re-add after drain
1317    // -------------------------------------------------------------------------
1318
1319    #[test]
1320    fn reuse_after_full_drain() {
1321        let now = Instant::now();
1322        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1323
1324        // Round 1: schedule and drain
1325        for i in 0..10 {
1326            wheel.schedule_forget(now + ms(1), i);
1327        }
1328        let mut buf = Vec::new();
1329        wheel.poll(now + ms(5), &mut buf);
1330        assert_eq!(buf.len(), 10);
1331        assert!(wheel.is_empty());
1332
1333        // Round 2: schedule and drain again — wheel must work normally
1334        buf.clear();
1335        for i in 10..20 {
1336            wheel.schedule_forget(now + ms(100), i);
1337        }
1338        assert_eq!(wheel.len(), 10);
1339
1340        wheel.poll(now + ms(200), &mut buf);
1341        assert_eq!(buf.len(), 10);
1342        assert!(wheel.is_empty());
1343    }
1344
1345    // -------------------------------------------------------------------------
1346    // All levels active simultaneously
1347    // -------------------------------------------------------------------------
1348
1349    #[test]
1350    fn all_levels_active() {
1351        let now = Instant::now();
1352        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1353
1354        // Schedule one timer per level with increasing distances.
1355        // Level 0: <64ms, Level 1: 64-511ms, Level 2: 512-4095ms, etc.
1356        let distances = [10, 100, 1000, 5000, 40_000, 300_000, 3_000_000];
1357        let mut handles: Vec<TimerHandle<u64>> = Vec::new();
1358        for (i, &d) in distances.iter().enumerate() {
1359            handles.push(wheel.schedule(now + ms(d), i as u64));
1360        }
1361        assert_eq!(wheel.len(), 7);
1362
1363        // Cancel in a shuffled order: 4, 1, 6, 0, 3, 5, 2
1364        let order = [4, 1, 6, 0, 3, 5, 2];
1365        // Take ownership by swapping with dummies — actually we need to
1366        // cancel by index. Let's use Option to track.
1367        let mut opt_handles: Vec<Option<TimerHandle<u64>>> =
1368            handles.into_iter().map(Some).collect();
1369
1370        for &idx in &order {
1371            let h = opt_handles[idx].take().unwrap();
1372            let val = wheel.cancel(h);
1373            assert_eq!(val, Some(idx as u64));
1374        }
1375        assert!(wheel.is_empty());
1376    }
1377
1378    // -------------------------------------------------------------------------
1379    // Poll values match
1380    // -------------------------------------------------------------------------
1381
1382    #[test]
1383    fn poll_values_match() {
1384        let now = Instant::now();
1385        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1386
1387        let expected: Vec<u64> = (100..110).collect();
1388        for &v in &expected {
1389            wheel.schedule_forget(now + ms(5), v);
1390        }
1391
1392        let mut buf = Vec::new();
1393        wheel.poll(now + ms(10), &mut buf);
1394
1395        buf.sort();
1396        assert_eq!(buf, expected);
1397    }
1398
1399    // -------------------------------------------------------------------------
1400    // Reschedule
1401    // -------------------------------------------------------------------------
1402
1403    #[test]
1404    fn reschedule_moves_deadline() {
1405        let now = Instant::now();
1406        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1407
1408        let h = wheel.schedule(now + ms(100), 42);
1409        assert_eq!(wheel.len(), 1);
1410
1411        // Reschedule to earlier
1412        let h = wheel.reschedule(h, now + ms(50));
1413        assert_eq!(wheel.len(), 1);
1414
1415        // Should NOT fire at 40ms
1416        let mut buf = Vec::new();
1417        let fired = wheel.poll(now + ms(40), &mut buf);
1418        assert_eq!(fired, 0);
1419
1420        // Should fire at 50ms
1421        let fired = wheel.poll(now + ms(55), &mut buf);
1422        assert_eq!(fired, 1);
1423        assert_eq!(buf, vec![42]);
1424
1425        // Clean up zombie
1426        wheel.cancel(h);
1427    }
1428
1429    #[test]
1430    fn reschedule_to_later() {
1431        let now = Instant::now();
1432        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1433
1434        let h = wheel.schedule(now + ms(50), 7);
1435
1436        // Reschedule to later
1437        let h = wheel.reschedule(h, now + ms(200));
1438
1439        // Should NOT fire at original deadline
1440        let mut buf = Vec::new();
1441        let fired = wheel.poll(now + ms(60), &mut buf);
1442        assert_eq!(fired, 0);
1443
1444        // Should fire at new deadline
1445        let fired = wheel.poll(now + ms(210), &mut buf);
1446        assert_eq!(fired, 1);
1447        assert_eq!(buf, vec![7]);
1448
1449        wheel.cancel(h);
1450    }
1451
1452    #[test]
1453    #[should_panic(expected = "cannot reschedule a fired timer")]
1454    fn reschedule_panics_on_zombie() {
1455        let now = Instant::now();
1456        let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1457
1458        let h = wheel.schedule(now + ms(10), 42);
1459
1460        let mut buf = Vec::new();
1461        wheel.poll(now + ms(20), &mut buf);
1462
1463        // h is now a zombie — reschedule should panic
1464        let _h = wheel.reschedule(h, now + ms(100));
1465    }
1466
1467    // -------------------------------------------------------------------------
1468    // Non-default builder configurations (L13)
1469    // -------------------------------------------------------------------------
1470
1471    #[test]
1472    fn custom_slots_per_level() {
1473        let now = Instant::now();
1474        // 32 slots/level instead of default 64
1475        let mut wheel: Wheel<u64> = WheelBuilder::default()
1476            .slots_per_level(32)
1477            .unbounded(256)
1478            .build(now);
1479
1480        // Level 0 range = 32 ticks (32ms with 1ms tick)
1481        // Deadline at 20ms should go to level 0
1482        let h1 = wheel.schedule(now + ms(20), 1);
1483        // Deadline at 40ms should go to level 1 (>= 32 ticks)
1484        let h2 = wheel.schedule(now + ms(40), 2);
1485
1486        let mut buf = Vec::new();
1487        wheel.poll(now + ms(25), &mut buf);
1488        assert_eq!(buf, vec![1]);
1489
1490        buf.clear();
1491        wheel.poll(now + ms(50), &mut buf);
1492        assert_eq!(buf, vec![2]);
1493
1494        wheel.cancel(h1);
1495        wheel.cancel(h2);
1496    }
1497
1498    #[test]
1499    fn custom_clk_shift() {
1500        let now = Instant::now();
1501        // clk_shift=2 means 4x multiplier between levels (instead of 8x)
1502        let mut wheel: Wheel<u64> = WheelBuilder::default()
1503            .clk_shift(2)
1504            .unbounded(256)
1505            .build(now);
1506
1507        // Level 0: 64 slots × 1ms = 64ms range
1508        // Level 1: 64 slots × 4ms = 256ms range (with 4x multiplier)
1509        let h1 = wheel.schedule(now + ms(50), 1); // level 0
1510        let h2 = wheel.schedule(now + ms(100), 2); // level 1 (>= 64 ticks, <256 ticks)
1511
1512        let mut buf = Vec::new();
1513        wheel.poll(now + ms(55), &mut buf);
1514        assert_eq!(buf, vec![1]);
1515
1516        buf.clear();
1517        wheel.poll(now + ms(110), &mut buf);
1518        assert_eq!(buf, vec![2]);
1519
1520        wheel.cancel(h1);
1521        wheel.cancel(h2);
1522    }
1523
1524    #[test]
1525    fn custom_num_levels() {
1526        let now = Instant::now();
1527        // Only 3 levels instead of 7
1528        let mut wheel: Wheel<u64> = WheelBuilder::default()
1529            .num_levels(3)
1530            .unbounded(256)
1531            .build(now);
1532
1533        // Level 0: 64ms, Level 1: 512ms, Level 2: 4096ms
1534        // Max range is level 2 = 4096ms. Beyond that, clamped.
1535        let h = wheel.schedule(now + ms(3000), 42);
1536        assert_eq!(wheel.len(), 1);
1537
1538        let mut buf = Vec::new();
1539        wheel.poll(now + ms(3100), &mut buf);
1540        assert_eq!(buf, vec![42]);
1541
1542        wheel.cancel(h);
1543    }
1544
1545    #[test]
1546    fn custom_tick_duration() {
1547        let now = Instant::now();
1548        // 100μs ticks instead of 1ms
1549        let mut wheel: Wheel<u64> = WheelBuilder::default()
1550            .tick_duration(Duration::from_micros(100))
1551            .unbounded(256)
1552            .build(now);
1553
1554        // 1ms = 10 ticks, should be level 0 (< 64 ticks)
1555        wheel.schedule_forget(now + ms(1), 1);
1556        // 10ms = 100 ticks, should be level 1 (>= 64 ticks)
1557        wheel.schedule_forget(now + ms(10), 2);
1558
1559        let mut buf = Vec::new();
1560        wheel.poll(now + ms(2), &mut buf);
1561        assert_eq!(buf, vec![1]);
1562
1563        buf.clear();
1564        wheel.poll(now + ms(15), &mut buf);
1565        assert_eq!(buf, vec![2]);
1566    }
1567
1568    #[test]
1569    fn bounded_custom_config() {
1570        let now = Instant::now();
1571        let mut wheel: BoundedWheel<u64> = WheelBuilder::default()
1572            .slots_per_level(16)
1573            .num_levels(4)
1574            .bounded(8)
1575            .build(now);
1576
1577        // Fill to capacity
1578        let mut handles = Vec::new();
1579        for i in 0..8 {
1580            handles.push(wheel.try_schedule(now + ms(i * 10 + 10), i).unwrap());
1581        }
1582        assert!(wheel.try_schedule(now + ms(100), 99).is_err());
1583
1584        // Cancel one, schedule another
1585        wheel.cancel(handles.remove(0));
1586        let h = wheel.try_schedule(now + ms(100), 99).unwrap();
1587        handles.push(h);
1588
1589        // Clean up
1590        for h in handles {
1591            wheel.cancel(h);
1592        }
1593    }
1594
1595    // -------------------------------------------------------------------------
1596    // Builder validation (L13)
1597    // -------------------------------------------------------------------------
1598
1599    #[test]
1600    #[should_panic(expected = "slots_per_level must be <= 64")]
1601    fn invalid_config_too_many_slots() {
1602        let now = Instant::now();
1603        WheelBuilder::default()
1604            .slots_per_level(128)
1605            .unbounded(1024)
1606            .build::<u64>(now);
1607    }
1608
1609    #[test]
1610    #[should_panic(expected = "num_levels must be > 0")]
1611    fn invalid_config_zero_levels() {
1612        let now = Instant::now();
1613        WheelBuilder::default()
1614            .num_levels(0)
1615            .unbounded(1024)
1616            .build::<u64>(now);
1617    }
1618
1619    #[test]
1620    #[should_panic(expected = "num_levels must be <= 8")]
1621    fn invalid_config_too_many_levels() {
1622        let now = Instant::now();
1623        WheelBuilder::default()
1624            .num_levels(9)
1625            .unbounded(1024)
1626            .build::<u64>(now);
1627    }
1628
1629    #[test]
1630    #[should_panic(expected = "clk_shift must be > 0")]
1631    fn invalid_config_zero_shift() {
1632        let now = Instant::now();
1633        WheelBuilder::default()
1634            .clk_shift(0)
1635            .unbounded(1024)
1636            .build::<u64>(now);
1637    }
1638
1639    #[test]
1640    #[should_panic(expected = "tick_duration must be non-zero")]
1641    fn invalid_config_zero_tick() {
1642        let now = Instant::now();
1643        WheelBuilder::default()
1644            .tick_duration(Duration::ZERO)
1645            .unbounded(1024)
1646            .build::<u64>(now);
1647    }
1648
1649    #[test]
1650    #[should_panic(expected = "overflow")]
1651    fn invalid_config_shift_overflow() {
1652        let now = Instant::now();
1653        // 8 levels × clk_shift=8 = 56 bits shift on level 7
1654        // Plus 64 slots (6 bits) = 62 bits, should be OK
1655        // But 8 levels × clk_shift=9 = 63 + 6 = 69 bits — overflow
1656        WheelBuilder::default()
1657            .num_levels(8)
1658            .clk_shift(9)
1659            .unbounded(1024)
1660            .build::<u64>(now);
1661    }
1662
1663    // -------------------------------------------------------------------------
1664    // Miri-compatible tests (L12)
1665    // -------------------------------------------------------------------------
1666    // These tests exercise the raw pointer paths (DLL operations, entry
1667    // lifecycle, poll) with Drop types to catch UB under Miri.
1668
1669    #[test]
1670    fn miri_schedule_cancel_drop_type() {
1671        let now = Instant::now();
1672        let mut wheel: Wheel<String> = Wheel::unbounded(64, now);
1673
1674        let h = wheel.schedule(now + ms(50), "hello".to_string());
1675        let val = wheel.cancel(h);
1676        assert_eq!(val, Some("hello".to_string()));
1677        assert!(wheel.is_empty());
1678    }
1679
1680    #[test]
1681    fn miri_poll_fires_drop_type() {
1682        let now = Instant::now();
1683        let mut wheel: Wheel<String> = Wheel::unbounded(64, now);
1684
1685        wheel.schedule_forget(now + ms(10), "a".to_string());
1686        wheel.schedule_forget(now + ms(10), "b".to_string());
1687        wheel.schedule_forget(now + ms(10), "c".to_string());
1688
1689        let mut buf = Vec::new();
1690        let fired = wheel.poll(now + ms(20), &mut buf);
1691        assert_eq!(fired, 3);
1692        assert_eq!(buf.len(), 3);
1693        assert!(wheel.is_empty());
1694    }
1695
1696    #[test]
1697    fn miri_cancel_zombie_drop_type() {
1698        let now = Instant::now();
1699        let mut wheel: Wheel<String> = Wheel::unbounded(64, now);
1700
1701        let h = wheel.schedule(now + ms(10), "zombie".to_string());
1702
1703        let mut buf = Vec::new();
1704        wheel.poll(now + ms(20), &mut buf);
1705        assert_eq!(buf, vec!["zombie".to_string()]);
1706
1707        // h is now a zombie — cancel frees the entry
1708        let val = wheel.cancel(h);
1709        assert_eq!(val, None);
1710    }
1711
1712    #[test]
1713    fn miri_free_active_and_zombie() {
1714        let now = Instant::now();
1715        let mut wheel: Wheel<String> = Wheel::unbounded(64, now);
1716
1717        // Active → fire-and-forget via free
1718        let h1 = wheel.schedule(now + ms(10), "active".to_string());
1719        wheel.free(h1);
1720
1721        // Poll fires the fire-and-forget entry
1722        let mut buf = Vec::new();
1723        wheel.poll(now + ms(20), &mut buf);
1724        assert_eq!(buf, vec!["active".to_string()]);
1725
1726        // Zombie → free
1727        let h2 = wheel.schedule(now + ms(10), "will-fire".to_string());
1728        buf.clear();
1729        wheel.poll(now + ms(20), &mut buf);
1730        wheel.free(h2); // zombie cleanup
1731    }
1732
1733    #[test]
1734    fn miri_reschedule_drop_type() {
1735        let now = Instant::now();
1736        let mut wheel: Wheel<String> = Wheel::unbounded(64, now);
1737
1738        let h = wheel.schedule(now + ms(100), "moveme".to_string());
1739        let h = wheel.reschedule(h, now + ms(50));
1740
1741        let mut buf = Vec::new();
1742        wheel.poll(now + ms(55), &mut buf);
1743        assert_eq!(buf, vec!["moveme".to_string()]);
1744
1745        wheel.cancel(h);
1746    }
1747
1748    #[test]
1749    fn miri_dll_multi_entry_same_slot() {
1750        // Multiple entries in same slot exercises DLL push/remove paths
1751        let now = Instant::now();
1752        let mut wheel: Wheel<Vec<u8>> = Wheel::unbounded(64, now);
1753
1754        let mut handles = Vec::new();
1755        for i in 0..5 {
1756            handles.push(wheel.schedule(now + ms(10), vec![i; 32]));
1757        }
1758
1759        // Cancel middle, then head, then tail
1760        let v2 = wheel.cancel(handles.remove(2));
1761        assert_eq!(v2.unwrap(), vec![2; 32]);
1762
1763        let v0 = wheel.cancel(handles.remove(0));
1764        assert_eq!(v0.unwrap(), vec![0; 32]);
1765
1766        // Poll remaining
1767        let mut buf = Vec::new();
1768        wheel.poll(now + ms(20), &mut buf);
1769        assert_eq!(buf.len(), 3);
1770
1771        // Clean up zombie handles
1772        for h in handles {
1773            wheel.cancel(h);
1774        }
1775    }
1776
1777    #[test]
1778    fn miri_drop_wheel_with_entries() {
1779        let now = Instant::now();
1780        let mut wheel: Wheel<String> = Wheel::unbounded(64, now);
1781
1782        // Schedule entries across multiple levels
1783        for i in 0..20 {
1784            wheel.schedule_forget(now + ms(i * 100), format!("entry-{i}"));
1785        }
1786        assert_eq!(wheel.len(), 20);
1787
1788        // Drop with active entries — must not leak or UB
1789        drop(wheel);
1790    }
1791
1792    #[test]
1793    fn miri_bounded_lifecycle() {
1794        let now = Instant::now();
1795        let mut wheel: BoundedWheel<String> = BoundedWheel::bounded(4, now);
1796
1797        let h1 = wheel.try_schedule(now + ms(10), "a".to_string()).unwrap();
1798        let h2 = wheel.try_schedule(now + ms(20), "b".to_string()).unwrap();
1799        let h3 = wheel.try_schedule(now + ms(30), "c".to_string()).unwrap();
1800        let h4 = wheel.try_schedule(now + ms(40), "d".to_string()).unwrap();
1801
1802        // Full
1803        let err = wheel.try_schedule(now + ms(50), "e".to_string());
1804        assert!(err.is_err());
1805
1806        // Cancel and reuse
1807        wheel.cancel(h1);
1808        let h5 = wheel.try_schedule(now + ms(50), "e".to_string()).unwrap();
1809
1810        // Poll fires some
1811        let mut buf = Vec::new();
1812        wheel.poll(now + ms(25), &mut buf);
1813
1814        // Clean up all remaining handles
1815        wheel.cancel(h2);
1816        wheel.free(h3);
1817        wheel.free(h4);
1818        wheel.free(h5);
1819    }
1820}
1821
1822#[cfg(test)]
1823mod proptests {
1824    use super::*;
1825    use proptest::prelude::*;
1826    use std::collections::HashSet;
1827    use std::mem;
1828    use std::time::{Duration, Instant};
1829
1830    /// Operation in a schedule/cancel interleaving.
1831    #[derive(Debug, Clone)]
1832    enum Op {
1833        /// Schedule a timer at `deadline_ms` milliseconds from epoch.
1834        Schedule { deadline_ms: u64 },
1835        /// Cancel the timer at the given index (modulo outstanding handles).
1836        Cancel { idx: usize },
1837    }
1838
1839    fn op_strategy() -> impl Strategy<Value = Op> {
1840        prop_oneof![
1841            // Schedule with deadlines from 1ms to 10_000ms
1842            (1u64..10_000).prop_map(|deadline_ms| Op::Schedule { deadline_ms }),
1843            // Cancel at random index
1844            any::<usize>().prop_map(|idx| Op::Cancel { idx }),
1845        ]
1846    }
1847
1848    proptest! {
1849        #![proptest_config(ProptestConfig::with_cases(500))]
1850
1851        /// Fuzz schedule/cancel interleaving.
1852        ///
1853        /// Random sequence of schedule and cancel operations. Invariants:
1854        /// - `len` always matches outstanding active timers
1855        /// - cancel on active handle returns `Some`
1856        /// - poll collects all un-cancelled values
1857        #[test]
1858        fn fuzz_schedule_cancel_interleaving(ops in proptest::collection::vec(op_strategy(), 1..200)) {
1859            let now = Instant::now();
1860            let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1861
1862            let mut handles: Vec<TimerHandle<u64>> = Vec::new();
1863            let mut active_values: HashSet<u64> = HashSet::new();
1864            let mut next_id: u64 = 0;
1865
1866            for op in &ops {
1867                match op {
1868                    Op::Schedule { deadline_ms } => {
1869                        let id = next_id;
1870                        next_id += 1;
1871                        let h = wheel.schedule(now + Duration::from_millis(*deadline_ms), id);
1872                        handles.push(h);
1873                        active_values.insert(id);
1874                    }
1875                    Op::Cancel { idx } => {
1876                        if !handles.is_empty() {
1877                            let i = idx % handles.len();
1878                            let h = handles.swap_remove(i);
1879                            let val = wheel.cancel(h);
1880                            // Value should be Some (all handles are for active timers)
1881                            let v = val.unwrap();
1882                            assert!(active_values.remove(&v));
1883                        }
1884                    }
1885                }
1886                // len must match active values
1887                prop_assert_eq!(wheel.len(), active_values.len());
1888            }
1889
1890            // Poll everything — should collect exactly the remaining active values
1891            let mut buf = Vec::new();
1892            // Use a far-future time to fire everything
1893            wheel.poll(now + Duration::from_secs(100_000), &mut buf);
1894
1895            // Clean up zombie handles (poll fired them, handles still exist)
1896            for h in handles {
1897                mem::forget(h);
1898            }
1899
1900            let fired_set: HashSet<u64> = buf.into_iter().collect();
1901            prop_assert_eq!(fired_set, active_values);
1902            prop_assert!(wheel.is_empty());
1903        }
1904
1905        /// Fuzz poll timing.
1906        ///
1907        /// Schedule N timers with random deadlines. Poll at random increasing
1908        /// times. Assert every timer fires exactly once, fired deadlines are
1909        /// all <= poll time, unfired deadlines are all > poll time.
1910        #[test]
1911        fn fuzz_poll_timing(
1912            deadlines in proptest::collection::vec(1u64..5000, 1..100),
1913            poll_times in proptest::collection::vec(1u64..10_000, 1..20),
1914        ) {
1915            let now = Instant::now();
1916            let mut wheel: Wheel<u64> = Wheel::unbounded(1024, now);
1917
1918            // Schedule all timers (fire-and-forget)
1919            for (i, &d) in deadlines.iter().enumerate() {
1920                wheel.schedule_forget(now + Duration::from_millis(d), i as u64);
1921            }
1922
1923            // Sort poll times to be monotonically increasing
1924            let mut sorted_times: Vec<u64> = poll_times;
1925            sorted_times.sort();
1926            sorted_times.dedup();
1927
1928            let mut all_fired: Vec<u64> = Vec::new();
1929
1930            for &t in &sorted_times {
1931                let mut buf = Vec::new();
1932                wheel.poll(now + Duration::from_millis(t), &mut buf);
1933
1934                // Every fired entry should have deadline_ms <= t
1935                for &id in &buf {
1936                    let deadline_ms = deadlines[id as usize];
1937                    prop_assert!(deadline_ms <= t,
1938                        "Timer {} with deadline {}ms fired at {}ms", id, deadline_ms, t);
1939                }
1940
1941                all_fired.extend(buf);
1942            }
1943
1944            // Fire everything remaining
1945            let mut final_buf = Vec::new();
1946            wheel.poll(now + Duration::from_secs(100_000), &mut final_buf);
1947            all_fired.extend(final_buf);
1948
1949            // Every timer should have fired exactly once
1950            all_fired.sort();
1951            let expected: Vec<u64> = (0..deadlines.len() as u64).collect();
1952            prop_assert_eq!(all_fired, expected, "Not all timers fired exactly once");
1953            prop_assert!(wheel.is_empty());
1954        }
1955    }
1956}