bacnet_router_core/table.rs
1//! The routing table (Clause 6.6.1), in caller-sized storage.
2//!
3//! [`Table`] maps each network the router can reach to a [`Route`]: the
4//! port it is reached through, the next router's MAC (none for a directly
5//! attached network), and a [`Reachability`] that keeps a lasting failure
6//! apart from congestion. Up to `R` routes are kept sorted by network
7//! number, so a lookup is a binary search and every listing comes out in
8//! ascending order with no sort.
9//!
10//! The rules are those of the hosted router's table:
11//!
12//! - **Direct routes are fixed.** No learning claim, reject, flow-control
13//! mark or management write changes one.
14//! - **Learning** ([`Table::apply_claim`]): under [`Convergence::Standard`]
15//! each claim applies at once, so the last one wins (Clause 6.6.3.2), and a
16//! route that changes port three times within 60 s is reported as
17//! flapping. [`Convergence::Hardened`], an opt-in, holds a move to another
18//! port until a second claim for the same network and port arrives within
19//! the corroboration window (60 s, inclusive). A new network, or a refresh
20//! on the same port, applies at once in both modes.
21//! - **Freshness is split.** `last_seen` is set by learning and management
22//! only, and aging keys off it; forwarding sets `last_used`, which never
23//! extends a route's life. Busy and Available marks touch neither.
24//! - **Rejects** ([`Table::apply_reject`]): reason 1 marks a learned route
25//! unreachable and reason 2 busy for 30 s; any other reason leaves it as
26//! it is, and is counted apart (Clause 6.6.3.5).
27//! Each change starts a 30 s hold-down for its network and arrival port,
28//! during which further reject-driven changes from that port are ignored.
29//! Hold-down records live in their own pool of `H`; a full pool evicts the
30//! oldest and counts it ([`ClaimCounters::hold_evicted`]).
31//! - **Busy marks** lapse at their deadline, which
32//! [`Table::effective_reachability`] reads inline, so the sweep's cadence
33//! never changes a forwarding decision. Only Router-Available or fresh
34//! learning lifts Unreachable.
35//!
36//! [`ClaimCounters`] counts the outcomes. The counts saturate and never
37//! change a decision.
38//!
39//! The table stores no timers: the calls that need the busy period, the
40//! hold-down or the corroboration window take a [`Timing`], which the
41//! router's options own. So [`Table::new`] is all zero bits, and a table in
42//! a `static` costs firmware no flash for an initial image.
43
44mod claims;
45mod holds;
46
47use core::fmt;
48
49use crate::{Duration, Instant, Mac, PortId, swap_apart};
50
51pub use claims::{LearnRefusal, Learned};
52pub use holds::RejectEffect;
53
54/// How far a route can be used.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Reachability {
57 /// Usable.
58 Reachable,
59 /// Congested until the route's busy deadline (Router-Busy, or reject
60 /// reason 2).
61 Busy,
62 /// Failed until Router-Available or fresh learning (reject reason 1).
63 Unreachable,
64}
65
66/// How a learning claim that moves a route to another port is taken.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub enum Convergence {
69 /// At once: the last claim wins (Clause 6.6.3.2).
70 #[default]
71 Standard,
72 /// After a second claim for the same network and port within the
73 /// corroboration window. It authenticates nothing, and it slows
74 /// legitimate moves: alternating claims never converge, and a single
75 /// claim waits for the window to lapse and a fresh one.
76 Hardened,
77}
78
79/// The way to one network: a copy of the table's row, which
80/// [`Table::lookup`] and [`Table::routes`] hand out.
81///
82/// Its fields are the table's bookkeeping, so they are read through its
83/// getters, and only the table's own calls change the row: a direct route
84/// stays direct, and the flap count and timestamps stay the table's. Build
85/// one with [`Route::direct`] or [`Route::learned`], to compare with what a
86/// lookup returns. It is `#[non_exhaustive]`.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88#[non_exhaustive]
89pub struct Route {
90 port: PortId,
91 direct: bool,
92 next_hop: Mac,
93 last_seen: Option<Instant>,
94 last_used: Option<Instant>,
95 reachability: Reachability,
96 busy_until: Option<Instant>,
97 flap_count: u8,
98 last_port_change: Option<Instant>,
99}
100
101impl Route {
102 /// All zero bits: what an unused slot holds.
103 const ZERO: Self = Self {
104 port: PortId(0),
105 direct: false,
106 next_hop: Mac::EMPTY,
107 last_seen: None,
108 last_used: None,
109 reachability: Reachability::Reachable,
110 busy_until: None,
111 flap_count: 0,
112 last_port_change: None,
113 };
114
115 /// A directly attached network on `port`.
116 #[must_use]
117 pub const fn direct(port: PortId) -> Self {
118 let mut route = Self::ZERO;
119 route.port = port;
120 route.direct = true;
121 route
122 }
123
124 /// A network reached through `next_hop` on `port`, confirmed `now`.
125 #[must_use]
126 pub const fn learned(port: PortId, next_hop: Mac, now: Instant) -> Self {
127 let mut route = Self::ZERO;
128 route.port = port;
129 route.next_hop = next_hop;
130 route.last_seen = Some(now);
131 route
132 }
133
134 /// The port the network is reached through.
135 #[must_use]
136 pub const fn port(&self) -> PortId {
137 self.port
138 }
139
140 /// Whether the network is attached to that port, rather than reached
141 /// through another router.
142 #[must_use]
143 pub const fn is_direct(&self) -> bool {
144 self.direct
145 }
146
147 /// The next router's MAC on that port; empty for a direct route.
148 #[must_use]
149 pub const fn next_hop(&self) -> &Mac {
150 &self.next_hop
151 }
152
153 /// When learning or management last confirmed the route; `None` for a
154 /// direct route. Aging keys off it.
155 #[must_use]
156 pub const fn last_seen(&self) -> Option<Instant> {
157 self.last_seen
158 }
159
160 /// When the route last carried traffic; `None` until it does, and again
161 /// after a move to another port. It never extends the route's life.
162 #[must_use]
163 pub const fn last_used(&self) -> Option<Instant> {
164 self.last_used
165 }
166
167 /// The stored state. A lapsed busy mark still reads `Busy` here until
168 /// the sweep; [`Table::effective_reachability`] reads it at a time.
169 #[must_use]
170 pub const fn reachability(&self) -> Reachability {
171 self.reachability
172 }
173
174 /// When a busy mark lapses.
175 #[must_use]
176 pub const fn busy_until(&self) -> Option<Instant> {
177 self.busy_until
178 }
179
180 /// Moves to another port within 60 s of the one before.
181 #[must_use]
182 pub const fn flap_count(&self) -> u8 {
183 self.flap_count
184 }
185
186 /// When the route last moved to another port.
187 #[must_use]
188 pub const fn last_port_change(&self) -> Option<Instant> {
189 self.last_port_change
190 }
191
192 /// Whether flow control from the router at `peer` on `port` covers it:
193 /// learned, through that port, with that next router.
194 fn served_via(&self, port: PortId, peer: &[u8]) -> bool {
195 !self.direct && self.port == port && self.next_hop == *peer
196 }
197}
198
199/// A hardened-mode claim waiting for its second (see [`Table::pending`]).
200///
201/// The table builds it. It is `#[non_exhaustive]`, so a new field isn't a
202/// breaking change: a pattern needs `..`.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204#[non_exhaustive]
205pub struct Pending {
206 /// The port the claim would move the route to.
207 pub port: PortId,
208 /// When the first claim came.
209 pub first_seen: Instant,
210}
211
212/// Counts of how routing claims fared. They saturate, never affect a
213/// decision, and name no peer.
214///
215/// Learning counts cover I-Am-Router and Initialize-Routing-Table-Ack
216/// entries the router inspects, not management writes or calls on the
217/// table itself. The router records some of them itself.
218///
219/// It is `#[non_exhaustive]`, so a new count isn't a breaking change.
220/// Outside this crate, build one from `ClaimCounters::default()` and set
221/// its fields.
222#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
223#[non_exhaustive]
224pub struct ClaimCounters {
225 /// Rejects that changed a learned route's state.
226 pub reject_applied: u64,
227 /// Rejects ignored, for either reason below; each counts once here.
228 pub reject_dampened: u64,
229 /// Rejects that would change nothing: no route, a direct one, or the
230 /// state already in effect. A repeat Busy never extends its deadline.
231 pub reject_dampened_same_state: u64,
232 /// State-changing rejects ignored within the hold-down.
233 pub reject_dampened_hold_down: u64,
234 /// Rejects for a learned route with a reason other than 1 or 2, which
235 /// leave it as it is (Clause 6.6.3.5).
236 pub reject_kept: u64,
237 /// Learning claims that added or refreshed a learned route.
238 pub learned_ok: u64,
239 /// Learning claims that met the router's route cap and ended the
240 /// message's learning, and I-Could-Be-Router offers the cap turned
241 /// away.
242 pub learned_cap_ignored: u64,
243 /// Moves that reached the flap warning.
244 pub flap_warned: u64,
245 /// Hardened-mode claims held pending, new or restarted after expiry.
246 pub pending_started: u64,
247 /// Moves applied by a second claim within the window.
248 pub corroborated_applied: u64,
249 /// Pending claims found past the window and dropped, by the next claim
250 /// or by the sweep. Other cleanup doesn't count.
251 pub pending_expired: u64,
252 /// Disconnect-Connection removals ignored, since there are no PTP
253 /// connections.
254 pub disconnect_removal_ignored: u64,
255 /// Hold-down records evicted, oldest first, from a full pool.
256 pub hold_evicted: u64,
257}
258
259/// The table's timers, which the router's options own and pass to each
260/// call that needs one.
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262pub struct Timing {
263 /// How long reject reason 2 marks a route busy.
264 pub busy_for: Duration,
265 /// How long a reject-driven change holds off the next one for its
266 /// network and arrival port.
267 pub hold_down: Duration,
268 /// How long a hardened-mode claim waits for its second.
269 pub corroboration: Duration,
270}
271
272impl Timing {
273 /// 30 s busy, 30 s hold-down, 60 s corroboration: the hosted router's.
274 pub const DEFAULT: Self = Self {
275 busy_for: Duration::from_secs(30),
276 hold_down: Duration::from_secs(30),
277 corroboration: Duration::from_secs(60),
278 };
279}
280
281impl Default for Timing {
282 fn default() -> Self {
283 Self::DEFAULT
284 }
285}
286
287/// One stored route and its pending claim.
288#[derive(Debug, Clone, Copy)]
289struct Slot {
290 network: u16,
291 route: Route,
292 pending: Option<Pending>,
293}
294
295/// An unused slot: all zero bits.
296const VACANT: Slot = Slot {
297 network: 0,
298 route: Route::ZERO,
299 pending: None,
300};
301
302/// Whether `network` names no single network (0, or the global 0xFFFF), so
303/// no route can lead to it. A router never learns one, and a non-routing
304/// node reading a route announcement drops it the same way.
305#[must_use]
306pub const fn is_reserved(network: u16) -> bool {
307 network == 0 || network == 0xFFFF
308}
309
310/// A routing table of up to `R` routes and `H` hold-down records.
311///
312/// A full table refuses a new network: [`add_direct`](Self::add_direct)
313/// and [`add_learned`](Self::add_learned) return `false`, and
314/// [`learn`](Self::learn) and [`apply_claim`](Self::apply_claim) return
315/// [`LearnRefusal::Full`]. Routes it holds can still be refreshed, moved and
316/// removed. A router keeps its route cap at or below `R`, so the cap, which
317/// it counts, is met first.
318///
319/// A [`Route`] is a copy of a row, so a route changes only through the
320/// table's calls, each of which keeps its rules: [`add_direct`](Self::add_direct)
321/// and [`add_learned`](Self::add_learned) install one;
322/// [`learn`](Self::learn) and [`apply_claim`](Self::apply_claim) learn one,
323/// with flap detection and convergence; [`mark_busy`](Self::mark_busy),
324/// [`mark_available`](Self::mark_available) and
325/// [`mark_unreachable`](Self::mark_unreachable) set its state;
326/// [`touch`](Self::touch) notes its use; and [`remove`](Self::remove) and
327/// [`purge_stale`](Self::purge_stale) take it out. None of them makes a
328/// learned route direct, or a direct one learned, except by replacing it.
329///
330/// [`new`](Self::new) is a `const fn` whose result is all zero bits, so a
331/// table can live in a zero-initialized static (`.bss`).
332#[derive(Clone)]
333pub struct Table<const R: usize, const H: usize> {
334 slots: [Slot; R],
335 len: usize,
336 holds: holds::Holds<H>,
337 counters: ClaimCounters,
338 convergence: Convergence,
339}
340
341/// The routes and holds stored, not the unused storage.
342impl<const R: usize, const H: usize> fmt::Debug for Table<R, H> {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 f.debug_struct("Table")
345 .field("routes", &self.stored())
346 .field("holds", &self.holds)
347 .field("counters", &self.counters)
348 .field("convergence", &self.convergence)
349 .finish()
350 }
351}
352
353impl<const R: usize, const H: usize> Default for Table<R, H> {
354 fn default() -> Self {
355 Self::new()
356 }
357}
358
359impl<const R: usize, const H: usize> Table<R, H> {
360 /// How many routes it can hold.
361 pub const CAPACITY: usize = R;
362
363 /// An empty table with standard convergence: all zero bits.
364 #[must_use]
365 pub const fn new() -> Self {
366 Self {
367 slots: [VACANT; R],
368 len: 0,
369 holds: holds::Holds::new(),
370 counters: ClaimCounters {
371 reject_applied: 0,
372 reject_dampened: 0,
373 reject_dampened_same_state: 0,
374 reject_dampened_hold_down: 0,
375 reject_kept: 0,
376 learned_ok: 0,
377 learned_cap_ignored: 0,
378 flap_warned: 0,
379 pending_started: 0,
380 corroborated_applied: 0,
381 pending_expired: 0,
382 disconnect_removal_ignored: 0,
383 hold_evicted: 0,
384 },
385 convergence: Convergence::Standard,
386 }
387 }
388
389 /// Forget every route, hold-down record and count, keeping the
390 /// convergence mode: what [`new`](Self::new) gives, without building a
391 /// second table.
392 pub(crate) fn clear(&mut self) {
393 self.slots[..self.len].fill(VACANT);
394 self.len = 0;
395 self.holds.clear();
396 self.counters = ClaimCounters::default();
397 }
398
399 /// Choose the convergence mode for later claims. Routes stay; a pending
400 /// claim waits for its next claim or the sweep, as before. It is a
401 /// `const fn`, so a table in a `static` can start hardened.
402 pub const fn set_convergence(&mut self, convergence: Convergence) {
403 self.convergence = convergence;
404 }
405
406 /// The convergence mode.
407 #[must_use]
408 pub const fn convergence(&self) -> Convergence {
409 self.convergence
410 }
411
412 /// The claim counts so far.
413 #[must_use]
414 pub const fn claim_counters(&self) -> ClaimCounters {
415 self.counters
416 }
417
418 /// How many routes it holds.
419 #[must_use]
420 pub const fn len(&self) -> usize {
421 self.len
422 }
423
424 /// Whether it holds none.
425 #[must_use]
426 pub const fn is_empty(&self) -> bool {
427 self.len == 0
428 }
429
430 /// Whether a new network has no room.
431 #[must_use]
432 pub const fn is_full(&self) -> bool {
433 self.len == R
434 }
435
436 /// The stored slots, sorted by network.
437 fn stored(&self) -> &[Slot] {
438 &self.slots[..self.len]
439 }
440
441 /// The index of `network`'s slot, or where it would go.
442 fn find(&self, network: u16) -> Result<usize, usize> {
443 self.stored()
444 .binary_search_by_key(&network, |slot| slot.network)
445 }
446
447 /// The slot for `network`, if it is stored.
448 fn slot_mut(&mut self, network: u16) -> Option<&mut Slot> {
449 let index = self.find(network).ok()?;
450 Some(&mut self.slots[index])
451 }
452
453 /// Store `route` for `network`, replacing any route and pending claim it
454 /// had. `false` when the network is new and there is no room.
455 fn put(&mut self, network: u16, route: Route) -> bool {
456 let slot = Slot {
457 network,
458 route,
459 pending: None,
460 };
461 match self.find(network) {
462 Ok(index) => self.slots[index] = slot,
463 Err(_) if self.len == R => return false,
464 Err(index) => {
465 // Into the free slot at the end, then back to its place one
466 // swap at a time, with no memmove (`swap_apart`).
467 self.slots[self.len] = slot;
468 for at in (index..self.len).rev() {
469 swap_apart(&mut self.slots, at, at + 1);
470 }
471 self.len += 1;
472 }
473 }
474 true
475 }
476
477 /// The route to `network`.
478 #[must_use]
479 pub fn lookup(&self, network: u16) -> Option<Route> {
480 let index = self.find(network).ok()?;
481 Some(self.slots[index].route)
482 }
483
484 /// Remove the route to `network`, with its pending claim and its
485 /// hold-down records.
486 pub fn remove(&mut self, network: u16) -> Option<Route> {
487 let index = self.find(network).ok()?;
488 let route = self.slots[index].route;
489 // The emptied slot moves to the end one swap at a time, with no
490 // memmove (`swap_apart`).
491 for at in index + 1..self.len {
492 swap_apart(&mut self.slots, at - 1, at);
493 }
494 self.len -= 1;
495 self.slots[self.len] = VACANT;
496 self.holds.forget(network);
497 Some(route)
498 }
499
500 /// Every route, in ascending network order. This and the other listings
501 /// can be cloned, so a writer can count before it writes.
502 pub fn routes(&self) -> impl Iterator<Item = (u16, &Route)> + Clone + '_ {
503 self.stored().iter().map(|slot| (slot.network, &slot.route))
504 }
505
506 /// Every network, in ascending order.
507 pub fn networks(&self) -> impl Iterator<Item = u16> + Clone + '_ {
508 self.stored().iter().map(|slot| slot.network)
509 }
510
511 /// The networks reached through `port`, in ascending order.
512 pub fn networks_on_port(&self, port: PortId) -> impl Iterator<Item = u16> + Clone + '_ {
513 self.routes()
514 .filter(move |(_, route)| route.port == port)
515 .map(|(network, _)| network)
516 }
517
518 /// The networks reached through any port but `port`, in ascending order.
519 pub fn networks_not_on_port(&self, port: PortId) -> impl Iterator<Item = u16> + Clone + '_ {
520 self.routes()
521 .filter(move |(_, route)| route.port != port)
522 .map(|(network, _)| network)
523 }
524
525 /// Install a directly attached network on `port`, replacing whatever
526 /// route it had and clearing its records. Network 0 and 0xFFFF are
527 /// ignored. `false` when nothing was installed.
528 pub fn add_direct(&mut self, network: u16, port: PortId) -> bool {
529 if is_reserved(network) {
530 return false;
531 }
532 self.holds.forget(network);
533 self.put(network, Route::direct(port))
534 }
535
536 /// Install or refresh a learned route at once, without corroboration or
537 /// flap detection: a management write, or a call on the table itself.
538 ///
539 /// It sets `last_seen`, keeps `last_used` on a refresh through the same
540 /// port and clears it otherwise, clears any mark, and re-arms the
541 /// hold-down. A direct route, network 0 and 0xFFFF are left alone.
542 /// `false` when nothing was installed.
543 pub fn add_learned(&mut self, network: u16, port: PortId, next_hop: Mac, now: Instant) -> bool {
544 if is_reserved(network) {
545 return false;
546 }
547 let last_used = match self.lookup(network) {
548 Some(existing) if existing.direct => return false,
549 Some(existing) if existing.port == port => existing.last_used,
550 _ => None,
551 };
552 self.holds.forget(network);
553 let mut route = Route::learned(port, next_hop, now);
554 route.last_used = last_used;
555 self.put(network, route)
556 }
557
558 /// Mark a learned route busy until `deadline`, unless it is unreachable:
559 /// congestion never revives a failed route. Direct routes are never
560 /// marked, and neither timestamp moves.
561 pub fn mark_busy(&mut self, network: u16, deadline: Instant) {
562 if let Some(slot) = self.slot_mut(network) {
563 busy(&mut slot.route, deadline);
564 }
565 }
566
567 /// Make a learned route reachable, lifting a busy or unreachable mark.
568 /// Direct routes and timestamps are untouched.
569 pub fn mark_available(&mut self, network: u16) {
570 if let Some(slot) = self.slot_mut(network) {
571 available(&mut slot.route);
572 }
573 }
574
575 /// [`mark_busy`](Self::mark_busy) every route served via the router at
576 /// `peer` on `port`, as a Router-Busy from it that lists no network asks,
577 /// and return how many were marked. Unreachable routes stay unreachable.
578 pub fn mark_busy_via_peer(&mut self, port: PortId, peer: &[u8], deadline: Instant) -> usize {
579 let mut marked = 0;
580 for slot in &mut self.slots[..self.len] {
581 if slot.route.served_via(port, peer) && busy(&mut slot.route, deadline) {
582 marked += 1;
583 }
584 }
585 marked
586 }
587
588 /// [`mark_available`](Self::mark_available) every route served via the
589 /// router at `peer` on `port`, as a Router-Available from it that lists
590 /// no network asks, and return how many there were.
591 pub fn mark_available_via_peer(&mut self, port: PortId, peer: &[u8]) -> usize {
592 let mut marked = 0;
593 for slot in &mut self.slots[..self.len] {
594 if slot.route.served_via(port, peer) {
595 available(&mut slot.route);
596 marked += 1;
597 }
598 }
599 marked
600 }
601
602 /// Mark a learned route unreachable, keeping it in the table. Direct
603 /// routes are untouched.
604 pub fn mark_unreachable(&mut self, network: u16) {
605 if let Some(slot) = self.slot_mut(network) {
606 let route = &mut slot.route;
607 if !route.direct {
608 route.reachability = Reachability::Unreachable;
609 route.busy_until = None;
610 }
611 }
612 }
613
614 /// Whether `network` is served by the router at `peer` on `port`: a
615 /// learned route through that port to that next router. This is the
616 /// scope of a flow-control message from `peer`, whose own list, when it
617 /// has one, is intersected with it.
618 #[must_use]
619 pub fn is_served_via_peer(&self, network: u16, port: PortId, peer: &[u8]) -> bool {
620 self.lookup(network)
621 .is_some_and(|route| route.served_via(port, peer))
622 }
623
624 /// The networks [`is_served_via_peer`](Self::is_served_via_peer) holds
625 /// for, in ascending order: the scope of a flow-control message from
626 /// `peer` that lists none.
627 pub fn routes_served_via_peer<'t>(
628 &'t self,
629 port: PortId,
630 peer: &'t [u8],
631 ) -> impl Iterator<Item = u16> + Clone + 't {
632 self.routes()
633 .filter(move |(_, route)| route.served_via(port, peer))
634 .map(|(network, _)| network)
635 }
636
637 /// Clear every busy mark whose deadline has come, returning how many.
638 pub fn clear_expired_busy(&mut self, now: Instant) -> usize {
639 let mut cleared = 0;
640 for slot in &mut self.slots[..self.len] {
641 let route = &mut slot.route;
642 if route.busy_until.is_some_and(|deadline| now >= deadline) {
643 route.reachability = Reachability::Reachable;
644 route.busy_until = None;
645 cleared += 1;
646 }
647 }
648 cleared
649 }
650
651 /// The reachability of `network` at `now`: a busy mark reads
652 /// `Reachable` from its deadline on, whether or not the sweep has run.
653 #[must_use]
654 pub fn effective_reachability(&self, network: u16, now: Instant) -> Option<Reachability> {
655 let route = self.lookup(network)?;
656 Some(match route.busy_until {
657 Some(deadline) if route.reachability == Reachability::Busy && now >= deadline => {
658 Reachability::Reachable
659 }
660 _ => route.reachability,
661 })
662 }
663
664 /// Note that a learned route carried traffic `now`. Only `last_used`
665 /// moves, so traffic never keeps a route alive.
666 pub fn touch(&mut self, network: u16, now: Instant) {
667 if let Some(slot) = self.slot_mut(network)
668 && !slot.route.direct
669 {
670 slot.route.last_used = Some(now);
671 }
672 }
673
674 /// Remove every learned route last confirmed more than `max_age` before
675 /// `now`, calling `purged` with each network in ascending order, and
676 /// return how many. Direct routes never age.
677 ///
678 /// Every call comes before anything is removed, so a `purged` that
679 /// panics leaves the table as it was.
680 pub fn purge_stale(
681 &mut self,
682 now: Instant,
683 max_age: Duration,
684 mut purged: impl FnMut(u16),
685 ) -> usize {
686 let stale = |slot: &Slot| {
687 !slot.route.direct
688 && slot
689 .route
690 .last_seen
691 .is_some_and(|seen| now.saturating_duration_since(seen) > max_age)
692 };
693 for slot in self.stored().iter().filter(|slot| stale(slot)) {
694 purged(slot.network);
695 }
696 // Each kept route is swapped forward, never copied over another,
697 // with no memmove (`swap_apart`).
698 let mut kept = 0;
699 for index in 0..self.len {
700 if stale(&self.slots[index]) {
701 self.holds.forget(self.slots[index].network);
702 } else {
703 swap_apart(&mut self.slots, kept, index);
704 kept += 1;
705 }
706 }
707 let removed = self.len - kept;
708 self.slots[kept..self.len].fill(VACANT);
709 self.len = kept;
710 removed
711 }
712}
713
714/// Mark `route` busy until `deadline` unless it is direct or unreachable;
715/// `true` when it was marked.
716fn busy(route: &mut Route, deadline: Instant) -> bool {
717 let marks = !route.direct && route.reachability != Reachability::Unreachable;
718 if marks {
719 route.reachability = Reachability::Busy;
720 route.busy_until = Some(deadline);
721 }
722 marks
723}
724
725/// Make `route` reachable unless it is direct.
726fn available(route: &mut Route) {
727 if !route.direct {
728 route.reachability = Reachability::Reachable;
729 route.busy_until = None;
730 }
731}
732
733#[cfg(test)]
734mod tests;