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 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}