use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use bacnet_types::constructed::BACnetDeviceObjectPropertyReference;
use bacnet_types::error::Error;
use bacnet_types::primitives::{Date, ObjectIdentifier, Time};
use super::codec::{self, SnapshotRecord};
use super::{LogAcquisitionSettings, LogCommitCadence, LogPersistence, LogSnapshot};
use crate::durable::{SaveWait, SaveWriter};
use crate::log_buffer::{LogRecordBuffer, ResidentLogRecord};
use crate::log_reporting::{records_between, BufferReadyReporting};
use crate::log_window::LogWindow;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct LogState {
buffer_size: u32,
log_enable: bool,
stop_when_full: bool,
total_record_count: u32,
record_count: usize,
last_notify_record: u32,
records_since_notification_from: u32,
notification_threshold: u32,
start_time: (Date, Time),
stop_time: (Date, Time),
acquisition: Option<LogAcquisitionSettings>,
log_device_object_property: Vec<BACnetDeviceObjectPropertyReference>,
}
impl LogState {
pub(crate) fn of<R: ResidentLogRecord>(
buffer: &LogRecordBuffer<R>,
log_enable: bool,
stop_when_full: bool,
window: &LogWindow,
reporting: &BufferReadyReporting,
) -> Self {
let (start_time, stop_time) = window.ends();
let (last_notify_record, records_since_notification_from) = reporting.counts();
Self {
buffer_size: buffer.capacity(),
log_enable,
stop_when_full,
total_record_count: buffer.total_record_count(),
record_count: buffer.records().len(),
last_notify_record,
records_since_notification_from,
notification_threshold: reporting.notification_threshold(),
start_time,
stop_time,
acquisition: None,
log_device_object_property: Vec::new(),
}
}
pub(crate) fn trend(
mut self,
acquisition: LogAcquisitionSettings,
references: Vec<BACnetDeviceObjectPropertyReference>,
) -> Self {
self.acquisition = Some(acquisition);
self.log_device_object_property = references;
self
}
fn snapshot<R: Clone>(&self, oid: ObjectIdentifier, records: &VecDeque<R>) -> LogSnapshot<R> {
LogSnapshot {
object_identifier: oid,
generation: 0,
buffer_size: self.buffer_size,
log_enable: self.log_enable,
stop_when_full: self.stop_when_full,
total_record_count: self.total_record_count,
records: records.iter().cloned().collect(),
last_notify_record: self.last_notify_record,
records_since_notification_from: self.records_since_notification_from,
notification_threshold: self.notification_threshold,
start_time: self.start_time,
stop_time: self.stop_time,
acquisition: self.acquisition,
log_device_object_property: self.log_device_object_property.clone(),
}
}
}
pub(crate) struct LogStore<R: SnapshotRecord> {
oid: ObjectIdentifier,
writer: SaveWriter<Mutex<LogSnapshot<R>>>,
generation: Arc<AtomicU64>,
cadence: LogCommitCadence,
queued: Option<LogState>,
last_failed: Arc<AtomicBool>,
queued_total: u32,
interval_from: Option<Duration>,
restart_interval: bool,
reopened: bool,
interruption_pending: bool,
}
impl<R: SnapshotRecord> LogStore<R> {
pub(crate) fn open(
oid: ObjectIdentifier,
buffer_size: u32,
persistence: Arc<dyn LogPersistence<R>>,
) -> Result<(Self, Option<LogSnapshot<R>>), Error> {
codec::check_buffer_size::<R>(buffer_size)?;
let loaded = persistence.load(oid)?;
if let Some(snapshot) = &loaded {
if snapshot.object_identifier != oid {
return Err(Error::Encoding(
"log snapshot belongs to another object".into(),
));
}
codec::validate(snapshot)?;
}
let last_failed = Arc::new(AtomicBool::new(false));
let failed = Arc::clone(&last_failed);
let generation = Arc::new(AtomicU64::new(
loaded.as_ref().map_or(0, |snapshot| snapshot.generation),
));
let committed = Arc::clone(&generation);
let writer = SaveWriter::new(
format!(
"bacnet-log-{}-{}-save",
oid.object_type().to_raw(),
oid.instance_number()
),
move |pending: &Mutex<LogSnapshot<R>>| {
let mut snapshot = pending.lock().unwrap_or_else(PoisonError::into_inner);
snapshot.generation = committed
.load(Ordering::Acquire)
.checked_add(1)
.ok_or_else(|| Error::OutOfRange("log snapshot generation exhausted".into()))?;
persistence.commit(&snapshot)?;
committed.store(snapshot.generation, Ordering::Release);
Ok(())
},
move |_, result| {
failed.store(result.is_err(), Ordering::Release);
if let Err(error) = result {
tracing::warn!(
log = %oid,
%error,
"Failed to commit a log snapshot; the next pass or stop tries again"
);
}
},
);
Ok((
Self {
oid,
writer,
generation,
cadence: LogCommitCadence::default(),
queued: None,
last_failed,
queued_total: 0,
interval_from: None,
restart_interval: false,
reopened: false,
interruption_pending: false,
},
loaded,
))
}
pub(crate) fn reopened(&mut self, state: LogState, resized: bool) {
self.queued_total = state.total_record_count;
self.queued = (!resized).then_some(state);
self.reopened = true;
self.interruption_pending = true;
}
pub(crate) fn is_reopened(&self) -> bool {
self.reopened
}
pub(crate) fn interruption_pending(&self) -> bool {
self.interruption_pending
}
pub(crate) fn interruption_recorded(&mut self) {
self.interruption_pending = false;
}
pub(crate) fn cadence(&self) -> LogCommitCadence {
self.cadence
}
pub(crate) fn set_cadence(&mut self, cadence: LogCommitCadence) {
self.cadence = cadence;
}
pub(crate) fn generation(&self) -> u64 {
self.generation.load(Ordering::Acquire)
}
pub(crate) fn count_due(&self, total: u32) -> bool {
self.cadence.records > 0
&& records_between(self.queued_total, total) >= self.cadence.records
}
pub(crate) fn interval_due(&mut self, now: Duration) -> bool {
match self.interval_from {
Some(from) if !self.restart_interval => {
!self.cadence.interval.is_zero()
&& now.saturating_sub(from) >= self.cadence.interval
}
_ => {
self.interval_from = Some(now);
self.restart_interval = false;
false
}
}
}
pub(crate) fn changed(&self, state: &LogState) -> bool {
self.last_failed.load(Ordering::Acquire) || self.queued.as_ref() != Some(state)
}
pub(crate) fn queue(&mut self, state: LogState, records: &VecDeque<R>) {
let snapshot = state.snapshot(self.oid, records);
self.writer.submit_coalescing(Mutex::new(snapshot));
self.queued_total = state.total_record_count;
self.queued = Some(state);
self.restart_interval = true;
}
pub(crate) fn idle(&self) -> SaveWait {
self.writer.idle()
}
}
pub(crate) fn fit_to_buffer_size<R>(snapshot: &mut LogSnapshot<R>, buffer_size: u32) -> bool {
if snapshot.buffer_size == buffer_size {
return false;
}
let stops_when_full = snapshot.log_enable && snapshot.stop_when_full;
let room = if stops_when_full {
(buffer_size as usize).saturating_sub(1)
} else {
buffer_size as usize
};
let dropped = snapshot.records.len().saturating_sub(room);
tracing::warn!(
log = %snapshot.object_identifier,
saved = snapshot.buffer_size,
configured = buffer_size,
dropped,
"Reopening a log at the configured Buffer_Size, not the saved one"
);
snapshot.records.drain(..dropped);
snapshot.buffer_size = buffer_size;
true
}
pub(crate) trait DurableLog {
type Record: SnapshotRecord;
fn log_state(&self) -> LogState;
fn durable_parts(
&mut self,
) -> (
Option<&mut LogStore<Self::Record>>,
&LogRecordBuffer<Self::Record>,
);
fn record_owed_interruption(&mut self, fallback: Option<(Date, Time)>) -> bool;
fn settle_interruption(&mut self, fallback: Option<(Date, Time)>) {
let owed = matches!(self.durable_parts().0, Some(store) if store.interruption_pending());
if owed && self.record_owed_interruption(fallback) {
if let Some(store) = self.durable_parts().0 {
store.interruption_recorded();
}
self.commit_if_counted();
}
}
fn commit_if_counted(&mut self) {
let due = {
let (store, buffer) = self.durable_parts();
store.is_some_and(|store| store.count_due(buffer.total_record_count()))
};
if due {
let state = self.log_state();
if let (Some(store), buffer) = self.durable_parts() {
store.queue(state, buffer.records());
}
}
}
fn commit_if_interval(&mut self, now: Duration) {
self.settle_interruption(None);
let due = self
.durable_parts()
.0
.is_some_and(|store| store.interval_due(now));
if due {
self.commit_if_changed();
}
}
fn commit_if_changed(&mut self) {
if self.durable_parts().0.is_none() {
return;
}
let state = self.log_state();
if let (Some(store), buffer) = self.durable_parts() {
if store.changed(&state) {
store.queue(state, buffer.records());
}
}
}
fn commit_final(&mut self) -> Option<SaveWait> {
self.commit_if_changed();
self.durable_parts().0.map(|store| store.idle())
}
}
macro_rules! impl_durable_log {
($log:ty) => {
impl $crate::durable::DurableWrites for $log {
fn stage_write(
&mut self,
_property: bacnet_types::enums::PropertyIdentifier,
_array_index: Option<u32>,
_value: &bacnet_types::primitives::PropertyValue,
) -> $crate::durable::StageStep {
$crate::durable::StageStep::Skip
}
fn release_staged_write(&mut self, _staged: &$crate::durable::SaveWait) {}
fn settle_forgotten_writes(&mut self) -> Option<$crate::durable::SaveWait> {
$crate::log_persistence::store::DurableLog::commit_final(self)
}
}
impl Drop for $log {
fn drop(&mut self) {
let _ = $crate::log_persistence::store::DurableLog::commit_final(self);
}
}
};
}
pub(crate) use impl_durable_log;