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 {
174 counts: [AtomicU64; 15],
175 /// The throttle on the warning for a notification that reached the
176 /// forwarding cap, one per server (#1950).
177 pub(crate) cap_warning: bacnet_transport::warn_throttle::WarnThrottle,
178}
179
180impl EventSuppressions {
181 pub(crate) fn record(&self, suppression: EventSuppression) {
182 // Relaxed and independent, like the other server counters: no
183 // ordering with the send path is promised.
184 #[allow(deprecated, reason = "try_update needs Rust 1.95; the MSRV is 1.93")]
185 let _ = self.counts[suppression as usize].fetch_update(
186 Ordering::Relaxed,
187 Ordering::Relaxed,
188 |n| Some(n.saturating_add(1)),
189 );
190 }
191
192 pub(crate) fn snapshot(&self) -> EventNotificationCounters {
193 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] =
194 self.counts.each_ref().map(|n| n.load(Ordering::Relaxed));
195 EventNotificationCounters {
196 notification_class_missing,
197 recipient_list_unavailable,
198 recipient_list_invalid,
199 recipient_list_too_long,
200 device_recipient_unbound,
201 recipient_unroutable,
202 confirmed_broadcast_recipient,
203 confirmed_no_invoke_id,
204 confirmed_rejected,
205 confirmed_unanswered,
206 unconfirmed_send_failed,
207 apdu_too_large,
208 received_not_forwarded,
209 forwarding_cap_dropped,
210 received_not_logged,
211 }
212 }
213}
214
215impl<T: TransportPort + 'static> BACnetServer<T> {
216 /// Sample the lifetime totals of undelivered event notifications,
217 /// including after `stop()`. See [`EventNotificationCounters`] for what
218 /// each field counts and what is left out.
219 pub fn event_notification_counters(&self) -> EventNotificationCounters {
220 self.event_suppressions.snapshot()
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 const ALL: [EventSuppression; 15] = [
229 EventSuppression::NotificationClassMissing,
230 EventSuppression::RecipientListUnavailable,
231 EventSuppression::RecipientListInvalid,
232 EventSuppression::RecipientListTooLong,
233 EventSuppression::DeviceRecipientUnbound,
234 EventSuppression::RecipientUnroutable,
235 EventSuppression::ConfirmedBroadcastRecipient,
236 EventSuppression::ConfirmedNoInvokeId,
237 EventSuppression::ConfirmedRejected,
238 EventSuppression::ConfirmedUnanswered,
239 EventSuppression::UnconfirmedSendFailed,
240 EventSuppression::ApduTooLarge,
241 EventSuppression::ReceivedNotForwarded,
242 EventSuppression::ForwardingCapDropped,
243 EventSuppression::ReceivedNotLogged,
244 ];
245
246 #[test]
247 fn each_suppression_moves_only_its_own_field() {
248 let counters = EventSuppressions::default();
249 for (step, suppression) in ALL.into_iter().enumerate() {
250 for _ in 0..=step {
251 counters.record(suppression);
252 }
253 }
254 assert_eq!(
255 counters.snapshot(),
256 EventNotificationCounters {
257 notification_class_missing: 1,
258 recipient_list_unavailable: 2,
259 recipient_list_invalid: 3,
260 recipient_list_too_long: 4,
261 device_recipient_unbound: 5,
262 recipient_unroutable: 6,
263 confirmed_broadcast_recipient: 7,
264 confirmed_no_invoke_id: 8,
265 confirmed_rejected: 9,
266 confirmed_unanswered: 10,
267 unconfirmed_send_failed: 11,
268 apdu_too_large: 12,
269 received_not_forwarded: 13,
270 forwarding_cap_dropped: 14,
271 received_not_logged: 15,
272 }
273 );
274 }
275
276 #[test]
277 fn counters_saturate_at_u64_max() {
278 let counters = EventSuppressions::default();
279 for counter in &counters.counts {
280 counter.store(u64::MAX - 1, Ordering::Relaxed);
281 }
282 for _ in 0..3 {
283 for suppression in ALL {
284 counters.record(suppression);
285 }
286 }
287 assert!(counters
288 .counts
289 .iter()
290 .all(|counter| counter.load(Ordering::Relaxed) == u64::MAX));
291 }
292}