mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 16:24:45 +00:00
Migrate the full non-async spanning-tree surface into proto/stp/, mirroring the
discovery/routing/fmp/mmp conversions. The classification ladder (parent-switch /
self-root / loop-drop / ancestry-update / periodic-rebroadcast / parent-lost) moves
out of the async node handlers into a pure Stp classify layer returning a
TreeDecision the shell drives, with effect ordering and per-arm invalidation
preserved verbatim. src/tree/ relocates wholesale: TreeState + ParentDeclaration data
+ coordinates into proto/stp/{state,coordinate}, the flap-dampening / hold-down
cluster into a FlapDampener in limits.rs, and the wire codec into wire.rs. The clock
is injected as u64 (wall-clock secs for the escaping declaration timestamp, monotonic
ms for the dampening timers via mmp::mono_ms); declaration crypto is field-partitioned
so sign/verify/hash run in the shell while the in-core modules carry data +
signing_bytes only. Peer maps/sets move to BTree; core/state/coordinate/limits are
core+alloc clean, with wire.rs the one std-tethered file. Behavior-neutral:
characterization tests added for the handler decision arms; convergence suite and
ci-local (36/36) green.
194 lines
5.8 KiB
Rust
194 lines
5.8 KiB
Rust
//! Cache entry with TTL and LRU tracking.
|
|
|
|
use crate::proto::stp::TreeCoordinate;
|
|
|
|
/// A cached coordinate entry.
|
|
#[derive(Clone, Debug)]
|
|
pub struct CacheEntry {
|
|
/// The cached coordinates.
|
|
coords: TreeCoordinate,
|
|
/// When this entry was created (Unix milliseconds).
|
|
created_at: u64,
|
|
/// When this entry was last used (Unix milliseconds).
|
|
last_used: u64,
|
|
/// When this entry expires (Unix milliseconds).
|
|
expires_at: u64,
|
|
/// Path MTU discovered during lookup (if available).
|
|
///
|
|
/// Set from the `LookupResponse.path_mtu` field when a discovery
|
|
/// response is cached. `None` when populated from SessionSetup or
|
|
/// other sources that don't carry path MTU information.
|
|
path_mtu: Option<u16>,
|
|
}
|
|
|
|
impl CacheEntry {
|
|
/// Create a new cache entry.
|
|
pub fn new(coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) -> Self {
|
|
Self {
|
|
coords,
|
|
created_at: current_time_ms,
|
|
last_used: current_time_ms,
|
|
expires_at: current_time_ms.saturating_add(ttl_ms),
|
|
path_mtu: None,
|
|
}
|
|
}
|
|
|
|
/// Get the cached coordinates.
|
|
pub fn coords(&self) -> &TreeCoordinate {
|
|
&self.coords
|
|
}
|
|
|
|
/// Get the creation timestamp.
|
|
pub fn created_at(&self) -> u64 {
|
|
self.created_at
|
|
}
|
|
|
|
/// Get the last used timestamp.
|
|
pub fn last_used(&self) -> u64 {
|
|
self.last_used
|
|
}
|
|
|
|
/// Get the expiry timestamp.
|
|
pub fn expires_at(&self) -> u64 {
|
|
self.expires_at
|
|
}
|
|
|
|
/// Get the path MTU discovered during lookup, if available.
|
|
pub fn path_mtu(&self) -> Option<u16> {
|
|
self.path_mtu
|
|
}
|
|
|
|
/// Set the path MTU discovered during lookup.
|
|
pub fn set_path_mtu(&mut self, mtu: u16) {
|
|
self.path_mtu = Some(mtu);
|
|
}
|
|
|
|
/// Check if this entry has expired.
|
|
pub fn is_expired(&self, current_time_ms: u64) -> bool {
|
|
current_time_ms > self.expires_at
|
|
}
|
|
|
|
/// Touch the entry to update last_used time.
|
|
pub fn touch(&mut self, current_time_ms: u64) {
|
|
self.last_used = current_time_ms;
|
|
}
|
|
|
|
/// Refresh the expiry time.
|
|
pub fn refresh(&mut self, current_time_ms: u64, ttl_ms: u64) {
|
|
self.expires_at = current_time_ms.saturating_add(ttl_ms);
|
|
self.last_used = current_time_ms;
|
|
}
|
|
|
|
/// Update the coordinates and refresh timestamps.
|
|
pub fn update(&mut self, coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) {
|
|
self.coords = coords;
|
|
self.last_used = current_time_ms;
|
|
self.expires_at = current_time_ms.saturating_add(ttl_ms);
|
|
}
|
|
|
|
/// Time since last use (for LRU eviction).
|
|
pub fn idle_time(&self, current_time_ms: u64) -> u64 {
|
|
current_time_ms.saturating_sub(self.last_used)
|
|
}
|
|
|
|
/// Age of the entry.
|
|
pub fn age(&self, current_time_ms: u64) -> u64 {
|
|
current_time_ms.saturating_sub(self.created_at)
|
|
}
|
|
|
|
/// Time until expiry (0 if already expired).
|
|
pub fn time_to_expiry(&self, current_time_ms: u64) -> u64 {
|
|
self.expires_at.saturating_sub(current_time_ms)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::NodeAddr;
|
|
|
|
fn make_node_addr(val: u8) -> NodeAddr {
|
|
let mut bytes = [0u8; 16];
|
|
bytes[0] = val;
|
|
NodeAddr::from_bytes(bytes)
|
|
}
|
|
|
|
fn make_coords(ids: &[u8]) -> TreeCoordinate {
|
|
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_cache_entry_expiry() {
|
|
let coords = make_coords(&[1, 0]);
|
|
let entry = CacheEntry::new(coords, 1000, 500);
|
|
|
|
assert!(!entry.is_expired(1000));
|
|
assert!(!entry.is_expired(1500)); // expires_at = 1500, not yet expired
|
|
assert!(entry.is_expired(1501)); // one ms after expiry
|
|
assert!(entry.is_expired(2000));
|
|
}
|
|
|
|
#[test]
|
|
fn test_cache_entry_refresh() {
|
|
let coords = make_coords(&[1, 0]);
|
|
let mut entry = CacheEntry::new(coords, 1000, 500);
|
|
|
|
assert!(entry.is_expired(1501)); // expires_at = 1500
|
|
|
|
entry.refresh(1400, 500); // new expires_at = 1900
|
|
|
|
assert!(!entry.is_expired(1600));
|
|
assert!(!entry.is_expired(1900)); // at exactly expiry, not expired
|
|
assert!(entry.is_expired(1901)); // one ms after expiry
|
|
}
|
|
|
|
#[test]
|
|
fn test_cache_entry_times() {
|
|
let coords = make_coords(&[1, 0]);
|
|
let entry = CacheEntry::new(coords, 1000, 500);
|
|
|
|
assert_eq!(entry.created_at(), 1000);
|
|
assert_eq!(entry.last_used(), 1000);
|
|
assert_eq!(entry.expires_at(), 1500);
|
|
assert_eq!(entry.age(1200), 200);
|
|
assert_eq!(entry.idle_time(1200), 200);
|
|
assert_eq!(entry.time_to_expiry(1200), 300);
|
|
assert_eq!(entry.time_to_expiry(1600), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cache_entry_touch() {
|
|
let coords = make_coords(&[1, 0]);
|
|
let mut entry = CacheEntry::new(coords, 1000, 500);
|
|
|
|
assert_eq!(entry.last_used(), 1000);
|
|
entry.touch(1300);
|
|
assert_eq!(entry.last_used(), 1300);
|
|
// Touch doesn't affect expiry
|
|
assert_eq!(entry.expires_at(), 1500);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cache_entry_update() {
|
|
let mut entry = CacheEntry::new(make_coords(&[1, 0]), 1000, 500);
|
|
|
|
let new_coords = make_coords(&[1, 2, 0]);
|
|
entry.update(new_coords.clone(), 2000, 600);
|
|
|
|
assert_eq!(entry.coords(), &new_coords);
|
|
assert_eq!(entry.last_used(), 2000);
|
|
assert_eq!(entry.expires_at(), 2600);
|
|
// created_at is unchanged
|
|
assert_eq!(entry.created_at(), 1000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cache_entry_saturating_add() {
|
|
let coords = make_coords(&[1, 0]);
|
|
let entry = CacheEntry::new(coords, u64::MAX - 10, 100);
|
|
|
|
// Should saturate to u64::MAX rather than overflow
|
|
assert_eq!(entry.expires_at(), u64::MAX);
|
|
}
|
|
}
|