mod device;
mod dispatch;
pub mod first;
mod jobs;
mod options;
pub mod poll;
mod sample;
mod shared;
use std::num::NonZeroU32;
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use bacnet_transport::port::TransportPort;
use bacnet_types::enums::PropertyIdentifier;
use bacnet_types::primitives::ObjectIdentifier;
use tokio::sync::watch;
use tokio::task::JoinHandle;
use tokio::time::Instant;
pub use self::options::{
Backoff, Fallback, MAX_ROWS, MAX_SAMPLES, MIN_POLL, MissingDevice, POLL_FLOOR, Reads,
WatchOptions,
};
pub use self::sample::{Sample, Samples, Via};
use self::shared::{Arrival, DeviceSlot, Registry, Running, Shared};
use crate::class::{self, Class};
use crate::engine::Engine;
use crate::format::RawRow;
use crate::limits::Account;
pub const POINT_PROPERTIES: [PropertyIdentifier; 10] = [
PropertyIdentifier::OBJECT_NAME,
PropertyIdentifier::PRESENT_VALUE,
PropertyIdentifier::STATUS_FLAGS,
PropertyIdentifier::EVENT_STATE,
PropertyIdentifier::RELIABILITY,
PropertyIdentifier::OUT_OF_SERVICE,
PropertyIdentifier::UNITS,
PropertyIdentifier::PRIORITY_ARRAY,
PropertyIdentifier::RELINQUISH_DEFAULT,
PropertyIdentifier::COV_INCREMENT,
];
const PROCESS_BASE: u32 = 0x4D43_0000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Lifetime(NonZeroU32);
impl Lifetime {
pub const DEFAULT: Self = Self(NonZeroU32::new(300).expect("300 is not zero"));
pub fn new(secs: u32) -> Option<Self> {
let secs = NonZeroU32::new(secs)?;
debug_assert_eq!(class::of_subscribe(secs.get()), Class::Read);
Some(Self(secs))
}
pub fn secs(self) -> u32 {
self.0.get()
}
fn duration(self) -> Duration {
Duration::from_secs(u64::from(self.secs()))
}
}
pub fn renewal_margin(secs: u32, request_budget: Duration) -> Duration {
let tenth = Duration::from_secs(u64::from(secs / 10).clamp(1, 30));
let half = Duration::from_secs(u64::from(secs)) / 2;
tenth.max(request_budget * 2).min(half)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Watched {
Object,
Property {
property: PropertyIdentifier,
index: Option<u32>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Point {
pub device: u32,
pub object: ObjectIdentifier,
pub watched: Watched,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
Cov,
Poll,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Starting,
Live,
Failed,
Unreachable,
Stopped,
Gone,
}
#[derive(Debug, Clone)]
pub struct Snapshot {
pub rows: Vec<RawRow>,
pub source: Option<Source>,
pub state: State,
pub note: Option<String>,
pub seq: u64,
pub updated: Option<Instant>,
pub notifications: u64,
pub coalesced: u64,
pub polls: u64,
pub renewals: u64,
pub why: Option<Why>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Why {
Refused(String),
NoAnswer(String),
Lapsed(Option<String>),
Missing,
}
impl Default for Snapshot {
fn default() -> Self {
Self::new()
}
}
impl Snapshot {
pub(super) fn new() -> Self {
Self {
rows: Vec::new(),
source: None,
state: State::Starting,
note: None,
seq: 0,
updated: None,
notifications: 0,
coalesced: 0,
polls: 0,
renewals: 0,
why: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WatchError {
Full {
max: usize,
},
Shut,
}
impl std::fmt::Display for WatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Full { max } => write!(f, "the hub watches its most points ({max})"),
Self::Shut => f.write_str("the hub has shut down"),
}
}
}
impl std::error::Error for WatchError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HubStats {
pub points: usize,
pub tasks: usize,
pub cov: usize,
pub polled: usize,
pub max_points: usize,
pub requests: u32,
pub throttled: Duration,
pub lagged: u64,
pub cancelling: usize,
}
pub struct Lease {
shared: Arc<Shared>,
registry: Arc<Registry>,
signal: watch::Receiver<u64>,
}
impl Lease {
pub fn point(&self) -> Point {
self.shared.point
}
pub fn snapshot(&self) -> Snapshot {
self.shared.snapshot()
}
pub async fn changed(&mut self) {
if self.signal.changed().await.is_err() {
std::future::pending::<()>().await;
}
}
pub fn samples(&self) -> Samples {
Samples::new(self.shared.samples.subscribe())
}
pub fn changes(&self) -> Changes {
Changes {
signal: self.signal.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct Changes {
signal: watch::Receiver<u64>,
}
impl Changes {
pub async fn changed(&mut self) {
if self.signal.changed().await.is_err() {
std::future::pending::<()>().await;
}
}
pub fn same(&self, other: &Changes) -> bool {
self.signal.same_channel(&other.signal)
}
}
impl Drop for Lease {
fn drop(&mut self) {
self.registry.release(&self.shared);
}
}
impl std::fmt::Debug for Lease {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Lease")
.field("point", &self.shared.point)
.finish()
}
}
struct Inner<T: TransportPort + 'static> {
engine: Arc<Engine<T>>,
options: WatchOptions,
registry: Arc<Registry>,
account: Arc<Account>,
handles: Mutex<Vec<JoinHandle<()>>>,
stop_dispatch: watch::Sender<bool>,
}
pub struct Hub<T: TransportPort + 'static> {
inner: Arc<Inner<T>>,
}
impl<T: TransportPort + 'static> Clone for Hub<T> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<T: TransportPort + 'static> Hub<T> {
pub fn new(engine: Arc<Engine<T>>, options: WatchOptions) -> Result<Self, String> {
options.validate()?;
let registry = Arc::new(Registry::new(
PROCESS_BASE,
options.coalesce,
options.reads.object.clone(),
options.reads.property_flags,
));
let (stop_dispatch, stopped) = watch::channel(false);
let notifications = engine.client().cov_notifications();
let dispatcher = tokio::spawn(dispatch::run(Arc::clone(®istry), notifications, stopped));
let account = Arc::new(Account::new(options.bucket.clone()));
Ok(Self {
inner: Arc::new(Inner {
engine,
options,
registry,
account,
handles: Mutex::new(vec![dispatcher]),
stop_dispatch,
}),
})
}
pub fn options(&self) -> &WatchOptions {
&self.inner.options
}
pub fn watch(&self, point: Point) -> Result<Lease, WatchError> {
let registry = &self.inner.registry;
let (shared, scheduler) = {
let mut table = registry.table();
if table.shut {
return Err(WatchError::Shut);
}
if let Some(slot) = table.entries.get_mut(&point) {
slot.leases += 1;
(Arc::clone(&slot.shared), None)
} else {
let max = self.inner.options.max_points;
if *registry.tasks.borrow() >= max {
return Err(WatchError::Full { max });
}
let mut process = table.next_process;
while process == 0 || table.by_process.contains_key(&process) {
process = process.wrapping_add(1);
}
table.next_process = process.wrapping_add(1);
let shared = Arc::new(Shared::new(point, process, self.inner.options.samples));
table.entries.insert(
point,
shared::Slot {
shared: Arc::clone(&shared),
leases: 1,
},
);
table.by_process.insert(process, Arc::clone(&shared));
let mut fresh = false;
let device = Arc::clone(table.devices.entry(point.device).or_insert_with(|| {
fresh = true;
Arc::new(DeviceSlot {
inbox: Mutex::new(Vec::new()),
wake: tokio::sync::Notify::new(),
})
}));
device.inbox().push(Arrival {
shared: Arc::clone(&shared),
running: Running::new(registry),
});
device.wake.notify_one();
(shared, fresh.then_some(device))
}
};
if let Some(slot) = scheduler {
let task = tokio::spawn(device::run(Arc::clone(&self.inner), point.device, slot));
let mut handles = self
.inner
.handles
.lock()
.unwrap_or_else(PoisonError::into_inner);
handles.retain(|handle| !handle.is_finished());
handles.push(task);
}
let signal = shared.signal.subscribe();
Ok(Lease {
shared,
registry: Arc::clone(registry),
signal,
})
}
pub fn peek(&self, point: &Point) -> Option<Snapshot> {
let shared = Arc::clone(&self.inner.registry.table().entries.get(point)?.shared);
Some(shared.snapshot())
}
pub fn seq(&self) -> u64 {
self.inner.registry.seq.load(Ordering::Relaxed)
}
pub fn stats(&self) -> HubStats {
let registry = &self.inner.registry;
let entries: Vec<Arc<Shared>> = registry
.table()
.entries
.values()
.map(|slot| Arc::clone(&slot.shared))
.collect();
let sources: Vec<Option<Source>> = entries
.iter()
.map(|shared| shared.lock().snapshot.source)
.collect();
HubStats {
points: entries.len(),
tasks: *registry.tasks.borrow(),
cov: sources.iter().filter(|s| **s == Some(Source::Cov)).count(),
polled: sources.iter().filter(|s| **s == Some(Source::Poll)).count(),
max_points: self.inner.options.max_points,
requests: self.inner.account.requests(),
throttled: self.inner.account.throttled(),
lagged: registry.lagged.load(Ordering::Relaxed),
cancelling: registry.cancelling.load(Ordering::Relaxed),
}
}
pub async fn shutdown(&self, wait: Duration) -> bool {
let (entries, devices): (Vec<Arc<Shared>>, Vec<Arc<DeviceSlot>>) = {
let mut table = self.inner.registry.table();
table.shut = true;
table.by_process.clear();
let entries = table.entries.drain().map(|(_, slot)| slot.shared).collect();
(entries, table.devices.values().cloned().collect())
};
for shared in &entries {
shared.stop();
}
for device in &devices {
device.wake.notify_one();
}
let mut tasks = self.inner.registry.tasks.subscribe();
let done = tokio::time::timeout(wait, tasks.wait_for(|running| *running == 0))
.await
.is_ok();
self.inner.stop_dispatch.send_replace(true);
let handles: Vec<JoinHandle<()>> = self
.inner
.handles
.lock()
.unwrap_or_else(PoisonError::into_inner)
.drain(..)
.collect();
for handle in &handles {
handle.abort();
}
for handle in handles {
let _ = handle.await;
}
let devices: Vec<Arc<DeviceSlot>> = self
.inner
.registry
.table()
.devices
.drain()
.map(|(_, d)| d)
.collect();
for device in devices {
drop(std::mem::take(&mut *device.inbox()));
}
if !done {
self.inner.registry.cancelling.store(0, Ordering::Relaxed);
tracing::warn!(
"gave up waiting for COV cancels; those subscriptions end with their lifetime"
);
}
done
}
}
#[cfg(test)]
mod lifecycle_tests;
#[cfg(test)]
mod scheduler_tests;
#[cfg(test)]
mod test_lab;
#[cfg(test)]
mod tests;