Skip to main content

reovim_kernel/sched/
timer.rs

1//! Timer mechanism for delayed and periodic work scheduling.
2//!
3//! Linux equivalent: `kernel/time/timer.c`
4//!
5//! This module provides timer primitives for scheduling work to execute
6//! after a delay or at regular intervals. Timers integrate with the
7//! [`Runtime`](super::Runtime) tick cycle and execute callbacks via the
8//! work queue for panic safety.
9//!
10//! # Architecture
11//!
12//! ```text
13//! ┌─────────────────────────────────────────────────────────────┐
14//! │                       TimerWheel                             │
15//! │  ┌──────────────────────────────────────────────────────┐   │
16//! │  │  HashMap<TimerId, TimerEntry>                        │   │
17//! │  │    - deadline: Instant                               │   │
18//! │  │    - work: TimerWork (OneShot or Periodic)           │   │
19//! │  └──────────────────────────────────────────────────────┘   │
20//! │                           │                                  │
21//! │                           │ tick(now)                        │
22//! │                           v                                  │
23//! │  ┌──────────────────────────────────────────────────────┐   │
24//! │  │  WorkQueue <- expired timers scheduled as Tasks      │   │
25//! │  └──────────────────────────────────────────────────────┘   │
26//! └─────────────────────────────────────────────────────────────┘
27//! ```
28//!
29//! # Example
30//!
31//! ```
32//! use reovim_kernel::api::v1::*;
33//! use std::time::Duration;
34//! use std::sync::atomic::{AtomicUsize, Ordering};
35//! use std::sync::Arc;
36//!
37//! let mut runtime = Runtime::new();
38//! runtime.boot();
39//!
40//! // Schedule a one-shot timer
41//! let counter = Arc::new(AtomicUsize::new(0));
42//! let counter_clone = counter.clone();
43//! let _handle = runtime.schedule_delayed(Duration::from_millis(10), move || {
44//!     counter_clone.fetch_add(1, Ordering::SeqCst);
45//! });
46//!
47//! // Timer fires when tick advances past deadline
48//! ```
49//!
50//! # Panic Safety
51//!
52//! Timer callbacks are executed via the work queue, which uses the executor's
53//! `catch_unwind` for panic safety. A panicking callback is marked as failed
54//! and does not repeat (for periodic timers).
55
56use std::{
57    collections::HashMap,
58    fmt,
59    sync::{
60        Arc, Weak,
61        atomic::{AtomicBool, AtomicU64, Ordering},
62    },
63    time::{Duration, Instant},
64};
65
66use crate::arch::sync::Mutex;
67
68use super::task::{Priority, Task};
69
70/// Unique timer identifier.
71///
72/// Timer IDs are monotonically increasing and never reused within a session.
73/// This follows the same pattern as [`TaskId`](super::TaskId) and
74/// [`BufferId`](crate::mm::BufferId).
75///
76/// # Example
77///
78/// ```
79/// use reovim_kernel::api::v1::*;
80///
81/// let id1 = TimerId::new();
82/// let id2 = TimerId::new();
83/// assert_ne!(id1, id2);
84/// assert!(id2.as_u64() > id1.as_u64());
85/// ```
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
87pub struct TimerId(u64);
88
89impl TimerId {
90    /// Create a new unique timer ID.
91    ///
92    /// IDs start at 1 and increment monotonically.
93    #[must_use]
94    pub fn new() -> Self {
95        static COUNTER: AtomicU64 = AtomicU64::new(1);
96        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
97    }
98
99    /// Get the raw numeric value.
100    #[inline]
101    #[must_use]
102    pub const fn as_u64(self) -> u64 {
103        self.0
104    }
105
106    /// Create from raw value.
107    ///
108    /// Primarily for testing and deserialization.
109    #[inline]
110    #[must_use]
111    pub const fn from_raw(value: u64) -> Self {
112        Self(value)
113    }
114}
115
116impl Default for TimerId {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl fmt::Display for TimerId {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(f, "Timer({})", self.0)
125    }
126}
127
128/// Timer configuration.
129///
130/// Specifies when a timer should fire and at what priority the callback
131/// should execute. This struct is provided for advanced use cases where
132/// you need more control than `Runtime::schedule_delayed()` provides.
133///
134/// # Example
135///
136/// ```
137/// use reovim_kernel::api::v1::*;
138/// use std::time::Duration;
139///
140/// // One-shot timer after 100ms
141/// let config = TimerConfig {
142///     delay: Duration::from_millis(100),
143///     interval: None,
144///     priority: Priority::NORMAL,
145/// };
146///
147/// // Periodic timer every 50ms
148/// let periodic = TimerConfig {
149///     delay: Duration::from_millis(50),
150///     interval: Some(Duration::from_millis(50)),
151///     priority: Priority::LOW,
152/// };
153/// ```
154#[derive(Debug, Clone)]
155pub struct TimerConfig {
156    /// Initial delay before first execution.
157    pub delay: Duration,
158
159    /// Optional interval for periodic timers.
160    ///
161    /// If `Some`, the timer will reschedule after each execution.
162    /// If `None`, the timer is one-shot and removed after firing.
163    pub interval: Option<Duration>,
164
165    /// Priority for the scheduled task.
166    ///
167    /// Determines execution order relative to other work in the queue.
168    pub priority: Priority,
169}
170
171impl Default for TimerConfig {
172    fn default() -> Self {
173        Self {
174            delay: Duration::ZERO,
175            interval: None,
176            priority: Priority::NORMAL,
177        }
178    }
179}
180
181/// Type-erased one-shot timer callback.
182pub(super) type BoxedOneShotCallback = Box<dyn FnOnce() + Send + 'static>;
183
184/// Type-erased periodic timer callback.
185///
186/// Uses `Arc` instead of `Box` because periodic callbacks need to be cloned
187/// for each invocation.
188pub(super) type BoxedPeriodicCallback = Arc<dyn Fn() + Send + Sync + 'static>;
189
190/// Timer callback storage.
191///
192/// Distinguishes between one-shot (`FnOnce`) and periodic (`Fn`) callbacks.
193pub(super) enum TimerWork {
194    /// One-shot timer: callback is consumed on execution.
195    OneShot(Option<BoxedOneShotCallback>),
196
197    /// Periodic timer: callback can be called multiple times.
198    Periodic(BoxedPeriodicCallback),
199}
200
201impl fmt::Debug for TimerWork {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        match self {
204            Self::OneShot(_) => write!(f, "OneShot(...)"),
205            Self::Periodic(_) => write!(f, "Periodic(...)"),
206        }
207    }
208}
209
210/// Internal timer entry in the wheel.
211pub(super) struct TimerEntry {
212    /// Unique identifier.
213    pub(super) id: TimerId,
214
215    /// When the timer should fire.
216    pub(super) deadline: Instant,
217
218    /// Interval for periodic rescheduling.
219    pub(super) interval: Option<Duration>,
220
221    /// Priority for the work task.
222    pub(super) priority: Priority,
223
224    /// The callback to execute.
225    pub(super) work: TimerWork,
226
227    /// Whether this timer has been cancelled.
228    pub(super) cancelled: AtomicBool,
229}
230
231impl TimerEntry {
232    /// Check if this timer has been cancelled.
233    #[inline]
234    pub(super) fn is_cancelled(&self) -> bool {
235        self.cancelled.load(Ordering::Acquire)
236    }
237
238    /// Mark this timer as cancelled.
239    #[inline]
240    pub(super) fn cancel(&self) {
241        self.cancelled.store(true, Ordering::Release);
242    }
243}
244
245impl fmt::Debug for TimerEntry {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        f.debug_struct("TimerEntry")
248            .field("id", &self.id)
249            .field("deadline", &self.deadline)
250            .field("interval", &self.interval)
251            .field("priority", &self.priority)
252            .field("work", &self.work)
253            .field("cancelled", &self.is_cancelled())
254            .finish()
255    }
256}
257
258/// Default maximum number of concurrent timers.
259pub const DEFAULT_MAX_TIMERS: usize = 1024;
260
261/// Timer wheel for managing scheduled timers.
262///
263/// The timer wheel stores active timers indexed by ID and checks for
264/// expired timers during each tick. Expired timers have their callbacks
265/// scheduled as tasks on the work queue.
266///
267/// # Thread Safety
268///
269/// The wheel uses a `Mutex<HashMap>` internally, making it safe to
270/// schedule and cancel from multiple threads.
271pub struct TimerWheel {
272    /// Active timers indexed by ID.
273    timers: Mutex<HashMap<TimerId, TimerEntry>>,
274
275    /// Next timer ID (atomic for thread-safe allocation).
276    next_id: AtomicU64,
277
278    /// Maximum number of timers allowed.
279    max_timers: usize,
280
281    /// Count of timers dropped due to capacity limit.
282    dropped: AtomicU64,
283}
284
285impl TimerWheel {
286    /// Create a new timer wheel with default capacity.
287    #[must_use]
288    pub fn new() -> Self {
289        Self::with_max_timers(DEFAULT_MAX_TIMERS)
290    }
291
292    /// Create a timer wheel with specified maximum timer count.
293    #[must_use]
294    pub fn with_max_timers(max_timers: usize) -> Self {
295        Self {
296            timers: Mutex::new(HashMap::new()),
297            next_id: AtomicU64::new(1),
298            max_timers,
299            dropped: AtomicU64::new(0),
300        }
301    }
302
303    /// Schedule a one-shot timer.
304    ///
305    /// Returns a handle that cancels the timer when dropped.
306    pub fn schedule_oneshot<F>(
307        self: &Arc<Self>,
308        delay: Duration,
309        priority: Priority,
310        work: F,
311    ) -> TimerHandle
312    where
313        F: FnOnce() + Send + 'static,
314    {
315        self.schedule_internal(delay, None, priority, TimerWork::OneShot(Some(Box::new(work))))
316    }
317
318    /// Schedule a periodic timer.
319    ///
320    /// Returns a handle that cancels the timer when dropped.
321    pub fn schedule_periodic<F>(
322        self: &Arc<Self>,
323        interval: Duration,
324        priority: Priority,
325        work: F,
326    ) -> TimerHandle
327    where
328        F: Fn() + Send + Sync + 'static,
329    {
330        self.schedule_internal(
331            interval,
332            Some(interval),
333            priority,
334            TimerWork::Periodic(Arc::new(work)),
335        )
336    }
337
338    /// Internal scheduling implementation.
339    fn schedule_internal(
340        self: &Arc<Self>,
341        delay: Duration,
342        interval: Option<Duration>,
343        priority: Priority,
344        work: TimerWork,
345    ) -> TimerHandle {
346        let id = TimerId(self.next_id.fetch_add(1, Ordering::Relaxed));
347        let deadline = Instant::now() + delay;
348
349        let mut timers = self.timers.lock();
350
351        // Check capacity limit
352        if timers.len() >= self.max_timers {
353            self.dropped.fetch_add(1, Ordering::Relaxed);
354            // Timer limit exceeded - return a failed handle
355            // Caller can check handle.is_failed() to detect this
356            return TimerHandle::failed(id);
357        }
358
359        let entry = TimerEntry {
360            id,
361            deadline,
362            interval,
363            priority,
364            work,
365            cancelled: AtomicBool::new(false),
366        };
367
368        timers.insert(id, entry);
369        drop(timers);
370
371        TimerHandle::new(id, Arc::downgrade(self))
372    }
373
374    /// Cancel a timer by ID.
375    ///
376    /// Returns `true` if the timer was found and cancelled, `false` if
377    /// it was already cancelled or has fired.
378    pub fn cancel(&self, id: TimerId) -> bool {
379        let timers = self.timers.lock();
380        timers.get(&id).is_some_and(|entry| {
381            if entry.is_cancelled() {
382                false
383            } else {
384                entry.cancel();
385                true
386            }
387        })
388    }
389
390    /// Check if a timer is still pending (scheduled but not yet fired).
391    ///
392    /// Returns `true` if the timer exists and hasn't been cancelled or fired.
393    pub fn is_pending(&self, id: TimerId) -> bool {
394        let timers = self.timers.lock();
395        timers.get(&id).is_some_and(|entry| !entry.is_cancelled())
396    }
397
398    /// Process expired timers and return tasks to execute.
399    ///
400    /// This should be called during each tick with the current time.
401    /// Returns a vector of tasks for expired timers.
402    #[cfg_attr(coverage_nightly, coverage(off))]
403    pub fn tick(&self, now: Instant) -> Vec<Task> {
404        let mut tasks = Vec::new();
405        let mut to_remove = Vec::new();
406        let mut to_reschedule = Vec::new();
407
408        {
409            let mut timers = self.timers.lock();
410
411            for (id, entry) in timers.iter_mut() {
412                // Skip cancelled timers
413                if entry.is_cancelled() {
414                    to_remove.push(*id);
415                    continue;
416                }
417
418                // Check if timer has expired
419                if entry.deadline <= now {
420                    match &mut entry.work {
421                        TimerWork::OneShot(work_opt) => {
422                            // Take the FnOnce callback
423                            if let Some(work) = work_opt.take() {
424                                let task = Task::with_priority(entry.priority, work);
425                                tasks.push(task);
426                            }
427                            to_remove.push(*id);
428                        }
429                        TimerWork::Periodic(work) => {
430                            // Clone the Arc callback for execution
431                            let work_clone = Arc::clone(work);
432                            let task = Task::with_priority(entry.priority, move || work_clone());
433                            tasks.push(task);
434
435                            // Reschedule for next interval
436                            if let Some(interval) = entry.interval {
437                                to_reschedule.push((*id, now + interval));
438                            }
439                        }
440                    }
441                }
442            }
443
444            // Remove completed/cancelled timers
445            for id in to_remove {
446                timers.remove(&id);
447            }
448
449            // Update deadlines for periodic timers
450            for (id, new_deadline) in to_reschedule {
451                if let Some(entry) = timers.get_mut(&id) {
452                    entry.deadline = new_deadline;
453                }
454            }
455        }
456
457        tasks
458    }
459
460    /// Get the number of pending timers.
461    #[must_use]
462    pub fn pending_count(&self) -> usize {
463        self.timers.lock().len()
464    }
465
466    /// Get the number of timers dropped due to capacity limits.
467    #[must_use]
468    pub fn dropped_count(&self) -> u64 {
469        self.dropped.load(Ordering::Relaxed)
470    }
471}
472
473impl Default for TimerWheel {
474    fn default() -> Self {
475        Self::new()
476    }
477}
478
479impl fmt::Debug for TimerWheel {
480    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481        f.debug_struct("TimerWheel")
482            .field("pending", &self.pending_count())
483            .field("max_timers", &self.max_timers)
484            .field("dropped", &self.dropped_count())
485            .finish_non_exhaustive()
486    }
487}
488
489/// Handle to a scheduled timer with RAII cancellation.
490///
491/// When dropped, the handle automatically cancels the timer if it hasn't
492/// fired yet. This follows the RAII pattern for resource management.
493///
494/// # Example
495///
496/// ```
497/// use reovim_kernel::api::v1::*;
498/// use std::time::Duration;
499///
500/// let mut runtime = Runtime::new();
501/// runtime.boot();
502///
503/// {
504///     // Timer is scheduled
505///     let handle = runtime.schedule_delayed(Duration::from_secs(10), || {
506///         println!("This won't print");
507///     });
508///     // handle dropped here - timer is cancelled
509/// }
510///
511/// // Timer was cancelled, won't fire
512/// ```
513///
514/// # Manual Cancellation
515///
516/// You can also explicitly cancel via [`Runtime::cancel_timer`](super::Runtime::cancel_timer):
517///
518/// ```
519/// use reovim_kernel::api::v1::*;
520/// use std::time::Duration;
521///
522/// let mut runtime = Runtime::new();
523/// runtime.boot();
524///
525/// let handle = runtime.schedule_delayed(Duration::from_secs(10), || {});
526/// let id = handle.id();
527///
528/// // Explicit cancellation
529/// std::mem::forget(handle); // Prevent drop cancellation
530/// let was_pending = runtime.cancel_timer(id);
531/// ```
532pub struct TimerHandle {
533    /// The timer's unique identifier.
534    id: TimerId,
535
536    /// Weak reference to the timer wheel for cancellation.
537    ///
538    /// If the wheel has been dropped (runtime shutdown), cancellation
539    /// is a no-op since all timers are already gone.
540    wheel: Option<Weak<TimerWheel>>,
541
542    /// Whether this handle represents a failed scheduling attempt.
543    ///
544    /// When `max_timers` is exceeded, we return a "failed" handle that
545    /// doesn't actually have a timer to cancel.
546    failed: bool,
547}
548
549impl TimerHandle {
550    /// Create a new timer handle.
551    pub(crate) const fn new(id: TimerId, wheel: Weak<TimerWheel>) -> Self {
552        Self {
553            id,
554            wheel: Some(wheel),
555            failed: false,
556        }
557    }
558
559    /// Create a failed handle (timer wasn't actually scheduled).
560    pub(crate) const fn failed(id: TimerId) -> Self {
561        Self {
562            id,
563            wheel: None,
564            failed: true,
565        }
566    }
567
568    /// Get the timer's unique identifier.
569    #[inline]
570    #[must_use]
571    pub const fn id(&self) -> TimerId {
572        self.id
573    }
574
575    /// Check if this handle represents a failed scheduling attempt.
576    ///
577    /// Returns `true` if the timer was not actually scheduled (e.g., due
578    /// to hitting the `max_timers` limit).
579    #[inline]
580    #[must_use]
581    pub const fn is_failed(&self) -> bool {
582        self.failed
583    }
584
585    /// Prevent automatic cancellation on drop.
586    ///
587    /// After calling this, dropping the handle will NOT cancel the timer.
588    /// Use this when you want the timer to fire regardless of handle lifetime.
589    ///
590    /// Returns the timer ID for manual cancellation if needed later.
591    #[must_use]
592    pub fn detach(mut self) -> TimerId {
593        let id = self.id;
594        self.wheel = None; // Prevent Drop from cancelling
595        id
596    }
597}
598
599impl Drop for TimerHandle {
600    fn drop(&mut self) {
601        // If we have a wheel reference and it's still alive, cancel the timer
602        if let Some(ref weak) = self.wheel
603            && let Some(wheel) = weak.upgrade()
604        {
605            wheel.cancel(self.id);
606        }
607    }
608}
609
610impl fmt::Debug for TimerHandle {
611    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
612        f.debug_struct("TimerHandle")
613            .field("id", &self.id)
614            .field("failed", &self.failed)
615            .field("wheel_alive", &self.wheel.as_ref().is_some_and(|w| w.strong_count() > 0))
616            .finish()
617    }
618}