use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::watch;
pub struct IdleMonitor {
last_activity_ms: Arc<AtomicU64>,
timeout: Duration,
shutdown_tx: watch::Sender<bool>,
}
impl IdleMonitor {
pub fn new(timeout_secs: u64, shutdown_tx: watch::Sender<bool>) -> Self {
let now_ms = now_millis();
Self {
last_activity_ms: Arc::new(AtomicU64::new(now_ms)),
timeout: Duration::from_secs(timeout_secs),
shutdown_tx,
}
}
pub fn touch(&self) {
self.last_activity_ms.store(now_millis(), Ordering::Relaxed);
}
pub fn handle(&self) -> IdleHandle {
IdleHandle {
last_activity_ms: Arc::clone(&self.last_activity_ms),
}
}
pub async fn run(&self) {
let check_interval = Duration::from_secs(30);
let mut interval = tokio::time::interval(check_interval);
loop {
interval.tick().await;
let last_ms = self.last_activity_ms.load(Ordering::Relaxed);
let now_ms = now_millis();
let idle_duration = Duration::from_millis(now_ms.saturating_sub(last_ms));
if idle_duration >= self.timeout {
tracing::info!(
"idle timeout reached ({:.0}s idle, {:.0}s limit) — shutting down",
idle_duration.as_secs_f64(),
self.timeout.as_secs_f64()
);
let _ = self.shutdown_tx.send(true);
break;
}
}
}
}
#[derive(Clone)]
pub struct IdleHandle {
last_activity_ms: Arc<AtomicU64>,
}
impl IdleHandle {
pub fn touch(&self) {
self.last_activity_ms.store(now_millis(), Ordering::Relaxed);
}
}
fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn now_millis_is_reasonable() {
let ms = now_millis();
assert!(ms > 1_704_067_200_000);
}
#[test]
fn idle_handle_touch_updates_timestamp() {
let (tx, _rx) = watch::channel(false);
let monitor = IdleMonitor::new(60, tx);
let handle = monitor.handle();
monitor.last_activity_ms.store(1_000_000, Ordering::Relaxed);
handle.touch();
let updated = monitor.last_activity_ms.load(Ordering::Relaxed);
assert!(updated > 1_000_000);
}
#[test]
fn monitor_touch_updates_timestamp() {
let (tx, _rx) = watch::channel(false);
let monitor = IdleMonitor::new(60, tx);
monitor.last_activity_ms.store(1_000_000, Ordering::Relaxed);
monitor.touch();
let updated = monitor.last_activity_ms.load(Ordering::Relaxed);
assert!(updated > 1_000_000);
}
#[tokio::test]
async fn idle_monitor_triggers_shutdown() {
let (tx, mut rx) = watch::channel(false);
let monitor = IdleMonitor::new(0, tx);
monitor.last_activity_ms.store(0, Ordering::Relaxed);
let monitor_task = tokio::spawn(async move {
monitor.run().await;
});
let _ = rx.changed().await;
assert!(*rx.borrow());
let _ = monitor_task.await;
}
#[tokio::test]
async fn idle_monitor_does_not_trigger_when_active() {
let (tx, mut rx) = watch::channel(false);
let monitor = IdleMonitor::new(10, tx);
let handle = monitor.handle();
let monitor_task = tokio::spawn(async move {
monitor.run().await;
});
handle.touch();
let result = tokio::time::timeout(Duration::from_millis(200), rx.changed()).await;
assert!(result.is_err(), "shutdown should not have triggered");
monitor_task.abort();
}
}