1use crate::alert::{Alert, AlertCallback, AlertCondition, AlertLevel, AlertManager};
7use crate::config::ScheduleError;
8use crate::heartbeat::BeatHeartbeat;
9use crate::history::{DependencyStatus, ExecutionRecord, ExecutionResult, HealthCheckResult};
10use crate::lock::LockManager;
11use crate::schedule::Schedule;
12use crate::task::ScheduledTask;
13use crate::FailureCallback;
14use celers_core::lock::DistributedLockBackend;
15use chrono::{DateTime, Duration, Utc};
16use serde::{Deserialize, Serialize};
17use std::collections::{HashMap, HashSet};
18use std::path::PathBuf;
19use std::sync::Arc;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23pub enum ConflictSeverity {
24 Low,
26 Medium,
28 High,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ScheduleConflict {
35 pub task1: String,
37 pub task2: String,
39 pub severity: ConflictSeverity,
41 pub overlap_seconds: u64,
43 pub description: String,
45 pub resolution: Option<String>,
47}
48
49impl ScheduleConflict {
50 pub fn new(
52 task1: String,
53 task2: String,
54 severity: ConflictSeverity,
55 overlap_seconds: u64,
56 description: String,
57 ) -> Self {
58 Self {
59 task1,
60 task2,
61 severity,
62 overlap_seconds,
63 description,
64 resolution: None,
65 }
66 }
67
68 pub fn with_resolution(mut self, resolution: String) -> Self {
70 self.resolution = Some(resolution);
71 self
72 }
73
74 pub fn is_high_severity(&self) -> bool {
76 self.severity == ConflictSeverity::High
77 }
78
79 pub fn is_medium_severity(&self) -> bool {
81 self.severity == ConflictSeverity::Medium
82 }
83
84 pub fn is_low_severity(&self) -> bool {
86 self.severity == ConflictSeverity::Low
87 }
88}
89
90impl std::fmt::Display for ScheduleConflict {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 write!(
93 f,
94 "Conflict[{:?}]: {} <-> {} (overlap: {}s) - {}",
95 self.severity, self.task1, self.task2, self.overlap_seconds, self.description
96 )
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SchedulerMetrics {
103 pub total_tasks: usize,
105 pub enabled_tasks: usize,
107 pub disabled_tasks: usize,
109 pub tasks_with_executions: usize,
111 pub total_successes: u64,
113 pub total_failures: u64,
115 pub total_timeouts: u64,
117 pub total_executions: u64,
119 pub overall_success_rate: f64,
121 pub tasks_in_retry: usize,
123 pub tasks_with_warnings: usize,
125 pub unhealthy_tasks: usize,
127 pub stuck_tasks: usize,
129}
130
131impl SchedulerMetrics {
132 pub fn from_scheduler(scheduler: &BeatScheduler) -> Self {
134 let total_tasks = scheduler.tasks.len();
135 let enabled_tasks = scheduler.tasks.values().filter(|t| t.enabled).count();
136 let disabled_tasks = total_tasks - enabled_tasks;
137 let tasks_with_executions = scheduler.tasks.values().filter(|t| t.has_run()).count();
138
139 let mut total_successes = 0u64;
140 let mut total_failures = 0u64;
141 let mut total_timeouts = 0u64;
142
143 for task in scheduler.tasks.values() {
144 total_successes += task.history_success_count() as u64;
145 total_failures += task.history_failure_count() as u64;
146 total_timeouts += task.history_timeout_count() as u64;
147 }
148
149 let total_executions = total_successes + total_failures + total_timeouts;
150 let overall_success_rate = if total_executions == 0 {
151 0.0
152 } else {
153 total_successes as f64 / total_executions as f64
154 };
155
156 let tasks_in_retry = scheduler
157 .tasks
158 .values()
159 .filter(|t| t.retry_count > 0)
160 .count();
161
162 let tasks_with_warnings = scheduler
163 .tasks
164 .values()
165 .map(|t| t.check_health())
166 .filter(|r| r.health.has_warnings())
167 .count();
168
169 let unhealthy_tasks = scheduler
170 .tasks
171 .values()
172 .map(|t| t.check_health())
173 .filter(|r| r.health.is_unhealthy())
174 .count();
175
176 let stuck_tasks = scheduler.get_stuck_tasks().len();
177
178 Self {
179 total_tasks,
180 enabled_tasks,
181 disabled_tasks,
182 tasks_with_executions,
183 total_successes,
184 total_failures,
185 total_timeouts,
186 total_executions,
187 overall_success_rate,
188 tasks_in_retry,
189 tasks_with_warnings,
190 unhealthy_tasks,
191 stuck_tasks,
192 }
193 }
194}
195
196#[derive(Debug, Clone)]
198pub struct TaskStatistics {
199 pub name: String,
201 pub success_count: usize,
203 pub failure_count: usize,
205 pub timeout_count: usize,
207 pub average_duration_ms: Option<u64>,
209 pub min_duration_ms: Option<u64>,
211 pub max_duration_ms: Option<u64>,
213 pub success_rate: f64,
215 pub failure_rate: f64,
217 pub retry_count: u32,
219 pub is_stuck: bool,
221}
222
223impl TaskStatistics {
224 pub fn from_task(task: &ScheduledTask) -> Self {
226 Self {
227 name: task.name.clone(),
228 success_count: task.history_success_count(),
229 failure_count: task.history_failure_count(),
230 timeout_count: task.history_timeout_count(),
231 average_duration_ms: task.average_duration_ms(),
232 min_duration_ms: task.min_duration_ms(),
233 max_duration_ms: task.max_duration_ms(),
234 success_rate: task.history_success_rate(),
235 failure_rate: task.failure_rate(),
236 retry_count: task.retry_count,
237 is_stuck: task.is_stuck().is_some(),
238 }
239 }
240}
241
242#[derive(Serialize, Deserialize)]
244pub struct BeatScheduler {
245 pub(crate) tasks: HashMap<String, ScheduledTask>,
247
248 #[serde(skip)]
250 pub(crate) state_file: Option<PathBuf>,
251
252 #[serde(skip)]
254 pub(crate) failure_callbacks: Vec<FailureCallback>,
255
256 #[serde(default)]
258 pub(crate) lock_manager: LockManager,
259
260 #[serde(skip)]
262 pub(crate) instance_id: String,
263
264 #[serde(default)]
266 pub(crate) alert_manager: AlertManager,
267
268 #[serde(skip)]
274 pub(crate) distributed_lock_backend: Option<Arc<dyn DistributedLockBackend>>,
275
276 #[serde(skip)]
281 pub(crate) heartbeat: Option<BeatHeartbeat>,
282}
283
284impl BeatScheduler {
285 pub fn new() -> Self {
286 use std::sync::atomic::{AtomicU64, Ordering};
287 static COUNTER: AtomicU64 = AtomicU64::new(0);
288 let id = COUNTER.fetch_add(1, Ordering::SeqCst);
289
290 Self {
291 tasks: HashMap::new(),
292 state_file: None,
293 failure_callbacks: Vec::new(),
294 lock_manager: LockManager::default(),
295 instance_id: format!("scheduler-{}", id),
296 alert_manager: AlertManager::default(),
297 distributed_lock_backend: None,
298 heartbeat: None,
299 }
300 }
301
302 pub fn with_persistence<P: Into<PathBuf>>(state_file: P) -> Self {
315 use std::sync::atomic::{AtomicU64, Ordering};
316 static COUNTER: AtomicU64 = AtomicU64::new(0);
317 let id = COUNTER.fetch_add(1, Ordering::SeqCst);
318
319 Self {
320 tasks: HashMap::new(),
321 state_file: Some(state_file.into()),
322 failure_callbacks: Vec::new(),
323 lock_manager: LockManager::default(),
324 instance_id: format!("scheduler-{}", id),
325 alert_manager: AlertManager::default(),
326 distributed_lock_backend: None,
327 heartbeat: None,
328 }
329 }
330
331 pub fn load_from_file<P: Into<PathBuf>>(path: P) -> Result<Self, ScheduleError> {
342 let path = path.into();
343
344 if !path.exists() {
345 use std::sync::atomic::{AtomicU64, Ordering};
347 static COUNTER: AtomicU64 = AtomicU64::new(0);
348 let id = COUNTER.fetch_add(1, Ordering::SeqCst);
349
350 return Ok(Self {
351 tasks: HashMap::new(),
352 state_file: Some(path),
353 failure_callbacks: Vec::new(),
354 lock_manager: LockManager::default(),
355 instance_id: format!("scheduler-{}", id),
356 alert_manager: AlertManager::default(),
357 distributed_lock_backend: None,
358 heartbeat: None,
359 });
360 }
361
362 let content = std::fs::read_to_string(&path)
363 .map_err(|e| ScheduleError::Persistence(format!("Failed to read state file: {}", e)))?;
364
365 let mut scheduler: BeatScheduler = serde_json::from_str(&content).map_err(|e| {
366 ScheduleError::Persistence(format!("Failed to parse state file: {}", e))
367 })?;
368
369 scheduler.state_file = Some(path);
371 if scheduler.instance_id.is_empty() {
372 use std::sync::atomic::{AtomicU64, Ordering};
373 static COUNTER: AtomicU64 = AtomicU64::new(0);
374 let id = COUNTER.fetch_add(1, Ordering::SeqCst);
375 scheduler.instance_id = format!("scheduler-{}", id);
376 }
377
378 Ok(scheduler)
379 }
380
381 pub fn with_lock_backend(&mut self, backend: Arc<dyn DistributedLockBackend>) -> &mut Self {
401 self.distributed_lock_backend = Some(backend);
402 self
403 }
404
405 pub fn distributed_lock_backend(&self) -> Option<&Arc<dyn DistributedLockBackend>> {
407 self.distributed_lock_backend.as_ref()
408 }
409
410 pub fn with_heartbeat(&mut self, heartbeat: BeatHeartbeat) -> &mut Self {
416 self.heartbeat = Some(heartbeat);
417 self
418 }
419
420 pub fn heartbeat(&self) -> Option<&BeatHeartbeat> {
422 self.heartbeat.as_ref()
423 }
424
425 pub async fn is_leader(&self) -> bool {
430 match &self.heartbeat {
431 Some(hb) => hb.is_leader().await,
432 None => true,
433 }
434 }
435
436 pub async fn try_acquire_distributed_lock(
445 &mut self,
446 task_name: &str,
447 ttl_secs: u64,
448 ) -> Result<bool, ScheduleError> {
449 if let Some(ref backend) = self.distributed_lock_backend {
450 backend
451 .try_acquire(task_name, &self.instance_id, ttl_secs)
452 .await
453 .map_err(|e| ScheduleError::Invalid(format!("Distributed lock error: {}", e)))
454 } else {
455 self.lock_manager
456 .try_acquire(task_name, &self.instance_id, Some(ttl_secs))
457 }
458 }
459
460 pub async fn release_distributed_lock(
465 &mut self,
466 task_name: &str,
467 ) -> Result<bool, ScheduleError> {
468 if let Some(ref backend) = self.distributed_lock_backend {
469 backend
470 .release(task_name, &self.instance_id)
471 .await
472 .map_err(|e| ScheduleError::Invalid(format!("Distributed lock error: {}", e)))
473 } else {
474 self.lock_manager.release(task_name, &self.instance_id)
475 }
476 }
477
478 pub async fn renew_distributed_lock(
483 &mut self,
484 task_name: &str,
485 ttl_secs: u64,
486 ) -> Result<bool, ScheduleError> {
487 if let Some(ref backend) = self.distributed_lock_backend {
488 backend
489 .renew(task_name, &self.instance_id, ttl_secs)
490 .await
491 .map_err(|e| ScheduleError::Invalid(format!("Distributed lock error: {}", e)))
492 } else {
493 self.lock_manager
494 .renew(task_name, &self.instance_id, Some(ttl_secs))
495 }
496 }
497
498 pub async fn is_distributed_locked(&self, task_name: &str) -> Result<bool, ScheduleError> {
503 if let Some(ref backend) = self.distributed_lock_backend {
504 backend
505 .is_locked(task_name)
506 .await
507 .map_err(|e| ScheduleError::Invalid(format!("Distributed lock error: {}", e)))
508 } else {
509 Ok(self.lock_manager.is_locked(task_name))
510 }
511 }
512
513 pub async fn release_all_distributed_locks(&mut self) -> Result<u64, ScheduleError> {
517 if let Some(ref backend) = self.distributed_lock_backend {
518 backend
519 .release_all(&self.instance_id)
520 .await
521 .map_err(|e| ScheduleError::Invalid(format!("Distributed lock error: {}", e)))
522 } else {
523 let count = self.lock_manager.get_active_locks().len() as u64;
524 self.lock_manager.release_all();
525 Ok(count)
526 }
527 }
528
529 pub fn save_state(&self) -> Result<(), ScheduleError> {
537 if let Some(ref path) = self.state_file {
538 let json = serde_json::to_string_pretty(&self).map_err(|e| {
539 ScheduleError::Persistence(format!("Failed to serialize state: {}", e))
540 })?;
541
542 std::fs::write(path, json).map_err(|e| {
543 ScheduleError::Persistence(format!("Failed to write state file: {}", e))
544 })?;
545 }
546 Ok(())
547 }
548
549 pub fn export_state(&self) -> Result<String, ScheduleError> {
565 serde_json::to_string_pretty(&self)
566 .map_err(|e| ScheduleError::Persistence(format!("Failed to serialize state: {}", e)))
567 }
568
569 pub fn list_tasks(&self) -> &HashMap<String, ScheduledTask> {
588 &self.tasks
589 }
590
591 pub fn get_task(&self, name: &str) -> Option<&ScheduledTask> {
605 self.tasks.get(name)
606 }
607
608 pub fn add_task(&mut self, mut task: ScheduledTask) -> Result<(), ScheduleError> {
609 task.update_next_run_cache();
611 self.tasks.insert(task.name.clone(), task);
612 self.save_state()?;
613 Ok(())
614 }
615
616 pub fn add_tasks_batch(&mut self, tasks: Vec<ScheduledTask>) -> Result<usize, ScheduleError> {
642 let mut added_count = 0;
643
644 for mut task in tasks {
645 task.update_next_run_cache();
647 self.tasks.insert(task.name.clone(), task);
648 added_count += 1;
649 }
650
651 if added_count > 0 {
653 self.save_state()?;
654 }
655
656 Ok(added_count)
657 }
658
659 pub fn remove_task(&mut self, name: &str) -> Result<Option<ScheduledTask>, ScheduleError> {
660 let task = self.tasks.remove(name);
661 self.save_state()?;
662 Ok(task)
663 }
664
665 pub fn remove_tasks_batch(&mut self, names: &[&str]) -> Result<usize, ScheduleError> {
689 let mut removed_count = 0;
690
691 for name in names {
692 if self.tasks.remove(*name).is_some() {
693 removed_count += 1;
694 }
695 }
696
697 if removed_count > 0 {
699 self.save_state()?;
700 }
701
702 Ok(removed_count)
703 }
704
705 pub fn mark_task_run(&mut self, name: &str) -> Result<(), ScheduleError> {
707 if let Some(task) = self.tasks.get_mut(name) {
708 task.last_run_at = Some(Utc::now());
709 task.total_run_count += 1;
710 self.save_state()?;
711 }
712 Ok(())
713 }
714
715 pub fn mark_task_success(&mut self, name: &str) -> Result<(), ScheduleError> {
717 let should_remove = if let Some(task) = self.tasks.get_mut(name) {
718 let now = Utc::now();
719 task.last_run_at = Some(now);
720 task.total_run_count += 1;
721 task.mark_success();
722
723 let record = ExecutionRecord::completed(now, ExecutionResult::Success);
725 task.add_execution_record(record);
726
727 task.update_next_run_cache();
729
730 task.schedule.is_onetime()
732 } else {
733 false
734 };
735
736 if should_remove {
738 self.tasks.remove(name);
739 }
740
741 self.save_state()?;
742 Ok(())
743 }
744
745 pub fn mark_task_success_with_start(
747 &mut self,
748 name: &str,
749 started_at: DateTime<Utc>,
750 ) -> Result<(), ScheduleError> {
751 let should_remove = if let Some(task) = self.tasks.get_mut(name) {
752 let now = Utc::now();
753 task.last_run_at = Some(now);
754 task.total_run_count += 1;
755 task.mark_success();
756
757 let record = ExecutionRecord::completed(started_at, ExecutionResult::Success);
759 task.add_execution_record(record);
760
761 task.update_next_run_cache();
763
764 task.schedule.is_onetime()
766 } else {
767 false
768 };
769
770 if should_remove {
772 self.tasks.remove(name);
773 }
774
775 self.save_state()?;
776 Ok(())
777 }
778
779 pub fn mark_task_failure(&mut self, name: &str) -> Result<(), ScheduleError> {
781 self.mark_task_failure_with_error(name, "Unknown error".to_string())
782 }
783
784 pub fn mark_task_failure_with_error(
786 &mut self,
787 name: &str,
788 error: String,
789 ) -> Result<(), ScheduleError> {
790 if let Some(task) = self.tasks.get_mut(name) {
791 let now = Utc::now();
792 task.mark_failure();
793
794 let record = ExecutionRecord::completed(
796 now,
797 ExecutionResult::Failure {
798 error: error.clone(),
799 },
800 );
801 task.add_execution_record(record);
802
803 self.invoke_failure_callbacks(name, &error);
805
806 self.save_state()?;
807 }
808 Ok(())
809 }
810
811 pub fn mark_task_failure_with_start(
813 &mut self,
814 name: &str,
815 started_at: DateTime<Utc>,
816 error: String,
817 ) -> Result<(), ScheduleError> {
818 if let Some(task) = self.tasks.get_mut(name) {
819 task.mark_failure();
820
821 let record = ExecutionRecord::completed(
823 started_at,
824 ExecutionResult::Failure {
825 error: error.clone(),
826 },
827 );
828 task.add_execution_record(record);
829
830 self.invoke_failure_callbacks(name, &error);
832
833 self.save_state()?;
834 }
835 Ok(())
836 }
837
838 pub fn mark_task_timeout(
840 &mut self,
841 name: &str,
842 started_at: DateTime<Utc>,
843 ) -> Result<(), ScheduleError> {
844 if let Some(task) = self.tasks.get_mut(name) {
845 task.mark_failure();
846
847 let record = ExecutionRecord::completed(started_at, ExecutionResult::Timeout);
849 task.add_execution_record(record);
850
851 self.save_state()?;
852 }
853 Ok(())
854 }
855
856 pub fn on_failure(&mut self, callback: FailureCallback) {
874 self.failure_callbacks.push(callback);
875 }
876
877 pub fn clear_failure_callbacks(&mut self) {
879 self.failure_callbacks.clear();
880 }
881
882 fn invoke_failure_callbacks(&self, task_name: &str, error: &str) {
884 for callback in &self.failure_callbacks {
885 callback(task_name, error);
886 }
887 }
888
889 pub fn on_alert(&mut self, callback: AlertCallback) {
907 self.alert_manager.add_callback(callback);
908 }
909
910 pub fn get_alerts(&self) -> &[Alert] {
912 self.alert_manager.get_alerts()
913 }
914
915 pub fn get_critical_alerts(&self) -> Vec<&Alert> {
917 self.alert_manager.get_critical_alerts()
918 }
919
920 pub fn get_warning_alerts(&self) -> Vec<&Alert> {
922 self.alert_manager.get_warning_alerts()
923 }
924
925 pub fn get_task_alerts(&self, task_name: &str) -> Vec<&Alert> {
927 self.alert_manager.get_task_alerts(task_name)
928 }
929
930 pub fn get_recent_alerts(&self, seconds: i64) -> Vec<&Alert> {
932 self.alert_manager.get_recent_alerts(seconds)
933 }
934
935 pub fn clear_alerts(&mut self) {
937 self.alert_manager.clear();
938 }
939
940 pub fn clear_task_alerts(&mut self, task_name: &str) {
942 self.alert_manager.clear_task_alerts(task_name);
943 }
944
945 pub fn check_task_alerts(&mut self, task_name: &str) -> usize {
955 let task = match self.tasks.get(task_name) {
956 Some(t) => t,
957 None => return 0,
958 };
959
960 if !task.alert_config.enabled {
961 return 0;
962 }
963
964 let mut alerts_triggered = 0;
965
966 let consecutive_failures = task.consecutive_failure_count();
968 if consecutive_failures >= task.alert_config.consecutive_failures_threshold {
969 let alert = Alert::new(
970 task_name.to_string(),
971 AlertLevel::Critical,
972 AlertCondition::ConsecutiveFailures {
973 count: consecutive_failures,
974 threshold: task.alert_config.consecutive_failures_threshold,
975 },
976 format!(
977 "Task has {} consecutive failures (threshold: {})",
978 consecutive_failures, task.alert_config.consecutive_failures_threshold
979 ),
980 );
981 if self.alert_manager.record_alert(alert) {
982 alerts_triggered += 1;
983 }
984 }
985
986 let failure_rate = task.failure_rate();
988 if failure_rate > task.alert_config.failure_rate_threshold {
989 let alert = Alert::new(
990 task_name.to_string(),
991 AlertLevel::Warning,
992 AlertCondition::HighFailureRate {
993 rate: format!("{:.2}", failure_rate),
994 threshold: format!("{:.2}", task.alert_config.failure_rate_threshold),
995 },
996 format!(
997 "Task has high failure rate: {:.1}% (threshold: {:.1}%)",
998 failure_rate * 100.0,
999 task.alert_config.failure_rate_threshold * 100.0
1000 ),
1001 );
1002 if self.alert_manager.record_alert(alert) {
1003 alerts_triggered += 1;
1004 }
1005 }
1006
1007 if let Some(threshold_ms) = task.alert_config.slow_execution_threshold_ms {
1009 if let Some(avg_duration_ms) = task.average_duration_ms() {
1010 if avg_duration_ms > threshold_ms {
1011 let alert = Alert::new(
1012 task_name.to_string(),
1013 AlertLevel::Warning,
1014 AlertCondition::SlowExecution {
1015 duration_ms: avg_duration_ms,
1016 threshold_ms,
1017 },
1018 format!(
1019 "Task execution is slow: {}ms average (threshold: {}ms)",
1020 avg_duration_ms, threshold_ms
1021 ),
1022 );
1023 if self.alert_manager.record_alert(alert) {
1024 alerts_triggered += 1;
1025 }
1026 }
1027 }
1028 }
1029
1030 if task.alert_config.alert_on_stuck {
1032 if let Some(stuck_duration) = task.is_stuck() {
1033 let expected_interval_secs = match &task.schedule {
1035 Schedule::Interval { every } => *every,
1036 #[cfg(feature = "cron")]
1037 Schedule::Crontab { .. } => 86400, #[cfg(feature = "solar")]
1039 Schedule::Solar { .. } => 86400, Schedule::OneTime { .. } => 0, };
1042
1043 let alert = Alert::new(
1044 task_name.to_string(),
1045 AlertLevel::Critical,
1046 AlertCondition::TaskStuck {
1047 idle_duration_seconds: stuck_duration.num_seconds(),
1048 expected_interval_seconds: expected_interval_secs,
1049 },
1050 format!(
1051 "Task is stuck: no execution for {}s (expected interval: {}s)",
1052 stuck_duration.num_seconds(),
1053 expected_interval_secs
1054 ),
1055 );
1056 if self.alert_manager.record_alert(alert) {
1057 alerts_triggered += 1;
1058 }
1059 }
1060 }
1061
1062 let health_result = task.check_health();
1064 if health_result.health.is_unhealthy() {
1065 let issues = health_result.health.get_issues();
1066 let alert = Alert::new(
1067 task_name.to_string(),
1068 AlertLevel::Critical,
1069 AlertCondition::TaskUnhealthy {
1070 issues: issues.clone(),
1071 },
1072 format!("Task is unhealthy: {}", issues.join(", ")),
1073 );
1074 if self.alert_manager.record_alert(alert) {
1075 alerts_triggered += 1;
1076 }
1077 }
1078
1079 alerts_triggered
1080 }
1081
1082 pub fn check_all_alerts(&mut self) -> usize {
1087 let task_names: Vec<String> = self
1088 .tasks
1089 .keys()
1090 .filter(|name| {
1091 if let Some(task) = self.tasks.get(*name) {
1092 task.enabled && task.alert_config.enabled
1093 } else {
1094 false
1095 }
1096 })
1097 .cloned()
1098 .collect();
1099
1100 let mut total_alerts = 0;
1101 for task_name in task_names {
1102 total_alerts += self.check_task_alerts(&task_name);
1103 }
1104 total_alerts
1105 }
1106
1107 pub fn get_retry_tasks(&self) -> Vec<&ScheduledTask> {
1109 self.tasks
1110 .values()
1111 .filter(|task| task.enabled && task.is_ready_for_retry())
1112 .collect()
1113 }
1114
1115 pub fn detect_crashed_tasks(&self) -> Vec<String> {
1123 self.tasks
1124 .iter()
1125 .filter(|(_, task)| task.detect_interrupted_execution())
1126 .map(|(name, _)| name.clone())
1127 .collect()
1128 }
1129
1130 pub fn recover_from_crash(&mut self) -> usize {
1152 let crashed_task_names = self.detect_crashed_tasks();
1153 let mut recovered_count = 0;
1154
1155 for task_name in crashed_task_names {
1156 if let Some(task) = self.tasks.get_mut(&task_name) {
1157 if let Some(duration) = task.recover_from_interruption() {
1158 eprintln!(
1159 "Recovered task '{}' from interrupted execution (was running for {}s)",
1160 task_name,
1161 duration.num_seconds()
1162 );
1163 recovered_count += 1;
1164 }
1165 }
1166 }
1167
1168 let _ = self.save_state();
1170
1171 recovered_count
1172 }
1173
1174 pub fn get_tasks_ready_for_crash_retry(&self) -> Vec<&ScheduledTask> {
1176 self.tasks
1177 .values()
1178 .filter(|task| task.enabled && task.is_ready_for_retry_after_crash())
1179 .collect()
1180 }
1181
1182 pub fn get_due_tasks(&self) -> Vec<&ScheduledTask> {
1183 self.tasks
1184 .values()
1185 .filter(|task| task.enabled && task.is_due().unwrap_or(false))
1186 .collect()
1187 }
1188
1189 pub fn get_due_tasks_by_priority(&self) -> Vec<&ScheduledTask> {
1219 let mut tasks: Vec<&ScheduledTask> = self
1220 .tasks
1221 .values()
1222 .filter(|task| task.enabled && task.is_due().unwrap_or(false))
1223 .collect();
1224
1225 tasks.sort_by(|a, b| {
1227 let priority_a = a.options.priority.unwrap_or(5);
1229 let priority_b = b.options.priority.unwrap_or(5);
1230
1231 match priority_b.cmp(&priority_a) {
1232 std::cmp::Ordering::Equal => {
1233 let next_a = a
1235 .schedule
1236 .next_run(a.last_run_at)
1237 .unwrap_or_else(|_| Utc::now());
1238 let next_b = b
1239 .schedule
1240 .next_run(b.last_run_at)
1241 .unwrap_or_else(|_| Utc::now());
1242 next_a.cmp(&next_b)
1243 }
1244 other => other,
1245 }
1246 });
1247
1248 tasks
1249 }
1250
1251 pub fn get_tasks_by_priority(&self) -> Vec<&ScheduledTask> {
1259 let mut tasks: Vec<&ScheduledTask> =
1260 self.tasks.values().filter(|task| task.enabled).collect();
1261
1262 tasks.sort_by(|a, b| {
1264 let priority_a = a.options.priority.unwrap_or(5);
1265 let priority_b = b.options.priority.unwrap_or(5);
1266 priority_b.cmp(&priority_a)
1267 });
1268
1269 tasks
1270 }
1271
1272 pub fn get_tasks_by_group(&self, group: &str) -> Vec<&ScheduledTask> {
1274 self.tasks
1275 .values()
1276 .filter(|task| task.is_in_group(group))
1277 .collect()
1278 }
1279
1280 pub fn get_tasks_by_tag(&self, tag: &str) -> Vec<&ScheduledTask> {
1282 self.tasks
1283 .values()
1284 .filter(|task| task.has_tag(tag))
1285 .collect()
1286 }
1287
1288 pub fn get_tasks_by_tags(&self, tags: &[&str]) -> Vec<&ScheduledTask> {
1290 self.tasks
1291 .values()
1292 .filter(|task| tags.iter().any(|tag| task.has_tag(tag)))
1293 .collect()
1294 }
1295
1296 pub fn get_tasks_with_all_tags(&self, tags: &[&str]) -> Vec<&ScheduledTask> {
1298 self.tasks
1299 .values()
1300 .filter(|task| tags.iter().all(|tag| task.has_tag(tag)))
1301 .collect()
1302 }
1303
1304 pub fn get_all_groups(&self) -> HashSet<String> {
1306 self.tasks
1307 .values()
1308 .filter_map(|task| task.group.clone())
1309 .collect()
1310 }
1311
1312 pub fn get_all_tags(&self) -> HashSet<String> {
1314 self.tasks
1315 .values()
1316 .flat_map(|task| task.tags.iter().cloned())
1317 .collect()
1318 }
1319
1320 pub fn enable_group(&mut self, group: &str) -> Result<usize, ScheduleError> {
1322 let mut count = 0;
1323 for task in self.tasks.values_mut() {
1324 if task.is_in_group(group) && !task.enabled {
1325 task.enabled = true;
1326 count += 1;
1327 }
1328 }
1329 if count > 0 {
1330 self.save_state()?;
1331 }
1332 Ok(count)
1333 }
1334
1335 pub fn disable_group(&mut self, group: &str) -> Result<usize, ScheduleError> {
1337 let mut count = 0;
1338 for task in self.tasks.values_mut() {
1339 if task.is_in_group(group) && task.enabled {
1340 task.enabled = false;
1341 count += 1;
1342 }
1343 }
1344 if count > 0 {
1345 self.save_state()?;
1346 }
1347 Ok(count)
1348 }
1349
1350 pub fn enable_tag(&mut self, tag: &str) -> Result<usize, ScheduleError> {
1352 let mut count = 0;
1353 for task in self.tasks.values_mut() {
1354 if task.has_tag(tag) && !task.enabled {
1355 task.enabled = true;
1356 count += 1;
1357 }
1358 }
1359 if count > 0 {
1360 self.save_state()?;
1361 }
1362 Ok(count)
1363 }
1364
1365 pub fn disable_tag(&mut self, tag: &str) -> Result<usize, ScheduleError> {
1367 let mut count = 0;
1368 for task in self.tasks.values_mut() {
1369 if task.has_tag(tag) && task.enabled {
1370 task.enabled = false;
1371 count += 1;
1372 }
1373 }
1374 if count > 0 {
1375 self.save_state()?;
1376 }
1377 Ok(count)
1378 }
1379
1380 pub fn check_all_tasks_health(&self) -> Vec<HealthCheckResult> {
1382 self.tasks
1383 .values()
1384 .map(|task| task.check_health())
1385 .collect()
1386 }
1387
1388 pub fn get_unhealthy_tasks(&self) -> Vec<HealthCheckResult> {
1390 self.tasks
1391 .values()
1392 .map(|task| task.check_health())
1393 .filter(|result| !result.health.is_healthy())
1394 .collect()
1395 }
1396
1397 pub fn detect_missed_schedules(&self, grace_period_seconds: u64) -> Vec<(String, Duration)> {
1409 let now = Utc::now();
1410 let grace_period = Duration::seconds(grace_period_seconds as i64);
1411 let mut missed = Vec::new();
1412
1413 for (name, task) in &self.tasks {
1414 if !task.enabled {
1415 continue;
1416 }
1417
1418 if let Ok(next_run) = task.schedule.next_run(task.last_run_at) {
1420 let deadline = next_run + grace_period;
1421
1422 if now > deadline {
1424 let missed_by = now - next_run;
1425 missed.push((name.clone(), missed_by));
1426 }
1427 }
1428 }
1429
1430 missed
1431 }
1432
1433 pub fn check_missed_schedules(&mut self, grace_period_seconds: Option<u64>) -> usize {
1444 let grace = grace_period_seconds.unwrap_or(60);
1445 let missed = self.detect_missed_schedules(grace);
1446 let mut alert_count = 0;
1447 let now = Utc::now();
1448
1449 for (task_name, missed_by) in missed {
1450 let expected_at = now - missed_by;
1452
1453 let alert = Alert {
1455 timestamp: now,
1456 task_name: task_name.clone(),
1457 level: AlertLevel::Warning,
1458 condition: AlertCondition::MissedSchedule {
1459 expected_at,
1460 detected_at: now,
1461 },
1462 message: format!(
1463 "Task missed its schedule by {} seconds",
1464 missed_by.num_seconds()
1465 ),
1466 metadata: HashMap::new(),
1467 };
1468
1469 if self.alert_manager.record_alert(alert) {
1470 alert_count += 1;
1471 }
1472 }
1473
1474 alert_count
1475 }
1476
1477 pub fn get_missed_schedule_stats(
1487 &self,
1488 grace_period_seconds: Option<u64>,
1489 ) -> Vec<(String, i64, String)> {
1490 let grace = grace_period_seconds.unwrap_or(60);
1491 let mut stats: Vec<(String, i64, String)> = self
1492 .detect_missed_schedules(grace)
1493 .into_iter()
1494 .map(|(name, missed_by)| {
1495 let schedule_type = if let Some(task) = self.tasks.get(&name) {
1496 format!("{}", task.schedule)
1497 } else {
1498 "Unknown".to_string()
1499 };
1500 (name, missed_by.num_seconds(), schedule_type)
1501 })
1502 .collect();
1503
1504 stats.sort_by_key(|b| std::cmp::Reverse(b.1));
1506
1507 stats
1508 }
1509
1510 pub fn get_tasks_with_warnings(&self) -> Vec<HealthCheckResult> {
1512 self.tasks
1513 .values()
1514 .map(|task| task.check_health())
1515 .filter(|result| result.health.has_warnings())
1516 .collect()
1517 }
1518
1519 pub fn get_tasks_with_errors(&self) -> Vec<HealthCheckResult> {
1521 self.tasks
1522 .values()
1523 .map(|task| task.check_health())
1524 .filter(|result| result.health.is_unhealthy())
1525 .collect()
1526 }
1527
1528 pub fn get_stuck_tasks(&self) -> Vec<&ScheduledTask> {
1530 self.tasks
1531 .values()
1532 .filter(|task| task.is_stuck().is_some())
1533 .collect()
1534 }
1535
1536 pub fn validate_all_schedules(&self) -> Vec<(String, Result<(), ScheduleError>)> {
1538 self.tasks
1539 .iter()
1540 .map(|(name, task)| (name.clone(), task.validate_schedule()))
1541 .collect()
1542 }
1543
1544 pub fn get_metrics(&self) -> SchedulerMetrics {
1546 SchedulerMetrics::from_scheduler(self)
1547 }
1548
1549 pub fn get_all_task_statistics(&self) -> Vec<TaskStatistics> {
1551 self.tasks.values().map(TaskStatistics::from_task).collect()
1552 }
1553
1554 pub fn get_task_statistics(&self, name: &str) -> Option<TaskStatistics> {
1556 self.tasks.get(name).map(TaskStatistics::from_task)
1557 }
1558
1559 pub fn get_group_statistics(&self, group: &str) -> Vec<TaskStatistics> {
1561 self.tasks
1562 .values()
1563 .filter(|task| task.is_in_group(group))
1564 .map(TaskStatistics::from_task)
1565 .collect()
1566 }
1567
1568 pub fn get_tag_statistics(&self, tag: &str) -> Vec<TaskStatistics> {
1570 self.tasks
1571 .values()
1572 .filter(|task| task.has_tag(tag))
1573 .map(TaskStatistics::from_task)
1574 .collect()
1575 }
1576
1577 pub fn has_circular_dependency(&self, task_name: &str) -> bool {
1579 let mut visited = HashSet::new();
1580 let mut stack = HashSet::new();
1581 self.has_circular_dependency_helper(task_name, &mut visited, &mut stack)
1582 }
1583
1584 fn has_circular_dependency_helper(
1585 &self,
1586 task_name: &str,
1587 visited: &mut HashSet<String>,
1588 stack: &mut HashSet<String>,
1589 ) -> bool {
1590 if stack.contains(task_name) {
1591 return true; }
1593
1594 if visited.contains(task_name) {
1595 return false; }
1597
1598 visited.insert(task_name.to_string());
1599 stack.insert(task_name.to_string());
1600
1601 if let Some(task) = self.tasks.get(task_name) {
1602 for dep in &task.dependencies {
1603 if self.has_circular_dependency_helper(dep, visited, stack) {
1604 return true;
1605 }
1606 }
1607 }
1608
1609 stack.remove(task_name);
1610 false
1611 }
1612
1613 pub fn get_dependency_chain(&self, task_name: &str) -> Result<Vec<String>, ScheduleError> {
1615 if self.has_circular_dependency(task_name) {
1616 return Err(ScheduleError::Invalid(format!(
1617 "Circular dependency detected for task '{}'",
1618 task_name
1619 )));
1620 }
1621
1622 let mut chain = Vec::new();
1623 let mut visited = HashSet::new();
1624 self.get_dependency_chain_helper(task_name, &mut chain, &mut visited);
1625 Ok(chain)
1626 }
1627
1628 fn get_dependency_chain_helper(
1629 &self,
1630 task_name: &str,
1631 chain: &mut Vec<String>,
1632 visited: &mut HashSet<String>,
1633 ) {
1634 if visited.contains(task_name) {
1635 return;
1636 }
1637
1638 visited.insert(task_name.to_string());
1639
1640 if let Some(task) = self.tasks.get(task_name) {
1641 for dep in &task.dependencies {
1642 self.get_dependency_chain_helper(dep, chain, visited);
1643 }
1644 }
1645
1646 chain.push(task_name.to_string());
1647 }
1648
1649 pub fn get_tasks_ready_with_dependencies(
1651 &self,
1652 completed_tasks: &HashSet<String>,
1653 failed_tasks: &HashSet<String>,
1654 ) -> Vec<&ScheduledTask> {
1655 self.tasks
1656 .values()
1657 .filter(|task| {
1658 if !task.enabled {
1659 return false;
1660 }
1661
1662 if !task.is_due().unwrap_or(false) {
1664 return false;
1665 }
1666
1667 if task.wait_for_dependencies {
1669 let status =
1670 task.check_dependencies_with_failures(completed_tasks, failed_tasks);
1671 status.is_satisfied()
1672 } else {
1673 true
1674 }
1675 })
1676 .collect()
1677 }
1678
1679 pub fn get_tasks_waiting_for_dependencies(
1681 &self,
1682 completed_tasks: &HashSet<String>,
1683 ) -> Vec<(&ScheduledTask, DependencyStatus)> {
1684 self.tasks
1685 .values()
1686 .filter_map(|task| {
1687 if task.enabled && task.has_dependencies() {
1688 let status = task.check_dependencies(completed_tasks);
1689 if !status.is_satisfied() {
1690 return Some((task, status));
1691 }
1692 }
1693 None
1694 })
1695 .collect()
1696 }
1697
1698 pub fn get_tasks_with_failed_dependencies(
1700 &self,
1701 completed_tasks: &HashSet<String>,
1702 failed_tasks: &HashSet<String>,
1703 ) -> Vec<(&ScheduledTask, DependencyStatus)> {
1704 self.tasks
1705 .values()
1706 .filter_map(|task| {
1707 if task.enabled && task.has_dependencies() {
1708 let status =
1709 task.check_dependencies_with_failures(completed_tasks, failed_tasks);
1710 if status.has_failures() {
1711 return Some((task, status));
1712 }
1713 }
1714 None
1715 })
1716 .collect()
1717 }
1718
1719 pub fn validate_dependencies(&self) -> Result<(), ScheduleError> {
1721 for (task_name, task) in &self.tasks {
1722 if self.has_circular_dependency(task_name) {
1724 return Err(ScheduleError::Invalid(format!(
1725 "Circular dependency detected for task '{}'",
1726 task_name
1727 )));
1728 }
1729
1730 for dep in &task.dependencies {
1732 if !self.tasks.contains_key(dep) {
1733 return Err(ScheduleError::Invalid(format!(
1734 "Task '{}' depends on non-existent task '{}'",
1735 task_name, dep
1736 )));
1737 }
1738 }
1739 }
1740
1741 Ok(())
1742 }
1743
1744 pub async fn heartbeat_tick(
1751 &self,
1752 ) -> Result<Option<crate::heartbeat::BeatRole>, ScheduleError> {
1753 if let Some(ref hb) = self.heartbeat {
1754 hb.tick()
1755 .await
1756 .map_err(|e| ScheduleError::Invalid(format!("Heartbeat tick error: {}", e)))?;
1757 Ok(Some(hb.role().await))
1758 } else {
1759 Ok(None)
1760 }
1761 }
1762
1763 pub async fn update_heartbeat_info(&self) {
1768 if let Some(ref hb) = self.heartbeat {
1769 let schedule_count = self.tasks.len();
1770 let next_due = self.get_due_tasks().first().map(|t| t.name.clone());
1771 hb.update_info(schedule_count, next_due).await;
1772 }
1773 }
1774
1775 pub async fn get_leader_due_tasks(&self) -> Vec<&ScheduledTask> {
1781 if !self.is_leader().await {
1782 return Vec::new();
1783 }
1784 self.get_due_tasks()
1785 }
1786
1787 pub async fn get_leader_due_tasks_by_priority(&self) -> Vec<&ScheduledTask> {
1793 if !self.is_leader().await {
1794 return Vec::new();
1795 }
1796 self.get_due_tasks_by_priority()
1797 }
1798
1799 pub async fn tick(&mut self) -> Result<Vec<String>, ScheduleError> {
1809 let role = self.heartbeat_tick().await?;
1811
1812 let is_leader = match role {
1814 Some(crate::heartbeat::BeatRole::Leader) => true,
1815 Some(_) => false,
1816 None => true, };
1818
1819 if !is_leader {
1820 self.update_heartbeat_info().await;
1822 return Ok(Vec::new());
1823 }
1824
1825 let due_task_names: Vec<String> = self
1827 .get_due_tasks_by_priority()
1828 .iter()
1829 .map(|t| t.name.clone())
1830 .collect();
1831
1832 for name in &due_task_names {
1834 let _ = self.mark_task_run(name);
1835 }
1836
1837 self.update_heartbeat_info().await;
1839
1840 let _ = self.save_state();
1842
1843 Ok(due_task_names)
1844 }
1845
1846 pub async fn shutdown_graceful(&mut self) -> Result<(), ScheduleError> {
1851 if let Some(ref hb) = self.heartbeat {
1852 hb.shutdown()
1853 .await
1854 .map_err(|e| ScheduleError::Invalid(format!("Heartbeat shutdown error: {}", e)))?;
1855 }
1856 self.save_state()?;
1857 Ok(())
1858 }
1859}
1860
1861impl Default for BeatScheduler {
1862 fn default() -> Self {
1863 Self::new()
1864 }
1865}