Skip to main content

bacnet_server/server/
event_suppression.rs

1//! Lifetime counters for event notifications the server did not deliver (#1142).
2use super::*;
3use bacnet_objects::notification_class::RecipientLookupOutcome;
4use std::sync::atomic::AtomicU64;
5
6/// Lifetime totals of event notifications that were not delivered, each
7/// saturating at `u64::MAX`. A new server starts at zero, and the totals stay
8/// readable after `stop()`. Fields are sampled independently, not as one
9/// atomic aggregate. Each refusal also logs a warning (the forwarding cap's
10/// at most once a minute); these totals are the
11/// running signal, not an audit log.
12///
13/// The first four count transitions (event or acknowledgment notifications)
14/// whose Notification Class lookup failed closed, so no recipient received
15/// them. A class whose list is empty, or whose destinations all filter the
16/// transition out by day, time or transition, is configured behaviour and is
17/// not counted. Neither are notifications held back by DeviceCommunicationControl
18/// or Event_Enable; DCC holds a confirmed one back at a retry too, ending it
19/// there uncounted.
20///
21/// The next three count destinations that matched the transition but were
22/// skipped while their route was resolved, once per destination: the rest of
23/// the transition's destinations are still served. They are grouped by what
24/// fixes them: a Device recipient the server holds no current address for,
25/// a recipient that can never be routed as written, and a confirmed
26/// recipient at a broadcast or other group address. The warning logged with
27/// each skip names the finer reason.
28///
29/// The next three count confirmed notifications to one recipient that were
30/// never acknowledged.
31///
32/// The next one counts unconfirmed notifications whose send failed at the
33/// transport, once per destination. The transition's other destinations are
34/// still served. There is no counter for a notification that fails to
35/// encode: the committed payload and message text are validated before the
36/// destinations are walked, so a well-formed transition always encodes, and
37/// the send loop only logs and skips if that invariant is ever broken.
38///
39/// The next three concern the Notification Forwarder objects (#1225): copies
40/// too large to send unsegmented, received notifications no forwarder took,
41/// and destinations past the cap on one notification's copies (#1259).
42///
43/// The last one counts received notifications the Event Logs that collect
44/// them would have taken but for their source's allowance (#1346).
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub struct EventNotificationCounters {
47    /// No Notification Class object has the class number the event object
48    /// names.
49    pub notification_class_missing: u64,
50    /// The class exists but reading its Recipient_List failed.
51    pub recipient_list_unavailable: u64,
52    /// The class's Recipient_List did not decode as a whole; no decodable
53    /// prefix is routed.
54    pub recipient_list_invalid: u64,
55    /// The class serves more than
56    /// [`MAX_RECIPIENT_LIST_DESTINATIONS`](bacnet_objects::notification_class::MAX_RECIPIENT_LIST_DESTINATIONS)
57    /// destinations, which only a custom Notification Class object can do.
58    pub recipient_list_too_long: u64,
59    /// Device recipients skipped because the server has no current binding
60    /// for them: none was configured or observed, or the observed one has
61    /// expired, and looking for the device found none. Such a recipient's
62    /// notification waits in its device's queue for the I-Am a targeted
63    /// Who-Is asks for (#1368), behind any already waiting there; it counts
64    /// here once when the device stays silent past the APDU timeout (a
65    /// minute at most), or at once when no Who-Is may go out for it (within
66    /// a minute of one that drew nothing, or with 256 devices already being
67    /// looked for) or 1,024 notifications already wait. Both clear once the
68    /// device's I-Am is observed again or a binding is configured. A
69    /// confirmed notification whose observed binding expires before a retry
70    /// ends at that retry, its invoke ID freed, and counts here rather than
71    /// in `confirmed_unanswered` (#1371).
72    pub device_recipient_unbound: u64,
73    /// Recipients that cannot be routed as configured, so no binding or
74    /// retry delivers them: a Device recipient whose identifier is not a
75    /// Device object, or whose binding is unusable on this link, and an
76    /// address that puts a MAC on the global broadcast network (65535),
77    /// which names neither a broadcast nor one device.
78    pub recipient_unroutable: u64,
79    /// Recipients configured for confirmed notifications at a broadcast
80    /// address (local, remote or global, the link's own broadcast MAC
81    /// included), or at any other group address the link reports
82    /// ([`TransportPort::is_group_destination`], such as a B/IP multicast
83    /// address, #1493). Clause 6.3 lets only unconfirmed requests be
84    /// broadcast, and sending one unconfirmed would drop the acknowledgment
85    /// the destination asks for. No invoke ID is reserved for them.
86    pub confirmed_broadcast_recipient: u64,
87    /// Confirmed notifications not sent because no confirmed transaction
88    /// could be reserved, normally because every invoke ID was in use.
89    /// Reservations refused while the server stops are not counted.
90    pub confirmed_no_invoke_id: u64,
91    /// Confirmed notifications the recipient answered with an Error, Reject
92    /// or Abort.
93    pub confirmed_rejected: u64,
94    /// Confirmed notifications that drew no acknowledgment after the last
95    /// retry, including attempts whose send failed locally.
96    pub confirmed_unanswered: u64,
97    /// Unconfirmed notifications the transport refused to send, one per
98    /// destination: a unicast, a broadcast or a routed send that returned an
99    /// error. Nothing is retried. Confirmed sends that fail locally count in
100    /// `confirmed_unanswered` instead.
101    pub unconfirmed_send_failed: u64,
102    /// Notifications not sent to one destination because their APDU is
103    /// longer than the local APDU capacity; notifications are never sent
104    /// segmented. A forwarded copy of a notification that arrived segmented
105    /// is the usual case. The other destinations are still served.
106    pub apdu_too_large: u64,
107    /// Received ConfirmedEventNotification and UnconfirmedEventNotification
108    /// requests that decoded but that no Notification Forwarder object took.
109    /// A confirmed one is still acknowledged. One sent by global broadcast,
110    /// which forwarders ignore, and a retransmitted confirmed one, which is
111    /// acknowledged without being offered to them again, are not counted.
112    pub received_not_forwarded: u64,
113    /// Destinations a notification was not forwarded to because it already
114    /// had [`MAX_FORWARDED_DESTINATIONS`] copies on their way across this
115    /// device's forwarders, one per destination dropped. Only copies that
116    /// would be sent take room, so a destination the loop rules refuse is
117    /// neither counted here nor against the cap. Destinations go in
118    /// forwarder order, then list order, so the ones dropped are the last.
119    /// A capped notification logs a warning at most once a minute,
120    /// process-wide, and at debug level otherwise.
121    pub forwarding_cap_dropped: u64,
122    /// Received ConfirmedEventNotification and UnconfirmedEventNotification
123    /// requests not logged because their source had used its allowance of
124    /// [`RECEIVED_EVENT_LOG_RATE`] records
125    /// in the current second, one per notification whatever the number of
126    /// logs. Notifications no Event Log collects are not counted.
127    pub received_not_logged: u64,
128}
129
130/// One undelivered event notification, as counted in
131/// [`EventNotificationCounters`].
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub(crate) enum EventSuppression {
134    NotificationClassMissing,
135    RecipientListUnavailable,
136    RecipientListInvalid,
137    RecipientListTooLong,
138    DeviceRecipientUnbound,
139    RecipientUnroutable,
140    ConfirmedBroadcastRecipient,
141    ConfirmedNoInvokeId,
142    ConfirmedRejected,
143    ConfirmedUnanswered,
144    UnconfirmedSendFailed,
145    ApduTooLarge,
146    ReceivedNotForwarded,
147    ForwardingCapDropped,
148    ReceivedNotLogged,
149}
150
151impl EventSuppression {
152    /// The counter a lookup outcome moves, or `None` when the outcome
153    /// delivers or is configured behaviour.
154    pub(super) fn from_lookup(outcome: &RecipientLookupOutcome) -> Option<Self> {
155        match outcome {
156            RecipientLookupOutcome::NotificationClassMissing => {
157                Some(Self::NotificationClassMissing)
158            }
159            RecipientLookupOutcome::RecipientListUnavailable => {
160                Some(Self::RecipientListUnavailable)
161            }
162            RecipientLookupOutcome::RecipientListInvalid => Some(Self::RecipientListInvalid),
163            RecipientLookupOutcome::RecipientListTooLong => Some(Self::RecipientListTooLong),
164            RecipientLookupOutcome::NoConfiguredDestinations
165            | RecipientLookupOutcome::NoMatchingDestinations
166            | RecipientLookupOutcome::Matched(_) => None,
167        }
168    }
169}
170
171/// The server's shared storage behind [`EventNotificationCounters`].
172#[derive(Debug, Default)]
173pub(crate) struct EventSuppressions([AtomicU64; 15]);
174
175impl EventSuppressions {
176    pub(crate) fn record(&self, suppression: EventSuppression) {
177        // Relaxed and independent, like the other server counters: no
178        // ordering with the send path is promised.
179        #[allow(deprecated, reason = "try_update needs Rust 1.95; the MSRV is 1.93")]
180        let _ =
181            self.0[suppression as usize].fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| {
182                Some(n.saturating_add(1))
183            });
184    }
185
186    pub(crate) fn snapshot(&self) -> EventNotificationCounters {
187        let [notification_class_missing, recipient_list_unavailable, recipient_list_invalid, recipient_list_too_long, device_recipient_unbound, recipient_unroutable, confirmed_broadcast_recipient, confirmed_no_invoke_id, confirmed_rejected, confirmed_unanswered, unconfirmed_send_failed, apdu_too_large, received_not_forwarded, forwarding_cap_dropped, received_not_logged] =
188            self.0.each_ref().map(|n| n.load(Ordering::Relaxed));
189        EventNotificationCounters {
190            notification_class_missing,
191            recipient_list_unavailable,
192            recipient_list_invalid,
193            recipient_list_too_long,
194            device_recipient_unbound,
195            recipient_unroutable,
196            confirmed_broadcast_recipient,
197            confirmed_no_invoke_id,
198            confirmed_rejected,
199            confirmed_unanswered,
200            unconfirmed_send_failed,
201            apdu_too_large,
202            received_not_forwarded,
203            forwarding_cap_dropped,
204            received_not_logged,
205        }
206    }
207}
208
209impl<T: TransportPort + 'static> BACnetServer<T> {
210    /// Sample the lifetime totals of undelivered event notifications,
211    /// including after `stop()`. See [`EventNotificationCounters`] for what
212    /// each field counts and what is left out.
213    pub fn event_notification_counters(&self) -> EventNotificationCounters {
214        self.event_suppressions.snapshot()
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    const ALL: [EventSuppression; 15] = [
223        EventSuppression::NotificationClassMissing,
224        EventSuppression::RecipientListUnavailable,
225        EventSuppression::RecipientListInvalid,
226        EventSuppression::RecipientListTooLong,
227        EventSuppression::DeviceRecipientUnbound,
228        EventSuppression::RecipientUnroutable,
229        EventSuppression::ConfirmedBroadcastRecipient,
230        EventSuppression::ConfirmedNoInvokeId,
231        EventSuppression::ConfirmedRejected,
232        EventSuppression::ConfirmedUnanswered,
233        EventSuppression::UnconfirmedSendFailed,
234        EventSuppression::ApduTooLarge,
235        EventSuppression::ReceivedNotForwarded,
236        EventSuppression::ForwardingCapDropped,
237        EventSuppression::ReceivedNotLogged,
238    ];
239
240    #[test]
241    fn each_suppression_moves_only_its_own_field() {
242        let counters = EventSuppressions::default();
243        for (step, suppression) in ALL.into_iter().enumerate() {
244            for _ in 0..=step {
245                counters.record(suppression);
246            }
247        }
248        assert_eq!(
249            counters.snapshot(),
250            EventNotificationCounters {
251                notification_class_missing: 1,
252                recipient_list_unavailable: 2,
253                recipient_list_invalid: 3,
254                recipient_list_too_long: 4,
255                device_recipient_unbound: 5,
256                recipient_unroutable: 6,
257                confirmed_broadcast_recipient: 7,
258                confirmed_no_invoke_id: 8,
259                confirmed_rejected: 9,
260                confirmed_unanswered: 10,
261                unconfirmed_send_failed: 11,
262                apdu_too_large: 12,
263                received_not_forwarded: 13,
264                forwarding_cap_dropped: 14,
265                received_not_logged: 15,
266            }
267        );
268    }
269
270    #[test]
271    fn counters_saturate_at_u64_max() {
272        let counters = EventSuppressions::default();
273        for counter in &counters.0 {
274            counter.store(u64::MAX - 1, Ordering::Relaxed);
275        }
276        for _ in 0..3 {
277            for suppression in ALL {
278                counters.record(suppression);
279            }
280        }
281        assert!(counters
282            .0
283            .iter()
284            .all(|counter| counter.load(Ordering::Relaxed) == u64::MAX));
285    }
286}