use std::time::Duration;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Redis error: {0}")]
Redis(#[from] fred::error::RedisError),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Task validation error: {0}")]
Validation(String),
#[error("Task not found: {0}")]
TaskNotFound(String),
#[error("Queue not found: {0}")]
QueueNotFound(String),
#[error("Queue is paused: {0}")]
QueuePaused(String),
#[error("Handler error: {0}")]
Handler(String),
#[error("Task timeout: {0}")]
Timeout(String),
#[error("Connection error: {0}")]
Connection(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Metrics error: {0}")]
Metrics(String),
#[error("Shutdown requested")]
Shutdown,
#[error("Unknown error: {0}")]
Unknown(String),
}
impl Error {
pub fn is_retryable(&self) -> bool {
matches!(
self,
Error::Redis(_) | Error::Connection(_) | Error::Timeout(_)
)
}
pub fn is_fatal(&self) -> bool {
matches!(
self,
Error::Validation(_) | Error::Config(_) | Error::Shutdown
)
}
pub fn retry_after(&self) -> Option<Duration> {
match self {
Error::Timeout(_) => Some(Duration::from_secs(60)),
Error::Connection(_) => Some(Duration::from_secs(5)),
Error::Redis(_) => Some(Duration::from_secs(1)),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum RetryStrategy {
FixedDelay {
max_retries: u32,
delay: Duration,
},
ExponentialBackoff {
max_retries: u32,
initial_delay: Duration,
max_delay: Duration,
multiplier: f64,
},
Linear {
max_retries: u32,
step: Duration,
},
}
impl RetryStrategy {
pub fn max_retries(&self) -> u32 {
match self {
RetryStrategy::FixedDelay { max_retries, .. } => *max_retries,
RetryStrategy::ExponentialBackoff { max_retries, .. } => *max_retries,
RetryStrategy::Linear { max_retries, .. } => *max_retries,
}
}
pub fn delay_for(&self, retry_count: u32) -> Option<Duration> {
if retry_count >= self.max_retries() {
return None;
}
match self {
RetryStrategy::FixedDelay { delay, .. } => Some(*delay),
RetryStrategy::ExponentialBackoff {
initial_delay,
max_delay,
multiplier,
..
} => {
let delay = initial_delay.as_millis() as f64 * multiplier.powi(retry_count as i32);
let delay = Duration::from_millis(delay as u64);
Some(delay.min(*max_delay))
}
RetryStrategy::Linear { step, .. } => {
Some(Duration::from_millis(step.as_millis() as u64 * (retry_count + 1) as u64))
}
}
}
}
impl Default for RetryStrategy {
fn default() -> Self {
RetryStrategy::ExponentialBackoff {
max_retries: 3,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
multiplier: 2.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retryable_errors() {
use fred::error::RedisErrorKind;
assert!(Error::Redis(fred::error::RedisError::new(RedisErrorKind::Unknown, "test")).is_retryable());
assert!(Error::Connection("test".to_string()).is_retryable());
assert!(Error::Timeout("test".to_string()).is_retryable());
assert!(!Error::Validation("test".to_string()).is_retryable());
assert!(!Error::Config("test".to_string()).is_retryable());
}
#[test]
fn test_exponential_backoff() {
let strategy = RetryStrategy::ExponentialBackoff {
max_retries: 5,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
multiplier: 2.0,
};
assert_eq!(strategy.delay_for(0), Some(Duration::from_secs(1)));
assert_eq!(strategy.delay_for(1), Some(Duration::from_secs(2)));
assert_eq!(strategy.delay_for(2), Some(Duration::from_secs(4)));
assert_eq!(strategy.delay_for(3), Some(Duration::from_secs(8)));
assert_eq!(strategy.delay_for(4), Some(Duration::from_secs(16)));
assert_eq!(strategy.delay_for(5), None); }
}