use std::collections::BTreeSet;
use std::fmt;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use reqwest::header::HeaderMap;
use crate::error::{Error, Result};
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
pub const DEFAULT_TOTAL_TIMEOUT: Duration = Duration::from_secs(10);
pub type RetryPredicate = Arc<dyn Fn(&Error) -> bool + Send + Sync>;
#[derive(Clone)]
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 max_retry_after: Duration,
pub retry_connection_errors: bool,
pub retry_timeouts: bool,
pub retry_if: Option<RetryPredicate>,
}
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("max_retry_after", &self.max_retry_after)
.field("retry_connection_errors", &self.retry_connection_errors)
.field("retry_timeouts", &self.retry_timeouts)
.field("retry_if", &self.retry_if.as_ref().map(|_| "<predicate>"))
.finish()
}
}
impl PartialEq for RetryPolicy {
fn eq(&self, other: &Self) -> bool {
self.max_retries == other.max_retries
&& self.backoff_initial == other.backoff_initial
&& self.backoff_max == other.backoff_max
&& self.backoff_jitter == other.backoff_jitter
&& self.http_statuses == other.http_statuses
&& self.respect_retry_after == other.respect_retry_after
&& self.max_retry_after == other.max_retry_after
&& self.retry_connection_errors == other.retry_connection_errors
&& self.retry_timeouts == other.retry_timeouts
&& match (&self.retry_if, &other.retry_if) {
(None, None) => true,
(Some(left), Some(right)) => Arc::ptr_eq(left, right),
_ => false,
}
}
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: 2,
backoff_initial: Duration::from_millis(500),
backoff_max: Duration::from_secs(5),
backoff_jitter: 0.25,
http_statuses: [408, 429].into_iter().chain(500..600).collect(),
respect_retry_after: true,
max_retry_after: Duration::from_secs(60),
retry_connection_errors: true,
retry_timeouts: true,
retry_if: None,
}
}
}
impl RetryPolicy {
pub fn none() -> Self {
Self {
max_retries: 0,
..Self::default()
}
}
pub fn validate(&self) -> Result<()> {
if !(0.0..=1.0).contains(&self.backoff_jitter) {
return Err(Error::Config(format!(
"`retry.backoff_jitter` must be between 0 and 1, got {}.",
self.backoff_jitter
)));
}
if let Some(status) = self
.http_statuses
.iter()
.find(|status| !(100..=999).contains(*status))
{
return Err(Error::Config(format!(
"`retry.http_statuses` must contain HTTP status codes, got {status}."
)));
}
Ok(())
}
pub fn is_retryable_status(&self, status: u16) -> bool {
self.http_statuses.contains(&status)
}
pub fn retry_if(mut self, predicate: impl Fn(&Error) -> bool + Send + Sync + 'static) -> Self {
self.retry_if = Some(Arc::new(predicate));
self
}
pub(crate) fn is_retryable_error(&self, err: &Error) -> bool {
let built_in = match err {
Error::Timeout { .. } => self.retry_timeouts,
Error::Connection { .. } => self.retry_connection_errors,
_ => false,
};
built_in || self.asks_to_retry(err)
}
pub(crate) fn asks_to_retry(&self, err: &Error) -> bool {
self.retry_if
.as_ref()
.is_some_and(|predicate| predicate(err))
}
pub fn delay(&self, attempt: u32, headers: Option<&HeaderMap>, random: f64) -> Duration {
if self.respect_retry_after
&& let Some(headers) = headers
&& let Some(server) = parse_retry_after(headers, SystemTime::now())
&& server <= self.max_retry_after
{
return server;
}
let factor = 2u32.checked_pow(attempt).unwrap_or(u32::MAX);
let exponential = self
.backoff_initial
.saturating_mul(factor)
.min(self.backoff_max);
let keep = (1.0 - random.clamp(0.0, 1.0) * self.backoff_jitter).clamp(0.0, 1.0);
exponential.mul_f64(keep)
}
}
pub fn parse_retry_after(headers: &HeaderMap, now: SystemTime) -> Option<Duration> {
if let Some(ms) = header_str(headers, "retry-after-ms").and_then(parse_non_negative)
&& let Ok(delay) = Duration::try_from_secs_f64(ms / 1000.0)
{
return Some(delay);
}
let raw = header_str(headers, "retry-after")?;
if let Ok(seconds) = raw.parse::<f64>() {
return (seconds.is_finite() && seconds >= 0.0)
.then(|| Duration::try_from_secs_f64(seconds).ok())
.flatten();
}
let date = httpdate::parse_http_date(raw).ok()?;
Some(date.duration_since(now).unwrap_or(Duration::ZERO))
}
fn header_str<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
headers
.get(name)
.and_then(|value| value.to_str().ok())
.map(str::trim)
}
fn parse_non_negative(raw: &str) -> Option<f64> {
raw.parse::<f64>()
.ok()
.filter(|value| value.is_finite() && *value >= 0.0)
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::header::HeaderValue;
fn headers(pairs: &[(&'static str, &str)]) -> HeaderMap {
let mut map = HeaderMap::new();
for (name, value) in pairs {
map.insert(*name, HeaderValue::from_str(value).unwrap());
}
map
}
#[test]
fn default_policy_matches_the_documented_values() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_retries, 2);
assert!(policy.is_retryable_status(408));
assert!(policy.is_retryable_status(429));
assert!(policy.is_retryable_status(500));
assert!(policy.is_retryable_status(599));
assert!(!policy.is_retryable_status(600));
assert!(!policy.is_retryable_status(409));
assert_eq!(policy.http_statuses.len(), 102);
assert!(policy.validate().is_ok());
}
#[test]
fn backoff_doubles_up_to_the_cap() {
let policy = RetryPolicy::default();
let ms = |attempt| policy.delay(attempt, None, 0.0).as_millis();
assert_eq!(
[ms(0), ms(1), ms(2), ms(3), ms(4)],
[500, 1000, 2000, 4000, 5000]
);
assert_eq!(ms(40), 5000);
assert_eq!(ms(u32::MAX), 5000);
}
#[test]
fn jitter_subtracts_at_most_the_configured_fraction() {
let policy = RetryPolicy::default();
assert_eq!(policy.delay(0, None, 1.0), Duration::from_millis(375));
assert_eq!(policy.delay(0, None, 0.5), Duration::from_micros(437_500));
}
#[test]
fn honors_retry_after_within_the_cap() {
let policy = RetryPolicy::default();
let server = headers(&[("retry-after", "2")]);
assert_eq!(policy.delay(0, Some(&server), 1.0), Duration::from_secs(2));
let ignoring = RetryPolicy {
respect_retry_after: false,
..RetryPolicy::default()
};
assert_eq!(
ignoring.delay(0, Some(&server), 0.0),
Duration::from_millis(500)
);
let capped = RetryPolicy {
max_retry_after: Duration::from_secs(1),
..RetryPolicy::default()
};
assert_eq!(
capped.delay(0, Some(&server), 0.0),
Duration::from_millis(500)
);
}
#[test]
fn parses_retry_after_forms() {
let now = SystemTime::now();
let parse = |pairs: &[(&'static str, &str)]| parse_retry_after(&headers(pairs), now);
assert_eq!(
parse(&[("retry-after-ms", "250")]),
Some(Duration::from_millis(250))
);
assert_eq!(
parse(&[("retry-after-ms", "1.5")]),
Some(Duration::from_micros(1500))
);
assert_eq!(parse(&[("retry-after", "3")]), Some(Duration::from_secs(3)));
assert_eq!(
parse(&[("retry-after", " 0.5 ")]),
Some(Duration::from_millis(500))
);
assert_eq!(
parse(&[("retry-after-ms", "100"), ("retry-after", "9")]),
Some(Duration::from_millis(100))
);
assert_eq!(
parse(&[("retry-after-ms", "-1"), ("retry-after", "9")]),
Some(Duration::from_secs(9))
);
assert_eq!(parse(&[("retry-after", "-3")]), None);
assert_eq!(parse(&[("retry-after", "inf")]), None);
assert_eq!(parse(&[("retry-after", "soon")]), None);
assert_eq!(parse(&[]), None);
}
#[test]
fn parses_http_dates_relative_to_now() {
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
let later = httpdate::fmt_http_date(now + Duration::from_secs(30));
let earlier = httpdate::fmt_http_date(now - Duration::from_secs(30));
assert_eq!(
parse_retry_after(&headers(&[("retry-after", &later)]), now),
Some(Duration::from_secs(30))
);
assert_eq!(
parse_retry_after(&headers(&[("retry-after", &earlier)]), now),
Some(Duration::ZERO)
);
}
#[test]
fn validation_names_the_bad_field() {
let bad = |policy: RetryPolicy| policy.validate().unwrap_err().to_string();
assert!(
bad(RetryPolicy {
backoff_jitter: 1.5,
..RetryPolicy::default()
})
.contains("retry.backoff_jitter")
);
assert!(
bad(RetryPolicy {
backoff_jitter: -0.1,
..RetryPolicy::default()
})
.contains("retry.backoff_jitter")
);
assert!(
bad(RetryPolicy {
backoff_jitter: f64::NAN,
..RetryPolicy::default()
})
.contains("retry.backoff_jitter")
);
assert!(
bad(RetryPolicy {
http_statuses: [503, 42].into(),
..RetryPolicy::default()
})
.contains("retry.http_statuses")
);
assert!(
RetryPolicy {
backoff_jitter: 1.0,
..RetryPolicy::none()
}
.validate()
.is_ok()
);
}
#[test]
fn retryable_errors_follow_the_policy_flags() {
let timeout = Error::Timeout {
timeout: Duration::from_secs(1),
};
let policy = RetryPolicy {
retry_timeouts: false,
..RetryPolicy::default()
};
assert!(!policy.is_retryable_error(&timeout));
assert!(RetryPolicy::default().is_retryable_error(&timeout));
assert!(!RetryPolicy::default().is_retryable_error(&Error::Config("x".into())));
}
}