use std::collections::VecDeque;
use std::sync::Arc;
use super::buffer::{apply_change, written_buffer_size, Timestamp};
use super::*;
use crate::durable::{
DurableWrites, Event, PendingWrite, SaveTicket, SaveWait, SaveWriter, StageStep,
};
const MAX_SETTLED: usize = 16;
#[derive(Debug)]
pub enum AuditBatchStage {
Done(ConfirmedAuditNotificationOutcome, bool),
Staged(StagedAuditBatch),
Busy(SaveWait),
}
#[derive(Debug)]
pub struct StagedAuditBatch {
token: u64,
saved: SaveWait,
}
impl StagedAuditBatch {
pub fn saved(&self) -> SaveWait {
self.saved.clone()
}
}
pub(super) enum StagedKind {
Batch {
token: u64,
outcome: ConfirmedAuditNotificationOutcome,
changed: bool,
},
Changes { steps: VecDeque<Step>, taken: usize },
}
pub(super) struct Step {
change: StagedChange,
state: Arc<AuditLogSnapshot>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum StagedChange {
LogEnable(bool),
BufferSize(u32),
Purge,
}
pub(super) struct StagedCommit {
snapshot: Arc<AuditLogSnapshot>,
ticket: SaveTicket,
kind: StagedKind,
released: Arc<Event>,
}
pub(super) struct Settled {
token: u64,
result: Result<(ConfirmedAuditNotificationOutcome, bool), Error>,
}
pub(super) fn operational_problem() -> Error {
Error::Protocol {
class: ErrorClass::DEVICE.to_raw() as u32,
code: ErrorCode::OPERATIONAL_PROBLEM.to_raw() as u32,
}
}
pub(super) fn writer(
oid: ObjectIdentifier,
persistence: Arc<dyn AuditLogPersistence>,
) -> SaveWriter<Arc<AuditLogSnapshot>> {
SaveWriter::new(
format!("bacnet-al-{}-save", oid.instance_number()),
move |snapshot: &Arc<AuditLogSnapshot>| persistence.commit(snapshot),
move |snapshot, result| {
if let Err(error) = result {
tracing::warn!(
audit_log = %oid,
generation = snapshot.generation,
%error,
"Failed to commit Audit Log snapshot"
);
}
},
)
}
impl AuditLogObject {
pub(super) fn commit_and_apply(&mut self, snapshot: AuditLogSnapshot) -> Result<(), Error> {
validate_snapshot(&snapshot)?;
let snapshot = Arc::new(snapshot);
self.writer.submit(Arc::clone(&snapshot)).take_outcome()?;
self.apply_snapshot(snapshot);
Ok(())
}
fn apply_snapshot(&mut self, snapshot: Arc<AuditLogSnapshot>) {
let snapshot = Arc::try_unwrap(snapshot).unwrap_or_else(|shared| (*shared).clone());
self.generation = snapshot.generation;
self.buffer_size = snapshot.capacity;
self.log_enable = snapshot.log_enable;
self.total_record_count = snapshot.total_record_count;
self.buffer = snapshot.records.into();
self.completed_receipts = snapshot.completed_receipts;
}
pub(super) fn settle_staged(&mut self) {
let Some(commit) = self.staged.take() else {
return;
};
commit.released.set();
match commit.kind {
StagedKind::Batch {
token,
outcome,
changed,
} => {
let result = commit.ticket.take_outcome().map(|()| {
self.apply_snapshot(commit.snapshot);
(outcome, changed)
});
if self.settled.len() == MAX_SETTLED {
self.settled.pop_front();
}
self.settled.push_back(Settled { token, result });
}
StagedKind::Changes { .. } => self.drop_staged_write(&commit.ticket),
}
}
fn drop_staged_write(&mut self, ticket: &SaveTicket) {
if let Some(served) = self.served_to_put_back(ticket) {
self.writer.submit_coalescing(Arc::new(served));
}
}
fn served_to_put_back(&self, ticket: &SaveTicket) -> Option<AuditLogSnapshot> {
if ticket.succeeded() == Some(false) {
return None;
}
let generation = self.generation.checked_add(1)?;
let mut served = self.current_snapshot();
served.generation = generation;
Some(served)
}
fn close_staged(&mut self) {
let Some(commit) = self
.staged
.take_if(|commit| matches!(commit.kind, StagedKind::Changes { .. }))
else {
return;
};
commit.released.set();
if let Some(served) = self.served_to_put_back(&commit.ticket) {
self.writer.put_back(Arc::new(served));
}
}
fn make_way(&mut self) -> Option<SaveWait> {
let commit = self.staged.as_ref()?;
let holds = match commit.kind {
StagedKind::Batch { .. } => !commit.ticket.is_done(),
StagedKind::Changes { .. } => !commit.ticket.outlived(),
};
if holds {
return Some(match commit.kind {
StagedKind::Batch { .. } => commit.ticket.wait(),
StagedKind::Changes { .. } => SaveWait::new(Arc::clone(&commit.released)),
});
}
self.settle_staged();
None
}
fn queue_staged(&mut self, snapshot: Arc<AuditLogSnapshot>, kind: StagedKind) -> SaveWait {
let ticket = self.writer.submit(Arc::clone(&snapshot));
let saved = ticket.wait();
self.staged = Some(StagedCommit {
snapshot,
ticket,
kind,
released: Arc::default(),
});
saved
}
pub(super) fn stage_notification_batch(
&mut self,
notifications: &[BACnetAuditNotification],
apdu_timeout_ms: u32,
receipt: Option<CompletedAuditReceipt>,
) -> Result<AuditBatchStage, Error> {
if let Some(wait) = self.make_way() {
return Ok(AuditBatchStage::Busy(wait));
}
if !self.log_enable {
return Err(Error::Protocol {
class: ErrorClass::SERVICES.to_raw() as u32,
code: ErrorCode::SERVICE_REQUEST_DENIED.to_raw() as u32,
});
}
if notifications.is_empty() {
return Err(Error::OutOfRange(
"Audit notification batch must not be empty".into(),
));
}
if let Some(receipt) = &receipt {
if receipt::contains(
&self.completed_receipts,
receipt.key(),
receipt.completed_at_unix_millis(),
)? {
return Ok(AuditBatchStage::Done(
ConfirmedAuditNotificationOutcome::Duplicate,
false,
));
}
}
let mut prospective = self.snapshot_for_next_generation()?;
let changed =
self.apply_notification_batch(&mut prospective, notifications, apdu_timeout_ms)?;
let confirmed = receipt.is_some();
let outcome = match receipt {
Some(receipt) => receipt::insert(&mut prospective.completed_receipts, receipt)?,
None => ConfirmedAuditNotificationOutcome::Stored,
};
if outcome == ConfirmedAuditNotificationOutcome::Duplicate || (!confirmed && !changed) {
return Ok(AuditBatchStage::Done(outcome, false));
}
let changed = changed && self.buffer_size != 0;
validate_snapshot(&prospective)?;
self.next_token = self.next_token.wrapping_add(1);
let token = self.next_token;
let saved = self.queue_staged(
Arc::new(prospective),
StagedKind::Batch {
token,
outcome,
changed,
},
);
Ok(AuditBatchStage::Staged(StagedAuditBatch { token, saved }))
}
pub(super) fn finish_notification_batch(
&mut self,
staged: StagedAuditBatch,
) -> Result<(ConfirmedAuditNotificationOutcome, bool), Error> {
let ours = self.staged.as_ref().is_some_and(|commit| {
matches!(commit.kind, StagedKind::Batch { token, .. } if token == staged.token)
});
if ours {
self.settle_staged();
}
let index = self
.settled
.iter()
.position(|settled| settled.token == staged.token)
.ok_or_else(operational_problem)?;
self.settled
.remove(index)
.expect("index found above")
.result
}
pub(super) fn commit_change(&mut self, change: StagedChange) -> Result<(), Error> {
if let Some(taken) = self.take_step(change) {
return taken.map_err(|_| operational_problem());
}
let timestamp = match change {
StagedChange::BufferSize(_) => None,
StagedChange::LogEnable(_) | StagedChange::Purge => Some(self.valid_timestamp()?),
};
let mut prospective = self.snapshot_for_next_generation()?;
apply_change(&mut prospective, change, timestamp)?;
self.commit_and_apply(prospective)
.map_err(|_| operational_problem())
}
fn take_step(&mut self, change: StagedChange) -> Option<Result<(), Error>> {
let served = self.generation;
let commit = self.staged.as_mut()?;
let generation = commit.snapshot.generation;
let StagedKind::Changes { steps, taken } = &mut commit.kind else {
return None;
};
let base = if *taken == 0 {
generation.checked_sub(1)
} else {
Some(generation)
};
if steps.front().map(|step| step.change) != Some(change) || base != Some(served) {
return None;
}
if let Err(error) = commit.ticket.take_outcome() {
self.staged.take().expect("matched above").released.set();
return Some(Err(error));
}
let step = steps.pop_front().expect("matched above");
*taken += 1;
if steps.is_empty() {
self.staged.take().expect("matched above").released.set();
}
self.apply_snapshot(step.state);
Some(Ok(()))
}
pub(super) fn settle_finished(&mut self) -> bool {
let Some(commit) = &self.staged else {
return false;
};
let finished = commit.ticket.is_done()
&& match commit.kind {
StagedKind::Batch { .. } => true,
StagedKind::Changes { .. } => commit.ticket.outlived(),
};
if finished {
self.settle_staged();
}
finished
}
#[cfg(test)]
pub(super) fn wait_for_commits(&self) {
self.writer.wait_idle();
}
fn stage_changes(&mut self, changes: &[StagedChange]) -> StageStep {
let Ok(mut state) = self.snapshot_for_next_generation() else {
return StageStep::Skip;
};
let timestamp: Option<Timestamp> = self.valid_timestamp().ok();
let mut steps = VecDeque::with_capacity(changes.len());
let mut made: Option<StagedChange> = None;
for &change in changes {
let before = made.map(|change| (change, state.clone()));
if apply_change(&mut state, change, timestamp).is_err() {
break;
}
if let Some((change, kept)) = before {
steps.push_back(Step {
change,
state: Arc::new(kept),
});
}
made = Some(change);
}
let Some(last) = made else {
return StageStep::Skip;
};
if validate_snapshot(&state).is_err() {
return StageStep::Skip;
}
let snapshot = Arc::new(state);
steps.push_back(Step {
change: last,
state: Arc::clone(&snapshot),
});
StageStep::Staged(self.queue_staged(snapshot, StagedKind::Changes { steps, taken: 0 }))
}
}
fn changes_made(
mut log_enable: bool,
mut buffer_size: u32,
writes: &[PendingWrite],
) -> Vec<StagedChange> {
let mut changes = Vec::new();
for write in writes {
let change = match (write.property, write.array_index, &write.value) {
(PropertyIdentifier::LOG_ENABLE, None, PropertyValue::Boolean(on)) => {
if *on == log_enable {
continue;
}
log_enable = *on;
StagedChange::LogEnable(*on)
}
(PropertyIdentifier::LOG_ENABLE, None, PropertyValue::Null) => continue,
(PropertyIdentifier::BUFFER_SIZE, None, value) => {
match written_buffer_size(log_enable, value) {
Ok(size) if size == buffer_size => continue,
Ok(size) => {
buffer_size = size;
StagedChange::BufferSize(size)
}
Err(_) if *value == PropertyValue::Null && !log_enable => continue,
Err(_) => break,
}
}
(PropertyIdentifier::LOG_ENABLE | PropertyIdentifier::BUFFER_SIZE, ..) => break,
_ => continue,
};
changes.push(change);
}
changes
}
impl DurableWrites for AuditLogObject {
fn stage_write(
&mut self,
property: PropertyIdentifier,
array_index: Option<u32>,
value: &PropertyValue,
) -> StageStep {
self.stage_writes(&[PendingWrite {
property,
array_index,
value: value.clone(),
}])
}
fn stage_writes(&mut self, writes: &[PendingWrite]) -> StageStep {
if !writes.iter().any(|write| {
matches!(
write.property,
PropertyIdentifier::LOG_ENABLE | PropertyIdentifier::BUFFER_SIZE
)
}) {
return StageStep::Skip;
}
if let Some(wait) = self.make_way() {
return StageStep::Busy(wait);
}
let changes = changes_made(self.log_enable, self.buffer_size, writes);
self.stage_changes(&changes)
}
fn stage_purge(&mut self) -> StageStep {
if let Some(wait) = self.make_way() {
return StageStep::Busy(wait);
}
self.stage_changes(&[StagedChange::Purge])
}
fn commit_purge(&mut self) -> Result<(), Error> {
self.purge().map(drop)
}
fn release_staged_write(&mut self, staged: &SaveWait) {
if self.staged.as_ref().is_some_and(|commit| {
matches!(commit.kind, StagedKind::Changes { .. }) && commit.ticket.issued(staged)
}) {
self.settle_staged();
}
}
fn has_staged_write(&self) -> bool {
self.staged.is_some()
}
fn settle_forgotten_writes(&mut self) -> Option<SaveWait> {
if self.staged.as_ref().is_some_and(|commit| {
commit.ticket.is_done() || matches!(commit.kind, StagedKind::Changes { .. })
}) {
self.settle_staged();
}
Some(self.writer.idle())
}
}
impl Drop for AuditLogObject {
fn drop(&mut self) {
self.close_staged();
}
}