use std::time::Duration;
use crate::retry_policy::RetryPolicy;
pub const DEFAULT_MAX_RETRIES: u32 = 3;
pub const DEFAULT_BASE_BACKOFF: Duration = Duration::from_millis(500);
pub const DEFAULT_MAX_BACKOFF: Duration = Duration::from_secs(30);
pub const DEFAULT_JITTER: Duration = Duration::from_millis(500);
#[derive(Debug, Clone)]
pub struct BackendRetryPolicy {
inner: RetryPolicy,
pub max_retry_after: Duration,
}
impl BackendRetryPolicy {
pub fn production() -> Self {
Self {
inner: RetryPolicy {
max_retries: DEFAULT_MAX_RETRIES,
base_delay: DEFAULT_BASE_BACKOFF,
max_delay: DEFAULT_MAX_BACKOFF,
backoff_multiplier: 2.0,
jitter: true,
},
max_retry_after: DEFAULT_MAX_BACKOFF,
}
}
pub fn no_retry() -> Self {
Self {
inner: RetryPolicy::no_retry(),
max_retry_after: Duration::ZERO,
}
}
pub fn max_retries(&self) -> u32 {
self.inner.max_retries
}
pub fn inner(&self) -> &RetryPolicy {
&self.inner
}
pub fn is_retryable_status(status: u16) -> bool {
status == 429 || status == 408 || (500..600).contains(&status)
}
pub fn delay_for_response(
&self,
attempt: u32,
retry_after_seconds: Option<u64>,
) -> Duration {
if let Some(secs) = retry_after_seconds {
return Duration::from_secs(secs).min(self.max_retry_after);
}
self.inner.delay_for_attempt(attempt)
}
pub fn delay_for_transport(&self, attempt: u32) -> Duration {
self.inner.delay_for_attempt(attempt)
}
}
impl Default for BackendRetryPolicy {
fn default() -> Self {
Self::production()
}
}
pub fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<u64> {
headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.trim().parse::<u64>().ok())
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::header::HeaderMap;
#[test]
fn defaults_match_python_v1_16_1_constants() {
let p = BackendRetryPolicy::production();
assert_eq!(p.max_retries(), 3);
assert_eq!(p.max_retry_after, Duration::from_secs(30));
}
#[test]
fn no_retry_policy_disables_retries() {
let p = BackendRetryPolicy::no_retry();
assert_eq!(p.max_retries(), 0);
}
#[test]
fn retryable_status_codes() {
assert!(BackendRetryPolicy::is_retryable_status(429));
assert!(BackendRetryPolicy::is_retryable_status(408));
assert!(BackendRetryPolicy::is_retryable_status(500));
assert!(BackendRetryPolicy::is_retryable_status(502));
assert!(BackendRetryPolicy::is_retryable_status(503));
assert!(BackendRetryPolicy::is_retryable_status(599));
}
#[test]
fn non_retryable_status_codes() {
assert!(!BackendRetryPolicy::is_retryable_status(400));
assert!(!BackendRetryPolicy::is_retryable_status(401));
assert!(!BackendRetryPolicy::is_retryable_status(403));
assert!(!BackendRetryPolicy::is_retryable_status(404));
assert!(!BackendRetryPolicy::is_retryable_status(200));
assert!(!BackendRetryPolicy::is_retryable_status(600));
}
#[test]
fn parse_retry_after_integer_seconds() {
let mut h = HeaderMap::new();
h.insert("retry-after", "60".parse().unwrap());
assert_eq!(parse_retry_after(&h), Some(60));
}
#[test]
fn parse_retry_after_with_whitespace() {
let mut h = HeaderMap::new();
h.insert("retry-after", " 120 ".parse().unwrap());
assert_eq!(parse_retry_after(&h), Some(120));
}
#[test]
fn parse_retry_after_missing_returns_none() {
let h = HeaderMap::new();
assert_eq!(parse_retry_after(&h), None);
}
#[test]
fn parse_retry_after_http_date_returns_none() {
let mut h = HeaderMap::new();
h.insert("retry-after", "Wed, 21 Oct 2026 07:28:00 GMT".parse().unwrap());
assert_eq!(parse_retry_after(&h), None);
}
#[test]
fn delay_for_response_honours_retry_after() {
let p = BackendRetryPolicy::production();
let delay = p.delay_for_response(0, Some(5));
assert_eq!(delay, Duration::from_secs(5));
}
#[test]
fn delay_for_response_caps_retry_after_at_max() {
let p = BackendRetryPolicy::production();
let delay = p.delay_for_response(0, Some(99999));
assert_eq!(delay, Duration::from_secs(30)); }
#[test]
fn delay_for_response_falls_back_to_exponential_when_no_header() {
let p = BackendRetryPolicy::production();
let delay = p.delay_for_response(0, None);
assert!(delay >= Duration::from_millis(500));
assert!(delay <= Duration::from_secs(30));
}
#[test]
fn delay_for_transport_uses_exponential_backoff() {
let p = BackendRetryPolicy::production();
let d0 = p.delay_for_transport(0);
let d2 = p.delay_for_transport(2);
assert!(d2 >= d0);
assert!(d2 <= Duration::from_secs(30)); }
}