Skip to main content

foxtive_supervisor/runtime/
core.rs

1//! Core TaskRuntime implementation
2//!
3//! This module contains the [`TaskRuntime`] struct, which is the central component
4//! for managing and orchestrating supervised tasks. It handles task registration,
5//! dependency resolution, prerequisite execution, and the spawning of supervision loops.
6
7use super::supervision::{SupervisionParams, supervise};
8use super::types::{DepSetupReceivers, PrerequisiteFuture, SupervisionResult, TaskEntry};
9use super::validation::validate_dependencies;
10use crate::contracts::{SupervisedTask, SupervisorEventListener};
11use crate::enums::{ControlMessage, HealthStatus, SupervisorEvent, TaskConfig};
12use crate::error::{SupervisorError, SupervisorResult};
13use crate::persistence::TaskStateStore;
14use std::collections::{HashMap, HashSet};
15use std::fmt;
16use std::future::Future;
17use std::sync::Arc;
18use std::time::Duration;
19use tokio::sync::{RwLock, Semaphore, broadcast, watch};
20use tokio::task::JoinHandle;
21use tracing::{error, info, warn};
22
23#[cfg(feature = "cron")]
24use foxtive_cron::Cron;
25
26/// The core runtime for managing supervised tasks.
27///
28/// `TaskRuntime` is responsible for:
29/// - Registering tasks and their configurations.
30/// - Managing task lifecycle (start, stop, pause, resume, restart).
31/// - Handling global and per-task concurrency limits.
32/// - Orchestrating task dependencies and prerequisites.
33/// - Distributing supervisor events to registered listeners.
34/// - Persisting and restoring task states.
35///
36/// It is typically created by the [`crate::Supervisor`] builder and then
37/// used to interact with the running supervised system.
38pub struct TaskRuntime {
39    pub(super) tasks: HashMap<&'static str, TaskEntry>,
40    pub(super) handles: HashMap<&'static str, JoinHandle<SupervisionResult>>,
41    /// Named async gates that must resolve before ANY task starts
42    pub(super) prerequisites: Vec<(&'static str, PrerequisiteFuture)>,
43    /// Build lookup: task_id -> setup watch sender so dependents can subscribe
44    pub(super) setup_signals: HashMap<&'static str, watch::Sender<Option<Result<(), String>>>>,
45    /// Global event broadcaster
46    pub(super) event_tx: broadcast::Sender<SupervisorEvent>,
47    /// Registered event listeners
48    pub(super) listeners: Vec<Arc<dyn SupervisorEventListener>>,
49    /// Optional persistence store for task state
50    pub(super) state_store: Option<Arc<dyn TaskStateStore>>,
51    /// Global concurrency limit
52    pub(super) global_concurrency_limit: Option<Arc<Semaphore>>,
53    /// Per-task concurrency limits
54    pub(crate) task_concurrency_limits: HashMap<&'static str, Arc<Semaphore>>,
55    /// Hot-reloadable task configurations
56    pub(super) task_configs: HashMap<&'static str, Arc<RwLock<TaskConfig>>>,
57    #[cfg(feature = "cron")]
58    #[allow(dead_code)]
59    pub(super) cron: Option<Arc<tokio::sync::Mutex<Cron>>>,
60    /// Optional application container for DI access
61    #[cfg(feature = "foxtive-app")]
62    pub(super) app: Option<Arc<foxtive::App>>,
63    /// Event broadcast channel capacity
64    pub(super) event_channel_capacity: usize,
65}
66
67impl fmt::Debug for TaskRuntime {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        f.debug_struct("TaskRuntime")
70            .field("tasks_count", &self.tasks.len())
71            .field("handles_count", &self.handles.len())
72            .field("prerequisites_count", &self.prerequisites.len())
73            .finish()
74    }
75}
76
77impl TaskRuntime {
78    /// Creates a new, empty `TaskRuntime`.
79    pub fn new() -> Self {
80        let event_channel_capacity = 1024;
81        let (event_tx, _) = broadcast::channel(event_channel_capacity);
82        #[allow(unused_mut)]
83        let mut runtime = Self {
84            tasks: HashMap::new(),
85            handles: HashMap::new(),
86            prerequisites: Vec::new(),
87            setup_signals: HashMap::new(),
88            event_tx,
89            listeners: Vec::new(),
90            state_store: None,
91            global_concurrency_limit: None,
92            task_concurrency_limits: HashMap::new(),
93            task_configs: HashMap::new(),
94            #[cfg(feature = "cron")]
95            cron: None,
96            #[cfg(feature = "foxtive-app")]
97            app: None,
98            event_channel_capacity,
99        };
100        runtime
101    }
102
103    // CONCURRENCY CONTROL
104
105    /// Sets a global concurrency limit for the supervisor.
106    ///
107    /// This limit applies to all tasks and restricts how many can be actively
108    /// running their `run()` method at any given time.
109    pub fn with_global_concurrency_limit(&mut self, limit: usize) -> &mut Self {
110        self.global_concurrency_limit = Some(Arc::new(Semaphore::new(limit)));
111        self
112    }
113
114    /// Sets the event broadcast channel capacity.
115    ///
116    /// The default is 1024. Increase this if you have many tasks emitting
117    /// events rapidly and observe `EventsDropped` warnings.
118    pub fn with_event_channel_capacity(&mut self, capacity: usize) -> &mut Self {
119        self.event_channel_capacity = capacity;
120        // Recreate the channel with the new capacity
121        let (event_tx, _) = broadcast::channel(capacity);
122        self.event_tx = event_tx;
123        self
124    }
125
126    /// Attach an application container for DI access within supervised tasks.
127    #[cfg(feature = "foxtive-app")]
128    pub fn with_app(&mut self, app: Arc<foxtive::App>) -> &mut Self {
129        self.app = Some(app);
130        self
131    }
132
133    /// Returns a reference to the attached application container, if any.
134    #[cfg(feature = "foxtive-app")]
135    pub fn app(&self) -> Option<&Arc<foxtive::App>> {
136        self.app.as_ref()
137    }
138
139    // TASK REGISTRATION
140
141    /// Registers a task for supervision.
142    ///
143    /// This method adds the task to the runtime's internal registry but does not
144    /// start its execution. Tasks are started when `start_all()` is called.
145    pub fn register<T: SupervisedTask + 'static>(&mut self, task: T) -> &mut Self {
146        let (setup_tx, _) = watch::channel(None);
147        let (control_tx, _) = broadcast::channel(64);
148        let id = task.id();
149
150        // Setup per-task concurrency limit if specified
151        if let Some(limit) = task.concurrency_limit() {
152            self.task_concurrency_limits
153                .insert(id, Arc::new(Semaphore::new(limit)));
154        }
155
156        // Initialize hot-reloadable config from task's current configuration
157        let config = TaskConfig::from_task(&task);
158        self.task_configs.insert(id, Arc::new(RwLock::new(config)));
159
160        self.setup_signals.insert(id, setup_tx.clone());
161        self.tasks.insert(
162            id,
163            TaskEntry {
164                task: Arc::new(task),
165                setup_tx,
166                control_tx,
167            },
168        );
169        // Event will be emitted during start_all() to ensure all listeners are registered first
170        self
171    }
172
173    /// Registers multiple tasks of the same type at once.
174    pub fn register_many<T: SupervisedTask + 'static>(&mut self, tasks: Vec<T>) -> &mut Self {
175        for task in tasks {
176            self.register(task);
177        }
178        self
179    }
180
181    /// Registers a task from a boxed trait object.
182    ///
183    /// Useful when managing a heterogeneous collection of tasks.
184    pub fn register_boxed(&mut self, task: Box<dyn SupervisedTask>) -> &mut Self {
185        let (setup_tx, _) = watch::channel(None);
186        let (control_tx, _) = broadcast::channel(64);
187        let id = task.id();
188
189        if let Some(limit) = task.concurrency_limit() {
190            self.task_concurrency_limits
191                .insert(id, Arc::new(Semaphore::new(limit)));
192        }
193
194        // Initialize hot-reloadable config from task's current configuration
195        let config = TaskConfig::from_task(task.as_ref());
196        self.task_configs.insert(id, Arc::new(RwLock::new(config)));
197
198        self.setup_signals.insert(id, setup_tx.clone());
199        self.tasks.insert(
200            id,
201            TaskEntry {
202                task: Arc::from(task),
203                setup_tx,
204                control_tx,
205            },
206        );
207        // Event will be emitted during start_all() for consistency with register()
208        self
209    }
210
211    /// Registers a task from an `Arc` (zero extra allocation).
212    ///
213    /// This is the most efficient way to add a task if you already have an `Arc` handle.
214    pub fn register_arc(&mut self, task: Arc<dyn SupervisedTask>) -> &mut Self {
215        let (setup_tx, _) = watch::channel(None);
216        let (control_tx, _) = broadcast::channel(64);
217        let id = task.id();
218
219        if let Some(limit) = task.concurrency_limit() {
220            self.task_concurrency_limits
221                .insert(id, Arc::new(Semaphore::new(limit)));
222        }
223
224        // Initialize hot-reloadable config from task's current configuration
225        let config = TaskConfig::from_task(task.as_ref());
226        self.task_configs.insert(id, Arc::new(RwLock::new(config)));
227
228        self.setup_signals.insert(id, setup_tx.clone());
229        self.tasks.insert(
230            id,
231            TaskEntry {
232                task,
233                setup_tx,
234                control_tx,
235            },
236        );
237        // Event will be emitted during start_all() for consistency with register()
238        self
239    }
240
241    // DYNAMIC TASK MANAGEMENT
242
243    /// Registers and starts a new task at runtime.
244    ///
245    /// This allows adding tasks to an already running supervisor.
246    ///
247    /// # Errors
248    /// Returns `SupervisorError::ConfigurationError` if a task with the same ID already exists.
249    /// Returns `SupervisorError::DependencyValidation` if a declared dependency is unknown.
250    pub fn add_task<T: SupervisedTask + 'static>(
251        &mut self,
252        task: T,
253    ) -> Result<(), SupervisorError> {
254        let id = task.id();
255        if self.tasks.contains_key(id) {
256            return Err(SupervisorError::config(
257                id,
258                "Task already exists",
259            ));
260        }
261
262        self.register(task);
263
264        // Validate dependency graph including cycle detection for the newly added task
265        let tasks_vec: Vec<&TaskEntry> = self.tasks.values().collect();
266        validate_dependencies(&tasks_vec)?;
267
268        let entry = self.tasks.get(id).unwrap();
269
270        // Collect receivers for each declared dependency
271        let mut dep_receivers = Vec::new();
272        let active_deps = entry.task.active_dependencies();
273        for dep_id in &active_deps {
274            if let Some(sender) = self.setup_signals.get(dep_id) {
275                dep_receivers.push((*dep_id, sender.subscribe()));
276            } else {
277                return Err(SupervisorError::dependency_validation(
278                    id,
279                    dep_id,
280                    crate::error::ValidationError::UnknownTaskId,
281                ));
282            }
283        }
284
285        let task_limit = self.task_concurrency_limits.get(id).cloned();
286        let task_config = self.task_configs.get(id).cloned();
287
288        let params = SupervisionParams {
289            task: entry.task.clone(),
290            setup_tx: entry.setup_tx.clone(),
291            control_rx: entry.control_tx.subscribe(),
292            event_tx: self.event_tx.clone(),
293            dep_receivers,
294            state_store: self.state_store.clone(),
295            global_semaphore: self.global_concurrency_limit.clone(),
296            task_semaphore: task_limit,
297            task_config,
298        };
299
300        let handle = supervise(params);
301        self.handles.insert(id, handle);
302
303        // Send TaskRegistered event for dynamically added task
304        let name = entry.task.name();
305        let _ = self.event_tx.send(SupervisorEvent::TaskRegistered {
306            id: id.to_string(),
307            name,
308        });
309
310        Ok(())
311    }
312
313    /// Manually restarts a task by its ID.
314    ///
315    /// This sends a `Restart` control message to the task's supervision loop.
316    ///
317    /// # Errors
318    /// Returns `SupervisorError::UnknownTask` if no task with the given ID is found.
319    pub fn restart_task(&self, id: &str) -> Result<(), SupervisorError> {
320        if let Some(entry) = self.tasks.get(id) {
321            let _ = entry.control_tx.send(ControlMessage::Restart);
322            Ok(())
323        } else {
324            Err(SupervisorError::UnknownTask(id.to_string()))
325        }
326    }
327
328    /// Pauses a task by its ID.
329    ///
330    /// This sends a `Pause` control message to the task's supervision loop.
331    /// A paused task will temporarily stop executing its `run()` method.
332    ///
333    /// # Errors
334    /// Returns `SupervisorError::UnknownTask` if no task with the given ID is found.
335    pub fn pause_task(&self, id: &str) -> Result<(), SupervisorError> {
336        if let Some(entry) = self.tasks.get(id) {
337            let _ = entry.control_tx.send(ControlMessage::Pause);
338            Ok(())
339        } else {
340            Err(SupervisorError::UnknownTask(id.to_string()))
341        }
342    }
343
344    /// Resumes a paused task by its ID.
345    ///
346    /// This sends a `Resume` control message to the task's supervision loop,
347    /// allowing it to continue execution.
348    ///
349    /// # Errors
350    /// Returns `SupervisorError::UnknownTask` if no task with the given ID is found.
351    pub fn resume_task(&self, id: &str) -> Result<(), SupervisorError> {
352        if let Some(entry) = self.tasks.get(id) {
353            let _ = entry.control_tx.send(ControlMessage::Resume);
354            Ok(())
355        } else {
356            Err(SupervisorError::UnknownTask(id.to_string()))
357        }
358    }
359
360    /// Resets the circuit breaker for a task by its ID.
361    ///
362    /// # Errors
363    /// Returns `SupervisorError::UnknownTask` if no task with the given ID is found.
364    pub fn reset_circuit_breaker(&self, id: &str) -> Result<(), SupervisorError> {
365        if let Some(entry) = self.tasks.get(id) {
366            let _ = entry.control_tx.send(ControlMessage::ResetCircuitBreaker);
367            Ok(())
368        } else {
369            Err(SupervisorError::UnknownTask(id.to_string()))
370        }
371    }
372
373    // HOT RELOAD CONFIGURATION
374
375    /// Updates the restart policy for a task at runtime.
376    ///
377    /// The new policy will take effect on the next restart attempt.
378    /// This does not trigger an immediate restart.
379    ///
380    /// # Arguments
381    /// * `id` - The task ID to update
382    /// * `new_policy` - The new restart policy to apply
383    ///
384    /// # Errors
385    /// Returns `SupervisorError::UnknownTask` if no task with the given ID is found.
386    pub async fn update_restart_policy(
387        &self,
388        id: &str,
389        new_policy: crate::enums::RestartPolicy,
390    ) -> Result<(), SupervisorError> {
391        let config_lock = self
392            .task_configs
393            .get(id)
394            .ok_or_else(|| SupervisorError::UnknownTask(id.to_string()))?;
395
396        // Validate the new policy
397        Self::validate_restart_policy(&new_policy)?;
398
399        let mut config = config_lock.write().await;
400        let old_policy = config.restart_policy.clone();
401        config.restart_policy = new_policy.clone();
402
403        // Emit configuration change event
404        if let Some(entry) = self.tasks.get(id) {
405            let name = entry.task.name();
406            let _ = self.event_tx.send(SupervisorEvent::TaskConfigUpdated {
407                id: id.to_string(),
408                name,
409                field: "restart_policy".to_string(),
410                old_value: format!("{:?}", old_policy),
411                new_value: format!("{:?}", new_policy),
412            });
413        }
414
415        info!(task_id = %id, ?old_policy, ?new_policy, "Updated restart policy");
416        Ok(())
417    }
418
419    /// Updates the backoff strategy for a task at runtime.
420    ///
421    /// The new strategy will take effect on the next restart attempt.
422    /// This does not trigger an immediate restart.
423    ///
424    /// # Arguments
425    /// * `id` - The task ID to update
426    /// * `new_strategy` - The new backoff strategy to apply
427    ///
428    /// # Errors
429    /// Returns `SupervisorError::UnknownTask` if no task with the given ID is found.
430    pub async fn update_backoff_strategy(
431        &self,
432        id: &str,
433        new_strategy: crate::enums::BackoffStrategy,
434    ) -> Result<(), SupervisorError> {
435        let config_lock = self
436            .task_configs
437            .get(id)
438            .ok_or_else(|| SupervisorError::UnknownTask(id.to_string()))?;
439
440        // Validate the new strategy
441        Self::validate_backoff_strategy(&new_strategy)?;
442
443        let mut config = config_lock.write().await;
444        let old_strategy = config.backoff_strategy.clone();
445        config.backoff_strategy = new_strategy.clone();
446
447        // Emit configuration change event
448        if let Some(entry) = self.tasks.get(id) {
449            let name = entry.task.name();
450            let _ = self.event_tx.send(SupervisorEvent::TaskConfigUpdated {
451                id: id.to_string(),
452                name,
453                field: "backoff_strategy".to_string(),
454                old_value: format!("{:?}", old_strategy),
455                new_value: format!("{:?}", new_strategy),
456            });
457        }
458
459        info!(task_id = %id, ?old_strategy, ?new_strategy, "Updated backoff strategy");
460        Ok(())
461    }
462
463    /// Enables or disables a task at runtime.
464    ///
465    /// A disabled task will not restart after failure but will continue running
466    /// if already executing. To stop a running task, use `stop_task()` first.
467    ///
468    /// # Arguments
469    /// * `id` - The task ID to enable/disable
470    /// * `enabled` - Whether the task should be enabled
471    ///
472    /// # Errors
473    /// Returns `SupervisorError::UnknownTask` if no task with the given ID is found.
474    pub async fn set_task_enabled(&self, id: &str, enabled: bool) -> Result<(), SupervisorError> {
475        let config_lock = self
476            .task_configs
477            .get(id)
478            .ok_or_else(|| SupervisorError::UnknownTask(id.to_string()))?;
479
480        let mut config = config_lock.write().await;
481        let old_enabled = config.enabled;
482        config.enabled = enabled;
483
484        // Emit configuration change event
485        if let Some(entry) = self.tasks.get(id) {
486            let name = entry.task.name();
487            let _ = self.event_tx.send(SupervisorEvent::TaskConfigUpdated {
488                id: id.to_string(),
489                name,
490                field: "enabled".to_string(),
491                old_value: old_enabled.to_string(),
492                new_value: enabled.to_string(),
493            });
494        }
495
496        info!(task_id = %id, old_enabled = old_enabled, new_enabled = enabled, "Updated task enabled status");
497        Ok(())
498    }
499
500    /// Gets the current configuration for a task.
501    ///
502    /// # Arguments
503    /// * `id` - The task ID to query
504    ///
505    /// # Returns
506    /// A clone of the current task configuration, or None if task not found.
507    pub async fn get_task_config(&self, id: &str) -> Option<TaskConfig> {
508        let config_lock = self.task_configs.get(id)?;
509        let config = config_lock.read().await;
510        Some(config.clone())
511    }
512
513    /// Checks if a task is currently enabled.
514    ///
515    /// # Arguments
516    /// * `id` - The task ID to check
517    ///
518    /// # Returns
519    /// `true` if the task is enabled, `false` otherwise.
520    pub async fn is_task_enabled(&self, id: &str) -> bool {
521        if let Some(config_lock) = self.task_configs.get(id) {
522            let config = config_lock.read().await;
523            config.enabled
524        } else {
525            false
526        }
527    }
528
529    // TASK GROUP MANAGEMENT
530
531    /// Starts all tasks in a specific group.
532    ///
533    /// This is useful for atomic operations where related tasks should be started together.
534    /// If any task in the group fails to start, the operation continues with other tasks.
535    ///
536    /// # Arguments
537    /// * `group_id` - The group identifier to start
538    ///
539    /// # Returns
540    /// Number of tasks started
541    pub fn start_group(&mut self, group_id: &str) -> usize {
542        let mut started_count = 0;
543
544        // Collect task IDs that belong to this group
545        let group_task_ids: Vec<&'static str> = self
546            .tasks
547            .iter()
548            .filter(|(_, entry)| entry.task.group_id() == Some(group_id))
549            .map(|(id, _)| *id)
550            .collect();
551
552        for task_id in group_task_ids {
553            if !self.handles.contains_key(task_id) {
554                // Task is registered but not started, start it now
555                if let Err(e) = self.add_task_by_id(task_id) {
556                    error!(task_id = %task_id, error = ?e, "Failed to start task in group");
557                } else {
558                    started_count += 1;
559                }
560            }
561        }
562
563        info!(group_id = %group_id, started = started_count, "Started task group");
564        started_count
565    }
566
567    /// Stops all tasks in a specific group.
568    ///
569    /// Sends Stop control messages to all tasks in the group and waits for them to terminate.
570    ///
571    /// # Arguments
572    /// * `group_id` - The group identifier to stop
573    ///
574    /// # Returns
575    /// Number of tasks stopped
576    pub async fn stop_group(&mut self, group_id: &str) -> usize {
577        let mut stopped_count = 0;
578
579        // Collect task IDs that belong to this group
580        let group_task_ids: Vec<&'static str> = self
581            .tasks
582            .iter()
583            .filter(|(_, entry)| entry.task.group_id() == Some(group_id))
584            .map(|(id, _)| *id)
585            .collect();
586
587        for task_id in group_task_ids {
588            if let Some(handle) = self.handles.remove(task_id) {
589                if let Some(entry) = self.tasks.get(task_id) {
590                    let _ = entry.control_tx.send(ControlMessage::Stop);
591                }
592
593                // Wait briefly for graceful shutdown
594                match tokio::time::timeout(Duration::from_secs(5), handle).await {
595                    Ok(_) => stopped_count += 1,
596                    Err(_) => {
597                        warn!(task_id = %task_id, "Task in group did not stop gracefully, aborting");
598                        stopped_count += 1; // Count it anyway
599                    }
600                }
601            }
602        }
603
604        info!(group_id = %group_id, stopped = stopped_count, "Stopped task group");
605        stopped_count
606    }
607
608    /// Restarts all tasks in a specific group.
609    ///
610    /// Sends Restart control messages to all running tasks in the group.
611    ///
612    /// # Arguments
613    /// * `group_id` - The group identifier to restart
614    ///
615    /// # Returns
616    /// Number of tasks restarted
617    pub fn restart_group(&self, group_id: &str) -> usize {
618        let mut restarted_count = 0;
619
620        for (task_id, entry) in &self.tasks {
621            if entry.task.group_id() == Some(group_id) && self.handles.contains_key(task_id) {
622                let _ = entry.control_tx.send(ControlMessage::Restart);
623                restarted_count += 1;
624            }
625        }
626
627        info!(group_id = %group_id, restarted = restarted_count, "Restarted task group");
628        restarted_count
629    }
630
631    /// Lists all task IDs in a specific group.
632    ///
633    /// # Arguments
634    /// * `group_id` - The group identifier to query
635    ///
636    /// # Returns
637    /// Vector of task IDs belonging to the group
638    pub fn list_group_tasks(&self, group_id: &str) -> Vec<String> {
639        self.tasks
640            .iter()
641            .filter(|(_, entry)| entry.task.group_id() == Some(group_id))
642            .map(|(id, _)| id.to_string())
643            .collect()
644    }
645
646    /// Gets the aggregated health status for a task group.
647    ///
648    /// The group health is determined by the worst health status among all tasks in the group:
649    /// - If any task is Unhealthy, the group is Unhealthy
650    /// - Else if any task is Degraded, the group is Degraded
651    /// - Else if all tasks are Healthy, the group is Healthy
652    /// - If no tasks are in the group, returns Unknown
653    ///
654    /// # Arguments
655    /// * `group_id` - The group identifier to query
656    ///
657    /// # Returns
658    /// Aggregated HealthStatus for the group
659    pub async fn get_group_health(&self, group_id: &str) -> HealthStatus {
660        // Fire all health checks concurrently for tasks in this group
661        let group_tasks: Vec<_> = self.tasks.values()
662            .filter(|entry| entry.task.group_id() == Some(group_id))
663            .collect();
664
665        if group_tasks.is_empty() {
666            return HealthStatus::Unknown;
667        }
668
669        let health_futures: Vec<_> = group_tasks.iter()
670            .map(|entry| entry.task.health_check())
671            .collect();
672        let results = futures_util::future::join_all(health_futures).await;
673
674        let mut has_healthy = false;
675        let mut has_degraded = false;
676        let mut has_unhealthy = false;
677
678        for health in results {
679            match health {
680                HealthStatus::Healthy => has_healthy = true,
681                HealthStatus::Degraded { .. } => has_degraded = true,
682                HealthStatus::Unhealthy { .. } => has_unhealthy = true,
683                HealthStatus::Unknown => {} // Don't affect aggregation
684            }
685        }
686
687        // Return worst status
688        if has_unhealthy {
689            HealthStatus::Unhealthy {
690                reason: "One or more tasks in group are unhealthy".to_string(),
691            }
692        } else if has_degraded {
693            HealthStatus::Degraded {
694                reason: "One or more tasks in group are degraded".to_string(),
695            }
696        } else if has_healthy {
697            HealthStatus::Healthy
698        } else {
699            HealthStatus::Unknown
700        }
701    }
702
703    /// Gets detailed health information for all tasks in a group.
704    ///
705    /// # Arguments
706    /// * `group_id` - The group identifier to query
707    ///
708    /// # Returns
709    /// Vector of TaskSummary for each task in the group
710    pub async fn get_group_health_details(&self, group_id: &str) -> Vec<TaskSummary> {
711        // Fire all health checks concurrently for tasks in this group
712        let group_tasks: Vec<_> = self.tasks.iter()
713            .filter(|(_, entry)| entry.task.group_id() == Some(group_id))
714            .collect();
715
716        let health_futures: Vec<_> = group_tasks.iter()
717            .map(|(task_id, entry)| async move {
718                let health = entry.task.health_check().await;
719                TaskSummary {
720                    id: task_id.to_string(),
721                    name: entry.task.name(),
722                    health,
723                }
724            })
725            .collect();
726
727        futures_util::future::join_all(health_futures).await
728    }
729
730    /// Helper method to start a task by ID (used internally for group operations)
731    fn add_task_by_id(&mut self, task_id: &'static str) -> Result<(), SupervisorError> {
732        let entry = self.tasks.get(task_id).unwrap();
733
734        // Collect receivers for each declared dependency
735        let mut dep_receivers = Vec::new();
736        let active_deps = entry.task.active_dependencies();
737        for dep_id in &active_deps {
738            if let Some(sender) = self.setup_signals.get(dep_id) {
739                dep_receivers.push((*dep_id, sender.subscribe()));
740            } else {
741                return Err(SupervisorError::dependency_validation(
742                    task_id,
743                    dep_id,
744                    crate::error::ValidationError::UnknownTaskId,
745                ));
746            }
747        }
748
749        let task_limit = self.task_concurrency_limits.get(task_id).cloned();
750        let task_config = self.task_configs.get(task_id).cloned();
751
752        let params = SupervisionParams {
753            task: entry.task.clone(),
754            setup_tx: entry.setup_tx.clone(),
755            control_rx: entry.control_tx.subscribe(),
756            event_tx: self.event_tx.clone(),
757            dep_receivers,
758            state_store: self.state_store.clone(),
759            global_semaphore: self.global_concurrency_limit.clone(),
760            task_semaphore: task_limit,
761            task_config,
762        };
763
764        let handle = supervise(params);
765        self.handles.insert(task_id, handle);
766
767        let name = entry.task.name();
768        let _ = self.event_tx.send(SupervisorEvent::TaskRegistered {
769            id: task_id.to_string(),
770            name,
771        });
772
773        Ok(())
774    }
775
776    /// Stops and removes a task by its ID.
777    ///
778    /// This sends a `Stop` control message to the task, waits for it to terminate,
779    /// and then removes it from the runtime.
780    ///
781    /// # Errors
782    /// Returns `SupervisorError::UnknownTask` if no task with the given ID is found.
783    /// Returns `SupervisorError::TaskPanicked` if the task panicked during removal.
784    pub async fn remove_task(
785        &mut self,
786        id: &str,
787    ) -> Result<Option<SupervisionResult>, SupervisorError> {
788        if let Some(entry) = self.tasks.remove(id) {
789            let _ = entry.control_tx.send(ControlMessage::Stop);
790            let name = entry.task.name();
791            self.setup_signals.remove(id);
792            let _ = self.event_tx.send(SupervisorEvent::TaskRemoved {
793                id: id.to_string(),
794                name,
795            });
796            if let Some(handle) = self.handles.remove(id) {
797                match handle.await {
798                    Ok(res) => Ok(Some(res)),
799                    Err(join_err) => Err(SupervisorError::task_panicked(id, join_err)),
800                }
801            } else {
802                Ok(None)
803            }
804        } else {
805            Err(SupervisorError::UnknownTask(id.to_string()))
806        }
807    }
808
809    /// Retrieves detailed information about a specific task.
810    ///
811    /// This includes its ID, name, and current health status.
812    ///
813    /// # Errors
814    /// Returns `SupervisorError::UnknownTask` if no task with the given ID is found.
815    pub async fn get_task_info(&self, id: &str) -> Result<TaskSummary, SupervisorError> {
816        if let Some(entry) = self.tasks.get(id) {
817            let health = entry.task.health_check().await;
818            Ok(TaskSummary {
819                id: id.to_string(),
820                name: entry.task.name(),
821                health,
822            })
823        } else {
824            Err(SupervisorError::UnknownTask(id.to_string()))
825        }
826    }
827
828    /// Lists summaries of all currently registered tasks.
829    pub async fn list_tasks(&self) -> Vec<TaskSummary> {
830        // Fire all health checks concurrently
831        let health_futures: Vec<_> = self.tasks.values()
832            .map(|entry| entry.task.health_check())
833            .collect();
834        let health_results = futures_util::future::join_all(health_futures).await;
835
836        self.tasks.iter()
837            .zip(health_results)
838            .map(|((id, entry), health)| TaskSummary {
839                id: id.to_string(),
840                name: entry.task.name(),
841                health,
842            })
843            .collect()
844    }
845
846    // PERSISTENCE
847
848    /// Sets a custom state store for persisting task states.
849    ///
850    /// This method is typically used by the [`crate::Supervisor`] builder.
851    pub fn with_state_store(&mut self, store: Arc<dyn TaskStateStore>) -> &mut Self {
852        self.state_store = Some(store);
853        self
854    }
855
856    // EVENT SYSTEM
857
858    /// Subscribes to the supervisor's event stream.
859    ///
860    /// Returns a `tokio::sync::broadcast::Receiver` that will receive all
861    /// [`SupervisorEvent`]s emitted by the runtime.
862    pub fn subscribe(&self) -> broadcast::Receiver<SupervisorEvent> {
863        self.event_tx.subscribe()
864    }
865
866    /// Registers an event listener.
867    ///
868    /// This method is typically used by the [`crate::Supervisor`] builder.
869    pub fn add_listener(&mut self, listener: Arc<dyn SupervisorEventListener>) -> &mut Self {
870        self.listeners.push(listener);
871        self
872    }
873
874    // PREREQUISITE REGISTRATION
875
876    /// Adds an asynchronous prerequisite gate that must resolve before any task starts.
877    ///
878    /// This method is typically used by the [`crate::Supervisor`] builder.
879    pub fn add_prerequisite(&mut self, name: &'static str, fut: PrerequisiteFuture) -> &mut Self {
880        self.prerequisites.push((name, fut));
881        self
882    }
883
884    /// Adds an asynchronous prerequisite gate using a closure.
885    ///
886    /// This method is typically used by the [`crate::Supervisor`] builder.
887    pub fn add_prerequisite_fn<F, Fut>(&mut self, name: &'static str, f: F) -> &mut Self
888    where
889        F: FnOnce() -> Fut + Send + 'static,
890        Fut: Future<Output = SupervisorResult<()>> + Send + 'static,
891    {
892        self.add_prerequisite(name, Box::pin(async move { f().await }))
893    }
894
895    // STARTUP
896
897    /// Starts all registered tasks.
898    ///
899    /// This method first runs all prerequisites, then validates dependencies,
900    /// and finally spawns the supervision loop for each task.
901    ///
902    /// # Errors
903    /// Returns [`SupervisorError`] if any prerequisite fails or if the dependency graph is invalid.
904    pub async fn start_all(&mut self) -> Result<(), SupervisorError> {
905        // Start event listener distribution
906        let mut event_rx = self.event_tx.subscribe();
907        let listeners = self.listeners.clone();
908        tokio::spawn(async move {
909            loop {
910                match event_rx.recv().await {
911                    Ok(event) => {
912                        for listener in &listeners {
913                            listener.on_event(event.clone()).await;
914                        }
915                    }
916                    Err(broadcast::error::RecvError::Lagged(n)) => {
917                        warn!(
918                            missed_events = n,
919                            "Event channel lagged, skipping missed events to prevent listener backlog"
920                        );
921                        // Continue listening for new events instead of terminating
922                        continue;
923                    }
924                    Err(broadcast::error::RecvError::Closed) => {
925                        info!("Event channel closed, stopping event distribution");
926                        break;
927                    }
928                }
929            }
930        });
931
932        // --- Phase 1: prerequisites (run in parallel) ---
933        if !self.prerequisites.is_empty() {
934            info!(
935                "[Supervisor] Running {} prerequisites in parallel...",
936                self.prerequisites.len()
937            );
938
939            let prereq_futures: Vec<_> = self
940                .prerequisites
941                .drain(..)
942                .map(|(name, fut)| async move {
943                    info!("[Supervisor] Awaiting prerequisite '{name}'...");
944                    fut.await.map_err(|e| (name, e))
945                })
946                .collect();
947
948            let results = futures_util::future::try_join_all(prereq_futures)
949                .await
950                .map_err(|(name, e)| SupervisorError::prerequisite_failed(name, Box::new(e)))?;
951
952            info!("[Supervisor] All {} prerequisites satisfied", results.len());
953        }
954
955        if self.tasks.is_empty() {
956            warn!("[Supervisor] No tasks registered");
957            return Ok(());
958        }
959
960        // --- Phase 2: validate dependency graph ---
961        let tasks_vec: Vec<&TaskEntry> = self.tasks.values().collect();
962        validate_dependencies(&tasks_vec)?;
963
964        info!(
965            "[Supervisor] Starting {} supervised tasks...",
966            self.tasks.len()
967        );
968
969        // Sort tasks by priority (highest first) for startup order
970        let mut sorted_ids: Vec<&'static str> = self.tasks.keys().copied().collect();
971        sorted_ids.sort_by(|a, b| {
972            let task_a = &self.tasks[a].task;
973            let task_b = &self.tasks[b].task;
974            task_b.priority().cmp(&task_a.priority())
975        });
976
977        // --- Phase 3: spawn ---
978        for id in sorted_ids {
979            let entry = &self.tasks[id];
980            // Emit TaskRegistered event during startup (all listeners are registered by now)
981            let name = entry.task.name();
982            let _ = self.event_tx.send(SupervisorEvent::TaskRegistered {
983                id: id.to_string(),
984                name,
985            });
986
987            // Collect receivers for each declared dependency
988            let active_deps = entry.task.active_dependencies();
989            let dep_receivers: DepSetupReceivers = active_deps
990                .iter()
991                .map(|dep_id| {
992                    let sender = self.setup_signals[dep_id].clone();
993                    (*dep_id, sender.subscribe())
994                })
995                .collect();
996
997            let task_limit = self.task_concurrency_limits.get(id).cloned();
998            let task_config = self.task_configs.get(id).cloned();
999
1000            let params = SupervisionParams {
1001                task: entry.task.clone(),
1002                setup_tx: entry.setup_tx.clone(),
1003                control_rx: entry.control_tx.subscribe(),
1004                event_tx: self.event_tx.clone(),
1005                dep_receivers,
1006                state_store: self.state_store.clone(),
1007                global_semaphore: self.global_concurrency_limit.clone(),
1008                task_semaphore: task_limit,
1009                task_config,
1010            };
1011
1012            let handle = supervise(params);
1013            self.handles.insert(id, handle);
1014        }
1015
1016        info!("[Supervisor] All tasks started");
1017        Ok(())
1018    }
1019
1020    /// Starts a single task with no dependencies (fire and forget).
1021    ///
1022    /// This is a convenience function for simple, isolated task supervision
1023    /// without needing a full `TaskRuntime` setup.
1024    pub fn start_one<T: SupervisedTask + 'static>(task: T) -> JoinHandle<SupervisionResult> {
1025        let (setup_tx, _) = watch::channel(None);
1026        let (control_tx, _) = broadcast::channel(64);
1027        let (event_tx, _) = broadcast::channel(1);
1028        let params = SupervisionParams {
1029            task: Arc::new(task),
1030            setup_tx,
1031            control_rx: control_tx.subscribe(),
1032            event_tx,
1033            dep_receivers: vec![],
1034            state_store: None,
1035            global_semaphore: None,
1036            task_semaphore: None,
1037            task_config: None,
1038        };
1039        supervise(params)
1040    }
1041
1042    // WAITING
1043
1044    /// Waits for any one supervised task to terminate.
1045    ///
1046    /// Returns the `SupervisionResult` of the first task that finishes.
1047    pub async fn wait_any(&mut self) -> SupervisionResult {
1048        if self.handles.is_empty() {
1049            warn!("[Supervisor] No tasks to wait for");
1050            return SupervisionResult {
1051                task_name: "none".to_string(),
1052                task_id: "none".to_string(),
1053                total_attempts: 0,
1054                final_status: crate::enums::SupervisionStatus::ManuallyStopped,
1055            };
1056        }
1057
1058        // Extract all handles from the HashMap to avoid borrow issues
1059        let mut handle_vec: Vec<(&'static str, JoinHandle<SupervisionResult>)> =
1060            self.handles.drain().collect();
1061
1062        // Use select_all to wait for the first one to complete
1063        use futures_util::future::select_all;
1064
1065        let mut futures = Vec::new();
1066        for (id, handle) in handle_vec.iter_mut() {
1067            let id_clone = *id;
1068            futures.push(Box::pin(async move {
1069                let result = handle.await;
1070                (id_clone, result)
1071            }));
1072        }
1073
1074        let ((finished_id, result), _idx, _remaining) = select_all(futures).await;
1075
1076        // Put remaining handles back into the HashMap
1077        for (i, (id, handle)) in handle_vec.into_iter().enumerate() {
1078            if i != _idx {
1079                self.handles.insert(id, handle);
1080            }
1081        }
1082
1083        match result {
1084            Ok(supervision_result) => {
1085                error!(
1086                    "[Supervisor] Task '{}' (id: {}) terminated: {:?}",
1087                    supervision_result.task_name,
1088                    supervision_result.task_id,
1089                    supervision_result.final_status
1090                );
1091                supervision_result
1092            }
1093            Err(join_err) => {
1094                error!("[Supervisor] Task {} panicked: {:?}", finished_id, join_err);
1095                SupervisionResult {
1096                    task_name: "unknown".to_string(),
1097                    task_id: finished_id.to_string(),
1098                    total_attempts: 0,
1099                    final_status: crate::enums::SupervisionStatus::ManuallyStopped,
1100                }
1101            }
1102        }
1103    }
1104
1105    /// Waits for all supervised tasks to terminate.
1106    ///
1107    /// Returns a vector of `SupervisionResult` for all tasks.
1108    pub async fn wait_all(&mut self) -> Vec<SupervisionResult> {
1109        let mut results = Vec::new();
1110        while !self.handles.is_empty() {
1111            results.push(self.wait_any().await);
1112        }
1113        results
1114    }
1115
1116    /// Initiates a graceful shutdown of all supervised tasks.
1117    ///
1118    /// This sends a `Stop` control message to each task and waits for them
1119    /// to complete their `on_shutdown()` hooks and terminate.
1120    ///
1121    /// Shutdown is performed in reverse dependency order (leaves first, then roots).
1122    pub async fn shutdown(mut self) {
1123        info!("[Supervisor] Shutting down {} tasks...", self.tasks.len());
1124        let _ = self
1125            .event_tx
1126            .send(SupervisorEvent::SupervisorShutdownStarted);
1127
1128        // Calculate shutdown order based on dependencies.
1129        // We want to shut down tasks that NO OTHER task depends on first.
1130        let shutdown_order = self.calculate_shutdown_order();
1131
1132        for id in shutdown_order {
1133            if let Some(entry) = self.tasks.get(id) {
1134                let name = entry.task.name();
1135                let timeout = entry.task.shutdown_timeout();
1136                info!(task_id = %id, "Signalling task '{}' to stop...", name);
1137
1138                let _ = entry.control_tx.send(ControlMessage::Stop);
1139
1140                if let Some(handle) = self.handles.remove(id) {
1141                    // Wait for the supervision loop to finish (it will abort run_handle and call cleanup)
1142                    // Pin the handle so we can abort it on timeout
1143                    tokio::pin!(handle);
1144                    match tokio::time::timeout(timeout, &mut handle).await {
1145                        Ok(res) => {
1146                            match res {
1147                                Ok(_supervision_res) => {
1148                                    info!(task_id = %id, "Task '{}' supervision completed.", name);
1149                                    // Call on_shutdown hook - separate from cleanup(), this runs once during graceful shutdown
1150                                    match tokio::time::timeout(timeout, entry.task.on_shutdown())
1151                                        .await
1152                                    {
1153                                        Ok(_) => {
1154                                            info!(task_id = %id, "Task '{}' on_shutdown completed.", name)
1155                                        }
1156                                        Err(_) => {
1157                                            warn!(task_id = %id, "Task '{}' on_shutdown timed out after {:?}.", name, timeout)
1158                                        }
1159                                    }
1160                                }
1161                                Err(e) => {
1162                                    error!(task_id = %id, "Task '{}' panicked during shutdown: {:?}", name, e)
1163                                }
1164                            }
1165                        }
1166                        Err(_) => {
1167                            warn!(task_id = %id, "Task '{}' did not stop within timeout {:?}. Aborting task.", name, timeout);
1168                            handle.abort();
1169                        }
1170                    }
1171                }
1172            }
1173        }
1174
1175        // Clean up any remaining handles (tasks that weren't in the dependency graph somehow)
1176        for (_id, handle) in self.handles.drain() {
1177            handle.abort();
1178        }
1179
1180        let _ = self
1181            .event_tx
1182            .send(SupervisorEvent::SupervisorShutdownCompleted);
1183        info!("[Supervisor] All tasks shut down");
1184    }
1185
1186    /// Calculates the order in which tasks should be shut down.
1187    /// Tasks with no dependents are shut down first.
1188    fn calculate_shutdown_order(&self) -> Vec<&'static str> {
1189        let mut order = Vec::new();
1190        let mut visited = HashSet::new();
1191
1192        // Build adjacency list for "is depended on by"
1193        let mut dependents: HashMap<&'static str, Vec<&'static str>> = HashMap::new();
1194        for (id, entry) in &self.tasks {
1195            let active_deps = entry.task.active_dependencies();
1196            for dep_id in &active_deps {
1197                dependents.entry(dep_id).or_default().push(*id);
1198            }
1199        }
1200
1201        // Helper for DFS to find shutdown order
1202        fn visit(
1203            id: &'static str,
1204            dependents: &HashMap<&'static str, Vec<&'static str>>,
1205            visited: &mut HashSet<&'static str>,
1206            order: &mut Vec<&'static str>,
1207        ) {
1208            if visited.contains(id) {
1209                return;
1210            }
1211
1212            // Visit all tasks that depend on this task first
1213            if let Some(deps) = dependents.get(id) {
1214                for dep in deps {
1215                    visit(dep, dependents, visited, order);
1216                }
1217            }
1218
1219            visited.insert(id);
1220            order.push(id);
1221        }
1222
1223        for id in self.tasks.keys() {
1224            visit(id, &dependents, &mut visited, &mut order);
1225        }
1226
1227        order
1228    }
1229
1230    /// Returns the number of tasks currently being supervised.
1231    pub fn task_count(&self) -> usize {
1232        self.handles.len()
1233    }
1234
1235    // CONFIGURATION VALIDATION
1236
1237    /// Validates a restart policy before applying it
1238    fn validate_restart_policy(
1239        _policy: &crate::enums::RestartPolicy,
1240    ) -> Result<(), SupervisorError> {
1241        // Currently all restart policies are valid
1242        // Future enhancements could add constraints like:
1243        // - MaxAttempts must be > 0
1244        // - Warn if changing from Always to Never on critical tasks
1245        Ok(())
1246    }
1247
1248    /// Validates a backoff strategy before applying it
1249    fn validate_backoff_strategy(
1250        strategy: &crate::enums::BackoffStrategy,
1251    ) -> Result<(), SupervisorError> {
1252        // Validate that delays are reasonable (not too short, not too long)
1253        match strategy {
1254            crate::enums::BackoffStrategy::Fixed(duration) => {
1255                if duration.as_secs() > 3600 {
1256                    return Err(SupervisorError::config(
1257                        "global",
1258                        "Fixed backoff delay cannot exceed 1 hour",
1259                    ));
1260                }
1261            }
1262            crate::enums::BackoffStrategy::Exponential { initial, max } => {
1263                if initial.as_secs() > 3600 || max.as_secs() > 3600 {
1264                    return Err(SupervisorError::config(
1265                        "global",
1266                        "Exponential backoff delays cannot exceed 1 hour",
1267                    ));
1268                }
1269                if initial > max {
1270                    return Err(SupervisorError::config(
1271                        "global",
1272                        "Initial backoff cannot be greater than max backoff",
1273                    ));
1274                }
1275            }
1276            crate::enums::BackoffStrategy::Linear {
1277                initial,
1278                increment: _,
1279                max,
1280            } => {
1281                if initial.as_secs() > 3600 || max.as_secs() > 3600 {
1282                    return Err(SupervisorError::config(
1283                        "global",
1284                        "Linear backoff delays cannot exceed 1 hour",
1285                    ));
1286                }
1287                if initial > max {
1288                    return Err(SupervisorError::config(
1289                        "global",
1290                        "Initial backoff cannot be greater than max backoff",
1291                    ));
1292                }
1293            }
1294            crate::enums::BackoffStrategy::Fibonacci { initial, max } => {
1295                if initial.as_secs() > 3600 || max.as_secs() > 3600 {
1296                    return Err(SupervisorError::config(
1297                        "global",
1298                        "Fibonacci backoff delays cannot exceed 1 hour",
1299                    ));
1300                }
1301                if initial > max {
1302                    return Err(SupervisorError::config(
1303                        "global",
1304                        "Initial backoff cannot be greater than max backoff",
1305                    ));
1306                }
1307            }
1308            crate::enums::BackoffStrategy::Custom(_) => {
1309                // Custom strategies can't be validated without executing them
1310                // Trust the user's implementation
1311            }
1312        }
1313        Ok(())
1314    }
1315}
1316
1317/// A summary of a task's current status.
1318#[derive(Debug, Clone)]
1319pub struct TaskSummary {
1320    pub id: String,
1321    pub name: String,
1322    pub health: HealthStatus,
1323}
1324
1325impl Default for TaskRuntime {
1326    fn default() -> Self {
1327        Self::new()
1328    }
1329}