Skip to main content

foxtive_supervisor/
enums.rs

1use std::time::Duration;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5pub enum SupervisionStatus {
6    /// Task completed successfully (expected termination)
7    CompletedNormally,
8    /// Task failed and won't restart (max attempts reached)
9    MaxAttemptsReached,
10    /// Task was manually stopped
11    ManuallyStopped,
12    /// Restart was prevented by should_restart hook
13    RestartPrevented,
14    /// Setup failed, task never started
15    SetupFailed,
16    /// A declared dependency failed its setup phase
17    DependencyFailed,
18    /// Task was stopped because the circuit breaker opened
19    CircuitBreakerOpened,
20}
21
22/// Defines when and how often a task should restart after failure
23#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
24pub enum RestartPolicy {
25    /// Always restart, no matter how many times it fails
26    /// Use for: Critical services that must always run
27    #[default]
28    Always,
29
30    /// Restart up to N times, then give up
31    /// Use for: Tasks with a reasonable failure threshold
32    MaxAttempts(usize),
33
34    /// Never restart (task runs once)
35    /// Use for: One-shot tasks, testing, or explicit manual control
36    Never,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub enum TaskState {
41    /// Task is running normally
42    Running,
43    /// Task is paused (not processing)
44    Paused,
45    /// Task is in error state but will retry
46    Retrying,
47    /// Task is shutting down
48    ShuttingDown,
49    /// Task has stopped
50    Stopped,
51    /// Task's circuit breaker is open (no executions allowed)
52    CircuitBreakerOpen,
53}
54
55/// Control messages sent to supervised tasks
56#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
57pub enum ControlMessage {
58    /// Pause the task (if supported)
59    Pause,
60    /// Resume a paused task
61    Resume,
62    /// Stop the task permanently
63    Stop,
64    /// Force an immediate restart
65    Restart,
66    /// Reset the task's circuit breaker
67    ResetCircuitBreaker,
68}
69
70/// Lifecycle events emitted by the supervisor
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub enum SupervisorEvent {
73    /// A new task was registered
74    TaskRegistered { id: String, name: String },
75    /// A task started its setup phase
76    TaskSetupStarted { id: String, name: String },
77    /// A task completed its setup phase successfully
78    TaskSetupCompleted { id: String, name: String },
79    /// A task setup failed
80    TaskSetupFailed { id: String, name: String, error: String },
81    /// A task's execution started (or restarted)
82    TaskStarted { id: String, name: String, attempt: usize },
83    /// A task's execution finished successfully
84    TaskFinished { id: String, name: String, attempt: usize },
85    /// A task's execution failed with an error
86    TaskFailed { id: String, name: String, attempt: usize, error: String },
87    /// A task's execution panicked
88    TaskPanicked { id: String, name: String, attempt: usize, panic_info: String },
89    /// A task is entering backoff before restart
90    TaskBackoff { id: String, name: String, attempt: usize, delay: Duration },
91    /// A task was manually stopped
92    TaskStopped { id: String, name: String },
93    /// A task was paused
94    TaskPaused { id: String, name: String },
95    /// A task was resumed
96    TaskResumed { id: String, name: String },
97    /// A task reached its maximum restart attempts
98    TaskMaxAttemptsReached { id: String, name: String, attempts: usize },
99    /// A task restart was prevented by a hook
100    TaskRestartPrevented { id: String, name: String, attempt: usize },
101    /// A task was removed from the supervisor
102    TaskRemoved { id: String, name: String },
103    /// A task's circuit breaker tripped (opened)
104    CircuitBreakerTripped { id: String, name: String, consecutive_failures: usize },
105    /// A task's circuit breaker was reset (closed)
106    CircuitBreakerReset { id: String, name: String },
107    /// A task's circuit breaker entered half-open state
108    CircuitBreakerHalfOpen { id: String, name: String },
109    /// Supervisor is shutting down
110    SupervisorShutdownStarted,
111    /// Supervisor completed shutdown
112    SupervisorShutdownCompleted,
113    /// A task's configuration was updated at runtime
114    TaskConfigUpdated { 
115        id: String, 
116        name: String, 
117        field: String, 
118        old_value: String, 
119        new_value: String 
120    },
121}
122
123/// Health status for monitoring and observability
124#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
125pub enum HealthStatus {
126    /// Task is operating normally
127    Healthy,
128
129    /// Task is functioning but with reduced capacity or warnings
130    Degraded { reason: String },
131
132    /// Task is not functioning properly
133    Unhealthy { reason: String },
134
135    /// Task is in unknown state (initialization, transition)
136    #[default]
137    Unknown,
138}
139
140/// Configuration for a task's circuit breaker
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct CircuitBreakerConfig {
143    /// Number of consecutive failures before the circuit opens
144    pub failure_threshold: usize,
145    /// How long the circuit stays open before transitioning to half-open
146    pub reset_timeout: Duration,
147}
148
149impl Default for CircuitBreakerConfig {
150    fn default() -> Self {
151        Self {
152            failure_threshold: 5,
153            reset_timeout: Duration::from_secs(60),
154        }
155    }
156}
157
158/// Runtime configuration for a supervised task that can be hot-reloaded
159#[derive(Debug, Clone)]
160pub struct TaskConfig {
161    /// Restart policy for the task
162    pub restart_policy: RestartPolicy,
163    /// Backoff strategy between restart attempts
164    pub backoff_strategy: BackoffStrategy,
165    /// Whether the task is enabled (can be disabled without removing)
166    pub enabled: bool,
167}
168
169impl TaskConfig {
170    /// Create a new TaskConfig from a SupervisedTask's current configuration
171    pub fn from_task(task: &dyn crate::contracts::SupervisedTask) -> Self {
172        Self {
173            restart_policy: task.restart_policy(),
174            backoff_strategy: task.backoff_strategy(),
175            enabled: true,
176        }
177    }
178
179    /// Create a TaskConfig with default values
180    pub fn new() -> Self {
181        Self {
182            restart_policy: RestartPolicy::default(),
183            backoff_strategy: BackoffStrategy::default(),
184            enabled: true,
185        }
186    }
187}
188
189impl Default for TaskConfig {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195/// Defines the delay between restart attempts
196pub enum BackoffStrategy {
197    /// Fixed delay between restarts
198    ///
199    /// Example: Always wait 5 seconds
200    Fixed(Duration),
201
202    /// Exponential backoff: initial * 2^(attempt-1), capped at max
203    ///
204    /// Example: 2s -> 4s -> 8s -> 16s -> ... -> 60s (max)
205    ///
206    /// Best for: Network failures, external service issues
207    Exponential { initial: Duration, max: Duration },
208
209    /// Linear backoff: initial + (increment * attempt)
210    ///
211    /// Example: 5s -> 10s -> 15s -> 20s -> ... -> 60s (max)
212    ///
213    /// Best for: Predictable delay patterns
214    Linear {
215        initial: Duration,
216        increment: Duration,
217        max: Duration,
218    },
219
220    /// Fibonacci backoff: delays follow Fibonacci sequence
221    ///
222    /// Example: 1s -> 1s -> 2s -> 3s -> 5s -> 8s -> ... -> max
223    ///
224    /// Best for: Graceful degradation scenarios
225    Fibonacci { initial: Duration, max: Duration },
226
227    /// Custom backoff with user-defined delay calculation
228    ///
229    /// Receives attempt number, returns delay duration
230    Custom(Box<dyn Fn(usize) -> Duration + Send + Sync>),
231}
232
233// Manual Debug implementation
234impl std::fmt::Debug for BackoffStrategy {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        match self {
237            Self::Fixed(duration) => f.debug_tuple("Fixed").field(duration).finish(),
238            Self::Exponential { initial, max } => f
239                .debug_struct("Exponential")
240                .field("initial", initial)
241                .field("max", max)
242                .finish(),
243            Self::Linear {
244                initial,
245                increment,
246                max,
247            } => f
248                .debug_struct("Linear")
249                .field("initial", initial)
250                .field("increment", increment)
251                .field("max", max)
252                .finish(),
253            Self::Fibonacci { initial, max } => f
254                .debug_struct("Fibonacci")
255                .field("initial", initial)
256                .field("max", max)
257                .finish(),
258            Self::Custom(_) => f.debug_tuple("Custom").field(&"<function>").finish(),
259        }
260    }
261}
262
263// Manual Clone implementation
264impl Clone for BackoffStrategy {
265    fn clone(&self) -> Self {
266        match self {
267            Self::Fixed(duration) => Self::Fixed(*duration),
268            Self::Exponential { initial, max } => Self::Exponential {
269                initial: *initial,
270                max: *max,
271            },
272            Self::Linear {
273                initial,
274                increment,
275                max,
276            } => Self::Linear {
277                initial: *initial,
278                increment: *increment,
279                max: *max,
280            },
281            Self::Fibonacci { initial, max } => Self::Fibonacci {
282                initial: *initial,
283                max: *max,
284            },
285            Self::Custom(_) => {
286                // For Custom variant, we clone with default exponential strategy
287                // This is a limitation - custom functions can't be cloned
288                Self::default()
289            }
290        }
291    }
292}
293
294impl Default for BackoffStrategy {
295    fn default() -> Self {
296        Self::Exponential {
297            initial: Duration::from_secs(2),
298            max: Duration::from_secs(60),
299        }
300    }
301}
302
303impl BackoffStrategy {
304    /// Calculate the delay for a given attempt number (1-indexed)
305    pub fn calculate_delay(&self, attempt: usize) -> Duration {
306        match self {
307            Self::Fixed(duration) => *duration,
308
309            Self::Exponential { initial, max } => {
310                // 2^(attempt-1), but cap the exponent to prevent overflow
311                let exponent = attempt.saturating_sub(1).min(20);
312                let multiplier = 2_u32.saturating_pow(exponent as u32);
313                initial.saturating_mul(multiplier).min(*max)
314            }
315
316            Self::Linear {
317                initial,
318                increment,
319                max,
320            } => {
321                let total_increment = increment.saturating_mul(attempt.saturating_sub(1) as u32);
322                initial.saturating_add(total_increment).min(*max)
323            }
324
325            Self::Fibonacci { initial, max } => {
326                let fib_multiplier = Self::fibonacci(attempt);
327                initial.saturating_mul(fib_multiplier as u32).min(*max)
328            }
329
330            Self::Custom(func) => func(attempt),
331        }
332    }
333
334    /// Calculate Fibonacci number for backoff
335    fn fibonacci(n: usize) -> usize {
336        match n {
337            0 => 0,
338            1 | 2 => 1,
339            n => {
340                let mut a: usize = 1;
341                let mut b = 1;
342                for _ in 2..n {
343                    let next = a.saturating_add(b);
344                    a = b;
345                    b = next;
346                }
347                b
348            }
349        }
350    }
351
352    /// Create a fixed delay strategy
353    pub fn fixed(duration: Duration) -> Self {
354        Self::Fixed(duration)
355    }
356
357    /// Create exponential backoff with defaults (2s -> 60s)
358    pub fn exponential() -> Self {
359        Self::Exponential {
360            initial: Duration::from_secs(2),
361            max: Duration::from_secs(60),
362        }
363    }
364
365    /// Create exponential backoff with custom parameters
366    pub fn exponential_custom(initial: Duration, max: Duration) -> Self {
367        Self::Exponential { initial, max }
368    }
369
370    /// Create linear backoff with defaults (5s increments, max 60s)
371    pub fn linear() -> Self {
372        Self::Linear {
373            initial: Duration::from_secs(5),
374            increment: Duration::from_secs(5),
375            max: Duration::from_secs(60),
376        }
377    }
378
379    /// Create linear backoff with custom parameters
380    pub fn linear_custom(initial: Duration, increment: Duration, max: Duration) -> Self {
381        Self::Linear {
382            initial,
383            increment,
384            max,
385        }
386    }
387
388    /// Create Fibonacci backoff with defaults (1s -> 60s)
389    pub fn fibonacci_with_default() -> Self {
390        Self::Fibonacci {
391            initial: Duration::from_secs(1),
392            max: Duration::from_secs(60),
393        }
394    }
395
396    /// Create custom backoff with user-defined function
397    pub fn custom<F>(func: F) -> Self
398    where
399        F: Fn(usize) -> Duration + Send + Sync + 'static,
400    {
401        Self::Custom(Box::new(func))
402    }
403}
404
405impl HealthStatus {
406    /// Check if the status is healthy
407    pub fn is_healthy(&self) -> bool {
408        matches!(self, Self::Healthy)
409    }
410
411    /// Check if the status is unhealthy
412    pub fn is_unhealthy(&self) -> bool {
413        matches!(self, Self::Unhealthy { .. })
414    }
415
416    /// Check if the status is degraded
417    pub fn is_degraded(&self) -> bool {
418        matches!(self, Self::Degraded { .. })
419    }
420
421    /// Get the reason for non-healthy status
422    pub fn reason(&self) -> Option<&str> {
423        match self {
424            Self::Degraded { reason } | Self::Unhealthy { reason } => Some(reason),
425            _ => None,
426        }
427    }
428
429    /// Create a healthy status
430    pub fn healthy() -> Self {
431        Self::Healthy
432    }
433
434    /// Create a degraded status with reason
435    pub fn degraded(reason: impl Into<String>) -> Self {
436        Self::Degraded {
437            reason: reason.into(),
438        }
439    }
440
441    /// Create an unhealthy status with reason
442    pub fn unhealthy(reason: impl Into<String>) -> Self {
443        Self::Unhealthy {
444            reason: reason.into(),
445        }
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn test_exponential_backoff() {
455        let strategy = BackoffStrategy::exponential();
456
457        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(2));
458        assert_eq!(strategy.calculate_delay(2), Duration::from_secs(4));
459        assert_eq!(strategy.calculate_delay(3), Duration::from_secs(8));
460        assert_eq!(strategy.calculate_delay(4), Duration::from_secs(16));
461        assert_eq!(strategy.calculate_delay(5), Duration::from_secs(32));
462        assert_eq!(strategy.calculate_delay(6), Duration::from_secs(60)); // capped at max
463    }
464
465    #[test]
466    fn test_linear_backoff() {
467        let strategy = BackoffStrategy::linear();
468
469        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(5));
470        assert_eq!(strategy.calculate_delay(2), Duration::from_secs(10));
471        assert_eq!(strategy.calculate_delay(3), Duration::from_secs(15));
472        assert_eq!(strategy.calculate_delay(4), Duration::from_secs(20));
473    }
474
475    #[test]
476    fn test_fibonacci_backoff() {
477        let strategy = BackoffStrategy::fibonacci_with_default();
478
479        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(1));
480        assert_eq!(strategy.calculate_delay(2), Duration::from_secs(1));
481        assert_eq!(strategy.calculate_delay(3), Duration::from_secs(2));
482        assert_eq!(strategy.calculate_delay(4), Duration::from_secs(3));
483        assert_eq!(strategy.calculate_delay(5), Duration::from_secs(5));
484        assert_eq!(strategy.calculate_delay(6), Duration::from_secs(8));
485    }
486
487    #[test]
488    fn test_fixed_backoff() {
489        let strategy = BackoffStrategy::fixed(Duration::from_secs(10));
490
491        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(10));
492        assert_eq!(strategy.calculate_delay(5), Duration::from_secs(10));
493        assert_eq!(strategy.calculate_delay(100), Duration::from_secs(10));
494    }
495
496    #[test]
497    fn test_custom_backoff() {
498        let strategy = BackoffStrategy::custom(|attempt| Duration::from_secs(attempt as u64 * 3));
499
500        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(3));
501        assert_eq!(strategy.calculate_delay(2), Duration::from_secs(6));
502        assert_eq!(strategy.calculate_delay(5), Duration::from_secs(15));
503    }
504
505    #[test]
506    fn test_health_status() {
507        let healthy = HealthStatus::healthy();
508        assert!(healthy.is_healthy());
509        assert!(!healthy.is_unhealthy());
510        assert_eq!(healthy.reason(), None);
511
512        let degraded = HealthStatus::degraded("High memory usage");
513        assert!(degraded.is_degraded());
514        assert_eq!(degraded.reason(), Some("High memory usage"));
515
516        let unhealthy = HealthStatus::unhealthy("Connection lost");
517        assert!(unhealthy.is_unhealthy());
518        assert_eq!(unhealthy.reason(), Some("Connection lost"));
519    }
520
521    #[test]
522    fn test_backoff_debug() {
523        let exponential = BackoffStrategy::exponential();
524        let debug_str = format!("{:?}", exponential);
525        assert!(debug_str.contains("Exponential"));
526
527        let custom = BackoffStrategy::custom(|n| Duration::from_secs(n as u64));
528        let debug_str = format!("{:?}", custom);
529        assert!(debug_str.contains("Custom"));
530    }
531
532    #[test]
533    fn test_backoff_clone() {
534        let original = BackoffStrategy::exponential();
535        let cloned = original.clone();
536
537        assert_eq!(original.calculate_delay(3), cloned.calculate_delay(3));
538    }
539}