use std::collections::HashMap;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct L1CacheStats {
pub hits: u64,
pub misses: u64,
pub entry_count: usize,
pub evict_count: u64,
}
impl L1CacheStats {
pub fn total_lookups(&self) -> u64 {
self.hits + self.misses
}
pub fn hit_rate(&self) -> f64 {
let total = self.total_lookups();
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
pub struct L1Cache<T> {
data: HashMap<i64, Arc<T>>,
lru_order: VecDeque<i64>,
capacity: usize,
hits: AtomicU64,
misses: AtomicU64,
evicts: AtomicU64,
}
impl<T> L1Cache<T> {
pub fn new(capacity: usize) -> Self {
Self {
data: HashMap::with_capacity(capacity),
lru_order: VecDeque::with_capacity(capacity),
capacity: capacity.max(1),
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
evicts: AtomicU64::new(0),
}
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn put(&mut self, key: i64, value: Arc<T>) {
if let std::collections::hash_map::Entry::Occupied(mut e) = self.data.entry(key) {
e.insert(value);
self.touch_lru(key);
return;
}
if self.data.len() >= self.capacity {
if let Some(victim) = self.lru_order.pop_front() {
self.data.remove(&victim);
self.evicts.fetch_add(1, Ordering::Relaxed);
}
}
self.data.insert(key, value);
self.lru_order.push_back(key);
}
pub fn get(&mut self, key: &i64) -> Option<Arc<T>> {
if let Some(value) = self.data.get(key).map(Arc::clone) {
self.hits.fetch_add(1, Ordering::Relaxed);
self.touch_lru(*key);
Some(value)
} else {
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
}
pub fn evict(&mut self, key: &i64) {
if self.data.remove(key).is_some() {
self.lru_order.retain(|k| k != key);
}
}
pub fn clear(&mut self) {
self.data.clear();
self.lru_order.clear();
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn stats(&self) -> L1CacheStats {
L1CacheStats {
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
entry_count: self.data.len(),
evict_count: self.evicts.load(Ordering::Relaxed),
}
}
fn touch_lru(&mut self, key: i64) {
self.lru_order.retain(|k| *k != key);
self.lru_order.push_back(key);
}
}
impl<T> Default for L1Cache<T> {
fn default() -> Self {
Self::new(1024)
}
}
pub struct L1L2Coordinator<T: Clone> {
l1: L1Cache<T>,
l2: Option<std::sync::Arc<crate::l2_cache::L2Cache>>,
}
impl<T: Clone> L1L2Coordinator<T> {
pub fn new(l1_capacity: usize) -> Self {
Self {
l1: L1Cache::new(l1_capacity),
l2: None,
}
}
pub fn with_l2(mut self, l2: std::sync::Arc<crate::l2_cache::L2Cache>) -> Self {
self.l2 = Some(l2);
self
}
pub fn get_or_load<F>(&mut self, table: &str, pk: i64, db_loader: F) -> Option<Arc<T>>
where
F: FnOnce() -> Option<T>,
{
if let Some(val) = self.l1.get(&pk) {
return Some(val);
}
if let Some(l2) = &self.l2 {
let l2_key = crate::l2_cache::CacheKey::by_pk(table, pk);
if let Some(crate::value::Value::String(s)) = l2.get(&l2_key) {
let val = Arc::new(T::clone(&db_loader().unwrap()));
let _ = s;
self.l1.put(pk, val.clone());
return Some(val);
}
}
if let Some(val) = db_loader() {
let arc_val = Arc::new(val);
self.l1.put(pk, arc_val.clone());
if let Some(l2) = &self.l2 {
let l2_key = crate::l2_cache::CacheKey::by_pk(table, pk);
l2.put(
&l2_key,
crate::value::Value::String(format!("{}", pk)),
None,
);
}
return Some(arc_val);
}
None
}
pub fn invalidate(&mut self, pk: i64) {
self.l1.evict(&pk);
}
pub fn clear(&mut self) {
self.l1.clear();
}
pub fn l1_stats(&self) -> L1CacheStats {
self.l1.stats()
}
pub fn l1_mut(&mut self) -> &mut L1Cache<T> {
&mut self.l1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identity_map_same_ptr() {
let mut cache: L1Cache<String> = L1Cache::new(10);
cache.put(1, Arc::new("Alice".to_string()));
let a = cache.get(&1).unwrap();
let b = cache.get(&1).unwrap();
assert!(
Arc::ptr_eq(&a, &b),
"Identity Map: same key must return same Arc ptr"
);
}
#[test]
fn test_identity_map_different_keys_different_ptrs() {
let mut cache: L1Cache<String> = L1Cache::new(10);
cache.put(1, Arc::new("Alice".to_string()));
cache.put(2, Arc::new("Bob".to_string()));
let a = cache.get(&1).unwrap();
let b = cache.get(&2).unwrap();
assert!(
!Arc::ptr_eq(&a, &b),
"Different keys should return different Arc ptrs"
);
}
#[test]
fn test_lru_eviction() {
let mut cache: L1Cache<i32> = L1Cache::new(3);
cache.put(1, Arc::new(10));
cache.put(2, Arc::new(20));
cache.put(3, Arc::new(30));
assert_eq!(cache.len(), 3);
cache.put(4, Arc::new(40));
assert_eq!(cache.len(), 3);
assert!(cache.get(&1).is_none(), "key=1 should be evicted (LRU)");
assert!(cache.get(&4).is_some());
let stats = cache.stats();
assert!(stats.evict_count >= 1, "evict count should be >= 1");
}
#[test]
fn test_lru_touch_on_get() {
let mut cache: L1Cache<i32> = L1Cache::new(3);
cache.put(1, Arc::new(10));
cache.put(2, Arc::new(20));
cache.put(3, Arc::new(30));
let _ = cache.get(&1);
cache.put(4, Arc::new(40));
assert!(
cache.get(&1).is_some(),
"key=1 should still exist (was accessed)"
);
assert!(
cache.get(&2).is_none(),
"key=2 should be evicted (LRU after touch)"
);
}
#[test]
fn test_stats_hits_misses() {
let mut cache: L1Cache<String> = L1Cache::new(10);
cache.put(1, Arc::new("Alice".to_string()));
let _ = cache.get(&1); let _ = cache.get(&1); let _ = cache.get(&99);
let stats = cache.stats();
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
assert_eq!(stats.entry_count, 1);
assert_eq!(stats.evict_count, 0);
}
#[test]
fn test_stats_hit_rate() {
let mut cache: L1Cache<i32> = L1Cache::new(10);
cache.put(1, Arc::new(100));
let _ = cache.get(&1); let _ = cache.get(&2); let _ = cache.get(&1);
let stats = cache.stats();
assert_eq!(stats.total_lookups(), 3);
assert!((stats.hit_rate() - 2.0 / 3.0).abs() < 1e-9);
}
#[test]
fn test_session_drop_clears_cache() {
let stats;
{
let mut cache: L1Cache<String> = L1Cache::new(10);
cache.put(1, Arc::new("Alice".to_string()));
assert_eq!(cache.len(), 1);
stats = cache.stats();
}
assert_eq!(stats.entry_count, 1); }
#[test]
fn test_different_sessions_isolated() {
let mut cache_a: L1Cache<String> = L1Cache::new(10);
let mut cache_b: L1Cache<String> = L1Cache::new(10);
cache_a.put(1, Arc::new("from_session_a".to_string()));
cache_b.put(1, Arc::new("from_session_b".to_string()));
let a = cache_a.get(&1).unwrap();
let b = cache_b.get(&1).unwrap();
assert_eq!(*a, "from_session_a");
assert_eq!(*b, "from_session_b");
assert!(
!Arc::ptr_eq(&a, &b),
"Different sessions should have isolated caches"
);
}
#[test]
fn test_evict_single_key() {
let mut cache: L1Cache<String> = L1Cache::new(10);
cache.put(1, Arc::new("Alice".to_string()));
cache.put(2, Arc::new("Bob".to_string()));
cache.evict(&1);
assert!(cache.get(&1).is_none(), "key=1 should be evicted");
assert!(cache.get(&2).is_some(), "key=2 should still exist");
}
#[test]
fn test_clear_all() {
let mut cache: L1Cache<String> = L1Cache::new(10);
cache.put(1, Arc::new("Alice".to_string()));
cache.put(2, Arc::new("Bob".to_string()));
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn test_write_operation_evict() {
let mut cache: L1Cache<String> = L1Cache::new(10);
cache.put(1, Arc::new("Alice".to_string()));
cache.evict(&1);
let result = cache.get(&1);
assert!(result.is_none(), "After write evict, get should miss");
let stats = cache.stats();
assert_eq!(stats.misses, 1);
}
#[test]
fn test_object_consistency_after_update() {
let mut cache: L1Cache<String> = L1Cache::new(10);
cache.put(1, Arc::new("original".to_string()));
let a = cache.get(&1).unwrap();
assert_eq!(*a, "original");
cache.put(1, Arc::new("updated".to_string()));
let b = cache.get(&1).unwrap();
assert_eq!(*b, "updated");
assert_eq!(*a, "original");
assert_eq!(*b, "updated");
}
#[test]
fn test_atomic_stats_thread_safe() {
use std::sync::Arc;
use std::thread;
let cache = Arc::new(std::sync::Mutex::new(L1Cache::<i32>::new(100)));
let mut handles = Vec::new();
for i in 0..4 {
let cache_clone = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let mut cache = cache_clone.lock().unwrap();
cache.put(i, Arc::new(i as i32));
let _ = cache.get(&i);
}));
}
for h in handles {
h.join().unwrap();
}
let cache = cache.lock().unwrap();
let stats = cache.stats();
assert_eq!(stats.entry_count, 4);
assert!(stats.hits >= 4);
}
#[test]
fn test_l1_l2_db_query_order() {
let mut coord: L1L2Coordinator<String> = L1L2Coordinator::new(10);
let db_call_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let db_count_clone = Arc::clone(&db_call_count);
let result = coord.get_or_load("users", 1, || {
db_count_clone.fetch_add(1, Ordering::Relaxed);
Some("Alice".to_string())
});
assert_eq!(*result.unwrap(), "Alice");
assert_eq!(
db_call_count.load(Ordering::Relaxed),
1,
"DB should be called once"
);
let db_count_clone2 = Arc::clone(&db_call_count);
let result2 = coord.get_or_load("users", 1, || {
db_count_clone2.fetch_add(1, Ordering::Relaxed);
Some("Alice".to_string())
});
assert_eq!(*result2.unwrap(), "Alice");
assert_eq!(
db_call_count.load(Ordering::Relaxed),
1,
"DB should NOT be called again (L1 hit)"
);
}
#[test]
fn test_l1_l2_db_invalidate_after_write() {
let mut coord: L1L2Coordinator<String> = L1L2Coordinator::new(10);
let result = coord.get_or_load("users", 1, || Some("Alice".to_string()));
assert_eq!(*result.unwrap(), "Alice");
coord.invalidate(1);
let result2 = coord.get_or_load("users", 1, || Some("Bob".to_string()));
assert_eq!(
*result2.unwrap(),
"Bob",
"After invalidate, should reload from DB"
);
}
#[test]
fn test_capacity_one() {
let mut cache: L1Cache<i32> = L1Cache::new(1);
cache.put(1, Arc::new(10));
cache.put(2, Arc::new(20));
assert!(
cache.get(&1).is_none(),
"key=1 should be evicted (capacity=1)"
);
assert!(cache.get(&2).is_some());
}
#[test]
fn test_empty_cache_get() {
let mut cache: L1Cache<i32> = L1Cache::new(10);
assert!(cache.get(&1).is_none());
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn test_default_capacity() {
let cache: L1Cache<i32> = L1Cache::default();
assert_eq!(cache.capacity(), 1024);
}
}