Skip to main content

ranvier_runtime/
retry.rs

1//! Configurable retry policies for Axon transitions.
2//!
3//! Provides `RetryPolicy` with fixed and exponential backoff strategies.
4
5use std::time::Duration;
6
7/// Configurable retry policy for individual Axon transitions.
8#[derive(Debug, Clone)]
9pub struct RetryPolicy {
10    /// Maximum number of retry attempts (not counting the initial attempt).
11    pub max_retries: u32,
12    /// Backoff strategy between retries.
13    pub backoff: BackoffStrategy,
14}
15
16/// Backoff strategy between retry attempts.
17#[derive(Debug, Clone)]
18pub enum BackoffStrategy {
19    /// Fixed delay between retries.
20    Fixed(Duration),
21    /// Exponential backoff with configurable initial delay, multiplier, and max delay.
22    Exponential {
23        initial: Duration,
24        multiplier: f64,
25        max: Duration,
26    },
27}
28
29impl RetryPolicy {
30    /// Create a retry policy with fixed delay between attempts.
31    pub fn fixed(max_retries: u32, delay: Duration) -> Self {
32        Self {
33            max_retries,
34            backoff: BackoffStrategy::Fixed(delay),
35        }
36    }
37
38    /// Create a retry policy with exponential backoff.
39    ///
40    /// # Arguments
41    /// - `max_retries`: Maximum number of retry attempts
42    /// - `initial`: Initial delay (e.g., 100ms)
43    /// - `multiplier`: Backoff multiplier (e.g., 2.0 for doubling)
44    /// - `max`: Maximum delay cap
45    pub fn exponential(max_retries: u32, initial: Duration, multiplier: f64, max: Duration) -> Self {
46        Self {
47            max_retries,
48            backoff: BackoffStrategy::Exponential {
49                initial,
50                multiplier,
51                max,
52            },
53        }
54    }
55
56    /// Convenience: exponential backoff starting at `initial_ms` milliseconds,
57    /// doubling each time, capped at 30 seconds.
58    pub fn exponential_default(max_retries: u32, initial_ms: u64) -> Self {
59        Self::exponential(
60            max_retries,
61            Duration::from_millis(initial_ms),
62            2.0,
63            Duration::from_secs(30),
64        )
65    }
66
67    /// Calculate the delay for the given attempt number (0-indexed).
68    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
69        match &self.backoff {
70            BackoffStrategy::Fixed(d) => *d,
71            BackoffStrategy::Exponential {
72                initial,
73                multiplier,
74                max,
75            } => {
76                let delay_ms =
77                    initial.as_millis() as f64 * multiplier.powi(attempt as i32);
78                let delay = Duration::from_millis(delay_ms as u64);
79                if delay > *max { *max } else { delay }
80            }
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn fixed_delay_is_constant() {
91        let policy = RetryPolicy::fixed(3, Duration::from_millis(100));
92        assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
93        assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(100));
94        assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(100));
95    }
96
97    #[test]
98    fn exponential_delay_doubles() {
99        let policy = RetryPolicy::exponential(
100            5,
101            Duration::from_millis(100),
102            2.0,
103            Duration::from_secs(10),
104        );
105        assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
106        assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(200));
107        assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(400));
108        assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(800));
109    }
110
111    #[test]
112    fn exponential_delay_caps_at_max() {
113        let policy = RetryPolicy::exponential(
114            10,
115            Duration::from_millis(100),
116            2.0,
117            Duration::from_millis(500),
118        );
119        assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
120        assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(200));
121        assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(400));
122        assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(500)); // capped
123        assert_eq!(policy.delay_for_attempt(4), Duration::from_millis(500)); // still capped
124    }
125
126    #[test]
127    fn default_exponential_starts_correctly() {
128        let policy = RetryPolicy::exponential_default(3, 100);
129        assert_eq!(policy.max_retries, 3);
130        assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
131        assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(200));
132    }
133}