1use std::time::Duration;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5pub enum SupervisionStatus {
6 CompletedNormally,
8 MaxAttemptsReached,
10 ManuallyStopped,
12 RestartPrevented,
14 SetupFailed,
16 DependencyFailed,
18 CircuitBreakerOpened,
20}
21
22#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
24pub enum RestartPolicy {
25 #[default]
28 Always,
29
30 MaxAttempts(usize),
33
34 Never,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub enum TaskState {
41 Running,
43 Paused,
45 Retrying,
47 ShuttingDown,
49 Stopped,
51 CircuitBreakerOpen,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
57pub enum ControlMessage {
58 Pause,
60 Resume,
62 Stop,
64 Restart,
66 ResetCircuitBreaker,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub enum SupervisorEvent {
73 TaskRegistered { id: String, name: String },
75 TaskSetupStarted { id: String, name: String },
77 TaskSetupCompleted { id: String, name: String },
79 TaskSetupFailed { id: String, name: String, error: String },
81 TaskStarted { id: String, name: String, attempt: usize },
83 TaskFinished { id: String, name: String, attempt: usize },
85 TaskFailed { id: String, name: String, attempt: usize, error: String },
87 TaskPanicked { id: String, name: String, attempt: usize, panic_info: String },
89 TaskBackoff { id: String, name: String, attempt: usize, delay: Duration },
91 TaskStopped { id: String, name: String },
93 TaskPaused { id: String, name: String },
95 TaskResumed { id: String, name: String },
97 TaskMaxAttemptsReached { id: String, name: String, attempts: usize },
99 TaskRestartPrevented { id: String, name: String, attempt: usize },
101 TaskRemoved { id: String, name: String },
103 CircuitBreakerTripped { id: String, name: String, consecutive_failures: usize },
105 CircuitBreakerReset { id: String, name: String },
107 CircuitBreakerHalfOpen { id: String, name: String },
109 SupervisorShutdownStarted,
111 SupervisorShutdownCompleted,
113 TaskConfigUpdated {
115 id: String,
116 name: String,
117 field: String,
118 old_value: String,
119 new_value: String
120 },
121}
122
123#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
125pub enum HealthStatus {
126 Healthy,
128
129 Degraded { reason: String },
131
132 Unhealthy { reason: String },
134
135 #[default]
137 Unknown,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct CircuitBreakerConfig {
143 pub failure_threshold: usize,
145 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#[derive(Debug, Clone)]
160pub struct TaskConfig {
161 pub restart_policy: RestartPolicy,
163 pub backoff_strategy: BackoffStrategy,
165 pub enabled: bool,
167}
168
169impl TaskConfig {
170 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 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
195pub enum BackoffStrategy {
197 Fixed(Duration),
201
202 Exponential { initial: Duration, max: Duration },
208
209 Linear {
215 initial: Duration,
216 increment: Duration,
217 max: Duration,
218 },
219
220 Fibonacci { initial: Duration, max: Duration },
226
227 Custom(Box<dyn Fn(usize) -> Duration + Send + Sync>),
231}
232
233impl 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
263impl 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 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 pub fn calculate_delay(&self, attempt: usize) -> Duration {
306 match self {
307 Self::Fixed(duration) => *duration,
308
309 Self::Exponential { initial, max } => {
310 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 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 pub fn fixed(duration: Duration) -> Self {
354 Self::Fixed(duration)
355 }
356
357 pub fn exponential() -> Self {
359 Self::Exponential {
360 initial: Duration::from_secs(2),
361 max: Duration::from_secs(60),
362 }
363 }
364
365 pub fn exponential_custom(initial: Duration, max: Duration) -> Self {
367 Self::Exponential { initial, max }
368 }
369
370 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 pub fn linear_custom(initial: Duration, increment: Duration, max: Duration) -> Self {
381 Self::Linear {
382 initial,
383 increment,
384 max,
385 }
386 }
387
388 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 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 pub fn is_healthy(&self) -> bool {
408 matches!(self, Self::Healthy)
409 }
410
411 pub fn is_unhealthy(&self) -> bool {
413 matches!(self, Self::Unhealthy { .. })
414 }
415
416 pub fn is_degraded(&self) -> bool {
418 matches!(self, Self::Degraded { .. })
419 }
420
421 pub fn reason(&self) -> Option<&str> {
423 match self {
424 Self::Degraded { reason } | Self::Unhealthy { reason } => Some(reason),
425 _ => None,
426 }
427 }
428
429 pub fn healthy() -> Self {
431 Self::Healthy
432 }
433
434 pub fn degraded(reason: impl Into<String>) -> Self {
436 Self::Degraded {
437 reason: reason.into(),
438 }
439 }
440
441 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)); }
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}