par_term_scripting/
restart.rs1use std::time::{Duration, Instant};
18
19use par_term_config::RestartPolicy;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RestartAction {
24 Idle,
26 Stop,
28 Restart,
30 Wait,
32}
33
34pub const MAX_RESTART_ATTEMPTS: u32 = 5;
40
41pub const RESTART_GRACE: Duration = Duration::from_secs(5);
43
44#[derive(Debug, Clone)]
46pub struct ScriptRestartState {
47 policy: RestartPolicy,
48 delay: Duration,
49 started_at: Option<Instant>,
51 deadline: Option<Instant>,
53 consecutive_failures: u32,
55}
56
57impl ScriptRestartState {
58 pub fn new(policy: RestartPolicy, delay_ms: u64) -> Self {
60 Self {
61 policy,
62 delay: Duration::from_millis(delay_ms),
63 started_at: None,
64 deadline: None,
65 consecutive_failures: 0,
66 }
67 }
68
69 pub fn reconfigure(&mut self, policy: RestartPolicy, delay_ms: u64) {
71 self.policy = policy;
72 self.delay = Duration::from_millis(delay_ms);
73 }
74
75 pub fn on_started(&mut self, now: Instant) {
77 self.started_at = Some(now);
78 self.deadline = None;
79 }
80
81 pub fn on_exit(&mut self, now: Instant, success: bool) -> RestartAction {
84 if let Some(start) = self.started_at
87 && now.duration_since(start) >= RESTART_GRACE
88 {
89 self.consecutive_failures = 0;
90 }
91 let should_restart = match self.policy {
92 RestartPolicy::Never => false,
93 RestartPolicy::Always => true,
94 RestartPolicy::OnFailure => !success,
95 };
96 if !should_restart {
97 self.deadline = None;
98 return RestartAction::Stop;
99 }
100 self.schedule(now)
101 }
102
103 pub fn poll(&mut self, now: Instant) -> RestartAction {
106 match self.deadline {
107 Some(deadline) if now >= deadline => {
108 self.deadline = None;
109 RestartAction::Restart
110 }
111 Some(_) => RestartAction::Wait,
112 None => RestartAction::Idle,
113 }
114 }
115
116 pub fn pending(&self) -> bool {
118 self.deadline.is_some()
119 }
120
121 pub fn reschedule(&mut self, now: Instant) -> RestartAction {
124 self.schedule(now)
125 }
126
127 pub fn consecutive_failures(&self) -> u32 {
129 self.consecutive_failures
130 }
131
132 pub fn policy(&self) -> RestartPolicy {
134 self.policy
135 }
136
137 fn schedule(&mut self, now: Instant) -> RestartAction {
138 if self.consecutive_failures >= MAX_RESTART_ATTEMPTS {
139 self.deadline = None;
140 return RestartAction::Stop;
141 }
142 self.consecutive_failures = self.consecutive_failures.saturating_add(1);
143 self.deadline = Some(now + self.delay);
144 RestartAction::Wait
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 fn anchor() -> Instant {
153 Instant::now()
154 }
155
156 #[test]
157 fn never_policy_stops_on_clean_exit() {
158 let t0 = anchor();
159 let mut s = ScriptRestartState::new(RestartPolicy::Never, 0);
160 s.on_started(t0);
161 assert_eq!(s.on_exit(t0, true), RestartAction::Stop);
162 assert!(!s.pending());
163 }
164
165 #[test]
166 fn never_policy_stops_on_failure_too() {
167 let t0 = anchor();
168 let mut s = ScriptRestartState::new(RestartPolicy::Never, 0);
169 s.on_started(t0);
170 assert_eq!(s.on_exit(t0, false), RestartAction::Stop);
171 }
172
173 #[test]
174 fn always_policy_waits_for_delay_then_restarts() {
175 let t0 = anchor();
176 let mut s = ScriptRestartState::new(RestartPolicy::Always, 100);
177 s.on_started(t0);
178 assert_eq!(s.on_exit(t0, true), RestartAction::Wait);
179 assert!(s.pending());
180 assert_eq!(s.poll(t0 + Duration::from_millis(99)), RestartAction::Wait);
181 assert_eq!(
182 s.poll(t0 + Duration::from_millis(100)),
183 RestartAction::Restart
184 );
185 assert!(!s.pending());
186 }
187
188 #[test]
189 fn always_policy_restarts_immediately_when_delay_zero() {
190 let t0 = anchor();
191 let mut s = ScriptRestartState::new(RestartPolicy::Always, 0);
192 s.on_started(t0);
193 assert_eq!(s.on_exit(t0, true), RestartAction::Wait);
194 assert_eq!(s.poll(t0), RestartAction::Restart);
195 }
196
197 #[test]
198 fn on_failure_stops_on_clean_exit() {
199 let t0 = anchor();
200 let mut s = ScriptRestartState::new(RestartPolicy::OnFailure, 100);
201 s.on_started(t0);
202 assert_eq!(s.on_exit(t0, true), RestartAction::Stop);
203 assert!(!s.pending());
204 }
205
206 #[test]
207 fn on_failure_restarts_on_failure_after_delay() {
208 let t0 = anchor();
209 let mut s = ScriptRestartState::new(RestartPolicy::OnFailure, 50);
210 s.on_started(t0);
211 assert_eq!(s.on_exit(t0, false), RestartAction::Wait);
212 assert_eq!(s.poll(t0 + Duration::from_millis(49)), RestartAction::Wait);
213 assert_eq!(
214 s.poll(t0 + Duration::from_millis(50)),
215 RestartAction::Restart
216 );
217 }
218
219 #[test]
220 fn idle_when_nothing_has_happened() {
221 let mut s = ScriptRestartState::new(RestartPolicy::Always, 100);
222 s.on_started(anchor());
223 assert_eq!(s.poll(anchor()), RestartAction::Idle);
224 assert!(!s.pending());
225 }
226
227 #[test]
228 fn gives_up_after_max_attempts_within_grace_window() {
229 let t0 = anchor();
230 let mut s = ScriptRestartState::new(RestartPolicy::Always, 10);
231 for _ in 0..MAX_RESTART_ATTEMPTS {
232 s.on_started(t0);
233 assert_eq!(s.on_exit(t0, false), RestartAction::Wait);
234 assert_eq!(
235 s.poll(t0 + Duration::from_millis(10)),
236 RestartAction::Restart
237 );
238 }
239 s.on_started(t0);
240 assert_eq!(s.on_exit(t0, false), RestartAction::Stop);
241 }
242
243 #[test]
244 fn long_lived_process_resets_failure_counter() {
245 let t0 = anchor();
246 let mut s = ScriptRestartState::new(RestartPolicy::Always, 10);
247 s.on_started(t0);
248 s.on_exit(t0, false);
249 assert_eq!(s.consecutive_failures(), 1);
250 s.poll(t0 + Duration::from_millis(10));
251 let restart_at = t0 + Duration::from_millis(20);
252 s.on_started(restart_at);
253 let after_grace = restart_at + RESTART_GRACE + Duration::from_millis(1);
254 s.on_exit(after_grace, false);
255 assert_eq!(s.consecutive_failures(), 1);
256 }
257
258 #[test]
259 fn reschedule_after_spawn_failure_counts_toward_cap() {
260 let t0 = anchor();
261 let mut s = ScriptRestartState::new(RestartPolicy::Always, 10);
262 s.on_started(t0);
263 s.on_exit(t0, false); s.poll(t0 + Duration::from_millis(10)); assert_eq!(
266 s.reschedule(t0 + Duration::from_millis(11)),
267 RestartAction::Wait
268 );
269 assert_eq!(s.consecutive_failures(), 2);
270 }
271
272 #[test]
273 fn reconfigure_swaps_policy_and_delay() {
274 let t0 = anchor();
275 let mut s = ScriptRestartState::new(RestartPolicy::Never, 0);
276 s.reconfigure(RestartPolicy::Always, 200);
277 s.on_started(t0);
278 assert_eq!(s.on_exit(t0, true), RestartAction::Wait);
279 assert_eq!(
280 s.poll(t0 + Duration::from_millis(200)),
281 RestartAction::Restart
282 );
283 }
284}