use dashmap::DashMap;
use std::collections::BinaryHeap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::padded::CachePadded;
const OPTIMAL_SHARD_COUNT: usize = 128;
const EVICT_BATCH_PERCENT: usize = 2;
const EVICT_BATCH_MIN: usize = 8;
struct CacheEntry {
url: Arc<str>,
html: Arc<str>,
last_access: AtomicU64,
created_at: Instant,
}
pub struct ColdCache {
cache: DashMap<u64, CacheEntry>,
max_entries: usize,
access_counter: CachePadded<AtomicU64>,
evicting: CachePadded<AtomicBool>,
ttl: Option<Duration>,
}
impl ColdCache {
#[allow(dead_code)]
pub fn new(max_entries: usize) -> Self {
Self {
cache: DashMap::with_capacity_and_shard_amount(max_entries, OPTIMAL_SHARD_COUNT),
max_entries,
access_counter: CachePadded::new(AtomicU64::new(0)),
evicting: CachePadded::new(AtomicBool::new(false)),
ttl: None,
}
}
pub fn with_ttl(max_entries: usize, ttl_secs: u64) -> Self {
Self {
cache: DashMap::with_capacity_and_shard_amount(max_entries, OPTIMAL_SHARD_COUNT),
max_entries,
access_counter: CachePadded::new(AtomicU64::new(0)),
evicting: CachePadded::new(AtomicBool::new(false)),
ttl: if ttl_secs > 0 {
Some(Duration::from_secs(ttl_secs))
} else {
None
},
}
}
#[inline(always)]
pub fn get(&self, url_hash: u64) -> Option<Arc<str>> {
let entry = self.cache.get(&url_hash)?;
if let Some(ttl) = self.ttl {
if entry.created_at.elapsed() > ttl {
drop(entry);
self.cache.remove(&url_hash);
return None;
}
}
let new_access = self.access_counter.fetch_add(1, Ordering::Relaxed);
entry.last_access.store(new_access, Ordering::Relaxed);
Some(Arc::clone(&entry.html))
}
pub fn insert(&self, url_hash: u64, url: &str, html: Arc<str>) -> usize {
let evicted = if self.cache.len() >= self.max_entries {
self.evict_batch()
} else {
0
};
let new_access = self.access_counter.fetch_add(1, Ordering::Relaxed);
self.cache.insert(
url_hash,
CacheEntry {
url: Arc::from(url),
html,
last_access: AtomicU64::new(new_access),
created_at: Instant::now(),
},
);
evicted
}
fn evict_batch(&self) -> usize {
if self
.evicting
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
return 0;
}
let batch =
(self.max_entries * EVICT_BATCH_PERCENT / 100).max(EVICT_BATCH_MIN);
let mut heap: BinaryHeap<(u64, u64)> = BinaryHeap::with_capacity(batch + 1);
for entry in self.cache.iter() {
let access = entry.last_access.load(Ordering::Relaxed);
let key = *entry.key();
if heap.len() < batch {
heap.push((access, key));
} else if let Some(&(top_access, _)) = heap.peek() {
if access < top_access {
heap.pop();
heap.push((access, key));
}
}
}
let evicted = heap.len();
for (_, key) in heap {
self.cache.remove(&key);
}
self.evicting.store(false, Ordering::Release);
evicted
}
pub fn len(&self) -> usize {
self.cache.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn remove(&self, url_hash: u64) -> bool {
self.cache.remove(&url_hash).is_some()
}
pub fn remove_by_prefix(&self, prefix: &str) -> usize {
let mut to_remove = Vec::new();
for entry in self.cache.iter() {
if entry.url.starts_with(prefix) {
to_remove.push(*entry.key());
}
}
let count = to_remove.len();
for key in to_remove {
self.cache.remove(&key);
}
count
}
pub fn clear(&self) {
self.cache.clear();
}
pub fn capacity(&self) -> usize {
self.max_entries
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_operations() {
let cache = ColdCache::new(100);
let html: Arc<str> = "test".into();
cache.insert(123, "/test", Arc::clone(&html));
assert!(cache.get(123).is_some());
assert!(cache.get(456).is_none());
}
#[test]
fn test_eviction() {
let cache = ColdCache::new(5);
for i in 0..10 {
let html: Arc<str> = format!("html{}", i).into();
cache.insert(i, &format!("/page/{}", i), html);
}
assert!(cache.len() <= 5);
}
#[test]
fn test_batch_eviction_returns_count() {
let cache = ColdCache::new(8);
for i in 0..8 {
let html: Arc<str> = format!("html{}", i).into();
cache.insert(i, &format!("/page/{}", i), html);
}
assert_eq!(cache.len(), 8);
let evicted = cache.insert(100, "/new", "new".into());
assert!(evicted >= 1);
assert!(cache.len() < 8);
}
#[test]
fn test_remove_single() {
let cache = ColdCache::new(100);
cache.insert(1, "/a", "html_a".into());
cache.insert(2, "/b", "html_b".into());
assert!(cache.remove(1));
assert!(cache.get(1).is_none());
assert!(cache.get(2).is_some());
}
#[test]
fn test_remove_by_prefix() {
let cache = ColdCache::new(100);
cache.insert(1, "/products/1", "p1".into());
cache.insert(2, "/products/2", "p2".into());
cache.insert(3, "/products/3", "p3".into());
cache.insert(4, "/about", "about".into());
cache.insert(5, "/home", "home".into());
let removed = cache.remove_by_prefix("/products");
assert_eq!(removed, 3);
assert_eq!(cache.len(), 2);
assert!(cache.get(4).is_some());
assert!(cache.get(5).is_some());
}
}