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