use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct NegativeCache {
ttl_ms: u64,
entries: HashMap<String, u64>,
}
impl NegativeCache {
#[must_use]
pub fn new(ttl_ms: u64) -> Self {
Self {
ttl_ms,
entries: HashMap::new(),
}
}
pub fn insert_miss(&mut self, key: &str, now_ms: u64) {
self.entries.insert(key.to_string(), now_ms);
}
#[must_use]
pub fn is_known_miss(&self, key: &str, now_ms: u64) -> bool {
match self.entries.get(key) {
Some(&recorded_ms) => {
let expiry = recorded_ms.saturating_add(self.ttl_ms);
now_ms < expiry
}
None => false,
}
}
pub fn remove(&mut self, key: &str) {
self.entries.remove(key);
}
pub fn evict_expired(&mut self, now_ms: u64) {
self.entries.retain(|_, &mut recorded_ms| {
let expiry = recorded_ms.saturating_add(self.ttl_ms);
now_ms < expiry
});
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn ttl_ms(&self) -> u64 {
self.ttl_ms
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_is_empty() {
let nc = NegativeCache::new(5_000);
assert!(nc.is_empty());
assert_eq!(nc.len(), 0);
}
#[test]
fn test_ttl_getter() {
let nc = NegativeCache::new(10_000);
assert_eq!(nc.ttl_ms(), 10_000);
}
#[test]
fn test_is_known_miss_before_expiry() {
let mut nc = NegativeCache::new(5_000);
nc.insert_miss("key1", 1_000_000);
assert!(nc.is_known_miss("key1", 1_002_000));
}
#[test]
fn test_is_known_miss_at_expiry_boundary() {
let mut nc = NegativeCache::new(5_000);
nc.insert_miss("k", 1_000_000);
assert!(!nc.is_known_miss("k", 1_005_000));
}
#[test]
fn test_is_known_miss_after_expiry() {
let mut nc = NegativeCache::new(5_000);
nc.insert_miss("k", 1_000_000);
assert!(!nc.is_known_miss("k", 1_100_000));
}
#[test]
fn test_is_known_miss_unknown_key_returns_false() {
let nc = NegativeCache::new(5_000);
assert!(!nc.is_known_miss("absent", 0));
}
#[test]
fn test_insert_miss_overwrites_existing() {
let mut nc = NegativeCache::new(5_000);
nc.insert_miss("k", 1_000_000);
nc.insert_miss("k", 2_000_000);
assert!(nc.is_known_miss("k", 2_001_000));
assert!(!nc.is_known_miss("k", 2_006_000));
}
#[test]
fn test_remove_makes_key_unknown() {
let mut nc = NegativeCache::new(5_000);
nc.insert_miss("k", 0);
nc.remove("k");
assert!(!nc.is_known_miss("k", 1_000));
}
#[test]
fn test_remove_nonexistent_is_noop() {
let mut nc = NegativeCache::new(5_000);
nc.remove("ghost"); assert!(nc.is_empty());
}
#[test]
fn test_evict_expired_removes_old_entries() {
let mut nc = NegativeCache::new(1_000);
nc.insert_miss("old", 0); nc.insert_miss("young", 5_000); nc.evict_expired(2_000); assert_eq!(nc.len(), 1, "Only 'young' should remain");
assert!(!nc.is_known_miss("old", 500));
}
#[test]
fn test_evict_expired_keeps_valid_entries() {
let mut nc = NegativeCache::new(10_000);
nc.insert_miss("a", 1_000);
nc.evict_expired(5_000); assert_eq!(nc.len(), 1);
}
#[test]
fn test_clear_removes_all_entries() {
let mut nc = NegativeCache::new(5_000);
nc.insert_miss("a", 1);
nc.insert_miss("b", 2);
nc.clear();
assert!(nc.is_empty());
}
#[test]
fn test_zero_ttl_expires_immediately() {
let mut nc = NegativeCache::new(0);
nc.insert_miss("k", 1_000);
assert!(!nc.is_known_miss("k", 1_000));
}
}