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