use std::time::{Duration, SystemTime};
use aimux_core::AiMuxError;
use rand::Rng;
pub async fn retry_with_exponential_backoff<F, T>(
max_retries: u32,
initial_delay: Duration,
mut f: F,
) -> Result<T, AiMuxError>
where
F: FnMut()
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T, AiMuxError>> + Send>>,
T: Send,
{
let mut last_error = AiMuxError::Other("no attempts made".to_string());
let mut delay = initial_delay;
for attempt in 0..=max_retries {
match f().await {
Ok(result) => return Ok(result),
Err(e) => {
last_error = e;
if !last_error.is_retryable() || attempt == max_retries {
return Err(last_error);
}
tokio::time::sleep(delay).await;
delay = delay.saturating_mul(2);
}
}
}
Err(last_error)
}
#[derive(Debug, Clone, Copy)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_delay: Duration,
pub backoff_factor: u32,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 2,
initial_delay: Duration::from_millis(2000),
backoff_factor: 2,
}
}
}
pub async fn retry_with_exponential_backoff_respecting_retry_headers<F, T>(
config: RetryConfig,
mut f: F,
) -> Result<T, AiMuxError>
where
F: FnMut()
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T, AiMuxError>> + Send>>,
T: Send,
{
let mut last_error = AiMuxError::Other("no attempts made".to_string());
let mut exponential_delay_ms = config.initial_delay.as_millis() as i64;
for attempt in 0..=config.max_retries {
match f().await {
Ok(result) => return Ok(result),
Err(e) => {
last_error = e;
if !last_error.is_retryable() || attempt == config.max_retries {
return Err(last_error);
}
let hint = last_error.retry_after_hint();
let delay_ms = {
let mut rng = rand::thread_rng();
get_retry_delay_ms_with_jitter(hint, exponential_delay_ms, &mut rng)
};
tokio::time::sleep(Duration::from_millis(delay_ms.max(0) as u64)).await;
exponential_delay_ms =
exponential_delay_ms.saturating_mul(config.backoff_factor as i64);
}
}
}
Err(last_error)
}
pub fn get_retry_delay_ms(hint: Option<i64>, exponential_delay_ms: i64) -> i64 {
match hint {
Some(ms) if ms >= 0 && (ms < 60_000 || ms < exponential_delay_ms) => ms,
_ => exponential_delay_ms,
}
}
pub fn get_retry_delay_ms_with_jitter(
hint: Option<i64>,
exponential_delay_ms: i64,
rng: &mut impl Rng,
) -> i64 {
let base = get_retry_delay_ms(hint, exponential_delay_ms);
if base <= 0 {
return 0;
}
rng.gen_range(0..base)
}
pub fn parse_retry_after(
retry_after_ms_header: Option<&str>,
retry_after_header: Option<&str>,
now: SystemTime,
) -> Option<i64> {
let mut ms: Option<i64> = None;
if let Some(raw) = retry_after_ms_header
&& let Ok(v) = raw.trim().parse::<f64>()
&& v.is_finite()
{
ms = Some(v as i64);
}
if ms.is_none()
&& let Some(raw) = retry_after_header
{
let trimmed = raw.trim();
if let Ok(seconds) = trimmed.parse::<f64>() {
if seconds.is_finite() {
ms = Some((seconds * 1000.0) as i64);
}
} else {
if let Ok(target) = httpdate::parse_http_date(trimmed) {
if let Ok(duration) = target.duration_since(now) {
ms = Some(duration.as_millis() as i64);
} else if let Ok(duration) = now.duration_since(target) {
ms = Some(-(duration.as_millis() as i64));
}
}
}
}
ms
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_delay_uses_hint_when_reasonable() {
assert_eq!(get_retry_delay_ms(Some(3000), 2000), 3000);
}
#[test]
fn get_delay_uses_exponential_when_hint_too_long() {
assert_eq!(get_retry_delay_ms(Some(70_000), 2000), 2000);
}
#[test]
fn get_delay_uses_exponential_when_hint_negative() {
assert_eq!(get_retry_delay_ms(Some(-1000), 2000), 2000);
}
#[test]
fn get_delay_uses_exponential_when_no_hint() {
assert_eq!(get_retry_delay_ms(None, 2000), 2000);
}
#[test]
fn get_delay_uses_hint_when_shorter_than_exponential_even_if_over_60s() {
assert_eq!(get_retry_delay_ms(Some(70_000), 80_000), 70_000);
}
#[test]
fn jitter_returns_zero_when_base_is_zero() {
let mut rng = rand::thread_rng();
assert_eq!(get_retry_delay_ms_with_jitter(None, 0, &mut rng), 0);
}
#[test]
fn jitter_returns_zero_when_base_negative() {
let mut rng = rand::thread_rng();
assert_eq!(get_retry_delay_ms_with_jitter(None, -100, &mut rng), 0);
}
#[test]
fn jitter_stays_within_full_jitter_bounds() {
let mut rng = rand::thread_rng();
for _ in 0..1000 {
let d = get_retry_delay_ms_with_jitter(None, 2000, &mut rng);
assert!((0..2000).contains(&d), "delay {d} out of [0, 2000)");
}
}
#[test]
fn jitter_uses_retry_after_hint_as_upper_bound() {
let mut rng = rand::thread_rng();
for _ in 0..1000 {
let d = get_retry_delay_ms_with_jitter(Some(3000), 2000, &mut rng);
assert!((0..3000).contains(&d), "delay {d} out of [0, 3000)");
}
}
#[test]
fn parse_retry_after_ms_header() {
assert_eq!(
parse_retry_after(Some("3000"), None, SystemTime::now()),
Some(3000)
);
}
#[test]
fn parse_retry_after_seconds_header() {
assert_eq!(
parse_retry_after(None, Some("5"), SystemTime::now()),
Some(5000)
);
}
#[test]
fn parse_retry_after_prefers_ms_over_seconds() {
assert_eq!(
parse_retry_after(Some("3000"), Some("10"), SystemTime::now()),
Some(3000)
);
}
#[test]
fn parse_retry_after_invalid_falls_back_to_none() {
assert_eq!(
parse_retry_after(Some("invalid"), Some("not-a-number"), SystemTime::now()),
None
);
}
#[test]
fn parse_retry_after_negative_ms() {
assert_eq!(
parse_retry_after(Some("-1000"), None, SystemTime::now()),
Some(-1000)
);
}
}