use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct LivenessPing {
pub liveness_ping: u64,
pub silence_window_ms: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct LivenessPong {
pub liveness_pong: u64,
}
#[derive(Debug)]
pub(crate) struct SilenceMonitor {
epoch: Instant,
last_inbound_ms: AtomicU64,
window_ms: AtomicU64,
}
impl SilenceMonitor {
pub(crate) fn new() -> Self {
Self {
epoch: Instant::now(),
last_inbound_ms: AtomicU64::new(0),
window_ms: AtomicU64::new(0),
}
}
fn now_ms(&self) -> u64 {
u64::try_from(self.epoch.elapsed().as_millis()).unwrap_or(u64::MAX)
}
pub(crate) fn record_inbound(&self) {
self.last_inbound_ms.store(self.now_ms(), Ordering::Relaxed);
}
pub(crate) fn arm(&self, window: Duration) {
let window_ms = u64::try_from(window.as_millis()).unwrap_or(u64::MAX);
if window_ms > 0 {
self.window_ms.store(window_ms, Ordering::Relaxed);
}
}
pub(crate) fn window(&self) -> Option<Duration> {
match self.window_ms.load(Ordering::Relaxed) {
0 => None,
millis => Some(Duration::from_millis(millis)),
}
}
pub(crate) fn silent_for(&self) -> Option<Duration> {
self.silent_for_at(self.now_ms())
}
fn silent_for_at(&self, now_ms: u64) -> Option<Duration> {
let window_ms = self.window_ms.load(Ordering::Relaxed);
if window_ms == 0 {
return None;
}
let silent_ms = now_ms.saturating_sub(self.last_inbound_ms.load(Ordering::Relaxed));
(silent_ms > window_ms).then(|| Duration::from_millis(silent_ms))
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{LivenessPing, LivenessPong, SilenceMonitor};
#[test]
fn an_unarmed_monitor_never_declares_death() {
let monitor = SilenceMonitor::new();
assert_eq!(monitor.window(), None);
assert_eq!(monitor.silent_for_at(u64::MAX), None);
}
#[test]
fn an_armed_monitor_declares_death_only_past_the_window() {
let monitor = SilenceMonitor::new();
monitor.arm(Duration::from_secs(1));
assert_eq!(monitor.window(), Some(Duration::from_secs(1)));
assert_eq!(
monitor.silent_for_at(1_000),
None,
"silence exactly at the window is still inside it"
);
assert_eq!(
monitor.silent_for_at(1_001),
Some(Duration::from_millis(1_001)),
"silence past the window is a declared death carrying its duration"
);
}
#[test]
fn an_inbound_frame_resets_the_silence_clock() {
let monitor = SilenceMonitor::new();
monitor.arm(Duration::from_millis(50));
monitor.record_inbound();
assert_eq!(
monitor.silent_for(),
None,
"a frame that just arrived leaves no silence to measure"
);
}
#[test]
fn a_zero_window_cannot_disarm_the_switch() {
let monitor = SilenceMonitor::new();
monitor.arm(Duration::from_millis(10));
monitor.arm(Duration::ZERO);
assert_eq!(monitor.window(), Some(Duration::from_millis(10)));
}
#[test]
fn the_liveness_pair_round_trips_through_json() -> Result<(), serde_json::Error> {
let ping = LivenessPing {
liveness_ping: 7,
silence_window_ms: 30_000,
};
let encoded = serde_json::to_string(&ping)?;
assert_eq!(
encoded, r#"{"liveness_ping":7,"silence_window_ms":30000}"#,
"the ping's wire shape is the cross-crate contract"
);
assert_eq!(serde_json::from_str::<LivenessPing>(&encoded)?, ping);
let answer = LivenessPong { liveness_pong: 7 };
let encoded = serde_json::to_string(&answer)?;
assert_eq!(encoded, r#"{"liveness_pong":7}"#);
assert_eq!(serde_json::from_str::<LivenessPong>(&encoded)?, answer);
Ok(())
}
}