use std::time::{Duration, Instant};
use crate::constants::{RETRY_GLOBAL_DEADLINE_GRACE_MS, RETRY_MAX_BUDGET_SECS, RETRY_SLICE_MS};
const MAX_BUDGET: Duration = Duration::from_secs(RETRY_MAX_BUDGET_SECS);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryPolicy {
base: Duration,
cap: Duration,
max_elapsed: Duration,
max_attempts: u32,
}
impl RetryPolicy {
#[must_use]
pub const fn new(base_ms: u64, cap_ms: u64, max_elapsed_ms: u64, max_attempts: u32) -> Self {
Self {
base: Duration::from_millis(base_ms),
cap: Duration::from_millis(cap_ms),
max_elapsed: Duration::from_millis(max_elapsed_ms),
max_attempts,
}
}
#[must_use]
pub fn with_shape_overrides(mut self, tuning: &crate::config::Tuning) -> Self {
if let Some(base_ms) = tuning.retry_base_ms {
self.base = Duration::from_millis(base_ms);
}
if let Some(max_ms) = tuning.retry_max_ms {
self.cap = Duration::from_millis(max_ms);
}
self
}
#[must_use]
pub fn with_budget_override(mut self, tuning: &crate::config::Tuning) -> Self {
if let Some(max_elapsed_ms) = tuning.retry_max_elapsed_ms {
self.max_elapsed = Duration::from_millis(max_elapsed_ms);
}
self
}
#[must_use]
pub fn ceiling(&self, attempt: u32) -> Duration {
let factor = 1u32.checked_shl(attempt).unwrap_or(u32::MAX);
self.base.saturating_mul(factor).min(self.cap)
}
#[must_use]
pub fn jittered(&self, attempt: u32, rng: &mut fastrand::Rng) -> Duration {
let ceiling = u64::try_from(self.ceiling(attempt).as_micros()).unwrap_or(u64::MAX);
Duration::from_micros(rng.u64(0..=ceiling))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum RetryStop {
#[error("retry cancelled by a signal or the global deadline")]
Cancelled,
#[error("retry time budget exhausted")]
DeadlineElapsed,
#[error("retry attempts exhausted")]
AttemptsExhausted,
}
#[derive(Debug)]
pub struct Backoff {
policy: RetryPolicy,
started: Instant,
deadline: Instant,
global_bound: bool,
attempt: u32,
rng: fastrand::Rng,
}
impl Backoff {
#[must_use]
pub fn new(policy: RetryPolicy) -> Self {
Self::with_parts(
policy.with_shape_overrides(&crate::config::tuning()),
fastrand::Rng::new(),
crate::signal::global_deadline_remaining(),
)
}
#[must_use]
pub fn with_parts(
policy: RetryPolicy,
rng: fastrand::Rng,
global_left: Option<Duration>,
) -> Self {
let started = Instant::now();
let (left, global_bound) = match global_left {
Some(global) if global <= policy.max_elapsed => (global, true),
_ => (policy.max_elapsed, false),
};
Self {
policy,
started,
deadline: started + left.min(MAX_BUDGET),
global_bound,
attempt: 0,
rng,
}
}
#[must_use]
pub fn attempts(&self) -> u32 {
self.attempt
}
#[must_use]
pub fn elapsed(&self) -> Duration {
self.started.elapsed()
}
pub fn wait(&mut self) -> Result<(), RetryStop> {
self.wait_with(crate::signal::is_global_shutdown)
}
pub fn wait_with(&mut self, cancelled: impl Fn() -> bool) -> Result<(), RetryStop> {
if cancelled() {
return Err(RetryStop::Cancelled);
}
if self.attempt >= self.policy.max_attempts {
return Err(RetryStop::AttemptsExhausted);
}
let left = self.deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(self.stop_at_deadline(&cancelled));
}
let delay = self.policy.jittered(self.attempt, &mut self.rng).min(left);
sleep_sliced(delay, &cancelled)?;
self.attempt = self.attempt.saturating_add(1);
Ok(())
}
fn stop_at_deadline(&self, cancelled: &impl Fn() -> bool) -> RetryStop {
if !self.global_bound {
return RetryStop::DeadlineElapsed;
}
let grace = Duration::from_millis(RETRY_GLOBAL_DEADLINE_GRACE_MS);
match sleep_sliced(grace, cancelled) {
Err(stop) => stop,
Ok(()) => RetryStop::DeadlineElapsed,
}
}
}
fn sleep_sliced(total: Duration, cancelled: &impl Fn() -> bool) -> Result<(), RetryStop> {
let slice = Duration::from_millis(RETRY_SLICE_MS);
let end = Instant::now() + total;
loop {
if cancelled() {
return Err(RetryStop::Cancelled);
}
let left = end.saturating_duration_since(Instant::now());
if left.is_zero() {
return Ok(());
}
std::thread::sleep(left.min(slice));
}
}
#[derive(Debug)]
pub enum Step<T, E> {
Done(T),
Retry(E),
Fail(E),
}
#[derive(Debug, thiserror::Error)]
pub enum RetryFailure<E> {
#[error("operation failed and cannot be retried")]
Permanent(E),
#[error("retry stopped after {attempts} waits: {stop}")]
Stopped {
stop: RetryStop,
last: E,
attempts: u32,
},
}
pub fn retry<T, E>(
policy: RetryPolicy,
mut op: impl FnMut() -> Step<T, E>,
) -> Result<T, RetryFailure<E>> {
let mut backoff = Backoff::new(policy.with_budget_override(&crate::config::tuning()));
loop {
let last = match op() {
Step::Done(value) => return Ok(value),
Step::Fail(e) => return Err(RetryFailure::Permanent(e)),
Step::Retry(e) => e,
};
if let Err(stop) = backoff.wait() {
return Err(RetryFailure::Stopped {
stop,
last,
attempts: backoff.attempts(),
});
}
}
}
include!("retry_tests.inc.rs");