use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::daemon_id::DaemonId;
pub(crate) static ACTIVITY: once_cell::sync::Lazy<ActivityTracker> =
once_cell::sync::Lazy::new(ActivityTracker::default);
#[derive(Debug)]
struct Entry {
in_flight: usize,
last_activity: Instant,
idle_stopping: bool,
}
impl Entry {
fn new(now: Instant) -> Self {
Self {
in_flight: 0,
last_activity: now,
idle_stopping: false,
}
}
}
#[derive(Debug, Default)]
pub(crate) struct ActivityTracker {
entries: Mutex<HashMap<DaemonId, Entry>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ActivitySnapshot {
pub in_flight: usize,
pub idle_for: Duration,
pub idle_stopping: bool,
}
impl ActivityTracker {
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<DaemonId, Entry>> {
self.entries.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn begin(&'static self, id: &DaemonId) -> Option<ActivityGuard> {
let now = Instant::now();
let mut entries = self.lock();
let entry = entries.entry(id.clone()).or_insert_with(|| Entry::new(now));
if entry.idle_stopping {
return None;
}
entry.in_flight += 1;
entry.last_activity = now;
Some(ActivityGuard {
tracker: self,
id: id.clone(),
})
}
pub(crate) fn begin_all(&'static self, ids: &[DaemonId]) -> Option<Vec<ActivityGuard>> {
let mut guards = Vec::with_capacity(ids.len());
for id in ids {
guards.push(self.begin(id)?);
}
Some(guards)
}
fn end(&self, id: &DaemonId) {
let mut entries = self.lock();
if let Some(entry) = entries.get_mut(id) {
entry.in_flight = entry.in_flight.saturating_sub(1);
entry.last_activity = Instant::now();
}
}
pub(crate) fn snapshot(&self, id: &DaemonId) -> ActivitySnapshot {
let now = Instant::now();
let mut entries = self.lock();
let entry = entries.entry(id.clone()).or_insert_with(|| Entry::new(now));
ActivitySnapshot {
in_flight: entry.in_flight,
idle_for: now.saturating_duration_since(entry.last_activity),
idle_stopping: entry.idle_stopping,
}
}
pub(crate) fn claim_idle_stop(&self, id: &DaemonId, grace: Duration) -> bool {
let now = Instant::now();
let mut entries = self.lock();
let entry = entries.entry(id.clone()).or_insert_with(|| Entry::new(now));
if entry.idle_stopping
|| entry.in_flight > 0
|| now.saturating_duration_since(entry.last_activity) < grace
{
return false;
}
entry.idle_stopping = true;
true
}
pub(crate) fn release_idle_stop(&self, id: &DaemonId) {
let mut entries = self.lock();
if let Some(entry) = entries.get_mut(id) {
entry.idle_stopping = false;
entry.last_activity = Instant::now();
}
}
pub(crate) fn is_idle_stopping(&self, id: &DaemonId) -> bool {
self.lock().get(id).is_some_and(|e| e.idle_stopping)
}
}
#[must_use = "activity ends as soon as the guard is dropped"]
pub(crate) struct ActivityGuard {
tracker: &'static ActivityTracker,
id: DaemonId,
}
impl std::fmt::Debug for ActivityGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ActivityGuard")
.field("id", &self.id)
.finish()
}
}
impl Drop for ActivityGuard {
fn drop(&mut self) {
self.tracker.end(&self.id);
}
}
pub(crate) struct GuardedBody<B> {
inner: B,
_guard: Option<ActivityGuard>,
}
impl<B> GuardedBody<B> {
pub(crate) fn new(inner: B, guard: Option<ActivityGuard>) -> Self {
Self {
inner,
_guard: guard,
}
}
}
impl<B> hyper::body::Body for GuardedBody<B>
where
B: hyper::body::Body + Unpin,
{
type Data = B::Data;
type Error = B::Error;
fn poll_frame(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
std::pin::Pin::new(&mut self.inner).poll_frame(cx)
}
fn is_end_stream(&self) -> bool {
self.inner.is_end_stream()
}
fn size_hint(&self) -> hyper::body::SizeHint {
self.inner.size_hint()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tracker() -> &'static ActivityTracker {
Box::leak(Box::default())
}
fn id(name: &str) -> DaemonId {
DaemonId::new("proj", name)
}
#[test]
fn in_flight_work_blocks_an_idle_claim() {
let t = tracker();
let guard = t.begin(&id("api")).unwrap();
assert!(!t.claim_idle_stop(&id("api"), Duration::ZERO));
drop(guard);
assert!(t.claim_idle_stop(&id("api"), Duration::ZERO));
}
#[test]
fn recent_activity_blocks_an_idle_claim_until_the_grace_passes() {
let t = tracker();
drop(t.begin(&id("api")).unwrap());
assert!(!t.claim_idle_stop(&id("api"), Duration::from_secs(60)));
assert!(t.claim_idle_stop(&id("api"), Duration::ZERO));
}
#[test]
fn a_claimed_stop_refuses_new_work_until_released() {
let t = tracker();
assert!(t.claim_idle_stop(&id("api"), Duration::ZERO));
assert!(t.is_idle_stopping(&id("api")));
assert!(t.begin(&id("api")).is_none());
assert!(!t.claim_idle_stop(&id("api"), Duration::ZERO));
t.release_idle_stop(&id("api"));
assert!(!t.is_idle_stopping(&id("api")));
assert!(t.begin(&id("api")).is_some());
}
#[test]
fn begin_all_takes_nothing_when_one_daemon_is_stopping() {
let t = tracker();
assert!(t.claim_idle_stop(&id("db"), Duration::ZERO));
assert!(t.begin_all(&[id("api"), id("db")]).is_none());
assert_eq!(t.snapshot(&id("api")).in_flight, 0);
}
#[test]
fn a_daemon_first_seen_by_a_snapshot_starts_a_fresh_idle_clock() {
let t = tracker();
let snap = t.snapshot(&id("adopted"));
assert_eq!(snap.in_flight, 0);
assert!(snap.idle_for < Duration::from_secs(1));
assert!(!t.claim_idle_stop(&id("adopted"), Duration::from_secs(60)));
}
#[test]
fn releasing_a_claim_restarts_the_idle_clock() {
let t = tracker();
assert!(t.claim_idle_stop(&id("api"), Duration::ZERO));
t.release_idle_stop(&id("api"));
assert!(!t.claim_idle_stop(&id("api"), Duration::from_secs(60)));
}
#[test]
fn guarded_body_ends_activity_when_dropped() {
let t = tracker();
let body = GuardedBody::new(
http_body_util::Empty::<hyper::body::Bytes>::new(),
t.begin(&id("api")),
);
assert_eq!(t.snapshot(&id("api")).in_flight, 1);
drop(body);
assert_eq!(t.snapshot(&id("api")).in_flight, 0);
}
}