#![allow(dead_code)]
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
pub node_id: u64,
pub port: u32,
pub generation: u64,
}
impl CacheKey {
pub fn new(node_id: u64, port: u32, generation: u64) -> Self {
Self {
node_id,
port,
generation,
}
}
}
impl std::fmt::Display for CacheKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}@{}", self.node_id, self.port, self.generation)
}
}
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub data: Vec<u8>,
pub size: usize,
pub access_count: u64,
pub created_at: u64,
pub last_accessed: u64,
}
impl CacheEntry {
pub fn new(data: Vec<u8>, timestamp: u64) -> Self {
let size = data.len();
Self {
data,
size,
access_count: 0,
created_at: timestamp,
last_accessed: timestamp,
}
}
pub fn touch(&mut self, timestamp: u64) {
self.access_count += 1;
self.last_accessed = timestamp;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvictionPolicy {
Lru,
Lfu,
Fifo,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CacheStatistics {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub invalidations: u64,
pub entry_count: usize,
pub memory_bytes: usize,
}
impl CacheStatistics {
#[allow(clippy::cast_precision_loss)]
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
return 0.0;
}
self.hits as f64 / total as f64
}
}
pub struct NodeCache {
entries: HashMap<CacheKey, CacheEntry>,
max_memory: usize,
current_memory: usize,
policy: EvictionPolicy,
clock: u64,
stats: CacheStatistics,
}
impl NodeCache {
pub fn new(max_memory: usize, policy: EvictionPolicy) -> Self {
Self {
entries: HashMap::new(),
max_memory,
current_memory: 0,
policy,
clock: 0,
stats: CacheStatistics::default(),
}
}
pub fn lru(max_memory: usize) -> Self {
Self::new(max_memory, EvictionPolicy::Lru)
}
pub fn insert(&mut self, key: CacheKey, data: Vec<u8>) {
let entry_size = data.len();
while self.current_memory + entry_size > self.max_memory && !self.entries.is_empty() {
self.evict_one();
}
if entry_size > self.max_memory {
return;
}
self.clock += 1;
let entry = CacheEntry::new(data, self.clock);
self.current_memory += entry_size;
if let Some(old) = self.entries.insert(key, entry) {
self.current_memory -= old.size;
}
self.stats.entry_count = self.entries.len();
self.stats.memory_bytes = self.current_memory;
}
pub fn get(&mut self, key: &CacheKey) -> Option<&[u8]> {
self.clock += 1;
let ts = self.clock;
if let Some(entry) = self.entries.get_mut(key) {
entry.touch(ts);
self.stats.hits += 1;
Some(&entry.data)
} else {
self.stats.misses += 1;
None
}
}
pub fn contains(&self, key: &CacheKey) -> bool {
self.entries.contains_key(key)
}
pub fn invalidate(&mut self, key: &CacheKey) -> bool {
if let Some(entry) = self.entries.remove(key) {
self.current_memory -= entry.size;
self.stats.invalidations += 1;
self.stats.entry_count = self.entries.len();
self.stats.memory_bytes = self.current_memory;
true
} else {
false
}
}
pub fn invalidate_node(&mut self, node_id: u64) {
let keys_to_remove: Vec<CacheKey> = self
.entries
.keys()
.filter(|k| k.node_id == node_id)
.cloned()
.collect();
for key in keys_to_remove {
self.invalidate(&key);
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.current_memory = 0;
self.stats.entry_count = 0;
self.stats.memory_bytes = 0;
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn memory_usage(&self) -> usize {
self.current_memory
}
pub fn statistics(&self) -> &CacheStatistics {
&self.stats
}
fn evict_one(&mut self) {
let victim = match self.policy {
EvictionPolicy::Lru => self.find_lru(),
EvictionPolicy::Lfu => self.find_lfu(),
EvictionPolicy::Fifo => self.find_fifo(),
};
if let Some(key) = victim {
if let Some(entry) = self.entries.remove(&key) {
self.current_memory -= entry.size;
self.stats.evictions += 1;
self.stats.entry_count = self.entries.len();
self.stats.memory_bytes = self.current_memory;
}
}
}
fn find_lru(&self) -> Option<CacheKey> {
self.entries
.iter()
.min_by_key(|(_, e)| e.last_accessed)
.map(|(k, _)| k.clone())
}
fn find_lfu(&self) -> Option<CacheKey> {
self.entries
.iter()
.min_by_key(|(_, e)| e.access_count)
.map(|(k, _)| k.clone())
}
fn find_fifo(&self) -> Option<CacheKey> {
self.entries
.iter()
.min_by_key(|(_, e)| e.created_at)
.map(|(k, _)| k.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_key_display() {
let key = CacheKey::new(42, 0, 7);
assert_eq!(format!("{key}"), "42:0@7");
}
#[test]
fn test_cache_insert_and_get() {
let mut cache = NodeCache::lru(1024);
let key = CacheKey::new(1, 0, 1);
cache.insert(key.clone(), vec![1, 2, 3, 4]);
let data = cache.get(&key).expect("get should succeed");
assert_eq!(data, &[1, 2, 3, 4]);
}
#[test]
fn test_cache_miss() {
let mut cache = NodeCache::lru(1024);
let key = CacheKey::new(1, 0, 1);
assert!(cache.get(&key).is_none());
assert_eq!(cache.statistics().misses, 1);
}
#[test]
fn test_cache_hit_rate() {
let mut cache = NodeCache::lru(1024);
let key = CacheKey::new(1, 0, 1);
cache.insert(key.clone(), vec![1, 2, 3]);
cache.get(&key); cache.get(&CacheKey::new(2, 0, 1)); let rate = cache.statistics().hit_rate();
assert!((rate - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_cache_eviction_lru() {
let mut cache = NodeCache::lru(10); cache.insert(CacheKey::new(1, 0, 1), vec![0; 5]);
cache.insert(CacheKey::new(2, 0, 1), vec![0; 5]);
assert_eq!(cache.len(), 2);
cache.insert(CacheKey::new(3, 0, 1), vec![0; 5]);
assert_eq!(cache.len(), 2);
assert!(!cache.contains(&CacheKey::new(1, 0, 1)));
assert!(cache.contains(&CacheKey::new(3, 0, 1)));
}
#[test]
fn test_cache_eviction_fifo() {
let mut cache = NodeCache::new(10, EvictionPolicy::Fifo);
cache.insert(CacheKey::new(1, 0, 1), vec![0; 5]);
cache.insert(CacheKey::new(2, 0, 1), vec![0; 5]);
cache.insert(CacheKey::new(3, 0, 1), vec![0; 5]);
assert!(!cache.contains(&CacheKey::new(1, 0, 1)));
}
#[test]
fn test_cache_eviction_lfu() {
let mut cache = NodeCache::new(15, EvictionPolicy::Lfu);
cache.insert(CacheKey::new(1, 0, 1), vec![0; 5]);
cache.insert(CacheKey::new(2, 0, 1), vec![0; 5]);
cache.insert(CacheKey::new(3, 0, 1), vec![0; 5]);
cache.get(&CacheKey::new(1, 0, 1));
cache.get(&CacheKey::new(1, 0, 1));
cache.get(&CacheKey::new(3, 0, 1));
cache.insert(CacheKey::new(4, 0, 1), vec![0; 5]);
assert!(!cache.contains(&CacheKey::new(2, 0, 1)));
}
#[test]
fn test_cache_invalidate() {
let mut cache = NodeCache::lru(1024);
let key = CacheKey::new(1, 0, 1);
cache.insert(key.clone(), vec![1, 2, 3]);
assert!(cache.invalidate(&key));
assert!(!cache.contains(&key));
assert_eq!(cache.statistics().invalidations, 1);
}
#[test]
fn test_cache_invalidate_node() {
let mut cache = NodeCache::lru(1024);
cache.insert(CacheKey::new(1, 0, 1), vec![1]);
cache.insert(CacheKey::new(1, 1, 1), vec![2]);
cache.insert(CacheKey::new(2, 0, 1), vec![3]);
cache.invalidate_node(1);
assert_eq!(cache.len(), 1);
assert!(cache.contains(&CacheKey::new(2, 0, 1)));
}
#[test]
fn test_cache_clear() {
let mut cache = NodeCache::lru(1024);
cache.insert(CacheKey::new(1, 0, 1), vec![1, 2]);
cache.insert(CacheKey::new(2, 0, 1), vec![3, 4]);
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.memory_usage(), 0);
}
#[test]
fn test_cache_memory_tracking() {
let mut cache = NodeCache::lru(1024);
cache.insert(CacheKey::new(1, 0, 1), vec![0; 100]);
assert_eq!(cache.memory_usage(), 100);
cache.insert(CacheKey::new(2, 0, 1), vec![0; 200]);
assert_eq!(cache.memory_usage(), 300);
cache.invalidate(&CacheKey::new(1, 0, 1));
assert_eq!(cache.memory_usage(), 200);
}
#[test]
fn test_cache_oversize_entry_not_inserted() {
let mut cache = NodeCache::lru(10);
cache.insert(CacheKey::new(1, 0, 1), vec![0; 20]);
assert!(cache.is_empty());
}
#[test]
fn test_cache_statistics_default() {
let stats = CacheStatistics::default();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert!((stats.hit_rate() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_cache_entry_touch() {
let mut entry = CacheEntry::new(vec![1, 2, 3], 10);
assert_eq!(entry.access_count, 0);
entry.touch(20);
assert_eq!(entry.access_count, 1);
assert_eq!(entry.last_accessed, 20);
}
}