use std::{
cmp,
fmt::Debug,
time::{Duration, Instant},
};
use rand::{Rng, RngExt};
#[derive(Debug, PartialEq, Eq)]
pub enum RetryAction {
OKAY,
WAIT,
}
pub trait RetryPolicy: Debug + PartialEq + Eq {
fn max_tries(&self) -> usize;
fn current_tries(&self) -> usize;
fn fail<R: Rng + ?Sized>(&mut self, now: Instant, rng: &mut R);
fn succeed(&mut self, now: Instant);
fn can_try(&self, _now: Instant) -> Option<RetryAction> {
if self.current_tries() >= self.max_tries() {
None
} else {
Some(RetryAction::OKAY)
}
}
fn is_down(&self) -> bool;
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum RetryPolicyWrapper {
ExponentialBackoff(ExponentialBackoffPolicy),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ExponentialBackoffPolicy {
max_tries: usize,
current_tries: usize,
last_try: Option<Instant>,
wait: Duration,
}
impl ExponentialBackoffPolicy {
pub fn new(max_tries: usize) -> Self {
ExponentialBackoffPolicy {
max_tries,
current_tries: 0,
last_try: None,
wait: Duration::ZERO,
}
}
fn since_last_try(&self, now: Instant) -> Duration {
self.last_try.map_or(Duration::ZERO, |last_try| {
now.saturating_duration_since(last_try)
})
}
}
impl RetryPolicy for ExponentialBackoffPolicy {
fn max_tries(&self) -> usize {
self.max_tries
}
fn current_tries(&self) -> usize {
self.current_tries
}
fn fail<R: Rng + ?Sized>(&mut self, now: Instant, rng: &mut R) {
if self.since_last_try(now).lt(&self.wait) {
return;
}
let max_secs = cmp::max(
1,
1u64.checked_shl(self.current_tries as u32)
.unwrap_or(u64::MAX),
);
let wait = if max_secs == 1 {
1
} else {
rng.random_range(1..max_secs)
};
self.wait = Duration::from_secs(wait);
self.last_try = Some(now);
self.current_tries = cmp::min(self.current_tries + 1, self.max_tries);
}
fn succeed(&mut self, now: Instant) {
self.wait = Duration::default();
self.last_try = Some(now);
self.current_tries = 0;
}
fn can_try(&self, now: Instant) -> Option<RetryAction> {
let action = if self.since_last_try(now).ge(&self.wait) {
RetryAction::OKAY
} else {
RetryAction::WAIT
};
Some(action)
}
fn is_down(&self) -> bool {
self.current_tries() >= self.max_tries()
}
}
#[cfg(test)]
impl ExponentialBackoffPolicy {
pub(crate) fn force_down(&mut self) {
self.current_tries = self.max_tries;
}
}
#[cfg(test)]
impl RetryPolicyWrapper {
pub(crate) fn force_down(&mut self) {
match self {
RetryPolicyWrapper::ExponentialBackoff(p) => p.force_down(),
}
}
}
impl From<ExponentialBackoffPolicy> for RetryPolicyWrapper {
fn from(val: ExponentialBackoffPolicy) -> Self {
RetryPolicyWrapper::ExponentialBackoff(val)
}
}
impl RetryPolicy for RetryPolicyWrapper {
fn max_tries(&self) -> usize {
match *self {
RetryPolicyWrapper::ExponentialBackoff(ref policy) => policy,
}
.max_tries()
}
fn current_tries(&self) -> usize {
match *self {
RetryPolicyWrapper::ExponentialBackoff(ref policy) => policy,
}
.current_tries()
}
fn fail<R: Rng + ?Sized>(&mut self, now: Instant, rng: &mut R) {
match *self {
RetryPolicyWrapper::ExponentialBackoff(ref mut policy) => policy,
}
.fail(now, rng)
}
fn succeed(&mut self, now: Instant) {
match *self {
RetryPolicyWrapper::ExponentialBackoff(ref mut policy) => policy,
}
.succeed(now)
}
fn can_try(&self, now: Instant) -> Option<RetryAction> {
match *self {
RetryPolicyWrapper::ExponentialBackoff(ref policy) => policy,
}
.can_try(now)
}
fn is_down(&self) -> bool {
match *self {
RetryPolicyWrapper::ExponentialBackoff(ref policy) => policy,
}
.is_down()
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use rand::{SeedableRng, rngs::StdRng};
use super::{ExponentialBackoffPolicy, RetryAction, RetryPolicy};
const MAX_FAILS: usize = 10;
fn rng() -> StdRng {
StdRng::seed_from_u64(0x5eed)
}
#[test]
fn no_fail() {
let policy = ExponentialBackoffPolicy::new(MAX_FAILS);
let can_try = policy.can_try(Instant::now());
assert_eq!(Some(RetryAction::OKAY), can_try)
}
#[test]
fn single_fail() {
let t0 = Instant::now();
let mut policy = ExponentialBackoffPolicy::new(MAX_FAILS);
policy.fail(t0, &mut rng());
let can_try = policy.can_try(t0);
assert_eq!(Some(RetryAction::WAIT), can_try)
}
#[test]
fn max_fails() {
let t0 = Instant::now();
let mut rng = rng();
let mut policy = ExponentialBackoffPolicy::new(MAX_FAILS);
for _ in 0..MAX_FAILS {
policy.fail(t0, &mut rng);
}
let can_try = policy.can_try(t0);
assert_eq!(Some(RetryAction::WAIT), can_try)
}
#[test]
fn recover_from_fail() {
let t0 = Instant::now();
let mut rng = rng();
let mut policy = ExponentialBackoffPolicy::new(MAX_FAILS);
for _ in 0..(MAX_FAILS - 1) {
policy.fail(t0, &mut rng);
}
policy.succeed(t0);
policy.fail(t0, &mut rng);
policy.fail(t0, &mut rng);
policy.fail(t0, &mut rng);
let can_try = policy.can_try(t0);
assert_eq!(Some(RetryAction::WAIT), can_try)
}
#[test]
fn the_backoff_window_follows_the_injected_clock() {
let t0 = Instant::now();
let mut policy = ExponentialBackoffPolicy::new(MAX_FAILS);
policy.fail(t0, &mut rng());
let one_second = Duration::from_secs(1);
assert_eq!(
policy.can_try(t0 + one_second - Duration::from_nanos(1)),
Some(RetryAction::WAIT),
"one nanosecond before the window closes, the policy must wait"
);
assert_eq!(
policy.can_try(t0 + one_second),
Some(RetryAction::OKAY),
"the window closes at t0 + 1s on the injected clock"
);
}
fn windows(seed: u64) -> Vec<Duration> {
let mut rng = StdRng::seed_from_u64(seed);
let mut policy = ExponentialBackoffPolicy::new(MAX_FAILS);
let mut now = Instant::now();
let mut windows = Vec::with_capacity(MAX_FAILS);
while !policy.is_down() {
policy.fail(now, &mut rng);
windows.push(policy.wait);
now += policy.wait;
}
windows
}
#[test]
fn one_seed_yields_one_sequence_of_backoff_windows() {
let first = windows(7);
assert_eq!(first.len(), MAX_FAILS);
assert_eq!(first, windows(7), "one seed must yield one sequence");
assert_ne!(
first,
windows(8),
"the jitter must come from the lent RNG, so another seed differs"
);
}
}