Skip to main content

bacnet_router_core/table/
holds.rs

1//! Received rejects and their hold-down records.
2
3use core::fmt;
4
5use super::{Reachability, Table, Timing};
6use crate::npdu::RejectMessageReason;
7use crate::{Instant, PortId, swap_apart};
8
9/// What [`Table::apply_reject`] did.
10///
11/// It is `#[non_exhaustive]`, so a new effect isn't a breaking change: a
12/// `match` needs a wildcard arm.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum RejectEffect {
16    /// It changed a learned route's state.
17    Applied,
18    /// Nothing to change: no route, a direct one, or the state already in
19    /// effect.
20    SameState,
21    /// It would change the route, but came within the hold-down of the last
22    /// change from the same arrival port.
23    HeldDown,
24    /// For a learned route, with a reason other than 1 or 2, which changes
25    /// no route (Clause 6.6.3.5).
26    Kept,
27    /// For a learned route through another router than the reject's
28    /// sender, under the router's next-hop policy (#1698): the router
29    /// didn't apply it. [`Table::apply_reject`] itself never returns this.
30    NotNextHop,
31}
32
33/// When a reject from one arrival port last changed one network's route.
34/// The crate's own: outside it, the hold-down shows only in what
35/// [`Table::apply_reject`] returns.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub(crate) struct Hold {
38    /// The network.
39    pub(crate) network: u16,
40    /// The port the reject arrived on.
41    pub(crate) port: PortId,
42    /// When it was applied.
43    pub(crate) since: Instant,
44}
45
46/// Unused pool storage: all zero bits.
47const NO_HOLD: Hold = Hold {
48    network: 0,
49    port: PortId(0),
50    since: Instant::ZERO,
51};
52
53/// Up to `H` hold-down records, unordered, at most one per network and port.
54#[derive(Clone)]
55pub(super) struct Holds<const H: usize> {
56    records: [Hold; H],
57    len: usize,
58}
59
60/// The records kept, not the unused storage.
61impl<const H: usize> fmt::Debug for Holds<H> {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        f.debug_list().entries(self.stored()).finish()
64    }
65}
66
67impl<const H: usize> Holds<H> {
68    pub(super) const fn new() -> Self {
69        Self {
70            records: [NO_HOLD; H],
71            len: 0,
72        }
73    }
74
75    fn stored(&self) -> &[Hold] {
76        &self.records[..self.len]
77    }
78
79    fn since(&self, network: u16, port: PortId) -> Option<Instant> {
80        self.stored()
81            .iter()
82            .find(|hold| hold.network == network && hold.port == port)
83            .map(|hold| hold.since)
84    }
85
86    /// Record a change at `now`, in the existing record or a new one. A full
87    /// pool gives up its oldest record; `true` when one was evicted.
88    fn record(&mut self, network: u16, port: PortId, now: Instant) -> bool {
89        let hold = Hold {
90            network,
91            port,
92            since: now,
93        };
94        let stored = &mut self.records[..self.len];
95        if let Some(existing) = stored
96            .iter_mut()
97            .find(|hold| hold.network == network && hold.port == port)
98        {
99            *existing = hold;
100            return false;
101        }
102        if self.len < H {
103            self.records[self.len] = hold;
104            self.len += 1;
105            return false;
106        }
107        if let Some(oldest) = self.records.iter_mut().min_by_key(|hold| hold.since) {
108            *oldest = hold;
109        }
110        true
111    }
112
113    /// Drop every record.
114    pub(super) fn clear(&mut self) {
115        self.records[..self.len].fill(NO_HOLD);
116        self.len = 0;
117    }
118
119    /// Drop every record for `network`: fresh learning or removal re-arms it.
120    pub(super) fn forget(&mut self, network: u16) {
121        // Each kept record is swapped forward, never copied over another:
122        // that copy linked a memmove call on ARMv6-M (`swap_apart`).
123        let mut kept = 0;
124        for index in 0..self.len {
125            if self.records[index].network != network {
126                swap_apart(&mut self.records, kept, index);
127                kept += 1;
128            }
129        }
130        self.records[kept..self.len].fill(NO_HOLD);
131        self.len = kept;
132    }
133}
134
135impl<const R: usize, const H: usize> Table<R, H> {
136    /// Apply what a received reject says about the route to `network`,
137    /// arriving on `port` with `reason` (Clause 6.6.3.5). Relaying the
138    /// reject is the router's business, not the table's.
139    ///
140    /// Reason 1 marks a learned route unreachable. Reason 2 marks it busy
141    /// for [`Timing::busy_for`], unless it is unreachable or still busy: a
142    /// repeat never extends the deadline. Any other reason leaves a learned
143    /// route as it is ([`RejectEffect::Kept`], counted in
144    /// [`ClaimCounters::reject_kept`](super::ClaimCounters::reject_kept)),
145    /// since the clause changes the table for those two only. A reject that
146    /// would change nothing, or one within [`Timing::hold_down`] of the last
147    /// change applied from the same port, is ignored and counted, and moves
148    /// neither the window nor the deadline. Direct routes never change.
149    pub fn apply_reject(
150        &mut self,
151        network: u16,
152        port: PortId,
153        reason: RejectMessageReason,
154        now: Instant,
155        timing: &Timing,
156    ) -> RejectEffect {
157        let learned = self.lookup(network).is_some_and(|route| !route.direct);
158        let other = !matches!(
159            reason,
160            RejectMessageReason::NOT_DIRECTLY_CONNECTED | RejectMessageReason::ROUTER_BUSY
161        );
162        if learned && other {
163            let kept = &mut self.counters.reject_kept;
164            *kept = kept.saturating_add(1);
165            return RejectEffect::Kept;
166        }
167        let changes = self.lookup(network).is_some_and(|route| {
168            // The effective state counts: a busy deadline may have passed
169            // before the sweep cleared it.
170            let lapsed = route.busy_until.is_some_and(|deadline| now >= deadline);
171            !route.direct
172                && match reason {
173                    RejectMessageReason::NOT_DIRECTLY_CONNECTED => {
174                        route.reachability != Reachability::Unreachable
175                    }
176                    RejectMessageReason::ROUTER_BUSY => {
177                        route.reachability != Reachability::Unreachable
178                            && (route.reachability != Reachability::Busy || lapsed)
179                    }
180                    _ => false,
181                }
182        });
183        let held_down = self
184            .holds
185            .since(network, port)
186            .is_some_and(|since| now.saturating_duration_since(since) < timing.hold_down);
187        let counters = &mut self.counters;
188        if !changes || held_down {
189            counters.reject_dampened = counters.reject_dampened.saturating_add(1);
190            return if changes {
191                counters.reject_dampened_hold_down =
192                    counters.reject_dampened_hold_down.saturating_add(1);
193                RejectEffect::HeldDown
194            } else {
195                counters.reject_dampened_same_state =
196                    counters.reject_dampened_same_state.saturating_add(1);
197                RejectEffect::SameState
198            };
199        }
200        counters.reject_applied = counters.reject_applied.saturating_add(1);
201        if reason == RejectMessageReason::NOT_DIRECTLY_CONNECTED {
202            self.mark_unreachable(network);
203        } else {
204            // Only reasons 1 and 2 change a route.
205            self.mark_busy(network, now + timing.busy_for);
206        }
207        if self.holds.record(network, port, now) {
208            self.counters.hold_evicted = self.counters.hold_evicted.saturating_add(1);
209        }
210        RejectEffect::Applied
211    }
212
213    /// When a reject from `port` last changed the route to `network`, while
214    /// that record is kept.
215    #[cfg(test)]
216    pub(crate) fn held_since(&self, network: u16, port: PortId) -> Option<Instant> {
217        self.holds.since(network, port)
218    }
219
220    /// Every hold-down record, in no particular order. Records for a network
221    /// go when it is learned afresh, made direct, removed or aged out.
222    #[cfg(test)]
223    pub(crate) fn holds(&self) -> impl Iterator<Item = Hold> + '_ {
224        self.holds.stored().iter().copied()
225    }
226}