use fibre_cache::{builder::CacheBuilder, policy::lru::LruPolicy, Cache};
use std::{thread, time::{Duration, Instant}};
const JANITOR_TICK: Duration = Duration::from_millis(50);
const JANITOR_WAIT_MULTIPLIER: u32 = 4;
const CONVERGENCE_TIMEOUT: Duration = Duration::from_secs(2);
fn wait_for_cost_convergence(cache: &Cache<i32, i32>, target_cost: u64) {
let deadline = Instant::now() + CONVERGENCE_TIMEOUT;
loop {
let current_cost = cache.metrics().current_cost;
if current_cost <= target_cost {
return; }
if Instant::now() > deadline {
panic!(
"Cache cost did not converge. Current: {}, Target: {}",
current_cost, target_cost
);
}
thread::sleep(JANITOR_TICK);
}
}
#[test]
fn test_sync_janitor_evicts_on_capacity() {
let cache = CacheBuilder::<i32, i32>::new()
.capacity(10)
.shards(1)
.cache_policy_factory(|| Box::new(LruPolicy::new()))
.janitor_tick_interval(JANITOR_TICK)
.maintenance_chance(1)
.build()
.unwrap();
for i in 0..11 {
cache.insert(i, i, 1);
}
assert_eq!(cache.metrics().current_cost, 11);
thread::sleep(JANITOR_TICK * 3);
assert_eq!(cache.metrics().current_cost, 10);
assert!(
cache.fetch(&0).is_none(),
"Key 0 should have been evicted by the janitor"
);
assert!(cache.fetch(&10).is_some());
assert_eq!(cache.metrics().evicted_by_capacity, 1);
}
#[test]
fn test_sync_insert_is_non_blocking_and_janitor_cleans_up() {
let cache = CacheBuilder::<i32, i32>::new()
.capacity(5)
.shards(1)
.cache_policy_factory(|| Box::new(LruPolicy::new())) .janitor_tick_interval(JANITOR_TICK)
.maintenance_chance(1)
.build()
.unwrap();
cache.insert(1, 1, 5);
assert_eq!(cache.metrics().current_cost, 5);
cache.insert(2, 2, 10);
assert_eq!(
cache.metrics().current_cost,
15,
"Cache should be temporarily over capacity"
);
assert!(cache.fetch(&1).is_some());
assert!(cache.fetch(&2).is_some());
thread::sleep(JANITOR_TICK * 3);
let final_cost = cache.metrics().current_cost;
assert!(
final_cost <= 5,
"Janitor should bring cost at or below capacity. Final cost: {}",
final_cost
);
let final_evictions = cache.metrics().evicted_by_capacity;
assert!(
final_evictions > 0,
"At least one item should have been evicted"
);
}
#[test]
fn test_sync_janitor_evicts_on_capacity_with_lru() {
let cache = CacheBuilder::<i32, i32>::new()
.capacity(10)
.shards(1)
.cache_policy_factory(|| Box::new(LruPolicy::new()))
.janitor_tick_interval(JANITOR_TICK)
.maintenance_chance(1)
.build()
.unwrap();
for i in 0..=10 {
cache.insert(i, i, 1);
}
assert_eq!(
cache.metrics().current_cost,
11,
"Cost should be 11 after inserting 11 items"
);
thread::sleep(JANITOR_TICK * JANITOR_WAIT_MULTIPLIER);
assert_eq!(
cache.metrics().current_cost,
10,
"Cost should be 10 (capacity) after janitor"
);
assert!(
cache.fetch(&0).is_none(), "Key 0 should have been evicted by the janitor"
);
for i in 1..=10 {
assert!(
cache.fetch(&i).is_some(),
"Key {} should still be present",
i
);
}
assert_eq!(cache.metrics().evicted_by_capacity, 1);
}
#[test]
fn test_sync_janitor_evicts_on_capacity_with_default_tinylfu() {
let cache_capacity = 3;
let cache = CacheBuilder::<i32, i32>::new()
.capacity(cache_capacity)
.shards(1)
.janitor_tick_interval(JANITOR_TICK)
.maintenance_chance(1)
.build()
.unwrap();
cache.insert(1, 10, 1);
cache.insert(2, 20, 1);
cache.insert(3, 30, 1);
assert_eq!(
cache.metrics().current_cost,
cache_capacity,
"Cache should be at capacity before overflow"
);
cache.fetch(&2);
cache.fetch(&3);
cache.insert(1, 11, 2);
let evictions_before_janitor = cache.metrics().evicted_by_capacity;
wait_for_cost_convergence(&cache, cache_capacity);
assert_eq!(
cache.metrics().current_cost,
cache_capacity,
"Janitor should bring cost back to capacity"
);
let evictions_after_janitor = cache.metrics().evicted_by_capacity;
assert_eq!(
evictions_after_janitor - evictions_before_janitor,
1,
"Janitor should evict exactly one item"
);
let item1_present = cache.fetch(&1).is_some(); let item2_present = cache.fetch(&2).is_some(); let item3_present = cache.fetch(&3).is_some();
assert!(item1_present, "Item 1 (high cost) should remain");
assert_ne!(
item2_present, item3_present,
"Exactly one of item 2 or 3 should have been evicted"
);
}
#[test]
fn test_sync_no_eviction_if_at_capacity() {
let cache_capacity = 5;
let cache = CacheBuilder::<i32, i32>::new()
.shards(1)
.capacity(cache_capacity)
.cache_policy_factory(|| Box::new(LruPolicy::new()))
.janitor_tick_interval(JANITOR_TICK)
.maintenance_chance(1)
.build()
.unwrap();
for i in 0..cache_capacity {
cache.insert(i as i32, i as i32, 1);
}
assert_eq!(
cache.metrics().current_cost,
cache_capacity,
"Cost should be at capacity"
);
let initial_evictions = cache.metrics().evicted_by_capacity;
thread::sleep(JANITOR_TICK * JANITOR_WAIT_MULTIPLIER);
assert_eq!(
cache.metrics().current_cost,
cache_capacity,
"Cost should remain at capacity"
);
assert_eq!(
cache.metrics().evicted_by_capacity,
initial_evictions,
"No new evictions should occur"
);
for i in 0..cache_capacity {
assert!(
cache.fetch(&(i as i32)).is_some(),
"Key {} should still be present",
i
);
}
}
#[test]
fn test_sync_insert_is_non_blocking_and_janitor_cleans_up_large_overflow() {
let cache_capacity = 5;
let cache = CacheBuilder::<i32, i32>::new()
.capacity(cache_capacity)
.shards(1)
.cache_policy_factory(|| Box::new(LruPolicy::new())) .janitor_tick_interval(JANITOR_TICK)
.maintenance_chance(1)
.build()
.unwrap();
cache.insert(1, 1, 1); assert_eq!(cache.metrics().current_cost, 1);
cache.insert(2, 2, 10);
assert_eq!(
cache.metrics().current_cost,
11, "Cache should be temporarily over capacity"
);
assert!(cache.fetch(&1).is_some());
assert!(cache.fetch(&2).is_some());
let evictions_before_janitor = cache.metrics().evicted_by_capacity;
thread::sleep(JANITOR_TICK * JANITOR_WAIT_MULTIPLIER);
let final_cost = cache.metrics().current_cost;
assert!(
final_cost <= cache_capacity,
"Janitor should bring cost at or below capacity. Final cost: {}",
final_cost
);
let evictions_after_janitor = cache.metrics().evicted_by_capacity;
assert!(
evictions_after_janitor > evictions_before_janitor,
"At least one item should have been evicted by janitor"
);
}