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 crate::prelude::*;
39use core::sync::atomic::Ordering;
40
41use crate::sync::{AtomicU64, Mutex};
42use alloc::sync::Arc;
43use core::time::Duration;
44#[cfg(all(test, feature = "std"))]
45use std::time::Instant;
46
47use bacnet_types::constructed::BACnetDestination;
48use bacnet_types::enums::PropertyIdentifier;
49use bacnet_types::error::Error;
50use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
51
52use super::persistence::{ForwarderSnapshot, NotificationForwarderPersistence};
53use crate::durable::staged::{StagedSaves, Step};
54use crate::durable::{SaveWait, SaveWriter, StageStep};
55use crate::subscribed_recipients::SubscribedRecipients;
56
57/// The least time between two saves the operation task makes for falling
58/// minutes or to retry a failed save.
59const RESAVE_INTERVAL: Duration = Duration::from_secs(60);
60
61/// Lifetime totals of one forwarder's saves, shared with the object: take it
62/// with [`save_counters`](super::NotificationForwarderObject::save_counters)
63/// before the object joins a database, and read it at any time after. Each
64/// total saturates at `u64::MAX`.
65#[derive(Clone, Debug, Default)]
66pub struct ForwarderSaveCounters {
67 failed_saves: Arc<AtomicU64>,
68}
69
70impl ForwarderSaveCounters {
71 /// Saves the persistence refused: those for a write, which the write then
72 /// fails with, and those the operation task makes, which it retries.
73 pub fn failed_saves(&self) -> u64 {
74 self.failed_saves.load(Ordering::Relaxed)
75 }
76
77 fn record_failure(&self) {
78 let _ = self
79 .failed_saves
80 .try_update(Ordering::Relaxed, Ordering::Relaxed, |n| {
81 Some(n.saturating_add(1))
82 });
83 }
84}
85
86/// The list a write leaves, in the form the forwarder holds it.
87pub(super) enum NextList {
88 RecipientList(Vec<BACnetDestination>),
89 SubscribedRecipients(SubscribedRecipients),
90}
91
92/// Put the list `next` leaves into `snapshot`.
93pub(super) fn apply(snapshot: &mut ForwarderSnapshot, next: &NextList) {
94 match next {
95 NextList::RecipientList(list) => snapshot.recipient_list = Some(list.clone()),
96 NextList::SubscribedRecipients(store) => {
97 snapshot.subscribed_recipients = store.subscriptions();
98 }
99 }
100}
101
102/// A forwarder's storage, its writer and staged write, and what storage
103/// last confirmed.
104pub(super) struct Storage {
105 saves: StagedSaves<ForwarderSnapshot, NextList>,
106 /// The lists storage last confirmed, updated by the writer's thread.
107 confirmed: Arc<Mutex<ForwarderSnapshot>>,
108 /// When the operation task last queued a save, on the store's clock.
109 last_attempt: Option<Duration>,
110 /// A lapse wants the next operation task call to save without waiting
111 /// for the minute.
112 resave_now: bool,
113}
114
115impl Storage {
116 /// Storage for `oid` that holds `confirmed`, as loaded when the
117 /// forwarder was built.
118 pub(super) fn new(
119 oid: ObjectIdentifier,
120 persistence: Arc<dyn NotificationForwarderPersistence>,
121 confirmed: ForwarderSnapshot,
122 counters: ForwarderSaveCounters,
123 ) -> Self {
124 let confirmed = Arc::new(Mutex::new(confirmed));
125 let saved = Arc::clone(&confirmed);
126 let writer = SaveWriter::new(
127 format!("bacnet-nf-{}-save", oid.instance_number()),
128 move |snapshot: &ForwarderSnapshot| persistence.save(oid, snapshot),
129 move |snapshot, result| match result {
130 Ok(()) => {
131 *saved.lock() = snapshot.clone();
132 }
133 Err(error) => {
134 counters.record_failure();
135 log_warn!(
136 forwarder = %oid,
137 %error,
138 "Failed to save Notification Forwarder lists"
139 );
140 }
141 },
142 );
143 Self {
144 saves: StagedSaves::new(writer),
145 confirmed,
146 last_attempt: None,
147 resave_now: false,
148 }
149 }
150
151 /// The wait for a staged write that still holds the forwarder (see
152 /// [`StagedSaves::busy`]).
153 pub(super) fn busy(&mut self) -> Option<SaveWait> {
154 self.saves.busy()
155 }
156
157 /// [`busy`](Self::busy), judged at `now`.
158 #[cfg(all(test, feature = "std"))]
159 pub(super) fn busy_at(&mut self, now: Instant) -> Option<SaveWait> {
160 self.saves.busy_at(now)
161 }
162
163 /// Stage one save of the lists `steps` leave, made from write count
164 /// `base` on; `served` holds the lists the forwarder serves meanwhile
165 /// (see [`StagedSaves::stage`]).
166 pub(super) fn stage(
167 &mut self,
168 steps: Vec<Step<NextList>>,
169 base: u64,
170 served: ForwarderSnapshot,
171 ) -> StageStep {
172 self.saves.stage(steps, base, served, apply)
173 }
174
175 /// Take the next staged list for a write (see [`StagedSaves::claim`]).
176 /// Both lists are BACnetLISTs, written whole.
177 pub(super) fn claim(
178 &mut self,
179 property: PropertyIdentifier,
180 value: &PropertyValue,
181 base: u64,
182 ) -> Option<Result<NextList, Error>> {
183 self.saves.claim(property, None, value, base)
184 }
185
186 /// Whether a list write is staged (see [`StagedSaves::is_staged`]).
187 pub(super) fn is_staged(&self) -> bool {
188 self.saves.is_staged()
189 }
190
191 /// Drop the staged write, for a write that supersedes it (see
192 /// [`StagedSaves::drop_staged`]).
193 pub(super) fn drop_staged(&mut self) {
194 self.saves.drop_staged();
195 }
196
197 /// Save `snapshot` and wait for the outcome, for a write nobody staged.
198 pub(super) fn save_now(&mut self, snapshot: ForwarderSnapshot) -> Result<(), Error> {
199 self.saves.save_now(snapshot)
200 }
201
202 /// The request that staged a write and was given `wait` is done. A
203 /// staged write another request made is left alone.
204 pub(super) fn release(&mut self, wait: &SaveWait) {
205 self.saves.release(wait);
206 }
207
208 /// After a storage call, with `served` giving the lists the forwarder
209 /// serves (see [`StagedSaves::correct`]).
210 pub(super) fn correct(&mut self, served: impl FnOnce() -> ForwarderSnapshot) {
211 self.saves.correct(served);
212 }
213
214 /// No request is left to take or release a staged write (see
215 /// [`StagedSaves::drop_forgotten`]).
216 pub(super) fn drop_forgotten(
217 &mut self,
218 served: impl FnOnce() -> ForwarderSnapshot,
219 ) -> SaveWait {
220 self.saves.drop_forgotten(served)
221 }
222
223 /// Bring storage up to date from the operation task: at once when an
224 /// entry lapsed, and otherwise at most once every [`RESAVE_INTERVAL`]
225 /// while the served lists differ from the confirmed copy. Nothing is
226 /// queued while a staged write holds the forwarder, since its lists would
227 /// land after this save.
228 pub(super) fn keep_current(
229 &mut self,
230 now: Duration,
231 lapsed: bool,
232 current: impl FnOnce() -> ForwarderSnapshot,
233 ) {
234 if lapsed {
235 self.resave_now = true;
236 }
237 // A staged write whose request is gone lets go here, on the store's
238 // clock or the wall clock, whichever runs out first.
239 self.saves.expire(now);
240 if self.busy().is_some() {
241 return;
242 }
243 // A staged write dropped just now: save the served lists below.
244 self.resave_now |= self.saves.take_correction();
245 let current = current();
246 if current == *self.confirmed.lock() {
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}