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}