Skip to main content

photon_ring/channel/
subscribable.rs

1// Copyright 2026 Photon Ring Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use super::subscriber::Subscriber;
5use crate::pod::Pod;
6use crate::ring::{Padded, SharedRing};
7use alloc::sync::Arc;
8use core::sync::atomic::{AtomicU64, Ordering};
9
10/// Clone-able handle for spawning [`Subscriber`]s.
11///
12/// Send this to other threads and call [`subscribe`](Subscribable::subscribe)
13/// to create independent consumers.
14pub struct Subscribable<T: Pod> {
15    pub(super) ring: Arc<SharedRing<T>>,
16}
17
18impl<T: Pod> Clone for Subscribable<T> {
19    fn clone(&self) -> Self {
20        Subscribable {
21            ring: self.ring.clone(),
22        }
23    }
24}
25
26unsafe impl<T: Pod> Send for Subscribable<T> {}
27unsafe impl<T: Pod> Sync for Subscribable<T> {}
28
29impl<T: Pod> Subscribable<T> {
30    /// Create a subscriber that will see only **future** messages.
31    pub fn subscribe(&self) -> Subscriber<T> {
32        let (start, tracker) = self.ring.register_tracker_at_head();
33        let slots_ptr = self.ring.slots_ptr();
34        let idx = self.ring.index;
35        Subscriber {
36            ring: self.ring.clone(),
37            slots_ptr,
38            index: idx,
39            cursor: start,
40            tracker,
41            total_lagged: 0,
42            total_received: 0,
43        }
44    }
45
46    /// Create a subscriber that never gates the publisher.
47    ///
48    /// On a bounded channel, [`subscribe`](Self::subscribe) registers the
49    /// subscriber for backpressure: the publisher refuses to overwrite a slot
50    /// this subscriber has not read yet. A **lossy** subscriber opts out of
51    /// that guarantee. The publisher ignores it entirely, and if it falls
52    /// behind it observes [`TryRecvError::Lagged`](crate::TryRecvError::Lagged)
53    /// with an exact skip count, exactly as on a lossy channel.
54    ///
55    /// This lets a single ring carry consumers with **different delivery
56    /// contracts**: a risk engine that must see every message, and telemetry
57    /// that must never stall the publisher, reading the same sequence numbers.
58    ///
59    /// ```
60    /// use photon_ring::channel_bounded;
61    ///
62    /// let (mut p, s) = channel_bounded::<u64>(4, 0);
63    /// let mut critical = s.subscribe();        // gates the publisher
64    /// let mut telemetry = s.subscribe_lossy(); // never gates it
65    ///
66    /// p.publish(1);
67    /// assert_eq!(critical.try_recv(), Ok(1));
68    /// assert_eq!(telemetry.try_recv(), Ok(1));
69    /// ```
70    ///
71    /// On a lossy channel this is identical to [`subscribe`](Self::subscribe),
72    /// since no subscriber gates the publisher there.
73    pub fn subscribe_lossy(&self) -> Subscriber<T> {
74        let head = self.ring.cursor.0.load(Ordering::Acquire);
75        let start = if head == u64::MAX { 0 } else { head + 1 };
76        let slots_ptr = self.ring.slots_ptr();
77        let idx = self.ring.index;
78        Subscriber {
79            ring: self.ring.clone(),
80            slots_ptr,
81            index: idx,
82            cursor: start,
83            // No tracker: the publisher's slowest-cursor scan never sees this
84            // subscriber, so it can never be blocked by it.
85            tracker: None,
86            total_lagged: 0,
87            total_received: 0,
88        }
89    }
90
91    /// Create a subscriber starting from the **oldest available** message
92    /// still in the ring (or 0 if nothing published yet).
93    ///
94    /// Note that on a bounded channel the no-loss guarantee only applies from
95    /// the subscription point forward. This starts at a sequence the publisher
96    /// was already entitled to overwrite, and registering cannot retroactively
97    /// reserve it, so the retained history it starts from may be lapped before
98    /// it is read. Use [`subscribe`](Self::subscribe) if you need the guarantee
99    /// from the first message you see.
100    ///
101    /// The converse also holds: once the publisher observes this subscriber's
102    /// tracker, the retained history counts as unread, so on a bounded channel
103    /// the publisher can be gated until up to a full ring of messages is
104    /// drained. Attach a replay consumer this way only if it will drain
105    /// promptly; a tap that must never stall the publisher should use
106    /// [`subscribe_lossy`](Self::subscribe_lossy) instead.
107    pub fn subscribe_from_oldest(&self) -> Subscriber<T> {
108        let head = self.ring.cursor.0.load(Ordering::Acquire);
109        let cap = self.ring.capacity();
110        let start = if head == u64::MAX {
111            0
112        } else if head >= cap {
113            head - cap + 1
114        } else {
115            0
116        };
117        let tracker = self.ring.register_tracker(start);
118        let slots_ptr = self.ring.slots_ptr();
119        let idx = self.ring.index;
120        Subscriber {
121            ring: self.ring.clone(),
122            slots_ptr,
123            index: idx,
124            cursor: start,
125            tracker,
126            total_lagged: 0,
127            total_received: 0,
128        }
129    }
130
131    /// Create a subscriber with an active cursor tracker.
132    ///
133    /// Use this when the subscriber will participate in a
134    /// [`DependencyBarrier`](crate::DependencyBarrier) as an upstream consumer.
135    ///
136    /// On **bounded** channels, this behaves identically to
137    /// [`subscribe()`](Self::subscribe) — those subscribers already have
138    /// trackers.
139    ///
140    /// On **lossy** channels, [`subscribe()`](Self::subscribe) omits the
141    /// tracker (zero overhead for the common case). This method creates a
142    /// standalone tracker so that a [`DependencyBarrier`](crate::DependencyBarrier) can read the
143    /// subscriber's cursor position. The tracker is **not** registered
144    /// with the ring's backpressure system — it is purely for dependency
145    /// graph coordination.
146    pub fn subscribe_tracked(&self) -> Subscriber<T> {
147        // On bounded channels this registers for backpressure; on lossy channels
148        // it returns None, so we create a standalone tracker purely for barriers.
149        let (start, tracker) = self.ring.register_tracker_at_head();
150        let tracker = tracker.or_else(|| Some(Arc::new(Padded(AtomicU64::new(start)))));
151        let slots_ptr = self.ring.slots_ptr();
152        let idx = self.ring.index;
153        Subscriber {
154            ring: self.ring.clone(),
155            slots_ptr,
156            index: idx,
157            cursor: start,
158            tracker,
159            total_lagged: 0,
160            total_received: 0,
161        }
162    }
163}