use std::time::{Duration, Instant};
const DEFAULT_PERIOD: Duration = Duration::from_secs(7);
#[derive(Debug, Clone)]
pub struct Heartbeat {
pub ok: bool,
last_beat_count: i64,
last_change: Option<Instant>,
first_scan: bool,
}
impl Heartbeat {
pub fn new() -> Self {
Self {
ok: false,
last_beat_count: 0,
last_change: None,
first_scan: true,
}
}
pub fn call(&mut self, beat_count: i64, period: Duration) {
if self.first_scan {
self.ok = false;
self.last_beat_count = beat_count;
self.first_scan = false;
return;
}
if beat_count != self.last_beat_count {
self.last_beat_count = beat_count;
self.last_change = Some(Instant::now());
self.ok = true;
} else if let Some(last) = self.last_change {
if last.elapsed() >= period {
self.ok = false;
}
}
}
pub fn with_defaults() -> Self {
Self::new()
}
pub const DEFAULT_PERIOD: Duration = DEFAULT_PERIOD;
}
impl Default for Heartbeat {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_first_scan_not_ok() {
let mut hb = Heartbeat::new();
hb.call(0, DEFAULT_PERIOD);
assert_eq!(hb.ok, false);
}
#[test]
fn test_change_sets_ok() {
let mut hb = Heartbeat::new();
hb.call(0, DEFAULT_PERIOD);
assert_eq!(hb.ok, false);
hb.call(1, DEFAULT_PERIOD);
assert_eq!(hb.ok, true);
}
#[test]
fn test_unchanged_within_period_stays_ok() {
let mut hb = Heartbeat::new();
let period = Duration::from_millis(100);
hb.call(0, period);
hb.call(1, period); assert!(hb.ok);
std::thread::sleep(Duration::from_millis(30));
hb.call(1, period);
assert!(hb.ok);
}
#[test]
fn test_timeout_clears_ok() {
let mut hb = Heartbeat::new();
let period = Duration::from_millis(50);
hb.call(0, period);
hb.call(1, period);
assert!(hb.ok);
std::thread::sleep(Duration::from_millis(60));
hb.call(1, period);
assert_eq!(hb.ok, false);
}
#[test]
fn test_recovery_after_timeout() {
let mut hb = Heartbeat::new();
let period = Duration::from_millis(50);
hb.call(0, period);
hb.call(1, period);
assert!(hb.ok);
std::thread::sleep(Duration::from_millis(60));
hb.call(1, period);
assert_eq!(hb.ok, false);
hb.call(2, period);
assert!(hb.ok);
}
#[test]
fn test_no_change_ever_stays_not_ok() {
let mut hb = Heartbeat::new();
let period = Duration::from_millis(50);
hb.call(42, period);
assert_eq!(hb.ok, false);
hb.call(42, period);
assert_eq!(hb.ok, false);
std::thread::sleep(Duration::from_millis(60));
hb.call(42, period);
assert_eq!(hb.ok, false);
}
#[test]
fn test_negative_values() {
let mut hb = Heartbeat::new();
let period = Duration::from_millis(100);
hb.call(-5, period);
assert_eq!(hb.ok, false);
hb.call(-4, period);
assert!(hb.ok);
hb.call(-3, period);
assert!(hb.ok);
}
#[test]
fn test_large_jump() {
let mut hb = Heartbeat::new();
let period = Duration::from_millis(100);
hb.call(0, period);
hb.call(1_000_000, period);
assert!(hb.ok);
}
#[test]
fn test_default_trait() {
let hb = Heartbeat::default();
assert_eq!(hb.ok, false);
}
#[test]
fn test_default_period_constant() {
assert_eq!(Heartbeat::DEFAULT_PERIOD, Duration::from_secs(7));
}
}