use std::collections::HashMap;
use std::sync::RwLock;
use std::time::{Duration, Instant};
pub struct ClosedTimestampTracker {
groups: RwLock<HashMap<u64, Instant>>,
}
impl ClosedTimestampTracker {
pub fn new() -> Self {
Self {
groups: RwLock::new(HashMap::new()),
}
}
pub fn mark_applied(&self, group_id: u64) {
let mut g = self.groups.write().unwrap_or_else(|p| p.into_inner());
g.insert(group_id, Instant::now());
}
pub fn mark_applied_at(&self, group_id: u64, at: Instant) {
let mut g = self.groups.write().unwrap_or_else(|p| p.into_inner());
g.insert(group_id, at);
}
pub fn is_fresh_enough(&self, group_id: u64, max_staleness: Duration) -> bool {
let g = self.groups.read().unwrap_or_else(|p| p.into_inner());
match g.get(&group_id) {
Some(last) => last.elapsed() <= max_staleness,
None => false,
}
}
pub fn staleness(&self, group_id: u64) -> Option<Duration> {
let g = self.groups.read().unwrap_or_else(|p| p.into_inner());
g.get(&group_id).map(|last| last.elapsed())
}
}
impl Default for ClosedTimestampTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_group_is_not_fresh() {
let tracker = ClosedTimestampTracker::new();
assert!(!tracker.is_fresh_enough(99, Duration::from_secs(10)));
}
#[test]
fn recently_applied_is_fresh() {
let tracker = ClosedTimestampTracker::new();
tracker.mark_applied(1);
assert!(tracker.is_fresh_enough(1, Duration::from_secs(5)));
}
#[test]
fn stale_group_is_not_fresh() {
let tracker = ClosedTimestampTracker::new();
let old = Instant::now() - Duration::from_secs(30);
tracker.mark_applied_at(1, old);
assert!(!tracker.is_fresh_enough(1, Duration::from_secs(5)));
}
#[test]
fn staleness_returns_none_for_unknown() {
let tracker = ClosedTimestampTracker::new();
assert!(tracker.staleness(42).is_none());
}
#[test]
fn staleness_returns_age_for_known() {
let tracker = ClosedTimestampTracker::new();
tracker.mark_applied(1);
let s = tracker.staleness(1).unwrap();
assert!(s < Duration::from_millis(100));
}
#[test]
fn mark_applied_updates_monotonically() {
let tracker = ClosedTimestampTracker::new();
let old = Instant::now() - Duration::from_secs(10);
tracker.mark_applied_at(1, old);
assert!(!tracker.is_fresh_enough(1, Duration::from_secs(5)));
tracker.mark_applied(1);
assert!(tracker.is_fresh_enough(1, Duration::from_secs(5)));
}
}