use std::time::Duration;
use crate::error::Error;
pub const DEFAULT_RETRY_STATUSES: [u16; 2] = [408, 429];
pub type RetryPredicate = std::sync::Arc<dyn Fn(&Error) -> bool + Send + Sync>;
#[derive(Clone)]
pub struct RetryPolicy {
pub(crate) max_retries: u32,
pub(crate) backoff_initial: Duration,
pub(crate) backoff_max: Duration,
pub(crate) backoff_jitter: f64,
pub(crate) retry_statuses: Vec<u16>,
pub(crate) retry_server_errors: bool,
pub(crate) respect_retry_after: bool,
pub(crate) retry_connection_errors: bool,
pub(crate) retry_timeout_errors: bool,
pub(crate) total_timeout: Option<Duration>,
pub(crate) retry_predicate: Option<RetryPredicate>,
}
impl std::fmt::Debug for RetryPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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("retry_statuses", &self.retry_statuses)
.field("retry_server_errors", &self.retry_server_errors)
.field("respect_retry_after", &self.respect_retry_after)
.field("retry_connection_errors", &self.retry_connection_errors)
.field("retry_timeout_errors", &self.retry_timeout_errors)
.field("total_timeout", &self.total_timeout)
.field("retry_predicate", &self.retry_predicate.is_some())
.finish()
}
}
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,
retry_statuses: DEFAULT_RETRY_STATUSES.to_vec(),
retry_server_errors: true,
respect_retry_after: true,
retry_connection_errors: true,
retry_timeout_errors: true,
total_timeout: Some(Duration::from_secs(30)),
retry_predicate: None,
}
}
}
impl RetryPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn max_retries(mut self, max_retries: u32) -> Self {
self.max_retries = max_retries;
self
}
pub fn backoff_initial(mut self, initial: Duration) -> Self {
self.backoff_initial = initial;
self
}
pub fn backoff_max(mut self, max: Duration) -> Self {
self.backoff_max = max;
self
}
pub fn backoff_jitter(mut self, jitter: f64) -> Self {
self.backoff_jitter = jitter;
self
}
pub fn retry_status(mut self, status: u16) -> Self {
if !self.retry_statuses.contains(&status) {
self.retry_statuses.push(status);
}
self
}
pub fn with_retry_statuses(mut self, statuses: impl IntoIterator<Item = u16>) -> Self {
self.retry_statuses = statuses.into_iter().collect();
self
}
pub fn retry_predicate(mut self, predicate: Option<RetryPredicate>) -> Self {
self.retry_predicate = predicate;
self
}
pub fn retry_server_errors(mut self, retry: bool) -> Self {
self.retry_server_errors = retry;
self
}
pub fn respect_retry_after(mut self, respect: bool) -> Self {
self.respect_retry_after = respect;
self
}
pub fn retry_connection_errors(mut self, retry: bool) -> Self {
self.retry_connection_errors = retry;
self
}
pub fn retry_timeout_errors(mut self, retry: bool) -> Self {
self.retry_timeout_errors = retry;
self
}
pub fn total_timeout(mut self, timeout: Option<Duration>) -> Self {
self.total_timeout = timeout;
self
}
pub(crate) fn validate(&self) -> Result<(), Error> {
if !(0.0..=1.0).contains(&self.backoff_jitter) || !self.backoff_jitter.is_finite() {
return Err(Error::Config(format!(
"backoff_jitter must be between 0.0 and 1.0, got {}",
self.backoff_jitter
)));
}
Ok(())
}
pub fn is_retryable(&self, error: &Error) -> bool {
let builtin = match error {
Error::Connection(_) => self.retry_connection_errors,
Error::Timeout { .. } => self.retry_timeout_errors,
Error::Api(api) => {
self.retry_server_errors && (500..600).contains(&api.status)
|| self.retry_statuses.contains(&api.status)
}
Error::Config(_)
| Error::Decode { .. }
| Error::InvalidResponse { .. }
| Error::ResponseTooLarge { .. } => false,
};
builtin || self.retry_predicate.as_ref().is_some_and(|f| f(error))
}
pub fn delay_for(&self, error: &Error, retry_index: u32) -> Duration {
if self.respect_retry_after {
if let Some(delay) = error.retry_after() {
return delay;
}
}
self.backoff_delay(retry_index)
}
pub fn backoff_delay(&self, retry_index: u32) -> Duration {
if self.backoff_initial.is_zero() || self.backoff_max.is_zero() {
return Duration::ZERO;
}
let initial_ns = u64::try_from(self.backoff_initial.as_nanos()).unwrap_or(u64::MAX);
let multiplier = 1u64.checked_shl(retry_index).unwrap_or(u64::MAX);
let exponential = initial_ns.saturating_mul(multiplier);
let maximum_ns = u64::try_from(self.backoff_max.as_nanos()).unwrap_or(u64::MAX);
let capped = exponential.min(maximum_ns);
let jitter_ns = (capped as f64 * self.backoff_jitter * fastrand::f64()) as u64;
Duration::from_nanos(capped.saturating_sub(jitter_ns))
}
}