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