Skip to main content

fips_core/cache/
entry.rs

1//! Cache entry with TTL and LRU tracking.
2
3use crate::tree::TreeCoordinate;
4
5/// A cached coordinate entry.
6#[derive(Clone, Debug)]
7pub struct CacheEntry {
8    /// The cached coordinates.
9    coords: TreeCoordinate,
10    /// When this entry was created (Unix milliseconds).
11    created_at: u64,
12    /// When this entry was last used (Unix milliseconds).
13    last_used: u64,
14    /// When this entry expires (Unix milliseconds).
15    expires_at: u64,
16    /// Path MTU discovered during lookup (if available).
17    ///
18    /// Set from the `LookupResponse.path_mtu` field when a discovery
19    /// response is cached. `None` when populated from SessionSetup or
20    /// other sources that don't carry path MTU information.
21    path_mtu: Option<u16>,
22}
23
24impl CacheEntry {
25    /// Create a new cache entry.
26    pub fn new(coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) -> Self {
27        Self {
28            coords,
29            created_at: current_time_ms,
30            last_used: current_time_ms,
31            expires_at: current_time_ms.saturating_add(ttl_ms),
32            path_mtu: None,
33        }
34    }
35
36    /// Get the cached coordinates.
37    pub fn coords(&self) -> &TreeCoordinate {
38        &self.coords
39    }
40
41    /// Get the creation timestamp.
42    pub fn created_at(&self) -> u64 {
43        self.created_at
44    }
45
46    /// Get the last used timestamp.
47    pub fn last_used(&self) -> u64 {
48        self.last_used
49    }
50
51    /// Get the expiry timestamp.
52    pub fn expires_at(&self) -> u64 {
53        self.expires_at
54    }
55
56    /// Get the path MTU discovered during lookup, if available.
57    pub fn path_mtu(&self) -> Option<u16> {
58        self.path_mtu
59    }
60
61    /// Set the path MTU discovered during lookup.
62    pub fn set_path_mtu(&mut self, mtu: u16) {
63        self.path_mtu = Some(mtu);
64    }
65
66    /// Check if this entry has expired.
67    pub fn is_expired(&self, current_time_ms: u64) -> bool {
68        current_time_ms > self.expires_at
69    }
70
71    /// Touch the entry to update last_used time.
72    pub fn touch(&mut self, current_time_ms: u64) {
73        self.last_used = current_time_ms;
74    }
75
76    /// Refresh the expiry time.
77    pub fn refresh(&mut self, current_time_ms: u64, ttl_ms: u64) {
78        self.expires_at = current_time_ms.saturating_add(ttl_ms);
79        self.last_used = current_time_ms;
80    }
81
82    /// Update the coordinates and refresh timestamps.
83    pub fn update(&mut self, coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) {
84        self.coords = coords;
85        self.last_used = current_time_ms;
86        self.expires_at = current_time_ms.saturating_add(ttl_ms);
87    }
88
89    /// Time since last use (for LRU eviction).
90    pub fn idle_time(&self, current_time_ms: u64) -> u64 {
91        current_time_ms.saturating_sub(self.last_used)
92    }
93
94    /// Age of the entry.
95    pub fn age(&self, current_time_ms: u64) -> u64 {
96        current_time_ms.saturating_sub(self.created_at)
97    }
98
99    /// Time until expiry (0 if already expired).
100    pub fn time_to_expiry(&self, current_time_ms: u64) -> u64 {
101        self.expires_at.saturating_sub(current_time_ms)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::NodeAddr;
109
110    fn make_node_addr(val: u8) -> NodeAddr {
111        let mut bytes = [0u8; 16];
112        bytes[0] = val;
113        NodeAddr::from_bytes(bytes)
114    }
115
116    fn make_coords(ids: &[u8]) -> TreeCoordinate {
117        TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
118    }
119
120    #[test]
121    fn test_cache_entry_expiry() {
122        let coords = make_coords(&[1, 0]);
123        let entry = CacheEntry::new(coords, 1000, 500);
124
125        assert!(!entry.is_expired(1000));
126        assert!(!entry.is_expired(1500)); // expires_at = 1500, not yet expired
127        assert!(entry.is_expired(1501)); // one ms after expiry
128        assert!(entry.is_expired(2000));
129    }
130
131    #[test]
132    fn test_cache_entry_refresh() {
133        let coords = make_coords(&[1, 0]);
134        let mut entry = CacheEntry::new(coords, 1000, 500);
135
136        assert!(entry.is_expired(1501)); // expires_at = 1500
137
138        entry.refresh(1400, 500); // new expires_at = 1900
139
140        assert!(!entry.is_expired(1600));
141        assert!(!entry.is_expired(1900)); // at exactly expiry, not expired
142        assert!(entry.is_expired(1901)); // one ms after expiry
143    }
144
145    #[test]
146    fn test_cache_entry_times() {
147        let coords = make_coords(&[1, 0]);
148        let entry = CacheEntry::new(coords, 1000, 500);
149
150        assert_eq!(entry.created_at(), 1000);
151        assert_eq!(entry.last_used(), 1000);
152        assert_eq!(entry.expires_at(), 1500);
153        assert_eq!(entry.age(1200), 200);
154        assert_eq!(entry.idle_time(1200), 200);
155        assert_eq!(entry.time_to_expiry(1200), 300);
156        assert_eq!(entry.time_to_expiry(1600), 0);
157    }
158
159    #[test]
160    fn test_cache_entry_touch() {
161        let coords = make_coords(&[1, 0]);
162        let mut entry = CacheEntry::new(coords, 1000, 500);
163
164        assert_eq!(entry.last_used(), 1000);
165        entry.touch(1300);
166        assert_eq!(entry.last_used(), 1300);
167        // Touch doesn't affect expiry
168        assert_eq!(entry.expires_at(), 1500);
169    }
170
171    #[test]
172    fn test_cache_entry_update() {
173        let mut entry = CacheEntry::new(make_coords(&[1, 0]), 1000, 500);
174
175        let new_coords = make_coords(&[1, 2, 0]);
176        entry.update(new_coords.clone(), 2000, 600);
177
178        assert_eq!(entry.coords(), &new_coords);
179        assert_eq!(entry.last_used(), 2000);
180        assert_eq!(entry.expires_at(), 2600);
181        // created_at is unchanged
182        assert_eq!(entry.created_at(), 1000);
183    }
184
185    #[test]
186    fn test_cache_entry_saturating_add() {
187        let coords = make_coords(&[1, 0]);
188        let entry = CacheEntry::new(coords, u64::MAX - 10, 100);
189
190        // Should saturate to u64::MAX rather than overflow
191        assert_eq!(entry.expires_at(), u64::MAX);
192    }
193}