use super::{
CovSubscriptionKey, DropReason, MultipleContextKey, TimedChange, TimedHistories, TimedHistory,
unsigned_len,
};
const CONFIRMED_HEADER: usize = 4;
const UNCONFIRMED_HEADER: usize = 2;
const DEVICE_IDENTIFIER: usize = 5;
const TIMESTAMP: usize = 12;
const LIST_TAGS: usize = 2;
pub(crate) fn request_header(confirmed: bool) -> usize {
if confirmed {
CONFIRMED_HEADER
} else {
UNCONFIRMED_HEADER
}
}
pub(crate) fn envelope_len(context: &MultipleContextKey, time_remaining: u32) -> usize {
request_header(context.confirmed)
+ unsigned_len(u64::from(context.process_id))
+ DEVICE_IDENTIFIER
+ unsigned_len(u64::from(time_remaining))
+ TIMESTAMP
+ LIST_TAGS
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct Held {
pub(super) octets: usize,
pub(super) memory: usize,
}
impl Held {
pub(super) fn of<'a>(changes: impl IntoIterator<Item = &'a TimedChange>) -> Self {
changes
.into_iter()
.fold(Self::default(), |held, change| Self {
octets: held.octets + change.octets,
memory: held.memory + change.memory(),
})
}
pub(super) fn grow(&mut self, more: Held) {
self.octets += more.octets;
self.memory += more.memory;
}
}
fn evictable(history: &TimedHistory) -> Option<usize> {
let newest = history.entries.len().checked_sub(1)?;
(0..newest).find(|&at| history.entries[at].seq != history.in_delivery)
}
impl TimedHistories {
pub(super) fn deliver_by_value(
&mut self,
key: &CovSubscriptionKey,
incarnation: u64,
seq: u64,
) {
if let Some(history) = self.incarnation_mut(key, incarnation) {
history.in_delivery = seq;
}
}
pub(super) fn enforce_bound(&mut self, key: &CovSubscriptionKey) {
let Some(context) = key.multiple_context().cloned() else {
return;
};
let Some(terms) = self.terms.get(&context).copied() else {
return;
};
let held = |histories: &Self| {
histories
.context_held
.get(&context)
.copied()
.unwrap_or_default()
};
while !terms.holds(held(self), self.local_apdu) {
let victim = match self.histories.get(key).and_then(evictable) {
Some(at) => Some((key.clone(), at)),
None => self
.histories
.iter()
.filter(|(k, _)| k.multiple_context() == Some(&context))
.filter_map(|(k, h)| evictable(h).map(|at| (k, at, h.entries[at].seq)))
.min_by_key(|&(_, _, seq)| seq)
.map(|(k, at, _)| (k.clone(), at)),
};
let Some((victim, at)) = victim else {
return;
};
let evicted = self
.histories
.get_mut(&victim)
.and_then(|h| h.entries.remove(at))
.expect("victim has an evictable change");
self.release(&victim, Held::of([&evicted]));
self.dropped(&victim, 1, DropReason::HistoryFull);
}
}
pub(super) fn release(&mut self, key: &CovSubscriptionKey, released: Held) {
let Some(context) = key.multiple_context() else {
return;
};
let Some(held) = self.context_held.get_mut(context) else {
debug_assert_eq!(released, Held::default(), "released more than held");
return;
};
debug_assert!(
held.octets >= released.octets && held.memory >= released.memory,
"released {released:?} of {held:?}"
);
held.octets = held.octets.saturating_sub(released.octets);
held.memory = held.memory.saturating_sub(released.memory);
if *held == Held::default() {
self.context_held.remove(context);
}
}
}
#[cfg(test)]
mod tests {
use bacnet_services::cov_multiple::{
COVNotificationItem, COVNotificationMultipleRequest, COVNotificationValue,
};
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use bytes::BytesMut;
use super::super::tests::{
change, context, dropped, frame, histories, key, seconds, timed_reference,
};
use super::super::{
CEILING_BYTES_PER_OCTET, CHANGE_MEMORY, HISTORY_NOTIFICATIONS, ITEM_FRAMING, TimedChange,
VALUE_MEMORY, value_len,
};
use super::*;
use crate::cov::{
AtomicCovCounters, CovObservation, CovSample, CovSubscriptionTable, SubscriberEndpoint,
};
use std::sync::Arc;
const LOCAL: usize = 1476;
fn notification(
context: &MultipleContextKey,
time_remaining: u32,
device: u32,
change: &TimedChange,
) -> Vec<u8> {
let frame = change.frame();
let request = COVNotificationMultipleRequest {
subscriber_process_identifier: context.process_id,
initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, device)
.unwrap(),
time_remaining,
timestamp: Some((frame.local_date, frame.local_time)),
list_of_cov_notifications: vec![COVNotificationItem {
monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 1)
.unwrap(),
list_of_values: change.values().to_vec(),
}],
};
let mut encoded = BytesMut::new();
request.encode(&mut encoded).unwrap();
encoded.to_vec()
}
fn real_present_value(second: u8) -> TimedChange {
let sample = CovSample::new(&PropertyValue::Real(22.5)).unwrap();
TimedChange::new(
frame(second),
vec![COVNotificationValue {
property_identifier: PropertyIdentifier::PRESENT_VALUE,
property_array_index: None,
value: vec![0x44, 0x41, 0xB4, 0x00, 0x00],
time_of_change: None,
}],
CovObservation::new(sample, None).unwrap(),
)
}
fn item_len(change: &TimedChange) -> usize {
ITEM_FRAMING + change.values().iter().map(value_len).sum::<usize>()
}
#[test]
fn the_envelope_is_what_the_encoder_lays_around_the_items() {
let change = real_present_value(30);
let widths = [0, 255, 256, 65_535, 65_536, 16_777_216, u32::MAX];
for confirmed in [false, true] {
for process_id in widths {
for time_remaining in widths {
for device in [0, 4_194_302] {
let context = MultipleContextKey {
process_id,
confirmed,
..context(1)
};
let wire = request_header(confirmed)
+ notification(&context, time_remaining, device, &change).len();
assert_eq!(
wire,
envelope_len(&context, time_remaining) + item_len(&change),
"confirmed={confirmed} process={process_id} \
remaining={time_remaining} device={device}"
);
}
}
}
}
}
#[test]
fn at_a_50_octet_subscriber_a_change_that_fits_the_wire_fits_the_bound() {
let (mut h, counters) = histories(8, 4);
let k = key(1, 1);
let small = context(1);
h.reset(&k, 1, 0);
h.set_sizing(&small, Some(50), 120);
let present_value = real_present_value(1);
let wire = request_header(false) + notification(&small, 120, 8, &present_value).len();
assert!(wire <= 50, "{wire} octets on the wire");
let largest = MultipleContextKey {
process_id: u32::MAX,
confirmed: true,
..context(1)
};
assert_eq!(envelope_len(&largest, u32::MAX), 33);
assert!(item_len(&present_value) > 50 - 33);
assert!(item_len(&present_value) <= h.terms[&small].notification());
for second in 1..=5 {
h.push(&k, 1, change(second, 4));
}
assert_eq!(seconds(&h.drain(&k, 1).1), [1, 2, 3, 4, 5]);
assert_eq!(dropped(&counters), 0);
h.set_sizing(&small, Some(50), 300);
for second in 6..=10 {
h.push(&k, 1, change(second, 4));
}
assert_eq!(seconds(&h.drain(&k, 1).1), [7, 8, 9, 10]);
assert_eq!(dropped(&counters), 1);
}
fn sizes(payload: usize) -> (usize, usize) {
let items = item_len(&change(0, payload));
(items, change(0, payload).memory())
}
#[cfg(target_pointer_width = "64")]
#[test]
fn the_memory_terms_are_the_measured_sizes() {
assert_eq!(std::mem::size_of::<TimedChange>(), 144);
assert_eq!(std::mem::size_of::<COVNotificationValue>(), 48);
assert_eq!((CHANGE_MEMORY, VALUE_MEMORY), (144, 56));
assert_eq!(change(0, 0).memory(), 200);
assert_eq!(real_present_value(0).memory(), 205);
}
#[test]
fn at_a_50_octet_subscriber_the_bound_keeps_four_notifications_of_changes() {
let counters = Arc::new(AtomicCovCounters::default());
let mut h = TimedHistories::new(LOCAL, Arc::clone(&counters));
let k = key(1, 1);
let small = context(1);
h.reset(&k, 1, 0);
h.set_sizing(&small, Some(50), 120);
let room = 50 - envelope_len(&small, 120);
let present_value = item_len(&real_present_value(0));
assert!(present_value <= room && 2 * present_value > room);
for second in 1..=10 {
h.push(&k, 1, real_present_value(second));
}
assert_eq!(seconds(&h.drain(&k, 1).1), [7, 8, 9, 10]);
assert_eq!(dropped(&counters), 6);
let (binary, _) = sizes(2);
assert_eq!((binary, HISTORY_NOTIFICATIONS * room), (18, 100));
for second in 11..=20 {
h.push(&k, 1, change(second, 2));
}
assert_eq!(seconds(&h.drain(&k, 1).1), [16, 17, 18, 19, 20]);
assert_eq!(dropped(&counters), 11);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn the_bound_keeps_what_fits_both_its_notifications_and_its_memory_ceiling() {
let (k, small) = (key(1, 1), context(1));
let envelope = envelope_len(&small, 120);
let ceiling = CEILING_BYTES_PER_OCTET * HISTORY_NOTIFICATIONS * (LOCAL - envelope);
for subscriber in [50, 128, 206, 480, 1024, 1476] {
let room = HISTORY_NOTIFICATIONS * (usize::from(subscriber) - envelope);
for payload in [0, 2, 5, 40, 200, 2000] {
let (items, memory) = sizes(payload);
let fit = (room / items).min(ceiling / memory).max(1);
if usize::from(subscriber) == LOCAL {
let ceiling_binds = ceiling / memory < room / items;
assert_eq!(ceiling_binds, payload <= 40, "payload {payload}");
}
let counters = Arc::new(AtomicCovCounters::default());
let mut h = TimedHistories::new(LOCAL, Arc::clone(&counters));
h.reset(&k, 1, 0);
h.set_sizing(&small, Some(subscriber), 120);
for n in 0..fit + 2 {
h.push(&k, 1, change(u8::try_from(n % 60).unwrap(), payload));
}
let kept = h.drain(&k, 1).1.len();
assert_eq!(kept, fit, "subscriber {subscriber}, payload {payload}");
assert_eq!(
dropped(&counters),
2,
"subscriber {subscriber}, payload {payload}"
);
}
}
}
#[cfg(target_pointer_width = "64")]
#[test]
fn the_memory_ceiling_caps_every_context_under_many_subscribers() {
let counters = Arc::new(AtomicCovCounters::default());
let mut h = TimedHistories::new(LOCAL, Arc::clone(&counters));
let (items, memory) = sizes(0);
assert_eq!((items, memory), (16, CHANGE_MEMORY + VALUE_MEMORY));
let contexts: Vec<_> = (1..=64u32)
.map(|process| (process, (process % 2 == 0).then_some(50u16)))
.collect();
for &(process, subscriber) in &contexts {
for instance in 1..=2 {
h.reset(&key(process, instance), 1, 0);
}
h.set_sizing(&context(process), subscriber, 120);
}
for second in 0..200u8 {
for &(process, _) in &contexts {
for instance in 1..=2 {
h.push(&key(process, instance), 1, change(second % 60, 0));
}
}
}
for &(process, subscriber) in &contexts {
let envelope = envelope_len(&context(process), 120);
let ceiling = CEILING_BYTES_PER_OCTET * HISTORY_NOTIFICATIONS * (LOCAL - envelope);
let held: usize = (1..=2)
.map(|instance| h.drain(&key(process, instance), 1).1.len())
.sum();
assert!(held * memory <= ceiling, "process {process}");
let (expected, fit) = match subscriber {
Some(apdu) => (
6,
HISTORY_NOTIFICATIONS * (usize::from(apdu) - envelope) / items,
),
None => (116, ceiling / memory),
};
assert_eq!((held, fit), (expected, expected), "process {process}");
}
}
#[test]
fn admission_sizes_the_envelope_from_the_lifetime_left() {
let route = SubscriberEndpoint::new(&[10, 0, 0, 1, 0xBA, 0xC0], None);
for (lifetime, envelope) in [(120, 25), (300, 26), (70_000, 27)] {
let mut table = CovSubscriptionTable::new();
let expires = std::time::Instant::now() + std::time::Duration::from_secs(lifetime);
let reference = timed_reference(1, expires);
table
.subscribe_multiple(&context(1), &route, expires, 10, Some(50), vec![reference])
.unwrap();
let terms = table.timed().lock().terms[&context(1)];
assert_eq!(
(terms.envelope, terms.notification()),
(envelope, 50 - envelope),
"lifetime {lifetime}"
);
}
}
}