use std::time::Duration;
use rand::Rng;
use reqwest::{Error as ReqwestError, Response, StatusCode};
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: u32,
pub base_delay: Duration,
pub max_delay: Duration,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
base_delay: Duration::from_millis(500),
max_delay: Duration::from_secs(30),
}
}
}
pub(crate) fn is_retryable_status(status: StatusCode) -> bool {
matches!(
status,
StatusCode::TOO_MANY_REQUESTS
| StatusCode::BAD_GATEWAY
| StatusCode::SERVICE_UNAVAILABLE
| StatusCode::GATEWAY_TIMEOUT
)
}
pub(crate) fn is_retryable_error(err: &ReqwestError) -> bool {
err.is_timeout() || err.is_connect() || err.is_request()
}
pub(crate) fn backoff_delay(config: &RetryConfig, attempt: u32) -> Duration {
let base_ms = config.base_delay.as_millis() as u64;
let exp_ms = base_ms.saturating_mul(1u64 << attempt.min(63));
let max_ms = config.max_delay.as_millis() as u64;
let capped_ms = exp_ms.min(max_ms);
let jitter_ms = rand::rng().random_range(0..=capped_ms / 2);
let total_ms = capped_ms.saturating_add(jitter_ms).min(max_ms);
Duration::from_millis(total_ms)
}
pub(crate) fn parse_retry_after(response: &Response) -> Option<Duration> {
response
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.map(Duration::from_secs)
}