use std::collections::{HashMap, VecDeque};
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub(crate) struct RetrieveCache<V: Clone> {
inner: Mutex<Inner<V>>,
capacity: usize,
ttl: Duration,
}
struct Inner<V> {
map: HashMap<u64, (Instant, V)>,
order: VecDeque<u64>,
}
impl<V: Clone> RetrieveCache<V> {
pub fn new(capacity: usize, ttl: Duration) -> Self {
Self {
inner: Mutex::new(Inner {
map: HashMap::new(),
order: VecDeque::new(),
}),
capacity,
ttl,
}
}
pub fn get(&self, key: u64) -> Option<V> {
let mut g = self.inner.lock().ok()?;
let now = Instant::now();
let entry = g.map.get(&key).cloned();
match entry {
Some((inserted, v)) if now.duration_since(inserted) <= self.ttl => {
if let Some(pos) = g.order.iter().position(|k| *k == key) {
g.order.remove(pos);
}
g.order.push_back(key);
Some(v)
}
Some(_) => {
g.map.remove(&key);
if let Some(pos) = g.order.iter().position(|k| *k == key) {
g.order.remove(pos);
}
None
}
None => None,
}
}
pub fn insert(&self, key: u64, value: V) {
let Ok(mut g) = self.inner.lock() else {
return;
};
let now = Instant::now();
if let std::collections::hash_map::Entry::Occupied(mut e) = g.map.entry(key) {
e.insert((now, value));
if let Some(pos) = g.order.iter().position(|k| *k == key) {
g.order.remove(pos);
}
g.order.push_back(key);
return;
}
while g.order.len() >= self.capacity {
match g.order.pop_front() {
Some(k) => {
g.map.remove(&k);
}
None => break,
}
}
g.map.insert(key, (now, value));
g.order.push_back(key);
}
pub fn clear(&self) {
let Ok(mut g) = self.inner.lock() else {
return;
};
g.map.clear();
g.order.clear();
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.inner.lock().map(|g| g.map.len()).unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
#[test]
fn hit_then_miss_after_clear() {
let c: RetrieveCache<i32> = RetrieveCache::new(8, Duration::from_secs(30));
c.insert(1, 100);
assert_eq!(c.get(1), Some(100));
c.clear();
assert_eq!(c.get(1), None);
}
#[test]
fn expired_entry_drops() {
let c: RetrieveCache<i32> = RetrieveCache::new(8, Duration::from_millis(20));
c.insert(1, 100);
sleep(Duration::from_millis(40));
assert_eq!(c.get(1), None);
assert_eq!(c.len(), 0);
}
#[test]
fn lru_eviction_at_capacity() {
let c: RetrieveCache<i32> = RetrieveCache::new(2, Duration::from_secs(30));
c.insert(1, 1);
c.insert(2, 2);
assert_eq!(c.get(1), Some(1));
c.insert(3, 3);
assert_eq!(c.get(2), None);
assert_eq!(c.get(1), Some(1));
assert_eq!(c.get(3), Some(3));
}
#[test]
fn refresh_does_not_grow() {
let c: RetrieveCache<i32> = RetrieveCache::new(8, Duration::from_secs(30));
c.insert(1, 100);
c.insert(1, 101);
assert_eq!(c.len(), 1);
assert_eq!(c.get(1), Some(101));
}
}