use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Clone, Copy)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize),
serde(try_from = "UncheckedBackoff")
)]
pub struct Backoff {
min: Duration,
max: Duration,
}
impl Backoff {
pub const DEFAULT_MIN: Duration = Duration::from_millis(250);
pub const DEFAULT_MAX: Duration = Duration::from_secs(4);
pub fn new(min: Duration, max: Duration) -> Result<Self, InvalidBackoff> {
if max < min {
return Err(InvalidBackoff { min, max });
}
Ok(Self { min, max })
}
pub fn min(self) -> Duration {
self.min
}
pub fn max(self) -> Duration {
self.max
}
pub(crate) fn duration(self, step: u32) -> Duration {
let factor = 1u32.checked_shl(step).unwrap_or(u32::MAX);
self.min.saturating_mul(factor).min(self.max)
}
}
impl Default for Backoff {
fn default() -> Self {
Self::new(Self::DEFAULT_MIN, Self::DEFAULT_MAX).expect("the default bounds are ordered")
}
}
#[derive(Debug, Error)]
#[error("max backoff {max:?} below min backoff {min:?}")]
pub struct InvalidBackoff {
min: Duration,
max: Duration,
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
struct UncheckedBackoff {
#[serde(with = "humantime_serde")]
min: Duration,
#[serde(with = "humantime_serde")]
max: Duration,
}
#[cfg(feature = "serde")]
impl TryFrom<UncheckedBackoff> for Backoff {
type Error = InvalidBackoff;
fn try_from(unchecked: UncheckedBackoff) -> Result<Self, Self::Error> {
Self::new(unchecked.min, unchecked.max)
}
}
#[cfg(test)]
mod tests {
use crate::backoff::Backoff;
use std::time::Duration;
const MIN: Duration = Duration::from_millis(250);
const MAX: Duration = Duration::from_secs(3);
#[test]
fn backoff_doubles_up_to_the_cap() {
let backoff = Backoff::new(MIN, MAX).expect("the bounds are ordered");
assert_eq!(backoff.duration(0), MIN);
assert_eq!(backoff.duration(1), MIN * 2);
assert_eq!(backoff.duration(2), MIN * 4);
assert_eq!(backoff.duration(64), MAX);
assert_eq!(backoff.duration(u32::MAX), MAX);
}
#[test]
fn backoff_of_zero_stays_zero() {
let backoff = Backoff::new(Duration::ZERO, MAX).expect("the bounds are ordered");
assert_eq!(backoff.duration(5), Duration::ZERO);
}
#[test]
fn a_cap_below_the_minimum_is_rejected() {
assert!(Backoff::new(MAX, MIN).is_err());
assert!(Backoff::new(MIN, MIN).is_ok());
}
#[cfg(feature = "serde")]
#[test]
fn deserializing_validates_the_bounds() {
let backoff = serde_json::from_str::<Backoff>(r#"{ "min": "250ms", "max": "3s" }"#)
.expect("the bounds are ordered");
assert_eq!(backoff.min(), MIN);
assert_eq!(backoff.max(), MAX);
assert!(serde_json::from_str::<Backoff>(r#"{ "min": "3s", "max": "250ms" }"#).is_err());
}
}