use crate::policy::lru_list::LruList;
use crate::policy::AdmissionDecision;
use super::CachePolicy;
use parking_lot::Mutex;
use std::hash::Hash;
#[derive(Debug)]
pub struct LruPolicy<K: Eq + Hash + Clone> {
list: Mutex<LruList<K>>,
}
impl<K: Eq + Hash + Clone> LruPolicy<K> {
pub fn new() -> Self {
Self {
list: Mutex::new(LruList::new()),
}
}
}
impl<K, V> CachePolicy<K, V> for LruPolicy<K>
where
K: Eq + Hash + Clone + Send + Sync,
V: Send + Sync,
{
fn on_access(&self, key: &K, _cost: u64) {
self.list.lock().move_to_front(key);
}
fn on_admit(&self, key: &K, cost: u64) -> AdmissionDecision<K> {
self.list.lock().push_front(key.clone(), cost);
AdmissionDecision::Admit }
fn on_remove(&self, key: &K) {
self.list.lock().remove(key);
}
fn evict(&self, cost_to_free: u64) -> (Vec<K>, u64) {
let mut victims = Vec::new();
let mut total_cost_freed = 0;
let mut list = self.list.lock();
while total_cost_freed < cost_to_free {
if let Some((key, cost)) = list.pop_back() {
total_cost_freed += cost;
victims.push(key);
} else {
break; }
}
(victims, total_cost_freed)
}
fn clear(&self) {
self.list.lock().clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_item_admitted_to_front() {
let policy = LruPolicy::<i32>::new();
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 1);
let list = policy.list.lock();
assert_eq!(
list.keys_as_vec(),
vec![2, 1],
"Newest item (2) should be at the front"
);
assert_eq!(list.current_total_cost(), 2);
}
#[test]
fn test_access_moves_item_to_front() {
let policy = LruPolicy::<i32>::new();
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 1);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &3, 1);
assert_eq!(policy.list.lock().keys_as_vec(), vec![3, 2, 1]);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_access(&policy, &1, 1);
assert_eq!(policy.list.lock().keys_as_vec(), vec![1, 3, 2]);
}
#[test]
fn test_re_admit_existing_key_moves_to_front() {
let policy = LruPolicy::<i32>::new();
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 1);
assert_eq!(policy.list.lock().keys_as_vec(), vec![2, 1]);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
assert_eq!(
policy.list.lock().keys_as_vec(),
vec![1, 2],
"Re-admitted key should move to front"
);
}
#[test]
fn test_re_admit_updates_cost() {
let policy = LruPolicy::<i32>::new();
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 2);
assert_eq!(policy.list.lock().current_total_cost(), 3);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 10);
assert_eq!(
policy.list.lock().current_total_cost(),
12,
"Cost should be updated (10 + 2)"
);
assert_eq!(policy.list.lock().keys_as_vec(), vec![1, 2]);
}
#[test]
fn test_evict_removes_lru_item() {
let policy = LruPolicy::<i32>::new();
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1); <LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 2);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &3, 3);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_access(&policy, &2, 2);
let (victims, cost_freed) = <LruPolicy<i32> as CachePolicy<i32, ()>>::evict(&policy, 1);
assert_eq!(victims, vec![1]);
assert_eq!(cost_freed, 1);
let list = policy.list.lock();
assert_eq!(list.keys_as_vec(), vec![2, 3]);
assert_eq!(list.current_total_cost(), 5);
}
#[test]
fn test_evict_multiple_items() {
let policy = LruPolicy::<i32>::new();
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1); <LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 1); <LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &3, 1);
let (victims, cost_freed) = <LruPolicy<i32> as CachePolicy<i32, ()>>::evict(&policy, 2);
assert_eq!(victims, vec![1, 2]);
assert_eq!(cost_freed, 2);
let list = policy.list.lock();
assert_eq!(list.keys_as_vec(), vec![3]);
assert_eq!(list.current_total_cost(), 1);
}
#[test]
fn test_on_remove_cleans_up_state() {
let policy = LruPolicy::<i32>::new();
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 2);
assert_eq!(policy.list.lock().current_total_cost(), 3);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_remove(&policy, &1);
let list = policy.list.lock();
assert!(!list.contains(&1));
assert_eq!(list.keys_as_vec(), vec![2]);
assert_eq!(list.current_total_cost(), 2);
}
#[test]
fn test_clear_resets_state() {
let policy = LruPolicy::<i32>::new();
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
<LruPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 1);
<LruPolicy<i32> as CachePolicy<i32, ()>>::clear(&policy);
assert_eq!(policy.list.lock().current_total_cost(), 0);
assert!(policy.list.lock().keys_as_vec().is_empty());
}
}