use std::sync::atomic::{AtomicU64, Ordering};
use moka::policy::EvictionPolicy;
use moka::sync::Cache as MokaCache;
use crate::cache::evictor::CacheEvictor;
use crate::cache::page_id::PageId;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EvictMode {
Lru,
Lfu,
}
pub struct MokaCacheEvictor {
cache: MokaCache<PageId, u64>,
counter: AtomicU64,
mode: EvictMode,
}
impl MokaCacheEvictor {
pub fn new_lru() -> Self {
Self {
cache: MokaCache::builder()
.max_capacity(u64::MAX)
.eviction_policy(EvictionPolicy::lru())
.build(),
counter: AtomicU64::new(0),
mode: EvictMode::Lru,
}
}
pub fn new_lfu() -> Self {
Self {
cache: MokaCache::builder()
.max_capacity(u64::MAX)
.eviction_policy(EvictionPolicy::tiny_lfu())
.build(),
counter: AtomicU64::new(0),
mode: EvictMode::Lfu,
}
}
#[inline]
fn next_tick(&self) -> u64 {
self.counter.fetch_add(1, Ordering::Relaxed) + 1
}
}
impl CacheEvictor for MokaCacheEvictor {
fn on_add(&self, id: &PageId) {
let value = match self.mode {
EvictMode::Lru => self.next_tick(),
EvictMode::Lfu => 1, };
self.cache.insert(id.clone(), value);
}
fn on_access(&self, id: &PageId) {
match self.mode {
EvictMode::Lru => {
let tick = self.next_tick();
self.cache.insert(id.clone(), tick);
}
EvictMode::Lfu => {
let current = self.cache.get(id).unwrap_or(0);
self.cache.insert(id.clone(), current.saturating_add(1));
}
}
}
fn on_remove(&self, id: &PageId) {
self.cache.invalidate(id);
}
fn evict_candidate(&self) -> Option<PageId> {
self.cache.run_pending_tasks();
self.cache
.iter()
.min_by_key(|(_, v)| *v)
.map(|(k, _)| k.as_ref().clone())
}
fn len(&self) -> usize {
self.cache.entry_count() as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pid(i: u64) -> PageId {
PageId::new("f", i)
}
#[test]
fn lru_evicts_least_recently_used() {
let e = MokaCacheEvictor::new_lru();
e.on_add(&pid(0));
e.on_add(&pid(1));
e.on_add(&pid(2));
e.on_access(&pid(0));
assert_eq!(e.evict_candidate(), Some(pid(1)));
e.on_remove(&pid(1));
assert_eq!(e.evict_candidate(), Some(pid(2)));
}
#[test]
fn lru_empty_has_no_candidate() {
let e = MokaCacheEvictor::new_lru();
assert!(e.is_empty());
assert_eq!(e.evict_candidate(), None);
}
#[test]
fn lru_access_updates_recency() {
let e = MokaCacheEvictor::new_lru();
e.on_add(&pid(0));
e.on_add(&pid(1));
assert_eq!(e.evict_candidate(), Some(pid(0)));
e.on_access(&pid(0));
assert_eq!(e.evict_candidate(), Some(pid(1)));
}
#[test]
fn lfu_evicts_least_frequently_used() {
let e = MokaCacheEvictor::new_lfu();
e.on_add(&pid(0)); e.on_add(&pid(1)); e.on_add(&pid(2));
e.on_access(&pid(0));
e.on_access(&pid(0));
let victim = e.evict_candidate().unwrap();
assert!(
victim == pid(1) || victim == pid(2),
"victim should be p1 or p2, got {victim:?}"
);
assert_ne!(victim, pid(0), "p0 (frequent) should not be evicted");
}
#[test]
fn lfu_empty_has_no_candidate() {
let e = MokaCacheEvictor::new_lfu();
assert!(e.is_empty());
assert_eq!(e.evict_candidate(), None);
}
#[test]
fn lfu_frequency_increments_on_access() {
let e = MokaCacheEvictor::new_lfu();
e.on_add(&pid(0)); e.on_add(&pid(1));
e.on_access(&pid(0));
e.on_access(&pid(0));
e.on_access(&pid(0));
assert_eq!(e.evict_candidate(), Some(pid(1)));
}
#[test]
fn lru_remove_updates_len() {
let e = MokaCacheEvictor::new_lru();
e.on_add(&pid(0));
e.on_add(&pid(1));
e.cache.run_pending_tasks();
assert_eq!(e.len(), 2);
e.on_remove(&pid(0));
e.cache.run_pending_tasks();
assert_eq!(e.len(), 1);
assert_eq!(e.evict_candidate(), Some(pid(1)));
}
#[test]
fn lfu_remove_updates_len() {
let e = MokaCacheEvictor::new_lfu();
e.on_add(&pid(0));
e.on_add(&pid(1));
e.cache.run_pending_tasks();
assert_eq!(e.len(), 2);
e.on_remove(&pid(0));
e.cache.run_pending_tasks();
assert_eq!(e.len(), 1);
assert_eq!(e.evict_candidate(), Some(pid(1)));
}
#[test]
fn concurrent_on_access_no_deadlock() {
use std::sync::Arc;
use std::thread;
for evictor in [MokaCacheEvictor::new_lru(), MokaCacheEvictor::new_lfu()] {
let e = Arc::new(evictor);
for i in 0..100u64 {
e.on_add(&pid(i));
}
let mut handles = Vec::new();
for t in 0..8 {
let e = Arc::clone(&e);
handles.push(thread::spawn(move || {
for i in 0..1000u64 {
let id = pid((t * 1000 + i) % 100);
e.on_access(&id);
}
}));
}
for h in handles {
h.join().unwrap();
}
e.cache.run_pending_tasks();
assert_eq!(e.len(), 100);
}
}
}