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}