use std::time::{Duration, Instant};
use par_term_config::RestartPolicy;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestartAction {
Idle,
Stop,
Restart,
Wait,
}
pub const MAX_RESTART_ATTEMPTS: u32 = 5;
pub const RESTART_GRACE: Duration = Duration::from_secs(5);
#[derive(Debug, Clone)]
pub struct ScriptRestartState {
policy: RestartPolicy,
delay: Duration,
started_at: Option<Instant>,
deadline: Option<Instant>,
consecutive_failures: u32,
}
impl ScriptRestartState {
pub fn new(policy: RestartPolicy, delay_ms: u64) -> Self {
Self {
policy,
delay: Duration::from_millis(delay_ms),
started_at: None,
deadline: None,
consecutive_failures: 0,
}
}
pub fn reconfigure(&mut self, policy: RestartPolicy, delay_ms: u64) {
self.policy = policy;
self.delay = Duration::from_millis(delay_ms);
}
pub fn on_started(&mut self, now: Instant) {
self.started_at = Some(now);
self.deadline = None;
}
pub fn on_exit(&mut self, now: Instant, success: bool) -> RestartAction {
if let Some(start) = self.started_at
&& now.duration_since(start) >= RESTART_GRACE
{
self.consecutive_failures = 0;
}
let should_restart = match self.policy {
RestartPolicy::Never => false,
RestartPolicy::Always => true,
RestartPolicy::OnFailure => !success,
};
if !should_restart {
self.deadline = None;
return RestartAction::Stop;
}
self.schedule(now)
}
pub fn poll(&mut self, now: Instant) -> RestartAction {
match self.deadline {
Some(deadline) if now >= deadline => {
self.deadline = None;
RestartAction::Restart
}
Some(_) => RestartAction::Wait,
None => RestartAction::Idle,
}
}
pub fn pending(&self) -> bool {
self.deadline.is_some()
}
pub fn reschedule(&mut self, now: Instant) -> RestartAction {
self.schedule(now)
}
pub fn consecutive_failures(&self) -> u32 {
self.consecutive_failures
}
fn schedule(&mut self, now: Instant) -> RestartAction {
if self.consecutive_failures >= MAX_RESTART_ATTEMPTS {
self.deadline = None;
return RestartAction::Stop;
}
self.consecutive_failures = self.consecutive_failures.saturating_add(1);
self.deadline = Some(now + self.delay);
RestartAction::Wait
}
}
#[cfg(test)]
mod tests {
use super::*;
fn anchor() -> Instant {
Instant::now()
}
#[test]
fn never_policy_stops_on_clean_exit() {
let t0 = anchor();
let mut s = ScriptRestartState::new(RestartPolicy::Never, 0);
s.on_started(t0);
assert_eq!(s.on_exit(t0, true), RestartAction::Stop);
assert!(!s.pending());
}
#[test]
fn never_policy_stops_on_failure_too() {
let t0 = anchor();
let mut s = ScriptRestartState::new(RestartPolicy::Never, 0);
s.on_started(t0);
assert_eq!(s.on_exit(t0, false), RestartAction::Stop);
}
#[test]
fn always_policy_waits_for_delay_then_restarts() {
let t0 = anchor();
let mut s = ScriptRestartState::new(RestartPolicy::Always, 100);
s.on_started(t0);
assert_eq!(s.on_exit(t0, true), RestartAction::Wait);
assert!(s.pending());
assert_eq!(s.poll(t0 + Duration::from_millis(99)), RestartAction::Wait);
assert_eq!(
s.poll(t0 + Duration::from_millis(100)),
RestartAction::Restart
);
assert!(!s.pending());
}
#[test]
fn always_policy_restarts_immediately_when_delay_zero() {
let t0 = anchor();
let mut s = ScriptRestartState::new(RestartPolicy::Always, 0);
s.on_started(t0);
assert_eq!(s.on_exit(t0, true), RestartAction::Wait);
assert_eq!(s.poll(t0), RestartAction::Restart);
}
#[test]
fn on_failure_stops_on_clean_exit() {
let t0 = anchor();
let mut s = ScriptRestartState::new(RestartPolicy::OnFailure, 100);
s.on_started(t0);
assert_eq!(s.on_exit(t0, true), RestartAction::Stop);
assert!(!s.pending());
}
#[test]
fn on_failure_restarts_on_failure_after_delay() {
let t0 = anchor();
let mut s = ScriptRestartState::new(RestartPolicy::OnFailure, 50);
s.on_started(t0);
assert_eq!(s.on_exit(t0, false), RestartAction::Wait);
assert_eq!(s.poll(t0 + Duration::from_millis(49)), RestartAction::Wait);
assert_eq!(
s.poll(t0 + Duration::from_millis(50)),
RestartAction::Restart
);
}
#[test]
fn idle_when_nothing_has_happened() {
let mut s = ScriptRestartState::new(RestartPolicy::Always, 100);
s.on_started(anchor());
assert_eq!(s.poll(anchor()), RestartAction::Idle);
assert!(!s.pending());
}
#[test]
fn gives_up_after_max_attempts_within_grace_window() {
let t0 = anchor();
let mut s = ScriptRestartState::new(RestartPolicy::Always, 10);
for _ in 0..MAX_RESTART_ATTEMPTS {
s.on_started(t0);
assert_eq!(s.on_exit(t0, false), RestartAction::Wait);
assert_eq!(
s.poll(t0 + Duration::from_millis(10)),
RestartAction::Restart
);
}
s.on_started(t0);
assert_eq!(s.on_exit(t0, false), RestartAction::Stop);
}
#[test]
fn long_lived_process_resets_failure_counter() {
let t0 = anchor();
let mut s = ScriptRestartState::new(RestartPolicy::Always, 10);
s.on_started(t0);
s.on_exit(t0, false);
assert_eq!(s.consecutive_failures(), 1);
s.poll(t0 + Duration::from_millis(10));
let restart_at = t0 + Duration::from_millis(20);
s.on_started(restart_at);
let after_grace = restart_at + RESTART_GRACE + Duration::from_millis(1);
s.on_exit(after_grace, false);
assert_eq!(s.consecutive_failures(), 1);
}
#[test]
fn reschedule_after_spawn_failure_counts_toward_cap() {
let t0 = anchor();
let mut s = ScriptRestartState::new(RestartPolicy::Always, 10);
s.on_started(t0);
s.on_exit(t0, false); s.poll(t0 + Duration::from_millis(10)); assert_eq!(
s.reschedule(t0 + Duration::from_millis(11)),
RestartAction::Wait
);
assert_eq!(s.consecutive_failures(), 2);
}
#[test]
fn reconfigure_swaps_policy_and_delay() {
let t0 = anchor();
let mut s = ScriptRestartState::new(RestartPolicy::Never, 0);
s.reconfigure(RestartPolicy::Always, 200);
s.on_started(t0);
assert_eq!(s.on_exit(t0, true), RestartAction::Wait);
assert_eq!(
s.poll(t0 + Duration::from_millis(200)),
RestartAction::Restart
);
}
}