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