TimerWheel
struct TimerWheelFixed-size hashed timing wheel with an overflow list.
Thread-unsafe; intended to be driven by the reactor on a single thread. Cross-thread cancellation is a Stage 2+ concern.
Implements Movable
__init__
def __init__(out self, now_ms: UInt64 = UInt64(0)) Create a wheel anchored at the given monotonic time.
- Args
now_ms: Absolute millisecond value representing "now" at construction. Subsequentadvance/scheduletimes must be expressed on the same clock.
schedule
def schedule(mut self, after_ms: Int, token: UInt64) -> UInt64 Schedule a timer to fire after after_ms milliseconds.
after_ms <= 0 is clamped to 1 — timers fire on the next
advance, not on the current tick (advance processes each
tick after incrementing the clock, so a timer placed in the
current slot would only fire after a full rotation).
- Args
after_ms: Delay in milliseconds relative to the current tick.token: Arbitrary user value reported back viaadvance.
cancel
def cancel(mut self, id: UInt64) -> Bool Cancel a previously-scheduled timer.
Safe to call for already-fired or never-scheduled IDs; returns
False in those cases. The cancellation is lazy: the entry is
flagged inactive and physically removed the next time the slot
containing it is processed by advance.
- Args
id: Timer ID returned byschedule.
advance
def advance(mut self, now_ms: UInt64, mut fired: List[UInt64]) Advance the wheel to now_ms, appending fired tokens to fired.
Each tick (millisecond) between the current time and now_ms is
processed in order:
- The current slot's entry list is drained; entries that are
still
activeappend their token tofiredand are then removed from_entries. Cancelled entries are silently removed. - Any overflow entries whose
fire_at_msnow fits within one rotation are promoted into the appropriate wheel slot.
now_ms must be monotonically non-decreasing across calls.
Regressing it is undefined behaviour.
A gap longer than one rotation is not walked tick by tick: the
wheel jumps to now_ms and re-buckets what is left, so the
cost follows the number of timers, not the length of the gap.
Timers due within one rotation are reported in fire order;
overflow timers due in the same jump come after them.
- Args
now_ms: Current absolute millisecond time (same clock asnow_mspassed to__init__).fired: Output list; fired tokens are appended. The caller does not need to clear it first.
now_ms
def now_ms(self) -> UInt64 Return the current tick time tracked by the wheel.
active_count
def active_count(self) -> Int Return the number of scheduled-and-not-yet-cancelled timers.
Inactive (cancelled but not yet reaped) entries are excluded.
next_fire_ms
def next_fire_ms(self) -> UInt64 Absolute time of the soonest scheduled fire, or now_ms + 2^32 if no timers are scheduled.
Sizing the reactor poll timeout: the reactor sleeps at most until
this time so the wheel can fire pending timers on its next
advance. Cost is a bounded scan of the wheel slots
(_WHEEL_SLOTS = 512), independent of the number of active
timers -- the earlier O(n) _entries scan is gone.
The result is a lower bound: a slot may hold only lazily cancelled ids, in which case the reactor wakes one tick early and re-polls (harmless). It is never later than the true next fire, so a timer can never be missed.
Overflow timers (more than one rotation out) are only re-examined
-- promoted into the wheel, and fired if due -- at the next slot-0
boundary, _WHEEL_SLOTS - _current_slot ticks away. While the
overflow list is non-empty the hint is therefore capped at that
boundary, both for the slot scan and for the fallback (RT-01):
a full-rotation fallback would be up to 511 ms late.