use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::RwLock;
use crate::layout::BlobGuid;
use super::walker::SearchKey;
const ROUTE_CACHE_CAPACITY: usize = 16_384;
const ROUTE_PREFIX_MAX: usize = 96;
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct RouteCacheSnapshot {
pub(crate) entries: usize,
pub(crate) hits: u64,
pub(crate) misses: u64,
pub(crate) learns: u64,
pub(crate) evictions: u64,
pub(crate) invalidations: u64,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct RouteHit {
pub(crate) child_guid: BlobGuid,
pub(crate) child_depth: usize,
}
#[derive(Debug, Clone)]
struct RouteEntry {
child_guid: BlobGuid,
child_depth: usize,
}
#[derive(Debug)]
struct RouteEntries {
map: HashMap<Vec<u8>, RouteEntry>,
order: Vec<Vec<u8>>,
lengths: Vec<usize>,
}
#[derive(Debug)]
pub(crate) struct RouteCache {
root_version: AtomicU64,
entries: RwLock<RouteEntries>,
replace_cursor: AtomicUsize,
hits: AtomicU64,
misses: AtomicU64,
learns: AtomicU64,
evictions: AtomicU64,
invalidations: AtomicU64,
}
impl Default for RouteCache {
fn default() -> Self {
Self::new()
}
}
impl RouteCache {
#[must_use]
pub(crate) fn new() -> Self {
Self {
root_version: AtomicU64::new(u64::MAX),
entries: RwLock::new(RouteEntries {
map: HashMap::with_capacity(ROUTE_CACHE_CAPACITY),
order: Vec::with_capacity(ROUTE_CACHE_CAPACITY),
lengths: Vec::new(),
}),
replace_cursor: AtomicUsize::new(0),
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
learns: AtomicU64::new(0),
evictions: AtomicU64::new(0),
invalidations: AtomicU64::new(0),
}
}
#[must_use]
pub(crate) fn stats(&self) -> RouteCacheSnapshot {
RouteCacheSnapshot {
entries: self.entries.read().unwrap().map.len(),
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
learns: self.learns.load(Ordering::Relaxed),
evictions: self.evictions.load(Ordering::Relaxed),
invalidations: self.invalidations.load(Ordering::Relaxed),
}
}
#[must_use]
pub(crate) fn lookup(&self, key: SearchKey<'_>, root_version: u64) -> Option<RouteHit> {
if self.root_version.load(Ordering::Acquire) != root_version {
self.misses.fetch_add(1, Ordering::Relaxed);
return None;
}
let entries = self.entries.read().unwrap();
for &len in &entries.lengths {
if let Some(prefix) = key.user_prefix(len) {
if let Some(entry) = entries.map.get(prefix) {
self.hits.fetch_add(1, Ordering::Relaxed);
return Some(RouteHit {
child_guid: entry.child_guid,
child_depth: entry.child_depth,
});
}
}
}
self.misses.fetch_add(1, Ordering::Relaxed);
None
}
pub(crate) fn learn(
&self,
key: SearchKey<'_>,
root_version: u64,
child_guid: BlobGuid,
child_depth: usize,
) {
let Some(prefix) = key.user_prefix(child_depth) else {
return;
};
if prefix.len() > ROUTE_PREFIX_MAX {
return;
}
let mut entries = self.entries.write().unwrap();
if self.root_version.load(Ordering::Relaxed) != root_version {
if !entries.map.is_empty() {
self.invalidations
.fetch_add(entries.map.len() as u64, Ordering::Relaxed);
}
entries.map.clear();
entries.order.clear();
entries.lengths.clear();
self.replace_cursor.store(0, Ordering::Relaxed);
self.root_version.store(root_version, Ordering::Release);
}
if let Some(entry) = entries.map.get_mut(prefix) {
entry.child_guid = child_guid;
entry.child_depth = child_depth;
self.learns.fetch_add(1, Ordering::Relaxed);
return;
}
let entry = RouteEntry {
child_guid,
child_depth,
};
remember_prefix_len(&mut entries.lengths, prefix.len());
self.learns.fetch_add(1, Ordering::Relaxed);
if entries.map.len() < ROUTE_CACHE_CAPACITY {
entries.order.push(prefix.to_vec());
entries.map.insert(prefix.to_vec(), entry);
return;
}
let idx = self.replace_cursor.fetch_add(1, Ordering::Relaxed) % ROUTE_CACHE_CAPACITY;
let new_prefix = prefix.to_vec();
let old = std::mem::replace(&mut entries.order[idx], new_prefix.clone());
entries.map.remove(old.as_slice());
entries.map.insert(new_prefix, entry);
self.evictions.fetch_add(1, Ordering::Relaxed);
}
}
fn remember_prefix_len(lengths: &mut Vec<usize>, len: usize) {
match lengths.binary_search_by(|known| known.cmp(&len).reverse()) {
Ok(_) => {}
Err(idx) => lengths.insert(idx, len),
}
}
#[cfg(test)]
mod tests {
use super::*;
const CHILD: BlobGuid = [7; 16];
#[test]
fn learns_and_matches_longest_prefix_for_same_root_version() {
let cache = RouteCache::new();
cache.learn(SearchKey::user(b"bucket-01/a"), 3, [1; 16], 10);
cache.learn(SearchKey::user(b"bucket-01/path/file"), 3, CHILD, 15);
let hit = cache
.lookup(SearchKey::user(b"bucket-01/path/other"), 3)
.unwrap();
assert_eq!(hit.child_guid, CHILD);
assert_eq!(hit.child_depth, 15);
}
#[test]
fn root_version_mismatch_misses() {
let cache = RouteCache::new();
cache.learn(SearchKey::user(b"bucket-01/path/file"), 3, CHILD, 15);
assert!(cache
.lookup(SearchKey::user(b"bucket-01/path/other"), 4)
.is_none());
}
#[test]
fn new_root_version_drops_old_routes() {
let cache = RouteCache::new();
cache.learn(SearchKey::user(b"bucket-01/path/file"), 3, CHILD, 15);
cache.learn(SearchKey::user(b"bucket-02/path/file"), 4, [9; 16], 15);
assert!(cache
.lookup(SearchKey::user(b"bucket-01/path/other"), 4)
.is_none());
let hit = cache
.lookup(SearchKey::user(b"bucket-02/path/other"), 4)
.unwrap();
assert_eq!(hit.child_guid, [9; 16]);
}
#[test]
fn does_not_cache_prefix_past_user_key() {
let cache = RouteCache::new();
cache.learn(SearchKey::user(b"abc"), 1, CHILD, 4);
assert!(cache.lookup(SearchKey::user(b"abc"), 1).is_none());
}
#[test]
fn stats_track_hits_misses_and_replacements() {
let cache = RouteCache::new();
cache.learn(SearchKey::user(b"bucket-00/path/file"), 1, CHILD, 15);
assert!(cache
.lookup(SearchKey::user(b"bucket-00/path/other"), 1)
.is_some());
assert!(cache
.lookup(SearchKey::user(b"bucket-01/path/other"), 1)
.is_none());
let stats = cache.stats();
assert_eq!(stats.entries, 1);
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
assert_eq!(stats.learns, 1);
for i in 0..=ROUTE_CACHE_CAPACITY {
let key = format!("bucket-{i:03}/path/file");
cache.learn(SearchKey::user(key.as_bytes()), 1, [i as u8; 16], 15);
}
let stats = cache.stats();
assert_eq!(stats.entries, ROUTE_CACHE_CAPACITY);
assert!(stats.evictions > 0);
}
#[test]
fn stats_count_root_version_invalidations() {
let cache = RouteCache::new();
cache.learn(SearchKey::user(b"bucket-00/path/file"), 1, CHILD, 15);
cache.learn(SearchKey::user(b"bucket-01/path/file"), 2, [8; 16], 15);
let stats = cache.stats();
assert_eq!(stats.entries, 1);
assert_eq!(stats.invalidations, 1);
}
}