1use serde::{Deserialize, Serialize};
11use std::time::Duration;
12
13pub const HARD_FAILURE_CEILING: u32 = 10;
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct Backoff {
19 pub base: Duration,
21 pub factor_milli: u32, pub max: Duration,
25}
26
27impl Backoff {
28 pub fn delay_for(&self, consecutive_failures: u32) -> Duration {
31 let raw_ms = if consecutive_failures <= 1 {
37 self.base.min(self.max).as_millis() as f64
38 } else {
39 let factor = (self.factor_milli as f64) / 1000.0;
40 let exp = (consecutive_failures - 1) as i32;
41 let base_ms = self.base.as_millis() as f64;
42 let scaled_ms = base_ms * factor.powi(exp);
43 let capped_ms = scaled_ms.min(self.max.as_millis() as f64);
44 if !capped_ms.is_finite() || capped_ms < 0.0 {
46 self.max.as_millis() as f64
47 } else {
48 capped_ms
49 }
50 };
51 Duration::from_millis(raw_ms.max(1.0) as u64)
57 }
58}
59
60impl Default for Backoff {
61 fn default() -> Self {
62 Backoff {
64 base: Duration::from_millis(500),
65 factor_milli: 2000,
66 max: Duration::from_secs(30),
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct RestartPolicy {
75 pub max_consecutive_failures: u32,
80 pub backoff: Backoff,
81}
82
83impl RestartPolicy {
84 pub fn new(max_consecutive_failures: u32, backoff: Backoff) -> Self {
85 RestartPolicy {
86 max_consecutive_failures,
87 backoff,
88 }
89 }
90
91 pub fn effective_ceiling(&self) -> u32 {
93 self.max_consecutive_failures.min(HARD_FAILURE_CEILING)
94 }
95
96 pub fn decide(&self, consecutive_failures: u32) -> RestartDecision {
98 if consecutive_failures >= self.effective_ceiling() {
99 RestartDecision::PermanentDead
100 } else {
101 RestartDecision::Restart {
102 after: self.backoff.delay_for(consecutive_failures),
103 }
104 }
105 }
106}
107
108impl Default for RestartPolicy {
109 fn default() -> Self {
110 RestartPolicy::new(5, Backoff::default())
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum RestartDecision {
119 Restart { after: Duration },
121 PermanentDead,
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn hard_ceiling_overrides_generous_provider_policy() {
131 let policy = RestartPolicy::new(100, Backoff::default());
133 assert_eq!(policy.effective_ceiling(), HARD_FAILURE_CEILING);
134 assert_eq!(
135 policy.decide(9),
136 RestartDecision::Restart {
137 after: policy.backoff.delay_for(9)
138 }
139 );
140 assert_eq!(policy.decide(10), RestartDecision::PermanentDead);
141 assert_eq!(policy.decide(50), RestartDecision::PermanentDead);
142 }
143
144 #[test]
145 fn conservative_provider_policy_triggers_permanent_dead_early() {
146 let policy = RestartPolicy::new(3, Backoff::default());
148 assert_eq!(policy.effective_ceiling(), 3);
149 assert!(matches!(policy.decide(2), RestartDecision::Restart { .. }));
150 assert_eq!(policy.decide(3), RestartDecision::PermanentDead);
151 }
152
153 #[test]
154 fn backoff_grows_and_caps() {
155 let backoff = Backoff {
156 base: Duration::from_millis(100),
157 factor_milli: 2000, max: Duration::from_millis(800),
159 };
160 assert_eq!(backoff.delay_for(1), Duration::from_millis(100));
161 assert_eq!(backoff.delay_for(2), Duration::from_millis(200));
162 assert_eq!(backoff.delay_for(3), Duration::from_millis(400));
163 assert_eq!(backoff.delay_for(4), Duration::from_millis(800));
164 assert_eq!(backoff.delay_for(10), Duration::from_millis(800));
166 }
167
168 #[test]
169 fn shrinking_factor_floors_above_zero() {
170 let backoff = Backoff {
173 base: Duration::from_millis(2),
174 factor_milli: 100, max: Duration::from_secs(30),
176 };
177 assert!(
179 backoff.delay_for(5) >= Duration::from_millis(1),
180 "backoff must never floor to zero: got {:?}",
181 backoff.delay_for(5)
182 );
183 }
184
185 #[test]
186 fn sub_millisecond_max_still_floors_above_zero() {
187 let backoff = Backoff {
190 base: Duration::from_micros(100),
191 factor_milli: 2000,
192 max: Duration::from_micros(500), };
194 assert!(
195 backoff.delay_for(1) >= Duration::from_millis(1),
196 "sub-ms max must still floor to >=1ms, got {:?}",
197 backoff.delay_for(1)
198 );
199 assert!(backoff.delay_for(5) >= Duration::from_millis(1));
200 }
201
202 #[test]
203 fn first_failure_uses_base_not_zero() {
204 let policy = RestartPolicy::default();
205 match policy.decide(1) {
206 RestartDecision::Restart { after } => assert!(after >= Duration::from_millis(1)),
207 other => panic!("expected restart, got {other:?}"),
208 }
209 }
210}