Skip to main content

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}