use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap};
use std::sync::{Arc, Mutex, MutexGuard};
use tracing::warn;
pub type RouteCacheEntry = (String, HashMap<String, String>);
#[derive(Clone)]
pub struct RouteCache {
inner: Arc<Mutex<LruCache>>,
}
impl RouteCache {
fn lock_inner(&self) -> MutexGuard<'_, LruCache> {
self.inner.lock().unwrap_or_else(|e| {
warn!("RouteCache Mutex poisoned, recovering with inner value");
e.into_inner()
})
}
pub fn new(capacity: usize) -> Self {
Self {
inner: Arc::new(Mutex::new(LruCache::new(capacity, None))),
}
}
pub fn with_max_memory(capacity: usize, max_memory_bytes: usize) -> Self {
Self {
inner: Arc::new(Mutex::new(LruCache::new(capacity, Some(max_memory_bytes)))),
}
}
pub fn get(&self, path: &str) -> Option<RouteCacheEntry> {
let mut inner = self.lock_inner();
inner.get(path)
}
pub fn put(&self, path: &str, entry: RouteCacheEntry) {
let mut inner = self.lock_inner();
inner.put(path.to_string(), entry);
}
pub fn clear(&self) {
let mut inner = self.lock_inner();
inner.clear();
}
pub fn len(&self) -> usize {
let inner = self.lock_inner();
inner.len()
}
pub fn is_empty(&self) -> bool {
let inner = self.lock_inner();
inner.is_empty()
}
pub fn capacity(&self) -> usize {
let inner = self.lock_inner();
inner.capacity()
}
pub fn estimated_memory(&self) -> usize {
let inner = self.lock_inner();
inner.estimated_memory
}
}
impl std::fmt::Debug for RouteCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RouteCache")
.field("capacity", &self.capacity())
.field("len", &self.len())
.field("estimated_memory", &self.estimated_memory())
.finish()
}
}
struct LruCache {
capacity: usize,
max_memory_bytes: Option<usize>,
estimated_memory: usize,
map: HashMap<String, (RouteCacheEntry, usize)>, heap: BinaryHeap<Reverse<(usize, String)>>, access_counter: usize,
}
const HEAP_COMPACTION_FACTOR: usize = 4;
impl LruCache {
fn new(capacity: usize, max_memory_bytes: Option<usize>) -> Self {
Self {
capacity,
max_memory_bytes,
estimated_memory: 0,
map: HashMap::new(),
heap: BinaryHeap::new(),
access_counter: 0,
}
}
fn estimate_entry_size(key: &str, entry: &RouteCacheEntry) -> usize {
let mut size = key.len() + entry.0.len();
for (k, v) in &entry.1 {
size += k.len() + v.len();
}
size
}
fn get(&mut self, path: &str) -> Option<RouteCacheEntry> {
if let Some((entry, order)) = self.map.get_mut(path) {
self.access_counter += 1;
*order = self.access_counter;
self.heap
.push(Reverse((self.access_counter, path.to_string())));
Some(entry.clone())
} else {
None
}
}
fn put(&mut self, path: String, entry: RouteCacheEntry) {
let new_entry_size = Self::estimate_entry_size(&path, &entry);
if let Some((old_entry, _)) = self.map.get(&path) {
self.estimated_memory -= Self::estimate_entry_size(&path, old_entry);
}
while !self.map.contains_key(&path) && self.map.len() >= self.capacity {
self.evict_lru();
}
if let Some(max_bytes) = self.max_memory_bytes {
while self.estimated_memory + new_entry_size > max_bytes && !self.map.is_empty() {
self.evict_lru();
}
}
self.access_counter += 1;
self.estimated_memory += new_entry_size;
self.heap.push(Reverse((self.access_counter, path.clone())));
self.map.insert(path, (entry, self.access_counter));
if self.heap.len() > self.map.len().saturating_mul(HEAP_COMPACTION_FACTOR) {
self.compact_heap();
}
}
fn compact_heap(&mut self) {
let mut new_heap = BinaryHeap::with_capacity(self.map.len());
for (key, (_, access_time)) in &self.map {
new_heap.push(Reverse((*access_time, key.clone())));
}
self.heap = new_heap;
}
fn evict_lru(&mut self) {
while let Some(Reverse((access_time, key))) = self.heap.pop() {
if let Some((_, current_access_time)) = self.map.get(&key)
&& *current_access_time == access_time
{
if let Some((entry, _)) = self.map.remove(&key) {
self.estimated_memory -= Self::estimate_entry_size(&key, &entry);
}
return;
}
}
}
fn clear(&mut self) {
self.map.clear();
self.heap.clear();
self.access_counter = 0;
self.estimated_memory = 0;
}
fn len(&self) -> usize {
self.map.len()
}
fn is_empty(&self) -> bool {
self.map.is_empty()
}
fn capacity(&self) -> usize {
self.capacity
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_route_cache_new() {
let cache = RouteCache::new(100);
assert_eq!(cache.capacity(), 100);
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
#[test]
fn test_route_cache_put_and_get() {
let cache = RouteCache::new(100);
let mut params = HashMap::new();
params.insert("id".to_string(), "123".to_string());
cache.put("/users/123/", ("user-detail".to_string(), params.clone()));
let result = cache.get("/users/123/");
assert!(result.is_some());
let (handler_id, cached_params) = result.unwrap();
assert_eq!(handler_id, "user-detail");
assert_eq!(cached_params.get("id"), Some(&"123".to_string()));
}
#[test]
fn test_route_cache_miss() {
let cache = RouteCache::new(100);
assert!(cache.get("/nonexistent/").is_none());
}
#[test]
fn test_route_cache_clear() {
let cache = RouteCache::new(100);
cache.put("/users/", ("users".to_string(), HashMap::new()));
assert_eq!(cache.len(), 1);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
#[test]
fn test_route_cache_lru_eviction() {
let cache = RouteCache::new(2);
cache.put("/route1/", ("handler1".to_string(), HashMap::new()));
cache.put("/route2/", ("handler2".to_string(), HashMap::new()));
assert_eq!(cache.len(), 2);
cache.put("/route3/", ("handler3".to_string(), HashMap::new()));
assert_eq!(cache.len(), 2);
assert!(cache.get("/route1/").is_none());
assert!(cache.get("/route2/").is_some());
assert!(cache.get("/route3/").is_some());
}
#[test]
fn test_route_cache_lru_access_order() {
let cache = RouteCache::new(2);
cache.put("/route1/", ("handler1".to_string(), HashMap::new()));
cache.put("/route2/", ("handler2".to_string(), HashMap::new()));
let _ = cache.get("/route1/");
cache.put("/route3/", ("handler3".to_string(), HashMap::new()));
assert!(cache.get("/route1/").is_some());
assert!(cache.get("/route2/").is_none());
assert!(cache.get("/route3/").is_some());
}
#[test]
fn test_route_cache_update_existing() {
let cache = RouteCache::new(2);
cache.put("/users/", ("handler1".to_string(), HashMap::new()));
cache.put("/posts/", ("handler2".to_string(), HashMap::new()));
let mut new_params = HashMap::new();
new_params.insert("new".to_string(), "param".to_string());
cache.put("/users/", ("handler1_updated".to_string(), new_params));
assert_eq!(cache.len(), 2);
assert!(cache.get("/users/").is_some());
assert!(cache.get("/posts/").is_some());
}
#[test]
fn test_route_cache_memory_bound_eviction() {
let cache = RouteCache::with_max_memory(100, 50);
let mut params = HashMap::new();
params.insert("long_key".to_string(), "long_value_data".to_string());
cache.put("/route1/", ("handler1".to_string(), params.clone()));
cache.put("/route2/", ("handler2".to_string(), params.clone()));
assert!(cache.len() <= 2);
assert!(cache.estimated_memory() <= 50 || cache.len() == 1);
}
#[test]
fn test_route_cache_estimated_memory_tracking() {
let cache = RouteCache::new(100);
assert_eq!(cache.estimated_memory(), 0);
cache.put("/users/", ("users".to_string(), HashMap::new()));
let mem_after_first = cache.estimated_memory();
assert!(mem_after_first > 0);
cache.put("/posts/", ("posts".to_string(), HashMap::new()));
assert!(cache.estimated_memory() > mem_after_first);
cache.clear();
assert_eq!(cache.estimated_memory(), 0);
}
#[test]
fn test_route_cache_thread_safety() {
use std::thread;
let cache = RouteCache::new(100);
let cache_clone = cache.clone();
let handle = thread::spawn(move || {
cache_clone.put("/thread1/", ("handler1".to_string(), HashMap::new()));
});
cache.put("/main/", ("handler_main".to_string(), HashMap::new()));
handle.join().unwrap();
assert!(cache.get("/main/").is_some());
assert!(cache.get("/thread1/").is_some());
}
}