Skip to main content

foxtive_supervisor/
enums.rs

1use serde::{Deserialize, Serialize};
2use std::time::Duration;
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 {
81        id: String,
82        name: String,
83        error: String,
84    },
85    /// A task's execution started (or restarted)
86    TaskStarted {
87        id: String,
88        name: String,
89        attempt: usize,
90    },
91    /// A task's execution finished successfully
92    TaskFinished {
93        id: String,
94        name: String,
95        attempt: usize,
96    },
97    /// A task's execution failed with an error
98    TaskFailed {
99        id: String,
100        name: String,
101        attempt: usize,
102        error: String,
103    },
104    /// A task's execution panicked
105    TaskPanicked {
106        id: String,
107        name: String,
108        attempt: usize,
109        panic_info: String,
110    },
111    /// A task is entering backoff before restart
112    TaskBackoff {
113        id: String,
114        name: String,
115        attempt: usize,
116        delay: Duration,
117    },
118    /// A task was manually stopped
119    TaskStopped { id: String, name: String },
120    /// A task was paused
121    TaskPaused { id: String, name: String },
122    /// A task was resumed
123    TaskResumed { id: String, name: String },
124    /// A task reached its maximum restart attempts
125    TaskMaxAttemptsReached {
126        id: String,
127        name: String,
128        attempts: usize,
129    },
130    /// A task restart was prevented by a hook
131    TaskRestartPrevented {
132        id: String,
133        name: String,
134        attempt: usize,
135    },
136    /// A task was removed from the supervisor
137    TaskRemoved { id: String, name: String },
138    /// A task's circuit breaker tripped (opened)
139    CircuitBreakerTripped {
140        id: String,
141        name: String,
142        consecutive_failures: usize,
143    },
144    /// A task's circuit breaker was reset (closed)
145    CircuitBreakerReset { id: String, name: String },
146    /// A task's circuit breaker entered half-open state
147    CircuitBreakerHalfOpen { id: String, name: String },
148    /// Supervisor is shutting down
149    SupervisorShutdownStarted,
150    /// Supervisor completed shutdown
151    SupervisorShutdownCompleted,
152    /// A task's configuration was updated at runtime
153    TaskConfigUpdated {
154        id: String,
155        name: String,
156        field: String,
157        old_value: String,
158        new_value: String,
159    },
160}
161
162/// Health status for monitoring and observability
163#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
164pub enum HealthStatus {
165    /// Task is operating normally
166    Healthy,
167
168    /// Task is functioning but with reduced capacity or warnings
169    Degraded { reason: String },
170
171    /// Task is not functioning properly
172    Unhealthy { reason: String },
173
174    /// Task is in unknown state (initialization, transition)
175    #[default]
176    Unknown,
177}
178
179/// Configuration for a task's circuit breaker
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct CircuitBreakerConfig {
182    /// Number of consecutive failures before the circuit opens
183    pub failure_threshold: usize,
184    /// How long the circuit stays open before transitioning to half-open
185    pub reset_timeout: Duration,
186}
187
188impl Default for CircuitBreakerConfig {
189    fn default() -> Self {
190        Self {
191            failure_threshold: 5,
192            reset_timeout: Duration::from_secs(60),
193        }
194    }
195}
196
197/// Runtime configuration for a supervised task that can be hot-reloaded
198#[derive(Debug, Clone)]
199pub struct TaskConfig {
200    /// Restart policy for the task
201    pub restart_policy: RestartPolicy,
202    /// Backoff strategy between restart attempts
203    pub backoff_strategy: BackoffStrategy,
204    /// Whether the task is enabled (can be disabled without removing)
205    pub enabled: bool,
206}
207
208impl TaskConfig {
209    /// Create a new TaskConfig from a SupervisedTask's current configuration
210    pub fn from_task(task: &dyn crate::contracts::SupervisedTask) -> Self {
211        Self {
212            restart_policy: task.restart_policy(),
213            backoff_strategy: task.backoff_strategy(),
214            enabled: true,
215        }
216    }
217
218    /// Create a TaskConfig with default values
219    pub fn new() -> Self {
220        Self {
221            restart_policy: RestartPolicy::default(),
222            backoff_strategy: BackoffStrategy::default(),
223            enabled: true,
224        }
225    }
226}
227
228impl Default for TaskConfig {
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234/// Defines the delay between restart attempts
235pub enum BackoffStrategy {
236    /// Fixed delay between restarts
237    ///
238    /// Example: Always wait 5 seconds
239    Fixed(Duration),
240
241    /// Exponential backoff: initial * 2^(attempt-1), capped at max
242    ///
243    /// Example: 2s -> 4s -> 8s -> 16s -> ... -> 60s (max)
244    ///
245    /// Best for: Network failures, external service issues
246    Exponential { initial: Duration, max: Duration },
247
248    /// Linear backoff: initial + (increment * attempt)
249    ///
250    /// Example: 5s -> 10s -> 15s -> 20s -> ... -> 60s (max)
251    ///
252    /// Best for: Predictable delay patterns
253    Linear {
254        initial: Duration,
255        increment: Duration,
256        max: Duration,
257    },
258
259    /// Fibonacci backoff: delays follow Fibonacci sequence
260    ///
261    /// Example: 1s -> 1s -> 2s -> 3s -> 5s -> 8s -> ... -> max
262    ///
263    /// Best for: Graceful degradation scenarios
264    Fibonacci { initial: Duration, max: Duration },
265
266    /// Custom backoff with user-defined delay calculation
267    ///
268    /// Receives attempt number, returns delay duration.
269    /// Wrapped in Arc for correct cloning behavior.
270    Custom(std::sync::Arc<dyn Fn(usize) -> Duration + Send + Sync>),
271}
272
273// Manual Debug implementation
274impl std::fmt::Debug for BackoffStrategy {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        match self {
277            Self::Fixed(duration) => f.debug_tuple("Fixed").field(duration).finish(),
278            Self::Exponential { initial, max } => f
279                .debug_struct("Exponential")
280                .field("initial", initial)
281                .field("max", max)
282                .finish(),
283            Self::Linear {
284                initial,
285                increment,
286                max,
287            } => f
288                .debug_struct("Linear")
289                .field("initial", initial)
290                .field("increment", increment)
291                .field("max", max)
292                .finish(),
293            Self::Fibonacci { initial, max } => f
294                .debug_struct("Fibonacci")
295                .field("initial", initial)
296                .field("max", max)
297                .finish(),
298            Self::Custom(_) => f.debug_tuple("Custom").field(&"<function>").finish(),
299        }
300    }
301}
302
303// Manual Clone implementation
304impl Clone for BackoffStrategy {
305    fn clone(&self) -> Self {
306        match self {
307            Self::Fixed(duration) => Self::Fixed(*duration),
308            Self::Exponential { initial, max } => Self::Exponential {
309                initial: *initial,
310                max: *max,
311            },
312            Self::Linear {
313                initial,
314                increment,
315                max,
316            } => Self::Linear {
317                initial: *initial,
318                increment: *increment,
319                max: *max,
320            },
321            Self::Fibonacci { initial, max } => Self::Fibonacci {
322                initial: *initial,
323                max: *max,
324            },
325            Self::Custom(func) => {
326                // Arc clone - custom function is preserved
327                Self::Custom(func.clone())
328            }
329        }
330    }
331}
332
333impl Default for BackoffStrategy {
334    fn default() -> Self {
335        Self::Exponential {
336            initial: Duration::from_secs(2),
337            max: Duration::from_secs(60),
338        }
339    }
340}
341
342impl BackoffStrategy {
343    /// Calculate the delay for a given attempt number (1-indexed)
344    pub fn calculate_delay(&self, attempt: usize) -> Duration {
345        match self {
346            Self::Fixed(duration) => *duration,
347
348            Self::Exponential { initial, max } => {
349                // 2^(attempt-1), but cap the exponent to prevent overflow
350                let exponent = attempt.saturating_sub(1).min(20);
351                let multiplier = 2_u32.saturating_pow(exponent as u32);
352                initial.saturating_mul(multiplier).min(*max)
353            }
354
355            Self::Linear {
356                initial,
357                increment,
358                max,
359            } => {
360                let total_increment = increment.saturating_mul(attempt.saturating_sub(1) as u32);
361                initial.saturating_add(total_increment).min(*max)
362            }
363
364            Self::Fibonacci { initial, max } => {
365                let fib_multiplier = Self::fibonacci(attempt);
366                initial.saturating_mul(fib_multiplier as u32).min(*max)
367            }
368
369            Self::Custom(func) => func(attempt),
370        }
371    }
372
373    /// Calculate Fibonacci number for backoff
374    fn fibonacci(n: usize) -> usize {
375        match n {
376            0 => 0,
377            1 | 2 => 1,
378            n => {
379                let mut a: usize = 1;
380                let mut b = 1;
381                for _ in 2..n {
382                    let next = a.saturating_add(b);
383                    a = b;
384                    b = next;
385                }
386                b
387            }
388        }
389    }
390
391    /// Create a fixed delay strategy
392    pub fn fixed(duration: Duration) -> Self {
393        Self::Fixed(duration)
394    }
395
396    /// Create exponential backoff with defaults (2s -> 60s)
397    pub fn exponential() -> Self {
398        Self::Exponential {
399            initial: Duration::from_secs(2),
400            max: Duration::from_secs(60),
401        }
402    }
403
404    /// Create exponential backoff with custom parameters
405    pub fn exponential_custom(initial: Duration, max: Duration) -> Self {
406        Self::Exponential { initial, max }
407    }
408
409    /// Create linear backoff with defaults (5s increments, max 60s)
410    pub fn linear() -> Self {
411        Self::Linear {
412            initial: Duration::from_secs(5),
413            increment: Duration::from_secs(5),
414            max: Duration::from_secs(60),
415        }
416    }
417
418    /// Create linear backoff with custom parameters
419    pub fn linear_custom(initial: Duration, increment: Duration, max: Duration) -> Self {
420        Self::Linear {
421            initial,
422            increment,
423            max,
424        }
425    }
426
427    /// Create Fibonacci backoff with defaults (1s -> 60s)
428    pub fn fibonacci_with_default() -> Self {
429        Self::Fibonacci {
430            initial: Duration::from_secs(1),
431            max: Duration::from_secs(60),
432        }
433    }
434
435    /// Create custom backoff with user-defined function
436    pub fn custom<F>(func: F) -> Self
437    where
438        F: Fn(usize) -> Duration + Send + Sync + 'static,
439    {
440        Self::Custom(std::sync::Arc::new(func))
441    }
442}
443
444impl HealthStatus {
445    /// Check if the status is healthy
446    pub fn is_healthy(&self) -> bool {
447        matches!(self, Self::Healthy)
448    }
449
450    /// Check if the status is unhealthy
451    pub fn is_unhealthy(&self) -> bool {
452        matches!(self, Self::Unhealthy { .. })
453    }
454
455    /// Check if the status is degraded
456    pub fn is_degraded(&self) -> bool {
457        matches!(self, Self::Degraded { .. })
458    }
459
460    /// Get the reason for non-healthy status
461    pub fn reason(&self) -> Option<&str> {
462        match self {
463            Self::Degraded { reason } | Self::Unhealthy { reason } => Some(reason),
464            _ => None,
465        }
466    }
467
468    /// Create a healthy status
469    pub fn healthy() -> Self {
470        Self::Healthy
471    }
472
473    /// Create a degraded status with reason
474    pub fn degraded(reason: impl Into<String>) -> Self {
475        Self::Degraded {
476            reason: reason.into(),
477        }
478    }
479
480    /// Create an unhealthy status with reason
481    pub fn unhealthy(reason: impl Into<String>) -> Self {
482        Self::Unhealthy {
483            reason: reason.into(),
484        }
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    #[test]
493    fn test_exponential_backoff() {
494        let strategy = BackoffStrategy::exponential();
495
496        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(2));
497        assert_eq!(strategy.calculate_delay(2), Duration::from_secs(4));
498        assert_eq!(strategy.calculate_delay(3), Duration::from_secs(8));
499        assert_eq!(strategy.calculate_delay(4), Duration::from_secs(16));
500        assert_eq!(strategy.calculate_delay(5), Duration::from_secs(32));
501        assert_eq!(strategy.calculate_delay(6), Duration::from_secs(60)); // capped at max
502    }
503
504    #[test]
505    fn test_linear_backoff() {
506        let strategy = BackoffStrategy::linear();
507
508        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(5));
509        assert_eq!(strategy.calculate_delay(2), Duration::from_secs(10));
510        assert_eq!(strategy.calculate_delay(3), Duration::from_secs(15));
511        assert_eq!(strategy.calculate_delay(4), Duration::from_secs(20));
512    }
513
514    #[test]
515    fn test_fibonacci_backoff() {
516        let strategy = BackoffStrategy::fibonacci_with_default();
517
518        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(1));
519        assert_eq!(strategy.calculate_delay(2), Duration::from_secs(1));
520        assert_eq!(strategy.calculate_delay(3), Duration::from_secs(2));
521        assert_eq!(strategy.calculate_delay(4), Duration::from_secs(3));
522        assert_eq!(strategy.calculate_delay(5), Duration::from_secs(5));
523        assert_eq!(strategy.calculate_delay(6), Duration::from_secs(8));
524    }
525
526    #[test]
527    fn test_fixed_backoff() {
528        let strategy = BackoffStrategy::fixed(Duration::from_secs(10));
529
530        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(10));
531        assert_eq!(strategy.calculate_delay(5), Duration::from_secs(10));
532        assert_eq!(strategy.calculate_delay(100), Duration::from_secs(10));
533    }
534
535    #[test]
536    fn test_custom_backoff() {
537        let strategy = BackoffStrategy::custom(|attempt| Duration::from_secs(attempt as u64 * 3));
538
539        assert_eq!(strategy.calculate_delay(1), Duration::from_secs(3));
540        assert_eq!(strategy.calculate_delay(2), Duration::from_secs(6));
541        assert_eq!(strategy.calculate_delay(5), Duration::from_secs(15));
542    }
543
544    #[test]
545    fn test_health_status() {
546        let healthy = HealthStatus::healthy();
547        assert!(healthy.is_healthy());
548        assert!(!healthy.is_unhealthy());
549        assert_eq!(healthy.reason(), None);
550
551        let degraded = HealthStatus::degraded("High memory usage");
552        assert!(degraded.is_degraded());
553        assert_eq!(degraded.reason(), Some("High memory usage"));
554
555        let unhealthy = HealthStatus::unhealthy("Connection lost");
556        assert!(unhealthy.is_unhealthy());
557        assert_eq!(unhealthy.reason(), Some("Connection lost"));
558    }
559
560    #[test]
561    fn test_backoff_debug() {
562        let exponential = BackoffStrategy::exponential();
563        let debug_str = format!("{:?}", exponential);
564        assert!(debug_str.contains("Exponential"));
565
566        let custom = BackoffStrategy::custom(|n| Duration::from_secs(n as u64));
567        let debug_str = format!("{:?}", custom);
568        assert!(debug_str.contains("Custom"));
569    }
570
571    #[test]
572    fn test_backoff_clone() {
573        let original = BackoffStrategy::exponential();
574        let cloned = original.clone();
575
576        assert_eq!(original.calculate_delay(3), cloned.calculate_delay(3));
577    }
578}