Skip to main content

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}