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}