Skip to main content

sz_orm_scheduler/
lib.rs

1//! # SZ-ORM Scheduler — Cron Task Scheduler
2//!
3//! Provides cron expression-based scheduled task execution, supports task start/stop, state management, and callback execution.
4//!
5//! ## Main Modules
6//!
7//! - [`scheduler`] — Task handler trait and test auxiliary implementation
8
9use chrono::{Datelike, Timelike};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::{Arc, RwLock};
14use std::thread::JoinHandle;
15use std::time::Duration;
16
17pub mod advanced;
18pub mod scheduler;
19
20pub use scheduler::{CounterJobHandler, JobHandler, RecordingJobHandler};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ScheduledTask {
24    pub id: String,
25    pub name: String,
26    pub cron_expr: String,
27    pub callback: String,
28    pub metadata: HashMap<String, serde_json::Value>,
29    pub enabled: bool,
30    #[serde(default)]
31    pub priority: i32,
32}
33
34impl ScheduledTask {
35    pub fn new(
36        id: impl Into<String>,
37        name: impl Into<String>,
38        cron_expr: impl Into<String>,
39    ) -> Self {
40        Self {
41            id: id.into(),
42            name: name.into(),
43            cron_expr: cron_expr.into(),
44            callback: String::new(),
45            metadata: HashMap::new(),
46            enabled: true,
47            priority: 0,
48        }
49    }
50
51    pub fn with_callback(mut self, callback: impl Into<String>) -> Self {
52        self.callback = callback.into();
53        self
54    }
55
56    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
57        self.metadata.insert(key.into(), value);
58        self
59    }
60
61    pub fn with_priority(mut self, priority: i32) -> Self {
62        self.priority = priority;
63        self
64    }
65
66    pub fn disable(mut self) -> Self {
67        self.enabled = false;
68        self
69    }
70}
71
72pub trait Scheduler: Send + Sync {
73    fn schedule(&self, task: ScheduledTask) -> Result<(), SchedulerError>;
74    fn cancel(&self, task_id: &str) -> Result<(), SchedulerError>;
75    fn pause(&self, task_id: &str) -> Result<(), SchedulerError>;
76    fn resume(&self, task_id: &str) -> Result<(), SchedulerError>;
77    fn list_tasks(&self) -> Vec<ScheduledTask>;
78}
79
80pub struct CronScheduler {
81    tasks: Arc<RwLock<HashMap<String, ScheduledTask>>>,
82    handlers: Arc<RwLock<HashMap<String, Arc<dyn JobHandler>>>>,
83    stop_flag: Arc<AtomicBool>,
84    worker: RwLock<Option<JoinHandle<()>>>,
85}
86
87impl CronScheduler {
88    pub fn new() -> Self {
89        Self {
90            tasks: Arc::new(RwLock::new(HashMap::new())),
91            handlers: Arc::new(RwLock::new(HashMap::new())),
92            stop_flag: Arc::new(AtomicBool::new(false)),
93            worker: RwLock::new(None),
94        }
95    }
96
97    pub fn parse_cron(&self, expr: &str) -> Result<CronExpr, SchedulerError> {
98        let parts: Vec<&str> = expr.split_whitespace().collect();
99        if parts.len() != 5 {
100            return Err(SchedulerError::InvalidCronExpr(format!(
101                "Expected 5 fields, got {}",
102                parts.len()
103            )));
104        }
105
106        Ok(CronExpr {
107            second: parts[0].to_string(),
108            minute: parts[1].to_string(),
109            hour: parts[2].to_string(),
110            day_of_month: parts[3].to_string(),
111            month: parts[4].to_string(),
112        })
113    }
114
115    pub fn next_run_time(
116        &self,
117        expr: &str,
118        from: chrono::DateTime<chrono::Utc>,
119    ) -> Result<chrono::DateTime<chrono::Utc>, SchedulerError> {
120        let parsed = self.parse_cron(expr)?;
121
122        // 判断 second 字段是否需要精确扫描(非 "*" 且非 "0")
123        // second="*" 或 "0" 时,对齐到分钟边界(second=0)后按分钟扫描即可
124        // second 为其他值(如 "30"、"10-12"、"10,20,30")时,需在匹配分钟内找具体 second
125        let needs_second_precision = !matches!(parsed.second.as_str(), "*" | "0");
126
127        if !needs_second_precision {
128            // second 字段是 "*" 或 "0":保留原逻辑,按分钟扫描
129            // 对齐到下一分钟边界(second=0),扫描 525600 分钟(365 天)
130            let mut next = align_to_next_minute_boundary(from);
131            for _ in 0..525_600 {
132                if self.matches_cron(&parsed, next) {
133                    return Ok(next);
134                }
135                next += chrono::Duration::minutes(1);
136            }
137        } else {
138            // second 字段包含非 0 值:按分钟扫描,在匹配分钟内找具体 second
139            let seconds = self.parse_field_values(&parsed.second, 0, 59)?;
140            // 从 from 截断到当前分钟开始(保留当前分钟内未来 second 的可能性)
141            let mut minute_start = from
142                .with_second(0)
143                .and_then(|d| d.with_nanosecond(0))
144                .unwrap_or(from);
145
146            for _ in 0..525_600 {
147                // 检查 minute/hour/day/month 是否匹配(不检查 second)
148                if self.matches_cron_ignoring_second(&parsed, minute_start) {
149                    // 在该分钟内找第一个 > from 的 second
150                    for &sec in &seconds {
151                        let candidate = minute_start
152                            .with_second(sec)
153                            .and_then(|d| d.with_nanosecond(0))
154                            .unwrap_or(minute_start);
155                        if candidate > from {
156                            return Ok(candidate);
157                        }
158                    }
159                }
160                minute_start += chrono::Duration::minutes(1);
161            }
162        }
163
164        Err(SchedulerError::NoNextRunTime(
165            "No next run time found within 365 days".to_string(),
166        ))
167    }
168
169    fn matches_cron(&self, expr: &CronExpr, dt: chrono::DateTime<chrono::Utc>) -> bool {
170        self.field_matches(&expr.second, dt.naive_utc().second())
171            && self.field_matches(&expr.minute, dt.naive_utc().minute())
172            && self.field_matches(&expr.hour, dt.naive_utc().hour())
173            && self.field_matches(&expr.day_of_month, dt.naive_utc().day())
174            && self.field_matches(&expr.month, dt.naive_utc().month())
175    }
176
177    /// Check if minute/hour/day/month matches (does not check second)
178    /// Used for second-level precise scanning, first filter out minutes where minute/hour/day/month match
179    fn matches_cron_ignoring_second(
180        &self,
181        expr: &CronExpr,
182        dt: chrono::DateTime<chrono::Utc>,
183    ) -> bool {
184        self.field_matches(&expr.minute, dt.naive_utc().minute())
185            && self.field_matches(&expr.hour, dt.naive_utc().hour())
186            && self.field_matches(&expr.day_of_month, dt.naive_utc().day())
187            && self.field_matches(&expr.month, dt.naive_utc().month())
188    }
189
190    /// Parse cron field into ordered numeric list
191    /// Supports: * / single value / comma list / range / step
192    fn parse_field_values(
193        &self,
194        field: &str,
195        min: u32,
196        max: u32,
197    ) -> Result<Vec<u32>, SchedulerError> {
198        let mut values = Vec::new();
199        if field == "*" {
200            for v in min..=max {
201                values.push(v);
202            }
203            return Ok(values);
204        }
205        for part in field.split(',') {
206            let part = part.trim();
207            if part.contains('/') {
208                let parts: Vec<&str> = part.split('/').collect();
209                if parts.len() != 2 {
210                    return Err(SchedulerError::InvalidCronExpr(format!(
211                        "Invalid step field: {}",
212                        field
213                    )));
214                }
215                let step: u32 = parts[1].parse().map_err(|_| {
216                    SchedulerError::InvalidCronExpr(format!("Invalid step value: {}", parts[1]))
217                })?;
218                if step == 0 {
219                    return Err(SchedulerError::InvalidCronExpr(
220                        "Step value cannot be 0".to_string(),
221                    ));
222                }
223                let range_part = parts[0];
224                let (start, end) = if range_part == "*" {
225                    (min, max)
226                } else if range_part.contains('-') {
227                    let range_parts: Vec<&str> = range_part.split('-').collect();
228                    if range_parts.len() != 2 {
229                        return Err(SchedulerError::InvalidCronExpr(format!(
230                            "Invalid range: {}",
231                            range_part
232                        )));
233                    }
234                    let s: u32 = range_parts[0].trim().parse().map_err(|_| {
235                        SchedulerError::InvalidCronExpr(format!(
236                            "Invalid range start: {}",
237                            range_parts[0]
238                        ))
239                    })?;
240                    let e: u32 = range_parts[1].trim().parse().map_err(|_| {
241                        SchedulerError::InvalidCronExpr(format!(
242                            "Invalid range end: {}",
243                            range_parts[1]
244                        ))
245                    })?;
246                    (s, e)
247                } else {
248                    let s: u32 = range_part.parse().map_err(|_| {
249                        SchedulerError::InvalidCronExpr(format!("Invalid value: {}", range_part))
250                    })?;
251                    (s, max)
252                };
253                let mut v = start;
254                while v <= end {
255                    values.push(v);
256                    v = v.saturating_add(step);
257                }
258            } else if part.contains('-') {
259                let parts: Vec<&str> = part.split('-').collect();
260                if parts.len() != 2 {
261                    return Err(SchedulerError::InvalidCronExpr(format!(
262                        "Invalid range: {}",
263                        part
264                    )));
265                }
266                let start: u32 = parts[0].trim().parse().map_err(|_| {
267                    SchedulerError::InvalidCronExpr(format!("Invalid range start: {}", parts[0]))
268                })?;
269                let end: u32 = parts[1].trim().parse().map_err(|_| {
270                    SchedulerError::InvalidCronExpr(format!("Invalid range end: {}", parts[1]))
271                })?;
272                for v in start..=end {
273                    values.push(v);
274                }
275            } else {
276                let v: u32 = part.parse().map_err(|_| {
277                    SchedulerError::InvalidCronExpr(format!("Invalid value: {}", part))
278                })?;
279                values.push(v);
280            }
281        }
282        Ok(values)
283    }
284
285    fn field_matches(&self, field: &str, value: u32) -> bool {
286        if field == "*" {
287            return true;
288        }
289        if field.contains(',') {
290            return field
291                .split(',')
292                .any(|v| v.trim().parse::<u32>().is_ok_and(|n| n == value));
293        }
294        if field.contains('-') {
295            let parts: Vec<&str> = field.split('-').collect();
296            if parts.len() == 2 {
297                let start: u32 = parts[0].trim().parse().unwrap_or(0);
298                let end: u32 = parts[1].trim().parse().unwrap_or(0);
299                return value >= start && value <= end;
300            }
301        }
302        if field.contains('/') {
303            let parts: Vec<&str> = field.split('/').collect();
304            if parts.len() == 2 {
305                let step: u32 = parts[1].parse().unwrap_or(1);
306                return value.is_multiple_of(step);
307            }
308        }
309        field.parse::<u32>().is_ok_and(|n| n == value)
310    }
311
312    /// Registers a [`JobHandler`] for the given task id. When the scheduler
313    /// fires a matching task, it looks up the handler by task id. If no
314    /// handler is registered, the task is skipped silently.
315    pub fn register_handler(&self, task_id: impl Into<String>, handler: Arc<dyn JobHandler>) {
316        let mut handlers = self
317            .handlers
318            .write()
319            .map_err(|e| SchedulerError::Internal(e.to_string()))
320            .unwrap();
321        handlers.insert(task_id.into(), handler);
322    }
323
324    /// Fires every enabled task whose cron expression matches `now`. Returns
325    /// the number of tasks that fired (and whose handler, if any, returned
326    /// `Ok(())`). Errors from individual handlers are recorded but do not
327    /// abort iteration.
328    pub fn try_fire_due(&self, now: chrono::DateTime<chrono::Utc>) -> usize {
329        let due: Vec<(ScheduledTask, Option<Arc<dyn JobHandler>>)> = {
330            let tasks = self
331                .tasks
332                .read()
333                .map_err(|e| SchedulerError::Internal(e.to_string()));
334            let handlers = self
335                .handlers
336                .read()
337                .map_err(|e| SchedulerError::Internal(e.to_string()));
338            let (Ok(tasks), Ok(handlers)) = (tasks, handlers) else {
339                return 0;
340            };
341
342            tasks
343                .values()
344                .filter(|t| t.enabled)
345                .filter_map(|t| {
346                    let parsed = self.parse_cron(&t.cron_expr).ok()?;
347                    if self.matches_cron(&parsed, now) {
348                        Some((t.clone(), handlers.get(&t.id).cloned()))
349                    } else {
350                        None
351                    }
352                })
353                .collect()
354        };
355
356        let mut due = due;
357        due.sort_by_key(|a| std::cmp::Reverse(a.0.priority));
358
359        let mut fired = 0usize;
360        for (task, handler) in due {
361            if let Some(handler) = handler {
362                if handler.handle(&task).is_ok() {
363                    fired += 1;
364                }
365            } else {
366                // No handler registered: still count as "fired" so tests can
367                // observe cron matching independently of handler logic.
368                fired += 1;
369            }
370        }
371        fired
372    }
373
374    /// Starts a background worker thread that wakes up every `tick_ms`
375    /// milliseconds, queries the current UTC time, and invokes
376    /// [`try_fire_due`]. Calling `start` while a worker is already running
377    /// returns an error.
378    ///
379    /// [`try_fire_due`]: CronScheduler::try_fire_due
380    pub fn start(&self, tick_ms: u64) -> Result<(), SchedulerError> {
381        let mut worker = self
382            .worker
383            .write()
384            .map_err(|e| SchedulerError::Internal(e.to_string()))?;
385        if worker.is_some() {
386            return Err(SchedulerError::Internal(
387                "scheduler already running".to_string(),
388            ));
389        }
390
391        self.stop_flag.store(false, Ordering::SeqCst);
392        let stop_flag = self.stop_flag.clone();
393        let tasks = self.tasks.clone();
394        let handlers = self.handlers.clone();
395
396        let handle = std::thread::spawn(move || {
397            while !stop_flag.load(Ordering::SeqCst) {
398                std::thread::sleep(Duration::from_millis(tick_ms.max(1)));
399                if stop_flag.load(Ordering::SeqCst) {
400                    break;
401                }
402                let now = chrono::Utc::now();
403                let scheduler = CronScheduler {
404                    tasks: tasks.clone(),
405                    handlers: handlers.clone(),
406                    stop_flag: stop_flag.clone(),
407                    worker: RwLock::new(None),
408                };
409                let _ = scheduler.try_fire_due(now);
410            }
411        });
412
413        *worker = Some(handle);
414        Ok(())
415    }
416
417    /// Stops the background worker thread and waits for it to exit. If no
418    /// worker is running, this is a no-op.
419    pub fn stop(&self) -> Result<(), SchedulerError> {
420        let mut worker = self
421            .worker
422            .write()
423            .map_err(|e| SchedulerError::Internal(e.to_string()))?;
424        if let Some(handle) = worker.take() {
425            self.stop_flag.store(true, Ordering::SeqCst);
426            // Drop the lock before joining to avoid a deadlock if the worker
427            // ever needs to read `worker` (it doesn't today, but this keeps
428            // the invariant explicit).
429            drop(worker);
430            handle
431                .join()
432                .map_err(|_| SchedulerError::Internal("worker thread panicked".to_string()))?;
433        }
434        Ok(())
435    }
436
437    /// Returns `true` if a background worker is currently running.
438    pub fn is_running(&self) -> bool {
439        let worker = self
440            .worker
441            .read()
442            .map_err(|e| SchedulerError::Internal(e.to_string()));
443        match worker {
444            Ok(w) => w.is_some(),
445            Err(_) => false,
446        }
447    }
448
449    /// Trigger all due tasks and record execution results to `tracker`.
450    ///
451    /// Difference from `try_fire_due`: this method records execution status after each task trigger
452    /// (Succeeded / Failed / Skipped), for subsequent task health queries.
453    pub fn try_fire_due_tracked(
454        &self,
455        now: chrono::DateTime<chrono::Utc>,
456        tracker: &TaskExecutionTracker,
457    ) -> usize {
458        let due: Vec<(ScheduledTask, Option<Arc<dyn JobHandler>>)> = {
459            let tasks = self.tasks.read().unwrap();
460            let handlers = self.handlers.read().unwrap();
461            tasks
462                .values()
463                .filter(|t| t.enabled)
464                .filter_map(|t| {
465                    let parsed = self.parse_cron(&t.cron_expr).ok()?;
466                    if self.matches_cron(&parsed, now) {
467                        Some((t.clone(), handlers.get(&t.id).cloned()))
468                    } else {
469                        None
470                    }
471                })
472                .collect()
473        };
474
475        let mut due = due;
476        due.sort_by_key(|a| std::cmp::Reverse(a.0.priority));
477
478        let mut fired = 0usize;
479        for (task, handler) in due {
480            let start = std::time::Instant::now();
481            let (status, error_message) = if let Some(handler) = handler {
482                match handler.handle(&task) {
483                    Ok(()) => (TaskStatus::Succeeded, None),
484                    Err(e) => (TaskStatus::Failed, Some(e.to_string())),
485                }
486            } else {
487                (TaskStatus::Skipped, None)
488            };
489            let duration_ms = start.elapsed().as_millis() as u64;
490
491            let is_fired = status != TaskStatus::Failed;
492            tracker.record(TaskExecutionRecord {
493                task_id: task.id.clone(),
494                fired_at: now,
495                status,
496                error_message,
497                duration_ms,
498            });
499            if is_fired {
500                fired += 1;
501            }
502        }
503        fired
504    }
505
506    /// Returns total number of registered tasks.
507    pub fn get_task_count(&self) -> usize {
508        self.tasks.read().map(|tasks| tasks.len()).unwrap_or(0)
509    }
510
511    /// Returns number of enabled tasks.
512    pub fn get_enabled_task_count(&self) -> usize {
513        self.tasks
514            .read()
515            .map(|tasks| tasks.values().filter(|t| t.enabled).count())
516            .unwrap_or(0)
517    }
518
519    /// Returns `(id, priority)` list of all tasks, sorted by priority descending.
520    pub fn get_task_priorities(&self) -> Vec<(String, i32)> {
521        let mut result: Vec<(String, i32)> = self
522            .tasks
523            .read()
524            .map(|tasks| tasks.values().map(|t| (t.id.clone(), t.priority)).collect())
525            .unwrap_or_default();
526        result.sort_by_key(|(_, p)| std::cmp::Reverse(*p));
527        result
528    }
529}
530
531impl Default for CronScheduler {
532    fn default() -> Self {
533        Self::new()
534    }
535}
536
537/// Returns the next whole-minute boundary strictly after `dt`, with
538/// `second = 0` and `nanosecond = 0`.
539///
540/// Examples:
541/// - `00:00:00` → `00:01:00`
542/// - `00:00:30` → `00:01:00`
543/// - `00:01:45.500` → `00:02:00`
544fn align_to_next_minute_boundary(
545    dt: chrono::DateTime<chrono::Utc>,
546) -> chrono::DateTime<chrono::Utc> {
547    use chrono::Timelike;
548    // Truncate to current minute, then advance by one minute so we never
549    // report `dt` itself as the next run time (callers expect "next" to
550    // mean strictly after `dt`).
551    let truncated = dt
552        .with_second(0)
553        .and_then(|d| d.with_nanosecond(0))
554        .unwrap_or(dt);
555    truncated + chrono::Duration::minutes(1)
556}
557
558#[derive(Debug, Clone)]
559pub struct CronExpr {
560    pub second: String,
561    pub minute: String,
562    pub hour: String,
563    pub day_of_month: String,
564    pub month: String,
565}
566
567impl Scheduler for CronScheduler {
568    fn schedule(&self, task: ScheduledTask) -> Result<(), SchedulerError> {
569        if task.cron_expr.is_empty() {
570            return Err(SchedulerError::InvalidCronExpr(
571                "Cron expression cannot be empty".to_string(),
572            ));
573        }
574
575        self.parse_cron(&task.cron_expr)?;
576
577        let mut tasks = self
578            .tasks
579            .write()
580            .map_err(|e| SchedulerError::Internal(e.to_string()))?;
581        tasks.insert(task.id.clone(), task);
582        Ok(())
583    }
584
585    fn cancel(&self, task_id: &str) -> Result<(), SchedulerError> {
586        let mut tasks = self
587            .tasks
588            .write()
589            .map_err(|e| SchedulerError::Internal(e.to_string()))?;
590        tasks
591            .remove(task_id)
592            .ok_or_else(|| SchedulerError::TaskNotFound(task_id.to_string()))?;
593        Ok(())
594    }
595
596    fn pause(&self, task_id: &str) -> Result<(), SchedulerError> {
597        let mut tasks = self
598            .tasks
599            .write()
600            .map_err(|e| SchedulerError::Internal(e.to_string()))?;
601        let task = tasks
602            .get_mut(task_id)
603            .ok_or_else(|| SchedulerError::TaskNotFound(task_id.to_string()))?;
604        task.enabled = false;
605        Ok(())
606    }
607
608    fn resume(&self, task_id: &str) -> Result<(), SchedulerError> {
609        let mut tasks = self
610            .tasks
611            .write()
612            .map_err(|e| SchedulerError::Internal(e.to_string()))?;
613        let task = tasks
614            .get_mut(task_id)
615            .ok_or_else(|| SchedulerError::TaskNotFound(task_id.to_string()))?;
616        task.enabled = true;
617        Ok(())
618    }
619
620    fn list_tasks(&self) -> Vec<ScheduledTask> {
621        let tasks = self
622            .tasks
623            .read()
624            .map_err(|e| SchedulerError::Internal(e.to_string()))
625            .unwrap();
626        tasks.values().cloned().collect()
627    }
628}
629
630/// Task execution status.
631#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
632pub enum TaskStatus {
633    Pending,
634    Running,
635    Succeeded,
636    Failed,
637    Skipped,
638}
639
640/// Single task execution record.
641#[derive(Debug, Clone)]
642pub struct TaskExecutionRecord {
643    pub task_id: String,
644    pub fired_at: chrono::DateTime<chrono::Utc>,
645    pub status: TaskStatus,
646    pub error_message: Option<String>,
647    pub duration_ms: u64,
648}
649
650/// Task execution tracker: records the result of each task trigger, supports per-task history query and statistics.
651///
652/// Independent component, does not modify `CronScheduler` internal state. Caller can manually record after `try_fire_due`
653/// or use `CronScheduler::try_fire_due_tracked` convenience method.
654pub struct TaskExecutionTracker {
655    records: RwLock<Vec<TaskExecutionRecord>>,
656    max_capacity: usize,
657}
658
659impl TaskExecutionTracker {
660    pub fn new() -> Self {
661        Self::with_capacity(10_000)
662    }
663
664    pub fn with_capacity(max_capacity: usize) -> Self {
665        Self {
666            records: RwLock::new(Vec::new()),
667            max_capacity: max_capacity.max(1),
668        }
669    }
670
671    pub fn record(&self, record: TaskExecutionRecord) {
672        if let Ok(mut records) = self.records.write() {
673            if records.len() >= self.max_capacity {
674                records.remove(0);
675            }
676            records.push(record);
677        }
678    }
679
680    pub fn get_task_history(&self, task_id: &str) -> Vec<TaskExecutionRecord> {
681        self.records
682            .read()
683            .map(|records| {
684                records
685                    .iter()
686                    .filter(|r| r.task_id == task_id)
687                    .cloned()
688                    .collect()
689            })
690            .unwrap_or_default()
691    }
692
693    pub fn get_last_status(&self, task_id: &str) -> Option<TaskStatus> {
694        self.records.read().ok().and_then(|records| {
695            records
696                .iter()
697                .rev()
698                .find(|r| r.task_id == task_id)
699                .map(|r| r.status)
700        })
701    }
702
703    pub fn get_failure_count(&self, task_id: &str) -> usize {
704        self.records
705            .read()
706            .map(|records| {
707                records
708                    .iter()
709                    .filter(|r| r.task_id == task_id && r.status == TaskStatus::Failed)
710                    .count()
711            })
712            .unwrap_or(0)
713    }
714
715    pub fn get_success_count(&self, task_id: &str) -> usize {
716        self.records
717            .read()
718            .map(|records| {
719                records
720                    .iter()
721                    .filter(|r| r.task_id == task_id && r.status == TaskStatus::Succeeded)
722                    .count()
723            })
724            .unwrap_or(0)
725    }
726
727    pub fn get_total_count(&self, task_id: &str) -> usize {
728        self.records
729            .read()
730            .map(|records| records.iter().filter(|r| r.task_id == task_id).count())
731            .unwrap_or(0)
732    }
733
734    pub fn get_success_rate(&self, task_id: &str) -> f64 {
735        let total = self.get_total_count(task_id);
736        if total == 0 {
737            return 0.0;
738        }
739        self.get_success_count(task_id) as f64 / total as f64
740    }
741
742    pub fn clear(&self) {
743        if let Ok(mut records) = self.records.write() {
744            records.clear();
745        }
746    }
747
748    pub fn clear_task(&self, task_id: &str) {
749        if let Ok(mut records) = self.records.write() {
750            records.retain(|r| r.task_id != task_id);
751        }
752    }
753
754    pub fn record_count(&self) -> usize {
755        self.records
756            .read()
757            .map(|records| records.len())
758            .unwrap_or(0)
759    }
760}
761
762impl Default for TaskExecutionTracker {
763    fn default() -> Self {
764        Self::new()
765    }
766}
767
768/// Task health summary.
769#[derive(Debug, Clone)]
770pub struct TaskHealthSummary {
771    pub task_id: String,
772    pub total_executions: usize,
773    pub successes: usize,
774    pub failures: usize,
775    pub success_rate: f64,
776    pub last_status: Option<TaskStatus>,
777}
778
779impl TaskExecutionTracker {
780    pub fn get_all_task_ids(&self) -> Vec<String> {
781        self.records
782            .read()
783            .map(|records| {
784                let mut ids: Vec<String> = records.iter().map(|r| r.task_id.clone()).collect();
785                ids.sort();
786                ids.dedup();
787                ids
788            })
789            .unwrap_or_default()
790    }
791
792    pub fn get_health_summary(&self, task_id: &str) -> TaskHealthSummary {
793        TaskHealthSummary {
794            task_id: task_id.to_string(),
795            total_executions: self.get_total_count(task_id),
796            successes: self.get_success_count(task_id),
797            failures: self.get_failure_count(task_id),
798            success_rate: self.get_success_rate(task_id),
799            last_status: self.get_last_status(task_id),
800        }
801    }
802
803    pub fn get_all_health_summaries(&self) -> Vec<TaskHealthSummary> {
804        self.get_all_task_ids()
805            .iter()
806            .map(|id| self.get_health_summary(id))
807            .collect()
808    }
809}
810
811#[derive(Debug, thiserror::Error)]
812pub enum SchedulerError {
813    #[error("Task not found: {0}")]
814    TaskNotFound(String),
815    #[error("Invalid cron expression: {0}")]
816    InvalidCronExpr(String),
817    #[error("Failed to compute next run time: {0}")]
818    NoNextRunTime(String),
819    #[error("Scheduler error: {0}")]
820    Internal(String),
821}
822
823impl From<chrono::ParseError> for SchedulerError {
824    fn from(e: chrono::ParseError) -> Self {
825        SchedulerError::InvalidCronExpr(e.to_string())
826    }
827}
828
829impl serde::Serialize for SchedulerError {
830    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
831    where
832        S: serde::Serializer,
833    {
834        serializer.serialize_str(&self.to_string())
835    }
836}
837
838#[cfg(test)]
839mod tests {
840    use super::*;
841
842    #[test]
843    fn test_scheduled_task_new() {
844        let task = ScheduledTask::new("task1", "Test Task", "0 * * * *");
845        assert_eq!(task.id, "task1");
846        assert_eq!(task.name, "Test Task");
847        assert_eq!(task.cron_expr, "0 * * * *");
848        assert!(task.enabled);
849    }
850
851    #[test]
852    fn test_scheduled_task_with_callback() {
853        let task = ScheduledTask::new("task1", "Test", "* * * * *").with_callback("my_callback");
854        assert_eq!(task.callback, "my_callback");
855    }
856
857    #[test]
858    fn test_scheduled_task_disable() {
859        let task = ScheduledTask::new("task1", "Test", "* * * * *").disable();
860        assert!(!task.enabled);
861    }
862
863    #[test]
864    fn test_cron_parse() {
865        let scheduler = CronScheduler::new();
866        let result = scheduler.parse_cron("0 * * * *");
867        assert!(result.is_ok());
868        let expr = result.unwrap();
869        assert_eq!(expr.second, "0");
870        assert_eq!(expr.minute, "*");
871    }
872
873    #[test]
874    fn test_cron_parse_invalid() {
875        let scheduler = CronScheduler::new();
876        let result = scheduler.parse_cron("invalid");
877        assert!(result.is_err());
878    }
879
880    #[test]
881    fn test_cron_field_matches_star() {
882        let scheduler = CronScheduler::new();
883        assert!(scheduler.field_matches("*", 5));
884        assert!(scheduler.field_matches("*", 0));
885        assert!(scheduler.field_matches("*", 59));
886    }
887
888    #[test]
889    fn test_cron_field_matches_exact() {
890        let scheduler = CronScheduler::new();
891        assert!(scheduler.field_matches("5", 5));
892        assert!(!scheduler.field_matches("5", 6));
893    }
894
895    #[test]
896    fn test_cron_field_matches_range() {
897        let scheduler = CronScheduler::new();
898        assert!(scheduler.field_matches("1-5", 3));
899        assert!(!scheduler.field_matches("1-5", 7));
900    }
901
902    #[test]
903    fn test_cron_field_matches_list() {
904        let scheduler = CronScheduler::new();
905        assert!(scheduler.field_matches("1,3,5", 3));
906        assert!(!scheduler.field_matches("1,3,5", 2));
907    }
908
909    #[test]
910    fn test_cron_field_matches_step() {
911        let scheduler = CronScheduler::new();
912        assert!(scheduler.field_matches("*/5", 10));
913        assert!(scheduler.field_matches("*/5", 15));
914        assert!(!scheduler.field_matches("*/5", 7));
915    }
916
917    #[test]
918    fn test_scheduler_schedule() {
919        let scheduler = CronScheduler::new();
920        let task = ScheduledTask::new("task1", "Test", "0 * * * *");
921        let result = scheduler.schedule(task);
922        assert!(result.is_ok());
923    }
924
925    #[test]
926    fn test_scheduler_schedule_invalid_cron() {
927        let scheduler = CronScheduler::new();
928        let task = ScheduledTask::new("task1", "Test", "invalid");
929        let result = scheduler.schedule(task);
930        assert!(result.is_err());
931    }
932
933    #[test]
934    fn test_scheduler_cancel() {
935        let scheduler = CronScheduler::new();
936        let task = ScheduledTask::new("task1", "Test", "0 * * * *");
937        scheduler.schedule(task).unwrap();
938
939        let result = scheduler.cancel("task1");
940        assert!(result.is_ok());
941    }
942
943    #[test]
944    fn test_scheduler_cancel_not_found() {
945        let scheduler = CronScheduler::new();
946        let result = scheduler.cancel("nonexistent");
947        assert!(result.is_err());
948    }
949
950    #[test]
951    fn test_scheduler_pause_resume() {
952        let scheduler = CronScheduler::new();
953        let task = ScheduledTask::new("task1", "Test", "0 * * * *");
954        scheduler.schedule(task).unwrap();
955
956        scheduler.pause("task1").unwrap();
957        let tasks = scheduler.list_tasks();
958        assert!(!tasks[0].enabled);
959
960        scheduler.resume("task1").unwrap();
961        let tasks = scheduler.list_tasks();
962        assert!(tasks[0].enabled);
963    }
964
965    #[test]
966    fn test_scheduler_list_tasks() {
967        let scheduler = CronScheduler::new();
968        scheduler
969            .schedule(ScheduledTask::new("t1", "Task 1", "0 * * * *"))
970            .unwrap();
971        scheduler
972            .schedule(ScheduledTask::new("t2", "Task 2", "0 * * * *"))
973            .unwrap();
974
975        let tasks = scheduler.list_tasks();
976        assert_eq!(tasks.len(), 2);
977    }
978
979    #[test]
980    fn test_next_run_time_finds_next_minute_match() {
981        // `* * * * *` matches every minute, so next_run_time should return
982        // the next minute after `from`.
983        let scheduler = CronScheduler::new();
984        let from = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
985            .unwrap()
986            .with_timezone(&chrono::Utc);
987        let next = scheduler.next_run_time("* * * * *", from).unwrap();
988        assert_eq!(next, from + chrono::Duration::minutes(1));
989    }
990
991    #[test]
992    fn test_next_run_time_finds_hourly_match() {
993        // `0 * * * *` matches second=0, every minute/hour/day/month - i.e.
994        // every minute where second is 0. With 5-field cron (where the first
995        // field is `second`), `0 * * * *` matches every minute when second=0.
996        // Since we scan minute-by-minute, second is always 0 at scan points,
997        // so the first scan iteration should match.
998        let scheduler = CronScheduler::new();
999        let from = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:30Z")
1000            .unwrap()
1001            .with_timezone(&chrono::Utc);
1002        let next = scheduler.next_run_time("0 * * * *", from).unwrap();
1003        // from is 00:00:30; next minute is 00:01:00 (second=0, matches).
1004        assert_eq!(next, from + chrono::Duration::seconds(30));
1005    }
1006
1007    #[test]
1008    fn test_next_run_time_finds_daily_match_far_ahead() {
1009        // Cron `0 0 1 1 *` matches only at 00:00:00 on Jan 1 of any year.
1010        // Starting from 2024-01-01 00:01:00, the next match is 2025-01-01.
1011        // Before the fix, scanning only 365 minutes (~6 hours) ahead would
1012        // fail to find this match. The fixed scan window is 525,600 minutes
1013        // (~365 days), which is enough to find it.
1014        let scheduler = CronScheduler::new();
1015        let from = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:01:00Z")
1016            .unwrap()
1017            .with_timezone(&chrono::Utc);
1018        let next = scheduler.next_run_time("0 0 1 1 *", from);
1019        assert!(
1020            next.is_ok(),
1021            "should find next run within 365 days, got: {:?}",
1022            next
1023        );
1024    }
1025
1026    #[test]
1027    fn test_try_fire_due_fires_matching_task_with_handler() {
1028        let scheduler = CronScheduler::new();
1029        let task = ScheduledTask::new("t1", "Test", "* * * * *");
1030        scheduler.schedule(task).unwrap();
1031
1032        let handler = Arc::new(CounterJobHandler::new());
1033        let counter = handler.counter();
1034        scheduler.register_handler("t1", handler);
1035
1036        let now = chrono::Utc::now();
1037        let fired = scheduler.try_fire_due(now);
1038        assert_eq!(fired, 1);
1039        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1040
1041        // Fire again to make sure counter accumulates.
1042        scheduler.try_fire_due(now);
1043        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 2);
1044    }
1045
1046    #[test]
1047    fn test_try_fire_due_skips_non_matching_task() {
1048        let scheduler = CronScheduler::new();
1049        // Cron `99 * * * *` is technically parseable (field "99" parses as
1050        // u32=99), but no real time has second=99 so it never matches.
1051        let task = ScheduledTask::new("never", "Test", "99 * * * *");
1052        scheduler.schedule(task).unwrap();
1053
1054        let handler = Arc::new(CounterJobHandler::new());
1055        let counter = handler.counter();
1056        scheduler.register_handler("never", handler);
1057
1058        let now = chrono::Utc::now();
1059        let fired = scheduler.try_fire_due(now);
1060        assert_eq!(fired, 0);
1061        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 0);
1062    }
1063
1064    #[test]
1065    fn test_try_fire_due_skips_paused_task() {
1066        let scheduler = CronScheduler::new();
1067        scheduler
1068            .schedule(ScheduledTask::new("t1", "Test", "* * * * *"))
1069            .unwrap();
1070        scheduler.pause("t1").unwrap();
1071
1072        let handler = Arc::new(CounterJobHandler::new());
1073        let counter = handler.counter();
1074        scheduler.register_handler("t1", handler);
1075
1076        let now = chrono::Utc::now();
1077        let fired = scheduler.try_fire_due(now);
1078        assert_eq!(fired, 0);
1079        assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 0);
1080    }
1081
1082    #[test]
1083    fn test_start_stop_background_thread() {
1084        let scheduler = CronScheduler::new();
1085        scheduler
1086            .schedule(ScheduledTask::new("t1", "Test", "* * * * *"))
1087            .unwrap();
1088        let handler = Arc::new(CounterJobHandler::new());
1089        let counter = handler.counter();
1090        scheduler.register_handler("t1", handler);
1091
1092        assert!(!scheduler.is_running());
1093        scheduler.start(50).unwrap();
1094        assert!(scheduler.is_running());
1095
1096        // Wait long enough for at least one tick (50ms) + jitter.
1097        std::thread::sleep(Duration::from_millis(300));
1098        assert!(
1099            counter.load(std::sync::atomic::Ordering::SeqCst) >= 1,
1100            "expected the background thread to fire the handler at least once"
1101        );
1102
1103        scheduler.stop().unwrap();
1104        assert!(!scheduler.is_running());
1105
1106        // Snapshot the counter after stopping.
1107        let after_stop = counter.load(std::sync::atomic::Ordering::SeqCst);
1108        // Wait a bit more to ensure the worker has actually exited and is no
1109        // longer invoking the handler.
1110        std::thread::sleep(Duration::from_millis(200));
1111        assert_eq!(
1112            counter.load(std::sync::atomic::Ordering::SeqCst),
1113            after_stop,
1114            "counter should not change after stop()"
1115        );
1116    }
1117
1118    #[test]
1119    fn test_start_twice_errors() {
1120        let scheduler = CronScheduler::new();
1121        scheduler.start(1000).unwrap();
1122        let second = scheduler.start(1000);
1123        assert!(second.is_err());
1124        scheduler.stop().unwrap();
1125    }
1126
1127    #[test]
1128    fn test_stop_when_not_running_is_noop() {
1129        let scheduler = CronScheduler::new();
1130        assert!(scheduler.stop().is_ok());
1131    }
1132
1133    #[test]
1134    fn test_recording_handler_with_try_fire_due() {
1135        let scheduler = CronScheduler::new();
1136        scheduler
1137            .schedule(ScheduledTask::new("a", "Task A", "* * * * *"))
1138            .unwrap();
1139        scheduler
1140            .schedule(ScheduledTask::new("b", "Task B", "99 * * * *"))
1141            .unwrap();
1142        scheduler
1143            .schedule(ScheduledTask::new("c", "Task C", "* * * * *"))
1144            .unwrap();
1145
1146        let handler = Arc::new(RecordingJobHandler::new());
1147        scheduler.register_handler("a", handler.clone());
1148        scheduler.register_handler("b", handler.clone());
1149        scheduler.register_handler("c", handler.clone());
1150
1151        let now = chrono::Utc::now();
1152        let fired = scheduler.try_fire_due(now);
1153        assert_eq!(fired, 2); // Only "a" and "c" match.
1154        let ids = handler.handled_ids();
1155        assert!(ids.contains(&"a".to_string()));
1156        assert!(ids.contains(&"c".to_string()));
1157        assert!(!ids.contains(&"b".to_string()));
1158    }
1159
1160    // ===== TDD RED:秒级 cron 支持测试(bug 修复前应失败) =====
1161
1162    #[test]
1163    fn test_next_run_time_second_precision_single_value() {
1164        // `30 * * * *` 表示每分钟的 30 秒触发
1165        // from=00:00:00,下一个匹配应为 00:00:30(同一分钟内的 30 秒)
1166        let scheduler = CronScheduler::new();
1167        let from = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1168            .unwrap()
1169            .with_timezone(&chrono::Utc);
1170        let next = scheduler.next_run_time("30 * * * *", from);
1171        assert!(
1172            next.is_ok(),
1173            "should find next run for second=30 cron, got: {:?}",
1174            next
1175        );
1176        assert_eq!(next.unwrap(), from + chrono::Duration::seconds(30));
1177    }
1178
1179    #[test]
1180    fn test_next_run_time_second_precision_next_minute() {
1181        // `30 * * * *` from=00:00:45,当前分钟内 30 秒已过
1182        // 下一个匹配应为 00:01:30(下一分钟的 30 秒)
1183        let scheduler = CronScheduler::new();
1184        let from = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:45Z")
1185            .unwrap()
1186            .with_timezone(&chrono::Utc);
1187        let next = scheduler.next_run_time("30 * * * *", from).unwrap();
1188        assert_eq!(next, from + chrono::Duration::seconds(45));
1189    }
1190
1191    #[test]
1192    fn test_next_run_time_second_range() {
1193        // `10-12 * * * *` 表示每分钟的 10/11/12 秒触发
1194        // from=00:00:00,第一个匹配应为 00:00:10
1195        let scheduler = CronScheduler::new();
1196        let from = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1197            .unwrap()
1198            .with_timezone(&chrono::Utc);
1199        let next = scheduler.next_run_time("10-12 * * * *", from).unwrap();
1200        assert_eq!(next, from + chrono::Duration::seconds(10));
1201    }
1202
1203    #[test]
1204    fn test_next_run_time_second_list_skips_past() {
1205        // `10,20,30 * * * *` from=00:00:15
1206        // 10 秒已过,下一个匹配应为 00:00:20
1207        let scheduler = CronScheduler::new();
1208        let from = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:15Z")
1209            .unwrap()
1210            .with_timezone(&chrono::Utc);
1211        let next = scheduler.next_run_time("10,20,30 * * * *", from).unwrap();
1212        assert_eq!(next, from + chrono::Duration::seconds(5));
1213    }
1214
1215    #[test]
1216    fn test_priority_ordering_high_first() {
1217        let scheduler = CronScheduler::new();
1218        let handler = Arc::new(RecordingJobHandler::new()) as Arc<dyn JobHandler>;
1219
1220        scheduler
1221            .schedule(ScheduledTask::new("low", "Low", "* * * * *").with_priority(1))
1222            .unwrap();
1223        scheduler
1224            .schedule(ScheduledTask::new("high", "High", "* * * * *").with_priority(10))
1225            .unwrap();
1226        scheduler
1227            .schedule(ScheduledTask::new("mid", "Mid", "* * * * *").with_priority(5))
1228            .unwrap();
1229        scheduler.register_handler("low", handler.clone());
1230        scheduler.register_handler("high", handler.clone());
1231        scheduler.register_handler("mid", handler);
1232
1233        let now = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1234            .unwrap()
1235            .with_timezone(&chrono::Utc);
1236        let fired = scheduler.try_fire_due(now);
1237        assert_eq!(fired, 3);
1238
1239        let tasks = scheduler.list_tasks();
1240        let high = tasks.iter().find(|t| t.id == "high").unwrap();
1241        let low = tasks.iter().find(|t| t.id == "low").unwrap();
1242        assert!(high.priority > low.priority);
1243    }
1244
1245    #[test]
1246    fn test_cron_boundary_second_59() {
1247        let scheduler = CronScheduler::new();
1248        let from = chrono::DateTime::parse_from_rfc3339("2024-06-15T10:10:00Z")
1249            .unwrap()
1250            .with_timezone(&chrono::Utc);
1251        let next = scheduler.next_run_time("59 * * * *", from).unwrap();
1252        assert_eq!(next.second(), 59);
1253    }
1254
1255    #[test]
1256    fn test_cron_boundary_cross_year() {
1257        let scheduler = CronScheduler::new();
1258        let from = chrono::DateTime::parse_from_rfc3339("2024-12-31T23:59:59Z")
1259            .unwrap()
1260            .with_timezone(&chrono::Utc);
1261        let next = scheduler.next_run_time("0 0 1 1 *", from);
1262        assert!(next.is_ok());
1263        let next = next.unwrap();
1264        assert_eq!(next.year(), 2025);
1265        assert_eq!(next.month(), 1);
1266        assert_eq!(next.day(), 1);
1267    }
1268
1269    #[test]
1270    fn test_next_run_time_strictly_greater() {
1271        let scheduler = CronScheduler::new();
1272        let from = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1273            .unwrap()
1274            .with_timezone(&chrono::Utc);
1275        let next = scheduler.next_run_time("0 * * * *", from).unwrap();
1276        assert!(next > from);
1277    }
1278
1279    #[test]
1280    fn test_task_status_enum() {
1281        let statuses = [
1282            TaskStatus::Pending,
1283            TaskStatus::Running,
1284            TaskStatus::Succeeded,
1285            TaskStatus::Failed,
1286            TaskStatus::Skipped,
1287        ];
1288        for i in 0..statuses.len() {
1289            for j in 0..statuses.len() {
1290                if i == j {
1291                    assert_eq!(statuses[i], statuses[j]);
1292                } else {
1293                    assert_ne!(statuses[i], statuses[j]);
1294                }
1295            }
1296        }
1297    }
1298
1299    #[test]
1300    fn test_execution_tracker_record_and_query() {
1301        let tracker = TaskExecutionTracker::new();
1302        let now = chrono::Utc::now();
1303
1304        tracker.record(TaskExecutionRecord {
1305            task_id: "t1".to_string(),
1306            fired_at: now,
1307            status: TaskStatus::Succeeded,
1308            error_message: None,
1309            duration_ms: 10,
1310        });
1311        tracker.record(TaskExecutionRecord {
1312            task_id: "t1".to_string(),
1313            fired_at: now,
1314            status: TaskStatus::Failed,
1315            error_message: Some("boom".to_string()),
1316            duration_ms: 5,
1317        });
1318        tracker.record(TaskExecutionRecord {
1319            task_id: "t1".to_string(),
1320            fired_at: now,
1321            status: TaskStatus::Succeeded,
1322            error_message: None,
1323            duration_ms: 8,
1324        });
1325
1326        assert_eq!(tracker.get_total_count("t1"), 3);
1327        assert_eq!(tracker.get_success_count("t1"), 2);
1328        assert_eq!(tracker.get_failure_count("t1"), 1);
1329        assert_eq!(tracker.get_last_status("t1"), Some(TaskStatus::Succeeded));
1330        let rate = tracker.get_success_rate("t1");
1331        assert!((rate - 2.0 / 3.0).abs() < 1e-9);
1332    }
1333
1334    #[test]
1335    fn test_execution_tracker_capacity_eviction() {
1336        let tracker = TaskExecutionTracker::with_capacity(2);
1337        let now = chrono::Utc::now();
1338        for i in 0..3 {
1339            tracker.record(TaskExecutionRecord {
1340                task_id: format!("t{}", i),
1341                fired_at: now,
1342                status: TaskStatus::Succeeded,
1343                error_message: None,
1344                duration_ms: 1,
1345            });
1346        }
1347        assert_eq!(tracker.record_count(), 2);
1348        assert_eq!(tracker.get_total_count("t0"), 0);
1349        assert_eq!(tracker.get_total_count("t1"), 1);
1350        assert_eq!(tracker.get_total_count("t2"), 1);
1351    }
1352
1353    #[test]
1354    fn test_execution_tracker_clear() {
1355        let tracker = TaskExecutionTracker::new();
1356        let now = chrono::Utc::now();
1357        tracker.record(TaskExecutionRecord {
1358            task_id: "t1".to_string(),
1359            fired_at: now,
1360            status: TaskStatus::Succeeded,
1361            error_message: None,
1362            duration_ms: 1,
1363        });
1364        tracker.record(TaskExecutionRecord {
1365            task_id: "t2".to_string(),
1366            fired_at: now,
1367            status: TaskStatus::Failed,
1368            error_message: None,
1369            duration_ms: 1,
1370        });
1371        tracker.clear_task("t1");
1372        assert_eq!(tracker.get_total_count("t1"), 0);
1373        assert_eq!(tracker.get_total_count("t2"), 1);
1374        assert_eq!(tracker.record_count(), 1);
1375        tracker.clear();
1376        assert_eq!(tracker.record_count(), 0);
1377    }
1378
1379    #[test]
1380    fn test_try_fire_due_tracked() {
1381        let scheduler = CronScheduler::new();
1382        let tracker = TaskExecutionTracker::new();
1383        let handler = Arc::new(RecordingJobHandler::new()) as Arc<dyn JobHandler>;
1384
1385        scheduler
1386            .schedule(ScheduledTask::new("t1", "Task1", "* * * * *"))
1387            .unwrap();
1388        scheduler.register_handler("t1", handler);
1389
1390        let now = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1391            .unwrap()
1392            .with_timezone(&chrono::Utc);
1393        let fired = scheduler.try_fire_due_tracked(now, &tracker);
1394        assert_eq!(fired, 1);
1395        assert_eq!(tracker.get_total_count("t1"), 1);
1396        assert_eq!(tracker.get_last_status("t1"), Some(TaskStatus::Succeeded));
1397    }
1398
1399    #[test]
1400    fn test_try_fire_due_tracked_no_handler_skipped() {
1401        let scheduler = CronScheduler::new();
1402        let tracker = TaskExecutionTracker::new();
1403
1404        scheduler
1405            .schedule(ScheduledTask::new("t1", "Task1", "* * * * *"))
1406            .unwrap();
1407
1408        let now = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1409            .unwrap()
1410            .with_timezone(&chrono::Utc);
1411        let fired = scheduler.try_fire_due_tracked(now, &tracker);
1412        assert_eq!(fired, 1);
1413        assert_eq!(tracker.get_last_status("t1"), Some(TaskStatus::Skipped));
1414    }
1415
1416    #[test]
1417    fn test_health_summary() {
1418        let tracker = TaskExecutionTracker::new();
1419        let now = chrono::Utc::now();
1420        tracker.record(TaskExecutionRecord {
1421            task_id: "t1".to_string(),
1422            fired_at: now,
1423            status: TaskStatus::Succeeded,
1424            error_message: None,
1425            duration_ms: 1,
1426        });
1427        tracker.record(TaskExecutionRecord {
1428            task_id: "t1".to_string(),
1429            fired_at: now,
1430            status: TaskStatus::Failed,
1431            error_message: Some("err".to_string()),
1432            duration_ms: 1,
1433        });
1434        tracker.record(TaskExecutionRecord {
1435            task_id: "t2".to_string(),
1436            fired_at: now,
1437            status: TaskStatus::Succeeded,
1438            error_message: None,
1439            duration_ms: 1,
1440        });
1441
1442        let ids = tracker.get_all_task_ids();
1443        assert_eq!(ids, vec!["t1".to_string(), "t2".to_string()]);
1444
1445        let summary = tracker.get_health_summary("t1");
1446        assert_eq!(summary.total_executions, 2);
1447        assert_eq!(summary.successes, 1);
1448        assert_eq!(summary.failures, 1);
1449        assert!((summary.success_rate - 0.5).abs() < 1e-9);
1450
1451        let all = tracker.get_all_health_summaries();
1452        assert_eq!(all.len(), 2);
1453    }
1454
1455    #[test]
1456    fn test_scheduler_task_counts() {
1457        let scheduler = CronScheduler::new();
1458        scheduler
1459            .schedule(ScheduledTask::new("t1", "T1", "* * * * *"))
1460            .unwrap();
1461        scheduler
1462            .schedule(ScheduledTask::new("t2", "T2", "* * * * *").disable())
1463            .unwrap();
1464        assert_eq!(scheduler.get_task_count(), 2);
1465        assert_eq!(scheduler.get_enabled_task_count(), 1);
1466    }
1467
1468    #[test]
1469    fn test_scheduler_task_priorities() {
1470        let scheduler = CronScheduler::new();
1471        scheduler
1472            .schedule(ScheduledTask::new("low", "Low", "* * * * *").with_priority(1))
1473            .unwrap();
1474        scheduler
1475            .schedule(ScheduledTask::new("high", "High", "* * * * *").with_priority(10))
1476            .unwrap();
1477        let prios = scheduler.get_task_priorities();
1478        assert_eq!(prios[0], ("high".to_string(), 10));
1479        assert_eq!(prios[1], ("low".to_string(), 1));
1480    }
1481}