Skip to main content

celers_beat/
scheduler.rs

1//! Beat scheduler and related types
2//!
3//! Contains the main `BeatScheduler` struct and its core implementation,
4//! along with conflict detection, scheduler metrics, and statistics types.
5
6use 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/// Schedule conflict severity level
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23pub enum ConflictSeverity {
24    /// Low priority - tasks can run concurrently
25    Low,
26    /// Medium priority - tasks may interfere
27    Medium,
28    /// High priority - tasks will definitely conflict
29    High,
30}
31
32/// Represents a conflict between two scheduled tasks
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ScheduleConflict {
35    /// First task name
36    pub task1: String,
37    /// Second task name
38    pub task2: String,
39    /// Conflict severity
40    pub severity: ConflictSeverity,
41    /// Time window where conflict occurs (in seconds)
42    pub overlap_seconds: u64,
43    /// Description of the conflict
44    pub description: String,
45    /// Suggested resolution
46    pub resolution: Option<String>,
47}
48
49impl ScheduleConflict {
50    /// Create a new schedule conflict
51    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    /// Add a suggested resolution
69    pub fn with_resolution(mut self, resolution: String) -> Self {
70        self.resolution = Some(resolution);
71        self
72    }
73
74    /// Check if this is a high severity conflict
75    pub fn is_high_severity(&self) -> bool {
76        self.severity == ConflictSeverity::High
77    }
78
79    /// Check if this is a medium severity conflict
80    pub fn is_medium_severity(&self) -> bool {
81        self.severity == ConflictSeverity::Medium
82    }
83
84    /// Check if this is a low severity conflict
85    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/// Scheduler statistics and metrics
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SchedulerMetrics {
103    /// Total number of registered tasks
104    pub total_tasks: usize,
105    /// Number of enabled tasks
106    pub enabled_tasks: usize,
107    /// Number of disabled tasks
108    pub disabled_tasks: usize,
109    /// Number of tasks that have executed at least once
110    pub tasks_with_executions: usize,
111    /// Total number of successful executions across all tasks
112    pub total_successes: u64,
113    /// Total number of failed executions across all tasks
114    pub total_failures: u64,
115    /// Total number of timeouts across all tasks
116    pub total_timeouts: u64,
117    /// Total execution count across all tasks
118    pub total_executions: u64,
119    /// Overall success rate (0.0 to 1.0)
120    pub overall_success_rate: f64,
121    /// Number of tasks currently in retry state
122    pub tasks_in_retry: usize,
123    /// Number of tasks with health warnings
124    pub tasks_with_warnings: usize,
125    /// Number of unhealthy tasks
126    pub unhealthy_tasks: usize,
127    /// Number of stuck tasks
128    pub stuck_tasks: usize,
129}
130
131impl SchedulerMetrics {
132    /// Create metrics from a BeatScheduler
133    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/// Per-task statistics
197#[derive(Debug, Clone)]
198pub struct TaskStatistics {
199    /// Task name
200    pub name: String,
201    /// Total successful executions (from history)
202    pub success_count: usize,
203    /// Total failed executions (from history)
204    pub failure_count: usize,
205    /// Total timeout executions (from history)
206    pub timeout_count: usize,
207    /// Average execution duration in milliseconds
208    pub average_duration_ms: Option<u64>,
209    /// Minimum execution duration in milliseconds
210    pub min_duration_ms: Option<u64>,
211    /// Maximum execution duration in milliseconds
212    pub max_duration_ms: Option<u64>,
213    /// Success rate from history (0.0 to 1.0)
214    pub success_rate: f64,
215    /// Overall failure rate including retries (0.0 to 1.0)
216    pub failure_rate: f64,
217    /// Current retry count
218    pub retry_count: u32,
219    /// Is task currently stuck
220    pub is_stuck: bool,
221}
222
223impl TaskStatistics {
224    /// Create statistics from a ScheduledTask
225    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/// Beat scheduler
243#[derive(Serialize, Deserialize)]
244pub struct BeatScheduler {
245    /// Registered scheduled tasks
246    pub(crate) tasks: HashMap<String, ScheduledTask>,
247
248    /// Optional state file path for persistence
249    #[serde(skip)]
250    pub(crate) state_file: Option<PathBuf>,
251
252    /// Failure notification callbacks
253    #[serde(skip)]
254    pub(crate) failure_callbacks: Vec<FailureCallback>,
255
256    /// Lock manager for preventing duplicate execution
257    #[serde(default)]
258    pub(crate) lock_manager: LockManager,
259
260    /// Scheduler instance ID for lock ownership
261    #[serde(skip)]
262    pub(crate) instance_id: String,
263
264    /// Alert manager for monitoring and notifications
265    #[serde(default)]
266    pub(crate) alert_manager: AlertManager,
267
268    /// Optional distributed lock backend for cross-instance coordination.
269    ///
270    /// When set, the scheduler will use this backend instead of the local
271    /// `LockManager` for all lock operations, enabling distributed locking
272    /// across multiple scheduler instances.
273    #[serde(skip)]
274    pub(crate) distributed_lock_backend: Option<Arc<dyn DistributedLockBackend>>,
275
276    /// Optional heartbeat manager for leader election and failover.
277    ///
278    /// When set, the scheduler participates in multi-instance coordination
279    /// where only the leader executes scheduled tasks.
280    #[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    /// Create scheduler with persistent state file
303    ///
304    /// # Arguments
305    /// * `state_file` - Path to JSON file for persisting scheduler state
306    ///
307    /// # Example
308    /// ```no_run
309    /// use celers_beat::BeatScheduler;
310    ///
311    /// let mut scheduler = BeatScheduler::with_persistence("schedules.json");
312    /// // Scheduler will automatically save state to schedules.json on updates
313    /// ```
314    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    /// Load scheduler state from file
332    ///
333    /// Creates a new scheduler with tasks loaded from the specified file.
334    /// If the file doesn't exist or can't be read, returns an empty scheduler.
335    ///
336    /// # Arguments
337    /// * `path` - Path to the state file
338    ///
339    /// # Returns
340    /// Scheduler loaded from file, or empty scheduler if file doesn't exist
341    pub fn load_from_file<P: Into<PathBuf>>(path: P) -> Result<Self, ScheduleError> {
342        let path = path.into();
343
344        if !path.exists() {
345            // File doesn't exist, return new scheduler with persistence enabled
346            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        // Set state file and generate instance ID
370        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    /// Set the distributed lock backend for cross-instance coordination.
382    ///
383    /// When a distributed lock backend is set, the scheduler will use it
384    /// for all lock operations. This enables distributed locking across
385    /// multiple scheduler instances (e.g., using Redis or a database).
386    ///
387    /// # Arguments
388    /// * `backend` - The distributed lock backend implementation
389    ///
390    /// # Example
391    /// ```
392    /// use celers_beat::BeatScheduler;
393    /// use celers_beat::lock::InMemoryLockBackend;
394    /// use std::sync::Arc;
395    ///
396    /// let mut scheduler = BeatScheduler::new();
397    /// let backend = Arc::new(InMemoryLockBackend::new());
398    /// scheduler.with_lock_backend(backend);
399    /// ```
400    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    /// Get a reference to the distributed lock backend, if set.
406    pub fn distributed_lock_backend(&self) -> Option<&Arc<dyn DistributedLockBackend>> {
407        self.distributed_lock_backend.as_ref()
408    }
409
410    /// Set the heartbeat manager for leader election and failover.
411    ///
412    /// When a heartbeat is configured, the scheduler participates in
413    /// multi-instance coordination. Only the leader instance executes
414    /// scheduled tasks; standby instances remain idle until failover.
415    pub fn with_heartbeat(&mut self, heartbeat: BeatHeartbeat) -> &mut Self {
416        self.heartbeat = Some(heartbeat);
417        self
418    }
419
420    /// Get a reference to the heartbeat manager, if configured.
421    pub fn heartbeat(&self) -> Option<&BeatHeartbeat> {
422        self.heartbeat.as_ref()
423    }
424
425    /// Check if this scheduler instance is the leader.
426    ///
427    /// Returns `true` if no heartbeat is configured (single-instance mode)
428    /// or if this instance holds the leader lock.
429    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    /// Try to acquire a distributed lock for a task (async version).
437    ///
438    /// If a distributed lock backend is set, uses it. Otherwise falls back
439    /// to the local `LockManager`.
440    ///
441    /// # Arguments
442    /// * `task_name` - Name of the task to lock
443    /// * `ttl_secs` - TTL for the lock in seconds
444    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    /// Release a distributed lock for a task (async version).
461    ///
462    /// If a distributed lock backend is set, uses it. Otherwise falls back
463    /// to the local `LockManager`.
464    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    /// Renew a distributed lock for a task (async version).
479    ///
480    /// If a distributed lock backend is set, uses it. Otherwise falls back
481    /// to the local `LockManager`.
482    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    /// Check if a task is locked via the distributed backend (async version).
499    ///
500    /// If a distributed lock backend is set, uses it. Otherwise falls back
501    /// to the local `LockManager`.
502    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    /// Release all distributed locks owned by this scheduler instance (async).
514    ///
515    /// Useful during graceful shutdown to release all held locks.
516    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    /// Save scheduler state to file
530    ///
531    /// Persists the current scheduler state (all tasks and their run history)
532    /// to the configured state file. If no state file is configured, this is a no-op.
533    ///
534    /// # Returns
535    /// Ok(()) if successful or no state file configured
536    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    /// Export scheduler state as JSON string
550    ///
551    /// Returns the complete scheduler state serialized as a JSON string.
552    /// This is useful for debugging, backup, or exporting to external systems.
553    ///
554    /// # Example
555    /// ```
556    /// use celers_beat::{BeatScheduler, Schedule, ScheduledTask};
557    ///
558    /// let mut scheduler = BeatScheduler::new();
559    /// scheduler.add_task(ScheduledTask::new("test".to_string(), Schedule::interval(60))).unwrap();
560    ///
561    /// let json = scheduler.export_state().unwrap();
562    /// assert!(json.contains("test"));
563    /// ```
564    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    /// List all scheduled tasks
570    ///
571    /// Returns a reference to the internal task HashMap, allowing iteration
572    /// over all scheduled tasks.
573    ///
574    /// # Example
575    /// ```
576    /// use celers_beat::{BeatScheduler, Schedule, ScheduledTask};
577    ///
578    /// let mut scheduler = BeatScheduler::new();
579    /// scheduler.add_task(ScheduledTask::new("task1".to_string(), Schedule::interval(60))).unwrap();
580    /// scheduler.add_task(ScheduledTask::new("task2".to_string(), Schedule::interval(120))).unwrap();
581    ///
582    /// let tasks = scheduler.list_tasks();
583    /// assert_eq!(tasks.len(), 2);
584    /// assert!(tasks.contains_key("task1"));
585    /// assert!(tasks.contains_key("task2"));
586    /// ```
587    pub fn list_tasks(&self) -> &HashMap<String, ScheduledTask> {
588        &self.tasks
589    }
590
591    /// Get a specific task by name
592    ///
593    /// # Example
594    /// ```
595    /// use celers_beat::{BeatScheduler, Schedule, ScheduledTask};
596    ///
597    /// let mut scheduler = BeatScheduler::new();
598    /// scheduler.add_task(ScheduledTask::new("test".to_string(), Schedule::interval(60))).unwrap();
599    ///
600    /// let task = scheduler.get_task("test");
601    /// assert!(task.is_some());
602    /// assert_eq!(task.unwrap().name, "test");
603    /// ```
604    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        // Initialize the next run cache when adding the task
610        task.update_next_run_cache();
611        self.tasks.insert(task.name.clone(), task);
612        self.save_state()?;
613        Ok(())
614    }
615
616    /// Add multiple tasks in a batch operation
617    ///
618    /// This is more efficient than adding tasks individually as it only saves
619    /// state once after all tasks are added.
620    ///
621    /// # Arguments
622    /// * `tasks` - Vector of tasks to add
623    ///
624    /// # Returns
625    /// Number of tasks successfully added
626    ///
627    /// # Example
628    /// ```
629    /// use celers_beat::{BeatScheduler, Schedule, ScheduledTask};
630    ///
631    /// let mut scheduler = BeatScheduler::new();
632    /// let tasks = vec![
633    ///     ScheduledTask::new("task1".to_string(), Schedule::interval(60)),
634    ///     ScheduledTask::new("task2".to_string(), Schedule::interval(120)),
635    ///     ScheduledTask::new("task3".to_string(), Schedule::interval(180)),
636    /// ];
637    ///
638    /// let count = scheduler.add_tasks_batch(tasks).unwrap();
639    /// assert_eq!(count, 3);
640    /// ```
641    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            // Initialize the next run cache when adding the task
646            task.update_next_run_cache();
647            self.tasks.insert(task.name.clone(), task);
648            added_count += 1;
649        }
650
651        // Save state only once after all tasks are added
652        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    /// Remove multiple tasks in a batch operation
666    ///
667    /// This is more efficient than removing tasks individually as it only saves
668    /// state once after all tasks are removed.
669    ///
670    /// # Arguments
671    /// * `names` - Slice of task names to remove
672    ///
673    /// # Returns
674    /// Number of tasks successfully removed
675    ///
676    /// # Example
677    /// ```
678    /// use celers_beat::{BeatScheduler, Schedule, ScheduledTask};
679    ///
680    /// let mut scheduler = BeatScheduler::new();
681    /// scheduler.add_task(ScheduledTask::new("task1".to_string(), Schedule::interval(60))).unwrap();
682    /// scheduler.add_task(ScheduledTask::new("task2".to_string(), Schedule::interval(120))).unwrap();
683    /// scheduler.add_task(ScheduledTask::new("task3".to_string(), Schedule::interval(180))).unwrap();
684    ///
685    /// let count = scheduler.remove_tasks_batch(&["task1", "task2"]).unwrap();
686    /// assert_eq!(count, 2);
687    /// ```
688    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        // Save state only once after all tasks are removed
698        if removed_count > 0 {
699            self.save_state()?;
700        }
701
702        Ok(removed_count)
703    }
704
705    /// Update task execution state (called after task runs)
706    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    /// Mark task execution as successful
716    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            // Add execution record
724            let record = ExecutionRecord::completed(now, ExecutionResult::Success);
725            task.add_execution_record(record);
726
727            // Update next run cache after execution
728            task.update_next_run_cache();
729
730            // Check if this is a one-time schedule
731            task.schedule.is_onetime()
732        } else {
733            false
734        };
735
736        // Remove one-time schedules after successful execution
737        if should_remove {
738            self.tasks.remove(name);
739        }
740
741        self.save_state()?;
742        Ok(())
743    }
744
745    /// Mark task execution as successful with custom start time
746    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            // Add execution record with actual start time
758            let record = ExecutionRecord::completed(started_at, ExecutionResult::Success);
759            task.add_execution_record(record);
760
761            // Update next run cache after execution
762            task.update_next_run_cache();
763
764            // Check if this is a one-time schedule
765            task.schedule.is_onetime()
766        } else {
767            false
768        };
769
770        // Remove one-time schedules after successful execution
771        if should_remove {
772            self.tasks.remove(name);
773        }
774
775        self.save_state()?;
776        Ok(())
777    }
778
779    /// Mark task execution as failed
780    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    /// Mark task execution as failed with error message
785    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            // Add execution record
795            let record = ExecutionRecord::completed(
796                now,
797                ExecutionResult::Failure {
798                    error: error.clone(),
799                },
800            );
801            task.add_execution_record(record);
802
803            // Invoke failure callbacks
804            self.invoke_failure_callbacks(name, &error);
805
806            self.save_state()?;
807        }
808        Ok(())
809    }
810
811    /// Mark task execution as failed with custom start time
812    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            // Add execution record with actual start time
822            let record = ExecutionRecord::completed(
823                started_at,
824                ExecutionResult::Failure {
825                    error: error.clone(),
826                },
827            );
828            task.add_execution_record(record);
829
830            // Invoke failure callbacks
831            self.invoke_failure_callbacks(name, &error);
832
833            self.save_state()?;
834        }
835        Ok(())
836    }
837
838    /// Mark task execution as timed out
839    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            // Add execution record
848            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    /// Register a failure notification callback
857    ///
858    /// The callback will be invoked whenever a task execution fails.
859    ///
860    /// # Arguments
861    /// * `callback` - Callback function that receives task name and error message
862    ///
863    /// # Example
864    /// ```
865    /// use celers_beat::BeatScheduler;
866    /// use std::sync::Arc;
867    ///
868    /// let mut scheduler = BeatScheduler::new();
869    /// scheduler.on_failure(Arc::new(|task_name, error| {
870    ///     eprintln!("Task {} failed: {}", task_name, error);
871    /// }));
872    /// ```
873    pub fn on_failure(&mut self, callback: FailureCallback) {
874        self.failure_callbacks.push(callback);
875    }
876
877    /// Clear all failure notification callbacks
878    pub fn clear_failure_callbacks(&mut self) {
879        self.failure_callbacks.clear();
880    }
881
882    /// Invoke all registered failure callbacks
883    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    /// Register an alert callback
890    ///
891    /// The callback will be invoked whenever an alert is triggered.
892    ///
893    /// # Arguments
894    /// * `callback` - Callback function that receives alert details
895    ///
896    /// # Example
897    /// ```
898    /// use celers_beat::BeatScheduler;
899    /// use std::sync::Arc;
900    ///
901    /// let mut scheduler = BeatScheduler::new();
902    /// scheduler.on_alert(Arc::new(|alert| {
903    ///     eprintln!("ALERT: {}", alert);
904    /// }));
905    /// ```
906    pub fn on_alert(&mut self, callback: AlertCallback) {
907        self.alert_manager.add_callback(callback);
908    }
909
910    /// Get all alerts
911    pub fn get_alerts(&self) -> &[Alert] {
912        self.alert_manager.get_alerts()
913    }
914
915    /// Get critical alerts
916    pub fn get_critical_alerts(&self) -> Vec<&Alert> {
917        self.alert_manager.get_critical_alerts()
918    }
919
920    /// Get warning alerts
921    pub fn get_warning_alerts(&self) -> Vec<&Alert> {
922        self.alert_manager.get_warning_alerts()
923    }
924
925    /// Get alerts for a specific task
926    pub fn get_task_alerts(&self, task_name: &str) -> Vec<&Alert> {
927        self.alert_manager.get_task_alerts(task_name)
928    }
929
930    /// Get recent alerts within specified seconds
931    pub fn get_recent_alerts(&self, seconds: i64) -> Vec<&Alert> {
932        self.alert_manager.get_recent_alerts(seconds)
933    }
934
935    /// Clear all alerts
936    pub fn clear_alerts(&mut self) {
937        self.alert_manager.clear();
938    }
939
940    /// Clear alerts for a specific task
941    pub fn clear_task_alerts(&mut self, task_name: &str) {
942        self.alert_manager.clear_task_alerts(task_name);
943    }
944
945    /// Check alert conditions for a task and trigger alerts if needed
946    ///
947    /// This should be called periodically or after task execution to monitor for alert conditions.
948    ///
949    /// # Arguments
950    /// * `task_name` - Name of the task to check
951    ///
952    /// # Returns
953    /// Number of alerts triggered
954    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        // Check consecutive failures
967        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        // Check failure rate
987        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        // Check slow execution
1008        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        // Check if task is stuck
1031        if task.alert_config.alert_on_stuck {
1032            if let Some(stuck_duration) = task.is_stuck() {
1033                // Calculate expected interval based on schedule type
1034                let expected_interval_secs = match &task.schedule {
1035                    Schedule::Interval { every } => *every,
1036                    #[cfg(feature = "cron")]
1037                    Schedule::Crontab { .. } => 86400, // Assume daily
1038                    #[cfg(feature = "solar")]
1039                    Schedule::Solar { .. } => 86400, // Daily
1040                    Schedule::OneTime { .. } => 0, // Won't be stuck
1041                };
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        // Check health status
1063        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    /// Check alert conditions for all enabled tasks
1083    ///
1084    /// # Returns
1085    /// Total number of alerts triggered across all tasks
1086    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    /// Get tasks that are ready for retry
1108    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    /// Detect tasks with interrupted executions (crash recovery)
1116    ///
1117    /// Scans all tasks to find those that were marked as running but appear
1118    /// to have been interrupted (e.g., due to scheduler crash).
1119    ///
1120    /// # Returns
1121    /// Vector of task names that have interrupted executions
1122    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    /// Recover from crash by handling all interrupted task executions
1131    ///
1132    /// This method should be called after loading scheduler state to detect
1133    /// and recover from any interrupted executions (e.g., after a crash).
1134    ///
1135    /// # Returns
1136    /// Number of tasks recovered from interruption
1137    ///
1138    /// # Example
1139    /// ```
1140    /// use celers_beat::BeatScheduler;
1141    ///
1142    /// // Load scheduler from persistent state
1143    /// let mut scheduler = BeatScheduler::load_from_file("schedules.json").unwrap();
1144    ///
1145    /// // Automatically recover from any crashes
1146    /// let recovered = scheduler.recover_from_crash();
1147    /// if recovered > 0 {
1148    ///     eprintln!("Recovered {} tasks from interrupted executions", recovered);
1149    /// }
1150    /// ```
1151    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        // Save state after recovery
1169        let _ = self.save_state();
1170
1171        recovered_count
1172    }
1173
1174    /// Get tasks that need retry after crash recovery
1175    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    /// Get due tasks sorted by priority (highest priority first)
1190    ///
1191    /// This method returns tasks that are due for execution, ordered by their priority.
1192    /// Higher priority tasks (higher numeric value) are returned first, allowing for
1193    /// priority-based execution scheduling.
1194    ///
1195    /// # Returns
1196    /// Vector of tasks sorted by priority (descending), then by next run time (ascending)
1197    ///
1198    /// # Example
1199    /// ```
1200    /// use celers_beat::{BeatScheduler, Schedule, ScheduledTask};
1201    ///
1202    /// let mut scheduler = BeatScheduler::new();
1203    ///
1204    /// // Add high priority task
1205    /// let mut high_priority = ScheduledTask::new("critical".to_string(), Schedule::interval(60));
1206    /// high_priority.options.priority = Some(9);
1207    /// scheduler.add_task(high_priority).unwrap();
1208    ///
1209    /// // Add low priority task
1210    /// let mut low_priority = ScheduledTask::new("background".to_string(), Schedule::interval(60));
1211    /// low_priority.options.priority = Some(1);
1212    /// scheduler.add_task(low_priority).unwrap();
1213    ///
1214    /// // Get tasks ordered by priority
1215    /// let due_tasks = scheduler.get_due_tasks_by_priority();
1216    /// // The critical task will be first
1217    /// ```
1218    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        // Sort by priority (descending), then by next run time (ascending)
1226        tasks.sort_by(|a, b| {
1227            // Higher priority comes first (reverse order)
1228            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                    // If same priority, sort by next run time
1234                    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    /// Get tasks ordered by priority regardless of due status
1252    ///
1253    /// This method returns all enabled tasks sorted by priority, which is useful for
1254    /// understanding task execution order and for manual task management.
1255    ///
1256    /// # Returns
1257    /// Vector of all enabled tasks sorted by priority (descending)
1258    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        // Sort by priority (descending)
1263        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    /// Get all tasks in a specific group
1273    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    /// Get all tasks with a specific tag
1281    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    /// Get all tasks with any of the specified tags
1289    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    /// Get all tasks with all of the specified tags
1297    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    /// Get all unique groups
1305    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    /// Get all unique tags
1313    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    /// Enable all tasks in a group
1321    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    /// Disable all tasks in a group
1336    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    /// Enable all tasks with a specific tag
1351    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    /// Disable all tasks with a specific tag
1366    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    /// Check health of all tasks
1381    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    /// Get unhealthy tasks (with warnings or errors)
1389    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    /// Detect tasks with missed schedules
1398    ///
1399    /// A schedule is considered "missed" if the task's next scheduled run time has passed
1400    /// but the task hasn't executed yet. This can happen if the scheduler was down or
1401    /// if task execution was delayed.
1402    ///
1403    /// # Arguments
1404    /// * `grace_period_seconds` - Additional time to allow before considering a schedule missed
1405    ///
1406    /// # Returns
1407    /// Vector of (task_name, missed_time) tuples, where missed_time is how long ago the task should have run
1408    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            // Calculate next run time
1419            if let Ok(next_run) = task.schedule.next_run(task.last_run_at) {
1420                let deadline = next_run + grace_period;
1421
1422                // Check if we've passed the deadline
1423                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    /// Check for missed schedules and trigger alerts
1434    ///
1435    /// This method combines missed schedule detection with the alerting system,
1436    /// automatically creating alerts for tasks that have missed their schedules.
1437    ///
1438    /// # Arguments
1439    /// * `grace_period_seconds` - Grace period before considering a schedule missed (default: 60)
1440    ///
1441    /// # Returns
1442    /// Number of alerts triggered
1443    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            // Calculate expected run time (now - missed_by)
1451            let expected_at = now - missed_by;
1452
1453            // Create alert for missed schedule
1454            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    /// Get statistics on missed schedules
1478    ///
1479    /// Returns detailed information about which tasks have missed schedules and by how much.
1480    ///
1481    /// # Arguments
1482    /// * `grace_period_seconds` - Grace period in seconds (default: 60)
1483    ///
1484    /// # Returns
1485    /// Vector of (task_name, seconds_missed, schedule_type) tuples sorted by severity
1486    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        // Sort by seconds missed (descending)
1505        stats.sort_by_key(|b| std::cmp::Reverse(b.1));
1506
1507        stats
1508    }
1509
1510    /// Get tasks with health warnings
1511    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    /// Get tasks with health errors
1520    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    /// Get stuck tasks (tasks that haven't executed in expected time)
1529    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    /// Validate all task schedules
1537    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    /// Get scheduler metrics and statistics
1545    pub fn get_metrics(&self) -> SchedulerMetrics {
1546        SchedulerMetrics::from_scheduler(self)
1547    }
1548
1549    /// Get statistics for all tasks
1550    pub fn get_all_task_statistics(&self) -> Vec<TaskStatistics> {
1551        self.tasks.values().map(TaskStatistics::from_task).collect()
1552    }
1553
1554    /// Get statistics for a specific task
1555    pub fn get_task_statistics(&self, name: &str) -> Option<TaskStatistics> {
1556        self.tasks.get(name).map(TaskStatistics::from_task)
1557    }
1558
1559    /// Get statistics for tasks in a specific group
1560    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    /// Get statistics for tasks with a specific tag
1569    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    /// Check for circular dependencies
1578    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; // Circular dependency detected
1592        }
1593
1594        if visited.contains(task_name) {
1595            return false; // Already processed this path
1596        }
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    /// Get dependency chain for a task (all tasks it depends on, recursively)
1614    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    /// Get tasks that are ready to run (dependencies satisfied)
1650    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                // Check basic schedule readiness
1663                if !task.is_due().unwrap_or(false) {
1664                    return false;
1665                }
1666
1667                // Check dependencies if enabled
1668                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    /// Get tasks waiting for dependencies
1680    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    /// Get tasks with failed dependencies
1699    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    /// Validate all task dependencies (check for circular dependencies and missing tasks)
1720    pub fn validate_dependencies(&self) -> Result<(), ScheduleError> {
1721        for (task_name, task) in &self.tasks {
1722            // Check for circular dependencies
1723            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            // Check for missing dependencies
1731            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    // ===== Heartbeat-Aware Execution Methods =====
1745
1746    /// Execute one tick of the heartbeat (if configured).
1747    ///
1748    /// Returns the current role after the tick, or `None` if no heartbeat
1749    /// is configured (single-instance mode).
1750    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    /// Update heartbeat info with current scheduler state.
1764    ///
1765    /// Should be called periodically to keep heartbeat metadata fresh.
1766    /// This is a no-op if no heartbeat is configured.
1767    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    /// Get due tasks, but only if this instance is the leader.
1776    ///
1777    /// Returns an empty vector if in standby mode (heartbeat configured but
1778    /// not leader). Returns all due tasks if no heartbeat is configured
1779    /// (single-instance mode).
1780    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    /// Get due tasks by priority, but only if this instance is the leader.
1788    ///
1789    /// Returns an empty vector if in standby mode (heartbeat configured but
1790    /// not leader). Returns all due tasks sorted by priority if no heartbeat
1791    /// is configured (single-instance mode).
1792    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    /// Perform a full scheduler tick:
1800    ///
1801    /// 1. Tick heartbeat (leader election / lease renewal)
1802    /// 2. If leader, get due tasks by priority
1803    /// 3. Mark due tasks as running
1804    /// 4. Update heartbeat info
1805    /// 5. Auto-save state if persistence is configured
1806    ///
1807    /// Returns the list of due task names (empty if this instance is standby).
1808    pub async fn tick(&mut self) -> Result<Vec<String>, ScheduleError> {
1809        // Step 1: Heartbeat tick
1810        let role = self.heartbeat_tick().await?;
1811
1812        // Step 2: Check if we should execute
1813        let is_leader = match role {
1814            Some(crate::heartbeat::BeatRole::Leader) => true,
1815            Some(_) => false,
1816            None => true, // No heartbeat = single instance, always execute
1817        };
1818
1819        if !is_leader {
1820            // Update heartbeat info even in standby mode
1821            self.update_heartbeat_info().await;
1822            return Ok(Vec::new());
1823        }
1824
1825        // Step 3: Get due tasks
1826        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        // Step 4: Mark tasks as running
1833        for name in &due_task_names {
1834            let _ = self.mark_task_run(name);
1835        }
1836
1837        // Step 5: Update heartbeat info
1838        self.update_heartbeat_info().await;
1839
1840        // Step 6: Auto-save state if persistence configured
1841        let _ = self.save_state();
1842
1843        Ok(due_task_names)
1844    }
1845
1846    /// Gracefully shutdown the scheduler.
1847    ///
1848    /// Releases the leader lock if this instance is the leader, saves
1849    /// state, and releases all distributed locks.
1850    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}