1use 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 let needs_second_precision = !matches!(parsed.second.as_str(), "*" | "0");
126
127 if !needs_second_precision {
128 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 let seconds = self.parse_field_values(&parsed.second, 0, 59)?;
140 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 if self.matches_cron_ignoring_second(&parsed, minute_start) {
149 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 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 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 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 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 fired += 1;
369 }
370 }
371 fired
372 }
373
374 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 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(worker);
430 handle
431 .join()
432 .map_err(|_| SchedulerError::Internal("worker thread panicked".to_string()))?;
433 }
434 Ok(())
435 }
436
437 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 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 pub fn get_task_count(&self) -> usize {
508 self.tasks.read().map(|tasks| tasks.len()).unwrap_or(0)
509 }
510
511 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 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
537fn align_to_next_minute_boundary(
545 dt: chrono::DateTime<chrono::Utc>,
546) -> chrono::DateTime<chrono::Utc> {
547 use chrono::Timelike;
548 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
632pub enum TaskStatus {
633 Pending,
634 Running,
635 Succeeded,
636 Failed,
637 Skipped,
638}
639
640#[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
650pub 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#[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 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 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 assert_eq!(next, from + chrono::Duration::seconds(30));
1005 }
1006
1007 #[test]
1008 fn test_next_run_time_finds_daily_match_far_ahead() {
1009 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 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 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 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 let after_stop = counter.load(std::sync::atomic::Ordering::SeqCst);
1108 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); 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 #[test]
1163 fn test_next_run_time_second_precision_single_value() {
1164 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 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 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 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}