Skip to main content

bacnet_objects/audit/
staging.rs

1//! Audit Log commits off the database lock (#1270).
2//!
3//! Every commit runs on the log's writer thread ([`crate::durable`]). The
4//! server stages an inbound notification batch, a Log_Enable or Buffer_Size
5//! write it receives, and an application's purge (#1238): the log builds the
6//! next snapshot and queues its commit, goes on serving the committed state,
7//! and takes the new snapshot only once the commit is durable. The server
8//! awaits the commit after dropping the database guard, so readers carry on
9//! meanwhile, and a confirmed batch is still acknowledged only after its
10//! records and receipt are stored. A commit that fails leaves the log as it
11//! was, and the request is refused.
12//!
13//! A request's Log_Enable and Buffer_Size writes are staged together: one
14//! commit holds them all, and the log keeps the state each leaves as a step
15//! the request takes when it makes that write. If the request stops before
16//! taking every step, the log keeps what it took and storage is set back to
17//! the state served.
18//!
19//! One commit is staged at a time; a request that finds one staged waits for
20//! it without the guard. Code that changes the log in place (an
21//! application's `add_record`, or a change nobody staged) first settles what
22//! is staged: a batch whose commit ran is applied if it succeeded, its
23//! outcome kept for the batch's request, and a staged change is dropped, its
24//! request then committing in place.
25
26use std::collections::VecDeque;
27use std::sync::Arc;
28
29use super::buffer::{apply_change, written_buffer_size, Timestamp};
30use super::*;
31use crate::durable::{
32    DurableWrites, Event, PendingWrite, SaveTicket, SaveWait, SaveWriter, StageStep,
33};
34
35/// Outcomes of batches settled before their requests came back for them.
36/// Past it the oldest is dropped and its request is refused with
37/// OPERATIONAL_PROBLEM. That needs more than 16 batches settled while their
38/// requests are all still away, which is not expected in practice.
39const MAX_SETTLED: usize = 16;
40
41/// What staging an Audit notification batch gave the caller.
42#[derive(Debug)]
43pub enum AuditBatchStage {
44    /// No staged commit was needed: the batch's outcome, and whether its
45    /// retained records changed. A duplicate confirmed batch, an unconfirmed
46    /// one that changed nothing, and any batch to a sink that stores at once
47    /// end here.
48    Done(ConfirmedAuditNotificationOutcome, bool),
49    /// The commit is queued. Await [`StagedAuditBatch::saved`] without the
50    /// database guard, then pass the batch to
51    /// [`AuditLogNotificationSink::finish_notification_batch`].
52    Staged(StagedAuditBatch),
53    /// Another staged commit holds the log. Await the wait, then stage the
54    /// batch again.
55    Busy(SaveWait),
56}
57
58/// A notification batch whose commit is queued on the Audit Log's writer.
59#[derive(Debug)]
60pub struct StagedAuditBatch {
61    token: u64,
62    saved: SaveWait,
63}
64
65impl StagedAuditBatch {
66    /// A future that resolves once the commit has run.
67    pub fn saved(&self) -> SaveWait {
68        self.saved.clone()
69    }
70}
71
72pub(super) enum StagedKind {
73    Batch {
74        token: u64,
75        outcome: ConfirmedAuditNotificationOutcome,
76        changed: bool,
77    },
78    /// The changes one request makes, in its order, all in one commit. The
79    /// request makes them one at a time under the guard, and the log takes
80    /// each change's step as it is made; `taken` counts the steps taken.
81    Changes { steps: VecDeque<Step>, taken: usize },
82}
83
84/// One staged change and the log as it leaves it. The last step's state is
85/// the snapshot committed.
86pub(super) struct Step {
87    change: StagedChange,
88    state: Arc<AuditLogSnapshot>,
89}
90
91/// A change a request stages and then makes under the guard, where the log
92/// takes the staged step if it still builds on the state served.
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub(super) enum StagedChange {
95    /// A Log_Enable write of this value.
96    LogEnable(bool),
97    /// A Buffer_Size write of this size.
98    BufferSize(u32),
99    /// The application's purge.
100    Purge,
101}
102
103/// The next snapshot, held aside while its commit runs.
104pub(super) struct StagedCommit {
105    snapshot: Arc<AuditLogSnapshot>,
106    ticket: SaveTicket,
107    kind: StagedKind,
108    /// Set when the commit is taken or dropped, for a request that found the
109    /// log busy.
110    released: Arc<Event>,
111}
112
113/// A batch settled before its request finished it.
114pub(super) struct Settled {
115    token: u64,
116    result: Result<(ConfirmedAuditNotificationOutcome, bool), Error>,
117}
118
119pub(super) fn operational_problem() -> Error {
120    Error::Protocol {
121        class: ErrorClass::DEVICE.to_raw() as u32,
122        code: ErrorCode::OPERATIONAL_PROBLEM.to_raw() as u32,
123    }
124}
125
126/// The writer that commits `oid`'s snapshots to `persistence`.
127pub(super) fn writer(
128    oid: ObjectIdentifier,
129    persistence: Arc<dyn AuditLogPersistence>,
130) -> SaveWriter<Arc<AuditLogSnapshot>> {
131    SaveWriter::new(
132        format!("bacnet-al-{}-save", oid.instance_number()),
133        move |snapshot: &Arc<AuditLogSnapshot>| persistence.commit(snapshot),
134        move |snapshot, result| {
135            if let Err(error) = result {
136                tracing::warn!(
137                    audit_log = %oid,
138                    generation = snapshot.generation,
139                    %error,
140                    "Failed to commit Audit Log snapshot"
141                );
142            }
143        },
144    )
145}
146
147impl AuditLogObject {
148    /// Commit `snapshot` and take it, waiting for the commit in place.
149    pub(super) fn commit_and_apply(&mut self, snapshot: AuditLogSnapshot) -> Result<(), Error> {
150        validate_snapshot(&snapshot)?;
151        let snapshot = Arc::new(snapshot);
152        self.writer.submit(Arc::clone(&snapshot)).take_outcome()?;
153        self.apply_snapshot(snapshot);
154        Ok(())
155    }
156
157    fn apply_snapshot(&mut self, snapshot: Arc<AuditLogSnapshot>) {
158        let snapshot = Arc::try_unwrap(snapshot).unwrap_or_else(|shared| (*shared).clone());
159        self.generation = snapshot.generation;
160        self.buffer_size = snapshot.capacity;
161        self.log_enable = snapshot.log_enable;
162        self.total_record_count = snapshot.total_record_count;
163        self.buffer = snapshot.records.into();
164        self.completed_receipts = snapshot.completed_receipts;
165    }
166
167    /// Before the log builds on its state in place, let a staged commit
168    /// land, waiting for it if it is still running.
169    pub(super) fn settle_staged(&mut self) {
170        let Some(commit) = self.staged.take() else {
171            return;
172        };
173        commit.released.set();
174        match commit.kind {
175            StagedKind::Batch {
176                token,
177                outcome,
178                changed,
179            } => {
180                let result = commit.ticket.take_outcome().map(|()| {
181                    self.apply_snapshot(commit.snapshot);
182                    (outcome, changed)
183                });
184                if self.settled.len() == MAX_SETTLED {
185                    self.settled.pop_front();
186                }
187                self.settled.push_back(Settled { token, result });
188            }
189            StagedKind::Changes { .. } => self.drop_staged_write(&commit.ticket),
190        }
191    }
192
193    /// Staged changes are dropped before the request took them all. Their
194    /// commit may have left storage with a snapshot the log never served;
195    /// commit the served state at the next generation so storage follows the
196    /// log again. A commit made in place right after lands later and
197    /// replaces it.
198    fn drop_staged_write(&mut self, ticket: &SaveTicket) {
199        if let Some(served) = self.served_to_put_back(ticket) {
200            self.writer.submit_coalescing(Arc::new(served));
201        }
202    }
203
204    /// The served state, at the next generation, that storage goes back to
205    /// after staged changes committed on `ticket` are dropped; `None` when
206    /// their commit failed, so storage never left the served state, or the
207    /// generations have run out.
208    fn served_to_put_back(&self, ticket: &SaveTicket) -> Option<AuditLogSnapshot> {
209        if ticket.succeeded() == Some(false) {
210            return None;
211        }
212        let generation = self.generation.checked_add(1)?;
213        let mut served = self.current_snapshot();
214        served.generation = generation;
215        Some(served)
216    }
217
218    /// The log is going away (#1363). Staged changes no request finished
219    /// are dropped as [`settle_staged`](Self::settle_staged) drops them, but
220    /// the commit of the served state is waited for here, so it lands before
221    /// the writer's own drop joins its thread, and a failure is logged. A
222    /// staged batch stays as it is: storage holds it, or will once its
223    /// commit runs, and the log takes a batch whose commit succeeded
224    /// whenever it settles one, so a restart serving it is what this log
225    /// would have done.
226    fn close_staged(&mut self) {
227        let Some(commit) = self
228            .staged
229            .take_if(|commit| matches!(commit.kind, StagedKind::Changes { .. }))
230        else {
231            return;
232        };
233        commit.released.set();
234        if let Some(served) = self.served_to_put_back(&commit.ticket) {
235            self.writer.put_back(Arc::new(served));
236        }
237    }
238
239    /// The wait for a staged commit that still holds the log: a batch's
240    /// while its commit runs, a write's until its request takes or drops it
241    /// (or [`STAGED_WRITE_LIFETIME`](crate::durable::STAGED_WRITE_LIFETIME)
242    /// after its commit finished). A batch whose commit ran is settled, and a
243    /// forgotten write dropped.
244    fn make_way(&mut self) -> Option<SaveWait> {
245        let commit = self.staged.as_ref()?;
246        let holds = match commit.kind {
247            StagedKind::Batch { .. } => !commit.ticket.is_done(),
248            StagedKind::Changes { .. } => !commit.ticket.outlived(),
249        };
250        if holds {
251            return Some(match commit.kind {
252                StagedKind::Batch { .. } => commit.ticket.wait(),
253                StagedKind::Changes { .. } => SaveWait::new(Arc::clone(&commit.released)),
254            });
255        }
256        self.settle_staged();
257        None
258    }
259
260    fn queue_staged(&mut self, snapshot: Arc<AuditLogSnapshot>, kind: StagedKind) -> SaveWait {
261        let ticket = self.writer.submit(Arc::clone(&snapshot));
262        let saved = ticket.wait();
263        self.staged = Some(StagedCommit {
264            snapshot,
265            ticket,
266            kind,
267            released: Arc::default(),
268        });
269        saved
270    }
271
272    /// Stage a notification batch: build the next snapshot as
273    /// `store_confirmed_notifications_with_change` (with a receipt) or
274    /// `store_notifications_with_change` (without) would, and queue its
275    /// commit.
276    pub(super) fn stage_notification_batch(
277        &mut self,
278        notifications: &[BACnetAuditNotification],
279        apdu_timeout_ms: u32,
280        receipt: Option<CompletedAuditReceipt>,
281    ) -> Result<AuditBatchStage, Error> {
282        if let Some(wait) = self.make_way() {
283            return Ok(AuditBatchStage::Busy(wait));
284        }
285        if !self.log_enable {
286            return Err(Error::Protocol {
287                class: ErrorClass::SERVICES.to_raw() as u32,
288                code: ErrorCode::SERVICE_REQUEST_DENIED.to_raw() as u32,
289            });
290        }
291        if notifications.is_empty() {
292            return Err(Error::OutOfRange(
293                "Audit notification batch must not be empty".into(),
294            ));
295        }
296        if let Some(receipt) = &receipt {
297            if receipt::contains(
298                &self.completed_receipts,
299                receipt.key(),
300                receipt.completed_at_unix_millis(),
301            )? {
302                return Ok(AuditBatchStage::Done(
303                    ConfirmedAuditNotificationOutcome::Duplicate,
304                    false,
305                ));
306            }
307        }
308        let mut prospective = self.snapshot_for_next_generation()?;
309        let changed =
310            self.apply_notification_batch(&mut prospective, notifications, apdu_timeout_ms)?;
311        let confirmed = receipt.is_some();
312        let outcome = match receipt {
313            Some(receipt) => receipt::insert(&mut prospective.completed_receipts, receipt)?,
314            None => ConfirmedAuditNotificationOutcome::Stored,
315        };
316        // As in place: a duplicate commits nothing, and neither does an
317        // unconfirmed batch that changes no record.
318        if outcome == ConfirmedAuditNotificationOutcome::Duplicate || (!confirmed && !changed) {
319            return Ok(AuditBatchStage::Done(outcome, false));
320        }
321        let changed = changed && self.buffer_size != 0;
322        validate_snapshot(&prospective)?;
323        self.next_token = self.next_token.wrapping_add(1);
324        let token = self.next_token;
325        let saved = self.queue_staged(
326            Arc::new(prospective),
327            StagedKind::Batch {
328                token,
329                outcome,
330                changed,
331            },
332        );
333        Ok(AuditBatchStage::Staged(StagedAuditBatch { token, saved }))
334    }
335
336    /// Take a staged batch once its commit has run: apply it if the commit
337    /// succeeded, or return the commit's error and leave the log as it was.
338    pub(super) fn finish_notification_batch(
339        &mut self,
340        staged: StagedAuditBatch,
341    ) -> Result<(ConfirmedAuditNotificationOutcome, bool), Error> {
342        let ours = self.staged.as_ref().is_some_and(|commit| {
343            matches!(commit.kind, StagedKind::Batch { token, .. } if token == staged.token)
344        });
345        if ours {
346            self.settle_staged();
347        }
348        let index = self
349            .settled
350            .iter()
351            .position(|settled| settled.token == staged.token)
352            .ok_or_else(operational_problem)?;
353        self.settled
354            .remove(index)
355            .expect("index found above")
356            .result
357    }
358
359    /// Make `change`: take its step if it is the next one staged on the
360    /// current state, or build it and commit it in place. Errors building it
361    /// (no clock) pass through; a commit that fails is refused with DEVICE /
362    /// OPERATIONAL_PROBLEM, as a Notification Forwarder refuses a list it
363    /// cannot save, and leaves the log as it was. The writer has logged the
364    /// storage error.
365    pub(super) fn commit_change(&mut self, change: StagedChange) -> Result<(), Error> {
366        if let Some(taken) = self.take_step(change) {
367            return taken.map_err(|_| operational_problem());
368        }
369        let timestamp = match change {
370            StagedChange::BufferSize(_) => None,
371            StagedChange::LogEnable(_) | StagedChange::Purge => Some(self.valid_timestamp()?),
372        };
373        let mut prospective = self.snapshot_for_next_generation()?;
374        apply_change(&mut prospective, change, timestamp)?;
375        self.commit_and_apply(prospective)
376            .map_err(|_| operational_problem())
377    }
378
379    /// Take the next staged step if it is `change` and builds on the state
380    /// served: the first step on the state the changes were staged on, a
381    /// later one on the step before it. A commit that failed drops every
382    /// step and returns its error.
383    fn take_step(&mut self, change: StagedChange) -> Option<Result<(), Error>> {
384        let served = self.generation;
385        let commit = self.staged.as_mut()?;
386        let generation = commit.snapshot.generation;
387        let StagedKind::Changes { steps, taken } = &mut commit.kind else {
388            return None;
389        };
390        let base = if *taken == 0 {
391            generation.checked_sub(1)
392        } else {
393            Some(generation)
394        };
395        if steps.front().map(|step| step.change) != Some(change) || base != Some(served) {
396            return None;
397        }
398        if let Err(error) = commit.ticket.take_outcome() {
399            self.staged.take().expect("matched above").released.set();
400            return Some(Err(error));
401        }
402        let step = steps.pop_front().expect("matched above");
403        *taken += 1;
404        if steps.is_empty() {
405            self.staged.take().expect("matched above").released.set();
406        }
407        self.apply_snapshot(step.state);
408        Some(Ok(()))
409    }
410
411    /// From the operation task: settle a batch whose commit has run, and drop
412    /// a write its request forgot.
413    pub(super) fn settle_finished(&mut self) -> bool {
414        let Some(commit) = &self.staged else {
415            return false;
416        };
417        let finished = commit.ticket.is_done()
418            && match commit.kind {
419                StagedKind::Batch { .. } => true,
420                StagedKind::Changes { .. } => commit.ticket.outlived(),
421            };
422        if finished {
423            self.settle_staged();
424        }
425        finished
426    }
427
428    /// Block until every queued commit has run.
429    #[cfg(test)]
430    pub(super) fn wait_for_commits(&self) {
431        self.writer.wait_idle();
432    }
433
434    /// Build a step for each of `changes` in turn and queue one commit of
435    /// the last. A change that cannot be built here, such as a purge without
436    /// a valid clock, ends the steps: made under the guard, it then fails as
437    /// it would unstaged, and the request makes no later change.
438    fn stage_changes(&mut self, changes: &[StagedChange]) -> StageStep {
439        let Ok(mut state) = self.snapshot_for_next_generation() else {
440            return StageStep::Skip;
441        };
442        let timestamp: Option<Timestamp> = self.valid_timestamp().ok();
443        let mut steps = VecDeque::with_capacity(changes.len());
444        // The change made to `state` but not yet kept as a step. Only the
445        // states between changes are copied; the last one is moved.
446        let mut made: Option<StagedChange> = None;
447        for &change in changes {
448            let before = made.map(|change| (change, state.clone()));
449            if apply_change(&mut state, change, timestamp).is_err() {
450                break;
451            }
452            if let Some((change, kept)) = before {
453                steps.push_back(Step {
454                    change,
455                    state: Arc::new(kept),
456                });
457            }
458            made = Some(change);
459        }
460        let Some(last) = made else {
461            return StageStep::Skip;
462        };
463        if validate_snapshot(&state).is_err() {
464            return StageStep::Skip;
465        }
466        let snapshot = Arc::new(state);
467        steps.push_back(Step {
468            change: last,
469            state: Arc::clone(&snapshot),
470        });
471        StageStep::Staged(self.queue_staged(snapshot, StagedKind::Changes { steps, taken: 0 }))
472    }
473}
474
475/// The Log_Enable and Buffer_Size changes `writes` make, in order, each
476/// judged as the log will be when the request makes it. A write the log will
477/// refuse ends the list, since the request makes no write after it; a write
478/// of the value the log will already hold changes nothing; other properties
479/// commit nothing. Neither property takes a NULL, so one the log refuses as
480/// the wrong datatype changes nothing either: the server answers it as a
481/// relinquish that leaves the property as it is, and the request goes on
482/// (#1396).
483fn changes_made(
484    mut log_enable: bool,
485    mut buffer_size: u32,
486    writes: &[PendingWrite],
487) -> Vec<StagedChange> {
488    let mut changes = Vec::new();
489    for write in writes {
490        let change = match (write.property, write.array_index, &write.value) {
491            (PropertyIdentifier::LOG_ENABLE, None, PropertyValue::Boolean(on)) => {
492                if *on == log_enable {
493                    continue;
494                }
495                log_enable = *on;
496                StagedChange::LogEnable(*on)
497            }
498            (PropertyIdentifier::LOG_ENABLE, None, PropertyValue::Null) => continue,
499            (PropertyIdentifier::BUFFER_SIZE, None, value) => {
500                match written_buffer_size(log_enable, value) {
501                    Ok(size) if size == buffer_size => continue,
502                    Ok(size) => {
503                        buffer_size = size;
504                        StagedChange::BufferSize(size)
505                    }
506                    // Logging off, a NULL is refused only as a datatype.
507                    Err(_) if *value == PropertyValue::Null && !log_enable => continue,
508                    Err(_) => break,
509                }
510            }
511            (PropertyIdentifier::LOG_ENABLE | PropertyIdentifier::BUFFER_SIZE, ..) => break,
512            _ => continue,
513        };
514        changes.push(change);
515    }
516    changes
517}
518
519impl DurableWrites for AuditLogObject {
520    fn stage_write(
521        &mut self,
522        property: PropertyIdentifier,
523        array_index: Option<u32>,
524        value: &PropertyValue,
525    ) -> StageStep {
526        self.stage_writes(&[PendingWrite {
527            property,
528            array_index,
529            value: value.clone(),
530        }])
531    }
532
533    /// Fold the request's Log_Enable and Buffer_Size writes into one staged
534    /// commit, so a WritePropertyMultiple that turns logging off and then
535    /// resizes commits both off the guard.
536    fn stage_writes(&mut self, writes: &[PendingWrite]) -> StageStep {
537        if !writes.iter().any(|write| {
538            matches!(
539                write.property,
540                PropertyIdentifier::LOG_ENABLE | PropertyIdentifier::BUFFER_SIZE
541            )
542        }) {
543            return StageStep::Skip;
544        }
545        // Wait for a change staged ahead of these first: they are judged by
546        // the log as it is once that lands.
547        if let Some(wait) = self.make_way() {
548            return StageStep::Busy(wait);
549        }
550        let changes = changes_made(self.log_enable, self.buffer_size, writes);
551        self.stage_changes(&changes)
552    }
553
554    fn stage_purge(&mut self) -> StageStep {
555        if let Some(wait) = self.make_way() {
556            return StageStep::Busy(wait);
557        }
558        self.stage_changes(&[StagedChange::Purge])
559    }
560
561    fn commit_purge(&mut self) -> Result<(), Error> {
562        self.purge().map(drop)
563    }
564
565    fn release_staged_write(&mut self, staged: &SaveWait) {
566        if self.staged.as_ref().is_some_and(|commit| {
567            matches!(commit.kind, StagedKind::Changes { .. }) && commit.ticket.issued(staged)
568        }) {
569            self.settle_staged();
570        }
571    }
572
573    fn has_staged_write(&self) -> bool {
574        self.staged.is_some()
575    }
576
577    /// Settle what the operation task would, without waiting out a
578    /// lifetime: drop staged changes, putting storage back, and take a
579    /// batch whose commit has run. A batch whose commit still runs is left
580    /// to land; the wait returned covers it.
581    fn settle_forgotten_writes(&mut self) -> Option<SaveWait> {
582        if self.staged.as_ref().is_some_and(|commit| {
583            commit.ticket.is_done() || matches!(commit.kind, StagedKind::Changes { .. })
584        }) {
585            self.settle_staged();
586        }
587        Some(self.writer.idle())
588    }
589}
590
591impl Drop for AuditLogObject {
592    fn drop(&mut self) {
593        self.close_staged();
594    }
595}