use lru::LruCache;
use std::num::NonZeroUsize;
use std::sync::Arc;
use cloudillo_types::prelude::TnId;
#[derive(Debug, Clone)]
pub struct DirEntry {
pub parent_id: Option<Box<str>>,
pub name: Box<str>,
pub is_folder: bool,
}
type Key = (TnId, Box<str>);
#[derive(Clone)]
pub struct DirCache {
inner: Arc<parking_lot::Mutex<LruCache<Key, DirEntry>>>,
}
impl std::fmt::Debug for DirCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let inner = self.inner.lock();
f.debug_struct("DirCache")
.field("len", &inner.len())
.field("cap", &inner.cap())
.finish()
}
}
impl DirCache {
pub fn new(capacity: usize) -> Self {
let n = NonZeroUsize::new(capacity.max(1)).unwrap_or(NonZeroUsize::MIN);
Self { inner: Arc::new(parking_lot::Mutex::new(LruCache::new(n))) }
}
pub fn get(&self, tn_id: TnId, file_id: &str) -> Option<DirEntry> {
let mut cache = self.inner.lock();
cache.get(&(tn_id, Box::from(file_id))).cloned()
}
pub fn put(&self, tn_id: TnId, file_id: &str, entry: DirEntry) {
let mut cache = self.inner.lock();
cache.put((tn_id, Box::from(file_id)), entry);
}
pub fn invalidate(&self, tn_id: TnId, file_id: &str) {
let mut cache = self.inner.lock();
cache.pop(&(tn_id, Box::from(file_id)));
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.inner.lock().len()
}
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.inner.lock().is_empty()
}
}
pub fn new_dir_cache() -> DirCache {
DirCache::new(1_000)
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(parent: Option<&str>, name: &str) -> DirEntry {
DirEntry { parent_id: parent.map(Box::from), name: Box::from(name), is_folder: true }
}
#[test]
fn insert_get_invalidate() {
let cache = DirCache::new(8);
let tn = TnId(1);
assert!(cache.get(tn, "f1").is_none());
cache.put(tn, "f1", entry(Some("p1"), "Folder One"));
let got = cache.get(tn, "f1").expect("present");
assert_eq!(got.parent_id.as_deref(), Some("p1"));
assert_eq!(got.name.as_ref(), "Folder One");
cache.invalidate(tn, "f1");
assert!(cache.get(tn, "f1").is_none());
}
#[test]
fn lru_eviction_beyond_capacity() {
let cache = DirCache::new(2);
let tn = TnId(1);
cache.put(tn, "a", entry(None, "A"));
cache.put(tn, "b", entry(None, "B"));
let _ = cache.get(tn, "a");
cache.put(tn, "c", entry(None, "C"));
assert!(cache.get(tn, "a").is_some());
assert!(cache.get(tn, "b").is_none(), "b should have been evicted");
assert!(cache.get(tn, "c").is_some());
assert_eq!(cache.len(), 2);
}
#[test]
fn tenant_isolation_same_file_id() {
let cache = DirCache::new(8);
let tn_a = TnId(1);
let tn_b = TnId(2);
cache.put(tn_a, "shared-id", entry(Some("p-a"), "From A"));
cache.put(tn_b, "shared-id", entry(Some("p-b"), "From B"));
let a = cache.get(tn_a, "shared-id").expect("a");
let b = cache.get(tn_b, "shared-id").expect("b");
assert_eq!(a.name.as_ref(), "From A");
assert_eq!(b.name.as_ref(), "From B");
assert_eq!(a.parent_id.as_deref(), Some("p-a"));
assert_eq!(b.parent_id.as_deref(), Some("p-b"));
cache.invalidate(tn_a, "shared-id");
assert!(cache.get(tn_a, "shared-id").is_none());
assert!(cache.get(tn_b, "shared-id").is_some(), "b unaffected by a invalidation");
}
}