Skip to main content

matter_controller/
subscription.rs

1//! A live attribute subscription: reports arrive via [`Subscription::next`].
2
3use matter_codec::Value;
4use matter_interaction::{AttributePath, EventReport};
5use tokio::sync::mpsc;
6
7use crate::actor::Command;
8use crate::error::Error;
9
10/// One reported attribute change from a subscription.
11#[derive(Clone, Debug, PartialEq)]
12pub struct AttributeReport {
13    /// The concrete attribute path the device reported.
14    pub path: AttributePath,
15    /// The new value.
16    pub value: Value,
17}
18
19/// An event from a live [`Subscription`].
20///
21/// This enum is `#[non_exhaustive]`: matching on it must include a wildcard arm,
22/// because future protocol work may add variants without a breaking change.
23#[derive(Debug)]
24#[non_exhaustive]
25pub enum SubscriptionEvent {
26    /// A reported attribute value (a priming value or a steady-state change).
27    Report(AttributeReport),
28    /// A reported event (a priming/historical event or a steady-state emission).
29    /// Delivered on the same bounded report channel as [`Self::Report`]; under
30    /// backpressure it is dropped and surfaced via [`Self::Lagged`] like any
31    /// report. Events have no list-merge semantics, so they are delivered as they
32    /// arrive (they bypass the chunked-attribute reassembler).
33    Event(EventReport),
34    /// The subscription was (re-)established by the device; carries the
35    /// device-assigned subscription id. Fired after each successful
36    /// `SubscribeResponse`, including after an auto-resubscribe (SH.2b). Priming
37    /// [`Self::Report`]s, if any, precede it (they arrive before the
38    /// `SubscribeResponse` on the wire).
39    ///
40    /// Delivered reliably even under report backpressure (see [`Subscription`]).
41    Established {
42        /// The device-assigned subscription id.
43        subscription_id: u32,
44    },
45    /// The subscription went stale (liveness timeout or session loss) and is
46    /// being transparently re-established; `cause` is why. Reports resume after
47    /// the next [`Self::Established`]. Emitted by the SH.2b resubscribe engine.
48    ///
49    /// Delivered reliably even under report backpressure (see [`Subscription`]).
50    Resubscribing {
51        /// Why the subscription is being re-established.
52        cause: Error,
53    },
54    /// One or more [`Self::Report`]s were dropped because the consumer did not
55    /// drain [`Subscription::next`] fast enough to keep up with the device's
56    /// reporting cadence, and the bounded report buffer filled. `dropped` is the
57    /// number of reports discarded since the previous `Lagged` (a coalesced
58    /// count, not one event per drop). Subsequent reports continue to arrive;
59    /// only the buffer-overflow ones were lost. A re-read or the next
60    /// [`Self::Established`] re-prime can be used to recover authoritative state.
61    Lagged {
62        /// Number of reports dropped since the last `Lagged` event.
63        dropped: usize,
64    },
65}
66
67/// Capacity of the bounded report channel feeding a [`Subscription`].
68///
69/// Steady-state attribute reports are buffered here. The cap bounds controller
70/// memory: a malicious or compromised device controls how many attribute items
71/// each `ReportData` carries and how often it sends them (`min_interval` is only
72/// a value we *request* — the device need not honour it), so an unbounded buffer
73/// would let such a device drive controller memory growth without limit
74/// (memory-DoS). When the buffer is full, further reports are dropped and a
75/// [`SubscriptionEvent::Lagged`] event signals how many were lost; control
76/// events ([`SubscriptionEvent::Established`] / [`SubscriptionEvent::Resubscribing`])
77/// are never dropped — they travel on a separate, low-volume channel.
78pub(crate) const SUBSCRIPTION_CHANNEL_CAP: usize = 256;
79
80/// A live attribute subscription. Await events with [`Self::next`]; dropping
81/// the handle cancels the subscription (best-effort).
82///
83/// Steady-state [`SubscriptionEvent::Report`]s are buffered in a **bounded**
84/// channel (capacity `SUBSCRIPTION_CHANNEL_CAP`, 256) so a device — whose reporting
85/// cadence and per-report size are attacker-controlled — cannot drive unbounded
86/// controller memory growth. If the consumer does not call [`Self::next`]
87/// promptly and the buffer fills, excess reports are dropped and a
88/// [`SubscriptionEvent::Lagged`] event reports how many were lost.
89///
90/// Control events ([`SubscriptionEvent::Established`] and
91/// [`SubscriptionEvent::Resubscribing`]) travel on a separate, low-volume channel
92/// and are delivered **reliably** even while reports are being dropped; they are
93/// also prioritised by [`Self::next`].
94pub struct Subscription {
95    /// Bounded channel of steady-state reports (and coalesced `Lagged` signals).
96    pub(crate) rx: mpsc::Receiver<SubscriptionEvent>,
97    /// Reliable, low-volume channel of control events (`Established` /
98    /// `Resubscribing`). Kept separate so a saturated report buffer can never
99    /// drop a control event.
100    pub(crate) ctrl_rx: mpsc::UnboundedReceiver<SubscriptionEvent>,
101    pub(crate) tx: mpsc::Sender<Command>,
102    pub(crate) key: crate::actor::SubId,
103    pub(crate) cancelled: bool,
104}
105
106impl Subscription {
107    /// Await the next subscription event, or `None` once the subscription has
108    /// ended (cancelled, or the controller task stopped).
109    ///
110    /// Control events ([`SubscriptionEvent::Established`] /
111    /// [`SubscriptionEvent::Resubscribing`]) are prioritised over buffered
112    /// reports, so a re-establishment is observed promptly even behind a backlog.
113    pub async fn next(&mut self) -> Option<SubscriptionEvent> {
114        tokio::select! {
115            biased;
116            // Prefer control events: they are rare, reliable, and ordering them
117            // ahead of buffered reports lets the consumer react to a
118            // (re-)establishment without first draining a report backlog.
119            ctrl = self.ctrl_rx.recv() => {
120                match ctrl {
121                    Some(ev) => Some(ev),
122                    // Control channel closed (actor gone): drain any reports that
123                    // are still buffered, then end.
124                    None => self.rx.recv().await,
125                }
126            }
127            report = self.rx.recv() => {
128                match report {
129                    Some(ev) => Some(ev),
130                    // Report channel closed: drain any control events still queued
131                    // (e.g. a final Resubscribing) before ending.
132                    None => self.ctrl_rx.recv().await,
133                }
134            }
135        }
136    }
137
138    /// Cancel the subscription explicitly and stop receiving reports.
139    ///
140    /// # Errors
141    ///
142    /// Returns [`Error::ControllerStopped`] if the owning task has already
143    /// stopped (the subscription is effectively cancelled either way).
144    pub async fn cancel(mut self) -> Result<(), Error> {
145        self.cancelled = true;
146        self.tx
147            .send(Command::CancelSubscription { key: self.key })
148            .await
149            .map_err(|_| Error::ControllerStopped)
150    }
151}
152
153impl Drop for Subscription {
154    fn drop(&mut self) {
155        if !self.cancelled {
156            // Best-effort cancel on drop; ignore a full/closed channel.
157            let _ = self
158                .tx
159                .try_send(Command::CancelSubscription { key: self.key });
160        }
161    }
162}