use std::time::Duration;
#[derive(Debug, Clone)]
pub struct DeadMansSwitch {
timeout_ms: i64,
deadline_ms: Option<i64>,
}
impl DeadMansSwitch {
#[must_use]
pub fn new(timeout: Duration) -> Self {
Self {
timeout_ms: timeout.as_millis() as i64,
deadline_ms: None,
}
}
pub fn heartbeat(&mut self, now_ms: i64) {
self.deadline_ms = Some(now_ms + self.timeout_ms);
}
#[must_use]
pub fn is_expired(&self, now_ms: i64) -> bool {
self.deadline_ms.is_some_and(|deadline| now_ms >= deadline)
}
#[must_use]
pub fn is_armed(&self) -> bool {
self.deadline_ms.is_some()
}
#[must_use]
pub fn deadline_ms(&self) -> Option<i64> {
self.deadline_ms
}
pub fn disarm(&mut self) {
self.deadline_ms = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starts_disarmed_and_never_expires() {
let switch = DeadMansSwitch::new(Duration::from_secs(5));
assert!(!switch.is_armed());
assert!(!switch.is_expired(1_000_000));
assert_eq!(switch.deadline_ms(), None);
}
#[test]
fn heartbeat_arms_and_sets_the_deadline() {
let mut switch = DeadMansSwitch::new(Duration::from_secs(5));
switch.heartbeat(1_000);
assert!(switch.is_armed());
assert_eq!(switch.deadline_ms(), Some(6_000));
assert!(!switch.is_expired(5_999));
assert!(switch.is_expired(6_000));
assert!(switch.is_expired(10_000));
}
#[test]
fn a_fresh_heartbeat_extends_the_deadline() {
let mut switch = DeadMansSwitch::new(Duration::from_secs(5));
switch.heartbeat(1_000);
assert!(switch.is_expired(6_000));
switch.heartbeat(5_000); assert_eq!(switch.deadline_ms(), Some(10_000));
assert!(!switch.is_expired(6_000));
assert!(switch.is_expired(10_000));
}
#[test]
fn disarm_clears_the_deadline() {
let mut switch = DeadMansSwitch::new(Duration::from_millis(100));
switch.heartbeat(0);
assert!(switch.is_expired(200));
switch.disarm();
assert!(!switch.is_armed());
assert!(!switch.is_expired(1_000_000));
}
}