Skip to main content

foxtive_supervisor/
contracts.rs

1use crate::enums::{
2    BackoffStrategy, CircuitBreakerConfig, HealthStatus, RestartPolicy, SupervisorEvent, TaskState,
3};
4use std::time::Duration;
5
6/// Core trait for any long-running task that needs supervision
7#[async_trait::async_trait]
8pub trait SupervisedTask: Send + Sync {
9    // REQUIRED METHODS
10
11    /// Unique identifier for this task (used in logs, monitoring, and dependency resolution)
12    fn id(&self) -> &'static str;
13
14    /// Human-readable name for this task (used in logs and monitoring)
15    fn name(&self) -> String {
16        self.id().to_string()
17    }
18
19    /// Main task execution - this should run until completion or error
20    async fn run(&self) -> anyhow::Result<()>;
21
22    // DEPENDENCY MANAGEMENT
23
24    /// Declare task IDs that must complete setup before this task starts
25    fn dependencies(&self) -> &'static [&'static str] {
26        &[]
27    }
28
29    // OPTIONAL CONFIGURATION
30
31    /// Restart policy when task fails or panics
32    fn restart_policy(&self) -> RestartPolicy {
33        RestartPolicy::Always
34    }
35
36    /// Backoff strategy between restart attempts
37    fn backoff_strategy(&self) -> BackoffStrategy {
38        BackoffStrategy::default()
39    }
40
41    /// Priority of the task (higher values = higher priority)
42    /// Used when concurrency limits are reached to decide which tasks start first
43    fn priority(&self) -> i32 {
44        0
45    }
46
47    /// Optional concurrency limit for this specific task
48    /// This is useful if the task is part of a pool or if multiple instances exist
49    fn concurrency_limit(&self) -> Option<usize> {
50        None
51    }
52
53    /// Configuration for the task's circuit breaker
54    /// If `None`, the circuit breaker is disabled for this task
55    fn circuit_breaker(&self) -> Option<CircuitBreakerConfig> {
56        None
57    }
58
59    /// Maximum time to wait for the task to finish its `run()` and `on_shutdown()`
60    /// before forced termination during supervisor shutdown.
61    fn shutdown_timeout(&self) -> Duration {
62        Duration::from_secs(30)
63    }
64
65    /// Optional cron expression for scheduled execution
66    #[cfg(feature = "cron")]
67    fn cron_schedule(&self) -> Option<&'static str> {
68        None
69    }
70
71    /// Optional initial delay before the first execution
72    ///
73    /// This is useful for staggering task startup to prevent thundering herd problems
74    /// or for giving dependencies time to initialize.
75    fn initial_delay(&self) -> Option<Duration> {
76        None
77    }
78
79    /// Optional jitter to add randomness to the initial delay
80    ///
81    /// Returns a tuple of (min_jitter, max_jitter) that will be randomly added
82    /// to the initial delay. This helps prevent thundering herd problems in
83    /// distributed deployments where multiple instances start simultaneously.
84    ///
85    /// Example: If initial_delay is 5s and jitter is (0, 2s), the actual delay
86    /// will be between 5s and 7s.
87    fn jitter(&self) -> Option<(Duration, Duration)> {
88        None
89    }
90
91    /// Minimum time between restart attempts (rate limiting)
92    ///
93    /// This prevents tasks from restarting too frequently, which can overwhelm
94    /// external services or resources. If a task fails and wants to restart,
95    /// it will wait at least this duration before attempting again.
96    ///
97    /// This is applied in addition to the backoff strategy - the actual delay
98    /// will be the maximum of the backoff delay and this minimum restart interval.
99    fn min_restart_interval(&self) -> Option<Duration> {
100        None
101    }
102
103    /// Time window restrictions for task execution
104    ///
105    /// Returns optional start and end times (as hours in 24-hour format) during
106    /// which the task is allowed to run. Outside this window, the task will wait
107    /// until the next allowed time.
108    ///
109    /// Example: Some((9, 17)) means the task can only run between 9 AM and 5 PM.
110    /// Use None for either value to indicate no restriction on that boundary.
111    ///
112    /// Note: Times are evaluated in UTC by default.
113    fn execution_time_window(&self) -> Option<(Option<u8>, Option<u8>)> {
114        None
115    }
116
117    /// Task group identifier for atomic operations
118    ///
119    /// Tasks with the same group ID can be started, stopped, or managed together.
120    /// This is useful for related tasks that should be treated as a unit.
121    ///
122    /// Example: A database connection pool and its query processor might be in
123    /// the same group so they start and stop together.
124    fn group_id(&self) -> Option<&'static str> {
125        None
126    }
127
128    /// Conditional dependencies based on environment or runtime conditions
129    ///
130    /// Returns a list of dependencies that are only active when certain conditions are met.
131    /// Each tuple contains (dependency_id, condition_function).
132    /// The condition function receives the current environment context and returns true
133    /// if the dependency should be enforced.
134    ///
135    /// Example: Only depend on "cache-service" if USE_CACHE env var is set
136    /// ```ignore
137    /// vec![("cache-service", Box::new(|| std::env::var("USE_CACHE").is_ok()))]
138    /// ```
139    #[allow(clippy::type_complexity)]
140    fn conditional_dependencies(&self) -> Vec<(&'static str, Box<dyn Fn() -> bool + Send + Sync>)> {
141        Vec::new()
142    }
143
144    /// Get all active dependencies (regular + conditional that evaluate to true)
145    ///
146    /// This combines regular dependencies with conditional dependencies whose
147    /// conditions currently evaluate to true.
148    fn active_dependencies(&self) -> Vec<&'static str> {
149        let mut deps = self.dependencies().to_vec();
150
151        // Add conditional dependencies whose conditions are met
152        for (dep_id, condition) in self.conditional_dependencies() {
153            if condition() {
154                deps.push(dep_id);
155            }
156        }
157
158        deps
159    }
160
161    // LIFECYCLE HOOKS
162
163    /// Called once before the first run() attempt
164    async fn setup(&self) -> anyhow::Result<()> {
165        Ok(())
166    }
167
168    /// Called after task stops (success, failure, or panic)
169    ///
170    /// **Purpose:** Internal resource cleanup and teardown.
171    ///
172    /// This hook is called automatically by the supervision loop whenever a task's
173    /// `run()` method completes (successfully or with error) or panics. It's meant for:
174    /// - Closing database connections
175    /// - Releasing file handles
176    /// - Dropping temporary resources
177    /// - Any internal cleanup needed before restart or shutdown
178    ///
179    /// **When it's called:**
180    /// - After every `run()` completion (success or failure)
181    /// - After a panic is caught
182    /// - When task receives Stop command
183    /// - During supervisor shutdown
184    ///
185    /// **Important:** This is NOT for graceful shutdown logic. Use `on_shutdown()` instead
186    /// if you need to perform actions specifically during supervisor-initiated shutdown.
187    async fn cleanup(&self) {
188        // Default: no cleanup
189    }
190
191    // RESTART CONTROL
192
193    /// Dynamic restart control - called before each restart
194    async fn should_restart(&self, _attempt: usize, _last_error: &str) -> bool {
195        true
196    }
197
198    // MONITORING & OBSERVABILITY
199
200    /// Health check for monitoring endpoints
201    async fn health_check(&self) -> HealthStatus {
202        HealthStatus::Healthy
203    }
204
205    /// Return task-specific metrics (optional)
206    async fn metrics(&self) -> Option<TaskMetrics> {
207        None
208    }
209
210    /// Called before each restart (not called on first attempt)
211    async fn on_restart(&self, _attempt: usize) {
212        // Default: no action
213    }
214
215    // ERROR HANDLING HOOKS
216
217    /// Called when run() returns an error
218    async fn on_error(&self, _error: &str, _attempt: usize) {
219        // Default: no action (supervisor logs by default)
220    }
221
222    /// Called when run() panics
223    async fn on_panic(&self, _panic_info: &str, _attempt: usize) {
224        // Default: no action (supervisor logs by default)
225    }
226
227    /// Called when the service is shutting down
228    ///
229    /// **Purpose:** Graceful shutdown hook for external/user-defined cleanup.
230    ///
231    /// This hook is called ONLY during supervisor-initiated graceful shutdown,
232    /// AFTER the supervision loop has terminated. It's meant for:
233    /// - Flushing buffers to disk/network
234    /// - Sending final notifications
235    /// - Performing graceful disconnections
236    /// - Any user-defined shutdown logic that should run once at the end
237    ///
238    /// **When it's called:**
239    /// - ONLY during `TaskRuntime::shutdown()`
240    /// - AFTER the supervision loop ends
241    /// - AFTER `cleanup()` has been called
242    /// - Once per task, in dependency-aware order
243    ///
244    /// **Key difference from `cleanup()`:**
245    /// - `cleanup()` = Internal teardown, called after EVERY run() completion
246    /// - `on_shutdown()` = Graceful shutdown, called ONCE during supervisor shutdown
247    ///
248    /// **Example:**
249    /// ```ignore
250    /// async fn on_shutdown(&self) {
251    ///     // Flush pending messages
252    ///     self.message_queue.flush().await.ok();
253    ///     // Notify monitoring service
254    ///     self.notify_shutdown().await.ok();
255    /// }
256    /// ```
257    async fn on_shutdown(&self) {
258        // Default implementation does nothing
259    }
260}
261
262/// Extension trait for tasks that need state tracking
263#[async_trait::async_trait]
264pub trait StatefulTask: SupervisedTask {
265    /// Get the current state of the task
266    async fn state(&self) -> TaskState;
267}
268
269/// Trait for components that listen to supervisor events
270#[async_trait::async_trait]
271pub trait SupervisorEventListener: Send + Sync {
272    /// Called when a supervisor event occurs
273    async fn on_event(&self, event: SupervisorEvent);
274}
275
276/// Optional metrics that tasks can expose
277#[derive(Debug, Clone)]
278pub struct TaskMetrics {
279    /// Number of successful operations
280    pub success_count: u64,
281    /// Number of failed operations
282    pub error_count: u64,
283    /// Number of messages/items processed
284    pub processed_count: u64,
285    /// Current processing rate (per second)
286    pub processing_rate: f64,
287    /// Average processing time (milliseconds)
288    pub avg_processing_time_ms: f64,
289    /// Custom metrics (key-value pairs)
290    pub custom: std::collections::HashMap<String, f64>,
291}
292
293impl Default for TaskMetrics {
294    fn default() -> Self {
295        Self {
296            success_count: 0,
297            error_count: 0,
298            processed_count: 0,
299            processing_rate: 0.0,
300            avg_processing_time_ms: 0.0,
301            custom: std::collections::HashMap::new(),
302        }
303    }
304}
305
306impl TaskMetrics {
307    pub fn new() -> Self {
308        Self::default()
309    }
310
311    /// Add a custom metric
312    pub fn add_custom(&mut self, key: impl Into<String>, value: f64) {
313        self.custom.insert(key.into(), value);
314    }
315}