Skip to main content

foxtive_supervisor/
contracts.rs

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