bacnet_objects/notification_class/persistence.rs
1//! Keeping a Notification Class's written Recipient_List across a restart
2//! (Clause 12.21.8, #1315).
3//!
4//! As for the Notification Forwarder
5//! ([`NotificationForwarderPersistence`](crate::notification_forwarder::NotificationForwarderPersistence)),
6//! the storage belongs to the application. The class loads it once, when it
7//! is built, and saves the list on its own writer thread each time a write
8//! changes it. The bundled server waits for that save with the database
9//! guard dropped; a write nobody staged waits where it is ([`crate::durable`]
10//! lists those paths).
11
12use std::path::Path;
13
14use bacnet_encoding::constructed::{decode_destination, encode_destination_list};
15use bacnet_types::constructed::BACnetDestination;
16use bacnet_types::error::Error;
17use bacnet_types::primitives::ObjectIdentifier;
18use bytes::BytesMut;
19
20use super::MAX_RECIPIENT_LIST_DESTINATIONS;
21use crate::durable::file::ObjectFile;
22
23/// What a Notification Class keeps across a restart.
24#[derive(Clone, Debug, Default, PartialEq)]
25pub struct NotificationClassSnapshot {
26 /// Recipient_List, in list order, once a write has set it. `None` until
27 /// then: the destinations the application configures apply at each
28 /// start.
29 pub recipient_list: Option<Vec<BACnetDestination>>,
30}
31
32/// Application-owned storage for one Notification Class's Recipient_List.
33///
34/// The class calls it from its writer thread, one call at a time. A written
35/// list lands in storage a moment before the class serves it: see [storage
36/// leads the served state](crate::durable#storage-leads-the-served-state).
37pub trait NotificationClassPersistence: Send + Sync {
38 /// What was last saved for `class`, or `None` when nothing was saved.
39 fn load(&self, class: ObjectIdentifier) -> Result<Option<NotificationClassSnapshot>, Error>;
40
41 /// Durably replace what is saved for `class` with `snapshot`.
42 fn save(
43 &self,
44 class: ObjectIdentifier,
45 snapshot: &NotificationClassSnapshot,
46 ) -> Result<(), Error>;
47}
48
49/// The format's tag. The Notification Forwarder's file (`RBNFWD01`) is a
50/// sibling: the same header, then both of a forwarder's lists.
51const MAGIC: &[u8; 8] = b"RBNNCL01";
52/// The Notification Forwarder's cap: a full Recipient_List of the longest
53/// destinations is 1,504 octets, so anything much larger is not a file this
54/// backend wrote.
55pub(super) const MAX_FILE_BYTES: u64 = 64 * 1024;
56
57/// A file holding one Notification Class's Recipient_List, replaced whole on
58/// each save, the same way as [`FileNotificationForwarderPersistence`]: a
59/// failed save leaves the previous list in place, and a completed one
60/// survives a power loss.
61///
62/// The file holds a magic tag, the class's object identifier, one octet that
63/// is 1 when a Recipient_List follows and 0 when not, and then the encoded
64/// Recipient_List. It does not coordinate between processes.
65///
66/// [`FileNotificationForwarderPersistence`]: crate::notification_forwarder::FileNotificationForwarderPersistence
67#[derive(Clone, Debug)]
68pub struct FileNotificationClassPersistence {
69 file: ObjectFile,
70}
71
72impl FileNotificationClassPersistence {
73 /// Keep the Recipient_List in the file at `path`.
74 pub fn new(path: impl AsRef<Path>) -> Result<Self, Error> {
75 Ok(Self {
76 file: ObjectFile::new(path.as_ref(), MAGIC, MAX_FILE_BYTES, "Notification Class")?,
77 })
78 }
79
80 /// The file the Recipient_List is kept in.
81 pub fn path(&self) -> &Path {
82 self.file.path()
83 }
84}
85
86impl NotificationClassPersistence for FileNotificationClassPersistence {
87 fn load(&self, class: ObjectIdentifier) -> Result<Option<NotificationClassSnapshot>, Error> {
88 let Some(body) = self.file.load(class)? else {
89 return Ok(None);
90 };
91 let recipient_list = match body.split_first() {
92 Some((0, [])) => None,
93 Some((1, list)) => Some(self.file.decode_capped(
94 list,
95 MAX_RECIPIENT_LIST_DESTINATIONS,
96 decode_destination,
97 )?),
98 _ => return Err(self.file.corrupt("has no valid header")),
99 };
100 Ok(Some(NotificationClassSnapshot { recipient_list }))
101 }
102
103 fn save(
104 &self,
105 class: ObjectIdentifier,
106 snapshot: &NotificationClassSnapshot,
107 ) -> Result<(), Error> {
108 let mut body = BytesMut::new();
109 body.extend_from_slice(&[u8::from(snapshot.recipient_list.is_some())]);
110 if let Some(list) = &snapshot.recipient_list {
111 encode_destination_list(&mut body, list)?;
112 }
113 self.file.save(class, &body)
114 }
115}