use moka::sync::Cache;
use moka::Expiry;
use std::time::{Duration, Instant};
#[derive(Clone)]
struct L1Entry {
data: Vec<u8>,
ttl: Duration,
created_at: Instant,
freshness_jitter: f64,
}
struct L1Expiry;
impl Expiry<String, L1Entry> for L1Expiry {
fn expire_after_create(
&self,
_key: &String,
value: &L1Entry,
_created_at: std::time::Instant,
) -> Option<Duration> {
Some(value.ttl)
}
fn expire_after_update(
&self,
_key: &String,
value: &L1Entry,
_updated_at: std::time::Instant,
_duration_until_expiry: Option<Duration>,
) -> Option<Duration> {
Some(value.ttl)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum L1SwrRead {
Fresh(Vec<u8>),
Stale(Vec<u8>),
Miss,
}
#[derive(Clone)]
pub struct L1Cache {
store: Cache<String, L1Entry>,
}
impl L1Cache {
pub fn new(capacity: usize) -> Self {
Self {
store: Cache::builder()
.max_capacity(u64::try_from(capacity).unwrap_or(u64::MAX))
.expire_after(L1Expiry)
.build(),
}
}
pub fn get(&self, key: &str) -> Option<Vec<u8>> {
self.store.get(key).map(|entry| entry.data.clone())
}
pub fn get_with_swr(&self, key: &str, threshold_ratio: f64) -> L1SwrRead {
let Some(entry) = self.store.get(key) else {
return L1SwrRead::Miss;
};
let threshold = entry.ttl.as_secs_f64() * threshold_ratio * entry.freshness_jitter;
if entry.created_at.elapsed().as_secs_f64() > threshold {
L1SwrRead::Stale(entry.data.clone())
} else {
L1SwrRead::Fresh(entry.data.clone())
}
}
pub fn set(&self, key: &str, value: &[u8], ttl: Duration) {
self.store.insert(
key.to_string(),
L1Entry {
data: value.to_vec(),
ttl,
created_at: Instant::now(),
freshness_jitter: 0.9 + crate::random_unit() * 0.2,
},
);
}
pub fn delete(&self, key: &str) {
self.store.invalidate(key);
}
pub fn run_pending_tasks(&self) {
self.store.run_pending_tasks();
}
}