#![allow(clippy::cast_precision_loss)]
use dashmap::DashMap;
use std::hash::Hash;
use std::sync::atomic::{AtomicU64, Ordering};
use super::LruCache;
pub struct LockFreeLruCache<K, V>
where
K: Hash + Eq + Clone + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
{
l1: DashMap<K, V>,
pub(crate) l2: LruCache<K, V>,
l1_capacity: usize,
l1_hits: AtomicU64,
l2_hits: AtomicU64,
misses: AtomicU64,
l1_overflow: AtomicU64,
}
#[derive(Debug, Clone, Default)]
pub struct LockFreeCacheStats {
pub l1_hits: u64,
pub l2_hits: u64,
pub misses: u64,
pub l1_size: usize,
pub l2_size: usize,
pub l1_overflow: u64,
}
impl LockFreeCacheStats {
#[must_use]
pub fn l1_hit_rate(&self) -> f64 {
let total = self.l1_hits + self.l2_hits + self.misses;
if total == 0 {
0.0
} else {
self.l1_hits as f64 / total as f64
}
}
#[must_use]
pub fn total_hit_rate(&self) -> f64 {
let total = self.l1_hits + self.l2_hits + self.misses;
if total == 0 {
0.0
} else {
(self.l1_hits + self.l2_hits) as f64 / total as f64
}
}
}
impl<K, V> LockFreeLruCache<K, V>
where
K: Hash + Eq + Clone + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
{
#[must_use]
pub fn new(l1_capacity: usize, l2_capacity: usize) -> Self {
Self {
l1: DashMap::with_capacity(l1_capacity),
l2: LruCache::new(l2_capacity),
l1_capacity,
l1_hits: AtomicU64::new(0),
l2_hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
l1_overflow: AtomicU64::new(0),
}
}
#[must_use]
pub fn get(&self, key: &K) -> Option<V> {
if let Some(entry) = self.l1.get(key) {
self.l1_hits.fetch_add(1, Ordering::Relaxed);
return Some(entry.value().clone());
}
if let Some(value) = self.l2.get(key) {
self.l2_hits.fetch_add(1, Ordering::Relaxed);
self.promote_to_l1(key.clone(), value.clone());
return Some(value);
}
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
#[must_use]
pub fn peek_l1(&self, key: &K) -> Option<V> {
self.l1.get(key).map(|entry| entry.value().clone())
}
#[must_use]
pub fn peek_l2(&self, key: &K) -> Option<V> {
self.l2.peek(key)
}
pub fn insert(&self, key: K, value: V) {
self.l1.insert(key.clone(), value.clone());
self.maybe_evict_l1();
self.l2.insert(key, value);
}
pub fn remove(&self, key: &K) {
self.l1.remove(key);
self.l2.remove(key);
}
pub fn clear(&self) {
self.l1.clear();
self.l2.clear();
}
#[must_use]
pub fn stats(&self) -> LockFreeCacheStats {
LockFreeCacheStats {
l1_hits: self.l1_hits.load(Ordering::Relaxed),
l2_hits: self.l2_hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
l1_size: self.l1.len(),
l2_size: self.l2.len(),
l1_overflow: self.l1_overflow.load(Ordering::Relaxed),
}
}
#[must_use]
pub fn l1_capacity(&self) -> usize {
self.l1_capacity
}
#[must_use]
pub fn l2_capacity(&self) -> usize {
self.l2.capacity()
}
fn promote_to_l1(&self, key: K, value: V) {
self.l1.insert(key, value);
self.maybe_evict_l1();
}
fn maybe_evict_l1(&self) {
let mut attempts = 0;
let max_attempts = 10;
while self.l1.len() > self.l1_capacity && attempts < max_attempts {
attempts += 1;
let keys_to_remove: Vec<K> = self
.l1
.iter()
.take(self.l1.len().saturating_sub(self.l1_capacity).max(1))
.map(|entry| entry.key().clone())
.collect();
if keys_to_remove.is_empty() {
break;
}
for key in keys_to_remove {
self.l1.remove(&key);
}
}
if self.l1.len() > self.l1_capacity {
self.l1_overflow.fetch_add(1, Ordering::Relaxed);
}
}
}
impl<K, V> Default for LockFreeLruCache<K, V>
where
K: Hash + Eq + Clone + Send + Sync + 'static,
V: Clone + Send + Sync + 'static,
{
fn default() -> Self {
Self::new(1_000, 10_000)
}
}