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}