1use std::{collections::VecDeque, error::Error, fmt, time::Duration};
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4pub enum RestartMode {
5 Automatic,
6 OnDemand,
7 Disabled,
8}
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub struct RestartPolicy {
12 mode: RestartMode,
13 max_restarts: u32,
14 window: Duration,
15 base_backoff: Duration,
16 max_backoff: Duration,
17 consecutive: Option<ConsecutiveFailures>,
18 reset_budget_on_ready: bool,
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub struct ConsecutiveFailures {
24 pub cap: u32,
26 pub cooldown: Duration,
29}
30
31impl RestartPolicy {
32 pub fn with_consecutive_failures(
37 mut self,
38 cap: u32,
39 cooldown: Duration,
40 ) -> Result<Self, InvalidRestartPolicy> {
41 if cap == 0 {
42 return Err(InvalidRestartPolicy);
43 }
44 self.consecutive = Some(ConsecutiveFailures { cap, cooldown });
45 Ok(self)
46 }
47 pub fn with_reset_budget_on_ready(mut self) -> Self {
51 self.reset_budget_on_ready = true;
52 self
53 }
54 pub fn consecutive_failures(&self) -> Option<ConsecutiveFailures> {
55 self.consecutive
56 }
57 pub fn resets_budget_on_ready(&self) -> bool {
58 self.reset_budget_on_ready
59 }
60 pub fn mode(&self) -> RestartMode {
61 self.mode
62 }
63 pub fn max_restarts(&self) -> u32 {
64 self.max_restarts
65 }
66 pub fn window(&self) -> Duration {
67 self.window
68 }
69 pub fn base_backoff(&self) -> Duration {
70 self.base_backoff
71 }
72 pub fn max_backoff(&self) -> Duration {
73 self.max_backoff
74 }
75}
76
77impl Default for RestartPolicy {
78 fn default() -> Self {
81 Self {
82 mode: RestartMode::Automatic,
83 max_restarts: 3,
84 window: Duration::from_secs(60),
85 base_backoff: Duration::from_millis(250),
86 max_backoff: Duration::from_secs(5),
87 consecutive: None,
88 reset_budget_on_ready: false,
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub struct InvalidRestartPolicy;
95
96impl fmt::Display for InvalidRestartPolicy {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 f.write_str("restart window must be non-zero, max backoff must cover base backoff, and a consecutive-failure cap must be at least 1")
99 }
100}
101
102impl Error for InvalidRestartPolicy {}
103
104impl RestartPolicy {
105 pub fn new(
106 mode: RestartMode,
107 max_restarts: u32,
108 window: Duration,
109 base_backoff: Duration,
110 max_backoff: Duration,
111 ) -> Result<Self, InvalidRestartPolicy> {
112 if window.is_zero() || max_backoff < base_backoff {
113 return Err(InvalidRestartPolicy);
114 }
115 Ok(Self {
116 mode,
117 max_restarts,
118 window,
119 base_backoff,
120 max_backoff,
121 consecutive: None,
122 reset_budget_on_ready: false,
123 })
124 }
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub struct Generation(u64);
129
130#[derive(Clone, Copy, Debug, Eq, PartialEq)]
131pub struct RestartPermit {
132 epoch: u64,
133 delay: Duration,
134 not_before: Duration,
135}
136
137impl RestartPermit {
138 pub fn delay(&self) -> Duration {
139 self.delay
140 }
141}
142
143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
144pub enum RestartOutcome {
145 Restart(RestartPermit),
146 AwaitingDemand,
147 Exhausted,
148 IgnoredStale,
149 Stopped,
150}
151
152#[derive(Clone, Debug)]
153pub struct RestartTracker {
154 policy: RestartPolicy,
155 active: Option<Generation>,
156 next_generation: u64,
157 restart_times: VecDeque<Duration>,
158 consecutive: u32,
159 permit_epoch: u64,
160 awaiting_demand: bool,
161 shutdown: bool,
162}
163
164impl RestartTracker {
165 pub fn new(policy: RestartPolicy) -> Self {
166 Self {
167 policy,
168 active: None,
169 next_generation: 0,
170 restart_times: VecDeque::new(),
171 consecutive: 0,
172 permit_epoch: 0,
173 awaiting_demand: false,
174 shutdown: false,
175 }
176 }
177
178 pub fn started(&mut self, _at: Duration) -> Generation {
179 self.next_generation = self.next_generation.saturating_add(1);
180 self.permit_epoch = self.permit_epoch.saturating_add(1);
181 self.awaiting_demand = false;
182 let generation = Generation(self.next_generation);
183 self.active = Some(generation);
184 generation
185 }
186
187 pub fn exited(&mut self, generation: Generation, at: Duration) -> RestartOutcome {
188 if self.shutdown {
189 return RestartOutcome::Stopped;
190 }
191 if self.active != Some(generation) {
192 return RestartOutcome::IgnoredStale;
193 }
194 self.active = None;
195 match self.policy.mode {
196 RestartMode::Automatic => self.schedule(at),
197 RestartMode::OnDemand => {
198 self.awaiting_demand = true;
199 RestartOutcome::AwaitingDemand
200 }
201 RestartMode::Disabled => RestartOutcome::Stopped,
202 }
203 }
204
205 pub fn request_restart(&mut self, at: Duration) -> RestartOutcome {
206 if self.shutdown || self.policy.mode == RestartMode::Disabled {
207 return RestartOutcome::Stopped;
208 }
209 if self.policy.mode == RestartMode::OnDemand && !self.awaiting_demand {
210 return RestartOutcome::AwaitingDemand;
211 }
212 self.awaiting_demand = false;
213 self.schedule(at)
214 }
215
216 pub fn ready(&mut self, generation: Generation) {
220 if self.active != Some(generation) {
221 return;
222 }
223 self.consecutive = 0;
224 if self.policy.reset_budget_on_ready {
225 self.restart_times.clear();
226 }
227 }
228
229 pub fn consecutive_failures(&self) -> u32 {
231 self.consecutive
232 }
233
234 pub fn restarts_in_window(&self, at: Duration) -> u32 {
236 let window_start = at.saturating_sub(self.policy.window);
237 self.restart_times
238 .iter()
239 .filter(|t| **t >= window_start)
240 .count() as u32
241 }
242
243 pub fn reset_budget(&mut self) {
245 self.restart_times.clear();
246 self.consecutive = 0;
247 self.awaiting_demand = false;
248 self.shutdown = false;
249 }
250
251 pub fn permit_valid(&self, permit: &RestartPermit, at: Duration) -> bool {
252 !self.shutdown && permit.epoch == self.permit_epoch && at >= permit.not_before
253 }
254
255 pub fn shutdown(&mut self) {
256 self.shutdown = true;
257 self.active = None;
258 self.awaiting_demand = false;
259 self.permit_epoch = self.permit_epoch.saturating_add(1);
260 }
261
262 fn schedule(&mut self, at: Duration) -> RestartOutcome {
263 let window_start = at.saturating_sub(self.policy.window);
264 while self
265 .restart_times
266 .front()
267 .is_some_and(|restart| *restart < window_start)
268 {
269 self.restart_times.pop_front();
270 }
271 if self.restart_times.len() >= self.policy.max_restarts as usize {
272 return RestartOutcome::Exhausted;
273 }
274
275 let capped = match self.policy.consecutive {
276 Some(c) if self.consecutive >= c.cap => {
277 if c.cooldown.is_zero() {
278 return RestartOutcome::Exhausted;
279 }
280 Some(c.cooldown)
281 }
282 _ => None,
283 };
284 let delay = match capped {
285 Some(cooldown) => {
286 self.consecutive = 0;
288 cooldown
289 }
290 None => {
291 let exponent = u32::try_from(self.restart_times.len()).unwrap_or(u32::MAX);
292 let multiplier = 1_u32.checked_shl(exponent.min(31)).unwrap_or(u32::MAX);
293 self.policy
294 .base_backoff
295 .checked_mul(multiplier)
296 .unwrap_or(self.policy.max_backoff)
297 .min(self.policy.max_backoff)
298 }
299 };
300 self.consecutive = self.consecutive.saturating_add(1);
301 self.restart_times.push_back(at);
302 self.permit_epoch = self.permit_epoch.saturating_add(1);
303 let permit = RestartPermit {
304 epoch: self.permit_epoch,
305 delay,
306 not_before: at.checked_add(delay).unwrap_or(Duration::MAX),
307 };
308 RestartOutcome::Restart(permit)
309 }
310}