use std::collections::BTreeSet;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use crate::error::{Error, Result};
pub type RetryPredicate = Arc<dyn Fn(&Error) -> bool + Send + Sync>;
#[derive(Clone)]
#[non_exhaustive]
pub struct RetryPolicy {
pub max_retries: u32,
pub backoff_initial: Duration,
pub backoff_max: Duration,
pub backoff_jitter: f64,
pub http_statuses: BTreeSet<u16>,
pub respect_retry_after: bool,
pub retry_connection_errors: bool,
pub retry_timeouts: bool,
pub predicate: Option<RetryPredicate>,
pub budget: Option<Duration>,
}
impl Default for RetryPolicy {
fn default() -> Self {
let mut statuses: BTreeSet<u16> = (500..600).collect();
statuses.extend([408, 429]);
Self {
max_retries: 2,
backoff_initial: Duration::from_millis(500),
backoff_max: Duration::from_secs(5),
backoff_jitter: 0.25,
http_statuses: statuses,
respect_retry_after: true,
retry_connection_errors: true,
retry_timeouts: true,
predicate: None,
budget: Some(Duration::from_secs(30)),
}
}
}
impl fmt::Debug for RetryPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RetryPolicy")
.field("max_retries", &self.max_retries)
.field("backoff_initial", &self.backoff_initial)
.field("backoff_max", &self.backoff_max)
.field("backoff_jitter", &self.backoff_jitter)
.field("http_statuses", &self.http_statuses)
.field("respect_retry_after", &self.respect_retry_after)
.field("retry_connection_errors", &self.retry_connection_errors)
.field("retry_timeouts", &self.retry_timeouts)
.field("predicate", &self.predicate.as_ref().map(|_| "<fn>"))
.field("budget", &self.budget)
.finish()
}
}
impl RetryPolicy {
pub fn none() -> Self {
Self {
max_retries: 0,
..Self::default()
}
}
pub fn max_retries(mut self, n: u32) -> Self {
self.max_retries = n;
self
}
pub fn backoff(mut self, initial: Duration, max: Duration) -> Self {
self.backoff_initial = initial;
self.backoff_max = max;
self
}
pub fn jitter(mut self, fraction: f64) -> Self {
self.backoff_jitter = fraction;
self
}
pub fn statuses(mut self, statuses: impl IntoIterator<Item = u16>) -> Self {
self.http_statuses = statuses.into_iter().collect();
self
}
pub fn budget(mut self, budget: Option<Duration>) -> Self {
self.budget = budget;
self
}
pub fn respect_retry_after(mut self, yes: bool) -> Self {
self.respect_retry_after = yes;
self
}
pub fn retry_connection_errors(mut self, yes: bool) -> Self {
self.retry_connection_errors = yes;
self
}
pub fn retry_timeouts(mut self, yes: bool) -> Self {
self.retry_timeouts = yes;
self
}
pub fn retry_if(mut self, f: impl Fn(&Error) -> bool + Send + Sync + 'static) -> Self {
self.predicate = Some(Arc::new(f));
self
}
pub(crate) fn validate(&self) -> Result<()> {
if !(0.0..=1.0).contains(&self.backoff_jitter) {
return Err(Error::Config(
"backoff_jitter must be between zero and one.".into(),
));
}
if self.budget == Some(Duration::ZERO) {
return Err(Error::Config(
"retry budget must be a positive duration.".into(),
));
}
Ok(())
}
pub(crate) fn is_retryable(&self, err: &Error) -> bool {
let builtin = match err {
Error::Timeout(_) => self.retry_timeouts,
Error::Connection(_) => self.retry_connection_errors,
Error::Api(e) => self.http_statuses.contains(&e.status),
_ => false,
};
builtin || self.predicate.as_ref().is_some_and(|p| p(err))
}
pub(crate) fn delay(&self, attempt: u32, err: &Error) -> Duration {
if self.respect_retry_after
&& let Some(d) = err.as_api().and_then(|e| e.retry_after())
{
return d;
}
backoff(
attempt,
self.backoff_initial,
self.backoff_max,
self.backoff_jitter,
rand::random::<f64>(),
)
}
pub(crate) fn should_stop(&self, attempts: u32, elapsed: Duration, upcoming: Duration) -> bool {
attempts > self.max_retries
|| self
.budget
.is_some_and(|b| elapsed.saturating_add(upcoming) >= b)
}
}
fn backoff(attempt: u32, initial: Duration, max: Duration, jitter: f64, r: f64) -> Duration {
let (initial, max) = (initial.as_secs_f64(), max.as_secs_f64());
if initial == 0.0 || max == 0.0 {
return Duration::ZERO;
}
let exponent = attempt.saturating_sub(1) as f64;
let exponential = if exponent >= max.log2() - initial.log2() {
max
} else {
initial * 2f64.powf(exponent)
};
let delay = exponential * (1.0 - r * jitter);
let rounded = (delay * 1000.0).round() / 1000.0;
Duration::from_secs_f64(exponential.min(rounded))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backoff_doubles_and_caps() {
let (i, m) = (Duration::from_millis(500), Duration::from_secs(5));
let d = |a| backoff(a, i, m, 0.25, 0.0);
assert_eq!(d(1), Duration::from_millis(500));
assert_eq!(d(2), Duration::from_secs(1));
assert_eq!(d(4), Duration::from_secs(4));
assert_eq!(d(5), Duration::from_secs(5));
assert_eq!(d(40), Duration::from_secs(5));
assert_eq!(backoff(1, i, m, 0.25, 1.0), Duration::from_millis(375));
assert_eq!(backoff(1, Duration::ZERO, m, 0.25, 0.5), Duration::ZERO);
}
#[test]
fn stop_rules() {
let p = RetryPolicy::default();
assert!(!p.should_stop(1, Duration::ZERO, Duration::from_secs(1)));
assert!(!p.should_stop(2, Duration::ZERO, Duration::from_secs(1)));
assert!(p.should_stop(3, Duration::ZERO, Duration::from_secs(1)));
assert!(p.should_stop(1, Duration::from_secs(29), Duration::from_secs(1)));
assert!(!RetryPolicy::default().budget(None).should_stop(
1,
Duration::from_secs(99),
Duration::from_secs(1)
));
}
#[test]
fn default_statuses_cover_529() {
let p = RetryPolicy::default();
for s in [408, 429, 500, 503, 529, 599] {
assert!(p.http_statuses.contains(&s));
}
assert!(!p.http_statuses.contains(&422));
assert!(RetryPolicy::default().jitter(1.5).validate().is_err());
}
}