Skip to main content

proxima_primitives/sync/
watch.rs

1//! `proxima::sync::watch` — single-producer, multi-consumer
2//! latest-value channel. Shape-compatible with `tokio::sync::watch`:
3//! each receiver tracks whether it has observed the current value;
4//! slow receivers don't block fast ones; back-to-back sends collapse
5//! to the last value if the consumer hasn't called `changed()` in
6//! between.
7//!
8//! Hand-rolled over `event_listener::Event` + `std::sync::RwLock<T>` +
9//! per-receiver version counter. Backing this with `async-broadcast`
10//! cap=1 didn't fit — broadcast delivers every value to every
11//! receiver, watch collapses to the latest, and the semantics of
12//! "this receiver has seen this version" are watch-specific.
13//!
14//! # Non-coverage
15//!
16//! - `Receiver::wait_for(closure) -> Result<Ref<T>, RecvError>` —
17//!   predicate-await helper that loops `changed().await` + read until
18//!   `closure(&value)` returns true. Compose at the call site if
19//!   needed; not exposed here because no internal caller uses it.
20//! - `Receiver::mark_changed()` / `Receiver::mark_unchanged()` —
21//!   manual version-cursor manipulation. The current callers only
22//!   need `borrow` / `borrow_and_update` / `changed`.
23//! - `Sender::send_replace(value) -> T` — set new value AND return
24//!   the prior value atomically. Compose via `borrow()` clone +
25//!   `send` at the call site.
26//! - `Sender::send_modify(closure)` — read-modify-write under the
27//!   write lock. Not exposed because the value type is typically a
28//!   small Copy state machine; if a caller needs in-place mutation
29//!   they can use a `Mutex<T>` next to the watch and only send a
30//!   version-cursor through the watch.
31//! - `Sender::send_if_modified(closure) -> bool` — same with a
32//!   skip-if-unchanged shortcut.
33//! - `Receiver::same_channel(&other)` — identity check across clones.
34//!
35//! # Loom coverage
36//!
37//! `--cfg loom` loom-tests this exact `Sender`/`Receiver` pair (not a
38//! hand-written model): the private `sync::loom_atomic` shim swaps `version` /
39//! `senders` / `receivers` and the `Arc`/`RwLock` for loom's
40//! instrumented equivalents, and `event_listener::Event` becomes
41//! loom-instrumented too (its `loom` Cargo feature is enabled under
42//! `[target.'cfg(loom)'.dependencies]`). See the `loom_tests` module
43//! below.
44
45use event_listener::Event;
46
47use crate::sync::loom_atomic::{Arc, AtomicU64, AtomicUsize, Ordering, RwLock, RwLockReadGuard};
48
49/// Returned by [`Sender::send`] when all receivers have been dropped.
50#[derive(Debug, thiserror::Error)]
51#[error("watch channel has no receivers")]
52pub struct SendError<T>(pub T);
53
54/// Returned by [`Receiver::changed`] / [`Receiver::has_changed`] when
55/// the sender has been dropped.
56#[derive(Debug, thiserror::Error)]
57#[error("watch channel sender dropped")]
58pub struct RecvError;
59
60struct Inner<T> {
61    value: RwLock<T>,
62    /// Bumped on every successful `send`. Receivers compare against
63    /// their last-seen version to detect a new value.
64    version: AtomicU64,
65    /// Wakes all receivers parked in `changed()`.
66    event: Event,
67    /// Live sender count. Goes to 0 when the last `Sender` drops,
68    /// which signals receivers to see `RecvError` from `changed()`.
69    senders: AtomicUsize,
70    /// Live receiver count. Used by `Sender::is_closed`.
71    receivers: AtomicUsize,
72}
73
74/// Producer side. Only one sender is supported per channel today; the
75/// API matches tokio's single-producer semantics.
76pub struct Sender<T> {
77    inner: Arc<Inner<T>>,
78}
79
80impl<T> Sender<T> {
81    /// Replace the current value, bumping the version and waking
82    /// every receiver parked in `changed()`. Returns the value back
83    /// via [`SendError`] if every receiver has been dropped.
84    pub fn send(&self, value: T) -> Result<(), SendError<T>> {
85        if self.inner.receivers.load(Ordering::Acquire) == 0 {
86            return Err(SendError(value));
87        }
88        {
89            let mut guard = self
90                .inner
91                .value
92                .write()
93                .unwrap_or_else(|err| err.into_inner());
94            *guard = value;
95        }
96        self.inner.version.fetch_add(1, Ordering::AcqRel);
97        self.inner.event.notify(usize::MAX);
98        Ok(())
99    }
100
101    /// Returns true once every receiver has been dropped.
102    pub fn is_closed(&self) -> bool {
103        self.inner.receivers.load(Ordering::Acquire) == 0
104    }
105
106    /// Build a fresh receiver subscribed at the current version. The
107    /// new receiver's first `changed()` call resolves only after a
108    /// *future* `send`.
109    pub fn subscribe(&self) -> Receiver<T> {
110        self.inner.receivers.fetch_add(1, Ordering::AcqRel);
111        Receiver {
112            inner: self.inner.clone(),
113            last_seen: self.inner.version.load(Ordering::Acquire),
114        }
115    }
116
117    /// Read-only borrow of the current value. Does NOT update any
118    /// per-receiver seen-state because the sender has no notion of
119    /// per-receiver state.
120    pub fn borrow(&self) -> Ref<'_, T> {
121        Ref {
122            guard: self
123                .inner
124                .value
125                .read()
126                .unwrap_or_else(|err| err.into_inner()),
127        }
128    }
129}
130
131impl<T> Drop for Sender<T> {
132    fn drop(&mut self) {
133        if self.inner.senders.fetch_sub(1, Ordering::AcqRel) == 1 {
134            // last sender — wake every receiver so changed() can
135            // observe RecvError.
136            self.inner.event.notify(usize::MAX);
137        }
138    }
139}
140
141/// Consumer side. Each receiver tracks its own seen-version, so two
142/// receivers can be at different positions in the value stream and
143/// neither blocks the other.
144pub struct Receiver<T> {
145    inner: Arc<Inner<T>>,
146    last_seen: u64,
147}
148
149impl<T> Receiver<T> {
150    /// Park until the current value differs from this receiver's
151    /// last-seen version. Returns `Err(RecvError)` if the sender has
152    /// been dropped and no further value can ever arrive. Does NOT
153    /// update the seen-version — call `borrow_and_update` after to
154    /// acknowledge.
155    pub async fn changed(&mut self) -> Result<(), RecvError> {
156        loop {
157            let current = self.inner.version.load(Ordering::Acquire);
158            if current != self.last_seen {
159                return Ok(());
160            }
161            if self.inner.senders.load(Ordering::Acquire) == 0 {
162                return Err(RecvError);
163            }
164            let listener = self.inner.event.listen();
165            // re-check after registering to close the lost-wakeup window.
166            let current = self.inner.version.load(Ordering::Acquire);
167            if current != self.last_seen {
168                return Ok(());
169            }
170            if self.inner.senders.load(Ordering::Acquire) == 0 {
171                return Err(RecvError);
172            }
173            listener.await;
174        }
175    }
176
177    /// Non-blocking check: has a new value arrived since the last
178    /// `borrow_and_update`?
179    pub fn has_changed(&self) -> Result<bool, RecvError> {
180        let current = self.inner.version.load(Ordering::Acquire);
181        if current != self.last_seen {
182            return Ok(true);
183        }
184        if self.inner.senders.load(Ordering::Acquire) == 0 {
185            return Err(RecvError);
186        }
187        Ok(false)
188    }
189
190    /// Read-only borrow of the current value without updating
191    /// seen-state. A follow-up `changed()` call will still return
192    /// `Ok(())` if the version changed before this `borrow`.
193    pub fn borrow(&self) -> Ref<'_, T> {
194        Ref {
195            guard: self
196                .inner
197                .value
198                .read()
199                .unwrap_or_else(|err| err.into_inner()),
200        }
201    }
202
203    /// Read the current value and mark it as seen so the next
204    /// `changed()` call only resolves on a *subsequent* send.
205    pub fn borrow_and_update(&mut self) -> Ref<'_, T> {
206        self.last_seen = self.inner.version.load(Ordering::Acquire);
207        self.borrow()
208    }
209}
210
211impl<T> Clone for Receiver<T> {
212    fn clone(&self) -> Self {
213        self.inner.receivers.fetch_add(1, Ordering::AcqRel);
214        Self {
215            inner: self.inner.clone(),
216            last_seen: self.last_seen,
217        }
218    }
219}
220
221impl<T> Drop for Receiver<T> {
222    fn drop(&mut self) {
223        self.inner.receivers.fetch_sub(1, Ordering::AcqRel);
224    }
225}
226
227/// Read guard returned by `borrow` / `borrow_and_update`. Holds the
228/// shared lock for the lifetime of the borrow.
229pub struct Ref<'lifetime, T> {
230    guard: RwLockReadGuard<'lifetime, T>,
231}
232
233impl<T> std::ops::Deref for Ref<'_, T> {
234    type Target = T;
235    fn deref(&self) -> &T {
236        &self.guard
237    }
238}
239
240/// Build a watch channel anchored at `initial`.
241pub fn channel<T>(initial: T) -> (Sender<T>, Receiver<T>) {
242    let inner = Arc::new(Inner {
243        value: RwLock::new(initial),
244        version: AtomicU64::new(0),
245        event: Event::new(),
246        senders: AtomicUsize::new(1),
247        receivers: AtomicUsize::new(1),
248    });
249    (
250        Sender {
251            inner: inner.clone(),
252        },
253        Receiver {
254            inner,
255            last_seen: 0,
256        },
257    )
258}
259
260// under `--cfg loom`, `event_listener::Event` and loom's atomics require
261// every call to happen inside a `loom::model()`; these plain, non-model
262// tests exist for the normal (non-loom) build only — `loom_tests` below
263// is the loom-model equivalent coverage.
264#[cfg(all(test, not(loom)))]
265#[allow(clippy::unwrap_used, clippy::expect_used)]
266mod tests {
267    use super::*;
268    use futures::executor::block_on;
269
270    #[test]
271    fn initial_value_is_visible_via_borrow() {
272        let (_sender, receiver) = channel(7_u32);
273        assert_eq!(*receiver.borrow(), 7);
274    }
275
276    #[test]
277    fn send_makes_changed_resolve_and_updates_value() {
278        let (sender, mut receiver) = channel(0_u32);
279        sender.send(42).expect("send");
280        block_on(async {
281            receiver.changed().await.expect("changed");
282            assert_eq!(*receiver.borrow_and_update(), 42);
283        });
284    }
285
286    #[test]
287    fn back_to_back_sends_collapse_to_latest() {
288        let (sender, mut receiver) = channel(0_u32);
289        sender.send(1).expect("send 1");
290        sender.send(2).expect("send 2");
291        sender.send(3).expect("send 3");
292        block_on(async {
293            receiver.changed().await.expect("changed");
294            assert_eq!(*receiver.borrow_and_update(), 3);
295            // no further changes pending
296            assert!(!receiver.has_changed().expect("has_changed"));
297        });
298    }
299
300    #[test]
301    fn dropping_sender_makes_changed_error() {
302        let (sender, mut receiver) = channel(0_u32);
303        drop(sender);
304        block_on(async {
305            let result = receiver.changed().await;
306            assert!(result.is_err());
307        });
308    }
309
310    #[test]
311    fn dropping_all_receivers_makes_send_error() {
312        let (sender, receiver) = channel(0_u32);
313        drop(receiver);
314        match sender.send(1) {
315            Err(SendError(value)) => assert_eq!(value, 1),
316            Ok(()) => panic!("send to receiver-less channel should error"),
317        }
318    }
319
320    #[test]
321    fn subscribe_starts_at_current_version_and_resolves_on_next_send() {
322        let (sender, _receiver) = channel(0_u32);
323        sender.send(1).expect("send 1");
324        let mut newcomer = sender.subscribe();
325        // newcomer has not observed any "change" yet (joined at v1)
326        assert!(!newcomer.has_changed().expect("has_changed"));
327        sender.send(2).expect("send 2");
328        block_on(async {
329            newcomer.changed().await.expect("changed");
330            assert_eq!(*newcomer.borrow_and_update(), 2);
331        });
332    }
333}
334
335/// Loom interleaving coverage for the REAL `channel`/`Sender`/`Receiver`
336/// above (not a hand-written model). Ported 1:1 from the former
337/// `proxima-loom/tests/loom_sync.rs`, rewritten to drive `changed()`
338/// (the real future) instead of poking the version counter directly —
339/// that's the actual send/version-bump/listener-wake race this module
340/// promises. `preemption_bound` is set inline so the search is bounded
341/// without relying on an operator remembering `LOOM_MAX_PREEMPTIONS`.
342///
343/// Run with:
344///   RUSTFLAGS="--cfg loom" cargo test -p proxima-primitives --release watch::loom_tests
345#[cfg(all(test, loom))]
346#[allow(clippy::unwrap_used, clippy::expect_used)]
347mod loom_tests {
348    use super::channel;
349    use loom::sync::Arc;
350
351    fn check(model: impl Fn() + Sync + Send + 'static) {
352        let mut builder = loom::model::Builder::new();
353        builder.preemption_bound = Some(3);
354        builder.check(model);
355    }
356
357    /// Producer sends before consumer checks. `changed()` must resolve
358    /// without parking and observe the new value.
359    #[test]
360    fn send_then_changed_observes_new_value() {
361        check(|| {
362            let (sender, mut receiver) = channel(0_u32);
363            sender.send(42).expect("send");
364            loom::future::block_on(async {
365                receiver.changed().await.expect("changed");
366            });
367            assert_eq!(*receiver.borrow_and_update(), 42);
368        });
369    }
370
371    /// Consumer registers first (parks in `changed()`), then the
372    /// producer thread sends. The listener must be woken and
373    /// `changed()` must resolve with the new value.
374    #[test]
375    fn changed_then_send_resolves() {
376        check(|| {
377            let (sender, mut receiver) = channel(0_u32);
378            let sender = Arc::new(sender);
379            let sender_producer = sender.clone();
380
381            let producer = loom::thread::spawn(move || {
382                sender_producer.send(1).expect("send");
383            });
384
385            loom::future::block_on(async {
386                receiver.changed().await.expect("changed");
387            });
388            producer.join().expect("producer thread panicked");
389
390            assert_eq!(*receiver.borrow_and_update(), 1);
391        });
392    }
393
394    /// Producer and consumer race on all interleavings up to the
395    /// preemption bound. Under every schedule, the consumer's
396    /// `changed()` must resolve and observe the sent value.
397    #[test]
398    fn concurrent_send_and_changed() {
399        check(|| {
400            let (sender, mut receiver) = channel(0_u32);
401            let sender = Arc::new(sender);
402            let sender_producer = sender.clone();
403
404            let producer = loom::thread::spawn(move || {
405                sender_producer.send(99).expect("send");
406            });
407
408            let consumer = loom::thread::spawn(move || {
409                loom::future::block_on(async {
410                    receiver.changed().await.expect("changed");
411                });
412                *receiver.borrow_and_update()
413            });
414
415            producer.join().expect("producer thread panicked");
416            let observed = consumer.join().expect("consumer thread panicked");
417            assert_eq!(observed, 99);
418        });
419    }
420}