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 fn schedule(&mut self, now: Instant) -> RestartAction {
133 if self.consecutive_failures >= MAX_RESTART_ATTEMPTS {
134 self.deadline = None;
135 return RestartAction::Stop;
136 }
137 self.consecutive_failures = self.consecutive_failures.saturating_add(1);
138 self.deadline = Some(now + self.delay);
139 RestartAction::Wait
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 fn anchor() -> Instant {
148 Instant::now()
149 }
150
151 #[test]
152 fn never_policy_stops_on_clean_exit() {
153 let t0 = anchor();
154 let mut s = ScriptRestartState::new(RestartPolicy::Never, 0);
155 s.on_started(t0);
156 assert_eq!(s.on_exit(t0, true), RestartAction::Stop);
157 assert!(!s.pending());
158 }
159
160 #[test]
161 fn never_policy_stops_on_failure_too() {
162 let t0 = anchor();
163 let mut s = ScriptRestartState::new(RestartPolicy::Never, 0);
164 s.on_started(t0);
165 assert_eq!(s.on_exit(t0, false), RestartAction::Stop);
166 }
167
168 #[test]
169 fn always_policy_waits_for_delay_then_restarts() {
170 let t0 = anchor();
171 let mut s = ScriptRestartState::new(RestartPolicy::Always, 100);
172 s.on_started(t0);
173 assert_eq!(s.on_exit(t0, true), RestartAction::Wait);
174 assert!(s.pending());
175 assert_eq!(s.poll(t0 + Duration::from_millis(99)), RestartAction::Wait);
176 assert_eq!(
177 s.poll(t0 + Duration::from_millis(100)),
178 RestartAction::Restart
179 );
180 assert!(!s.pending());
181 }
182
183 #[test]
184 fn always_policy_restarts_immediately_when_delay_zero() {
185 let t0 = anchor();
186 let mut s = ScriptRestartState::new(RestartPolicy::Always, 0);
187 s.on_started(t0);
188 assert_eq!(s.on_exit(t0, true), RestartAction::Wait);
189 assert_eq!(s.poll(t0), RestartAction::Restart);
190 }
191
192 #[test]
193 fn on_failure_stops_on_clean_exit() {
194 let t0 = anchor();
195 let mut s = ScriptRestartState::new(RestartPolicy::OnFailure, 100);
196 s.on_started(t0);
197 assert_eq!(s.on_exit(t0, true), RestartAction::Stop);
198 assert!(!s.pending());
199 }
200
201 #[test]
202 fn on_failure_restarts_on_failure_after_delay() {
203 let t0 = anchor();
204 let mut s = ScriptRestartState::new(RestartPolicy::OnFailure, 50);
205 s.on_started(t0);
206 assert_eq!(s.on_exit(t0, false), RestartAction::Wait);
207 assert_eq!(s.poll(t0 + Duration::from_millis(49)), RestartAction::Wait);
208 assert_eq!(
209 s.poll(t0 + Duration::from_millis(50)),
210 RestartAction::Restart
211 );
212 }
213
214 #[test]
215 fn idle_when_nothing_has_happened() {
216 let mut s = ScriptRestartState::new(RestartPolicy::Always, 100);
217 s.on_started(anchor());
218 assert_eq!(s.poll(anchor()), RestartAction::Idle);
219 assert!(!s.pending());
220 }
221
222 #[test]
223 fn gives_up_after_max_attempts_within_grace_window() {
224 let t0 = anchor();
225 let mut s = ScriptRestartState::new(RestartPolicy::Always, 10);
226 for _ in 0..MAX_RESTART_ATTEMPTS {
227 s.on_started(t0);
228 assert_eq!(s.on_exit(t0, false), RestartAction::Wait);
229 assert_eq!(
230 s.poll(t0 + Duration::from_millis(10)),
231 RestartAction::Restart
232 );
233 }
234 s.on_started(t0);
235 assert_eq!(s.on_exit(t0, false), RestartAction::Stop);
236 }
237
238 #[test]
239 fn long_lived_process_resets_failure_counter() {
240 let t0 = anchor();
241 let mut s = ScriptRestartState::new(RestartPolicy::Always, 10);
242 s.on_started(t0);
243 s.on_exit(t0, false);
244 assert_eq!(s.consecutive_failures(), 1);
245 s.poll(t0 + Duration::from_millis(10));
246 let restart_at = t0 + Duration::from_millis(20);
247 s.on_started(restart_at);
248 let after_grace = restart_at + RESTART_GRACE + Duration::from_millis(1);
249 s.on_exit(after_grace, false);
250 assert_eq!(s.consecutive_failures(), 1);
251 }
252
253 #[test]
254 fn reschedule_after_spawn_failure_counts_toward_cap() {
255 let t0 = anchor();
256 let mut s = ScriptRestartState::new(RestartPolicy::Always, 10);
257 s.on_started(t0);
258 s.on_exit(t0, false); s.poll(t0 + Duration::from_millis(10)); assert_eq!(
261 s.reschedule(t0 + Duration::from_millis(11)),
262 RestartAction::Wait
263 );
264 assert_eq!(s.consecutive_failures(), 2);
265 }
266
267 #[test]
268 fn reconfigure_swaps_policy_and_delay() {
269 let t0 = anchor();
270 let mut s = ScriptRestartState::new(RestartPolicy::Never, 0);
271 s.reconfigure(RestartPolicy::Always, 200);
272 s.on_started(t0);
273 assert_eq!(s.on_exit(t0, true), RestartAction::Wait);
274 assert_eq!(
275 s.poll(t0 + Duration::from_millis(200)),
276 RestartAction::Restart
277 );
278 }
279}