Skip to main content

auralis_task/
scheduler.rs

1//! Built-in [`ScheduleFlush`] implementations.
2//!
3//! Before this module existed, every application had to implement
4//! [`ScheduleFlush`] by hand (~15 lines of boilerplate).  Now you can
5//! pick [`DeferredScheduler`] for production (buffered, needs a drain
6//! loop) or use [`auralis_devtools::init`] for zero-setup diagnostics.
7
8use std::cell::RefCell;
9use std::rc::Rc;
10
11use crate::ScheduleFlush;
12
13/// A [`ScheduleFlush`] that buffers callbacks and drains them when
14/// [`drain`](Self::drain) is called.
15///
16/// This is the recommended scheduler for CLI / native applications.
17/// Call [`drain`](Self::drain) in your main loop to process pending
18/// signal notifications and timer expirations.
19///
20/// # Example
21///
22/// ```rust,ignore
23/// use auralis_task::scheduler::DeferredScheduler;
24/// use auralis_task::init_flush_scheduler;
25///
26/// let sched = DeferredScheduler::new();
27/// init_flush_scheduler(sched.clone());
28///
29/// // In your main loop:
30/// loop {
31///     // ... application logic ...
32///     sched.drain();
33/// }
34/// ```
35pub struct DeferredScheduler {
36    pending: RefCell<Vec<Box<dyn FnOnce()>>>,
37}
38
39impl DeferredScheduler {
40    /// Create a new deferred scheduler.
41    #[must_use]
42    pub fn new() -> Rc<Self> {
43        Rc::new(Self {
44            pending: RefCell::new(Vec::new()),
45        })
46    }
47
48    /// Drain all pending flush callbacks.
49    ///
50    /// Call this periodically (e.g. once per frame / loop iteration)
51    /// to process deferred signal notifications, timer expirations,
52    /// and task polls.
53    pub fn drain(&self) {
54        let cbs: Vec<Box<dyn FnOnce()>> = self.pending.borrow_mut().drain(..).collect();
55        for cb in cbs {
56            cb();
57        }
58    }
59
60    /// Return the number of pending callbacks.
61    #[must_use]
62    pub fn pending_count(&self) -> usize {
63        self.pending.borrow().len()
64    }
65}
66
67impl Default for DeferredScheduler {
68    fn default() -> Self {
69        Self {
70            pending: RefCell::new(Vec::new()),
71        }
72    }
73}
74
75impl ScheduleFlush for DeferredScheduler {
76    fn schedule(&self, callback: Box<dyn FnOnce()>) {
77        self.pending.borrow_mut().push(callback);
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use std::cell::Cell;
84    use std::rc::Rc;
85
86    use super::*;
87
88    #[test]
89    fn deferred_scheduler_buffers_and_drains() {
90        let sched = DeferredScheduler::new();
91        let calls = Rc::new(Cell::new(0u32));
92
93        let c1 = calls.clone();
94        sched.schedule(Box::new(move || c1.set(c1.get() + 1)));
95        let c2 = calls.clone();
96        sched.schedule(Box::new(move || c2.set(c2.get() + 2)));
97
98        assert_eq!(sched.pending_count(), 2);
99        assert_eq!(calls.get(), 0);
100
101        sched.drain();
102        assert_eq!(calls.get(), 3);
103        assert_eq!(sched.pending_count(), 0);
104    }
105
106    #[test]
107    fn drain_empty_is_noop() {
108        let sched = DeferredScheduler::new();
109        sched.drain(); // should not panic
110        assert_eq!(sched.pending_count(), 0);
111    }
112
113    #[test]
114    fn default_scheduler_has_no_pending() {
115        let sched = DeferredScheduler::default();
116        assert_eq!(sched.pending_count(), 0);
117    }
118
119    #[test]
120    fn reentrant_schedule_during_drain_deferred_to_next_drain() {
121        // Callbacks pushed during drain() must not execute in the same
122        // drain cycle — they should be held for the next drain().
123        let sched = DeferredScheduler::new();
124        let calls = Rc::new(Cell::new(0u32));
125
126        // Use a weak reference so the callback can schedule onto the
127        // same scheduler instance.
128        let sched_weak = Rc::downgrade(&sched);
129        let c1 = calls.clone();
130        sched.schedule(Box::new(move || {
131            c1.set(1);
132            // Re-entrant schedule on the same scheduler during drain.
133            if let Some(s) = sched_weak.upgrade() {
134                s.schedule(Box::new(|| {}));
135            }
136        }));
137
138        assert_eq!(sched.pending_count(), 1);
139        sched.drain();
140        assert_eq!(calls.get(), 1);
141        // The re-entrant callback was NOT processed in the current drain
142        // (drain(..) took a snapshot before iterating).
143        assert_eq!(
144            sched.pending_count(),
145            1,
146            "re-entrant callback should be pending"
147        );
148
149        sched.drain();
150        assert_eq!(sched.pending_count(), 0);
151    }
152}