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 let _ = self.counts[suppression as usize].try_update(
185 Ordering::Relaxed,
186 Ordering::Relaxed,
187 |n| Some(n.saturating_add(1)),
188 );
189 }
190
191 pub(crate) fn snapshot(&self) -> EventNotificationCounters {
192 let [
193 notification_class_missing,
194 recipient_list_unavailable,
195 recipient_list_invalid,
196 recipient_list_too_long,
197 device_recipient_unbound,
198 recipient_unroutable,
199 confirmed_broadcast_recipient,
200 confirmed_no_invoke_id,
201 confirmed_rejected,
202 confirmed_unanswered,
203 unconfirmed_send_failed,
204 apdu_too_large,
205 received_not_forwarded,
206 forwarding_cap_dropped,
207 received_not_logged,
208 ] = self.counts.each_ref().map(|n| n.load(Ordering::Relaxed));
209 EventNotificationCounters {
210 notification_class_missing,
211 recipient_list_unavailable,
212 recipient_list_invalid,
213 recipient_list_too_long,
214 device_recipient_unbound,
215 recipient_unroutable,
216 confirmed_broadcast_recipient,
217 confirmed_no_invoke_id,
218 confirmed_rejected,
219 confirmed_unanswered,
220 unconfirmed_send_failed,
221 apdu_too_large,
222 received_not_forwarded,
223 forwarding_cap_dropped,
224 received_not_logged,
225 }
226 }
227}
228
229impl<T: TransportPort + 'static> BACnetServer<T> {
230 /// Sample the lifetime totals of undelivered event notifications,
231 /// including after `stop()`. See [`EventNotificationCounters`] for what
232 /// each field counts and what is left out.
233 pub fn event_notification_counters(&self) -> EventNotificationCounters {
234 self.event_suppressions.snapshot()
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 const ALL: [EventSuppression; 15] = [
243 EventSuppression::NotificationClassMissing,
244 EventSuppression::RecipientListUnavailable,
245 EventSuppression::RecipientListInvalid,
246 EventSuppression::RecipientListTooLong,
247 EventSuppression::DeviceRecipientUnbound,
248 EventSuppression::RecipientUnroutable,
249 EventSuppression::ConfirmedBroadcastRecipient,
250 EventSuppression::ConfirmedNoInvokeId,
251 EventSuppression::ConfirmedRejected,
252 EventSuppression::ConfirmedUnanswered,
253 EventSuppression::UnconfirmedSendFailed,
254 EventSuppression::ApduTooLarge,
255 EventSuppression::ReceivedNotForwarded,
256 EventSuppression::ForwardingCapDropped,
257 EventSuppression::ReceivedNotLogged,
258 ];
259
260 #[test]
261 fn each_suppression_moves_only_its_own_field() {
262 let counters = EventSuppressions::default();
263 for (step, suppression) in ALL.into_iter().enumerate() {
264 for _ in 0..=step {
265 counters.record(suppression);
266 }
267 }
268 assert_eq!(
269 counters.snapshot(),
270 EventNotificationCounters {
271 notification_class_missing: 1,
272 recipient_list_unavailable: 2,
273 recipient_list_invalid: 3,
274 recipient_list_too_long: 4,
275 device_recipient_unbound: 5,
276 recipient_unroutable: 6,
277 confirmed_broadcast_recipient: 7,
278 confirmed_no_invoke_id: 8,
279 confirmed_rejected: 9,
280 confirmed_unanswered: 10,
281 unconfirmed_send_failed: 11,
282 apdu_too_large: 12,
283 received_not_forwarded: 13,
284 forwarding_cap_dropped: 14,
285 received_not_logged: 15,
286 }
287 );
288 }
289
290 #[test]
291 fn counters_saturate_at_u64_max() {
292 let counters = EventSuppressions::default();
293 for counter in &counters.counts {
294 counter.store(u64::MAX - 1, Ordering::Relaxed);
295 }
296 for _ in 0..3 {
297 for suppression in ALL {
298 counters.record(suppression);
299 }
300 }
301 assert!(
302 counters
303 .counts
304 .iter()
305 .all(|counter| counter.load(Ordering::Relaxed) == u64::MAX)
306 );
307 }
308}