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}