Skip to main content

bacnet_server/server/
event_forwarding.rs

1//! Event notifications through this device's Notification Forwarder objects
2//! (Clause 12.51).
3//!
4//! Two paths reach the forwarders. A ConfirmedEventNotification or
5//! UnconfirmedEventNotification the server receives is offered to them as it
6//! arrived. A notification one of this device's own objects generates is
7//! offered when its Notification Class names this device's Device object as
8//! a recipient, with that recipient's process identifier. Each forwarder that
9//! takes the notification ([`forwarding_targets`]) sends it on to its
10//! destinations, with only the process identifier changed, through the
11//! server's one send path.
12//!
13//! A destination that names this device again hands the copy to the
14//! forwarders that have not yet taken this notification, so forwarders can
15//! chain within the device without a copy going round twice. Across such a
16//! chain each destination (recipient, process identifier and confirmation)
17//! gets one copy, however many forwarders name it.
18//!
19//! A received notification that no forwarder takes is still acknowledged
20//! when it came confirmed; it counts in
21//! [`EventNotificationCounters::received_not_forwarded`].
22//!
23//! One notification goes to at most [`MAX_FORWARDED_DESTINATIONS`]
24//! destinations across every forwarder in such a chain, counting only the
25//! copies the loop and route rules let through ([`ForwardingBudget`]); the
26//! rest are dropped and counted in
27//! [`EventNotificationCounters::forwarding_cap_dropped`]. A local
28//! notification that names this device's Device object under several
29//! process identifiers is one notification, with one cap.
30//!
31//! The loop rules that need a destination's route are applied as each copy is
32//! sent ([`ForwardOrigin::admits`]). The server is one node on one network, so a
33//! received notification always arrives through Port_ID 0, and its network is
34//! the local one. Once the server knows that network's number, configured on
35//! the registered Network Port or learned from Network-Number-Is with or
36//! without one, the send path reads it from the network layer and turns a
37//! route naming it into a local route ([`RecipientRoute::localize`]). The loop
38//! rules see that local route, and the copy goes with no DNET.
39
40use super::event_recipient_route::{RecipientRoute, system_utc_recipient_filter_time};
41use super::event_send::OutboundNotification;
42use super::event_suppression::EventSuppression;
43use super::*;
44use bacnet_encoding::constructed::encode_event_notification;
45use bacnet_objects::notification_class::MAX_RECIPIENT_LIST_DESTINATIONS;
46use bacnet_objects::notification_forwarder::{ForwardingInput, forwarding_targets};
47use bacnet_objects::subscribed_recipients::MAX_SUBSCRIBED_RECIPIENTS;
48use bacnet_services::alarm_event::ForwardedEventNotification;
49use bacnet_types::constructed::BACnetRecipient;
50use std::sync::atomic::AtomicUsize;
51
52/// The Port_ID of the one network port this server receives through: a node
53/// that does not route numbers its port 0 (Clause 12.51.11).
54const RECEIVING_PORT: u8 = 0;
55
56/// The most destinations one notification is forwarded to, across every
57/// Notification Forwarder in this device that takes it: as many as one
58/// forwarder's full Recipient_List and Subscribed_Recipients name, so no
59/// single forwarder is cut short. It bounds how far one received notification
60/// can multiply. Only copies that would be sent count: a destination the
61/// loop rules refuse, whose route is skipped, or whose copy is too large is
62/// not. Destinations naming this device's own Device object, which hand the
63/// notification on within the device, do not count either. The destinations
64/// past the cap count in [`EventNotificationCounters::forwarding_cap_dropped`].
65pub const MAX_FORWARDED_DESTINATIONS: usize =
66    MAX_RECIPIENT_LIST_DESTINATIONS + MAX_SUBSCRIBED_RECIPIENTS;
67
68/// What is left of one notification's [`MAX_FORWARDED_DESTINATIONS`],
69/// shared by every copy it makes. The send path takes room for a copy only
70/// once the copy passed every rule and is about to go out.
71pub(super) struct ForwardingBudget {
72    remaining: AtomicUsize,
73    dropped: AtomicUsize,
74}
75
76impl ForwardingBudget {
77    fn new() -> Self {
78        Self {
79            remaining: AtomicUsize::new(MAX_FORWARDED_DESTINATIONS),
80            dropped: AtomicUsize::new(0),
81        }
82    }
83
84    /// Take room for one more copy, or, when none is left, note it as
85    /// dropped and return `false`. Each take is one atomic update, and no
86    /// other memory is ordered by it, so relaxed ordering is enough, a copy
87    /// that waited for its device's I-Am taking room from its own task
88    /// included (#1368).
89    pub(super) fn take(&self) -> bool {
90        let taken = self
91            .remaining
92            .try_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_sub(1))
93            .is_ok();
94        if !taken {
95            self.dropped.fetch_add(1, Ordering::Relaxed);
96        }
97        taken
98    }
99
100    fn dropped(&self) -> usize {
101        self.dropped.load(Ordering::Relaxed)
102    }
103}
104
105/// How a notification reached this device's forwarders.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub(super) enum ForwardOrigin {
108    /// One of this device's own objects generated it.
109    Local,
110    /// It arrived from the network.
111    Received(Reception),
112}
113
114/// How a received notification was addressed.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub(super) struct Reception {
117    /// Sent to a broadcast or multicast address, so every node on the
118    /// receiving network already has it.
119    pub(super) group: bool,
120    /// Sent by global broadcast.
121    pub(super) global: bool,
122}
123
124impl Reception {
125    /// A notification addressed to this device alone.
126    pub(super) const UNICAST: Self = Self {
127        group: false,
128        global: false,
129    };
130
131    /// The addressing of an unconfirmed request as the network layer saw it.
132    pub(super) fn of(received: &bacnet_network::layer::ReceivedApdu) -> Self {
133        Self {
134            group: received.is_group,
135            global: received.global_broadcast,
136        }
137    }
138}
139
140/// Where a route takes a copy, as the loop rules see it.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142enum Reach {
143    /// Every network, by global broadcast.
144    Everywhere,
145    /// Every node on the local network.
146    LocalBroadcast,
147    /// One node on the local network.
148    LocalNode,
149    /// A remote network, or a node on one.
150    Elsewhere,
151}
152
153impl Reach {
154    /// Classify `route`. The send path has already turned a route naming
155    /// the local network's number into a local one
156    /// ([`RecipientRoute::localize`]), so a route that still names a network
157    /// names a remote one.
158    fn of(route: &RecipientRoute) -> Self {
159        match route {
160            RecipientRoute::GlobalBroadcast => Self::Everywhere,
161            RecipientRoute::LocalBroadcast => Self::LocalBroadcast,
162            RecipientRoute::LocalUnicast(_) | RecipientRoute::BoundLocalUnicast { .. } => {
163                Self::LocalNode
164            }
165            _ => Self::Elsewhere,
166        }
167    }
168}
169
170impl ForwardOrigin {
171    /// Whether a forwarded copy may go where `reach` says (Clause 12.51). No
172    /// copy goes by global broadcast. A received notification is not
173    /// broadcast back onto the network it arrived from, and one that arrived
174    /// by broadcast goes to no node on that network, since each already has
175    /// it.
176    fn admits(self, reach: Reach) -> bool {
177        match (self, reach) {
178            (_, Reach::Everywhere) => false,
179            (ForwardOrigin::Local, _) => true,
180            (ForwardOrigin::Received(_), Reach::LocalBroadcast) => false,
181            (ForwardOrigin::Received(reception), Reach::LocalNode) => !reception.group,
182            (ForwardOrigin::Received(_), Reach::Elsewhere) => true,
183        }
184    }
185
186    fn receiving_port(self) -> Option<u8> {
187        match self {
188            ForwardOrigin::Local => None,
189            ForwardOrigin::Received(_) => Some(RECEIVING_PORT),
190        }
191    }
192}
193
194/// Split off the recipients that name `local_device`, returning their
195/// distinct process identifiers.
196fn take_local(
197    recipients: &mut Vec<(BACnetRecipient, u32, bool)>,
198    local_device: ObjectIdentifier,
199) -> Vec<u32> {
200    let mut local = Vec::new();
201    recipients.retain(|(recipient, process_identifier, _)| {
202        let is_local = *recipient == BACnetRecipient::Device(local_device);
203        if is_local && !local.contains(process_identifier) {
204            local.push(*process_identifier);
205        }
206        !is_local
207    });
208    local
209}
210
211impl<T: TransportPort + 'static> BACnetServer<T> {
212    /// Send a notification this device generated to the recipients its
213    /// Notification Class selected. Recipients naming this device's own
214    /// Device object take it through the local forwarders instead of the
215    /// network.
216    pub(super) async fn deliver_local_notification(
217        ctx: &EventDelivery<'_, T>,
218        notification: EventNotificationRequest,
219        recipients: &[(BACnetRecipient, u32, bool)],
220    ) {
221        let mut remote = recipients.to_vec();
222        let local = take_local(&mut remote, notification.initiating_device_identifier);
223        if !remote.is_empty() {
224            let encode_for = |process_identifier| {
225                let mut targeted = notification.clone();
226                targeted.process_identifier = process_identifier;
227                let mut buf = BytesMut::new();
228                encode_event_notification(&targeted, &mut buf).map(|()| buf.freeze())
229            };
230            let outbound = OutboundNotification {
231                notification_class: notification.notification_class,
232                priority: notification.priority,
233                encode_for: &encode_for,
234                admits: Arc::new(|_| true),
235                budget: None,
236            };
237            Self::send_event_notification(ctx, &outbound, &remote).await;
238        }
239        if local.is_empty() {
240            return;
241        }
242        let forwarded = match ForwardedEventNotification::from_request(&notification) {
243            Ok(forwarded) => forwarded,
244            Err(error) => {
245                warn!(%error, "Failed to encode EventNotification for local forwarding");
246                return;
247            }
248        };
249        // One notification, whatever number of process identifiers names
250        // this device: one cap, and one copy per destination.
251        let offered = local
252            .into_iter()
253            .map(|process_identifier| forwarded.retargeted(process_identifier))
254            .collect();
255        Self::forward_event_notification(ctx, offered, ForwardOrigin::Local).await;
256    }
257
258    /// Offer one notification, as `offered` gives it to this device's
259    /// forwarders (one entry per process identifier it reaches them under),
260    /// and send each copy they ask for, up to [`MAX_FORWARDED_DESTINATIONS`]
261    /// destinations in all. Returns once every unconfirmed copy is sent and
262    /// every confirmed one is handed to its notification worker; it never
263    /// reports a delivery outcome to the caller.
264    pub(super) async fn forward_event_notification(
265        ctx: &EventDelivery<'_, T>,
266        offered: Vec<ForwardedEventNotification>,
267        origin: ForwardOrigin,
268    ) {
269        if matches!(origin, ForwardOrigin::Received(reception) if reception.global) {
270            debug!("Forwarders ignore an event notification sent by global broadcast");
271            return;
272        }
273        if ctx.comm_state.initiation_restricted() {
274            return;
275        }
276        let mut taken = Vec::new();
277        let mut sent: Vec<(BACnetRecipient, u32, bool)> = Vec::new();
278        // Shared with the copies that wait for their device's I-Am, which
279        // draw on it when they go out (#1368).
280        let budget = Arc::new(ForwardingBudget::new());
281        // A stack, so each entry's hand-offs within the device run before the
282        // next entry; reversed so the entries run in the order given.
283        let mut pending = offered;
284        pending.reverse();
285        while let Some(notification) = pending.pop() {
286            let (local_device, mut recipients) = {
287                let db = ctx.db.read().await;
288                let system_utc = std::time::SystemTime::now()
289                    .duration_since(std::time::UNIX_EPOCH)
290                    .unwrap_or_default();
291                let (today, current_time) = match db.clock_frame() {
292                    Some(frame) if frame.is_valid_actual_datetime() => (
293                        frame
294                            .day_of_week()
295                            .expect("validated ClockFrame has a day of week"),
296                        frame.local_time,
297                    ),
298                    _ => system_utc_recipient_filter_time(system_utc),
299                };
300                let input = ForwardingInput {
301                    process_identifier: notification.process_identifier,
302                    to_state: notification.to_state,
303                    locally_initiated: origin == ForwardOrigin::Local,
304                    receiving_port: origin.receiving_port(),
305                    today,
306                    current_time: &current_time,
307                };
308                let targets = forwarding_targets(&db, &input, &taken);
309                if taken.is_empty()
310                    && targets.forwarders.is_empty()
311                    && matches!(origin, ForwardOrigin::Received(_))
312                {
313                    debug!(
314                        process_identifier = notification.process_identifier,
315                        "No Notification Forwarder takes the received event notification"
316                    );
317                    ctx.suppressions
318                        .record(EventSuppression::ReceivedNotForwarded);
319                }
320                taken.extend(targets.forwarders);
321                (db.selected_device(), targets.recipients)
322            };
323            if let Some(local_device) = local_device {
324                for process_identifier in take_local(&mut recipients, local_device) {
325                    pending.push(notification.retargeted(process_identifier));
326                }
327            }
328            recipients.retain(|destination| !sent.contains(destination));
329            if recipients.is_empty() {
330                continue;
331            }
332            sent.extend(recipients.iter().cloned());
333            let encode_for = |process_identifier| Ok(notification.encode_for(process_identifier));
334            let outbound = OutboundNotification {
335                notification_class: notification.notification_class,
336                priority: notification.priority,
337                encode_for: &encode_for,
338                admits: Arc::new(move |route: &RecipientRoute| origin.admits(Reach::of(route))),
339                budget: Some(Arc::clone(&budget)),
340            };
341            Self::send_event_notification(ctx, &outbound, &recipients).await;
342        }
343        // A copy still waiting for its device's I-Am draws on the cap later;
344        // one dropped then is counted but not in this warning.
345        // One warning per interval for this server, with the capped
346        // notifications since the last; the rest log at debug (#1950).
347        let dropped = budget.dropped();
348        if dropped > 0 {
349            warn_throttled!(
350                ctx.suppressions.cap_warning,
351                dropped,
352                cap = MAX_FORWARDED_DESTINATIONS,
353                "Event notification reached the forwarding cap; destinations dropped"
354            );
355        }
356    }
357}