Skip to main content

ave_actors_actor/
supervision.rs

1//! Retry and supervision strategies for actor startup failures.
2
3use std::{collections::VecDeque, fmt::Debug, time::Duration};
4
5/// Defines how many times and how quickly a failing actor is restarted.
6///
7/// Implement this to create a custom backoff policy. The actor system calls
8/// `max_retries` to determine the attempt budget and `next_backoff` before
9/// each retry to get the delay (or `None` for immediate retry).
10pub trait RetryStrategy: Debug + Send + Sync {
11    /// Returns the maximum number of restart attempts before the actor is permanently stopped.
12    fn max_retries(&self) -> usize;
13
14    /// Returns the delay before the next restart attempt, or `None` to retry immediately.
15    fn next_backoff(&mut self) -> Option<Duration>;
16}
17
18/// Determines what happens when an actor fails during startup.
19///
20/// Pass this when creating an actor to control whether the system stops
21/// on first failure or retries with a configurable back-off strategy.
22#[derive(Debug, Clone)]
23pub enum SupervisionStrategy {
24    /// Stop the actor permanently on the first startup error.
25    Stop,
26    /// Retry startup using the given [`Strategy`].
27    Retry(Strategy),
28}
29
30/// Concrete retry strategy implementations. Choose `NoInterval`, `FixedInterval`, or `CustomIntervalStrategy`.
31#[derive(Debug, Clone)]
32pub enum Strategy {
33    /// Retry immediately with no delay between attempts.
34    NoInterval(NoIntervalStrategy),
35    /// Retry with a fixed delay between attempts.
36    FixedInterval(FixedIntervalStrategy),
37    /// Retry with custom-defined delays for each attempt.
38    CustomIntervalStrategy(CustomIntervalStrategy),
39}
40
41impl RetryStrategy for Strategy {
42    fn max_retries(&self) -> usize {
43        match self {
44            Self::NoInterval(strategy) => strategy.max_retries(),
45            Self::FixedInterval(strategy) => strategy.max_retries(),
46            Self::CustomIntervalStrategy(strategy) => strategy.max_retries(),
47        }
48    }
49
50    fn next_backoff(&mut self) -> Option<Duration> {
51        match self {
52            Self::NoInterval(strategy) => strategy.next_backoff(),
53            Self::FixedInterval(strategy) => strategy.next_backoff(),
54            Self::CustomIntervalStrategy(strategy) => strategy.next_backoff(),
55        }
56    }
57}
58
59impl Default for Strategy {
60    fn default() -> Self {
61        Self::NoInterval(NoIntervalStrategy::default())
62    }
63}
64
65/// Retries startup immediately with no delay between attempts, up to `max_retries` times.
66#[derive(Debug, Default, Clone)]
67pub struct NoIntervalStrategy {
68    /// Maximum number of retry attempts.
69    max_retries: usize,
70}
71
72impl NoIntervalStrategy {
73    /// Creates the strategy with up to `max_retries` immediate restart attempts.
74    pub const fn new(max_retries: usize) -> Self {
75        Self { max_retries }
76    }
77}
78
79impl RetryStrategy for NoIntervalStrategy {
80    fn max_retries(&self) -> usize {
81        self.max_retries
82    }
83
84    fn next_backoff(&mut self) -> Option<Duration> {
85        None
86    }
87}
88
89/// Retries startup after a fixed delay between each attempt, up to `max_retries` times.
90#[derive(Debug, Default, Clone)]
91pub struct FixedIntervalStrategy {
92    /// Maximum number of retries before permanently failing an actor.
93    max_retries: usize,
94    /// Fixed wait duration before each retry attempt.
95    duration: Duration,
96}
97
98impl FixedIntervalStrategy {
99    /// Creates the strategy with up to `max_retries` attempts and `duration` wait between each.
100    pub const fn new(max_retries: usize, duration: Duration) -> Self {
101        Self {
102            max_retries,
103            duration,
104        }
105    }
106}
107
108impl RetryStrategy for FixedIntervalStrategy {
109    fn max_retries(&self) -> usize {
110        self.max_retries
111    }
112
113    fn next_backoff(&mut self) -> Option<Duration> {
114        Some(self.duration)
115    }
116}
117
118/// Retries startup with a per-attempt delay sequence defined by a `VecDeque<Duration>`.
119///
120/// The number of durations provided sets the retry budget: each call to
121/// `next_backoff` pops one duration from the front until the queue is empty.
122#[derive(Debug, Default, Clone)]
123pub struct CustomIntervalStrategy {
124    /// Queue of delay durations for each retry attempt.
125    /// Each call to next_backoff() pops one duration from the front.
126    durations: VecDeque<Duration>,
127    /// Maximum number of retries (equal to the number of durations provided).
128    max_retries: usize,
129}
130
131impl CustomIntervalStrategy {
132    /// Creates the strategy from `durations`; `max_retries` is set to `durations.len()`.
133    pub fn new(durations: VecDeque<Duration>) -> Self {
134        let max_retries = durations.len();
135        Self {
136            durations,
137            max_retries,
138        }
139    }
140}
141
142impl RetryStrategy for CustomIntervalStrategy {
143    fn max_retries(&self) -> usize {
144        self.max_retries
145    }
146
147    fn next_backoff(&mut self) -> Option<Duration> {
148        self.durations.pop_front()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154
155    use super::*;
156
157    #[test]
158    fn test_no_interval_strategy() {
159        let mut strategy = NoIntervalStrategy::new(3);
160        assert_eq!(strategy.max_retries(), 3);
161        assert_eq!(strategy.next_backoff(), None);
162    }
163
164    #[test]
165    fn test_fixed_interval_strategy() {
166        let mut strategy =
167            FixedIntervalStrategy::new(3, Duration::from_secs(1));
168        assert_eq!(strategy.max_retries(), 3);
169        assert_eq!(strategy.next_backoff(), Some(Duration::from_secs(1)));
170    }
171
172    #[test]
173    fn test_exponential_custom_strategy() {
174        let mut strategy = CustomIntervalStrategy::new(VecDeque::from([
175            Duration::from_secs(1),
176            Duration::from_secs(2),
177            Duration::from_secs(3),
178        ]));
179        assert_eq!(strategy.max_retries(), 3);
180        assert!(strategy.next_backoff().is_some());
181        assert!(strategy.next_backoff().is_some());
182        assert!(strategy.next_backoff().is_some());
183        assert!(strategy.next_backoff().is_none());
184    }
185}