cloudillo_core/
dir_cache.rs1use lru::LruCache;
18use std::num::NonZeroUsize;
19use std::sync::Arc;
20
21use cloudillo_types::prelude::TnId;
22
23#[derive(Debug, Clone)]
24pub struct DirEntry {
25 pub parent_id: Option<Box<str>>,
28 pub name: Box<str>,
29 pub is_folder: bool,
34}
35
36type Key = (TnId, Box<str>);
37
38#[derive(Clone)]
41pub struct DirCache {
42 inner: Arc<parking_lot::Mutex<LruCache<Key, DirEntry>>>,
43}
44
45impl std::fmt::Debug for DirCache {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 let inner = self.inner.lock();
48 f.debug_struct("DirCache")
49 .field("len", &inner.len())
50 .field("cap", &inner.cap())
51 .finish()
52 }
53}
54
55impl DirCache {
56 pub fn new(capacity: usize) -> Self {
57 let n = NonZeroUsize::new(capacity.max(1)).unwrap_or(NonZeroUsize::MIN);
58 Self { inner: Arc::new(parking_lot::Mutex::new(LruCache::new(n))) }
59 }
60
61 pub fn get(&self, tn_id: TnId, file_id: &str) -> Option<DirEntry> {
62 let mut cache = self.inner.lock();
63 cache.get(&(tn_id, Box::from(file_id))).cloned()
64 }
65
66 pub fn put(&self, tn_id: TnId, file_id: &str, entry: DirEntry) {
67 let mut cache = self.inner.lock();
68 cache.put((tn_id, Box::from(file_id)), entry);
69 }
70
71 pub fn invalidate(&self, tn_id: TnId, file_id: &str) {
72 let mut cache = self.inner.lock();
73 cache.pop(&(tn_id, Box::from(file_id)));
74 }
75
76 #[cfg(test)]
77 pub fn len(&self) -> usize {
78 self.inner.lock().len()
79 }
80
81 #[cfg(test)]
82 pub fn is_empty(&self) -> bool {
83 self.inner.lock().is_empty()
84 }
85}
86
87pub fn new_dir_cache() -> DirCache {
90 DirCache::new(1_000)
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 fn entry(parent: Option<&str>, name: &str) -> DirEntry {
98 DirEntry { parent_id: parent.map(Box::from), name: Box::from(name), is_folder: true }
99 }
100
101 #[test]
102 fn insert_get_invalidate() {
103 let cache = DirCache::new(8);
104 let tn = TnId(1);
105
106 assert!(cache.get(tn, "f1").is_none());
107
108 cache.put(tn, "f1", entry(Some("p1"), "Folder One"));
109 let got = cache.get(tn, "f1").expect("present");
110 assert_eq!(got.parent_id.as_deref(), Some("p1"));
111 assert_eq!(got.name.as_ref(), "Folder One");
112
113 cache.invalidate(tn, "f1");
114 assert!(cache.get(tn, "f1").is_none());
115 }
116
117 #[test]
118 fn lru_eviction_beyond_capacity() {
119 let cache = DirCache::new(2);
120 let tn = TnId(1);
121
122 cache.put(tn, "a", entry(None, "A"));
123 cache.put(tn, "b", entry(None, "B"));
124 let _ = cache.get(tn, "a");
126 cache.put(tn, "c", entry(None, "C"));
127
128 assert!(cache.get(tn, "a").is_some());
129 assert!(cache.get(tn, "b").is_none(), "b should have been evicted");
130 assert!(cache.get(tn, "c").is_some());
131 assert_eq!(cache.len(), 2);
132 }
133
134 #[test]
135 fn tenant_isolation_same_file_id() {
136 let cache = DirCache::new(8);
137 let tn_a = TnId(1);
138 let tn_b = TnId(2);
139
140 cache.put(tn_a, "shared-id", entry(Some("p-a"), "From A"));
141 cache.put(tn_b, "shared-id", entry(Some("p-b"), "From B"));
142
143 let a = cache.get(tn_a, "shared-id").expect("a");
144 let b = cache.get(tn_b, "shared-id").expect("b");
145 assert_eq!(a.name.as_ref(), "From A");
146 assert_eq!(b.name.as_ref(), "From B");
147 assert_eq!(a.parent_id.as_deref(), Some("p-a"));
148 assert_eq!(b.parent_id.as_deref(), Some("p-b"));
149
150 cache.invalidate(tn_a, "shared-id");
151 assert!(cache.get(tn_a, "shared-id").is_none());
152 assert!(cache.get(tn_b, "shared-id").is_some(), "b unaffected by a invalidation");
153 }
154}
155
156