Skip to main content

synaps_cli/extensions/runtime/
restart.rs

1//! Restart policy with exponential backoff for process extensions.
2
3use std::time::Duration;
4
5#[derive(Debug, Clone, Copy)]
6pub struct RestartPolicy {
7    pub max_attempts: u32,
8    pub base_delay: Duration,
9    pub max_delay: Duration,
10    /// Multiplier per attempt. Defaults to 2.0.
11    pub multiplier: f32,
12}
13
14impl Default for RestartPolicy {
15    fn default() -> Self {
16        Self {
17            max_attempts: 3,
18            base_delay: Duration::from_millis(250),
19            max_delay: Duration::from_secs(5),
20            multiplier: 2.0,
21        }
22    }
23}
24
25impl RestartPolicy {
26    /// Compute the delay before attempt `n` (1-indexed). Attempt 1 → base_delay.
27    /// Capped at `max_delay`. Returns `None` if `n > max_attempts` or `n == 0`.
28    pub fn delay_for_attempt(&self, attempt: u32) -> Option<Duration> {
29        if attempt == 0 || attempt > self.max_attempts {
30            return None;
31        }
32        let exp = (attempt - 1) as i32;
33        let factor = self.multiplier.powi(exp);
34        let nanos = (self.base_delay.as_nanos() as f64 * factor as f64)
35            .min(self.max_delay.as_nanos() as f64) as u64;
36        Some(Duration::from_nanos(nanos))
37    }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    #[test]
45    fn default_policy_attempt_1_returns_base_delay() {
46        let p = RestartPolicy::default();
47        assert_eq!(p.delay_for_attempt(1), Some(Duration::from_millis(250)));
48    }
49
50    #[test]
51    fn attempt_2_doubles_base() {
52        let p = RestartPolicy::default();
53        assert_eq!(p.delay_for_attempt(2), Some(Duration::from_millis(500)));
54    }
55
56    #[test]
57    fn attempt_capped_at_max_delay() {
58        let p = RestartPolicy {
59            max_attempts: 5,
60            base_delay: Duration::from_secs(1),
61            max_delay: Duration::from_secs(2),
62            multiplier: 10.0,
63        };
64        assert_eq!(p.delay_for_attempt(2), Some(Duration::from_secs(2)));
65    }
66
67    #[test]
68    fn attempt_zero_returns_none() {
69        let p = RestartPolicy::default();
70        assert_eq!(p.delay_for_attempt(0), None);
71    }
72
73    #[test]
74    fn attempt_beyond_max_returns_none() {
75        let p = RestartPolicy::default();
76        assert_eq!(p.delay_for_attempt(p.max_attempts + 1), None);
77    }
78
79    #[test]
80    fn custom_multiplier() {
81        let p = RestartPolicy {
82            max_attempts: 5,
83            base_delay: Duration::from_millis(100),
84            max_delay: Duration::from_secs(60),
85            multiplier: 3.0,
86        };
87        // attempt 3 -> base * 3^2 = 900ms
88        assert_eq!(p.delay_for_attempt(3), Some(Duration::from_millis(900)));
89    }
90
91    #[test]
92    fn default_max_attempts_is_three() {
93        assert_eq!(RestartPolicy::default().max_attempts, 3);
94    }
95}