Skip to main content

oxihuman_core/
scheduler.rs

1//! Task scheduler with priorities and time-based execution.
2
3#[allow(dead_code)]
4#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
5pub enum TaskPriority {
6    Low,
7    Normal,
8    High,
9    Critical,
10}
11
12#[allow(dead_code)]
13#[derive(Clone)]
14pub struct ScheduledTask {
15    pub id: u64,
16    pub name: String,
17    pub priority: TaskPriority,
18    /// Absolute scheduled time in seconds.
19    pub scheduled_time: f64,
20    /// None = one-shot, Some = repeating interval.
21    pub interval: Option<f64>,
22    pub enabled: bool,
23    pub run_count: u32,
24}
25
26#[allow(dead_code)]
27pub struct Scheduler {
28    pub tasks: Vec<ScheduledTask>,
29    pub current_time: f64,
30    pub next_id: u64,
31}
32
33// ---------------------------------------------------------------------------
34// Construction
35// ---------------------------------------------------------------------------
36
37#[allow(dead_code)]
38pub fn new_scheduler() -> Scheduler {
39    Scheduler {
40        tasks: Vec::new(),
41        current_time: 0.0,
42        next_id: 1,
43    }
44}
45
46// ---------------------------------------------------------------------------
47// Scheduling
48// ---------------------------------------------------------------------------
49
50#[allow(dead_code)]
51pub fn schedule_once(sched: &mut Scheduler, name: &str, delay: f64, priority: TaskPriority) -> u64 {
52    let id = sched.next_id;
53    sched.next_id += 1;
54    sched.tasks.push(ScheduledTask {
55        id,
56        name: name.to_string(),
57        priority,
58        scheduled_time: sched.current_time + delay,
59        interval: None,
60        enabled: true,
61        run_count: 0,
62    });
63    id
64}
65
66#[allow(dead_code)]
67pub fn schedule_repeating(
68    sched: &mut Scheduler,
69    name: &str,
70    interval: f64,
71    priority: TaskPriority,
72) -> u64 {
73    let id = sched.next_id;
74    sched.next_id += 1;
75    sched.tasks.push(ScheduledTask {
76        id,
77        name: name.to_string(),
78        priority,
79        scheduled_time: sched.current_time + interval,
80        interval: Some(interval),
81        enabled: true,
82        run_count: 0,
83    });
84    id
85}
86
87#[allow(dead_code)]
88pub fn cancel_task(sched: &mut Scheduler, id: u64) -> bool {
89    let before = sched.tasks.len();
90    sched.tasks.retain(|t| t.id != id);
91    sched.tasks.len() < before
92}
93
94// ---------------------------------------------------------------------------
95// Time advancement
96// ---------------------------------------------------------------------------
97
98/// Advance scheduler time by `dt`. Returns all tasks that fired during this step.
99#[allow(dead_code)]
100pub fn advance_time(sched: &mut Scheduler, dt: f64) -> Vec<ScheduledTask> {
101    sched.current_time += dt;
102    let current = sched.current_time;
103
104    let mut fired: Vec<ScheduledTask> = Vec::new();
105
106    for task in sched.tasks.iter_mut() {
107        if !task.enabled {
108            continue;
109        }
110        if task.scheduled_time <= current {
111            let snapshot = task.clone();
112            fired.push(snapshot);
113            task.run_count += 1;
114            if let Some(interval) = task.interval {
115                // Reschedule repeating task.
116                task.scheduled_time += interval;
117            }
118        }
119    }
120
121    // Sort fired by priority (highest first), then by scheduled_time.
122    fired.sort_by(|a, b| {
123        b.priority.cmp(&a.priority).then(
124            a.scheduled_time
125                .partial_cmp(&b.scheduled_time)
126                .unwrap_or(std::cmp::Ordering::Equal),
127        )
128    });
129
130    fired
131}
132
133// ---------------------------------------------------------------------------
134// Queries
135// ---------------------------------------------------------------------------
136
137#[allow(dead_code)]
138pub fn due_tasks(sched: &Scheduler) -> Vec<&ScheduledTask> {
139    sched
140        .tasks
141        .iter()
142        .filter(|t| t.enabled && t.scheduled_time <= sched.current_time)
143        .collect()
144}
145
146#[allow(dead_code)]
147pub fn task_count(sched: &Scheduler) -> usize {
148    sched.tasks.len()
149}
150
151#[allow(dead_code)]
152pub fn enabled_task_count(sched: &Scheduler) -> usize {
153    sched.tasks.iter().filter(|t| t.enabled).count()
154}
155
156#[allow(dead_code)]
157pub fn get_scheduled_task(sched: &Scheduler, id: u64) -> Option<&ScheduledTask> {
158    sched.tasks.iter().find(|t| t.id == id)
159}
160
161#[allow(dead_code)]
162pub fn set_task_enabled(sched: &mut Scheduler, id: u64, enabled: bool) {
163    if let Some(task) = sched.tasks.iter_mut().find(|t| t.id == id) {
164        task.enabled = enabled;
165    }
166}
167
168/// Returns tasks sorted by priority (highest first, then by scheduled_time).
169#[allow(dead_code)]
170pub fn tasks_by_priority(sched: &Scheduler) -> Vec<&ScheduledTask> {
171    let mut sorted: Vec<&ScheduledTask> = sched.tasks.iter().collect();
172    sorted.sort_by(|a, b| {
173        b.priority.cmp(&a.priority).then(
174            a.scheduled_time
175                .partial_cmp(&b.scheduled_time)
176                .unwrap_or(std::cmp::Ordering::Equal),
177        )
178    });
179    sorted
180}
181
182#[allow(dead_code)]
183pub fn next_due_time(sched: &Scheduler) -> Option<f64> {
184    sched
185        .tasks
186        .iter()
187        .filter(|t| t.enabled)
188        .map(|t| t.scheduled_time)
189        .reduce(f64::min)
190}
191
192#[allow(dead_code)]
193pub fn clear_completed_tasks(sched: &mut Scheduler) {
194    // Remove one-shot tasks that have been run at least once.
195    sched
196        .tasks
197        .retain(|t| !(t.interval.is_none() && t.run_count > 0));
198}
199
200// ---------------------------------------------------------------------------
201// Tests
202// ---------------------------------------------------------------------------
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_new_scheduler() {
210        let s = new_scheduler();
211        assert_eq!(task_count(&s), 0);
212        assert!((s.current_time).abs() < 1e-9);
213    }
214
215    #[test]
216    fn test_schedule_once() {
217        let mut s = new_scheduler();
218        let id = schedule_once(&mut s, "task_a", 1.0, TaskPriority::Normal);
219        assert_eq!(task_count(&s), 1);
220        assert!(get_scheduled_task(&s, id).is_some());
221    }
222
223    #[test]
224    fn test_schedule_repeating() {
225        let mut s = new_scheduler();
226        let id = schedule_repeating(&mut s, "rep", 0.5, TaskPriority::High);
227        let task = get_scheduled_task(&s, id).expect("should succeed");
228        assert!(task.interval.is_some());
229    }
230
231    #[test]
232    fn test_cancel_task() {
233        let mut s = new_scheduler();
234        let id = schedule_once(&mut s, "tmp", 1.0, TaskPriority::Low);
235        assert!(cancel_task(&mut s, id));
236        assert_eq!(task_count(&s), 0);
237        assert!(!cancel_task(&mut s, id));
238    }
239
240    #[test]
241    fn test_advance_time_fires_task() {
242        let mut s = new_scheduler();
243        schedule_once(&mut s, "go", 1.0, TaskPriority::Normal);
244        let fired = advance_time(&mut s, 1.5);
245        assert_eq!(fired.len(), 1);
246        assert_eq!(fired[0].name, "go");
247    }
248
249    #[test]
250    fn test_advance_time_no_fire_early() {
251        let mut s = new_scheduler();
252        schedule_once(&mut s, "future", 5.0, TaskPriority::Normal);
253        let fired = advance_time(&mut s, 1.0);
254        assert!(fired.is_empty());
255    }
256
257    #[test]
258    fn test_repeating_reschedules() {
259        let mut s = new_scheduler();
260        let id = schedule_repeating(&mut s, "rep", 1.0, TaskPriority::Normal);
261        let fired1 = advance_time(&mut s, 1.1);
262        assert_eq!(fired1.len(), 1);
263        // Task should still be in the scheduler (rescheduled).
264        assert!(get_scheduled_task(&s, id).is_some());
265        let task = get_scheduled_task(&s, id).expect("should succeed");
266        assert!(task.scheduled_time > s.current_time - 0.001);
267    }
268
269    #[test]
270    fn test_task_count() {
271        let mut s = new_scheduler();
272        schedule_once(&mut s, "a", 1.0, TaskPriority::Low);
273        schedule_once(&mut s, "b", 2.0, TaskPriority::High);
274        assert_eq!(task_count(&s), 2);
275    }
276
277    #[test]
278    fn test_due_tasks() {
279        let mut s = new_scheduler();
280        schedule_once(&mut s, "now", 0.0, TaskPriority::Normal);
281        schedule_once(&mut s, "later", 10.0, TaskPriority::Normal);
282        let due = due_tasks(&s);
283        assert_eq!(due.len(), 1);
284        assert_eq!(due[0].name, "now");
285    }
286
287    #[test]
288    fn test_tasks_by_priority_order() {
289        let mut s = new_scheduler();
290        schedule_once(&mut s, "low", 1.0, TaskPriority::Low);
291        schedule_once(&mut s, "crit", 1.0, TaskPriority::Critical);
292        schedule_once(&mut s, "norm", 1.0, TaskPriority::Normal);
293        let sorted = tasks_by_priority(&s);
294        assert_eq!(sorted[0].priority, TaskPriority::Critical);
295        assert_eq!(sorted[sorted.len() - 1].priority, TaskPriority::Low);
296    }
297
298    #[test]
299    fn test_next_due_time() {
300        let mut s = new_scheduler();
301        schedule_once(&mut s, "a", 3.0, TaskPriority::Normal);
302        schedule_once(&mut s, "b", 1.0, TaskPriority::Normal);
303        let next = next_due_time(&s).expect("should succeed");
304        assert!((next - 1.0).abs() < 1e-9);
305    }
306
307    #[test]
308    fn test_next_due_time_empty() {
309        let s = new_scheduler();
310        assert!(next_due_time(&s).is_none());
311    }
312
313    #[test]
314    fn test_clear_completed_tasks() {
315        let mut s = new_scheduler();
316        schedule_once(&mut s, "one_shot", 0.5, TaskPriority::Normal);
317        schedule_repeating(&mut s, "rep", 1.0, TaskPriority::Normal);
318        advance_time(&mut s, 1.5);
319        clear_completed_tasks(&mut s);
320        // One-shot should be removed; repeating should remain.
321        assert_eq!(task_count(&s), 1);
322        assert_eq!(s.tasks[0].name, "rep");
323    }
324
325    #[test]
326    fn test_enabled_task_count() {
327        let mut s = new_scheduler();
328        let id1 = schedule_once(&mut s, "a", 1.0, TaskPriority::Normal);
329        schedule_once(&mut s, "b", 1.0, TaskPriority::Normal);
330        set_task_enabled(&mut s, id1, false);
331        assert_eq!(enabled_task_count(&s), 1);
332    }
333
334    #[test]
335    fn test_set_task_enabled() {
336        let mut s = new_scheduler();
337        let id = schedule_once(&mut s, "x", 0.0, TaskPriority::Normal);
338        set_task_enabled(&mut s, id, false);
339        let fired = advance_time(&mut s, 1.0);
340        // Disabled task should not fire.
341        assert!(fired.is_empty());
342    }
343}