use super::CachePolicy;
use crate::policy::lru_list::LruList;
use crate::policy::AdmissionDecision;
use parking_lot::Mutex;
use std::hash::Hash;
#[derive(Debug)]
struct ArcState<K: Eq + Hash + Clone> {
p: u64,
t1: LruList<K>,
t2: LruList<K>,
b1: LruList<K>,
b2: LruList<K>,
}
impl<K: Eq + Hash + Clone> ArcState<K> {
fn new() -> Self {
Self {
p: 0,
t1: LruList::new(),
t2: LruList::new(),
b1: LruList::new(),
b2: LruList::new(),
}
}
fn replace(&mut self, capacity: u64, key_in_b2: bool) -> Option<(K, u64)> {
let t1_cost = self.t1.current_total_cost();
if t1_cost > 0 && (t1_cost >= self.p || (key_in_b2 && t1_cost == self.p)) {
if let Some((key, cost)) = self.t1.pop_back() {
self.b1.push_front(key.clone(), cost);
if self.b1.current_total_cost() > capacity {
self.b1.pop_back();
}
return Some((key, cost));
}
} else if let Some((key, cost)) = self.t2.pop_back() {
self.b2.push_front(key.clone(), cost);
if self.b2.current_total_cost() > capacity {
self.b2.pop_back();
}
return Some((key, cost));
}
None
}
}
#[derive(Debug)]
pub struct ArcPolicy<K: Eq + Hash + Clone> {
state: Mutex<ArcState<K>>,
capacity: u64,
}
impl<K: Eq + Hash + Clone> ArcPolicy<K> {
pub fn new(capacity: usize) -> Self {
Self {
state: Mutex::new(ArcState::new()),
capacity: capacity as u64,
}
}
}
impl<K, V> CachePolicy<K, V> for ArcPolicy<K>
where
K: Eq + Hash + Clone + Send + Sync,
V: Send + Sync,
{
fn on_access(&self, key: &K, cost: u64) {
let mut state = self.state.lock();
if state.t1.remove(key).is_some() {
state.t2.push_front(key.clone(), cost);
return;
}
if state.t2.contains(key) {
state.t2.push_front(key.clone(), cost);
}
}
fn on_admit(&self, key: &K, cost: u64) -> AdmissionDecision<K> {
let mut state = self.state.lock();
if state.t1.remove(key).is_some() {
state.t2.push_front(key.clone(), cost);
return AdmissionDecision::Admit;
}
if state.t2.contains(key) {
state.t2.push_front(key.clone(), cost);
return AdmissionDecision::Admit;
}
let mut key_in_b2 = false;
if state.b1.remove(key).is_some() {
let b2_cost = state.b2.current_total_cost();
let b1_cost = state.b1.current_total_cost();
let delta = if b1_cost > 0 && b2_cost > b1_cost {
(b2_cost as f64 / b1_cost as f64).round() as u64
} else {
1
}
.max(1);
state.p = (state.p + delta).min(self.capacity);
} else if state.b2.remove(key).is_some() {
key_in_b2 = true;
let b1_cost = state.b1.current_total_cost();
let b2_cost = state.b2.current_total_cost();
let delta = if b2_cost > 0 && b1_cost > b2_cost {
(b1_cost as f64 / b2_cost as f64).round() as u64
} else {
1
}
.max(1);
state.p = state.p.saturating_sub(delta);
}
let t2_cost = state.t2.current_total_cost();
if state.t1.current_total_cost() + t2_cost >= self.capacity {
state.replace(self.capacity, key_in_b2);
}
state.t1.push_front(key.clone(), cost);
AdmissionDecision::Admit
}
fn on_remove(&self, key: &K) {
let mut state = self.state.lock();
if state.t1.remove(key).is_some() {
return;
}
if state.t2.remove(key).is_some() {
return;
}
if state.b1.remove(key).is_some() {
return;
}
state.b2.remove(key);
}
fn evict(&self, cost_to_free: u64) -> (Vec<K>, u64) {
let mut state = self.state.lock();
let mut victims = Vec::new();
let mut total_cost_freed = 0;
while total_cost_freed < cost_to_free {
let tail_key = state.t1.tail.map(|idx| state.t1.nodes[idx].key.clone());
let key_in_b2 = tail_key.map_or(false, |k| state.b2.contains(&k));
if let Some((key, cost)) = state.replace(self.capacity, key_in_b2) {
total_cost_freed += cost;
victims.push(key);
} else {
break;
}
}
(victims, total_cost_freed)
}
fn clear(&self) {
let mut state = self.state.lock();
state.p = 0;
state.t1.clear();
state.t2.clear();
state.b1.clear();
state.b2.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_item_goes_to_t1() {
let policy = ArcPolicy::<i32>::new(10);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
assert!(policy.state.lock().t1.contains(&1));
assert!(!policy.state.lock().t2.contains(&1));
assert_eq!(policy.state.lock().t1.current_total_cost(), 1);
}
#[test]
fn test_access_promotes_from_t1_to_t2() {
let policy = ArcPolicy::<i32>::new(10);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1); <ArcPolicy<i32> as CachePolicy<i32, ()>>::on_access(&policy, &1, 1);
assert!(!policy.state.lock().t1.contains(&1));
assert!(policy.state.lock().t2.contains(&1));
assert_eq!(policy.state.lock().t2.current_total_cost(), 1);
}
#[test]
fn test_re_admit_acts_as_access() {
let policy = ArcPolicy::<i32>::new(10);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1); <ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 2);
assert!(!policy.state.lock().t1.contains(&1));
assert!(policy.state.lock().t2.contains(&1));
assert_eq!(
policy.state.lock().t2.current_total_cost(),
2,
"Cost should be updated"
);
}
#[test]
fn test_eviction_from_t1() {
let policy = ArcPolicy::<i32>::new(2);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &3, 1);
assert!(!policy.state.lock().t1.contains(&1));
assert!(policy.state.lock().t1.contains(&2)); assert!(policy.state.lock().t1.contains(&3)); assert!(
policy.state.lock().b1.contains(&1),
"Evicted key should be in B1 ghost list"
);
}
#[test]
fn test_eviction_from_t2() {
let policy = ArcPolicy::<i32>::new(2);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_access(&policy, &1, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_access(&policy, &2, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &3, 1);
assert!(!policy.state.lock().t2.contains(&1));
assert!(policy.state.lock().t2.contains(&2)); assert!(policy.state.lock().t1.contains(&3)); assert!(
policy.state.lock().b2.contains(&1),
"Evicted key should be in B2 ghost list"
);
}
#[test]
fn test_adaptive_p_grows_on_b1_hit() {
let policy = ArcPolicy::<i32>::new(2);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &3, 1);
assert_eq!(policy.state.lock().p, 0);
assert!(policy.state.lock().b1.contains(&1));
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
assert_eq!(policy.state.lock().p, 1, "p should increase after a B1 hit");
assert!(
!policy.state.lock().b1.contains(&1),
"Ghost item should be consumed"
);
}
#[test]
fn test_adaptive_p_shrinks_on_b2_hit() {
let policy = ArcPolicy::<i32>::new(2);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_access(&policy, &1, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_access(&policy, &2, 1);
policy.state.lock().p = 1;
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &3, 1);
assert_eq!(policy.state.lock().p, 1);
assert!(policy.state.lock().b2.contains(&1));
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1);
assert_eq!(policy.state.lock().p, 0, "p should decrease after a B2 hit");
assert!(
!policy.state.lock().b2.contains(&1),
"Ghost item should be consumed"
);
}
#[test]
fn test_on_remove_cleans_up_all_lists() {
let policy = ArcPolicy::<i32>::new(10);
policy.state.lock().t1.push_front(1, 1);
policy.state.lock().t2.push_front(2, 1);
policy.state.lock().b1.push_front(3, 1);
policy.state.lock().b2.push_front(4, 1);
assert!(policy.state.lock().t1.contains(&1));
assert!(policy.state.lock().t2.contains(&2));
assert!(policy.state.lock().b1.contains(&3));
assert!(policy.state.lock().b2.contains(&4));
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_remove(&policy, &1); assert!(!policy.state.lock().t1.contains(&1));
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_remove(&policy, &2); assert!(!policy.state.lock().t2.contains(&2));
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_remove(&policy, &3); assert!(!policy.state.lock().b1.contains(&3));
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_remove(&policy, &4); assert!(!policy.state.lock().b2.contains(&4));
}
#[test]
fn test_evict_frees_enough_cost() {
let policy = ArcPolicy::<i32>::new(5);
for i in 1..=5 {
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &i, 1);
}
assert_eq!(policy.state.lock().t1.current_total_cost(), 5);
let (victims, cost_freed) = <ArcPolicy<i32> as CachePolicy<i32, ()>>::evict(&policy, 3);
assert_eq!(cost_freed, 3, "Should free exactly 3 cost");
assert_eq!(victims.len(), 3);
assert_eq!(victims, vec![1, 2, 3], "Should evict LRU items from T1");
assert_eq!(policy.state.lock().t1.current_total_cost(), 2);
assert!(policy.state.lock().t1.contains(&4));
assert!(policy.state.lock().t1.contains(&5));
}
#[test]
fn test_clear_empties_all_lists() {
let policy = ArcPolicy::<i32>::new(4);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &1, 1); <ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &2, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_access(&policy, &2, 1); <ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &3, 1);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::on_admit(&policy, &4, 1);
assert!(
policy.state.lock().p > 0
|| policy.state.lock().t1.current_total_cost() > 0
|| policy.state.lock().t2.current_total_cost() > 0
|| policy.state.lock().b1.current_total_cost() > 0
);
<ArcPolicy<i32> as CachePolicy<i32, ()>>::clear(&policy);
assert_eq!(policy.state.lock().p, 0);
assert_eq!(policy.state.lock().t1.current_total_cost(), 0);
assert_eq!(policy.state.lock().t2.current_total_cost(), 0);
assert_eq!(policy.state.lock().b1.current_total_cost(), 0);
assert_eq!(policy.state.lock().b2.current_total_cost(), 0);
}
}