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