use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backoff {
Fixed(Duration),
Exponential {
base: Duration,
max: Duration,
},
None,
}
impl Backoff {
pub fn exponential() -> Self {
Self::Exponential { base: Duration::from_millis(100), max: Duration::from_secs(10) }
}
pub fn fixed(wait: Duration) -> Self {
Self::Fixed(wait)
}
pub fn wait_before(&self, attempt: u32) -> Duration {
if attempt <= 1 {
return Duration::ZERO;
}
match self {
Self::None => Duration::ZERO,
Self::Fixed(wait) => *wait,
Self::Exponential { base, max } => {
let doublings = (attempt - 2).min(20);
let wait = base.saturating_mul(2u32.saturating_pow(doublings));
wait.min(*max)
}
}
}
}
impl Default for Backoff {
fn default() -> Self {
Self::exponential()
}
}
pub fn is_retryable(status: Option<u16>) -> bool {
match status {
None => true,
Some(408 | 425 | 429) => true,
Some(status) => status >= 500,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nothing_waits_before_the_first_attempt() {
for backoff in
[Backoff::exponential(), Backoff::fixed(Duration::from_secs(1)), Backoff::None]
{
assert_eq!(backoff.wait_before(1), Duration::ZERO, "{backoff:?}");
assert_eq!(backoff.wait_before(0), Duration::ZERO, "{backoff:?}");
}
}
#[test]
fn exponential_starts_at_the_base_and_doubles() {
let backoff =
Backoff::Exponential { base: Duration::from_millis(100), max: Duration::from_secs(60) };
assert_eq!(backoff.wait_before(2), Duration::from_millis(100));
assert_eq!(backoff.wait_before(3), Duration::from_millis(200));
assert_eq!(backoff.wait_before(4), Duration::from_millis(400));
}
#[test]
fn exponential_is_capped() {
let backoff =
Backoff::Exponential { base: Duration::from_millis(100), max: Duration::from_secs(2) };
assert_eq!(backoff.wait_before(20), Duration::from_secs(2));
assert_eq!(backoff.wait_before(1000), Duration::from_secs(2));
}
#[test]
fn fixed_is_the_same_every_time() {
let backoff = Backoff::fixed(Duration::from_millis(250));
assert_eq!(backoff.wait_before(2), Duration::from_millis(250));
assert_eq!(backoff.wait_before(9), Duration::from_millis(250));
}
#[test]
fn a_transport_failure_is_retried() {
assert!(is_retryable(None));
}
#[test]
fn the_server_saying_later_is_retried() {
for status in [408, 425, 429, 500, 502, 503, 504] {
assert!(is_retryable(Some(status)), "{status}");
}
}
#[test]
fn a_client_mistake_is_not_retried() {
for status in [400, 401, 402, 403, 404, 409, 422] {
assert!(!is_retryable(Some(status)), "{status}");
}
}
#[test]
fn success_is_not_retried() {
for status in [200, 201, 204, 301, 302] {
assert!(!is_retryable(Some(status)), "{status}");
}
}
}