Skip to main content

fips_core/cache/
coord_cache.rs

1//! Coordinate cache for routing decisions.
2//!
3//! Maps node addresses to their tree coordinates, enabling data packets
4//! to be routed without carrying coordinates in every packet. Populated
5//! by SessionSetup packets.
6
7use std::collections::HashMap;
8
9use super::CacheStats;
10use super::entry::CacheEntry;
11use crate::NodeAddr;
12use crate::tree::TreeCoordinate;
13
14/// Default maximum entries in coordinate cache.
15pub const DEFAULT_COORD_CACHE_SIZE: usize = 50_000;
16
17/// Default TTL for coordinate cache entries (5 minutes in milliseconds).
18pub const DEFAULT_COORD_CACHE_TTL_MS: u64 = 300_000;
19
20/// Coordinate cache for routing decisions.
21///
22/// Maps node addresses to their tree coordinates, enabling data packets
23/// to be routed without carrying coordinates in every packet. Populated
24/// by SessionSetup packets.
25#[derive(Clone, Debug)]
26pub struct CoordCache {
27    /// NodeAddr -> coordinates mapping.
28    entries: HashMap<NodeAddr, CacheEntry>,
29    /// Maximum number of entries.
30    max_entries: usize,
31    /// Default TTL for entries (milliseconds).
32    default_ttl_ms: u64,
33}
34
35impl CoordCache {
36    /// Create a new coordinate cache.
37    pub fn new(max_entries: usize, default_ttl_ms: u64) -> Self {
38        Self {
39            entries: HashMap::with_capacity(max_entries.min(1000)),
40            max_entries,
41            default_ttl_ms,
42        }
43    }
44
45    /// Create a cache with default parameters.
46    pub fn with_defaults() -> Self {
47        Self::new(DEFAULT_COORD_CACHE_SIZE, DEFAULT_COORD_CACHE_TTL_MS)
48    }
49
50    /// Get the maximum capacity.
51    pub fn max_entries(&self) -> usize {
52        self.max_entries
53    }
54
55    /// Get the default TTL.
56    pub fn default_ttl_ms(&self) -> u64 {
57        self.default_ttl_ms
58    }
59
60    /// Set the default TTL.
61    pub fn set_default_ttl_ms(&mut self, ttl_ms: u64) {
62        self.default_ttl_ms = ttl_ms;
63    }
64
65    /// Insert or update a cache entry.
66    pub fn insert(&mut self, addr: NodeAddr, coords: TreeCoordinate, current_time_ms: u64) {
67        // Update existing entry if present
68        if let Some(entry) = self.entries.get_mut(&addr) {
69            entry.update(coords, current_time_ms, self.default_ttl_ms);
70            return;
71        }
72
73        // Evict if at capacity
74        if self.entries.len() >= self.max_entries {
75            self.evict_one(current_time_ms);
76        }
77
78        let entry = CacheEntry::new(coords, current_time_ms, self.default_ttl_ms);
79        self.entries.insert(addr, entry);
80    }
81
82    /// Insert or update a cache entry with path MTU information.
83    ///
84    /// Used by discovery response handling to store the discovered path MTU
85    /// alongside the target's coordinates.
86    pub fn insert_with_path_mtu(
87        &mut self,
88        addr: NodeAddr,
89        coords: TreeCoordinate,
90        current_time_ms: u64,
91        path_mtu: u16,
92    ) {
93        if let Some(entry) = self.entries.get_mut(&addr) {
94            entry.update(coords, current_time_ms, self.default_ttl_ms);
95            entry.set_path_mtu(path_mtu);
96            return;
97        }
98
99        if self.entries.len() >= self.max_entries {
100            self.evict_one(current_time_ms);
101        }
102
103        let mut entry = CacheEntry::new(coords, current_time_ms, self.default_ttl_ms);
104        entry.set_path_mtu(path_mtu);
105        self.entries.insert(addr, entry);
106    }
107
108    /// Insert with a custom TTL.
109    pub fn insert_with_ttl(
110        &mut self,
111        addr: NodeAddr,
112        coords: TreeCoordinate,
113        current_time_ms: u64,
114        ttl_ms: u64,
115    ) {
116        if let Some(entry) = self.entries.get_mut(&addr) {
117            entry.update(coords, current_time_ms, ttl_ms);
118            return;
119        }
120
121        if self.entries.len() >= self.max_entries {
122            self.evict_one(current_time_ms);
123        }
124
125        let entry = CacheEntry::new(coords, current_time_ms, ttl_ms);
126        self.entries.insert(addr, entry);
127    }
128
129    /// Look up coordinates for an address (without touching).
130    pub fn get(&self, addr: &NodeAddr, current_time_ms: u64) -> Option<&TreeCoordinate> {
131        self.entries.get(addr).and_then(|entry| {
132            if entry.is_expired(current_time_ms) {
133                None
134            } else {
135                Some(entry.coords())
136            }
137        })
138    }
139
140    /// Look up coordinates and refresh (update last_used and extend TTL).
141    pub fn get_and_touch(
142        &mut self,
143        addr: &NodeAddr,
144        current_time_ms: u64,
145    ) -> Option<&TreeCoordinate> {
146        // Check and remove if expired
147        if let Some(entry) = self.entries.get(addr)
148            && entry.is_expired(current_time_ms)
149        {
150            self.entries.remove(addr);
151            return None;
152        }
153
154        // Refresh TTL and return
155        if let Some(entry) = self.entries.get_mut(addr) {
156            entry.refresh(current_time_ms, self.default_ttl_ms);
157            Some(entry.coords())
158        } else {
159            None
160        }
161    }
162
163    /// Get the full cache entry.
164    pub fn get_entry(&self, addr: &NodeAddr) -> Option<&CacheEntry> {
165        self.entries.get(addr)
166    }
167
168    /// Remove an entry.
169    pub fn remove(&mut self, addr: &NodeAddr) -> Option<CacheEntry> {
170        self.entries.remove(addr)
171    }
172
173    /// Check if an address is cached (and not expired).
174    pub fn contains(&self, addr: &NodeAddr, current_time_ms: u64) -> bool {
175        self.get(addr, current_time_ms).is_some()
176    }
177
178    /// Number of entries (including expired).
179    pub fn len(&self) -> usize {
180        self.entries.len()
181    }
182
183    /// Check if empty.
184    pub fn is_empty(&self) -> bool {
185        self.entries.is_empty()
186    }
187
188    /// Iterate over non-expired entries.
189    pub fn iter(&self, current_time_ms: u64) -> impl Iterator<Item = (&NodeAddr, &CacheEntry)> {
190        self.entries
191            .iter()
192            .filter(move |(_, entry)| !entry.is_expired(current_time_ms))
193    }
194
195    /// Remove all expired entries.
196    pub fn purge_expired(&mut self, current_time_ms: u64) -> usize {
197        let before = self.entries.len();
198        self.entries
199            .retain(|_, entry| !entry.is_expired(current_time_ms));
200        before - self.entries.len()
201    }
202
203    /// Clear all entries.
204    pub fn clear(&mut self) {
205        self.entries.clear();
206    }
207
208    /// Drop entries whose cached destination ancestry contains `node_addr`.
209    ///
210    /// When this node changes position in the tree, destinations downstream of
211    /// its old coordinate prefix must be re-learned. Unrelated same-root entries
212    /// remain usable and are retained.
213    pub fn invalidate_via_node(&mut self, node_addr: &NodeAddr) -> usize {
214        let len_before = self.entries.len();
215        self.entries
216            .retain(|_, entry| !entry.coords().contains(node_addr));
217        len_before - self.entries.len()
218    }
219
220    /// Drop entries whose cached root differs from `current_root`.
221    ///
222    /// Entries from a stale root cannot route after a root change; removing them
223    /// prevents active traffic from keeping those stale entries alive forever by
224    /// refreshing their TTL.
225    pub fn invalidate_other_roots(&mut self, current_root: &NodeAddr) -> usize {
226        let len_before = self.entries.len();
227        self.entries
228            .retain(|_, entry| entry.coords().root_id() == current_root);
229        len_before - self.entries.len()
230    }
231
232    /// Evict one entry (expired first, then LRU).
233    fn evict_one(&mut self, current_time_ms: u64) {
234        // First try to evict an expired entry
235        let expired_key = self
236            .entries
237            .iter()
238            .find(|(_, e)| e.is_expired(current_time_ms))
239            .map(|(k, _)| *k);
240
241        if let Some(key) = expired_key {
242            self.entries.remove(&key);
243            return;
244        }
245
246        // Otherwise evict LRU (oldest last_used)
247        let lru_key = self
248            .entries
249            .iter()
250            .max_by_key(|(_, e)| e.idle_time(current_time_ms))
251            .map(|(k, _)| *k);
252
253        if let Some(key) = lru_key {
254            self.entries.remove(&key);
255        }
256    }
257
258    /// Get cache statistics.
259    pub fn stats(&self, current_time_ms: u64) -> CacheStats {
260        let mut expired = 0;
261        let mut total_age = 0u64;
262
263        for entry in self.entries.values() {
264            if entry.is_expired(current_time_ms) {
265                expired += 1;
266            }
267            total_age += entry.age(current_time_ms);
268        }
269
270        CacheStats {
271            entries: self.entries.len(),
272            max_entries: self.max_entries,
273            expired,
274            avg_age_ms: if self.entries.is_empty() {
275                0
276            } else {
277                total_age / self.entries.len() as u64
278            },
279        }
280    }
281}
282
283impl Default for CoordCache {
284    fn default() -> Self {
285        Self::with_defaults()
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    fn make_node_addr(val: u8) -> NodeAddr {
294        let mut bytes = [0u8; 16];
295        bytes[0] = val;
296        NodeAddr::from_bytes(bytes)
297    }
298
299    fn make_coords(ids: &[u8]) -> TreeCoordinate {
300        TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
301    }
302
303    #[test]
304    fn test_coord_cache_basic() {
305        let mut cache = CoordCache::new(100, 1000);
306        let addr = make_node_addr(1);
307        let coords = make_coords(&[1, 0]);
308
309        cache.insert(addr, coords.clone(), 0);
310
311        assert!(cache.contains(&addr, 0));
312        assert_eq!(cache.get(&addr, 0), Some(&coords));
313        assert_eq!(cache.len(), 1);
314    }
315
316    #[test]
317    fn test_coord_cache_expiry() {
318        let mut cache = CoordCache::new(100, 1000);
319        let addr = make_node_addr(1);
320        let coords = make_coords(&[1, 0]);
321
322        cache.insert(addr, coords, 0);
323
324        assert!(cache.contains(&addr, 500));
325        assert!(!cache.contains(&addr, 1500));
326    }
327
328    #[test]
329    fn test_coord_cache_update() {
330        let mut cache = CoordCache::new(100, 1000);
331        let addr = make_node_addr(1);
332
333        cache.insert(addr, make_coords(&[1, 0]), 0);
334        cache.insert(addr, make_coords(&[1, 2, 0]), 500);
335
336        assert_eq!(cache.len(), 1);
337        let coords = cache.get(&addr, 500).unwrap();
338        assert_eq!(coords.depth(), 2);
339    }
340
341    #[test]
342    fn test_coord_cache_eviction() {
343        let mut cache = CoordCache::new(2, 10000);
344
345        let addr1 = make_node_addr(1);
346        let addr2 = make_node_addr(2);
347        let addr3 = make_node_addr(3);
348
349        cache.insert(addr1, make_coords(&[1, 0]), 0);
350        cache.insert(addr2, make_coords(&[2, 0]), 100);
351
352        // Touch addr2 to make it more recent
353        let _ = cache.get_and_touch(&addr2, 200);
354
355        // Insert addr3, should evict addr1 (LRU)
356        cache.insert(addr3, make_coords(&[3, 0]), 300);
357
358        assert!(!cache.contains(&addr1, 300));
359        assert!(cache.contains(&addr2, 300));
360        assert!(cache.contains(&addr3, 300));
361    }
362
363    #[test]
364    fn test_coord_cache_evict_expired_first() {
365        let mut cache = CoordCache::new(2, 100);
366
367        cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
368        cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50);
369
370        // At time 150, addr1 is expired, addr2 is not
371        cache.insert(make_node_addr(3), make_coords(&[3, 0]), 150);
372
373        // addr1 should be evicted (expired), not addr2 (LRU but not expired)
374        assert!(!cache.contains(&make_node_addr(1), 150));
375        assert!(cache.contains(&make_node_addr(2), 150));
376        assert!(cache.contains(&make_node_addr(3), 150));
377    }
378
379    #[test]
380    fn test_coord_cache_purge_expired() {
381        let mut cache = CoordCache::new(100, 100);
382
383        cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); // expires at 100
384        cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50); // expires at 150
385        cache.insert(make_node_addr(3), make_coords(&[3, 0]), 200); // expires at 300
386
387        assert_eq!(cache.len(), 3);
388
389        let purged = cache.purge_expired(151); // both addr1 and addr2 expired
390
391        // Entry 1 and 2 expired, entry 3 still valid
392        assert_eq!(purged, 2);
393        assert_eq!(cache.len(), 1);
394        assert!(cache.contains(&make_node_addr(3), 151));
395    }
396
397    #[test]
398    fn test_coord_cache_stats() {
399        let mut cache = CoordCache::new(100, 100);
400
401        cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
402        cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50);
403
404        let stats = cache.stats(150);
405
406        assert_eq!(stats.entries, 2);
407        assert_eq!(stats.max_entries, 100);
408        assert_eq!(stats.expired, 1); // addr1 expired
409        assert!(stats.avg_age_ms > 0);
410    }
411
412    #[test]
413    fn test_coord_cache_insert_with_ttl() {
414        let mut cache = CoordCache::new(100, 1000);
415        let addr = make_node_addr(1);
416
417        cache.insert_with_ttl(addr, make_coords(&[1, 0]), 0, 200);
418
419        // Should expire at 200, not the default 1000
420        assert!(cache.contains(&addr, 100));
421        assert!(!cache.contains(&addr, 201));
422    }
423
424    #[test]
425    fn test_coord_cache_insert_with_ttl_update() {
426        let mut cache = CoordCache::new(100, 1000);
427        let addr = make_node_addr(1);
428
429        cache.insert_with_ttl(addr, make_coords(&[1, 0]), 0, 200);
430        cache.insert_with_ttl(addr, make_coords(&[1, 2, 0]), 100, 300);
431
432        assert_eq!(cache.len(), 1);
433        let coords = cache.get(&addr, 100).unwrap();
434        assert_eq!(coords.depth(), 2);
435        // New TTL: 100 + 300 = 400
436        assert!(cache.contains(&addr, 399));
437        assert!(!cache.contains(&addr, 401));
438    }
439
440    #[test]
441    fn test_coord_cache_get_and_touch_removes_expired() {
442        let mut cache = CoordCache::new(100, 100);
443        let addr = make_node_addr(1);
444
445        cache.insert(addr, make_coords(&[1, 0]), 0);
446        assert_eq!(cache.len(), 1);
447
448        // Entry expired at time 200
449        let result = cache.get_and_touch(&addr, 200);
450        assert!(result.is_none());
451        // Entry should be removed from the map
452        assert_eq!(cache.len(), 0);
453    }
454
455    #[test]
456    fn test_coord_cache_get_entry() {
457        let mut cache = CoordCache::new(100, 1000);
458        let addr = make_node_addr(1);
459
460        cache.insert(addr, make_coords(&[1, 0]), 500);
461
462        let entry = cache.get_entry(&addr).unwrap();
463        assert_eq!(entry.created_at(), 500);
464        assert_eq!(entry.expires_at(), 1500);
465
466        assert!(cache.get_entry(&make_node_addr(99)).is_none());
467    }
468
469    #[test]
470    fn test_coord_cache_remove() {
471        let mut cache = CoordCache::new(100, 1000);
472        let addr = make_node_addr(1);
473
474        cache.insert(addr, make_coords(&[1, 0]), 0);
475        assert_eq!(cache.len(), 1);
476
477        let removed = cache.remove(&addr);
478        assert!(removed.is_some());
479        assert_eq!(cache.len(), 0);
480
481        // Removing again returns None
482        assert!(cache.remove(&addr).is_none());
483    }
484
485    #[test]
486    fn test_coord_cache_clear_and_is_empty() {
487        let mut cache = CoordCache::new(100, 1000);
488
489        assert!(cache.is_empty());
490
491        cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
492        cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0);
493
494        assert!(!cache.is_empty());
495
496        cache.clear();
497        assert!(cache.is_empty());
498        assert_eq!(cache.len(), 0);
499    }
500
501    #[test]
502    fn test_coord_cache_invalidate_via_node_is_surgical() {
503        let mut cache = CoordCache::new(100, 1000);
504        let node = make_node_addr(1);
505        let downstream = make_node_addr(2);
506        let sibling = make_node_addr(3);
507
508        cache.insert(downstream, make_coords(&[2, 1, 0]), 0);
509        cache.insert(sibling, make_coords(&[3, 4, 0]), 0);
510
511        assert_eq!(cache.invalidate_via_node(&node), 1);
512        assert!(!cache.contains(&downstream, 0));
513        assert!(cache.contains(&sibling, 0));
514    }
515
516    #[test]
517    fn test_coord_cache_invalidate_other_roots_keeps_current_root() {
518        let mut cache = CoordCache::new(100, 1000);
519        let current_root = make_node_addr(0);
520        let current = make_node_addr(2);
521        let stale = make_node_addr(3);
522
523        cache.insert(current, make_coords(&[2, 1, 0]), 0);
524        cache.insert(stale, make_coords(&[3, 4, 9]), 0);
525
526        assert_eq!(cache.invalidate_other_roots(&current_root), 1);
527        assert!(cache.contains(&current, 0));
528        assert!(!cache.contains(&stale, 0));
529    }
530
531    #[test]
532    fn test_coord_cache_default() {
533        let cache = CoordCache::default();
534
535        assert_eq!(cache.max_entries(), DEFAULT_COORD_CACHE_SIZE);
536        assert_eq!(cache.default_ttl_ms(), DEFAULT_COORD_CACHE_TTL_MS);
537        assert!(cache.is_empty());
538    }
539
540    #[test]
541    fn test_coord_cache_set_default_ttl() {
542        let mut cache = CoordCache::new(100, 1000);
543        let addr = make_node_addr(1);
544
545        cache.set_default_ttl_ms(200);
546        assert_eq!(cache.default_ttl_ms(), 200);
547
548        cache.insert(addr, make_coords(&[1, 0]), 0);
549        // New TTL applies: expires at 200
550        assert!(cache.contains(&addr, 100));
551        assert!(!cache.contains(&addr, 201));
552    }
553
554    #[test]
555    fn test_coord_cache_stats_empty() {
556        let cache = CoordCache::new(100, 1000);
557        let stats = cache.stats(0);
558
559        assert_eq!(stats.entries, 0);
560        assert_eq!(stats.max_entries, 100);
561        assert_eq!(stats.expired, 0);
562        assert_eq!(stats.avg_age_ms, 0);
563    }
564}