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 crate::prelude::*;
77use core::time::Duration;
78
79use bacnet_types::constructed::BACnetDeviceObjectPropertyReference;
80use bacnet_types::enums::LoggingType;
81use bacnet_types::error::Error;
82use bacnet_types::primitives::{Date, ObjectIdentifier, Time};
83
84// The snapshot format: only the file backend reads and writes it whole.
85#[cfg_attr(not(feature = "std"), allow(dead_code))]
86mod codec;
87#[cfg(feature = "std")]
88mod file;
89pub(crate) mod store;
90
91#[cfg(feature = "std")]
92pub use file::FileLogPersistence;
93
94/// The settings that say how a Trend Log or Trend Log Multiple acquires its
95/// records, as a snapshot keeps them.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct LogAcquisitionSettings {
98 /// Logging_Type: POLLED, TRIGGERED, or on a Trend Log COV.
99 pub logging_type: LoggingType,
100 /// Log_Interval in hundredths of a second; zero while TRIGGERED or COV.
101 pub log_interval: u32,
102 /// Align_Intervals.
103 pub align_intervals: bool,
104 /// Interval_Offset in hundredths of a second.
105 pub interval_offset: u32,
106}
107
108/// The durable state of one Trend Log (`R` = `BACnetLogRecord`), Trend Log
109/// Multiple (`BACnetLogMultipleRecord`) or Event Log (`BACnetEventLogRecord`).
110#[derive(Debug, Clone, PartialEq)]
111pub struct LogSnapshot<R> {
112 /// The log this state belongs to.
113 pub object_identifier: ObjectIdentifier,
114 /// Nonzero, and one higher on each commit, so a two-slot store can tell
115 /// the newer slot.
116 pub generation: u64,
117 /// Buffer_Size.
118 pub buffer_size: u32,
119 /// Enable.
120 pub log_enable: bool,
121 /// Stop_When_Full.
122 pub stop_when_full: bool,
123 /// Total_Record_Count; the newest record carries this number.
124 pub total_record_count: u32,
125 /// The resident records, oldest first.
126 pub records: Vec<R>,
127 /// Last_Notify_Record.
128 pub last_notify_record: u32,
129 /// The Total_Record_Count Records_Since_Notification counts from: the
130 /// count at the last report, purge or start of detection.
131 pub records_since_notification_from: u32,
132 /// Notification_Threshold: the records that make a BUFFER_READY report,
133 /// zero for none.
134 pub notification_threshold: u32,
135 /// Start_Time, as a local date and time; every field unspecified for an
136 /// open start.
137 pub start_time: (Date, Time),
138 /// Stop_Time, under the same rules as `start_time`.
139 pub stop_time: (Date, Time),
140 /// The acquisition settings of a Trend Log or Trend Log Multiple; `None`
141 /// for an Event Log, which has none.
142 pub acquisition: Option<LogAcquisitionSettings>,
143 /// Log_DeviceObjectProperty: at most one reference on a Trend Log, up to
144 /// [`MAX_LOG_DEVICE_OBJECT_PROPERTIES`](crate::trend::MAX_LOG_DEVICE_OBJECT_PROPERTIES)
145 /// on a Trend Log Multiple, none on an Event Log.
146 pub log_device_object_property: Vec<BACnetDeviceObjectPropertyReference>,
147}
148
149/// Application-owned storage for one log's [`LogSnapshot`].
150///
151/// The log calls `load` once, as it opens, and `commit` from its writer
152/// thread, one call at a time.
153pub trait LogPersistence<R>: Send + Sync {
154 /// Load the newest valid snapshot for `expected_object`, or `None` when
155 /// storage holds none yet.
156 fn load(&self, expected_object: ObjectIdentifier) -> Result<Option<LogSnapshot<R>>, Error>;
157
158 /// Durably store `snapshot`, whose generation is one past the last one
159 /// committed (or loaded).
160 fn commit(&self, snapshot: &LogSnapshot<R>) -> Result<(), Error>;
161}
162
163/// When a durable log commits its snapshot: after `records` new records or
164/// `interval` of time with a change, whichever comes first, and once more
165/// as the server stops or the log is dropped.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub struct LogCommitCadence {
168 /// Commit once Total_Record_Count has moved this far past the last
169 /// commit. Zero turns the count trigger off.
170 pub records: u32,
171 /// Commit once this long has passed since the last commit and the state
172 /// changed. The server looks once a second, so it is met to within about
173 /// a second. Zero turns the time trigger off.
174 pub interval: Duration,
175}
176
177impl LogCommitCadence {
178 /// The default count trigger: 100 records.
179 pub const DEFAULT_RECORDS: u32 = 100;
180 /// The default time trigger: 60 seconds.
181 pub const DEFAULT_INTERVAL: Duration = Duration::from_secs(60);
182}
183
184impl Default for LogCommitCadence {
185 /// 100 records or 60 seconds.
186 fn default() -> Self {
187 Self {
188 records: Self::DEFAULT_RECORDS,
189 interval: Self::DEFAULT_INTERVAL,
190 }
191 }
192}
193
194#[cfg(all(test, feature = "std"))]
195mod tests;
196
197#[cfg(all(test, feature = "std"))]
198mod reopen_tests;
199
200#[cfg(all(test, feature = "std"))]
201mod cadence_tests;
202
203#[cfg(all(test, feature = "std"))]
204mod reopen_fit_tests;