use std::io;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub struct Ack {
epoch: Instant,
done_ms: Arc<AtomicU64>,
}
impl Ack {
pub fn done(&self) {
self.done_ms.store(
self.epoch.elapsed().as_millis() as u64 + 1,
Ordering::Release,
);
}
}
#[derive(Clone, Copy, Debug)]
pub struct StallConfig {
pub poll: Duration,
pub threshold: Duration,
pub observer_gap: Duration,
}
impl Default for StallConfig {
fn default() -> Self {
Self {
poll: Duration::from_millis(50),
threshold: Duration::from_millis(200),
observer_gap: Duration::from_secs(1),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StallEvent {
Stalled {
episode: u64,
gap: Duration,
recovered: bool,
},
Recovered { episode: u64, gap: Duration },
ObserverGap { gap: Duration },
Stopped { reason: String },
}
#[derive(Debug, Default)]
struct Episode {
reported: bool,
}
impl Episode {
fn detect(&mut self, gap: Duration, threshold: Duration) -> bool {
if gap < threshold || self.reported {
return false;
}
self.reported = true;
true
}
}
#[derive(Debug, Clone, Copy)]
pub struct Cooldown {
every: Duration,
last: Option<Duration>,
}
impl Cooldown {
pub fn new(every: Duration) -> Self {
Self { every, last: None }
}
pub fn reserve(&mut self, now: Duration) -> bool {
if self
.last
.is_some_and(|l| now.saturating_sub(l) < self.every)
{
return false;
}
self.last = Some(now);
true
}
}
pub struct StallWatchdog {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl StallWatchdog {
pub fn spawn(
name: &str,
config: StallConfig,
ping: impl Fn(Ack) -> Result<(), String> + Send + 'static,
on_event: impl Fn(StallEvent) + Send + 'static,
) -> io::Result<Self> {
let stop = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&stop);
let handle = thread::Builder::new()
.name(name.to_string())
.spawn(move || run(config, flag, ping, on_event))?;
Ok(Self {
stop,
handle: Some(handle),
})
}
pub fn stop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
impl Drop for StallWatchdog {
fn drop(&mut self) {
self.stop();
}
}
fn run(
cfg: StallConfig,
stop: Arc<AtomicBool>,
ping: impl Fn(Ack) -> Result<(), String>,
on_event: impl Fn(StallEvent),
) {
let epoch = Instant::now();
let ms = |at: Instant| at.saturating_duration_since(epoch).as_millis() as u64 + 1;
let mut done = Arc::new(AtomicU64::new(0));
let mut pending: Option<u64> = None;
let mut episode = Episode::default();
let mut episode_no = 0u64;
let mut previous_poll = ms(Instant::now());
while !stop.load(Ordering::SeqCst) {
thread::sleep(cfg.poll);
if stop.load(Ordering::SeqCst) {
return;
}
let now = ms(Instant::now());
let observer = Duration::from_millis(now.saturating_sub(previous_poll));
previous_poll = now;
if observer > cfg.observer_gap {
on_event(StallEvent::ObserverGap { gap: observer });
if pending.is_some() {
pending = Some(now);
}
episode.reported = false;
}
if let Some(sent) = pending {
let acked = done.load(Ordering::Acquire);
let gap = Duration::from_millis(if acked != 0 {
acked.saturating_sub(sent)
} else {
now.saturating_sub(sent)
});
if episode.detect(gap, cfg.threshold) {
episode_no += 1;
on_event(StallEvent::Stalled {
episode: episode_no,
gap,
recovered: acked != 0,
});
}
if acked == 0 {
continue;
}
if episode.reported {
on_event(StallEvent::Recovered {
episode: episode_no,
gap,
});
}
episode.reported = false;
pending = None;
}
if pending.is_none() {
done = Arc::new(AtomicU64::new(0));
let ack = Ack {
epoch,
done_ms: Arc::clone(&done),
};
match ping(ack) {
Ok(()) => pending = Some(ms(Instant::now())),
Err(reason) => {
on_event(StallEvent::Stopped { reason });
return;
}
}
}
}
}