use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub enum SupervisionStatus {
CompletedNormally,
MaxAttemptsReached,
ManuallyStopped,
RestartPrevented,
SetupFailed,
DependencyFailed,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum RestartPolicy {
#[default]
Always,
MaxAttempts(usize),
Never,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TaskState {
Running,
Paused,
Retrying,
ShuttingDown,
Stopped,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum HealthStatus {
Healthy,
Degraded { reason: String },
Unhealthy { reason: String },
#[default]
Unknown,
}
pub enum BackoffStrategy {
Fixed(Duration),
Exponential { initial: Duration, max: Duration },
Linear {
initial: Duration,
increment: Duration,
max: Duration,
},
Fibonacci { initial: Duration, max: Duration },
Custom(Box<dyn Fn(usize) -> Duration + Send + Sync>),
}
impl std::fmt::Debug for BackoffStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Fixed(duration) => f.debug_tuple("Fixed").field(duration).finish(),
Self::Exponential { initial, max } => f
.debug_struct("Exponential")
.field("initial", initial)
.field("max", max)
.finish(),
Self::Linear {
initial,
increment,
max,
} => f
.debug_struct("Linear")
.field("initial", initial)
.field("increment", increment)
.field("max", max)
.finish(),
Self::Fibonacci { initial, max } => f
.debug_struct("Fibonacci")
.field("initial", initial)
.field("max", max)
.finish(),
Self::Custom(_) => f.debug_tuple("Custom").field(&"<function>").finish(),
}
}
}
impl Clone for BackoffStrategy {
fn clone(&self) -> Self {
match self {
Self::Fixed(duration) => Self::Fixed(*duration),
Self::Exponential { initial, max } => Self::Exponential {
initial: *initial,
max: *max,
},
Self::Linear {
initial,
increment,
max,
} => Self::Linear {
initial: *initial,
increment: *increment,
max: *max,
},
Self::Fibonacci { initial, max } => Self::Fibonacci {
initial: *initial,
max: *max,
},
Self::Custom(_) => {
Self::default()
}
}
}
}
impl Default for BackoffStrategy {
fn default() -> Self {
Self::Exponential {
initial: Duration::from_secs(2),
max: Duration::from_secs(60),
}
}
}
impl BackoffStrategy {
pub fn calculate_delay(&self, attempt: usize) -> Duration {
match self {
Self::Fixed(duration) => *duration,
Self::Exponential { initial, max } => {
let exponent = attempt.saturating_sub(1).min(20);
let multiplier = 2_u32.saturating_pow(exponent as u32);
initial.saturating_mul(multiplier).min(*max)
}
Self::Linear {
initial,
increment,
max,
} => {
let total_increment = increment.saturating_mul(attempt.saturating_sub(1) as u32);
initial.saturating_add(total_increment).min(*max)
}
Self::Fibonacci { initial, max } => {
let fib_multiplier = Self::fibonacci(attempt);
initial.saturating_mul(fib_multiplier as u32).min(*max)
}
Self::Custom(func) => func(attempt),
}
}
fn fibonacci(n: usize) -> usize {
match n {
0 => 0,
1 | 2 => 1,
n => {
let mut a: usize = 1;
let mut b = 1;
for _ in 2..n {
let next = a.saturating_add(b);
a = b;
b = next;
}
b
}
}
}
pub fn fixed(duration: Duration) -> Self {
Self::Fixed(duration)
}
pub fn exponential() -> Self {
Self::Exponential {
initial: Duration::from_secs(2),
max: Duration::from_secs(60),
}
}
pub fn exponential_custom(initial: Duration, max: Duration) -> Self {
Self::Exponential { initial, max }
}
pub fn linear() -> Self {
Self::Linear {
initial: Duration::from_secs(5),
increment: Duration::from_secs(5),
max: Duration::from_secs(60),
}
}
pub fn linear_custom(initial: Duration, increment: Duration, max: Duration) -> Self {
Self::Linear {
initial,
increment,
max,
}
}
pub fn fibonacci_with_default() -> Self {
Self::Fibonacci {
initial: Duration::from_secs(1),
max: Duration::from_secs(60),
}
}
pub fn custom<F>(func: F) -> Self
where
F: Fn(usize) -> Duration + Send + Sync + 'static,
{
Self::Custom(Box::new(func))
}
}
impl HealthStatus {
pub fn is_healthy(&self) -> bool {
matches!(self, Self::Healthy)
}
pub fn is_unhealthy(&self) -> bool {
matches!(self, Self::Unhealthy { .. })
}
pub fn is_degraded(&self) -> bool {
matches!(self, Self::Degraded { .. })
}
pub fn reason(&self) -> Option<&str> {
match self {
Self::Degraded { reason } | Self::Unhealthy { reason } => Some(reason),
_ => None,
}
}
pub fn healthy() -> Self {
Self::Healthy
}
pub fn degraded(reason: impl Into<String>) -> Self {
Self::Degraded {
reason: reason.into(),
}
}
pub fn unhealthy(reason: impl Into<String>) -> Self {
Self::Unhealthy {
reason: reason.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exponential_backoff() {
let strategy = BackoffStrategy::exponential();
assert_eq!(strategy.calculate_delay(1), Duration::from_secs(2));
assert_eq!(strategy.calculate_delay(2), Duration::from_secs(4));
assert_eq!(strategy.calculate_delay(3), Duration::from_secs(8));
assert_eq!(strategy.calculate_delay(4), Duration::from_secs(16));
assert_eq!(strategy.calculate_delay(5), Duration::from_secs(32));
assert_eq!(strategy.calculate_delay(6), Duration::from_secs(60)); }
#[test]
fn test_linear_backoff() {
let strategy = BackoffStrategy::linear();
assert_eq!(strategy.calculate_delay(1), Duration::from_secs(5));
assert_eq!(strategy.calculate_delay(2), Duration::from_secs(10));
assert_eq!(strategy.calculate_delay(3), Duration::from_secs(15));
assert_eq!(strategy.calculate_delay(4), Duration::from_secs(20));
}
#[test]
fn test_fibonacci_backoff() {
let strategy = BackoffStrategy::fibonacci_with_default();
assert_eq!(strategy.calculate_delay(1), Duration::from_secs(1));
assert_eq!(strategy.calculate_delay(2), Duration::from_secs(1));
assert_eq!(strategy.calculate_delay(3), Duration::from_secs(2));
assert_eq!(strategy.calculate_delay(4), Duration::from_secs(3));
assert_eq!(strategy.calculate_delay(5), Duration::from_secs(5));
assert_eq!(strategy.calculate_delay(6), Duration::from_secs(8));
}
#[test]
fn test_fixed_backoff() {
let strategy = BackoffStrategy::fixed(Duration::from_secs(10));
assert_eq!(strategy.calculate_delay(1), Duration::from_secs(10));
assert_eq!(strategy.calculate_delay(5), Duration::from_secs(10));
assert_eq!(strategy.calculate_delay(100), Duration::from_secs(10));
}
#[test]
fn test_custom_backoff() {
let strategy = BackoffStrategy::custom(|attempt| Duration::from_secs(attempt as u64 * 3));
assert_eq!(strategy.calculate_delay(1), Duration::from_secs(3));
assert_eq!(strategy.calculate_delay(2), Duration::from_secs(6));
assert_eq!(strategy.calculate_delay(5), Duration::from_secs(15));
}
#[test]
fn test_health_status() {
let healthy = HealthStatus::healthy();
assert!(healthy.is_healthy());
assert!(!healthy.is_unhealthy());
assert_eq!(healthy.reason(), None);
let degraded = HealthStatus::degraded("High memory usage");
assert!(degraded.is_degraded());
assert_eq!(degraded.reason(), Some("High memory usage"));
let unhealthy = HealthStatus::unhealthy("Connection lost");
assert!(unhealthy.is_unhealthy());
assert_eq!(unhealthy.reason(), Some("Connection lost"));
}
#[test]
fn test_backoff_debug() {
let exponential = BackoffStrategy::exponential();
let debug_str = format!("{:?}", exponential);
assert!(debug_str.contains("Exponential"));
let custom = BackoffStrategy::custom(|n| Duration::from_secs(n as u64));
let debug_str = format!("{:?}", custom);
assert!(debug_str.contains("Custom"));
}
#[test]
fn test_backoff_clone() {
let original = BackoffStrategy::exponential();
let cloned = original.clone();
assert_eq!(original.calculate_delay(3), cloned.calculate_delay(3));
}
}