use std::time::Duration;
use tokio::time::sleep;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_backoff: Duration,
pub max_backoff: Duration,
pub multiplier: f64,
pub max_time: Duration,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(60),
multiplier: 2.0,
max_time: Duration::from_secs(60),
}
}
}
impl RetryConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_retries(mut self, max_retries: u32) -> Self {
self.max_retries = max_retries;
self
}
pub fn with_initial_backoff(mut self, duration: Duration) -> Self {
self.initial_backoff = duration;
self
}
pub fn with_max_backoff(mut self, duration: Duration) -> Self {
self.max_backoff = duration;
self
}
pub fn with_multiplier(mut self, multiplier: f64) -> Self {
self.multiplier = multiplier;
self
}
pub fn with_max_time(mut self, duration: Duration) -> Self {
self.max_time = duration;
self
}
}
pub async fn retry_with_backoff<F, Fut, T>(config: RetryConfig, mut f: F) -> Result<T>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let start_time = std::time::Instant::now();
let mut attempt = 0;
let mut current_backoff = config.initial_backoff;
let mut last_error = String::from("unknown error");
loop {
if start_time.elapsed() > config.max_time {
return Err(Error::RetriesExceeded(last_error));
}
match f().await {
Ok(result) => return Ok(result),
Err(err) => {
if !err.is_retryable() {
return Err(err);
}
last_error = err.to_string();
attempt += 1;
if attempt > config.max_retries {
return Err(Error::RetriesExceeded(last_error));
}
sleep(current_backoff).await;
current_backoff = Duration::from_secs_f64(
(current_backoff.as_secs_f64() * config.multiplier)
.min(config.max_backoff.as_secs_f64()),
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retry_config_default() {
let config = RetryConfig::default();
assert_eq!(config.max_retries, 3);
assert_eq!(config.initial_backoff, Duration::from_millis(100));
assert_eq!(config.max_backoff, Duration::from_secs(60));
assert_eq!(config.multiplier, 2.0);
}
#[test]
fn test_retry_config_builder() {
let config = RetryConfig::new()
.with_max_retries(5)
.with_initial_backoff(Duration::from_millis(200))
.with_max_backoff(Duration::from_secs(30))
.with_multiplier(1.5);
assert_eq!(config.max_retries, 5);
assert_eq!(config.initial_backoff, Duration::from_millis(200));
assert_eq!(config.max_backoff, Duration::from_secs(30));
assert_eq!(config.multiplier, 1.5);
}
#[tokio::test]
async fn test_retry_success_first_attempt() {
use std::sync::{Arc, Mutex};
let config = RetryConfig::default();
let call_count = Arc::new(Mutex::new(0));
let call_count_clone = call_count.clone();
let result = retry_with_backoff(config, move || {
let count = call_count_clone.clone();
async move {
*count.lock().unwrap() += 1;
Ok::<i32, Error>(42)
}
})
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
assert_eq!(*call_count.lock().unwrap(), 1);
}
#[tokio::test]
async fn test_retry_success_after_failures() {
use std::sync::{Arc, Mutex};
let config = RetryConfig::default().with_initial_backoff(Duration::from_millis(1));
let call_count = Arc::new(Mutex::new(0));
let call_count_clone = call_count.clone();
let result = retry_with_backoff(config, move || {
let count = call_count_clone.clone();
async move {
let mut c = count.lock().unwrap();
*c += 1;
let current = *c;
drop(c);
if current < 3 {
Err(Error::Inference("Temporary error".to_string()))
} else {
Ok(42)
}
}
})
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
assert_eq!(*call_count.lock().unwrap(), 3);
}
#[tokio::test]
async fn test_retry_non_retryable_error() {
use std::sync::{Arc, Mutex};
let config = RetryConfig::default();
let call_count = Arc::new(Mutex::new(0));
let call_count_clone = call_count.clone();
let result = retry_with_backoff(config, move || {
let count = call_count_clone.clone();
async move {
*count.lock().unwrap() += 1;
Err::<i32, Error>(Error::NonRetryable("Bad request".to_string()))
}
})
.await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::NonRetryable(_)));
assert_eq!(*call_count.lock().unwrap(), 1); }
#[tokio::test]
async fn test_retry_exceeds_max_retries() {
use std::sync::{Arc, Mutex};
let config = RetryConfig::default()
.with_max_retries(2)
.with_initial_backoff(Duration::from_millis(1));
let call_count = Arc::new(Mutex::new(0));
let call_count_clone = call_count.clone();
let result = retry_with_backoff(config, move || {
let count = call_count_clone.clone();
async move {
*count.lock().unwrap() += 1;
Err::<i32, Error>(Error::Inference("Persistent error".to_string()))
}
})
.await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::RetriesExceeded(_)));
assert_eq!(*call_count.lock().unwrap(), 3); }
#[tokio::test]
async fn test_retry_exceeds_max_time() {
let config = RetryConfig::default()
.with_max_time(Duration::from_millis(50))
.with_initial_backoff(Duration::from_millis(30));
let result = retry_with_backoff(config, || async {
Err::<i32, Error>(Error::Inference("Persistent error".to_string()))
})
.await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::RetriesExceeded(_)));
}
#[tokio::test]
async fn test_backoff_increases_exponentially() {
use std::sync::{Arc, Mutex};
let config = RetryConfig::default()
.with_initial_backoff(Duration::from_millis(10))
.with_multiplier(2.0)
.with_max_retries(3);
let start = std::time::Instant::now();
let call_count = Arc::new(Mutex::new(0));
let call_count_clone = call_count.clone();
let _ = retry_with_backoff(config, move || {
let count = call_count_clone.clone();
async move {
*count.lock().unwrap() += 1;
Err::<i32, Error>(Error::Inference("Error".to_string()))
}
})
.await;
let elapsed = start.elapsed();
assert!(elapsed >= Duration::from_millis(70));
assert_eq!(*call_count.lock().unwrap(), 4); }
#[tokio::test]
async fn test_backoff_respects_max_backoff() {
let config = RetryConfig::default()
.with_initial_backoff(Duration::from_millis(10))
.with_max_backoff(Duration::from_millis(15))
.with_multiplier(2.0)
.with_max_retries(3);
let start = std::time::Instant::now();
let _ = retry_with_backoff(config, || async {
Err::<i32, Error>(Error::Inference("Error".to_string()))
})
.await;
let elapsed = start.elapsed();
assert!(elapsed >= Duration::from_millis(40));
assert!(elapsed < Duration::from_millis(100)); }
}