use parking_lot::Mutex;
use std::collections::{HashMap, HashSet};
use std::time::{Duration, Instant};
pub type Counts = (usize, usize);
struct Entry<T> {
counts: Counts,
refreshed_at: Instant,
value: T,
}
pub struct StatsGate<T> {
entries: Mutex<HashMap<String, Entry<T>>>,
full_refresh: Duration,
}
impl<T: Clone> StatsGate<T> {
pub fn new(full_refresh: Duration) -> Self {
Self {
entries: Mutex::new(HashMap::new()),
full_refresh,
}
}
pub fn needs_refresh(&self, key: &str, counts: Counts) -> bool {
match self.entries.lock().get(key) {
Some(entry) => {
entry.counts != counts || entry.refreshed_at.elapsed() >= self.full_refresh
}
None => true,
}
}
pub fn cached(&self, key: &str) -> Option<T> {
self.entries.lock().get(key).map(|e| e.value.clone())
}
pub fn record(&self, key: &str, counts: Counts, value: T) {
self.entries.lock().insert(
key.to_string(),
Entry {
counts,
refreshed_at: Instant::now(),
value,
},
);
}
pub fn retain(&self, live: &HashSet<String>) {
self.entries.lock().retain(|key, _| live.contains(key));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unseen_key_needs_refresh() {
let gate: StatsGate<u64> = StatsGate::new(Duration::from_secs(300));
assert!(gate.needs_refresh("a", (0, 0)));
assert_eq!(gate.cached("a"), None);
}
#[test]
fn unchanged_counts_are_gated() {
let gate = StatsGate::new(Duration::from_secs(300));
gate.record("a", (5, 0), 42u64);
assert!(!gate.needs_refresh("a", (5, 0)));
assert_eq!(gate.cached("a"), Some(42));
}
#[test]
fn moved_counts_reopen_the_gate() {
let gate = StatsGate::new(Duration::from_secs(300));
gate.record("a", (5, 0), 42u64);
assert!(gate.needs_refresh("a", (6, 0)));
assert!(gate.needs_refresh("a", (5, 1)));
}
#[test]
fn periodic_refresh_falls_due() {
let gate = StatsGate::new(Duration::from_secs(0));
gate.record("a", (5, 0), 42u64);
assert!(gate.needs_refresh("a", (5, 0)));
}
#[test]
fn retain_drops_vanished_collections() {
let gate = StatsGate::new(Duration::from_secs(300));
gate.record("gone", (1, 0), 1u64);
gate.record("here", (1, 0), 2u64);
let live: HashSet<String> = ["here".to_string()].into_iter().collect();
gate.retain(&live);
assert_eq!(gate.cached("gone"), None);
assert_eq!(gate.cached("here"), Some(2));
}
}