use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap};
use std::time::SystemTime;
#[derive(Debug, Default)]
pub(crate) struct Expirations {
due: BinaryHeap<Reverse<(SystemTime, String)>>,
current: HashMap<String, SystemTime>,
#[cfg(test)]
examined: usize,
}
impl Expirations {
pub(crate) fn set(&mut self, uri: &str, expires_at: SystemTime) {
self.due.push(Reverse((expires_at, uri.to_string())));
self.current.insert(uri.to_string(), expires_at);
}
pub(crate) fn forget(&mut self, uri: &str) {
self.current.remove(uri);
}
pub(crate) fn clear(&mut self) {
self.due.clear();
self.current.clear();
}
pub(crate) fn take_due(&mut self, now: SystemTime) -> Vec<String> {
let mut lapsed = Vec::new();
while let Some(Reverse((deadline, uri))) = self.due.pop() {
#[cfg(test)]
{
self.examined += 1;
}
if deadline > now {
self.due.push(Reverse((deadline, uri)));
break;
}
if self.current.get(&uri) == Some(&deadline) {
self.current.remove(&uri);
lapsed.push(uri);
}
}
lapsed
}
pub(crate) fn queued(&self) -> usize {
self.due.len()
}
}
#[cfg(test)]
impl Expirations {
pub(crate) fn tracked(&self) -> usize {
self.current.len()
}
pub(crate) fn examined(&self) -> usize {
self.examined
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn at(offset_secs: i64) -> SystemTime {
let base = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
let offset = Duration::from_secs(offset_secs.unsigned_abs());
if offset_secs < 0 {
base - offset
} else {
base + offset
}
}
fn now() -> SystemTime {
at(0)
}
#[test]
fn nothing_is_due_before_its_deadline() {
let mut expirations = Expirations::default();
expirations.set("agent://example.com/a/one", at(60));
assert!(expirations.take_due(now()).is_empty());
assert_eq!(expirations.tracked(), 1);
}
#[test]
fn a_deadline_that_has_arrived_is_due() {
let mut expirations = Expirations::default();
expirations.set("agent://example.com/a/one", now());
assert_eq!(
expirations.take_due(now()),
vec!["agent://example.com/a/one"]
);
assert_eq!(expirations.tracked(), 0);
}
#[test]
fn the_soonest_deadline_comes_first() {
let mut expirations = Expirations::default();
expirations.set("agent://example.com/a/late", at(-1));
expirations.set("agent://example.com/a/early", at(-60));
assert_eq!(
expirations.take_due(now()),
vec!["agent://example.com/a/early", "agent://example.com/a/late"]
);
}
#[test]
fn only_what_is_due_is_examined() {
let mut expirations = Expirations::default();
for index in 0..100 {
expirations.set(&format!("agent://example.com/a/live{index}"), at(60));
}
expirations.set("agent://example.com/a/lapsed", at(-1));
assert_eq!(
expirations.take_due(now()),
vec!["agent://example.com/a/lapsed"]
);
assert_eq!(
expirations.examined(),
2,
"a drain must stop at the first deadline that has not arrived"
);
assert_eq!(expirations.queued(), 100, "a live deadline was consumed");
}
#[test]
fn a_deadline_moved_later_survives_the_one_it_replaced() {
let mut expirations = Expirations::default();
expirations.set("agent://example.com/a/one", at(-1));
expirations.set("agent://example.com/a/one", at(60));
assert!(expirations.take_due(now()).is_empty());
assert_eq!(expirations.tracked(), 1);
}
#[test]
fn a_deadline_moved_earlier_is_due_at_the_new_one() {
let mut expirations = Expirations::default();
expirations.set("agent://example.com/a/one", at(3600));
expirations.set("agent://example.com/a/one", at(-1));
assert_eq!(
expirations.take_due(now()),
vec!["agent://example.com/a/one"]
);
}
#[test]
fn a_forgotten_registration_is_not_due() {
let mut expirations = Expirations::default();
expirations.set("agent://example.com/a/one", at(-1));
expirations.forget("agent://example.com/a/one");
assert!(expirations.take_due(now()).is_empty());
}
#[test]
fn re_registering_does_not_inherit_the_old_deadline() {
let mut expirations = Expirations::default();
expirations.set("agent://example.com/a/one", at(-1));
expirations.forget("agent://example.com/a/one");
expirations.set("agent://example.com/a/one", at(3600));
assert!(expirations.take_due(now()).is_empty());
assert_eq!(expirations.tracked(), 1);
}
#[test]
fn a_due_registration_is_reported_once() {
let mut expirations = Expirations::default();
expirations.set("agent://example.com/a/one", at(-60));
expirations.set("agent://example.com/a/one", at(-1));
assert_eq!(
expirations.take_due(now()),
vec!["agent://example.com/a/one"]
);
assert!(
expirations.take_due(now()).is_empty(),
"the record is gone; its remaining entries speak for nothing"
);
}
#[test]
fn clearing_forgets_everything() {
let mut expirations = Expirations::default();
expirations.set("agent://example.com/a/one", at(-1));
expirations.clear();
assert!(expirations.take_due(now()).is_empty());
assert_eq!(expirations.queued(), 0);
assert_eq!(expirations.tracked(), 0);
}
}