1use std::collections::BTreeMap;
20
21use exo_core::{Did, Timestamp};
22
23use crate::chain::AuthorityChain;
24
25const DEFAULT_MAX_ENTRIES: usize = 10000;
27
28#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
30struct CacheKey {
31 from: String,
32 to: String,
33}
34
35#[derive(Debug, Clone)]
37struct CacheEntry {
38 chain: AuthorityChain,
39 last_accessed: Timestamp,
40}
41
42#[derive(Debug)]
44pub struct ChainCache {
45 entries: BTreeMap<CacheKey, CacheEntry>,
46 max_entries: usize,
47}
48
49impl ChainCache {
50 #[must_use]
52 pub fn new() -> Self {
53 Self {
54 entries: BTreeMap::new(),
55 max_entries: DEFAULT_MAX_ENTRIES,
56 }
57 }
58
59 #[must_use]
61 pub fn with_capacity(max_entries: usize) -> Self {
62 Self {
63 entries: BTreeMap::new(),
64 max_entries,
65 }
66 }
67
68 pub fn get(&mut self, from: &Did, to: &Did, now: &Timestamp) -> Option<&AuthorityChain> {
70 let key = CacheKey {
71 from: from.as_str().to_owned(),
72 to: to.as_str().to_owned(),
73 };
74 if let Some(entry) = self.entries.get_mut(&key) {
75 entry.last_accessed = *now;
76 Some(&entry.chain)
77 } else {
78 None
79 }
80 }
81
82 pub fn insert(&mut self, from: &Did, to: &Did, chain: AuthorityChain, now: &Timestamp) {
84 if self.max_entries == 0 {
85 self.entries.clear();
86 return;
87 }
88
89 while self.entries.len() >= self.max_entries {
91 self.evict_oldest();
92 }
93
94 let key = CacheKey {
95 from: from.as_str().to_owned(),
96 to: to.as_str().to_owned(),
97 };
98 self.entries.insert(
99 key,
100 CacheEntry {
101 chain,
102 last_accessed: *now,
103 },
104 );
105 }
106
107 pub fn invalidate(&mut self, did: &Did) {
109 let did_str = did.as_str();
110 self.entries
111 .retain(|key, _| key.from != did_str && key.to != did_str);
112 }
113
114 #[must_use]
116 pub fn len(&self) -> usize {
117 self.entries.len()
118 }
119
120 #[must_use]
122 pub fn is_empty(&self) -> bool {
123 self.entries.is_empty()
124 }
125
126 pub fn clear(&mut self) {
128 self.entries.clear();
129 }
130
131 fn evict_oldest(&mut self) {
133 if let Some(oldest_key) = self
134 .entries
135 .iter()
136 .min_by_key(|(_, v)| v.last_accessed)
137 .map(|(k, _)| k.clone())
138 {
139 self.entries.remove(&oldest_key);
140 }
141 }
142}
143
144impl Default for ChainCache {
145 fn default() -> Self {
146 Self::new()
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use exo_core::Signature;
153
154 use super::*;
155 use crate::{
156 chain::{AuthorityChain, AuthorityLink},
157 permission::Permission,
158 };
159
160 fn did(name: &str) -> Did {
161 Did::new(&format!("did:exo:{name}")).unwrap()
162 }
163 fn ts(ms: u64) -> Timestamp {
164 Timestamp::new(ms, 0)
165 }
166
167 fn make_chain(from: &str, to: &str) -> AuthorityChain {
168 AuthorityChain {
169 links: vec![AuthorityLink {
170 delegator_did: did(from),
171 delegate_did: did(to),
172 scope: vec![Permission::Read],
173 created: ts(1000),
174 expires: None,
175 signature: Signature::from_bytes([0xA5u8; 64]),
176 depth: 0,
177 delegatee_kind: crate::chain::DelegateeKind::Human,
178 }],
179 max_depth: 5,
180 }
181 }
182
183 #[test]
184 fn insert_and_get() {
185 let mut cache = ChainCache::new();
186 let chain = make_chain("alice", "bob");
187 cache.insert(&did("alice"), &did("bob"), chain, &ts(1000));
188 let got = cache.get(&did("alice"), &did("bob"), &ts(2000));
189 assert!(got.is_some());
190 assert_eq!(got.unwrap().depth(), 1);
191 }
192
193 #[test]
194 fn get_updates_access_time() {
195 let mut cache = ChainCache::new();
196 cache.insert(
197 &did("alice"),
198 &did("bob"),
199 make_chain("alice", "bob"),
200 &ts(1000),
201 );
202 cache.get(&did("alice"), &did("bob"), &ts(5000));
203 cache.insert(&did("x"), &did("y"), make_chain("x", "y"), &ts(2000));
205 assert_eq!(cache.len(), 2);
207 }
208
209 #[test]
210 fn get_miss() {
211 let mut cache = ChainCache::new();
212 assert!(cache.get(&did("alice"), &did("bob"), &ts(1000)).is_none());
213 }
214
215 #[test]
216 fn invalidate_removes_related() {
217 let mut cache = ChainCache::new();
218 cache.insert(
219 &did("alice"),
220 &did("bob"),
221 make_chain("alice", "bob"),
222 &ts(1000),
223 );
224 cache.insert(
225 &did("alice"),
226 &did("charlie"),
227 make_chain("alice", "charlie"),
228 &ts(1000),
229 );
230 cache.insert(
231 &did("dave"),
232 &did("eve"),
233 make_chain("dave", "eve"),
234 &ts(1000),
235 );
236 assert_eq!(cache.len(), 3);
237
238 cache.invalidate(&did("alice"));
239 assert_eq!(cache.len(), 1);
240 assert!(cache.get(&did("dave"), &did("eve"), &ts(2000)).is_some());
241 }
242
243 #[test]
244 fn invalidate_as_target() {
245 let mut cache = ChainCache::new();
246 cache.insert(
247 &did("alice"),
248 &did("bob"),
249 make_chain("alice", "bob"),
250 &ts(1000),
251 );
252 cache.invalidate(&did("bob"));
253 assert!(cache.is_empty());
254 }
255
256 #[test]
257 fn eviction_on_capacity() {
258 let mut cache = ChainCache::with_capacity(2);
259 cache.insert(&did("a"), &did("b"), make_chain("a", "b"), &ts(1000));
260 cache.insert(&did("c"), &did("d"), make_chain("c", "d"), &ts(2000));
261 assert_eq!(cache.len(), 2);
262
263 cache.insert(&did("e"), &did("f"), make_chain("e", "f"), &ts(3000));
265 assert_eq!(cache.len(), 2);
266 assert!(cache.get(&did("a"), &did("b"), &ts(4000)).is_none());
267 assert!(cache.get(&did("c"), &did("d"), &ts(4000)).is_some());
268 }
269
270 #[test]
271 fn clear() {
272 let mut cache = ChainCache::new();
273 cache.insert(&did("a"), &did("b"), make_chain("a", "b"), &ts(1000));
274 cache.insert(&did("c"), &did("d"), make_chain("c", "d"), &ts(1000));
275 cache.clear();
276 assert!(cache.is_empty());
277 }
278
279 #[test]
280 fn default_is_empty() {
281 let cache = ChainCache::default();
282 assert!(cache.is_empty());
283 assert_eq!(cache.len(), 0);
284 }
285
286 #[test]
287 fn invalidate_unknown_did_is_noop() {
288 let mut cache = ChainCache::new();
289 cache.insert(&did("a"), &did("b"), make_chain("a", "b"), &ts(1000));
290 cache.invalidate(&did("unknown"));
291 assert_eq!(cache.len(), 1);
292 }
293
294 #[test]
295 fn eviction_respects_access_time_update() {
296 let mut cache = ChainCache::with_capacity(2);
297 cache.insert(&did("a"), &did("b"), make_chain("a", "b"), &ts(1000));
298 cache.insert(&did("c"), &did("d"), make_chain("c", "d"), &ts(2000));
299
300 cache.get(&did("a"), &did("b"), &ts(3000));
302
303 cache.insert(&did("e"), &did("f"), make_chain("e", "f"), &ts(4000));
305 assert!(cache.get(&did("a"), &did("b"), &ts(5000)).is_some());
306 assert!(cache.get(&did("c"), &did("d"), &ts(5000)).is_none());
307 }
308
309 #[test]
310 fn zero_capacity_insert_returns_without_caching() {
311 let (tx, rx) = std::sync::mpsc::channel();
312 std::thread::spawn(move || {
313 let mut cache = ChainCache::with_capacity(0);
314 cache.insert(&did("a"), &did("b"), make_chain("a", "b"), &ts(1000));
315 tx.send(cache.len()).unwrap();
316 });
317
318 let len = rx
319 .recv_timeout(std::time::Duration::from_millis(100))
320 .expect("zero-capacity insert must return instead of looping forever");
321 assert_eq!(len, 0);
322 }
323}