bacnet_objects/notification_forwarder/saving.rs
1//! When a forwarder saves its lists (Clauses 12.51.8 and 12.51.9, #1270).
2//!
3//! A write to Recipient_List or Subscribed_Recipients saves both lists before
4//! the forwarder serves the new one, and a write that cannot be saved is
5//! refused with DEVICE / OPERATIONAL_PROBLEM, leaving the old list. The bundled
6//! server stages such a write ([`DurableWrites`](crate::durable::DurableWrites)):
7//! the save runs on the forwarder's writer thread while the database guard is
8//! dropped, and the write then takes the saved lists without saving again. A
9//! WritePropertyMultiple that writes both lists, or one more than once,
10//! stages one save of what its last write leaves (#1423). A write that was
11//! not staged queues its save and waits for it where it is.
12//!
13//! A staged write its request never makes, because the request failed first
14//! or is gone, is dropped. Its save may already have put lists in storage
15//! that the forwarder never served, so the forwarder queues a save of the
16//! lists it does serve at once; a restart then serves those. That holds at
17//! the end too (#1363): the server's `stop()` drops a staged write still
18//! held and waits for that save, and a forwarder dropped with one still
19//! held saves the lists it serves before its writer stops. The drop has no
20//! chance to read the lists afresh, so it saves them as they stood at the
21//! forwarder's last storage call, at most one operation-task tick old: with
22//! a bound clock, an entry that lapsed since that tick can come back with a
23//! minute left. `stop()` reads them as they stand.
24//!
25//! Between writes Subscribed_Recipients still changes: entries lapse, and each
26//! entry's served minutes fall by one a minute. The operation task calls the
27//! object every second, and each call compares the lists the forwarder serves
28//! with the copy storage last confirmed. A lapse queues a save at once;
29//! falling minutes queue one at most once a minute. These saves coalesce on
30//! the writer, so a burst costs one save of the latest lists. A saved entry
31//! therefore never carries more than about a minute beyond what it had left,
32//! so a device that restarts often still sees each entry run out.
33//!
34//! A failed save is logged and counted in [`ForwarderSaveCounters`]. The
35//! operation task retries its saves a minute later, and storage keeps the last
36//! lists that were saved.
37
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::sync::{Arc, Mutex, PoisonError};
40use std::time::{Duration, Instant};
41
42use bacnet_types::constructed::BACnetDestination;
43use bacnet_types::enums::PropertyIdentifier;
44use bacnet_types::error::Error;
45use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
46
47use super::persistence::{ForwarderSnapshot, NotificationForwarderPersistence};
48use crate::durable::staged::{StagedSaves, Step};
49use crate::durable::{SaveWait, SaveWriter, StageStep};
50use crate::subscribed_recipients::SubscribedRecipients;
51
52/// The least time between two saves the operation task makes for falling
53/// minutes or to retry a failed save.
54const RESAVE_INTERVAL: Duration = Duration::from_secs(60);
55
56/// Lifetime totals of one forwarder's saves, shared with the object: take it
57/// with [`save_counters`](super::NotificationForwarderObject::save_counters)
58/// before the object joins a database, and read it at any time after. Each
59/// total saturates at `u64::MAX`.
60#[derive(Clone, Debug, Default)]
61pub struct ForwarderSaveCounters {
62 failed_saves: Arc<AtomicU64>,
63}
64
65impl ForwarderSaveCounters {
66 /// Saves the persistence refused: those for a write, which the write then
67 /// fails with, and those the operation task makes, which it retries.
68 pub fn failed_saves(&self) -> u64 {
69 self.failed_saves.load(Ordering::Relaxed)
70 }
71
72 fn record_failure(&self) {
73 #[allow(deprecated, reason = "try_update needs Rust 1.95; the MSRV is 1.93")]
74 let _ = self
75 .failed_saves
76 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| {
77 Some(n.saturating_add(1))
78 });
79 }
80}
81
82/// The list a write leaves, in the form the forwarder holds it.
83pub(super) enum NextList {
84 RecipientList(Vec<BACnetDestination>),
85 SubscribedRecipients(SubscribedRecipients),
86}
87
88/// Put the list `next` leaves into `snapshot`.
89pub(super) fn apply(snapshot: &mut ForwarderSnapshot, next: &NextList) {
90 match next {
91 NextList::RecipientList(list) => snapshot.recipient_list = Some(list.clone()),
92 NextList::SubscribedRecipients(store) => {
93 snapshot.subscribed_recipients = store.subscriptions();
94 }
95 }
96}
97
98/// A forwarder's storage, its writer and staged write, and what storage
99/// last confirmed.
100pub(super) struct Storage {
101 saves: StagedSaves<ForwarderSnapshot, NextList>,
102 /// The lists storage last confirmed, updated by the writer's thread.
103 confirmed: Arc<Mutex<ForwarderSnapshot>>,
104 /// When the operation task last queued a save, on the store's clock.
105 last_attempt: Option<Duration>,
106 /// A lapse wants the next operation task call to save without waiting
107 /// for the minute.
108 resave_now: bool,
109}
110
111impl Storage {
112 /// Storage for `oid` that holds `confirmed`, as loaded when the
113 /// forwarder was built.
114 pub(super) fn new(
115 oid: ObjectIdentifier,
116 persistence: Arc<dyn NotificationForwarderPersistence>,
117 confirmed: ForwarderSnapshot,
118 counters: ForwarderSaveCounters,
119 ) -> Self {
120 let confirmed = Arc::new(Mutex::new(confirmed));
121 let saved = Arc::clone(&confirmed);
122 let writer = SaveWriter::new(
123 format!("bacnet-nf-{}-save", oid.instance_number()),
124 move |snapshot: &ForwarderSnapshot| persistence.save(oid, snapshot),
125 move |snapshot, result| match result {
126 Ok(()) => {
127 *saved.lock().unwrap_or_else(PoisonError::into_inner) = snapshot.clone();
128 }
129 Err(error) => {
130 counters.record_failure();
131 tracing::warn!(
132 forwarder = %oid,
133 %error,
134 "Failed to save Notification Forwarder lists"
135 );
136 }
137 },
138 );
139 Self {
140 saves: StagedSaves::new(writer),
141 confirmed,
142 last_attempt: None,
143 resave_now: false,
144 }
145 }
146
147 /// The wait for a staged write that still holds the forwarder (see
148 /// [`StagedSaves::busy`]).
149 pub(super) fn busy(&mut self) -> Option<SaveWait> {
150 self.busy_at(Instant::now())
151 }
152
153 /// [`busy`](Self::busy), judged at `now`.
154 pub(super) fn busy_at(&mut self, now: Instant) -> Option<SaveWait> {
155 self.saves.busy_at(now)
156 }
157
158 /// Stage one save of the lists `steps` leave, made from write count
159 /// `base` on; `served` holds the lists the forwarder serves meanwhile
160 /// (see [`StagedSaves::stage`]).
161 pub(super) fn stage(
162 &mut self,
163 steps: Vec<Step<NextList>>,
164 base: u64,
165 served: ForwarderSnapshot,
166 ) -> StageStep {
167 self.saves.stage(steps, base, served, apply)
168 }
169
170 /// Take the next staged list for a write (see [`StagedSaves::claim`]).
171 /// Both lists are BACnetLISTs, written whole.
172 pub(super) fn claim(
173 &mut self,
174 property: PropertyIdentifier,
175 value: &PropertyValue,
176 base: u64,
177 ) -> Option<Result<NextList, Error>> {
178 self.saves.claim(property, None, value, base)
179 }
180
181 /// Whether a list write is staged (see [`StagedSaves::is_staged`]).
182 pub(super) fn is_staged(&self) -> bool {
183 self.saves.is_staged()
184 }
185
186 /// Drop the staged write, for a write that supersedes it (see
187 /// [`StagedSaves::drop_staged`]).
188 pub(super) fn drop_staged(&mut self) {
189 self.saves.drop_staged();
190 }
191
192 /// Save `snapshot` and wait for the outcome, for a write nobody staged.
193 pub(super) fn save_now(&mut self, snapshot: ForwarderSnapshot) -> Result<(), Error> {
194 self.saves.save_now(snapshot)
195 }
196
197 /// The request that staged a write and was given `wait` is done. A
198 /// staged write another request made is left alone.
199 pub(super) fn release(&mut self, wait: &SaveWait) {
200 self.saves.release(wait);
201 }
202
203 /// After a storage call, with `served` giving the lists the forwarder
204 /// serves (see [`StagedSaves::correct`]).
205 pub(super) fn correct(&mut self, served: impl FnOnce() -> ForwarderSnapshot) {
206 self.saves.correct(served);
207 }
208
209 /// No request is left to take or release a staged write (see
210 /// [`StagedSaves::drop_forgotten`]).
211 pub(super) fn drop_forgotten(
212 &mut self,
213 served: impl FnOnce() -> ForwarderSnapshot,
214 ) -> SaveWait {
215 self.saves.drop_forgotten(served)
216 }
217
218 /// Bring storage up to date from the operation task: at once when an
219 /// entry lapsed, and otherwise at most once every [`RESAVE_INTERVAL`]
220 /// while the served lists differ from the confirmed copy. Nothing is
221 /// queued while a staged write holds the forwarder, since its lists would
222 /// land after this save.
223 pub(super) fn keep_current(
224 &mut self,
225 now: Duration,
226 lapsed: bool,
227 current: impl FnOnce() -> ForwarderSnapshot,
228 ) {
229 if lapsed {
230 self.resave_now = true;
231 }
232 // A staged write whose request is gone lets go here, on the store's
233 // clock or the wall clock, whichever runs out first.
234 self.saves.expire(now);
235 if self.busy().is_some() {
236 return;
237 }
238 // A staged write dropped just now: save the served lists below.
239 self.resave_now |= self.saves.take_correction();
240 let current = current();
241 if current
242 == *self
243 .confirmed
244 .lock()
245 .unwrap_or_else(PoisonError::into_inner)
246 {
247 self.resave_now = false;
248 return;
249 }
250 let due = self.resave_now
251 || self
252 .last_attempt
253 .is_none_or(|at| now.saturating_sub(at) >= RESAVE_INTERVAL);
254 if !due {
255 return;
256 }
257 self.last_attempt = Some(now);
258 self.resave_now = false;
259 self.saves.submit_coalescing(current);
260 }
261
262 /// The store's clock changed: times kept on the old one no longer apply,
263 /// and a staged list holds deadlines on the old clock.
264 pub(super) fn clock_changed(&mut self) {
265 self.last_attempt = None;
266 self.saves.drop_staged();
267 }
268
269 /// Block until every queued save has run.
270 pub(super) fn wait_idle(&self) {
271 self.saves.wait_idle();
272 }
273}