use std::sync::Arc;
use std::time::Duration;
use crate::error::GraphError;
#[derive(Clone)]
pub enum RetryOn {
Any,
Timeout,
Custom(Arc<dyn Fn(&GraphError) -> bool + Send + Sync>),
}
impl std::fmt::Debug for RetryOn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Any => f.write_str("Any"),
Self::Timeout => f.write_str("Timeout"),
Self::Custom(_) => f.write_str("Custom(..)"),
}
}
}
impl RetryOn {
pub fn should_retry(&self, error: &GraphError) -> bool {
if matches!(error, GraphError::Interrupted(_)) {
return false;
}
match self {
Self::Any => true,
Self::Timeout => matches!(error, GraphError::NodeTimedOut { .. }),
Self::Custom(predicate) => predicate(error),
}
}
}
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub initial_delay: Duration,
pub max_delay: Duration,
pub backoff_factor: f64,
pub jitter: f64,
pub retry_on: RetryOn,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 10,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
backoff_factor: 2.0,
jitter: 1.0,
retry_on: RetryOn::Any,
}
}
}
impl RetryPolicy {
pub fn new(max_attempts: u32) -> Self {
Self { max_attempts: max_attempts.max(1), ..Default::default() }
}
pub fn with_initial_delay(mut self, delay: Duration) -> Self {
self.initial_delay = delay;
self
}
pub fn with_max_delay(mut self, delay: Duration) -> Self {
self.max_delay = delay;
self
}
pub fn with_backoff_factor(mut self, factor: f64) -> Self {
self.backoff_factor = factor;
self
}
pub fn with_jitter(mut self, jitter: f64) -> Self {
self.jitter = jitter.clamp(0.0, 1.0);
self
}
pub fn with_retry_on(mut self, retry_on: RetryOn) -> Self {
self.retry_on = retry_on;
self
}
pub fn allows_another_attempt(&self, attempts_so_far: u32) -> bool {
attempts_so_far < self.max_attempts
}
pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
if attempt == 0 {
return Duration::ZERO;
}
let factor = if self.backoff_factor <= 0.0 { 1.0 } else { self.backoff_factor };
let mut millis = self.initial_delay.as_millis() as f64;
for _ in 1..attempt {
millis *= factor;
}
let capped = millis.min(self.max_delay.as_millis() as f64);
if self.jitter <= 0.0 {
return Duration::from_millis(capped as u64);
}
let spread = capped * self.jitter;
let offset = pseudo_random_unit() * 2.0 * spread - spread;
Duration::from_millis((capped + offset).max(0.0) as u64)
}
}
fn pseudo_random_unit() -> f64 {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.subsec_nanos()).unwrap_or(0);
f64::from(nanos % 1_000_000) / 1_000_000.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_default_policy_retries_ten_times() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_attempts, 10);
assert!(policy.allows_another_attempt(1), "a first failure is retried");
assert!(policy.allows_another_attempt(9), "and so is a ninth");
assert!(!policy.allows_another_attempt(10), "the tenth is the last");
}
#[test]
fn the_default_gives_up_after_about_four_minutes() {
let policy = RetryPolicy::default().with_jitter(0.0);
let total: Duration = (1..policy.max_attempts).map(|n| policy.delay_for_attempt(n)).sum();
assert_eq!(total, Duration::from_secs(243));
}
#[test]
fn delays_grow_by_the_backoff_factor() {
let policy = RetryPolicy::new(5)
.with_initial_delay(Duration::from_millis(100))
.with_backoff_factor(3.0)
.with_jitter(0.0);
assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(100));
assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(300));
assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(900));
}
#[test]
fn a_delay_is_capped() {
let policy = RetryPolicy::new(10)
.with_initial_delay(Duration::from_millis(100))
.with_max_delay(Duration::from_millis(250))
.with_backoff_factor(10.0)
.with_jitter(0.0);
assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(250));
}
#[test]
fn a_non_positive_backoff_factor_gives_a_constant_delay() {
let policy = RetryPolicy::new(4)
.with_initial_delay(Duration::from_millis(50))
.with_backoff_factor(0.0)
.with_jitter(0.0);
assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(50));
assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(50));
}
#[test]
fn an_interrupt_is_never_retried() {
let interrupt = GraphError::Interrupted(Box::new(crate::error::InterruptedExecution::new(
"t".to_string(),
"c".to_string(),
crate::interrupt::Interrupt::Before("n".to_string()),
Default::default(),
0,
)));
assert!(!RetryOn::Any.should_retry(&interrupt));
let always = RetryOn::Custom(Arc::new(|_| true));
assert!(!always.should_retry(&interrupt), "even a permissive predicate must not retry it");
}
#[test]
fn timeout_only_retries_a_timeout() {
let timeout =
GraphError::NodeTimedOut { node: "slow".to_string(), elapsed: Duration::from_secs(1) };
let other =
GraphError::NodeExecutionFailed { node: "n".to_string(), message: "boom".to_string() };
assert!(RetryOn::Timeout.should_retry(&timeout));
assert!(!RetryOn::Timeout.should_retry(&other));
assert!(RetryOn::Any.should_retry(&other));
}
}