use std::time::Duration;
pub const MAX_ATTEMPTS: u32 = 4;
pub const BACKOFF_BASE: Duration = Duration::from_millis(250);
pub const BACKOFF_CAP: Duration = Duration::from_secs(4);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailureKind {
Transport,
Status(u16),
Decode,
}
pub fn is_retryable(kind: FailureKind) -> bool {
match kind {
FailureKind::Transport => true,
FailureKind::Status(code) => code >= 500 || code == 429,
FailureKind::Decode => false,
}
}
pub fn backoff_delay(attempt: u32, jitter: f64) -> Duration {
let ceiling = BACKOFF_BASE
.saturating_mul(2u32.saturating_pow(attempt.min(16)))
.min(BACKOFF_CAP);
ceiling.mul_f64(jitter.clamp(0.0, 1.0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_missing_species_is_not_retried() {
assert!(!is_retryable(FailureKind::Status(404)));
}
#[test]
fn client_errors_are_final() {
for code in [400, 401, 403, 404, 410, 418] {
assert!(!is_retryable(FailureKind::Status(code)), "{code}");
}
}
#[test]
fn server_errors_are_retried() {
for code in [500, 502, 503, 504] {
assert!(is_retryable(FailureKind::Status(code)), "{code}");
}
}
#[test]
fn being_rate_limited_is_retried() {
assert!(is_retryable(FailureKind::Status(429)));
}
#[test]
fn a_success_status_is_never_classified_as_retryable() {
assert!(!is_retryable(FailureKind::Status(200)));
assert!(!is_retryable(FailureKind::Status(304)));
}
#[test]
fn timeouts_and_connection_failures_are_retried() {
assert!(is_retryable(FailureKind::Transport));
}
#[test]
fn a_malformed_body_is_not_retried() {
assert!(!is_retryable(FailureKind::Decode));
}
#[test]
fn the_backoff_ceiling_doubles_per_attempt() {
assert_eq!(backoff_delay(0, 1.0), BACKOFF_BASE);
assert_eq!(backoff_delay(1, 1.0), BACKOFF_BASE * 2);
assert_eq!(backoff_delay(2, 1.0), BACKOFF_BASE * 4);
}
#[test]
fn the_delay_never_exceeds_the_cap() {
for attempt in 0..64 {
assert!(backoff_delay(attempt, 1.0) <= BACKOFF_CAP, "{attempt}");
}
}
#[test]
fn every_delay_stays_within_its_jittered_range() {
for attempt in 0..8 {
let ceiling = backoff_delay(attempt, 1.0);
for step in 0..=10 {
let delay = backoff_delay(attempt, step as f64 / 10.0);
assert!(delay <= ceiling, "attempt {attempt}, step {step}");
}
}
}
#[test]
fn full_jitter_can_draw_the_bottom_of_the_range() {
assert_eq!(backoff_delay(3, 0.0), Duration::ZERO);
}
#[test]
fn jitter_outside_the_unit_range_is_clamped() {
assert_eq!(backoff_delay(1, 5.0), backoff_delay(1, 1.0));
assert_eq!(backoff_delay(1, -5.0), backoff_delay(1, 0.0));
}
#[test]
fn a_large_attempt_number_does_not_overflow() {
assert_eq!(backoff_delay(u32::MAX, 1.0), BACKOFF_CAP);
}
}