Skip to main content

sonos_state/
iter.rs

1//! Sync-first change iterator for property updates
2//!
3//! Provides a blocking iterator over property change events.
4//! Only emits events for properties that have been watched.
5//!
6//! Every [`ChangeIterator`] is an **independent subscriber**: each one receives
7//! every event emitted after it was created, rather than competing with its
8//! siblings for a shared queue.
9//!
10//! # Example
11//!
12//! ```rust,ignore
13//! use sonos_state::StateManager;
14//!
15//! let manager = StateManager::new()?;
16//! // ... add devices and watch properties ...
17//!
18//! // Blocking iteration — the new value rides along on the event
19//! for event in manager.iter() {
20//!     println!("{} changed on {}: {:?}", event.property_key(), event.speaker_id, event.change);
21//! }
22//!
23//! // Non-blocking check
24//! for event in manager.iter().try_iter() {
25//!     println!("{} changed", event.property_key());
26//! }
27//!
28//! // With timeout
29//! if let Some(event) = manager.iter().recv_timeout(Duration::from_secs(1)) {
30//!     println!("Got event: {:?}", event);
31//! }
32//! ```
33
34use std::sync::{mpsc, Arc, Weak};
35use std::time::Duration;
36
37use parking_lot::Mutex;
38
39use crate::state::ChangeEvent;
40
41// ============================================================================
42// EventFanout - one queue per subscriber
43// ============================================================================
44
45/// Broadcasts each [`ChangeEvent`] to every live subscriber.
46///
47/// # Why this exists
48///
49/// `iter()` previously handed every caller a clone of one `Arc<Mutex<Receiver>>`.
50/// Two `for event in system.iter()` loops therefore *split* the stream — each
51/// event went to whichever loop happened to win the mutex — even though `iter()`
52/// returning an independent iterator is the universal Rust idiom for "iterate
53/// the whole thing". Nothing errored and nothing was logged; a dashboard that
54/// added a second event loop simply started missing roughly half its updates.
55/// This type makes the API mean what it says.
56///
57/// # Why not `tokio::sync::broadcast`
58///
59/// This crate is deliberately sync-first: `recv()` blocks and no runtime is
60/// assumed. `broadcast::Receiver::blocking_recv()` *panics* when called from a
61/// thread already inside a Tokio runtime ("Cannot block the current thread from
62/// within a runtime") — the same runtime-within-runtime failure already tracked
63/// against `sonos-stream` in `docs/STATUS.md`. It also has no `recv_timeout`,
64/// which [`ChangeIterator::recv_timeout`] needs, and its fixed ring buffer drops
65/// events for slow consumers. A registry of plain `std::sync::mpsc` senders needs
66/// no runtime, keeps `recv_timeout`, and drops nothing.
67///
68/// # Delivery guarantees
69///
70/// - **Nothing is dropped.** Each subscriber owns an *unbounded* `mpsc` queue, so
71///   a slow consumer never loses an event and never blocks a fast one. There is
72///   no lag or overflow state for a consumer to detect, because there is no lag.
73///   The cost is the flip side of the same coin: a subscriber that never drains
74///   grows its own queue without bound (see `docs/specs/sonos-state.md` 14.1).
75/// - **Order is preserved per subscriber.** Every event is pushed to all
76///   subscribers under one lock in emit order, so each subscriber observes the
77///   same sequence the emitter produced. This is what keeps the observation-time
78///   write ordering of spec 4.1a meaningful on the consumer side.
79/// - **Subscribe before you emit.** A subscriber only receives events emitted
80///   after `subscribe()`. There is no replay buffer; the store holds current
81///   state for that job.
82pub(crate) struct EventFanout {
83    inner: Mutex<FanoutInner>,
84}
85
86struct FanoutInner {
87    /// Monotonic subscriber id source. Never reused, so a late `unsubscribe`
88    /// from an already-reaped subscriber cannot evict a newer one.
89    next_id: u64,
90    subscribers: Vec<(u64, mpsc::Sender<ChangeEvent>)>,
91}
92
93impl EventFanout {
94    pub(crate) fn new() -> Self {
95        Self {
96            inner: Mutex::new(FanoutInner {
97                next_id: 0,
98                subscribers: Vec::new(),
99            }),
100        }
101    }
102
103    /// Register a new subscriber, returning its id and its private queue.
104    pub(crate) fn subscribe(&self) -> (u64, mpsc::Receiver<ChangeEvent>) {
105        let (tx, rx) = mpsc::channel();
106        let mut inner = self.inner.lock();
107        let id = inner.next_id;
108        inner.next_id += 1;
109        inner.subscribers.push((id, tx));
110        tracing::trace!(
111            "EventFanout: subscriber {} registered ({} total)",
112            id,
113            inner.subscribers.len()
114        );
115        (id, rx)
116    }
117
118    /// Drop a subscriber by id. Called by `ChangeIterator::drop` so a departing
119    /// consumer is released immediately rather than lingering until the next
120    /// event happens to fail a send.
121    pub(crate) fn unsubscribe(&self, id: u64) {
122        let mut inner = self.inner.lock();
123        inner.subscribers.retain(|(sub_id, _)| *sub_id != id);
124        tracing::trace!(
125            "EventFanout: subscriber {} removed ({} remain)",
126            id,
127            inner.subscribers.len()
128        );
129    }
130
131    /// Deliver `event` to every live subscriber, returning how many got it.
132    ///
133    /// Subscribers whose receiver has gone away are reaped here. That is a
134    /// safety net independent of [`Self::unsubscribe`]: it covers a receiver
135    /// dropped without its `ChangeIterator` wrapper, and it guarantees the
136    /// registry cannot accumulate dead senders forever.
137    pub(crate) fn send(&self, event: ChangeEvent) -> usize {
138        let mut inner = self.inner.lock();
139        let mut delivered = 0usize;
140        inner
141            .subscribers
142            .retain(|(_, tx)| match tx.send(event.clone()) {
143                Ok(()) => {
144                    delivered += 1;
145                    true
146                }
147                Err(_) => false,
148            });
149        delivered
150    }
151
152    /// Number of live subscribers. Used by tests to assert that a dropped
153    /// consumer is actually released.
154    #[cfg(test)]
155    pub(crate) fn subscriber_count(&self) -> usize {
156        self.inner.lock().subscribers.len()
157    }
158}
159
160// ============================================================================
161// ChangeIterator
162// ============================================================================
163
164/// Blocking iterator over property change events
165///
166/// Receives change events for watched properties via `std::sync::mpsc`.
167/// All methods are synchronous - no async/await required.
168///
169/// Each `ChangeIterator` is an **independent subscriber**: two iterators taken
170/// from the same `StateManager` both receive every event, rather than competing
171/// for them. An iterator only sees events emitted *after* it was created, so
172/// take it before the writes you care about.
173pub struct ChangeIterator {
174    /// Kept so `Drop` can deregister this subscriber.
175    ///
176    /// Deliberately `Weak`: a strong reference would keep the fan-out — and
177    /// therefore this subscriber's own `Sender` — alive for exactly as long as
178    /// the iterator, so `recv()` could never observe a closed channel and would
179    /// block forever after the `StateManager` was dropped. `Weak` preserves the
180    /// pre-fan-out behaviour, where dropping the manager dropped the sender and
181    /// `recv()` returned `None`.
182    fanout: Weak<EventFanout>,
183    id: u64,
184    /// This subscriber's private queue.
185    ///
186    /// Behind a `Mutex` only to keep `ChangeIterator: Sync`, so `&self` still
187    /// crosses threads as it did before. Sharing one iterator between threads
188    /// makes them compete for *this* iterator's events — which is now an
189    /// explicit choice rather than the silent default.
190    rx: Mutex<mpsc::Receiver<ChangeEvent>>,
191}
192
193impl ChangeIterator {
194    /// Subscribe to a fan-out, creating an independent event queue.
195    pub(crate) fn new(fanout: &Arc<EventFanout>) -> Self {
196        let (id, rx) = fanout.subscribe();
197        Self {
198            fanout: Arc::downgrade(fanout),
199            id,
200            rx: Mutex::new(rx),
201        }
202    }
203
204    /// Block until the next event is available
205    ///
206    /// Returns `None` if the channel is closed.
207    pub fn recv(&self) -> Option<ChangeEvent> {
208        let event = self.rx.lock().recv().ok();
209        if let Some(ref e) = event {
210            tracing::trace!(
211                "ChangeIterator::recv yielded {} for {}",
212                e.property_key(),
213                e.speaker_id.as_str()
214            );
215        }
216        event
217    }
218
219    /// Block until the next event or timeout expires
220    ///
221    /// Returns `None` if the timeout expires or channel is closed.
222    pub fn recv_timeout(&self, timeout: Duration) -> Option<ChangeEvent> {
223        let event = self.rx.lock().recv_timeout(timeout).ok();
224        if let Some(ref e) = event {
225            tracing::trace!(
226                "ChangeIterator::recv_timeout yielded {} for {}",
227                e.property_key(),
228                e.speaker_id.as_str()
229            );
230        }
231        event
232    }
233
234    /// Try to receive an event without blocking
235    ///
236    /// Returns `None` if no event is currently available.
237    pub fn try_recv(&self) -> Option<ChangeEvent> {
238        let event = self.rx.lock().try_recv().ok();
239        if let Some(ref e) = event {
240            tracing::trace!(
241                "ChangeIterator::try_recv yielded {} for {}",
242                e.property_key(),
243                e.speaker_id.as_str()
244            );
245        }
246        event
247    }
248
249    /// Get a non-blocking iterator over currently available events
250    ///
251    /// Returns an iterator that yields all events currently in the queue
252    /// without blocking. Useful for batch processing.
253    ///
254    /// This is a *view* of this iterator's own queue, not a new subscriber, so
255    /// it consumes the same events `recv()` would. Call `iter()` again for an
256    /// independent stream.
257    pub fn try_iter(&self) -> TryIter<'_> {
258        TryIter { inner: self }
259    }
260
261    /// Get a blocking iterator with timeout
262    ///
263    /// Returns an iterator that blocks for up to `timeout` on each call
264    /// to `next()`. Stops when timeout expires without events.
265    ///
266    /// Like [`Self::try_iter`], a view of this iterator's queue rather than a
267    /// new subscriber.
268    pub fn timeout_iter(&self, timeout: Duration) -> TimeoutIter<'_> {
269        TimeoutIter {
270            inner: self,
271            timeout,
272        }
273    }
274}
275
276impl Drop for ChangeIterator {
277    fn drop(&mut self) {
278        // No upgrade means the fan-out is already gone, so there is no registry
279        // left to clean up.
280        if let Some(fanout) = self.fanout.upgrade() {
281            fanout.unsubscribe(self.id);
282        }
283    }
284}
285
286impl Iterator for ChangeIterator {
287    type Item = ChangeEvent;
288
289    /// Block until the next change event
290    ///
291    /// Returns `None` if the channel is closed.
292    fn next(&mut self) -> Option<Self::Item> {
293        self.recv()
294    }
295}
296
297/// Non-blocking iterator over currently available events
298pub struct TryIter<'a> {
299    inner: &'a ChangeIterator,
300}
301
302impl<'a> Iterator for TryIter<'a> {
303    type Item = ChangeEvent;
304
305    fn next(&mut self) -> Option<Self::Item> {
306        self.inner.try_recv()
307    }
308}
309
310/// Blocking iterator with timeout
311pub struct TimeoutIter<'a> {
312    inner: &'a ChangeIterator,
313    timeout: Duration,
314}
315
316impl<'a> Iterator for TimeoutIter<'a> {
317    type Item = ChangeEvent;
318
319    fn next(&mut self) -> Option<Self::Item> {
320        self.inner.recv_timeout(self.timeout)
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::decoder::PropertyChange;
328    use crate::model::SpeakerId;
329    use crate::property::Volume;
330    use crate::state::{ChangeSource, WriteStamp};
331    use std::thread;
332    use std::time::Instant;
333
334    fn create_test_event() -> ChangeEvent {
335        event_with_volume(42)
336    }
337
338    fn event_with_volume(v: u8) -> ChangeEvent {
339        ChangeEvent::new(
340            SpeakerId::new("test-speaker"),
341            PropertyChange::Volume(Volume::new(v)),
342            WriteStamp::now(ChangeSource::Event),
343        )
344    }
345
346    fn volume_of(event: &ChangeEvent) -> u8 {
347        match &event.change {
348            PropertyChange::Volume(v) => v.value(),
349            other => panic!("expected a Volume change, got {other:?}"),
350        }
351    }
352
353    /// A fan-out plus one iterator, the shape every test below starts from.
354    fn fanout_with_iter() -> (Arc<EventFanout>, ChangeIterator) {
355        let fanout = Arc::new(EventFanout::new());
356        let iter = ChangeIterator::new(&fanout);
357        (fanout, iter)
358    }
359
360    #[test]
361    fn test_try_recv_empty() {
362        let (_fanout, iter) = fanout_with_iter();
363
364        // Should return None when empty
365        assert!(iter.try_recv().is_none());
366    }
367
368    #[test]
369    fn test_try_recv_with_event() {
370        let (fanout, iter) = fanout_with_iter();
371
372        fanout.send(create_test_event());
373
374        // Should receive the event
375        let event = iter.try_recv().unwrap();
376        assert_eq!(event.property_key(), "volume");
377        assert_eq!(event.speaker_id.as_str(), "test-speaker");
378
379        // Should return None now
380        assert!(iter.try_recv().is_none());
381    }
382
383    #[test]
384    fn test_recv_timeout() {
385        let (_fanout, iter) = fanout_with_iter();
386
387        // Should timeout when empty
388        let start = Instant::now();
389        let result = iter.recv_timeout(Duration::from_millis(50));
390        assert!(result.is_none());
391        assert!(start.elapsed() >= Duration::from_millis(45));
392    }
393
394    #[test]
395    fn test_recv_timeout_with_event() {
396        let (fanout, iter) = fanout_with_iter();
397
398        // Send event after a short delay
399        let sender = Arc::clone(&fanout);
400        thread::spawn(move || {
401            thread::sleep(Duration::from_millis(10));
402            sender.send(create_test_event());
403        });
404
405        // Should receive within timeout
406        let result = iter.recv_timeout(Duration::from_millis(500));
407        assert!(result.is_some());
408    }
409
410    #[test]
411    fn test_try_iter() {
412        let (fanout, iter) = fanout_with_iter();
413
414        for _ in 0..3 {
415            fanout.send(create_test_event());
416        }
417
418        // Should get all events via try_iter
419        let events: Vec<_> = iter.try_iter().collect();
420        assert_eq!(events.len(), 3);
421
422        // Should be empty now
423        assert!(iter.try_recv().is_none());
424    }
425
426    #[test]
427    fn test_blocking_recv() {
428        let (fanout, iter) = fanout_with_iter();
429
430        let sender = Arc::clone(&fanout);
431        thread::spawn(move || {
432            thread::sleep(Duration::from_millis(10));
433            sender.send(create_test_event());
434        });
435
436        // Should block and receive
437        let event = iter.recv().unwrap();
438        assert_eq!(event.property_key(), "volume");
439    }
440
441    /// Dropping the fan-out must close every subscriber's queue, so a blocked
442    /// `recv()` wakes up and returns `None` instead of hanging forever.
443    ///
444    /// This is why `ChangeIterator::fanout` is `Weak`. With a strong `Arc` the
445    /// iterator keeps the fan-out — and therefore its own `Sender` — alive for as
446    /// long as it lives, so the channel can never close and `recv()` blocks
447    /// forever. That is a deadlock, not a wrong value, so the assertion runs on a
448    /// worker thread with a bounded join: the test *fails* rather than wedging
449    /// the whole suite.
450    #[test]
451    fn test_channel_closed() {
452        let (fanout, iter) = fanout_with_iter();
453
454        let (done_tx, done_rx) = mpsc::channel();
455        thread::spawn(move || {
456            let got = iter.recv();
457            let _ = done_tx.send(got);
458        });
459
460        // Dropping the fan-out closes every subscriber's queue.
461        drop(fanout);
462
463        match done_rx.recv_timeout(Duration::from_secs(5)) {
464            Ok(None) => {} // correct: closed channel reported as end of stream
465            Ok(Some(e)) => panic!("expected no event from a closed fan-out, got {e:?}"),
466            Err(err) => panic!(
467                "recv() did not return after the fan-out was dropped ({err:?}) — the \
468                 iterator is holding its own sender alive (should be `Weak`)"
469            ),
470        }
471    }
472
473    /// A slow subscriber loses nothing while a fast one races ahead, and neither
474    /// blocks the other. This is the failure mode of the design we rejected: a
475    /// bounded broadcast ring would have overwritten the slow subscriber's
476    /// backlog and it would have had no way to know.
477    #[test]
478    fn test_slow_subscriber_loses_no_events() {
479        let fanout = Arc::new(EventFanout::new());
480        let fast = ChangeIterator::new(&fanout);
481        let slow = ChangeIterator::new(&fanout);
482
483        // Far more events than any plausible ring buffer would hold. Capped at
484        // 100 because `Volume::new` clamps there, and the assertion below needs
485        // each event to carry a distinguishable value.
486        for v in 0..100u8 {
487            assert_eq!(
488                fanout.send(event_with_volume(v)),
489                2,
490                "both subscribers must be delivered to"
491            );
492        }
493
494        // The fast subscriber drains everything...
495        let fast_seen: Vec<u8> = fast.try_iter().map(|e| volume_of(&e)).collect();
496        assert_eq!(fast_seen, (0..100u8).collect::<Vec<_>>());
497
498        // ...and the slow one, which never ran until now, still has all 100 in
499        // order. Nothing was dropped and nothing overwrote its backlog.
500        let slow_seen: Vec<u8> = slow.try_iter().map(|e| volume_of(&e)).collect();
501        assert_eq!(slow_seen, (0..100u8).collect::<Vec<_>>());
502    }
503
504    /// Dropping an iterator releases its slot immediately, without waiting for a
505    /// send to notice. Guards `ChangeIterator::drop`.
506    #[test]
507    fn test_dropped_iterator_deregisters_immediately() {
508        let fanout = Arc::new(EventFanout::new());
509        let keep = ChangeIterator::new(&fanout);
510        let discard = ChangeIterator::new(&fanout);
511        assert_eq!(fanout.subscriber_count(), 2);
512
513        drop(discard);
514
515        // No event has been sent in between: the slot is gone because `Drop`
516        // removed it, not because a failed send reaped it.
517        assert_eq!(
518            fanout.subscriber_count(),
519            1,
520            "a dropped ChangeIterator must deregister itself on drop"
521        );
522
523        // The survivor keeps working and is not stalled by the departure.
524        assert_eq!(fanout.send(event_with_volume(9)), 1);
525        assert_eq!(volume_of(&keep.recv().unwrap()), 9);
526    }
527
528    /// A subscriber whose receiver vanished without a `ChangeIterator` wrapper is
529    /// reaped on the next send, so the registry cannot grow dead senders forever.
530    /// Guards the `retain` in `EventFanout::send`, independently of `Drop`.
531    #[test]
532    fn test_send_reaps_dead_subscriber() {
533        let fanout = Arc::new(EventFanout::new());
534        let keep = ChangeIterator::new(&fanout);
535
536        // A raw subscription, so no `Drop` impl is involved in its removal.
537        let (_id, raw_rx) = fanout.subscribe();
538        assert_eq!(fanout.subscriber_count(), 2);
539        drop(raw_rx);
540
541        // Still registered — nothing has tried to send to it yet.
542        assert_eq!(fanout.subscriber_count(), 2);
543
544        // The send delivers to the live subscriber only, and reaps the dead one.
545        assert_eq!(fanout.send(event_with_volume(5)), 1);
546        assert_eq!(
547            fanout.subscriber_count(),
548            1,
549            "a send to a dead subscriber must remove it"
550        );
551        assert_eq!(volume_of(&keep.recv().unwrap()), 5);
552    }
553}