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::{swap_apart, Duration, Instant, Mac, PortId};
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).
303const fn is_reserved(network: u16) -> bool {
304 network == 0 || network == 0xFFFF
305}
306
307/// A routing table of up to `R` routes and `H` hold-down records.
308///
309/// A full table refuses a new network: [`add_direct`](Self::add_direct)
310/// and [`add_learned`](Self::add_learned) return `false`, and
311/// [`learn`](Self::learn) and [`apply_claim`](Self::apply_claim) return
312/// [`LearnRefusal::Full`]. Routes it holds can still be refreshed, moved and
313/// removed. A router keeps its route cap at or below `R`, so the cap, which
314/// it counts, is met first.
315///
316/// A [`Route`] is a copy of a row, so a route changes only through the
317/// table's calls, each of which keeps its rules: [`add_direct`](Self::add_direct)
318/// and [`add_learned`](Self::add_learned) install one;
319/// [`learn`](Self::learn) and [`apply_claim`](Self::apply_claim) learn one,
320/// with flap detection and convergence; [`mark_busy`](Self::mark_busy),
321/// [`mark_available`](Self::mark_available) and
322/// [`mark_unreachable`](Self::mark_unreachable) set its state;
323/// [`touch`](Self::touch) notes its use; and [`remove`](Self::remove) and
324/// [`purge_stale`](Self::purge_stale) take it out. None of them makes a
325/// learned route direct, or a direct one learned, except by replacing it.
326///
327/// [`new`](Self::new) is a `const fn` whose result is all zero bits, so a
328/// table can live in a zero-initialized static (`.bss`).
329#[derive(Clone)]
330pub struct Table<const R: usize, const H: usize> {
331 slots: [Slot; R],
332 len: usize,
333 holds: holds::Holds<H>,
334 counters: ClaimCounters,
335 convergence: Convergence,
336}
337
338/// The routes and holds stored, not the unused storage.
339impl<const R: usize, const H: usize> fmt::Debug for Table<R, H> {
340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341 f.debug_struct("Table")
342 .field("routes", &self.stored())
343 .field("holds", &self.holds)
344 .field("counters", &self.counters)
345 .field("convergence", &self.convergence)
346 .finish()
347 }
348}
349
350impl<const R: usize, const H: usize> Default for Table<R, H> {
351 fn default() -> Self {
352 Self::new()
353 }
354}
355
356impl<const R: usize, const H: usize> Table<R, H> {
357 /// How many routes it can hold.
358 pub const CAPACITY: usize = R;
359
360 /// An empty table with standard convergence: all zero bits.
361 #[must_use]
362 pub const fn new() -> Self {
363 Self {
364 slots: [VACANT; R],
365 len: 0,
366 holds: holds::Holds::new(),
367 counters: ClaimCounters {
368 reject_applied: 0,
369 reject_dampened: 0,
370 reject_dampened_same_state: 0,
371 reject_dampened_hold_down: 0,
372 reject_kept: 0,
373 learned_ok: 0,
374 learned_cap_ignored: 0,
375 flap_warned: 0,
376 pending_started: 0,
377 corroborated_applied: 0,
378 pending_expired: 0,
379 disconnect_removal_ignored: 0,
380 hold_evicted: 0,
381 },
382 convergence: Convergence::Standard,
383 }
384 }
385
386 /// Forget every route, hold-down record and count, keeping the
387 /// convergence mode: what [`new`](Self::new) gives, without building a
388 /// second table.
389 pub(crate) fn clear(&mut self) {
390 self.slots[..self.len].fill(VACANT);
391 self.len = 0;
392 self.holds.clear();
393 self.counters = ClaimCounters::default();
394 }
395
396 /// Choose the convergence mode for later claims. Routes stay; a pending
397 /// claim waits for its next claim or the sweep, as before. It is a
398 /// `const fn`, so a table in a `static` can start hardened.
399 pub const fn set_convergence(&mut self, convergence: Convergence) {
400 self.convergence = convergence;
401 }
402
403 /// The convergence mode.
404 #[must_use]
405 pub const fn convergence(&self) -> Convergence {
406 self.convergence
407 }
408
409 /// The claim counts so far.
410 #[must_use]
411 pub const fn claim_counters(&self) -> ClaimCounters {
412 self.counters
413 }
414
415 /// How many routes it holds.
416 #[must_use]
417 pub const fn len(&self) -> usize {
418 self.len
419 }
420
421 /// Whether it holds none.
422 #[must_use]
423 pub const fn is_empty(&self) -> bool {
424 self.len == 0
425 }
426
427 /// Whether a new network has no room.
428 #[must_use]
429 pub const fn is_full(&self) -> bool {
430 self.len == R
431 }
432
433 /// The stored slots, sorted by network.
434 fn stored(&self) -> &[Slot] {
435 &self.slots[..self.len]
436 }
437
438 /// The index of `network`'s slot, or where it would go.
439 fn find(&self, network: u16) -> Result<usize, usize> {
440 self.stored()
441 .binary_search_by_key(&network, |slot| slot.network)
442 }
443
444 /// The slot for `network`, if it is stored.
445 fn slot_mut(&mut self, network: u16) -> Option<&mut Slot> {
446 let index = self.find(network).ok()?;
447 Some(&mut self.slots[index])
448 }
449
450 /// Store `route` for `network`, replacing any route and pending claim it
451 /// had. `false` when the network is new and there is no room.
452 fn put(&mut self, network: u16, route: Route) -> bool {
453 let slot = Slot {
454 network,
455 route,
456 pending: None,
457 };
458 match self.find(network) {
459 Ok(index) => self.slots[index] = slot,
460 Err(_) if self.len == R => return false,
461 Err(index) => {
462 // Into the free slot at the end, then back to its place one
463 // swap at a time, with no memmove (`swap_apart`).
464 self.slots[self.len] = slot;
465 for at in (index..self.len).rev() {
466 swap_apart(&mut self.slots, at, at + 1);
467 }
468 self.len += 1;
469 }
470 }
471 true
472 }
473
474 /// The route to `network`.
475 #[must_use]
476 pub fn lookup(&self, network: u16) -> Option<Route> {
477 let index = self.find(network).ok()?;
478 Some(self.slots[index].route)
479 }
480
481 /// Remove the route to `network`, with its pending claim and its
482 /// hold-down records.
483 pub fn remove(&mut self, network: u16) -> Option<Route> {
484 let index = self.find(network).ok()?;
485 let route = self.slots[index].route;
486 // The emptied slot moves to the end one swap at a time, with no
487 // memmove (`swap_apart`).
488 for at in index + 1..self.len {
489 swap_apart(&mut self.slots, at - 1, at);
490 }
491 self.len -= 1;
492 self.slots[self.len] = VACANT;
493 self.holds.forget(network);
494 Some(route)
495 }
496
497 /// Every route, in ascending network order. This and the other listings
498 /// can be cloned, so a writer can count before it writes.
499 pub fn routes(&self) -> impl Iterator<Item = (u16, &Route)> + Clone + '_ {
500 self.stored().iter().map(|slot| (slot.network, &slot.route))
501 }
502
503 /// Every network, in ascending order.
504 pub fn networks(&self) -> impl Iterator<Item = u16> + Clone + '_ {
505 self.stored().iter().map(|slot| slot.network)
506 }
507
508 /// The networks reached through `port`, in ascending order.
509 pub fn networks_on_port(&self, port: PortId) -> impl Iterator<Item = u16> + Clone + '_ {
510 self.routes()
511 .filter(move |(_, route)| route.port == port)
512 .map(|(network, _)| network)
513 }
514
515 /// The networks reached through any port but `port`, in ascending order.
516 pub fn networks_not_on_port(&self, port: PortId) -> impl Iterator<Item = u16> + Clone + '_ {
517 self.routes()
518 .filter(move |(_, route)| route.port != port)
519 .map(|(network, _)| network)
520 }
521
522 /// Install a directly attached network on `port`, replacing whatever
523 /// route it had and clearing its records. Network 0 and 0xFFFF are
524 /// ignored. `false` when nothing was installed.
525 pub fn add_direct(&mut self, network: u16, port: PortId) -> bool {
526 if is_reserved(network) {
527 return false;
528 }
529 self.holds.forget(network);
530 self.put(network, Route::direct(port))
531 }
532
533 /// Install or refresh a learned route at once, without corroboration or
534 /// flap detection: a management write, or a call on the table itself.
535 ///
536 /// It sets `last_seen`, keeps `last_used` on a refresh through the same
537 /// port and clears it otherwise, clears any mark, and re-arms the
538 /// hold-down. A direct route, network 0 and 0xFFFF are left alone.
539 /// `false` when nothing was installed.
540 pub fn add_learned(&mut self, network: u16, port: PortId, next_hop: Mac, now: Instant) -> bool {
541 if is_reserved(network) {
542 return false;
543 }
544 let last_used = match self.lookup(network) {
545 Some(existing) if existing.direct => return false,
546 Some(existing) if existing.port == port => existing.last_used,
547 _ => None,
548 };
549 self.holds.forget(network);
550 let mut route = Route::learned(port, next_hop, now);
551 route.last_used = last_used;
552 self.put(network, route)
553 }
554
555 /// Mark a learned route busy until `deadline`, unless it is unreachable:
556 /// congestion never revives a failed route. Direct routes are never
557 /// marked, and neither timestamp moves.
558 pub fn mark_busy(&mut self, network: u16, deadline: Instant) {
559 if let Some(slot) = self.slot_mut(network) {
560 busy(&mut slot.route, deadline);
561 }
562 }
563
564 /// Make a learned route reachable, lifting a busy or unreachable mark.
565 /// Direct routes and timestamps are untouched.
566 pub fn mark_available(&mut self, network: u16) {
567 if let Some(slot) = self.slot_mut(network) {
568 available(&mut slot.route);
569 }
570 }
571
572 /// [`mark_busy`](Self::mark_busy) every route served via the router at
573 /// `peer` on `port`, as a Router-Busy from it that lists no network asks,
574 /// and return how many were marked. Unreachable routes stay unreachable.
575 pub fn mark_busy_via_peer(&mut self, port: PortId, peer: &[u8], deadline: Instant) -> usize {
576 let mut marked = 0;
577 for slot in &mut self.slots[..self.len] {
578 if slot.route.served_via(port, peer) && busy(&mut slot.route, deadline) {
579 marked += 1;
580 }
581 }
582 marked
583 }
584
585 /// [`mark_available`](Self::mark_available) every route served via the
586 /// router at `peer` on `port`, as a Router-Available from it that lists
587 /// no network asks, and return how many there were.
588 pub fn mark_available_via_peer(&mut self, port: PortId, peer: &[u8]) -> usize {
589 let mut marked = 0;
590 for slot in &mut self.slots[..self.len] {
591 if slot.route.served_via(port, peer) {
592 available(&mut slot.route);
593 marked += 1;
594 }
595 }
596 marked
597 }
598
599 /// Mark a learned route unreachable, keeping it in the table. Direct
600 /// routes are untouched.
601 pub fn mark_unreachable(&mut self, network: u16) {
602 if let Some(slot) = self.slot_mut(network) {
603 let route = &mut slot.route;
604 if !route.direct {
605 route.reachability = Reachability::Unreachable;
606 route.busy_until = None;
607 }
608 }
609 }
610
611 /// Whether `network` is served by the router at `peer` on `port`: a
612 /// learned route through that port to that next router. This is the
613 /// scope of a flow-control message from `peer`, whose own list, when it
614 /// has one, is intersected with it.
615 #[must_use]
616 pub fn is_served_via_peer(&self, network: u16, port: PortId, peer: &[u8]) -> bool {
617 self.lookup(network)
618 .is_some_and(|route| route.served_via(port, peer))
619 }
620
621 /// The networks [`is_served_via_peer`](Self::is_served_via_peer) holds
622 /// for, in ascending order: the scope of a flow-control message from
623 /// `peer` that lists none.
624 pub fn routes_served_via_peer<'t>(
625 &'t self,
626 port: PortId,
627 peer: &'t [u8],
628 ) -> impl Iterator<Item = u16> + Clone + 't {
629 self.routes()
630 .filter(move |(_, route)| route.served_via(port, peer))
631 .map(|(network, _)| network)
632 }
633
634 /// Clear every busy mark whose deadline has come, returning how many.
635 pub fn clear_expired_busy(&mut self, now: Instant) -> usize {
636 let mut cleared = 0;
637 for slot in &mut self.slots[..self.len] {
638 let route = &mut slot.route;
639 if route.busy_until.is_some_and(|deadline| now >= deadline) {
640 route.reachability = Reachability::Reachable;
641 route.busy_until = None;
642 cleared += 1;
643 }
644 }
645 cleared
646 }
647
648 /// The reachability of `network` at `now`: a busy mark reads
649 /// `Reachable` from its deadline on, whether or not the sweep has run.
650 #[must_use]
651 pub fn effective_reachability(&self, network: u16, now: Instant) -> Option<Reachability> {
652 let route = self.lookup(network)?;
653 Some(match route.busy_until {
654 Some(deadline) if route.reachability == Reachability::Busy && now >= deadline => {
655 Reachability::Reachable
656 }
657 _ => route.reachability,
658 })
659 }
660
661 /// Note that a learned route carried traffic `now`. Only `last_used`
662 /// moves, so traffic never keeps a route alive.
663 pub fn touch(&mut self, network: u16, now: Instant) {
664 if let Some(slot) = self.slot_mut(network) {
665 if !slot.route.direct {
666 slot.route.last_used = Some(now);
667 }
668 }
669 }
670
671 /// Remove every learned route last confirmed more than `max_age` before
672 /// `now`, calling `purged` with each network in ascending order, and
673 /// return how many. Direct routes never age.
674 ///
675 /// Every call comes before anything is removed, so a `purged` that
676 /// panics leaves the table as it was.
677 pub fn purge_stale(
678 &mut self,
679 now: Instant,
680 max_age: Duration,
681 mut purged: impl FnMut(u16),
682 ) -> usize {
683 let stale = |slot: &Slot| {
684 !slot.route.direct
685 && slot
686 .route
687 .last_seen
688 .is_some_and(|seen| now.saturating_duration_since(seen) > max_age)
689 };
690 for slot in self.stored().iter().filter(|slot| stale(slot)) {
691 purged(slot.network);
692 }
693 // Each kept route is swapped forward, never copied over another,
694 // with no memmove (`swap_apart`).
695 let mut kept = 0;
696 for index in 0..self.len {
697 if stale(&self.slots[index]) {
698 self.holds.forget(self.slots[index].network);
699 } else {
700 swap_apart(&mut self.slots, kept, index);
701 kept += 1;
702 }
703 }
704 let removed = self.len - kept;
705 self.slots[kept..self.len].fill(VACANT);
706 self.len = kept;
707 removed
708 }
709}
710
711/// Mark `route` busy until `deadline` unless it is direct or unreachable;
712/// `true` when it was marked.
713fn busy(route: &mut Route, deadline: Instant) -> bool {
714 let marks = !route.direct && route.reachability != Reachability::Unreachable;
715 if marks {
716 route.reachability = Reachability::Busy;
717 route.busy_until = Some(deadline);
718 }
719 marks
720}
721
722/// Make `route` reachable unless it is direct.
723fn available(route: &mut Route) {
724 if !route.direct {
725 route.reachability = Reachability::Reachable;
726 route.busy_until = None;
727 }
728}
729
730#[cfg(test)]
731mod tests;