Skip to main content

photon_ring/channel/
subscriber.rs

1// Copyright 2026 Photon Ring Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use super::errors::TryRecvError;
5use crate::barrier::DependencyBarrier;
6use crate::pod::Pod;
7use crate::ring::{Padded, RingIndex, SharedRing};
8use crate::slot::Slot;
9use crate::wait::WaitStrategy;
10use alloc::sync::Arc;
11use core::sync::atomic::{AtomicU64, Ordering};
12
13/// The read side of a Photon SPMC channel.
14///
15/// Each subscriber has its own cursor — no contention between consumers.
16pub struct Subscriber<T: Pod> {
17    pub(super) ring: Arc<SharedRing<T>>,
18    /// Cached raw pointer to the slot array. Avoids Arc + Box deref on the
19    /// hot path. Valid for the lifetime of `ring` (the Arc keeps it alive).
20    pub(super) slots_ptr: *const Slot<T>,
21    /// Precomputed slot indexing (capacity, mask, reciprocal, pow2 flag).
22    pub(super) index: RingIndex,
23    pub(super) cursor: u64,
24    /// Per-subscriber cursor tracker for backpressure. `None` on regular
25    /// (lossy) channels — zero overhead.
26    pub(super) tracker: Option<Arc<Padded<AtomicU64>>>,
27    /// Cumulative messages skipped due to lag.
28    pub(super) total_lagged: u64,
29    /// Cumulative messages successfully received.
30    pub(super) total_received: u64,
31}
32
33unsafe impl<T: Pod> Send for Subscriber<T> {}
34
35impl<T: Pod> Subscriber<T> {
36    /// Try to receive the next message without blocking.
37    #[inline]
38    pub fn try_recv(&mut self) -> Result<T, TryRecvError> {
39        self.read_slot()
40    }
41
42    /// Spin until the next message is available and return it.
43    ///
44    /// Uses a two-phase spin strategy: bare spin for the first 64 iterations
45    /// (minimum wakeup latency, ~0 ns reaction time), then `PAUSE`-based spin
46    /// (saves power, yields to SMT sibling). On Skylake+, `PAUSE` adds ~140
47    /// cycles of delay per iteration — the bare-spin phase avoids this penalty
48    /// when the message arrives quickly (typical for cross-thread pub/sub).
49    #[inline]
50    pub fn recv(&mut self) -> T {
51        // SAFETY: slots_ptr is valid for the lifetime of self.ring (Arc-owned).
52        let slot = unsafe { &*self.slots_ptr.add(self.index.slot(self.cursor)) };
53        let expected = self.cursor * 2 + 2;
54        // Phase 1: bare spin — no PAUSE, minimum wakeup latency
55        for _ in 0..64 {
56            match slot.try_read(self.cursor) {
57                Ok(Some(value)) => {
58                    self.cursor += 1;
59                    self.update_tracker();
60                    self.total_received += 1;
61                    return value;
62                }
63                Ok(None) => {}
64                Err(stamp) => {
65                    if stamp >= expected {
66                        return self.recv_slow();
67                    }
68                }
69            }
70        }
71        // Phase 2: power-efficient spin.
72        // On aarch64: SEVL + WFE loop — the core sleeps until a cache-line
73        // invalidation event (the publisher's stamp store), waking in ~12 ns.
74        // On x86: PAUSE yields the pipeline to the SMT sibling (~140 cycles).
75        #[cfg(target_arch = "aarch64")]
76        unsafe {
77            core::arch::asm!("sevl", options(nomem, nostack));
78        }
79        loop {
80            #[cfg(target_arch = "aarch64")]
81            unsafe {
82                core::arch::asm!("wfe", options(nomem, nostack));
83            }
84            match slot.try_read(self.cursor) {
85                Ok(Some(value)) => {
86                    self.cursor += 1;
87                    self.update_tracker();
88                    self.total_received += 1;
89                    return value;
90                }
91                Ok(None) => {
92                    #[cfg(not(target_arch = "aarch64"))]
93                    core::hint::spin_loop();
94                }
95                Err(stamp) => {
96                    if stamp < expected {
97                        #[cfg(not(target_arch = "aarch64"))]
98                        core::hint::spin_loop();
99                    } else {
100                        return self.recv_slow();
101                    }
102                }
103            }
104        }
105    }
106
107    /// Slow path for lag recovery in recv().
108    #[cold]
109    #[inline(never)]
110    fn recv_slow(&mut self) -> T {
111        loop {
112            match self.try_recv() {
113                Ok(val) => return val,
114                Err(TryRecvError::Empty) => core::hint::spin_loop(),
115                Err(TryRecvError::Lagged { .. }) => {}
116            }
117        }
118    }
119
120    /// Block until the next message using the given [`WaitStrategy`].
121    ///
122    /// Unlike [`recv()`](Self::recv), which hard-codes a two-phase spin,
123    /// this method delegates idle behaviour to the strategy — enabling
124    /// yield-based, park-based, or adaptive waiting.
125    ///
126    /// # Example
127    /// ```
128    /// use photon_ring::{channel, WaitStrategy};
129    ///
130    /// let (mut p, s) = channel::<u64>(64);
131    /// let mut sub = s.subscribe();
132    /// p.publish(7);
133    /// assert_eq!(sub.recv_with(WaitStrategy::BusySpin), 7);
134    /// ```
135    #[inline]
136    pub fn recv_with(&mut self, strategy: WaitStrategy) -> T {
137        let slot = unsafe { &*self.slots_ptr.add(self.index.slot(self.cursor)) };
138        let expected = self.cursor * 2 + 2;
139        let mut iter: u32 = 0;
140        loop {
141            match slot.try_read(self.cursor) {
142                Ok(Some(value)) => {
143                    self.cursor += 1;
144                    self.update_tracker();
145                    self.total_received += 1;
146                    return value;
147                }
148                Ok(None) => {
149                    strategy.wait(iter);
150                    iter = iter.saturating_add(1);
151                }
152                Err(stamp) => {
153                    if stamp >= expected {
154                        return self.recv_with_slow(strategy);
155                    }
156                    strategy.wait(iter);
157                    iter = iter.saturating_add(1);
158                }
159            }
160        }
161    }
162
163    #[cold]
164    #[inline(never)]
165    fn recv_with_slow(&mut self, strategy: WaitStrategy) -> T {
166        let mut iter: u32 = 0;
167        loop {
168            match self.try_recv() {
169                Ok(val) => return val,
170                Err(TryRecvError::Empty) => {
171                    strategy.wait(iter);
172                    iter = iter.saturating_add(1);
173                }
174                Err(TryRecvError::Lagged { .. }) => {
175                    iter = 0;
176                }
177            }
178        }
179    }
180
181    /// Skip to the **latest** published message (discards intermediate ones).
182    ///
183    /// Returns `None` only if nothing has been published yet. Under heavy
184    /// producer load, retries internally if the target slot is mid-write.
185    pub fn latest(&mut self) -> Option<T> {
186        loop {
187            let head = self.ring.cursor.0.load(Ordering::Acquire);
188            if head == u64::MAX {
189                return None;
190            }
191            self.cursor = head;
192            match self.read_slot() {
193                Ok(v) => return Some(v),
194                Err(TryRecvError::Empty) => return None,
195                Err(TryRecvError::Lagged { .. }) => {
196                    // Producer lapped us between cursor read and slot read.
197                    // Retry with updated head.
198                }
199            }
200        }
201    }
202
203    /// The sequence number this subscriber will read next.
204    ///
205    /// Sequence numbers are shared across every subscriber on the ring, so two
206    /// consumers can correlate their positions — a lossy telemetry tap and a
207    /// gating risk consumer refer to the same message by the same number.
208    #[inline]
209    pub fn cursor(&self) -> u64 {
210        self.cursor
211    }
212
213    /// How many messages are available to read (capped at ring capacity).
214    #[inline]
215    pub fn pending(&self) -> u64 {
216        let head = self.ring.cursor.0.load(Ordering::Acquire);
217        if head == u64::MAX || self.cursor > head {
218            0
219        } else {
220            let raw = head - self.cursor + 1;
221            raw.min(self.ring.capacity())
222        }
223    }
224
225    /// Total messages successfully received by this subscriber.
226    #[inline]
227    pub fn total_received(&self) -> u64 {
228        self.total_received
229    }
230
231    /// Total messages lost due to lag (consumer fell behind the ring).
232    #[inline]
233    pub fn total_lagged(&self) -> u64 {
234        self.total_lagged
235    }
236
237    /// Ratio of received to total (received + lagged). Returns 0.0 if no
238    /// messages have been processed.
239    #[inline]
240    pub fn receive_ratio(&self) -> f64 {
241        let total = self.total_received + self.total_lagged;
242        if total == 0 {
243            0.0
244        } else {
245            self.total_received as f64 / total as f64
246        }
247    }
248
249    /// Receive up to `buf.len()` messages in a single call.
250    ///
251    /// Messages are written into the provided slice starting at index 0.
252    /// Returns the number of messages received. On lag, the cursor is
253    /// advanced and filling continues from the oldest available message.
254    #[inline]
255    pub fn recv_batch(&mut self, buf: &mut [T]) -> usize {
256        let mut count = 0;
257        for slot in buf.iter_mut() {
258            match self.try_recv() {
259                Ok(value) => {
260                    *slot = value;
261                    count += 1;
262                }
263                Err(TryRecvError::Empty) => break,
264                Err(TryRecvError::Lagged { .. }) => {
265                    // Cursor was advanced — retry from oldest available.
266                    match self.try_recv() {
267                        Ok(value) => {
268                            *slot = value;
269                            count += 1;
270                        }
271                        Err(_) => break,
272                    }
273                }
274            }
275        }
276        count
277    }
278
279    /// Returns an iterator that drains all currently available messages.
280    /// Stops when no more messages are available. Handles lag transparently
281    /// by retrying after cursor advancement.
282    pub fn drain(&mut self) -> Drain<'_, T> {
283        Drain { sub: self }
284    }
285
286    /// Get this subscriber's cursor tracker for use in a
287    /// [`DependencyBarrier`].
288    ///
289    /// Returns `None` if the subscriber was created on a lossy channel
290    /// without [`subscribe_tracked()`](crate::Subscribable::subscribe_tracked).
291    /// Use `subscribe_tracked()` to ensure a tracker is always present.
292    #[inline]
293    pub fn tracker(&self) -> Option<Arc<Padded<AtomicU64>>> {
294        self.tracker.clone()
295    }
296
297    /// Try to receive the next message, but only if all upstream
298    /// subscribers in the barrier have already processed it.
299    ///
300    /// Returns [`TryRecvError::Empty`] if the upstream barrier has not
301    /// yet advanced past this subscriber's cursor, or if no new message
302    /// is available from the ring.
303    ///
304    /// # Example
305    ///
306    /// ```
307    /// use photon_ring::{channel, DependencyBarrier, TryRecvError};
308    ///
309    /// let (mut pub_, subs) = channel::<u64>(64);
310    /// let mut upstream = subs.subscribe_tracked();
311    /// let barrier = DependencyBarrier::from_subscribers(&[&upstream]);
312    /// let mut downstream = subs.subscribe();
313    ///
314    /// pub_.publish(42);
315    ///
316    /// // Downstream can't read — upstream hasn't consumed it yet
317    /// assert_eq!(downstream.try_recv_gated(&barrier), Err(TryRecvError::Empty));
318    ///
319    /// upstream.try_recv().unwrap();
320    ///
321    /// // Now downstream can proceed
322    /// assert_eq!(downstream.try_recv_gated(&barrier), Ok(42));
323    /// ```
324    #[inline]
325    pub fn try_recv_gated(&mut self, barrier: &DependencyBarrier) -> Result<T, TryRecvError> {
326        // The barrier's slowest() returns the minimum tracker value among
327        // upstreams, which is the *next sequence to read* for the slowest
328        // upstream. If slowest() <= self.cursor, the slowest upstream hasn't
329        // finished reading self.cursor yet.
330        if barrier.slowest() <= self.cursor {
331            return Err(TryRecvError::Empty);
332        }
333        self.try_recv()
334    }
335
336    /// Blocking receive gated by a dependency barrier.
337    ///
338    /// Spins until all upstream subscribers in the barrier have processed
339    /// the next message, then reads and returns it. On lag, the cursor is
340    /// advanced and the method retries.
341    ///
342    /// # Example
343    ///
344    /// ```
345    /// use photon_ring::{channel, DependencyBarrier};
346    ///
347    /// let (mut pub_, subs) = channel::<u64>(64);
348    /// let mut upstream = subs.subscribe_tracked();
349    /// let barrier = DependencyBarrier::from_subscribers(&[&upstream]);
350    /// let mut downstream = subs.subscribe();
351    ///
352    /// pub_.publish(99);
353    /// upstream.try_recv().unwrap();
354    ///
355    /// assert_eq!(downstream.recv_gated(&barrier), 99);
356    /// ```
357    #[inline]
358    pub fn recv_gated(&mut self, barrier: &DependencyBarrier) -> T {
359        loop {
360            match self.try_recv_gated(barrier) {
361                Ok(val) => return val,
362                Err(TryRecvError::Empty) => core::hint::spin_loop(),
363                Err(TryRecvError::Lagged { .. }) => {}
364            }
365        }
366    }
367
368    /// Update the backpressure tracker to reflect the current cursor position.
369    /// No-op on regular (lossy) channels.
370    #[inline]
371    fn update_tracker(&self) {
372        if let Some(ref tracker) = self.tracker {
373            tracker.0.store(self.cursor, Ordering::Relaxed);
374        }
375    }
376
377    /// Stamp-only fast-path read. The consumer's local `self.cursor` tells us
378    /// which slot and expected stamp to check — no shared cursor load needed
379    /// on the hot path.
380    #[inline]
381    fn read_slot(&mut self) -> Result<T, TryRecvError> {
382        // SAFETY: slots_ptr is valid for the lifetime of self.ring (Arc-owned).
383        let slot = unsafe { &*self.slots_ptr.add(self.index.slot(self.cursor)) };
384        let expected = self.cursor * 2 + 2;
385
386        match slot.try_read(self.cursor) {
387            Ok(Some(value)) => {
388                self.cursor += 1;
389                self.update_tracker();
390                self.total_received += 1;
391                Ok(value)
392            }
393            Ok(None) => {
394                // Torn read or write-in-progress — treat as empty for try_recv
395                Err(TryRecvError::Empty)
396            }
397            Err(actual_stamp) => {
398                // Odd stamp means write-in-progress — not ready yet
399                if actual_stamp & 1 != 0 {
400                    return Err(TryRecvError::Empty);
401                }
402                if actual_stamp < expected {
403                    // Slot holds an older (or no) sequence — not published yet
404                    Err(TryRecvError::Empty)
405                } else {
406                    // stamp > expected: slot was overwritten — slow path.
407                    // Read head cursor to compute exact lag.
408                    let head = self.ring.cursor.0.load(Ordering::Acquire);
409                    let cap = self.ring.capacity();
410                    if head == u64::MAX || self.cursor > head {
411                        // Rare race: stamp updated but cursor not yet visible
412                        return Err(TryRecvError::Empty);
413                    }
414                    if head >= cap {
415                        let oldest = head - cap + 1;
416                        if self.cursor < oldest {
417                            let skipped = oldest - self.cursor;
418                            self.cursor = oldest;
419                            self.update_tracker();
420                            self.total_lagged += skipped;
421                            return Err(TryRecvError::Lagged { skipped });
422                        }
423                    }
424                    // Head hasn't caught up yet (rare timing race)
425                    Err(TryRecvError::Empty)
426                }
427            }
428        }
429    }
430}
431
432impl<T: Pod> Drop for Subscriber<T> {
433    fn drop(&mut self) {
434        if let Some(ref tracker) = self.tracker {
435            if let Some(ref bp) = self.ring.backpressure {
436                let weak = Arc::downgrade(tracker);
437                let mut trackers = bp.trackers.lock();
438                trackers.retain(|t| !t.ptr_eq(&weak));
439            }
440        }
441    }
442}
443
444// ---------------------------------------------------------------------------
445// Drain iterator
446// ---------------------------------------------------------------------------
447
448/// An iterator that drains all currently available messages from a
449/// [`Subscriber`]. Stops when no more messages are available. Handles lag transparently
450/// by retrying after cursor advancement.
451///
452/// Created by [`Subscriber::drain`].
453pub struct Drain<'a, T: Pod> {
454    pub(super) sub: &'a mut Subscriber<T>,
455}
456
457impl<'a, T: Pod> Iterator for Drain<'a, T> {
458    type Item = T;
459    fn next(&mut self) -> Option<T> {
460        loop {
461            match self.sub.try_recv() {
462                Ok(v) => return Some(v),
463                Err(TryRecvError::Empty) => return None,
464                Err(TryRecvError::Lagged { .. }) => {
465                    // Cursor was advanced — retry from oldest available.
466                }
467            }
468        }
469    }
470}