use lru::LruCache;
use std::borrow::Borrow;
use std::hash::Hash;
use std::time::{Duration, Instant};
pub(crate) trait CacheWeight {
fn cache_weight(&self) -> usize;
}
#[derive(Debug)]
struct WeightedValue<V> {
value: V,
weight: usize,
}
#[derive(Debug)]
pub(crate) struct ByteLruCache<K, V>
where
K: Hash + Eq,
{
entries: LruCache<K, WeightedValue<V>>,
max_weight: usize,
current_weight: usize,
}
impl<K, V> ByteLruCache<K, V>
where
K: Hash + Eq,
V: CacheWeight,
{
pub(crate) fn new(max_weight: usize) -> Self {
Self {
entries: LruCache::unbounded(),
max_weight,
current_weight: 0,
}
}
pub(crate) fn get_cloned<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
V: Clone,
{
self.entries.get(key).map(|entry| entry.value.clone())
}
pub(crate) fn insert(&mut self, key: K, value: V) -> bool {
let weight = value.cache_weight();
if self.max_weight == 0 || weight > self.max_weight {
return false;
}
if let Some((_old_key, old_value)) = self.entries.pop_entry(&key) {
self.current_weight = self.current_weight.saturating_sub(old_value.weight);
}
self.entries.push(key, WeightedValue { value, weight });
self.current_weight = self.current_weight.saturating_add(weight);
self.evict_to_budget();
true
}
#[cfg(test)]
pub(crate) fn current_weight(&self) -> usize {
self.current_weight
}
fn evict_to_budget(&mut self) {
while self.current_weight > self.max_weight {
let Some((_key, value)) = self.entries.pop_lru() else {
break;
};
self.current_weight = self.current_weight.saturating_sub(value.weight);
}
}
}
#[derive(Debug)]
struct TimedValue<V> {
value: V,
expires_at: Instant,
}
#[derive(Debug)]
pub(crate) struct TtlLruCache<K, V>
where
K: Hash + Eq,
{
entries: LruCache<K, TimedValue<V>>,
ttl: Duration,
max_entries: usize,
}
impl<K, V> TtlLruCache<K, V>
where
K: Hash + Eq,
{
pub(crate) fn new(max_entries: usize, ttl: Duration) -> Self {
Self {
entries: LruCache::unbounded(),
ttl,
max_entries,
}
}
pub(crate) fn get_cloned<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
V: Clone,
{
if self
.entries
.peek(key)
.is_some_and(|entry| entry.expires_at <= Instant::now())
{
let _ = self.entries.pop(key);
return None;
}
self.entries.get(key).map(|entry| entry.value.clone())
}
pub(crate) fn insert(&mut self, key: K, value: V) {
if self.max_entries == 0 {
return;
}
self.entries.push(
key,
TimedValue {
value,
expires_at: Instant::now() + self.ttl,
},
);
self.prune_expired_lru_tail();
while self.entries.len() > self.max_entries {
let _ = self.entries.pop_lru();
}
}
fn prune_expired_lru_tail(&mut self) {
while self
.entries
.peek_lru()
.is_some_and(|(_key, value)| value.expires_at <= Instant::now())
{
let _ = self.entries.pop_lru();
}
}
}
#[cfg(test)]
mod tests {
use super::{ByteLruCache, CacheWeight, TtlLruCache};
use std::thread;
use std::time::Duration;
#[derive(Clone, Debug, Eq, PartialEq)]
struct Bytes(Vec<u8>);
impl CacheWeight for Bytes {
fn cache_weight(&self) -> usize {
self.0.len()
}
}
#[test]
fn byte_lru_cache_evicts_to_weight_budget() {
let mut cache = ByteLruCache::new(8);
assert!(cache.insert(1_u8, Bytes(vec![1, 2, 3, 4])));
assert!(cache.insert(2_u8, Bytes(vec![5, 6, 7, 8])));
assert_eq!(cache.get_cloned(&1_u8), Some(Bytes(vec![1, 2, 3, 4])));
assert!(cache.insert(3_u8, Bytes(vec![9, 10, 11, 12])));
assert_eq!(cache.current_weight(), 8);
assert_eq!(cache.get_cloned(&1_u8), Some(Bytes(vec![1, 2, 3, 4])));
assert_eq!(cache.get_cloned(&2_u8), None);
assert_eq!(cache.get_cloned(&3_u8), Some(Bytes(vec![9, 10, 11, 12])));
}
#[test]
fn ttl_lru_cache_expires_entries() {
let mut cache = TtlLruCache::new(8, Duration::from_millis(25));
cache.insert(String::from("HEAD"), 42_u8);
assert_eq!(cache.get_cloned("HEAD"), Some(42));
thread::sleep(Duration::from_millis(35));
assert_eq!(cache.get_cloned("HEAD"), None);
}
}