use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::{Duration, Instant};
use super::error::DistError;
pub(super) fn run_with_deadline<T: Send + 'static>(
what: &str,
deadline: Duration,
f: impl FnOnce() -> T + Send + 'static,
) -> Result<T, DistError> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
std::thread::Builder::new()
.name(format!("mamba-dist-{what}"))
.spawn(move || {
let _ = tx.send(f());
})
.map_err(|e| DistError::Transport(format!("{what}: helper thread spawn: {e}")))?;
match rx.recv_timeout(deadline) {
Ok(v) => Ok(v),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(DistError::Transport(format!(
"{what} did not complete within {deadline:?} — failing fast \
(the supervisor reaps the world; the blocked helper thread \
is reclaimed by process exit)"
))),
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Err(DistError::Transport(format!(
"{what}: helper thread panicked before returning a result"
))),
}
}
const ARMED: u8 = 0;
const DISARMED: u8 = 1;
const FIRED: u8 = 2;
pub(super) struct Watchdog {
state: Arc<AtomicU8>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl Watchdog {
pub(super) fn arm(
name: &str,
deadline: Duration,
on_deadline: impl FnOnce() + Send + 'static,
) -> Result<Self, DistError> {
let state = Arc::new(AtomicU8::new(ARMED));
let seen = state.clone();
let handle = std::thread::Builder::new()
.name(format!("mamba-watchdog-{name}"))
.spawn(move || {
let end = Instant::now() + deadline;
loop {
if seen.load(Ordering::Acquire) != ARMED {
return;
}
let now = Instant::now();
if now >= end {
break;
}
std::thread::park_timeout(end - now);
}
if seen
.compare_exchange(ARMED, FIRED, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
on_deadline();
}
})
.map_err(|e| DistError::Transport(format!("watchdog {name}: spawn: {e}")))?;
Ok(Self {
state,
handle: Some(handle),
})
}
pub(super) fn disarm(mut self) -> bool {
let won = self
.state
.compare_exchange(ARMED, DISARMED, Ordering::AcqRel, Ordering::Acquire)
.is_ok();
if let Some(h) = self.handle.take() {
h.thread().unpark();
let _ = h.join();
}
!won
}
}
impl Drop for Watchdog {
fn drop(&mut self) {
let _ = self
.state
.compare_exchange(ARMED, DISARMED, Ordering::AcqRel, Ordering::Acquire);
if let Some(h) = self.handle.take() {
h.thread().unpark();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
#[test]
fn deadline_returns_result_when_fast() {
let out = run_with_deadline("fast", Duration::from_secs(5), || 41 + 1).unwrap();
assert_eq!(out, 42);
}
#[test]
fn deadline_errors_when_blocked() {
let err = run_with_deadline("stuck", Duration::from_millis(50), || {
std::thread::sleep(Duration::from_secs(600));
})
.unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("did not complete"), "{msg}");
}
#[test]
fn watchdog_fires_on_deadline_and_reports_through_disarm() {
let fired = Arc::new(AtomicUsize::new(0));
let f2 = fired.clone();
let wd = Watchdog::arm("fire", Duration::from_millis(30), move || {
f2.fetch_add(1, Ordering::SeqCst);
})
.unwrap();
std::thread::sleep(Duration::from_millis(120));
assert_eq!(fired.load(Ordering::SeqCst), 1, "must fire exactly once");
assert!(wd.disarm(), "disarm must report the fire");
assert_eq!(fired.load(Ordering::SeqCst), 1);
}
#[test]
fn watchdog_stays_quiet_when_disarmed_in_time() {
let fired = Arc::new(AtomicUsize::new(0));
let f2 = fired.clone();
let wd = Watchdog::arm("quiet", Duration::from_millis(200), move || {
f2.fetch_add(1, Ordering::SeqCst);
})
.unwrap();
assert!(!wd.disarm(), "in-time disarm must report no fire");
std::thread::sleep(Duration::from_millis(300));
assert_eq!(fired.load(Ordering::SeqCst), 0, "disarmed watchdog fired");
}
#[test]
fn disarm_is_immediate_not_poll_paced() {
let wd = Watchdog::arm("swift", Duration::from_secs(30), || {}).unwrap();
let t0 = Instant::now();
assert!(!wd.disarm());
assert!(
t0.elapsed() < Duration::from_millis(8),
"disarm took {:?}",
t0.elapsed()
);
}
}