Skip to main content

photon_ring/
event.rs

1// Copyright 2026 Photon Ring Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! A ring for values that are not `Pod`.
5//!
6//! The [`Pod`](crate::Pod) bound on [`channel`](crate::channel()) exists so that a
7//! reader racing a writer observes a harmless torn value rather than undefined
8//! behaviour. That race is only possible when the publisher is allowed to
9//! overwrite a slot a subscriber has not read — which on a bounded ring, with
10//! every subscriber registered for backpressure, cannot happen.
11//!
12//! So this ring drops the bound. Slots own their values, created once by a
13//! factory and **mutated in place** rather than overwritten, so nothing is ever
14//! copied into or out of the ring and `String`, `Vec`, enums and `Option` are
15//! all ordinary payloads. Because no reader can ever observe a slot mid-write,
16//! there is no seqlock either: the cursor's `Release`/`Acquire` pair is the
17//! entire publication edge.
18//!
19//! Because nothing is copied, the cost is proportional to what you actually
20//! touch rather than to `size_of::<T>()`. Updating a few fields of a large,
21//! mostly-stable message is dramatically cheaper than a ring that must copy the
22//! whole value in and out; rewriting the entire payload every message is not.
23//!
24//! The trade is that **every** subscriber gates the publisher. There are no
25//! lossy observers here — a reader that could be lapped is exactly the reader
26//! this design excludes. Use [`channel`](crate::channel()) when you want those.
27//!
28//! ```
29//! use photon_ring::event_channel;
30//!
31//! # #[derive(Default)]
32//! struct Order { symbol: String, qty: u32 }
33//!
34//! let (mut tx, rx) = event_channel(64, || Order { symbol: String::new(), qty: 0 });
35//! let mut sub = rx.subscribe();
36//!
37//! tx.publish(|o| {
38//!     o.symbol.clear();
39//!     o.symbol.push_str("ETHUSD");   // reuses the existing allocation
40//!     o.qty = 5;
41//! });
42//!
43//! let qty = sub.process(|o| o.qty).unwrap();
44//! assert_eq!(qty, 5);
45//! ```
46
47extern crate alloc;
48
49use alloc::boxed::Box;
50use alloc::sync::{Arc, Weak};
51use alloc::vec::Vec;
52use core::cell::UnsafeCell;
53use core::sync::atomic::{AtomicU64, Ordering};
54use spin::Mutex;
55
56use crate::ring::{Padded, RingIndex};
57
58/// Shared state: slots that own their values, plus the publication cursor and
59/// the subscriber trackers the publisher is gated by.
60struct EventRing<T> {
61    slots: Box<[UnsafeCell<T>]>,
62    index: RingIndex,
63    /// Sequence of the last published message; `u64::MAX` before the first.
64    cursor: Padded<AtomicU64>,
65    trackers: Mutex<Vec<Weak<Padded<AtomicU64>>>>,
66}
67
68// SAFETY: a slot is only ever accessed by the publisher while no subscriber can
69// reach it (guaranteed by the backpressure invariant: the publisher will not
70// advance past `slowest + capacity`), and only ever by subscribers once the
71// cursor has been released past it. `T: Send` is required to move values across
72// the threads that touch them, and `T: Sync` because several subscribers can
73// hold `&T` into the same slot at once — without it a payload with interior
74// mutability would let safe code race through those shared references.
75unsafe impl<T: Send + Sync> Send for EventRing<T> {}
76unsafe impl<T: Send + Sync> Sync for EventRing<T> {}
77
78impl<T> EventRing<T> {
79    /// The lowest sequence any live subscriber still needs, or `None` if there
80    /// are none. Prunes dropped subscribers, which is what lets a dead consumer
81    /// release the publisher instead of wedging it.
82    fn slowest(&self) -> Option<u64> {
83        let mut trackers = self.trackers.lock();
84        let mut min = u64::MAX;
85        let mut any = false;
86        trackers.retain(|weak| match weak.upgrade() {
87            Some(t) => {
88                min = min.min(t.0.load(Ordering::Acquire));
89                any = true;
90                true
91            }
92            None => false,
93        });
94        any.then_some(min)
95    }
96}
97
98/// The write side. Single producer: `&mut self` enforces it without atomics.
99pub struct EventPublisher<T> {
100    ring: Arc<EventRing<T>>,
101    seq: u64,
102    cached_slowest: u64,
103}
104
105// SAFETY: see EventRing.
106unsafe impl<T: Send + Sync> Send for EventPublisher<T> {}
107
108impl<T> EventPublisher<T> {
109    /// Whether sequence `self.seq` can be written without overtaking a
110    /// subscriber. Mirrors the bounded channel's cached fast path.
111    fn has_room(&mut self) -> bool {
112        let capacity = self.ring.index.capacity;
113        if self.seq >= self.cached_slowest + capacity {
114            match self.ring.slowest() {
115                Some(slowest) => {
116                    self.cached_slowest = slowest;
117                    if self.seq >= slowest + capacity {
118                        return false;
119                    }
120                }
121                // No subscribers: nothing to protect.
122                None => return true,
123            }
124        }
125        true
126    }
127
128    /// Fill the next slot, blocking until a subscriber frees one.
129    ///
130    /// The closure receives the slot's existing value to mutate. Nothing is
131    /// allocated or copied, so a payload that owns a `String` or `Vec` reuses
132    /// the capacity it had from the previous time round the ring.
133    ///
134    /// If the closure panics the value is left as the closure altered it —
135    /// still a valid `T` — and the message is not published.
136    pub fn publish(&mut self, f: impl FnOnce(&mut T)) {
137        while !self.has_room() {
138            core::hint::spin_loop();
139        }
140        self.write(f);
141    }
142
143    /// Fill the next slot, or return `false` if that would overtake a subscriber.
144    pub fn try_publish(&mut self, f: impl FnOnce(&mut T)) -> bool {
145        if !self.has_room() {
146            return false;
147        }
148        self.write(f);
149        true
150    }
151
152    fn write(&mut self, f: impl FnOnce(&mut T)) {
153        let idx = self.ring.index.slot(self.seq);
154        // SAFETY: `has_room` established that no subscriber can still be reading
155        // this slot, and `&mut self` means no other publisher exists.
156        f(unsafe { &mut *self.ring.slots[idx].get() });
157        // Release: the mutation above is visible to any subscriber that acquires
158        // this cursor value. This is the entire publication edge.
159        self.ring.cursor.0.store(self.seq, Ordering::Release);
160        self.seq += 1;
161    }
162
163    /// Messages published so far.
164    pub fn published(&self) -> u64 {
165        self.seq
166    }
167
168    /// Ring capacity.
169    pub fn capacity(&self) -> u64 {
170        self.ring.index.capacity
171    }
172}
173
174/// Clone-able handle for creating subscribers.
175pub struct EventSubscribable<T> {
176    ring: Arc<EventRing<T>>,
177}
178
179// SAFETY: see EventRing.
180unsafe impl<T: Send + Sync> Send for EventSubscribable<T> {}
181unsafe impl<T: Send + Sync> Sync for EventSubscribable<T> {}
182
183impl<T> Clone for EventSubscribable<T> {
184    fn clone(&self) -> Self {
185        EventSubscribable {
186            ring: self.ring.clone(),
187        }
188    }
189}
190
191impl<T> EventSubscribable<T> {
192    /// Create a subscriber, starting from the next message published.
193    ///
194    /// Every subscriber gates the publisher, so one that stops reading will
195    /// stop the ring. Dropping it releases the publisher again.
196    pub fn subscribe(&self) -> EventSubscriber<T> {
197        // The start position is chosen under the tracker lock so that it is
198        // atomic with respect to the publisher's scan; see
199        // `SharedRing::register_tracker_at_head` for why that matters.
200        let mut trackers = self.ring.trackers.lock();
201        let head = self.ring.cursor.0.load(Ordering::Acquire);
202        let start = if head == u64::MAX { 0 } else { head + 1 };
203        let tracker = Arc::new(Padded(AtomicU64::new(start)));
204        trackers.push(Arc::downgrade(&tracker));
205        drop(trackers);
206        EventSubscriber {
207            ring: self.ring.clone(),
208            cursor: start,
209            tracker,
210        }
211    }
212}
213
214/// The read side. Reads borrow the value in place; nothing is copied out.
215pub struct EventSubscriber<T> {
216    ring: Arc<EventRing<T>>,
217    cursor: u64,
218    tracker: Arc<Padded<AtomicU64>>,
219}
220
221// SAFETY: see EventRing.
222unsafe impl<T: Send + Sync> Send for EventSubscriber<T> {}
223
224impl<T> EventSubscriber<T> {
225    /// Run `f` on the next message, if one is available.
226    ///
227    /// The value is borrowed from the ring, not copied out of it. The slot is
228    /// released for reuse when `f` returns; if `f` panics the message is not
229    /// consumed and will be seen again.
230    pub fn process<R>(&mut self, f: impl FnOnce(&T) -> R) -> Option<R> {
231        // Acquire pairs with the publisher's Release store, making its mutation
232        // of this slot visible.
233        let head = self.ring.cursor.0.load(Ordering::Acquire);
234        if head == u64::MAX || self.cursor > head {
235            return None;
236        }
237        let idx = self.ring.index.slot(self.cursor);
238        // SAFETY: the publisher cannot reach this slot while our tracker sits at
239        // `self.cursor`, and the cursor load above established the value is
240        // fully written.
241        let out = f(unsafe { &*self.ring.slots[idx].get() });
242        self.cursor += 1;
243        // Release: everything we read above happens-before the publisher's
244        // Acquire load of this tracker, so it cannot overwrite the slot early.
245        self.tracker.0.store(self.cursor, Ordering::Release);
246        Some(out)
247    }
248
249    /// Sequence this subscriber will read next.
250    pub fn cursor(&self) -> u64 {
251        self.cursor
252    }
253
254    /// Messages published but not yet processed by this subscriber.
255    pub fn pending(&self) -> u64 {
256        let head = self.ring.cursor.0.load(Ordering::Acquire);
257        if head == u64::MAX || self.cursor > head {
258            0
259        } else {
260            head - self.cursor + 1
261        }
262    }
263}
264
265/// Create a ring of `capacity` values built by `factory`.
266///
267/// `T` must be `Sync` as well as `Send`: several subscribers can hold a
268/// reference into the same slot at once, so a payload with interior mutability
269/// would let them race.
270///
271/// Values are created once, up front, and reused for the life of the ring, so
272/// steady-state publishing allocates nothing even for payloads that own heap
273/// data.
274///
275/// # Panics
276///
277/// Panics if `capacity < 2`.
278pub fn event_channel<T: Send + Sync + 'static>(
279    capacity: usize,
280    mut factory: impl FnMut() -> T,
281) -> (EventPublisher<T>, EventSubscribable<T>) {
282    let index = RingIndex::new(capacity);
283    let slots: Vec<UnsafeCell<T>> = (0..capacity).map(|_| UnsafeCell::new(factory())).collect();
284    let ring = Arc::new(EventRing {
285        slots: slots.into_boxed_slice(),
286        index,
287        cursor: Padded(AtomicU64::new(u64::MAX)),
288        trackers: Mutex::new(Vec::new()),
289    });
290    (
291        EventPublisher {
292            ring: ring.clone(),
293            seq: 0,
294            cached_slowest: 0,
295        },
296        EventSubscribable { ring },
297    )
298}