aion_server/worker/supervisor/
policy.rs1use std::num::NonZeroU32;
8use std::time::Duration;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub struct SupervisionPolicy {
13 pub restart_backoff_initial: Duration,
15 pub restart_backoff_max: Duration,
17 pub restart_backoff_multiplier: NonZeroU32,
19 pub restart_window: Duration,
21 pub max_restarts_per_window: NonZeroU32,
23 pub stop_grace: Duration,
25}
26
27impl SupervisionPolicy {
28 #[must_use]
36 pub fn backoff_after(&self, restarts_in_window: u32) -> Duration {
37 let multiplier = self.restart_backoff_multiplier.get();
38 let mut delay = self.restart_backoff_initial;
39 for _ in 0..restarts_in_window {
40 delay = delay.saturating_mul(multiplier);
41 if delay >= self.restart_backoff_max {
42 return self.restart_backoff_max;
43 }
44 }
45 delay.min(self.restart_backoff_max)
46 }
47}
48
49pub const UNCOMMISSIONED_REMEDY: &str = "managed-worker supervision is not configured on this \
55 server: add a `[worker_supervision]` section naming \
56 `restart_backoff_initial_ms`, `restart_backoff_max_ms`, \
57 `restart_backoff_multiplier`, `restart_window_ms`, \
58 `max_restarts_per_window`, and `stop_grace_ms` (there are no defaults — \
59 the restart discipline is an operator decision), then restart the server";
60
61#[cfg(test)]
62mod tests {
63 use std::num::NonZeroU32;
64 use std::time::Duration;
65
66 use super::SupervisionPolicy;
67
68 fn policy(initial_ms: u64, max_ms: u64, multiplier: u32) -> Option<SupervisionPolicy> {
69 Some(SupervisionPolicy {
70 restart_backoff_initial: Duration::from_millis(initial_ms),
71 restart_backoff_max: Duration::from_millis(max_ms),
72 restart_backoff_multiplier: NonZeroU32::new(multiplier)?,
73 restart_window: Duration::from_secs(60),
74 max_restarts_per_window: NonZeroU32::new(3)?,
75 stop_grace: Duration::from_secs(1),
76 })
77 }
78
79 #[test]
80 fn first_restart_waits_exactly_the_configured_initial_backoff() -> Result<(), &'static str> {
81 let policy = policy(100, 10_000, 2).ok_or("policy must build")?;
82 assert_eq!(policy.backoff_after(0), Duration::from_millis(100));
83 Ok(())
84 }
85
86 #[test]
87 fn backoff_grows_geometrically_then_clamps_to_the_ceiling() -> Result<(), &'static str> {
88 let policy = policy(100, 400, 2).ok_or("policy must build")?;
89 assert_eq!(policy.backoff_after(1), Duration::from_millis(200));
90 assert_eq!(policy.backoff_after(2), Duration::from_millis(400));
91 assert_eq!(policy.backoff_after(3), Duration::from_millis(400));
92 assert_eq!(policy.backoff_after(64), Duration::from_millis(400));
93 Ok(())
94 }
95
96 #[test]
99 fn a_multiplier_of_one_keeps_the_initial_delay_forever() -> Result<(), &'static str> {
100 let policy = policy(250, 10_000, 1).ok_or("policy must build")?;
101 assert_eq!(policy.backoff_after(0), Duration::from_millis(250));
102 assert_eq!(policy.backoff_after(9), Duration::from_millis(250));
103 Ok(())
104 }
105
106 #[test]
109 fn extreme_growth_saturates_into_the_ceiling() -> Result<(), &'static str> {
110 let policy = policy(1_000, 30_000, u32::MAX).ok_or("policy must build")?;
111 assert_eq!(policy.backoff_after(16), Duration::from_secs(30));
112 Ok(())
113 }
114}