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