use crate::error_category::{ErrorCategory, classify_anyhow_error};
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_retries: u32,
pub base_delay_ms: u64,
pub max_delay_ms: u64,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: 3,
base_delay_ms: 1000,
max_delay_ms: 30_000,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetryDecision {
pub retryable: bool,
pub category: ErrorCategory,
}
impl RetryPolicy {
pub fn classify_anyhow(&self, error: &anyhow::Error) -> RetryDecision {
let category = classify_anyhow_error(error);
RetryDecision {
retryable: category.is_retryable(),
category,
}
}
pub fn classify_status(&self, status: u16) -> RetryDecision {
let category = match status {
429 => ErrorCategory::RateLimit,
500 | 502 | 504 => ErrorCategory::Network,
503 => ErrorCategory::ServiceUnavailable,
401 | 403 => ErrorCategory::Authentication,
_ => ErrorCategory::ExecutionError,
};
RetryDecision {
retryable: category.is_retryable(),
category,
}
}
pub fn delay_ms_for_attempt(&self, attempt: u32) -> u64 {
let delay = self.base_delay_ms.saturating_mul(1u64 << attempt.min(16));
delay.min(self.max_delay_ms)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_policy_has_reasonable_values() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_retries, 3);
assert_eq!(policy.base_delay_ms, 1000);
assert_eq!(policy.max_delay_ms, 30_000);
}
#[test]
fn classify_status_rate_limit() {
let policy = RetryPolicy::default();
let decision = policy.classify_status(429);
assert!(decision.retryable);
assert_eq!(decision.category, ErrorCategory::RateLimit);
}
#[test]
fn classify_status_server_error() {
let policy = RetryPolicy::default();
let decision = policy.classify_status(503);
assert!(decision.retryable);
assert_eq!(decision.category, ErrorCategory::ServiceUnavailable);
}
#[test]
fn classify_status_auth_not_retryable() {
let policy = RetryPolicy::default();
let decision = policy.classify_status(401);
assert!(!decision.retryable);
assert_eq!(decision.category, ErrorCategory::Authentication);
}
#[test]
fn classify_anyhow_network_error() {
let policy = RetryPolicy::default();
let err = anyhow::anyhow!("connection refused");
let decision = policy.classify_anyhow(&err);
assert!(decision.retryable);
}
#[test]
fn delay_capped_at_max() {
let policy = RetryPolicy {
max_retries: 10,
base_delay_ms: 1000,
max_delay_ms: 5000,
};
assert_eq!(policy.delay_ms_for_attempt(0), 1000);
assert_eq!(policy.delay_ms_for_attempt(1), 2000);
assert_eq!(policy.delay_ms_for_attempt(2), 4000);
assert_eq!(policy.delay_ms_for_attempt(3), 5000); assert_eq!(policy.delay_ms_for_attempt(10), 5000); }
}