use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Duration;
use bacnet_objects::clock::ClockFrame;
use bacnet_services::cov_multiple::COVNotificationValue;
use tokio::sync::Notify;
use tokio::time::Instant;
use super::{AtomicCovCounters, CovObservation, CovSubscriptionKey, MultipleContextKey};
pub(crate) const HISTORY_NOTIFICATIONS: usize = 4;
pub(crate) const ITEM_FRAMING: usize = 7;
pub(crate) const CHANGE_MEMORY: usize = std::mem::size_of::<TimedChange>();
pub(crate) const VALUE_MEMORY: usize =
std::mem::size_of::<COVNotificationValue>() + std::mem::size_of::<usize>();
pub(crate) const CEILING_BYTES_PER_OCTET: usize = 4;
const DEADLINE_FLOOR: Duration = Duration::from_secs(1);
#[derive(Debug, Clone)]
pub(crate) struct TimedChange {
seq: u64,
frame: ClockFrame,
captured_at: Instant,
values: Vec<COVNotificationValue>,
observation: CovObservation,
octets: usize,
positions: Vec<usize>,
continues: bool,
}
fn unsigned_len(value: u64) -> usize {
1 + (1..8)
.find(|octets| value >> (8 * octets) == 0)
.unwrap_or(8)
}
pub(crate) fn value_len(value: &COVNotificationValue) -> usize {
unsigned_len(u64::from(value.property_identifier.to_raw()))
+ value
.property_array_index
.map_or(0, |index| unsigned_len(u64::from(index)))
+ 2
+ value.value.len()
+ if value.time_of_change.is_some() { 5 } else { 0 }
}
fn item_octets(values: &[COVNotificationValue]) -> usize {
ITEM_FRAMING + values.iter().map(value_len).sum::<usize>()
}
impl TimedChange {
pub(crate) fn new(
frame: ClockFrame,
mut values: Vec<COVNotificationValue>,
observation: CovObservation,
) -> Self {
for value in &mut values {
value.time_of_change = Some(frame.local_time);
}
Self {
seq: 0,
frame,
captured_at: Instant::now(),
octets: item_octets(&values),
positions: (0..values.len()).collect(),
values,
observation,
continues: false,
}
}
fn into_values(self) -> impl Iterator<Item = TimedChange> {
self.values
.into_iter()
.zip(self.positions)
.map(move |(value, position)| TimedChange {
seq: self.seq,
frame: self.frame,
captured_at: self.captured_at,
octets: item_octets(std::slice::from_ref(&value)),
values: vec![value],
observation: self.observation.clone(),
positions: vec![position],
continues: true,
})
}
fn rejoin(&mut self, other: TimedChange) -> usize {
let mut merged: Vec<_> = std::mem::take(&mut self.positions)
.into_iter()
.zip(std::mem::take(&mut self.values))
.chain(other.positions.into_iter().zip(other.values))
.collect();
merged.sort_by_key(|(position, _)| *position);
(self.positions, self.values) = merged.into_iter().unzip();
let before = self.octets;
self.octets = item_octets(&self.values);
self.octets - before
}
pub(crate) fn memory(&self) -> usize {
CHANGE_MEMORY
+ self
.values
.iter()
.map(|value| VALUE_MEMORY + value.value.len())
.sum::<usize>()
}
pub(crate) fn seq(&self) -> u64 {
self.seq
}
pub(crate) fn frame(&self) -> ClockFrame {
self.frame
}
pub(crate) fn values(&self) -> &[COVNotificationValue] {
&self.values
}
pub(crate) fn observation(&self) -> &CovObservation {
&self.observation
}
}
#[derive(Debug, Clone)]
struct FieldRecord {
value: Vec<u8>,
seq: u64,
frame: ClockFrame,
}
#[derive(Debug)]
struct UntimedReference {
generation: u64,
delay: Duration,
owed_since: Option<Instant>,
}
#[derive(Debug)]
struct TimedHistory {
incarnation: u64,
generation: u64,
baseline: Option<CovObservation>,
field: Option<FieldRecord>,
latest: u64,
committed: u64,
in_delivery: u64,
delay: Duration,
entries: VecDeque<TimedChange>,
}
#[derive(Debug, Clone, Copy)]
struct ContextTerms {
apdu: usize,
envelope: usize,
reserve: usize,
warned: DropWarnings,
}
impl ContextTerms {
fn notification(self) -> usize {
self.apdu.saturating_sub(self.envelope)
}
fn room(self) -> usize {
(HISTORY_NOTIFICATIONS * self.notification()).saturating_sub(self.reserve)
}
fn ceiling(self, local: usize) -> usize {
CEILING_BYTES_PER_OCTET * HISTORY_NOTIFICATIONS * local.saturating_sub(self.envelope)
}
fn holds(self, held: Held, local: usize) -> bool {
held.octets <= self.room() && held.memory <= self.ceiling(local)
}
}
fn own_value(key: &CovSubscriptionKey, values: &[COVNotificationValue]) -> Option<Vec<u8>> {
let CovSubscriptionKey::Multiple {
property, index, ..
} = key
else {
return None;
};
values
.iter()
.find(|value| {
value.property_identifier == *property && value.property_array_index == *index
})
.map(|value| value.value.clone())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FieldTiming {
NotLive,
NoTime,
At(u64, ClockFrame),
}
#[derive(Debug)]
pub(crate) struct TimedHistories {
histories: HashMap<CovSubscriptionKey, TimedHistory>,
untimed: HashMap<CovSubscriptionKey, UntimedReference>,
terms: HashMap<MultipleContextKey, ContextTerms>,
sending: HashMap<MultipleContextKey, bool>,
context_held: HashMap<MultipleContextKey, Held>,
not_before: HashMap<MultipleContextKey, Instant>,
wake: Arc<Notify>,
next_seq: u64,
next_incarnation: u64,
local_apdu: usize,
counters: Arc<AtomicCovCounters>,
}
impl TimedHistories {
fn new(local_apdu: usize, counters: Arc<AtomicCovCounters>) -> Self {
Self {
histories: HashMap::new(),
untimed: HashMap::new(),
terms: HashMap::new(),
sending: HashMap::new(),
context_held: HashMap::new(),
not_before: HashMap::new(),
wake: Arc::default(),
next_seq: 1,
next_incarnation: 1,
local_apdu,
counters,
}
}
pub(super) fn set_delay(&mut self, context: &MultipleContextKey, seconds: u32) {
let delay = Duration::from_secs(u64::from(seconds));
let mut shorter = false;
for (key, history) in &mut self.histories {
if key.multiple_context() == Some(context) {
shorter |= delay < history.delay;
history.delay = delay;
}
}
for (key, reference) in &mut self.untimed {
if key.multiple_context() == Some(context) {
shorter |= reference.owed_since.is_some() && delay < reference.delay;
reference.delay = delay;
}
}
if shorter {
self.rearm_context(context);
}
}
pub(super) fn set_sizing(
&mut self,
context: &MultipleContextKey,
subscriber: Option<u16>,
time_remaining: u32,
) {
let apdu = subscriber.map_or(self.local_apdu, |subscriber| {
self.local_apdu.min(usize::from(subscriber))
});
if let Some(terms) = self.terms.get_mut(context) {
if terms.apdu != apdu {
terms.warned = DropWarnings::default();
}
terms.apdu = apdu;
terms.envelope = envelope_len(context, time_remaining);
terms.reserve = 0;
}
}
pub(crate) fn note_reserve(&mut self, context: &MultipleContextKey, octets: usize) {
if let Some(terms) = self.terms.get_mut(context) {
terms.reserve = terms.reserve.max(octets.min(terms.notification()));
}
}
fn begin_send(&mut self, context: &MultipleContextKey) -> bool {
match self.sending.get_mut(context) {
Some(owed) => {
*owed = true;
false
}
None => {
self.sending.insert(context.clone(), false);
true
}
}
}
fn end_send(&mut self, context: &MultipleContextKey) -> bool {
self.sending.remove(context).unwrap_or(false)
}
pub(crate) fn rearm(&mut self) {
self.not_before.clear();
self.wake.notify_one();
}
fn rearm_context(&mut self, context: &MultipleContextKey) {
self.not_before.remove(context);
self.wake.notify_one();
}
pub(crate) fn hold_until(&mut self, context: &MultipleContextKey, until: Instant) {
if self
.histories
.keys()
.any(|key| key.multiple_context() == Some(context))
|| self.owes(context)
{
self.not_before.insert(context.clone(), until);
self.wake.notify_one();
}
}
pub(crate) fn take_due(&mut self, now: Instant) -> (Vec<CovSubscriptionKey>, Option<Instant>) {
let mut pending: HashMap<&MultipleContextKey, (Instant, Duration)> = HashMap::new();
let timed = self.histories.iter().filter_map(|(key, history)| {
Some((key, history.entries.front()?.captured_at, history.delay))
});
let owed = self
.untimed
.iter()
.filter_map(|(key, reference)| Some((key, reference.owed_since?, reference.delay)));
for (key, since, delay) in timed.chain(owed) {
let Some(context) = key.multiple_context() else {
continue;
};
pending
.entry(context)
.and_modify(|(at, shortest)| {
*at = (*at).min(since);
*shortest = (*shortest).min(delay);
})
.or_insert((since, delay));
}
let mut due = Vec::new();
let mut next: Option<Instant> = None;
for (&context, &(first, delay)) in &pending {
let spacing = delay.max(DEADLINE_FLOOR);
let mut deadline = first + spacing;
if let Some(&wait) = self.not_before.get(context) {
deadline = deadline.max(wait);
}
if deadline <= now {
due.push((context.clone(), spacing));
} else {
next = Some(next.map_or(deadline, |at| at.min(deadline)));
}
}
self.not_before
.retain(|context, _| pending.contains_key(context));
for (context, spacing) in &due {
self.not_before.insert(context.clone(), now + *spacing);
}
let due: Vec<_> = due.into_iter().map(|(context, _)| context).collect();
let is_due = |key: &CovSubscriptionKey| {
key.multiple_context()
.is_some_and(|context| due.contains(context))
};
let timed = self
.histories
.iter()
.filter(|(key, history)| !history.entries.is_empty() && is_due(key))
.map(|(key, _)| key.clone());
let owed = self
.untimed
.iter()
.filter(|(key, reference)| reference.owed_since.is_some() && is_due(key))
.map(|(key, _)| key.clone());
(timed.chain(owed).collect(), next)
}
pub(super) fn reset(&mut self, key: &CovSubscriptionKey, generation: u64, delay: u32) {
let delay = Duration::from_secs(u64::from(delay));
let incarnation = self.next_incarnation;
if let Some(context) = key.multiple_context() {
let apdu = self.local_apdu;
let envelope = envelope_len(context, u32::MAX);
let terms = self.terms.entry(context.clone()).or_insert(ContextTerms {
apdu,
envelope,
reserve: 0,
warned: DropWarnings::default(),
});
terms.warned = DropWarnings::default();
}
let history = self
.histories
.entry(key.clone())
.or_insert_with(|| TimedHistory {
incarnation,
generation,
baseline: None,
field: None,
latest: 0,
committed: 0,
in_delivery: 0,
delay,
entries: VecDeque::new(),
});
if history.incarnation == incarnation {
self.next_incarnation += 1;
}
let shorter = delay < history.delay;
history.generation = generation;
history.baseline = None;
history.delay = delay;
if let Some(context) = key.multiple_context().filter(|_| shorter) {
self.rearm_context(context);
}
}
pub(super) fn remove(&mut self, key: &CovSubscriptionKey) {
self.untimed.remove(key);
if let Some(history) = self.histories.remove(key) {
self.release(key, Held::of(&history.entries));
}
if let Some(context) = key.multiple_context() {
if self
.histories
.keys()
.any(|other| other.multiple_context() == Some(context))
{
if let Some(terms) = self.terms.get_mut(context) {
terms.reserve = 0;
}
} else {
if !self.owes(context) {
self.not_before.remove(context);
}
self.terms.remove(context);
}
}
}
#[cfg(test)]
pub(crate) fn held(&self) -> (usize, usize) {
let timed = self
.histories
.len()
.max(self.terms.len())
.max(self.context_held.len());
(timed + self.untimed.len(), self.not_before.len())
}
pub(crate) fn baseline(
&self,
key: &CovSubscriptionKey,
generation: u64,
) -> Option<&CovObservation> {
self.history(key, generation)?.baseline.as_ref()
}
pub(crate) fn field_time(
&mut self,
key: &CovSubscriptionKey,
generation: u64,
value: &[u8],
now: impl FnOnce() -> Option<ClockFrame>,
) -> FieldTiming {
let seq = self.next_seq;
let Some(history) = self.history_mut(key, generation) else {
return FieldTiming::NotLive;
};
if let Some(field) = history.field.as_ref().filter(|field| field.value == value) {
return FieldTiming::At(field.seq, field.frame);
}
let Some(frame) = now() else {
return FieldTiming::NoTime;
};
history.field = Some(FieldRecord {
value: value.to_vec(),
seq,
frame,
});
self.next_seq += 1;
FieldTiming::At(seq, frame)
}
pub(crate) fn note_field(
&mut self,
key: &CovSubscriptionKey,
generation: u64,
values: &[COVNotificationValue],
frame: ClockFrame,
) {
let seq = self.next_seq;
let (Some(history), Some(value)) =
(self.history_mut(key, generation), own_value(key, values))
else {
return;
};
if history
.field
.as_ref()
.is_some_and(|field| field.value == value)
{
return;
}
history.field = Some(FieldRecord { value, seq, frame });
self.next_seq += 1;
}
pub(crate) fn push(
&mut self,
key: &CovSubscriptionKey,
generation: u64,
change: TimedChange,
) -> bool {
if self.history(key, generation).is_none() {
return false;
}
let change = self.adopt(key, generation, change);
self.insert_ordered(key, generation, vec![change], true);
true
}
pub(crate) fn adopt(
&mut self,
key: &CovSubscriptionKey,
generation: u64,
mut change: TimedChange,
) -> TimedChange {
change.seq = self.next_seq;
self.next_seq += 1;
if let Some(history) = self.history_mut(key, generation) {
history.baseline = Some(change.observation.clone());
history.field = own_value(key, &change.values).map(|value| FieldRecord {
value,
seq: change.seq,
frame: change.frame,
});
history.latest = change.seq;
}
change
}
pub(crate) fn drain(
&mut self,
key: &CovSubscriptionKey,
generation: u64,
) -> (u64, Vec<TimedChange>) {
let Some(history) = self.history_mut(key, generation) else {
return (0, Vec::new());
};
let incarnation = history.incarnation;
if history
.entries
.back()
.is_some_and(|newest| newest.seq < history.latest)
{
return (incarnation, Vec::new());
}
let drained: Vec<_> = history.entries.drain(..).collect();
self.release(key, Held::of(&drained));
(incarnation, drained)
}
fn commit(&mut self, key: &CovSubscriptionKey, incarnation: u64, seq: u64) {
let Some(history) = self.incarnation_mut(key, incarnation) else {
return;
};
history.committed = history.committed.max(seq);
let committed = history.committed;
let stale = history
.entries
.partition_point(|change| change.seq < committed);
let removed: Vec<_> = history.entries.drain(..stale).collect();
if !removed.is_empty() {
self.release(key, Held::of(&removed));
self.dropped(key, removed.len(), DropReason::Superseded);
}
}
fn requeue(
&mut self,
key: &CovSubscriptionKey,
incarnation: u64,
changes: Vec<TimedChange>,
evict: bool,
) {
let Some(history) = self.incarnation_mut(key, incarnation) else {
return;
};
let generation = history.generation;
let committed = history.committed;
let (keep, stale): (Vec<_>, Vec<_>) = changes
.into_iter()
.partition(|change| change.seq > committed);
if !stale.is_empty() {
self.dropped(key, stale.len(), DropReason::Superseded);
}
self.insert_ordered(key, generation, keep, evict);
}
fn insert_ordered(
&mut self,
key: &CovSubscriptionKey,
generation: u64,
changes: Vec<TimedChange>,
evict: bool,
) {
let Some(history) = self.history_mut(key, generation) else {
return;
};
let mut added = Held::default();
let mut new_front = false;
for mut change in changes {
change.continues = false;
let at = history.entries.partition_point(|e| e.seq < change.seq);
match history.entries.get_mut(at).filter(|e| e.seq == change.seq) {
Some(entry) => {
let before = entry.memory();
added.octets += entry.rejoin(change);
added.memory += entry.memory() - before;
}
None => {
added.octets += change.octets;
added.memory += change.memory();
new_front |= at == 0;
history.entries.insert(at, change);
}
}
}
if let Some(context) = key.multiple_context().filter(|_| added != Held::default()) {
self.context_held
.entry(context.clone())
.or_default()
.grow(added);
}
if evict {
self.enforce_bound(key);
}
if new_front {
self.wake.notify_one();
}
}
fn history(&self, key: &CovSubscriptionKey, generation: u64) -> Option<&TimedHistory> {
self.histories
.get(key)
.filter(|h| h.generation == generation)
}
fn history_mut(
&mut self,
key: &CovSubscriptionKey,
generation: u64,
) -> Option<&mut TimedHistory> {
self.histories
.get_mut(key)
.filter(|h| h.generation == generation)
}
fn incarnation_mut(
&mut self,
key: &CovSubscriptionKey,
incarnation: u64,
) -> Option<&mut TimedHistory> {
self.histories
.get_mut(key)
.filter(|h| h.incarnation == incarnation)
}
}
#[derive(Debug, Clone)]
pub(crate) struct TimedStore {
histories: Arc<Mutex<TimedHistories>>,
}
impl TimedStore {
pub(super) fn new(max_apdu_length: usize, counters: Arc<AtomicCovCounters>) -> Self {
Self {
histories: Arc::new(Mutex::new(TimedHistories::new(max_apdu_length, counters))),
}
}
pub(crate) fn lock(&self) -> MutexGuard<'_, TimedHistories> {
self.histories
.lock()
.unwrap_or_else(|poison| poison.into_inner())
}
pub(crate) async fn next_due(&self) -> Vec<CovSubscriptionKey> {
let wake = Arc::clone(&self.lock().wake);
loop {
let notified = wake.notified();
tokio::pin!(notified);
notified.as_mut().enable();
let (due, next) = self.lock().take_due(Instant::now());
if !due.is_empty() {
return due;
}
match next {
Some(deadline) => {
tokio::select! {
() = tokio::time::sleep_until(deadline) => {}
() = notified => {}
}
}
None => notified.await,
}
}
}
pub(crate) fn rearm(&self) {
self.lock().rearm();
}
pub(crate) fn hold_until(&self, context: &MultipleContextKey, until: Instant) {
self.lock().hold_until(context, until);
}
}
mod bound;
mod claim;
mod drops;
mod owed;
use bound::Held;
pub(crate) use bound::{envelope_len, request_header};
pub(crate) use claim::{SendTurn, TimedClaim, ValueFit};
#[cfg(test)]
pub(crate) use drops::DropWarningCount;
use drops::{DropReason, DropWarnings};
#[cfg(test)]
#[path = "timed_split_tests.rs"]
mod split_tests;
#[cfg(test)]
#[path = "timed_tests.rs"]
mod tests;