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