bacnet_ops/watch/mod.rs
1//! The watch hub (#2067): one managed BACnet subscription or poller per
2//! point, however many listeners share it.
3//!
4//! [`Hub::watch`] hands out a [`Lease`] on a [`Point`]. The first lease on a
5//! point starts its entry, which goes to its device's scheduler (#2115):
6//! one task per watched device, running one job at a time. The scheduler
7//! reads the point once (what it reads is [`WatchOptions::reads`]), then
8//! asks the device for a finite COV subscription (SubscribeCOV for an
9//! object, SubscribeCOVProperty for one property) through the client's
10//! managed subscriptions, which renew it before it lapses. Points the
11//! device won't subscribe are polled instead, all of a device's together
12//! in batched ReadPropertyMultiple requests ([`poll`]), never faster than
13//! [`WatchOptions::poll_floor`]; a device that stops answering is polled
14//! after a backoff. Later leases on the point share the entry; the last
15//! one dropped stops it, and the scheduler cancels its subscription the
16//! way the TUI's watch list does when a row goes. A listener joining while
17//! the last one leaves gets either the running entry or a new one with its
18//! own subscriber process identifier: both happen under one lock, so no
19//! entry is shared after its stop.
20//!
21//! The hub keeps each point's latest values ([`Snapshot`]), so reading a
22//! watched point sends nothing. COV notifications come from one receiver of
23//! the client's channel ([`BACnetClient::cov_notifications`]), the path the
24//! TUI's watch list and the managed renewals already use; the hub adds no
25//! second notification path. The receiver never blocks the client: a burst
26//! it falls behind on is counted as lagged, and changes to one point within
27//! [`WatchOptions::coalesce`] are folded into one signal and counted as
28//! coalesced, while the values themselves are always the latest. A front
29//! end that keeps a history reads every value from [`Lease::samples`].
30//!
31//! Every request a scheduler sends (first reads, subscriptions, polls)
32//! is charged to the hub's own [`Account`], whose token bucket
33//! ([`WatchOptions::bucket`]) holds the hub to its own rate besides the
34//! engine's limits. A shared point costs the same whatever the number of
35//! listeners, and notifications cost nothing. Renewals take the device's
36//! gate but no rate slot, as in the TUI; cancels skip the hub's bucket so a
37//! shutdown isn't held to it.
38//!
39//! Lifetimes are finite by construction ([`Lifetime`] can't be zero): a
40//! subscription that never lapses is a write in [`crate::class`], and the
41//! hub never sends one.
42//!
43//! [`BACnetClient::cov_notifications`]: bacnet_client::client::BACnetClient::cov_notifications
44
45mod device;
46mod dispatch;
47pub mod first;
48mod jobs;
49mod options;
50pub mod poll;
51mod sample;
52mod shared;
53
54use std::num::NonZeroU32;
55use std::sync::atomic::Ordering;
56use std::sync::{Arc, Mutex, PoisonError};
57use std::time::Duration;
58
59use bacnet_transport::port::TransportPort;
60use bacnet_types::enums::PropertyIdentifier;
61use bacnet_types::primitives::ObjectIdentifier;
62use tokio::sync::watch;
63use tokio::task::JoinHandle;
64use tokio::time::Instant;
65
66pub use self::options::{
67 Backoff, Fallback, MAX_ROWS, MAX_SAMPLES, MIN_POLL, MissingDevice, POLL_FLOOR, Reads,
68 WatchOptions,
69};
70pub use self::sample::{Sample, Samples, Via};
71use self::shared::{Arrival, DeviceSlot, Registry, Running, Shared};
72use crate::class::{self, Class};
73use crate::engine::Engine;
74use crate::format::RawRow;
75use crate::limits::Account;
76
77/// The properties an object watch reads: what an agent or an operator needs
78/// to reason about a point (value, status, units, priorities, increment).
79pub const POINT_PROPERTIES: [PropertyIdentifier; 10] = [
80 PropertyIdentifier::OBJECT_NAME,
81 PropertyIdentifier::PRESENT_VALUE,
82 PropertyIdentifier::STATUS_FLAGS,
83 PropertyIdentifier::EVENT_STATE,
84 PropertyIdentifier::RELIABILITY,
85 PropertyIdentifier::OUT_OF_SERVICE,
86 PropertyIdentifier::UNITS,
87 PropertyIdentifier::PRIORITY_ARRAY,
88 PropertyIdentifier::RELINQUISH_DEFAULT,
89 PropertyIdentifier::COV_INCREMENT,
90];
91
92/// Subscriber process identifiers start here, clear of the small numbers
93/// the TUI's watch rows use.
94const PROCESS_BASE: u32 = 0x4D43_0000;
95
96/// A COV subscription's lifetime in seconds: never zero, so never the
97/// subscription that doesn't lapse.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct Lifetime(NonZeroU32);
100
101impl Lifetime {
102 /// Five minutes, the TUI's default.
103 pub const DEFAULT: Self = Self(NonZeroU32::new(300).expect("300 is not zero"));
104
105 /// A lifetime of `secs`; `None` for 0.
106 pub fn new(secs: u32) -> Option<Self> {
107 let secs = NonZeroU32::new(secs)?;
108 debug_assert_eq!(class::of_subscribe(secs.get()), Class::Read);
109 Some(Self(secs))
110 }
111
112 /// The lifetime in seconds, at least 1.
113 pub fn secs(self) -> u32 {
114 self.0.get()
115 }
116
117 fn duration(self) -> Duration {
118 Duration::from_secs(u64::from(self.secs()))
119 }
120}
121
122/// How long before a lifetime of `secs` ends its renewal should go: a
123/// tenth of it (1 to 30 s), but never later than two `request_budget`s
124/// before the end. A budget is the time one request takes with every
125/// retry: the renewal waits at most one for the request holding the
126/// device's gate (#1791), which may be a whole budget on a device that
127/// stops answering, then needs one of its own, so neither a busy device nor
128/// one lost packet lets it lapse. Each worker queues at most one request
129/// per device behind that holder, and they rarely all coincide with a
130/// renewal, so two budgets cover the case that matters without renewing
131/// far earlier than needed. The margin stays at most half the lifetime.
132pub fn renewal_margin(secs: u32, request_budget: Duration) -> Duration {
133 let tenth = Duration::from_secs(u64::from(secs / 10).clamp(1, 30));
134 let half = Duration::from_secs(u64::from(secs)) / 2;
135 tenth.max(request_budget * 2).min(half)
136}
137
138/// What a watch follows on an object.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
140pub enum Watched {
141 /// The object: SubscribeCOV, its [`POINT_PROPERTIES`] read and polled.
142 Object,
143 /// One property, or one element of it: SubscribeCOVProperty.
144 Property {
145 /// The property.
146 property: PropertyIdentifier,
147 /// The array element, if one.
148 index: Option<u32>,
149 },
150}
151
152/// A watched point: an object of a device, whole or one property.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
154pub struct Point {
155 /// The device instance, looked up in the client's discovery table.
156 pub device: u32,
157 /// The object.
158 pub object: ObjectIdentifier,
159 /// The object or one property.
160 pub watched: Watched,
161}
162
163/// Where a point's values come from now.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum Source {
166 /// COV notifications under a managed subscription.
167 Cov,
168 /// Polls, because the device refused COV or stopped renewing it.
169 Poll,
170}
171
172/// How a point's watch is going.
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum State {
175 /// Its first read hasn't finished.
176 Starting,
177 /// Subscribed, or the last poll answered.
178 Live,
179 /// The device answered the last read with an error for the whole
180 /// object (see [`Snapshot::note`]).
181 Failed,
182 /// The device didn't answer the last request.
183 Unreachable,
184 /// The watch ended (the hub shut down).
185 Stopped,
186 /// Nothing watches it any more: its device isn't in the discovery
187 /// table and [`WatchOptions::missing`] says not to wait for it (see
188 /// [`Snapshot::note`]).
189 Gone,
190}
191
192/// A point's latest values and how its watch is going.
193#[derive(Debug, Clone)]
194pub struct Snapshot {
195 /// Each property's latest raw value, or why the device gave none.
196 pub rows: Vec<RawRow>,
197 /// Where the values come from; `None` before the first read.
198 pub source: Option<Source>,
199 /// How the watch is going.
200 pub state: State,
201 /// Why the point is polled or not live, as text.
202 pub note: Option<String>,
203 /// The hub's sequence number at the last change; 0 before any.
204 pub seq: u64,
205 /// When the values last changed.
206 pub updated: Option<Instant>,
207 /// COV notifications applied.
208 pub notifications: u64,
209 /// Changes folded into another's signal.
210 pub coalesced: u64,
211 /// Polls answered.
212 pub polls: u64,
213 /// Renewals the device took.
214 pub renewals: u64,
215 /// Why the point is polled, or was given up, as its start or a failed
216 /// renewal found; a poll's error, which goes to [`note`](Self::note),
217 /// leaves it. `None` while subscribed, and before the first start ends.
218 pub why: Option<Why>,
219}
220
221/// Why a point is polled or given up ([`Snapshot::why`]).
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub enum Why {
224 /// The device refused the subscription or didn't answer it, with the
225 /// error.
226 Refused(String),
227 /// The first read had no answer, so no subscription was asked for.
228 NoAnswer(String),
229 /// A renewal failed, with its error, or stopped.
230 Lapsed(Option<String>),
231 /// The device isn't in the discovery table.
232 Missing,
233}
234
235impl Default for Snapshot {
236 /// A point not read yet: starting, no rows, nothing counted.
237 fn default() -> Self {
238 Self::new()
239 }
240}
241
242impl Snapshot {
243 pub(super) fn new() -> Self {
244 Self {
245 rows: Vec::new(),
246 source: None,
247 state: State::Starting,
248 note: None,
249 seq: 0,
250 updated: None,
251 notifications: 0,
252 coalesced: 0,
253 polls: 0,
254 renewals: 0,
255 why: None,
256 }
257 }
258}
259
260/// Why a watch wasn't started.
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub enum WatchError {
263 /// The hub watches its most points already.
264 Full {
265 /// [`WatchOptions::max_points`].
266 max: usize,
267 },
268 /// The hub has shut down.
269 Shut,
270}
271
272impl std::fmt::Display for WatchError {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 match self {
275 Self::Full { max } => write!(f, "the hub watches its most points ({max})"),
276 Self::Shut => f.write_str("the hub has shut down"),
277 }
278 }
279}
280
281impl std::error::Error for WatchError {}
282
283/// The hub's footprint, for a session view.
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct HubStats {
286 /// Points watched.
287 pub points: usize,
288 /// Entries still running, those cancelling included.
289 pub tasks: usize,
290 /// Points under a COV subscription.
291 pub cov: usize,
292 /// Points polled.
293 pub polled: usize,
294 /// [`WatchOptions::max_points`].
295 pub max_points: usize,
296 /// Requests the entries sent.
297 pub requests: u32,
298 /// How long they waited for the limits.
299 pub throttled: Duration,
300 /// COV notifications lost because the hub's receiver fell behind.
301 pub lagged: u64,
302 /// Cancels queued or running: subscriptions the devices may still hold
303 /// for points no longer watched.
304 pub cancelling: usize,
305}
306
307/// A lease on a watched point: while any lease on it lives, the hub keeps
308/// it watched. Dropping the last one stops the watch and cancels the
309/// subscription.
310pub struct Lease {
311 shared: Arc<Shared>,
312 registry: Arc<Registry>,
313 signal: watch::Receiver<u64>,
314}
315
316impl Lease {
317 /// The point.
318 pub fn point(&self) -> Point {
319 self.shared.point
320 }
321
322 /// Its latest values and state.
323 pub fn snapshot(&self) -> Snapshot {
324 self.shared.snapshot()
325 }
326
327 /// Wait for the next signalled change. Changes within the coalescing
328 /// window make one signal, and a listener that falls behind sees only
329 /// the latest.
330 pub async fn changed(&mut self) {
331 // The sender lives as long as the entry, which this lease holds.
332 if self.signal.changed().await.is_err() {
333 std::future::pending::<()>().await;
334 }
335 }
336
337 /// Every value the point gets from now on, as read or notified, which
338 /// neither coalescing nor a slow listener folds away; it holds
339 /// [`WatchOptions::samples`] and counts what a reader that falls behind
340 /// loses.
341 pub fn samples(&self) -> Samples {
342 Samples::new(self.shared.samples.subscribe())
343 }
344
345 /// A second view of this lease's change signal, for a waiter that
346 /// can't hold the lease itself (one kept under a lock, say). It starts
347 /// where the lease stands and keeps nothing watched: once the point's
348 /// last lease goes, it never fires again.
349 pub fn changes(&self) -> Changes {
350 Changes {
351 signal: self.signal.clone(),
352 }
353 }
354}
355
356/// A point's change signal without a lease ([`Lease::changes`]).
357#[derive(Debug, Clone)]
358pub struct Changes {
359 signal: watch::Receiver<u64>,
360}
361
362impl Changes {
363 /// Wait for the next signalled change, as [`Lease::changed`] does;
364 /// pending for ever once the point is no longer watched. Cancel safe.
365 pub async fn changed(&mut self) {
366 if self.signal.changed().await.is_err() {
367 std::future::pending::<()>().await;
368 }
369 }
370
371 /// Whether `other` is a signal of the same watch: false once the point
372 /// was let go and watched again, which makes a new watch.
373 pub fn same(&self, other: &Changes) -> bool {
374 self.signal.same_channel(&other.signal)
375 }
376}
377
378impl Drop for Lease {
379 fn drop(&mut self) {
380 self.registry.release(&self.shared);
381 }
382}
383
384impl std::fmt::Debug for Lease {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 f.debug_struct("Lease")
387 .field("point", &self.shared.point)
388 .finish()
389 }
390}
391
392struct Inner<T: TransportPort + 'static> {
393 engine: Arc<Engine<T>>,
394 options: WatchOptions,
395 registry: Arc<Registry>,
396 account: Arc<Account>,
397 handles: Mutex<Vec<JoinHandle<()>>>,
398 stop_dispatch: watch::Sender<bool>,
399}
400
401/// The watch hub over one engine. Cheap to clone: clones share one hub.
402pub struct Hub<T: TransportPort + 'static> {
403 inner: Arc<Inner<T>>,
404}
405
406impl<T: TransportPort + 'static> Clone for Hub<T> {
407 fn clone(&self) -> Self {
408 Self {
409 inner: Arc::clone(&self.inner),
410 }
411 }
412}
413
414impl<T: TransportPort + 'static> Hub<T> {
415 /// A hub over `engine`, under checked `options`. It starts its
416 /// notification receiver, so it must be made inside a Tokio runtime.
417 pub fn new(engine: Arc<Engine<T>>, options: WatchOptions) -> Result<Self, String> {
418 options.validate()?;
419 let registry = Arc::new(Registry::new(
420 PROCESS_BASE,
421 options.coalesce,
422 options.reads.object.clone(),
423 options.reads.property_flags,
424 ));
425 let (stop_dispatch, stopped) = watch::channel(false);
426 let notifications = engine.client().cov_notifications();
427 let dispatcher = tokio::spawn(dispatch::run(Arc::clone(®istry), notifications, stopped));
428 let account = Arc::new(Account::new(options.bucket.clone()));
429 Ok(Self {
430 inner: Arc::new(Inner {
431 engine,
432 options,
433 registry,
434 account,
435 handles: Mutex::new(vec![dispatcher]),
436 stop_dispatch,
437 }),
438 })
439 }
440
441 /// The hub's settings.
442 pub fn options(&self) -> &WatchOptions {
443 &self.inner.options
444 }
445
446 /// A lease on `point`, starting its watch if it has none. A new
447 /// watch goes to its device's scheduler, started with it when the
448 /// device has none.
449 pub fn watch(&self, point: Point) -> Result<Lease, WatchError> {
450 let registry = &self.inner.registry;
451 let (shared, scheduler) = {
452 let mut table = registry.table();
453 if table.shut {
454 return Err(WatchError::Shut);
455 }
456 if let Some(slot) = table.entries.get_mut(&point) {
457 slot.leases += 1;
458 (Arc::clone(&slot.shared), None)
459 } else {
460 let max = self.inner.options.max_points;
461 if *registry.tasks.borrow() >= max {
462 return Err(WatchError::Full { max });
463 }
464 let mut process = table.next_process;
465 // 0 is skipped when the count wraps.
466 while process == 0 || table.by_process.contains_key(&process) {
467 process = process.wrapping_add(1);
468 }
469 table.next_process = process.wrapping_add(1);
470 let shared = Arc::new(Shared::new(point, process, self.inner.options.samples));
471 table.entries.insert(
472 point,
473 shared::Slot {
474 shared: Arc::clone(&shared),
475 leases: 1,
476 },
477 );
478 table.by_process.insert(process, Arc::clone(&shared));
479 let mut fresh = false;
480 let device = Arc::clone(table.devices.entry(point.device).or_insert_with(|| {
481 fresh = true;
482 Arc::new(DeviceSlot {
483 inbox: Mutex::new(Vec::new()),
484 wake: tokio::sync::Notify::new(),
485 })
486 }));
487 device.inbox().push(Arrival {
488 shared: Arc::clone(&shared),
489 running: Running::new(registry),
490 });
491 device.wake.notify_one();
492 (shared, fresh.then_some(device))
493 }
494 };
495 if let Some(slot) = scheduler {
496 let task = tokio::spawn(device::run(Arc::clone(&self.inner), point.device, slot));
497 let mut handles = self
498 .inner
499 .handles
500 .lock()
501 .unwrap_or_else(PoisonError::into_inner);
502 handles.retain(|handle| !handle.is_finished());
503 handles.push(task);
504 }
505 let signal = shared.signal.subscribe();
506 Ok(Lease {
507 shared,
508 registry: Arc::clone(registry),
509 signal,
510 })
511 }
512
513 /// `point`'s latest values, if it is watched: nothing is sent.
514 pub fn peek(&self, point: &Point) -> Option<Snapshot> {
515 let shared = Arc::clone(&self.inner.registry.table().entries.get(point)?.shared);
516 Some(shared.snapshot())
517 }
518
519 /// The hub's sequence number: every change takes the next one, so a
520 /// point changed since an earlier reading of this when its
521 /// [`Snapshot::seq`] is greater.
522 pub fn seq(&self) -> u64 {
523 self.inner.registry.seq.load(Ordering::Relaxed)
524 }
525
526 /// The hub's footprint now.
527 pub fn stats(&self) -> HubStats {
528 let registry = &self.inner.registry;
529 let entries: Vec<Arc<Shared>> = registry
530 .table()
531 .entries
532 .values()
533 .map(|slot| Arc::clone(&slot.shared))
534 .collect();
535 let sources: Vec<Option<Source>> = entries
536 .iter()
537 .map(|shared| shared.lock().snapshot.source)
538 .collect();
539 HubStats {
540 points: entries.len(),
541 tasks: *registry.tasks.borrow(),
542 cov: sources.iter().filter(|s| **s == Some(Source::Cov)).count(),
543 polled: sources.iter().filter(|s| **s == Some(Source::Poll)).count(),
544 max_points: self.inner.options.max_points,
545 requests: self.inner.account.requests(),
546 throttled: self.inner.account.throttled(),
547 lagged: registry.lagged.load(Ordering::Relaxed),
548 cancelling: registry.cancelling.load(Ordering::Relaxed),
549 }
550 }
551
552 /// Stop every watch, wait up to `wait` for their cancels, and stop the
553 /// notification receiver. Later [`watch`](Self::watch) calls fail.
554 /// True when every cancel finished in time; the subscriptions of those
555 /// that didn't lapse with their lifetime.
556 pub async fn shutdown(&self, wait: Duration) -> bool {
557 let (entries, devices): (Vec<Arc<Shared>>, Vec<Arc<DeviceSlot>>) = {
558 let mut table = self.inner.registry.table();
559 table.shut = true;
560 table.by_process.clear();
561 let entries = table.entries.drain().map(|(_, slot)| slot.shared).collect();
562 (entries, table.devices.values().cloned().collect())
563 };
564 for shared in &entries {
565 shared.stop();
566 }
567 for device in &devices {
568 device.wake.notify_one();
569 }
570 let mut tasks = self.inner.registry.tasks.subscribe();
571 let done = tokio::time::timeout(wait, tasks.wait_for(|running| *running == 0))
572 .await
573 .is_ok();
574 self.inner.stop_dispatch.send_replace(true);
575 let handles: Vec<JoinHandle<()>> = self
576 .inner
577 .handles
578 .lock()
579 .unwrap_or_else(PoisonError::into_inner)
580 .drain(..)
581 .collect();
582 // Every task is aborted, so the joins below take no time and the
583 // call keeps to `wait`. Once every point is done, cancels included,
584 // a scheduler can only be running a poll of points no longer
585 // watched, which nothing needs; past the deadline, a cancel still
586 // waiting is given up and its subscription lapses with its
587 // lifetime.
588 for handle in &handles {
589 handle.abort();
590 }
591 for handle in handles {
592 let _ = handle.await;
593 }
594 // Points an aborted scheduler never took are counted out.
595 let devices: Vec<Arc<DeviceSlot>> = self
596 .inner
597 .registry
598 .table()
599 .devices
600 .drain()
601 .map(|(_, d)| d)
602 .collect();
603 for device in devices {
604 drop(std::mem::take(&mut *device.inbox()));
605 }
606 if !done {
607 // The aborted cancels never finish to count themselves out.
608 self.inner.registry.cancelling.store(0, Ordering::Relaxed);
609 tracing::warn!(
610 "gave up waiting for COV cancels; those subscriptions end with their lifetime"
611 );
612 }
613 done
614 }
615}
616
617#[cfg(test)]
618mod lifecycle_tests;
619#[cfg(test)]
620mod scheduler_tests;
621#[cfg(test)]
622mod test_lab;
623#[cfg(test)]
624mod tests;