Skip to main content

kevy_embedded/
pubsub.rs

1//! In-process pub/sub bus for embedded `Store`.
2//!
3//! Mirrors the Redis/kevy server pub/sub semantics inside a single process:
4//! `Store::publish` walks the channel + pattern subscriber tables and
5//! enqueues a [`PubsubEvent`] onto each matching [`Subscription`]'s
6//! `std::sync::mpsc` channel. Each `Subscription` drains its own queue via
7//! [`Subscription::recv`] / [`Subscription::recv_timeout`] /
8//! [`Subscription::try_recv`].
9//!
10//! The bus lives inside `Inner` and is reached only under the embedded
11//! mutex; per-publish we clone the matching senders out, drop the lock,
12//! then `send()` — so a slow receiver can't stall publishes on unrelated
13//! channels.
14
15// A send to a subscriber that has gone away is the normal end of a
16// subscription, not an error to handle: the receiver drops when the
17// client disconnects, and the bus removes it on the next sweep.
18// Reporting here would turn every ordinary disconnect into a log line.
19#![expect(
20    clippy::let_underscore_must_use,
21    reason = "a dropped receiver is how a subscription ends"
22)]
23
24use crate::{KevyError, KevyResult};
25use std::collections::HashSet;
26use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError, channel};
27use std::sync::{Arc, Mutex, RwLock};
28use std::time::Duration;
29
30use crate::store::Inner;
31
32/// One pub/sub event delivered to a [`Subscription`] — the same type the
33/// network clients receive, so a consumer handles either source alike.
34pub use kevy_resp::PubsubEvent;
35
36// `BusEntry` + `PubsubBus` live in [`crate::pubsub_bus`] — split out so
37// this file stays under the 500-LOC house rule. Re-exported below so
38// `crate::store::Inner` keeps its existing `pubsub::PubsubBus` import.
39pub(crate) use crate::pubsub_bus::PubsubBus;
40
41/// A handle to one subscription — owns the receive end of the bus channel.
42///
43/// Drop unsubscribes from everything automatically. While the handle is
44/// alive, [`recv`](Self::recv) / [`recv_timeout`](Self::recv_timeout) /
45/// [`try_recv`](Self::try_recv) drain queued [`PubsubEvent`]s in arrival
46/// order.
47///
48/// **Threading.** `Subscription` is `Send + Sync` —
49/// `Arc<Subscription>` works, so multiple async tasks (or
50/// `spawn_blocking` jobs) can share one subscription and call `recv`
51/// concurrently. The underlying `std::sync::mpsc::Receiver` is
52/// !Sync, so we wrap it (and the matching ack `Sender`) in a `Mutex`;
53/// concurrent `recv` callers serialise on that lock, with each call
54/// receiving a *different* frame in arrival order (single-consumer
55/// semantics — NOT broadcast fanout). `try_recv` is non-blocking even
56/// under contention: if the lock is held by a blocking `recv`,
57/// `try_recv` returns `Ok(None)` rather than waiting.
58///
59/// If you need broadcast fanout (every subscriber sees every message),
60/// open a separate `Subscription` per consumer — they're cheap.
61///
62/// ```
63/// use kevy_embedded::{Config, PubsubEvent, Store};
64///
65/// let s = Store::open(Config::default())?;
66/// let sub = s.subscribe(&[b"news"]);
67/// // the subscribe ack is queued first
68/// assert!(matches!(sub.recv()?, PubsubEvent::Subscribe { count: 1, .. }));
69/// assert_eq!(s.publish(b"news", b"hello"), 1, "one subscriber received it");
70/// let PubsubEvent::Message { channel, payload } = sub.recv()? else {
71///     panic!("a publish arrives as a message");
72/// };
73/// assert_eq!((channel, payload), (b"news".to_vec(), b"hello".to_vec()));
74/// drop(sub); // unsubscribes
75/// assert_eq!(s.publish(b"news", b"again"), 0);
76/// # Ok::<(), kevy_embedded::KevyError>(())
77/// ```
78#[allow(missing_debug_implementations)]
79pub struct Subscription {
80    inner: Arc<RwLock<Inner>>,
81    // Keeps the AOF/reaper alive as long as a Subscription does — so
82    // dropping every `Store` clone while a subscriber is still active
83    // leaves the keyspace intact until the subscriber also goes away.
84    _guard: Arc<crate::store::DropGuard>,
85    // `Receiver<T>` is `Send + !Sync`; wrap so `Subscription: Sync`.
86    // Hot path (recv) acquires + holds the lock during the blocking
87    // wait — single consumer at a time; concurrent recv callers
88    // serialise and each get a different frame. See type-level
89    // doc-comment for the trade-off.
90    receiver: Mutex<Receiver<PubsubEvent>>,
91    // `Sender<T>` is also !Sync (Send + Clone but cannot be shared by
92    // reference across threads). Wrap so the ack-frame path (called
93    // from subscribe/unsubscribe / Drop) can run from any thread.
94    sender: Mutex<Sender<PubsubEvent>>,
95    id: u64,
96    channels: HashSet<Vec<u8>>,
97    patterns: HashSet<Vec<u8>>,
98}
99
100impl Subscription {
101    pub(crate) fn new(inner: Arc<RwLock<Inner>>, guard: Arc<crate::store::DropGuard>) -> Self {
102        let (sender, receiver) = channel();
103        let id = inner.write().unwrap_or_else(std::sync::PoisonError::into_inner).bus.alloc_id();
104        Self {
105            inner,
106            _guard: guard,
107            receiver: Mutex::new(receiver),
108            sender: Mutex::new(sender),
109            id,
110            channels: HashSet::new(),
111            patterns: HashSet::new(),
112        }
113    }
114
115    /// Clone of the inbound `Sender`. Used both for ack frames (Subscribe /
116    /// Unsubscribe / ...) and to register a sender clone inside
117    /// `PubsubBus`. Calling this acquires the sender lock briefly (~20 ns).
118    fn sender_clone(&self) -> Sender<PubsubEvent> {
119        self.sender.lock().unwrap_or_else(std::sync::PoisonError::into_inner).clone()
120    }
121
122    /// `SUBSCRIBE channel [channel ...]`. Per-channel `Subscribe` acks are
123    /// enqueued onto the receive queue in order.
124    pub fn subscribe(&mut self, channels: &[&[u8]]) {
125        let s = self.sender_clone();
126        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
127        for ch in channels {
128            let owned = ch.to_vec();
129            let added = g.bus.add_channel(self.id, &s, owned.clone());
130            if added {
131                self.channels.insert(owned.clone());
132            }
133            let count = g.bus.count_for(self.id);
134            let _ = s.send(PubsubEvent::Subscribe { channel: owned, count: count as i64 });
135        }
136    }
137
138    /// `PSUBSCRIBE pattern [pattern ...]`. Patterns use Redis glob syntax
139    /// (`*`, `?`, `[abc]`).
140    pub fn psubscribe(&mut self, patterns: &[&[u8]]) {
141        let s = self.sender_clone();
142        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
143        for pat in patterns {
144            let owned = pat.to_vec();
145            let added = g.bus.add_pattern(self.id, &s, owned.clone());
146            if added {
147                self.patterns.insert(owned.clone());
148            }
149            let count = g.bus.count_for(self.id);
150            let _ = s.send(PubsubEvent::Psubscribe { pattern: owned, count: count as i64 });
151        }
152    }
153
154    /// `UNSUBSCRIBE [channel ...]`. Empty `channels` removes every channel
155    /// subscription this handle holds (matching the Redis wire shape:
156    /// individual ack frames for each channel that was actually removed,
157    /// or a single `Unsubscribe { channel: None }` if none were held).
158    pub fn unsubscribe(&mut self, channels: &[&[u8]]) {
159        if channels.is_empty() {
160            self.drain_channel_subs();
161            return;
162        }
163        let s = self.sender_clone();
164        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
165        for ch in channels {
166            let owned = ch.to_vec();
167            let _ = g.bus.remove_channel(self.id, &owned);
168            self.channels.remove(&owned);
169            let count = g.bus.count_for(self.id);
170            let _ = s.send(PubsubEvent::Unsubscribe { channel: Some(owned), count: count as i64 });
171        }
172    }
173
174    /// `PUNSUBSCRIBE [pattern ...]`. Empty `patterns` removes every pattern.
175    pub fn punsubscribe(&mut self, patterns: &[&[u8]]) {
176        if patterns.is_empty() {
177            self.drain_pattern_subs();
178            return;
179        }
180        let s = self.sender_clone();
181        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
182        for pat in patterns {
183            let owned = pat.to_vec();
184            let _ = g.bus.remove_pattern(self.id, &owned);
185            self.patterns.remove(&owned);
186            let count = g.bus.count_for(self.id);
187            let _ = s.send(PubsubEvent::Punsubscribe { pattern: Some(owned), count: count as i64 });
188        }
189    }
190
191    fn drain_channel_subs(&mut self) {
192        let s = self.sender_clone();
193        let owned: Vec<Vec<u8>> = self.channels.drain().collect();
194        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
195        if owned.is_empty() {
196            let count = g.bus.count_for(self.id);
197            let _ = s.send(PubsubEvent::Unsubscribe { channel: None, count: count as i64 });
198            return;
199        }
200        for ch in owned {
201            let _ = g.bus.remove_channel(self.id, &ch);
202            let count = g.bus.count_for(self.id);
203            let _ = s.send(PubsubEvent::Unsubscribe { channel: Some(ch), count: count as i64 });
204        }
205    }
206
207    fn drain_pattern_subs(&mut self) {
208        let s = self.sender_clone();
209        let owned: Vec<Vec<u8>> = self.patterns.drain().collect();
210        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
211        if owned.is_empty() {
212            let count = g.bus.count_for(self.id);
213            let _ = s.send(PubsubEvent::Punsubscribe { pattern: None, count: count as i64 });
214            return;
215        }
216        for p in owned {
217            let _ = g.bus.remove_pattern(self.id, &p);
218            let count = g.bus.count_for(self.id);
219            let _ = s.send(PubsubEvent::Punsubscribe { pattern: Some(p), count: count as i64 });
220        }
221    }
222
223    /// Block until one frame is queued. `Err(io::ErrorKind::UnexpectedEof)`
224    /// once the underlying bus tears down (last `Store` clone dropped).
225    ///
226    /// Acquires the receiver mutex for the entire blocking wait — other
227    /// `recv`/`recv_timeout` callers serialise behind this one. Concurrent
228    /// `try_recv` calls return `Ok(None)` while a `recv` is blocked (no
229    /// wait on the lock); see the type-level doc for the trade-off.
230    pub fn recv(&self) -> KevyResult<PubsubEvent> {
231        let g = self.receiver.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
232        g.recv().map_err(|_| KevyError::Closed)
233    }
234
235    /// Bounded blocking recv. `Err(KevyError::TimedOut)` when `dur`
236    /// elapses; `Err(KevyError::Closed)` when the bus is gone.
237    pub fn recv_timeout(&self, dur: Duration) -> KevyResult<PubsubEvent> {
238        let g = self.receiver.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
239        g.recv_timeout(dur).map_err(|e| match e {
240            RecvTimeoutError::Timeout => KevyError::TimedOut,
241            RecvTimeoutError::Disconnected => KevyError::Closed,
242        })
243    }
244
245    /// Non-blocking recv. `Ok(None)` if the queue is empty;
246    /// `Err(KevyError::Closed)` when the bus is gone.
247    ///
248    /// Uses `try_lock` so a concurrent blocking `recv` doesn't make
249    /// `try_recv` itself block — lock contention is reported as `Ok(None)`
250    /// (semantically: "no frame available right now"). Same shape callers
251    /// already handle for an empty queue.
252    pub fn try_recv(&self) -> KevyResult<Option<PubsubEvent>> {
253        let Ok(g) = self.receiver.try_lock() else {
254            return Ok(None);
255        };
256        match g.try_recv() {
257            Ok(f) => Ok(Some(f)),
258            Err(TryRecvError::Empty) => Ok(None),
259            Err(TryRecvError::Disconnected) => Err(KevyError::Closed),
260        }
261    }
262}
263
264impl std::fmt::Debug for Subscription {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        f.debug_struct("Subscription")
267            .field("id", &self.id)
268            .field("channels", &self.channels.len())
269            .field("patterns", &self.patterns.len())
270            .finish_non_exhaustive()
271    }
272}
273
274impl Drop for Subscription {
275    fn drop(&mut self) {
276        // Best-effort cleanup. Recover from poison (a panic elsewhere left the
277        // bus intact) so our entries are always removed.
278        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
279        g.bus.remove_all_for(self.id);
280    }
281}
282
283#[cfg(test)]
284#[path = "pubsub_tests.rs"]
285mod tests;