use dashmap::DashMap;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_entries: usize,
pub ttl: Duration,
pub track_access: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_entries: 100_000,
ttl: Duration::from_secs(3600), track_access: true,
}
}
}
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub data: Vec<u8>,
pub created_at: Instant,
pub accessed_at: Instant,
pub access_count: u32,
}
impl CacheEntry {
fn new(data: Vec<u8>) -> Self {
let now = Instant::now();
Self {
data,
created_at: now,
accessed_at: now,
access_count: 1,
}
}
fn access(&mut self) {
self.accessed_at = Instant::now();
self.access_count = self.access_count.saturating_add(1);
}
fn is_expired(&self, ttl: Duration) -> bool {
self.created_at.elapsed() > ttl
}
}
pub struct HotCache {
map: Arc<DashMap<String, CacheEntry>>,
config: CacheConfig,
hits: AtomicU64,
misses: AtomicU64,
evictions: AtomicU64,
size: AtomicUsize,
}
impl HotCache {
pub fn new(config: CacheConfig) -> Self {
Self {
map: Arc::new(DashMap::with_capacity(config.max_entries)),
config,
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
evictions: AtomicU64::new(0),
size: AtomicUsize::new(0),
}
}
pub fn get(&self, key: &str) -> Option<CacheEntry> {
if let Some(mut entry) = self.map.get_mut(key) {
if entry.is_expired(self.config.ttl) {
drop(entry); self.map.remove(key);
self.misses.fetch_add(1, Ordering::Relaxed);
self.size.fetch_sub(1, Ordering::Relaxed);
return None;
}
if self.config.track_access {
entry.access();
}
self.hits.fetch_add(1, Ordering::Relaxed);
Some(entry.clone())
} else {
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
}
pub fn insert(&self, key: &str, data: Vec<u8>) {
if self.size.load(Ordering::Relaxed) >= self.config.max_entries {
self.evict_lru();
}
let entry = CacheEntry::new(data);
if self.map.insert(key.to_string(), entry).is_none() {
self.size.fetch_add(1, Ordering::Relaxed);
}
}
pub fn remove(&self, key: &str) -> Option<CacheEntry> {
let result = self.map.remove(key).map(|(_, v)| v);
if result.is_some() {
self.size.fetch_sub(1, Ordering::Relaxed);
}
result
}
pub fn clear(&self) {
self.map.clear();
self.size.store(0, Ordering::Relaxed);
self.evictions.fetch_add(
self.size.load(Ordering::Relaxed) as u64,
Ordering::Relaxed,
);
}
pub fn len(&self) -> usize {
self.size.load(Ordering::Relaxed)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn hit_rate(&self) -> f64 {
let hits = self.hits.load(Ordering::Relaxed);
let misses = self.misses.load(Ordering::Relaxed);
let total = hits + misses;
if total == 0 {
0.0
} else {
hits as f64 / total as f64
}
}
fn evict_lru(&self) {
let mut oldest_key: Option<String> = None;
let mut oldest_time = Instant::now();
for entry in self.map.iter() {
if entry.accessed_at < oldest_time {
oldest_time = entry.accessed_at;
oldest_key = Some(entry.key().clone());
}
}
if let Some(key) = oldest_key {
self.map.remove(&key);
self.evictions.fetch_add(1, Ordering::Relaxed);
self.size.fetch_sub(1, Ordering::Relaxed);
}
}
pub fn stats(&self) -> CacheStats {
CacheStats {
entries: self.len(),
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
evictions: self.evictions.load(Ordering::Relaxed),
hit_rate: self.hit_rate(),
}
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub entries: usize,
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub hit_rate: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_basic_operations() {
let cache = HotCache::new(CacheConfig::default());
cache.insert("key1", vec![1, 2, 3]);
assert_eq!(cache.len(), 1);
let entry = cache.get("key1");
assert!(entry.is_some());
assert_eq!(entry.unwrap().data, vec![1, 2, 3]);
let removed = cache.remove("key1");
assert!(removed.is_some());
assert_eq!(cache.len(), 0);
}
#[test]
fn test_cache_expiration() {
let _config = CacheConfig {
ttl: Duration::from_millis(100),
..Default::default()
};
let cache = HotCache::new(config);
cache.insert("key1", vec![1, 2, 3]);
assert!(cache.get("key1").is_some());
std::thread::sleep(Duration::from_millis(150));
assert!(cache.get("key1").is_none());
}
#[test]
fn test_cache_lru_eviction() {
let _config = CacheConfig {
max_entries: 2,
..Default::default()
};
let cache = HotCache::new(config);
cache.insert("key1", vec![1]);
cache.insert("key2", vec![2]);
cache.get("key1");
cache.insert("key3", vec![3]);
assert!(cache.get("key1").is_some());
assert!(cache.get("key2").is_none());
assert!(cache.get("key3").is_some());
}
#[test]
fn test_cache_concurrent_access() {
use std::sync::Arc;
use std::thread;
let cache = Arc::new(HotCache::new(CacheConfig::default()));
let mut handles = vec![];
for i in 0..10 {
let cache = cache.clone();
let handle = thread::spawn(move || {
for j in 0..100 {
let key = format!("key_{}_{}", i, j);
cache.insert(&key, vec![i as u8, j as u8]);
cache.get(&key);
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert!(cache.len() > 0);
assert!(cache.hit_rate() > 0.0);
}
#[test]
fn test_cache_hit_rate() {
let cache = HotCache::new(CacheConfig::default());
cache.insert("key1", vec![1]);
for _ in 0..9 {
cache.get("key1");
}
cache.get("key2");
let hit_rate = cache.hit_rate();
assert!((hit_rate - 0.9).abs() < 0.01);
}
}