use dashmap::DashMap;
use crate::invalidation_client::InvalidationSink;
pub const DEFAULT_CAPACITY: usize = 10_000;
pub struct PolicyL1Cache<V> {
entries: DashMap<String, V>,
capacity: usize,
}
impl<V> Default for PolicyL1Cache<V> {
fn default() -> Self {
Self {
entries: DashMap::new(),
capacity: DEFAULT_CAPACITY,
}
}
}
impl<V> PolicyL1Cache<V> {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
entries: DashMap::new(),
capacity: capacity.max(1),
}
}
pub fn insert(&self, agent_id: impl Into<String>, value: V) {
let key = agent_id.into();
if self.entries.len() >= self.capacity && !self.entries.contains_key(&key) {
let victim = self.entries.iter().next().map(|e| e.key().clone());
if let Some(victim) = victim {
self.entries.remove(&victim);
}
}
self.entries.insert(key, value);
}
pub fn invalidate(&self, agent_id: &str) {
self.entries.remove(agent_id);
}
pub fn invalidate_all(&self) {
self.entries.clear();
}
pub fn contains(&self, agent_id: &str) -> bool {
self.entries.contains_key(agent_id)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl<V: Clone> PolicyL1Cache<V> {
pub fn get(&self, agent_id: &str) -> Option<V> {
self.entries.get(agent_id).map(|entry| entry.value().clone())
}
}
impl<V: Send + Sync> InvalidationSink for PolicyL1Cache<V> {
fn on_policy_invalidated(&self, agent_id: &str) {
if agent_id.is_empty() {
self.invalidate_all();
} else {
self.invalidate(agent_id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_and_get_roundtrip() {
let cache: PolicyL1Cache<bool> = PolicyL1Cache::new();
cache.insert("agent-a", true);
assert_eq!(cache.get("agent-a"), Some(true));
assert!(cache.contains("agent-a"));
assert_eq!(cache.len(), 1);
}
#[test]
fn invalidate_drops_only_the_named_entry() {
let cache: PolicyL1Cache<u8> = PolicyL1Cache::new();
cache.insert("agent-a", 1);
cache.insert("agent-b", 2);
cache.invalidate("agent-a");
assert!(!cache.contains("agent-a"));
assert_eq!(cache.get("agent-b"), Some(2));
assert_eq!(cache.len(), 1);
}
#[test]
fn sink_named_agent_invalidates_one() {
let cache: PolicyL1Cache<u8> = PolicyL1Cache::new();
cache.insert("agent-a", 1);
cache.insert("agent-b", 2);
cache.on_policy_invalidated("agent-a");
assert!(!cache.contains("agent-a"));
assert!(cache.contains("agent-b"));
}
#[test]
fn insert_enforces_capacity_bound() {
let cache: PolicyL1Cache<u32> = PolicyL1Cache::with_capacity(4);
for i in 0..100u32 {
cache.insert(format!("agent-{i}"), i);
}
assert!(cache.len() <= 4, "len {} exceeded capacity", cache.len());
}
#[test]
fn insert_existing_key_does_not_evict() {
let cache: PolicyL1Cache<u32> = PolicyL1Cache::with_capacity(2);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("a", 9);
assert_eq!(cache.len(), 2);
assert_eq!(cache.get("a"), Some(9));
assert_eq!(cache.get("b"), Some(2));
}
#[test]
fn sink_empty_agent_invalidates_all() {
let cache: PolicyL1Cache<u8> = PolicyL1Cache::new();
cache.insert("agent-a", 1);
cache.insert("agent-b", 2);
cache.on_policy_invalidated("");
assert!(cache.is_empty());
}
}