Skip to main content

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(&registry), 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;