use std::collections::HashMap;
use std::hash::Hash;
use std::sync::{Mutex, MutexGuard};
use std::time::{Duration, Instant};
struct Entry<V> {
value: V,
stored: Instant,
used: u64,
}
struct Inner<K, V> {
entries: HashMap<K, Entry<V>>,
tick: u64,
}
pub struct Cache<K, V> {
inner: Mutex<Inner<K, V>>,
capacity: usize,
ttl: Option<Duration>,
}
impl<K: Hash + Eq + Clone, V: Clone> Cache<K, V> {
pub fn new(capacity: usize) -> Self {
Self {
inner: Mutex::new(Inner {
entries: HashMap::new(),
tick: 0,
}),
capacity: capacity.max(1),
ttl: None,
}
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn get(&self, key: &K) -> Option<V> {
let mut inner = self.lock();
let ttl = self.ttl;
inner.tick += 1;
let tick = inner.tick;
if let Some(entry) = inner.entries.get_mut(key) {
if ttl.is_none_or(|ttl| entry.stored.elapsed() < ttl) {
entry.used = tick;
return Some(entry.value.clone());
}
inner.entries.remove(key);
}
None
}
pub fn insert(&self, key: K, value: V) {
let mut inner = self.lock();
inner.tick += 1;
let used = inner.tick;
if !inner.entries.contains_key(&key) && inner.entries.len() >= self.capacity {
let oldest = inner
.entries
.iter()
.min_by_key(|(_, entry)| entry.used)
.map(|(key, _)| key.clone());
if let Some(oldest) = oldest {
inner.entries.remove(&oldest);
}
}
inner.entries.insert(
key,
Entry {
value,
stored: Instant::now(),
used,
},
);
}
pub fn get_or_insert_with(&self, key: K, load: impl FnOnce() -> V) -> V {
if let Some(value) = self.get(&key) {
return value;
}
let value = load();
self.insert(key, value.clone());
value
}
pub fn invalidate(&self, key: &K) {
self.lock().entries.remove(key);
}
pub fn invalidate_all(&self) {
self.lock().entries.clear();
}
pub fn len(&self) -> usize {
self.lock().entries.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn lock(&self) -> MutexGuard<'_, Inner<K, V>> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn evicts_least_recently_used() {
let cache = Cache::new(2);
cache.insert("a", 1);
cache.insert("b", 2);
assert_eq!(cache.get(&"a"), Some(1)); cache.insert("c", 3);
assert_eq!(cache.get(&"b"), None);
assert_eq!(cache.get(&"a"), Some(1));
assert_eq!(cache.get(&"c"), Some(3));
assert_eq!(cache.len(), 2);
}
#[test]
fn overwriting_does_not_evict() {
let cache = Cache::new(2);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("a", 3);
assert_eq!((cache.get(&"a"), cache.get(&"b")), (Some(3), Some(2)));
}
#[test]
fn expires_after_ttl() {
let cache = Cache::new(4).with_ttl(Duration::from_millis(200));
cache.insert("a", 1);
assert_eq!(cache.get(&"a"), Some(1));
std::thread::sleep(Duration::from_millis(250));
assert_eq!(cache.get(&"a"), None);
assert!(cache.is_empty());
}
#[test]
fn reads_through_once_and_invalidates() {
let cache = Cache::new(4);
let mut loads = 0;
for _ in 0..3 {
cache.get_or_insert_with("k", || {
loads += 1;
7
});
}
assert_eq!(loads, 1);
cache.invalidate(&"k");
assert_eq!(cache.get_or_insert_with("k", || 8), 8);
cache.invalidate_all();
assert!(cache.is_empty());
}
}