use std::sync::Mutex;
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use std::time::Duration;
pub struct Activity {
in_flight: AtomicU64,
last_activity_ms: AtomicI64,
last_tool: Mutex<Option<String>>,
last_tool_ms: AtomicI64,
}
impl Default for Activity {
fn default() -> Self {
Self::new()
}
}
impl Activity {
pub fn new() -> Self {
Self {
in_flight: AtomicU64::new(0),
last_activity_ms: AtomicI64::new(now_ms()),
last_tool: Mutex::new(None),
last_tool_ms: AtomicI64::new(0),
}
}
pub fn start(&self) {
self.in_flight.fetch_add(1, Ordering::SeqCst);
self.bump();
}
pub fn end(&self) {
self.in_flight.fetch_sub(1, Ordering::SeqCst);
self.bump();
}
pub fn bump(&self) {
self.last_activity_ms.store(now_ms(), Ordering::Relaxed);
}
pub fn in_flight(&self) -> u64 {
self.in_flight.load(Ordering::SeqCst)
}
pub fn last_activity_ms(&self) -> i64 {
self.last_activity_ms.load(Ordering::Relaxed)
}
pub fn record_tool(&self, name: &str) {
let now = now_ms();
self.last_activity_ms.store(now, Ordering::Relaxed);
self.last_tool_ms.store(now, Ordering::Relaxed);
if let Ok(mut slot) = self.last_tool.lock() {
*slot = Some(name.to_string());
}
}
pub fn last_tool_name(&self) -> Option<String> {
self.last_tool.lock().ok().and_then(|slot| slot.clone())
}
pub fn last_tool_ms(&self) -> i64 {
self.last_tool_ms.load(Ordering::Relaxed)
}
pub fn idle_for(&self, dur: Duration) -> bool {
if self.in_flight() > 0 {
return false;
}
let last = self.last_activity_ms.load(Ordering::Relaxed);
let elapsed = now_ms().saturating_sub(last);
elapsed >= dur.as_millis() as i64
}
}
fn now_ms() -> i64 {
chrono::Utc::now().timestamp_millis()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fresh_activity_is_not_idle() {
let a = Activity::new();
assert!(!a.idle_for(Duration::from_secs(1)));
}
#[test]
fn becomes_idle_after_window() {
let a = Activity::new();
std::thread::sleep(Duration::from_millis(50));
assert!(a.idle_for(Duration::from_millis(20)));
}
#[test]
fn in_flight_blocks_idle() {
let a = Activity::new();
a.start();
std::thread::sleep(Duration::from_millis(50));
assert!(!a.idle_for(Duration::from_millis(10)));
a.end();
std::thread::sleep(Duration::from_millis(50));
assert!(a.idle_for(Duration::from_millis(10)));
}
#[test]
fn bump_resets_idle_window() {
let a = Activity::new();
std::thread::sleep(Duration::from_millis(50));
assert!(a.idle_for(Duration::from_millis(30)));
a.bump();
assert!(!a.idle_for(Duration::from_millis(30)));
}
}