use std::collections::{HashMap, VecDeque};
#[derive(Eq, PartialEq, Hash, Clone)]
struct CacheKey {
tenant_id: u32,
collection: String,
document_id: String,
}
pub struct DocCache {
entries: HashMap<CacheKey, Vec<u8>>,
order: VecDeque<CacheKey>,
capacity: usize,
hits: u64,
misses: u64,
}
impl DocCache {
pub fn new(capacity: usize) -> Self {
Self {
entries: HashMap::with_capacity(capacity.min(8192)),
order: VecDeque::with_capacity(capacity.min(8192)),
capacity,
hits: 0,
misses: 0,
}
}
pub fn get(&mut self, tenant_id: u32, collection: &str, document_id: &str) -> Option<&[u8]> {
let key = Self::make_key(tenant_id, collection, document_id);
if let Some(val) = self.entries.get(&key) {
self.hits += 1;
Some(val)
} else {
self.misses += 1;
None
}
}
pub fn put(&mut self, tenant_id: u32, collection: &str, document_id: &str, value: &[u8]) {
let key = Self::make_key(tenant_id, collection, document_id);
#[allow(clippy::map_entry)]
if self.entries.contains_key(&key) {
self.entries.insert(key, value.to_vec());
return;
}
while self.entries.len() >= self.capacity {
if let Some(oldest) = self.order.pop_front() {
self.entries.remove(&oldest);
} else {
break;
}
}
self.entries.insert(key.clone(), value.to_vec());
self.order.push_back(key);
}
pub fn invalidate(&mut self, tenant_id: u32, collection: &str, document_id: &str) {
let key = Self::make_key(tenant_id, collection, document_id);
self.entries.remove(&key);
}
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn total_lookups(&self) -> u64 {
self.hits + self.misses
}
fn make_key(tenant_id: u32, collection: &str, document_id: &str) -> CacheKey {
CacheKey {
tenant_id,
collection: collection.to_string(),
document_id: document_id.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_put_get() {
let mut cache = DocCache::new(16);
cache.put(1, "users", "u1", b"alice");
assert_eq!(cache.get(1, "users", "u1"), Some(b"alice".as_slice()));
assert_eq!(cache.get(1, "users", "u2"), None);
}
#[test]
fn overwrite_updates_value() {
let mut cache = DocCache::new(16);
cache.put(1, "users", "u1", b"alice");
cache.put(1, "users", "u1", b"ALICE");
assert_eq!(cache.get(1, "users", "u1"), Some(b"ALICE".as_slice()));
}
#[test]
fn invalidate_removes_entry() {
let mut cache = DocCache::new(16);
cache.put(1, "users", "u1", b"alice");
cache.invalidate(1, "users", "u1");
assert_eq!(cache.get(1, "users", "u1"), None);
}
#[test]
fn eviction_at_capacity() {
let mut cache = DocCache::new(3);
cache.put(1, "c", "a", b"1");
cache.put(1, "c", "b", b"2");
cache.put(1, "c", "c", b"3");
assert_eq!(cache.len(), 3);
cache.put(1, "c", "d", b"4");
assert_eq!(cache.len(), 3);
assert_eq!(cache.get(1, "c", "a"), None); assert_eq!(cache.get(1, "c", "d"), Some(b"4".as_slice())); }
#[test]
fn tenant_isolation() {
let mut cache = DocCache::new(16);
cache.put(1, "users", "u1", b"tenant1");
cache.put(2, "users", "u1", b"tenant2");
assert_eq!(cache.get(1, "users", "u1"), Some(b"tenant1".as_slice()));
assert_eq!(cache.get(2, "users", "u1"), Some(b"tenant2".as_slice()));
}
#[test]
fn hit_rate_tracking() {
let mut cache = DocCache::new(16);
cache.put(1, "c", "a", b"1");
cache.get(1, "c", "a"); cache.get(1, "c", "a"); cache.get(1, "c", "b");
assert!((cache.hit_rate() - 0.6667).abs() < 0.01);
assert_eq!(cache.total_lookups(), 3);
}
}