use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub struct TtlCache<K, V> {
inner: Mutex<CacheInner<K, V>>,
default_ttl: Duration,
max_entries: usize,
}
struct CacheInner<K, V> {
entries: HashMap<K, CacheEntry<V>>,
}
struct CacheEntry<V> {
value: V,
expires_at: Instant,
}
impl<K: Eq + Hash + Clone, V: Clone> TtlCache<K, V> {
pub fn new(default_ttl: Duration, max_entries: usize) -> Self {
Self {
inner: Mutex::new(CacheInner {
entries: HashMap::new(),
}),
default_ttl,
max_entries,
}
}
pub fn insert(&self, key: K, value: V) {
self.insert_with_ttl(key, value, self.default_ttl);
}
pub fn insert_with_ttl(&self, key: K, value: V, ttl: Duration) {
let mut inner = self.inner.lock().unwrap();
if inner.entries.len() >= self.max_entries {
let now = Instant::now();
inner.entries.retain(|_, e| e.expires_at > now);
}
if inner.entries.len() >= self.max_entries
&& let Some(oldest_key) = inner
.entries
.iter()
.min_by_key(|(_, e)| e.expires_at)
.map(|(k, _)| k.clone())
{
inner.entries.remove(&oldest_key);
}
inner.entries.insert(
key,
CacheEntry {
value,
expires_at: Instant::now() + ttl,
},
);
}
pub fn get(&self, key: &K) -> Option<V> {
let mut inner = self.inner.lock().unwrap();
let entry = inner.entries.get(key)?;
if entry.expires_at <= Instant::now() {
inner.entries.remove(key);
None
} else {
Some(entry.value.clone())
}
}
pub fn remove(&self, key: &K) -> Option<V> {
let mut inner = self.inner.lock().unwrap();
inner.entries.remove(key).map(|e| e.value)
}
pub fn cleanup(&self) {
let mut inner = self.inner.lock().unwrap();
let now = Instant::now();
inner.entries.retain(|_, e| e.expires_at > now);
}
pub fn len(&self) -> usize {
self.inner.lock().unwrap().entries.len()
}
pub fn is_empty(&self) -> bool {
self.inner.lock().unwrap().entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_and_get() {
let cache = TtlCache::new(Duration::from_secs(60), 100);
cache.insert("key1", "value1");
assert_eq!(cache.get(&"key1"), Some("value1"));
}
#[test]
fn expired_entry_returns_none() {
let cache = TtlCache::new(Duration::from_millis(1), 100);
cache.insert("key1", "value1");
std::thread::sleep(Duration::from_millis(10));
assert_eq!(cache.get(&"key1"), None);
}
#[test]
fn max_entries_eviction() {
let cache = TtlCache::new(Duration::from_secs(60), 2);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3); assert_eq!(cache.len(), 2);
assert!(cache.get(&"c").is_some());
}
#[test]
fn remove() {
let cache = TtlCache::new(Duration::from_secs(60), 100);
cache.insert("key1", "value1");
assert_eq!(cache.remove(&"key1"), Some("value1"));
assert_eq!(cache.get(&"key1"), None);
}
#[test]
fn cleanup_removes_expired() {
let cache = TtlCache::new(Duration::from_millis(1), 100);
cache.insert("a", 1);
cache.insert("b", 2);
std::thread::sleep(Duration::from_millis(10));
cache.cleanup();
assert_eq!(cache.len(), 0);
}
}