Skip to main content

bacnet_objects/
log_persistence.rs

1//! Durable storage for the Trend Log, Trend Log Multiple and Event Log
2//! (#1541).
3//!
4//! A log built with `with_persistence` (for example
5//! [`TrendLogObject::with_persistence`]) reopens from the newest snapshot its
6//! [`LogPersistence`] holds and then commits a fresh snapshot of itself on a
7//! cadence ([`LogCommitCadence`]):
8//!
9//! - once Total_Record_Count has moved [`records`](LogCommitCadence::records)
10//!   past the count of the last commit, as a record lands;
11//! - once [`interval`](LogCommitCadence::interval) has passed since the last
12//!   commit and anything the snapshot holds has changed, as the server's
13//!   once-a-second operation pass looks at the log. After a quiet spell
14//!   longer than the interval, the first change commits at the next pass:
15//!   the interval has already run out, whichever trigger comes first;
16//! - once more when the server stops, and when the log is dropped, if
17//!   anything changed since the last commit.
18//!
19//! The defaults, 100 records or 60 seconds, bound what a power loss costs to
20//! about one interval of records, while a log polling every second writes to
21//! disk about once a minute instead of on every sample. A log that only an
22//! application drives, with no server, commits by count and when dropped.
23//!
24//! The snapshot is taken under the database guard, which is only a copy of
25//! the records and settings; the storage call runs on the log's own writer
26//! thread ([`crate::durable`]), so no file I/O happens under the guard. A
27//! commit queued while an earlier one is still waiting to start replaces it,
28//! so a slow disk costs one write of the latest state.
29//!
30//! # Reopening
31//!
32//! The snapshot gives the log back its Buffer_Size, Enable, Stop_When_Full,
33//! records and Total_Record_Count, its BUFFER_READY counting
34//! (Last_Notify_Record and where Records_Since_Notification counts from), and
35//! the settings a peer can write: Start_Time and Stop_Time, and on the trend
36//! logs Logging_Type, Log_Interval, Align_Intervals, Interval_Offset and
37//! Log_DeviceObjectProperty. Buffer_Size is the application's (peers can't
38//! write it), so the one the constructor is given wins over the saved one:
39//! a larger size keeps every record, and a smaller one keeps the newest
40//! records that fit and logs a warning naming both sizes. A Buffer_Size
41//! whose full buffer of the family's worst-case records couldn't fit a
42//! slot's 256 MiB is refused.
43//! Settings the application applies after opening replace the stored ones,
44//! so an application that configures its logs on every start should skip
45//! that for a log that [reopened](crate::trend::TrendLogObject::reopened).
46//!
47//! A reopened log then appends one LOG_INTERRUPTED status record, which
48//! tells readers samples may be missing across the restart (Clauses
49//! 12.25.14, 12.27.13 and 12.30.19). It is stamped with the device clock the
50//! first time the log can read a valid one: as it joins an `ObjectDatabase`
51//! with a clock, or at the next record or window look after that. A record
52//! that arrives while no valid clock is bound lends the interruption its own
53//! timestamp, so the record still lands after it. Calling
54//! `record_interruption` first stands in for it. A log that reopens full
55//! and stopped (Enable FALSE, as Stop_When_Full leaves it) skips the marker
56//! rather than push out its oldest record; a stopped log with room takes it,
57//! marked LOG_DISABLED too.
58//!
59//! Notification_Threshold comes back with the counts, since BUFFER_READY
60//! resumes from them.
61//!
62//! # Failed commits
63//!
64//! A commit that fails is logged and leaves the log counted as changed, so
65//! the next interval pass, or the final commit at stop or drop, tries again
66//! even if nothing else changed.
67//!
68//! # Deleting a log
69//!
70//! Storage outlives the object: a log removed from the database leaves its
71//! slots on disk, and one built again on the same storage reopens from them.
72//! Delete the slot files to start afresh.
73//!
74//! [`TrendLogObject::with_persistence`]: crate::trend::TrendLogObject::with_persistence
75
76use std::time::Duration;
77
78use bacnet_types::constructed::BACnetDeviceObjectPropertyReference;
79use bacnet_types::enums::LoggingType;
80use bacnet_types::error::Error;
81use bacnet_types::primitives::{Date, ObjectIdentifier, Time};
82
83mod codec;
84mod file;
85pub(crate) mod store;
86
87pub use file::FileLogPersistence;
88
89/// The settings that say how a Trend Log or Trend Log Multiple acquires its
90/// records, as a snapshot keeps them.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct LogAcquisitionSettings {
93    /// Logging_Type: POLLED, TRIGGERED, or on a Trend Log COV.
94    pub logging_type: LoggingType,
95    /// Log_Interval in hundredths of a second; zero while TRIGGERED or COV.
96    pub log_interval: u32,
97    /// Align_Intervals.
98    pub align_intervals: bool,
99    /// Interval_Offset in hundredths of a second.
100    pub interval_offset: u32,
101}
102
103/// The durable state of one Trend Log (`R` = `BACnetLogRecord`), Trend Log
104/// Multiple (`BACnetLogMultipleRecord`) or Event Log (`BACnetEventLogRecord`).
105#[derive(Debug, Clone, PartialEq)]
106pub struct LogSnapshot<R> {
107    /// The log this state belongs to.
108    pub object_identifier: ObjectIdentifier,
109    /// Nonzero, and one higher on each commit, so a two-slot store can tell
110    /// the newer slot.
111    pub generation: u64,
112    /// Buffer_Size.
113    pub buffer_size: u32,
114    /// Enable.
115    pub log_enable: bool,
116    /// Stop_When_Full.
117    pub stop_when_full: bool,
118    /// Total_Record_Count; the newest record carries this number.
119    pub total_record_count: u32,
120    /// The resident records, oldest first.
121    pub records: Vec<R>,
122    /// Last_Notify_Record.
123    pub last_notify_record: u32,
124    /// The Total_Record_Count Records_Since_Notification counts from: the
125    /// count at the last report, purge or start of detection.
126    pub records_since_notification_from: u32,
127    /// Notification_Threshold: the records that make a BUFFER_READY report,
128    /// zero for none.
129    pub notification_threshold: u32,
130    /// Start_Time, as a local date and time; every field unspecified for an
131    /// open start.
132    pub start_time: (Date, Time),
133    /// Stop_Time, under the same rules as `start_time`.
134    pub stop_time: (Date, Time),
135    /// The acquisition settings of a Trend Log or Trend Log Multiple; `None`
136    /// for an Event Log, which has none.
137    pub acquisition: Option<LogAcquisitionSettings>,
138    /// Log_DeviceObjectProperty: at most one reference on a Trend Log, up to
139    /// [`MAX_LOG_DEVICE_OBJECT_PROPERTIES`](crate::trend::MAX_LOG_DEVICE_OBJECT_PROPERTIES)
140    /// on a Trend Log Multiple, none on an Event Log.
141    pub log_device_object_property: Vec<BACnetDeviceObjectPropertyReference>,
142}
143
144/// Application-owned storage for one log's [`LogSnapshot`].
145///
146/// The log calls `load` once, as it opens, and `commit` from its writer
147/// thread, one call at a time.
148pub trait LogPersistence<R>: Send + Sync {
149    /// Load the newest valid snapshot for `expected_object`, or `None` when
150    /// storage holds none yet.
151    fn load(&self, expected_object: ObjectIdentifier) -> Result<Option<LogSnapshot<R>>, Error>;
152
153    /// Durably store `snapshot`, whose generation is one past the last one
154    /// committed (or loaded).
155    fn commit(&self, snapshot: &LogSnapshot<R>) -> Result<(), Error>;
156}
157
158/// When a durable log commits its snapshot: after `records` new records or
159/// `interval` of time with a change, whichever comes first, and once more
160/// as the server stops or the log is dropped.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub struct LogCommitCadence {
163    /// Commit once Total_Record_Count has moved this far past the last
164    /// commit. Zero turns the count trigger off.
165    pub records: u32,
166    /// Commit once this long has passed since the last commit and the state
167    /// changed. The server looks once a second, so it is met to within about
168    /// a second. Zero turns the time trigger off.
169    pub interval: Duration,
170}
171
172impl LogCommitCadence {
173    /// The default count trigger: 100 records.
174    pub const DEFAULT_RECORDS: u32 = 100;
175    /// The default time trigger: 60 seconds.
176    pub const DEFAULT_INTERVAL: Duration = Duration::from_secs(60);
177}
178
179impl Default for LogCommitCadence {
180    /// 100 records or 60 seconds.
181    fn default() -> Self {
182        Self {
183            records: Self::DEFAULT_RECORDS,
184            interval: Self::DEFAULT_INTERVAL,
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests;
191
192#[cfg(test)]
193mod reopen_tests;
194
195#[cfg(test)]
196mod cadence_tests;
197
198#[cfg(test)]
199mod reopen_fit_tests;