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