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}