use std::time::Duration;
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: u32,
pub base_delay: Duration,
pub max_delay: Duration,
pub multiplier: f64,
pub honor_retry_after: bool,
pub max_elapsed: Option<Duration>,
}
impl Default for RetryConfig {
fn default() -> Self {
RetryConfig {
max_retries: 3,
base_delay: Duration::from_millis(250),
max_delay: Duration::from_secs(30),
multiplier: 2.0,
honor_retry_after: true,
max_elapsed: None,
}
}
}
impl RetryConfig {
pub fn disabled() -> Self {
RetryConfig {
max_retries: 0,
..Default::default()
}
}
pub(crate) fn backoff(&self, attempt: u32) -> Duration {
let base = self.base_delay.as_millis() as f64;
let cap = self.max_delay.as_millis() as f64;
let ceiling = (base * self.multiplier.powi(attempt as i32))
.min(cap)
.max(0.0);
Duration::from_millis((fastrand::f64() * ceiling) as u64)
}
}
pub(crate) fn retryable_status(code: u16) -> bool {
matches!(code, 429 | 500 | 502 | 503 | 504)
}
pub(crate) fn retryable_reqwest(e: &reqwest::Error) -> bool {
e.is_timeout() || e.is_connect() || e.is_request()
}
pub(crate) fn retryable_tonic(code: tonic::Code) -> bool {
use tonic::Code::*;
matches!(
code,
Unavailable | ResourceExhausted | Internal | DeadlineExceeded | Aborted
)
}
pub(crate) fn parse_retry_after(value: &str) -> Option<Duration> {
value.trim().parse::<u64>().ok().map(Duration::from_secs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backoff_is_bounded_by_max_delay() {
let cfg = RetryConfig {
base_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(5),
multiplier: 2.0,
..Default::default()
};
for attempt in 0..12 {
for _ in 0..16 {
assert!(cfg.backoff(attempt) <= cfg.max_delay);
}
}
}
#[test]
fn classification_table() {
for code in [429, 500, 502, 503, 504] {
assert!(retryable_status(code), "{code} should retry");
}
for code in [200, 400, 401, 403, 404, 422] {
assert!(!retryable_status(code), "{code} should not retry");
}
assert!(retryable_tonic(tonic::Code::Unavailable));
assert!(!retryable_tonic(tonic::Code::InvalidArgument));
assert!(!retryable_tonic(tonic::Code::Unauthenticated));
}
#[test]
fn retry_after_parsing() {
assert_eq!(parse_retry_after("1"), Some(Duration::from_secs(1)));
assert_eq!(parse_retry_after(" 30 "), Some(Duration::from_secs(30)));
assert_eq!(parse_retry_after("Wed, 21 Oct 2015 07:28:00 GMT"), None);
assert_eq!(parse_retry_after("nonsense"), None);
}
}