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}