bacnet_objects/notification_class/saving.rs
1//! When a Notification Class saves its Recipient_List (Clause 12.21.8,
2//! #1315).
3//!
4//! A class built with [`NotificationClass::with_persistence`] saves a written
5//! Recipient_List before serving it, and refuses a write it cannot save with
6//! DEVICE / OPERATIONAL_PROBLEM, keeping the old list. This is the
7//! Notification Forwarder's scheme ([`crate::durable`]), shared with it
8//! through `StagedSaves`: the bundled server stages the write, the save runs
9//! on the class's writer thread while the database guard is dropped, and the
10//! write then takes the saved list without saving again. A
11//! WritePropertyMultiple that writes the list more than once stages one
12//! save of the last (#1423). A write that was not staged queues its save and
13//! waits for it where it is.
14//!
15//! A staged write its request releases without making is dropped, and the
16//! class at once queues a save of the list it serves, so storage goes back to
17//! that list. A staged write whose request vanished without releasing it, as
18//! when `stop()` aborts a request or an application drops a local write's
19//! future, is dropped the same way once
20//! [`STAGED_WRITE_LIFETIME`](crate::durable::STAGED_WRITE_LIFETIME) has
21//! passed since its save finished: by the next write that stages, or by the
22//! server's once-a-second operation task, which calls
23//! `advance_monotonic_time_internal`. The class saves nothing else between
24//! writes.
25//!
26//! Neither runs once the server has stopped. So `stop()`, once it has
27//! joined its requests, drops a staged write that is still held and waits
28//! for the save of the served list, and a class dropped with one still
29//! held saves the served list as it goes, unless the staged save failed
30//! (#1363). Either way a restart serves the list the class served.
31//!
32//! A written list wins over the destinations the application configures:
33//! once a write has set the list and it was saved, a rebuilt class serves the
34//! saved list, and [`add_destination`](NotificationClass::add_destination)
35//! checks a destination without adding it. The configured destinations are
36//! never saved.
37
38use std::sync::Arc;
39use std::time::Duration;
40
41use bacnet_types::constructed::BACnetDestination;
42use bacnet_types::enums::{ErrorClass, ErrorCode, PropertyIdentifier};
43use bacnet_types::error::Error;
44use bacnet_types::primitives::PropertyValue;
45
46use super::persistence::{NotificationClassPersistence, NotificationClassSnapshot};
47use super::{recipient_list, NotificationClass, MAX_RECIPIENT_LIST_DESTINATIONS};
48use crate::common;
49use crate::durable::staged::{self, StagedSaves};
50use crate::durable::{DurableWrites, PendingWrite, SaveWait, SaveWriter, StageStep};
51
52/// A class's writer and the Recipient_List write staged on it.
53pub(super) type Storage = StagedSaves<NotificationClassSnapshot, Vec<BACnetDestination>>;
54
55impl NotificationClass {
56 /// A Notification Class that keeps a written Recipient_List in
57 /// `persistence`, starting from the list saved there for this object, if
58 /// any. Its Notification_Class number defaults to the instance number,
59 /// as with [`new`](Self::new).
60 ///
61 /// Fails when the saved list cannot be loaded, or holds what a write
62 /// would refuse: more than [`MAX_RECIPIENT_LIST_DESTINATIONS`]
63 /// destinations, or an address recipient whose MAC is too long.
64 pub fn with_persistence(
65 instance: u32,
66 name: impl Into<String>,
67 persistence: Arc<dyn NotificationClassPersistence>,
68 ) -> Result<Self, Error> {
69 let mut class = Self::new(instance, name)?;
70 let oid = class.oid;
71 if let Some(list) = persistence
72 .load(oid)?
73 .and_then(|saved| saved.recipient_list)
74 {
75 if list.len() > MAX_RECIPIENT_LIST_DESTINATIONS {
76 return Err(recipient_list::no_space_error());
77 }
78 for destination in &list {
79 recipient_list::check_added(destination)?;
80 }
81 class.recipient_list = list;
82 class.recipient_list_written = true;
83 }
84 let writer = SaveWriter::new(
85 format!("bacnet-nc-{instance}-save"),
86 move |snapshot: &NotificationClassSnapshot| persistence.save(oid, snapshot),
87 move |_, result| {
88 if let Err(error) = result {
89 tracing::warn!(
90 class = %oid,
91 %error,
92 "Failed to save a Notification Class Recipient_List"
93 );
94 }
95 },
96 );
97 class.storage = Some(StagedSaves::new(writer));
98 Ok(class)
99 }
100
101 /// Whether storage holds a Recipient_List a write set, now or before a
102 /// restart. [`add_destination`](Self::add_destination) then leaves the
103 /// list alone. Always false without persistence.
104 pub fn recipient_list_saved(&self) -> bool {
105 self.storage.is_some() && self.recipient_list_written
106 }
107
108 /// Block until the saves queued so far have run. Dropping the class
109 /// waits for them too.
110 pub fn wait_for_saves(&self) {
111 if let Some(storage) = &self.storage {
112 storage.wait_idle();
113 }
114 }
115
116 /// What storage holds for the list the class serves.
117 fn snapshot(&self) -> NotificationClassSnapshot {
118 NotificationClassSnapshot {
119 recipient_list: self
120 .recipient_list_written
121 .then(|| self.recipient_list.clone()),
122 }
123 }
124
125 fn install(&mut self, list: Vec<BACnetDestination>) {
126 self.recipient_list = list;
127 self.recipient_list_written = true;
128 self.list_writes = self.list_writes.wrapping_add(1);
129 }
130
131 /// Replace Recipient_List with a written list, saving it before the class
132 /// serves it. A staged write takes the list already saved; any other
133 /// saves now. A list that cannot be saved is refused with DEVICE /
134 /// OPERATIONAL_PROBLEM and the old list stays.
135 pub(super) fn write_recipient_list(&mut self, value: PropertyValue) -> Result<(), Error> {
136 let refused =
137 |_| common::protocol_error(ErrorClass::DEVICE, ErrorCode::OPERATIONAL_PROBLEM);
138 if let Some(taken) = self.take_staged(&value) {
139 return taken.map_err(refused);
140 }
141 // A list the class refuses leaves a write staged for another
142 // request alone (#1424).
143 let next = recipient_list::decode_write(value)?;
144 if self.storage.is_some() {
145 let mut snapshot = self.snapshot();
146 apply(&mut snapshot, &next);
147 // This write will be made, so it supersedes a staged one: storage
148 // goes back to the served list ahead of its save.
149 self.with_storage(Storage::drop_staged);
150 let storage = self.storage.as_mut().expect("checked above");
151 storage.save_now(snapshot).map_err(refused)?;
152 }
153 self.install(next);
154 Ok(())
155 }
156
157 /// Take the staged step for a Recipient_List write of `value`, if it is
158 /// the next one: the class then serves its list, or the save's error
159 /// comes back. `None` when nothing staged is this write.
160 fn take_staged(&mut self, value: &PropertyValue) -> Option<Result<(), Error>> {
161 let mut storage = self.storage.take()?;
162 let base = self.list_writes;
163 let taken = storage
164 .claim(PropertyIdentifier::RECIPIENT_LIST, None, value, base)
165 .map(|claimed| claimed.map(|list| self.install(list)));
166 // Steps the request has still to take keep the list served now.
167 storage.correct(|| self.snapshot());
168 self.storage = Some(storage);
169 taken
170 }
171
172 /// From the server's operation task at monotonic `now`: drop a staged
173 /// write whose request is gone and queue the save of the served list
174 /// (see the module docs). The save runs on the writer thread.
175 pub(super) fn expire_staged_write(&mut self, now: Duration) {
176 self.with_storage(|storage| storage.expire(now));
177 }
178
179 /// Run `f` on storage; then, if `f` dropped a staged write, queue the
180 /// save that puts storage back to the served list. `None` without
181 /// persistence.
182 fn with_storage<R>(&mut self, f: impl FnOnce(&mut Storage) -> R) -> Option<R> {
183 let mut storage = self.storage.take()?;
184 let result = f(&mut storage);
185 storage.correct(|| self.snapshot());
186 self.storage = Some(storage);
187 Some(result)
188 }
189}
190
191/// Put a written Recipient_List into `snapshot`.
192fn apply(snapshot: &mut NotificationClassSnapshot, list: &[BACnetDestination]) {
193 snapshot.recipient_list = Some(list.to_vec());
194}
195
196impl DurableWrites for NotificationClass {
197 fn stage_write(
198 &mut self,
199 property: PropertyIdentifier,
200 array_index: Option<u32>,
201 value: &PropertyValue,
202 ) -> StageStep {
203 self.stage_writes(&[PendingWrite {
204 property,
205 array_index,
206 value: value.clone(),
207 }])
208 }
209
210 /// Fold the request's Recipient_List writes into one staged save
211 /// (#1423); the list each leaves replaces the one before it.
212 fn stage_writes(&mut self, writes: &[PendingWrite]) -> StageStep {
213 let listed = |write: &PendingWrite| write.property == PropertyIdentifier::RECIPIENT_LIST;
214 if self.storage.is_none() || !writes.iter().any(listed) {
215 return StageStep::Skip;
216 }
217 if let Some(wait) = self.with_storage(Storage::busy).flatten() {
218 return StageStep::Busy(wait);
219 }
220 let steps = staged::steps(writes, |_, write| {
221 if !listed(write) {
222 return Ok(None);
223 }
224 if write.array_index.is_some() {
225 return Err(common::property_is_not_an_array_error());
226 }
227 recipient_list::decode_write(write.value.clone()).map(Some)
228 });
229 let served = self.snapshot();
230 let base = self.list_writes;
231 let storage = self.storage.as_mut().expect("checked above");
232 storage.stage(steps, base, served, |snapshot, list| apply(snapshot, list))
233 }
234
235 fn release_staged_write(&mut self, staged: &SaveWait) {
236 self.with_storage(|storage| storage.release(staged));
237 }
238
239 fn has_staged_write(&self) -> bool {
240 self.storage.as_ref().is_some_and(Storage::is_staged)
241 }
242
243 fn settle_forgotten_writes(&mut self) -> Option<SaveWait> {
244 let mut storage = self.storage.take()?;
245 let wait = storage.drop_forgotten(|| self.snapshot());
246 self.storage = Some(storage);
247 Some(wait)
248 }
249}