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 [`PubsubFrame`] 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
15use crate::{KevyError, KevyResult};
16use std::collections::HashSet;
17use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError, channel};
18use std::sync::{Arc, Mutex, RwLock};
19use std::time::Duration;
20
21use crate::store::Inner;
22
23/// One pub/sub event delivered to a [`Subscription`].
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum PubsubFrame {
26    /// Ack: `SUBSCRIBE` succeeded on `channel`.
27    Subscribe {
28        /// Channel that was just subscribed.
29        channel: Vec<u8>,
30        /// Total channels + patterns this subscription holds after the op.
31        count: usize,
32    },
33    /// Ack: `PSUBSCRIBE` succeeded on `pattern`.
34    Psubscribe {
35        /// Pattern that was just subscribed.
36        pattern: Vec<u8>,
37        /// Total channels + patterns this subscription holds after the op.
38        count: usize,
39    },
40    /// Ack: `UNSUBSCRIBE` removed `channel` (or "all", when `None`).
41    Unsubscribe {
42        /// Channel that was just unsubscribed (`None` = "all").
43        channel: Option<Vec<u8>>,
44        /// Total channels + patterns still held after the op.
45        count: usize,
46    },
47    /// Ack: `PUNSUBSCRIBE` removed `pattern` (or "all", when `None`).
48    Punsubscribe {
49        /// Pattern that was just unsubscribed (`None` = "all").
50        pattern: Option<Vec<u8>>,
51        /// Total channels + patterns still held after the op.
52        count: usize,
53    },
54    /// A `PUBLISH` reached a channel this subscription holds directly.
55    Message {
56        /// Channel the publish was made to.
57        channel: Vec<u8>,
58        /// Raw payload bytes.
59        payload: Vec<u8>,
60    },
61    /// A `PUBLISH` reached a channel matching one of this subscription's
62    /// patterns.
63    Pmessage {
64        /// Pattern the channel matched.
65        pattern: Vec<u8>,
66        /// Channel the publish was made to.
67        channel: Vec<u8>,
68        /// Raw payload bytes.
69        payload: Vec<u8>,
70    },
71}
72
73impl PubsubFrame {
74    /// The raw message payload, moved out of the frame.
75    ///
76    /// `Some(payload)` for the two delivery frames ([`Message`](Self::Message)
77    /// and [`Pmessage`](Self::Pmessage)); `None` for every control/ack frame
78    /// (subscribe / unsubscribe / …), which carries no payload. Consuming
79    /// `self` lets a scalar drain hand a push subscriber just the bytes with
80    /// no extra copy — the pub/sub analog of the KV scalar door. The channel
81    /// and the message-vs-pmessage distinction are dropped; a caller that
82    /// needs either keeps matching on the frame.
83    #[must_use]
84    pub fn into_payload(self) -> Option<Vec<u8>> {
85        match self {
86            PubsubFrame::Message { payload, .. } | PubsubFrame::Pmessage { payload, .. } => {
87                Some(payload)
88            }
89            _ => None,
90        }
91    }
92}
93
94// `BusEntry` + `PubsubBus` live in [`crate::pubsub_bus`] — split out so
95// this file stays under the 500-LOC house rule. Re-exported below so
96// `crate::store::Inner` keeps its existing `pubsub::PubsubBus` import.
97pub(crate) use crate::pubsub_bus::PubsubBus;
98
99/// A handle to one subscription — owns the receive end of the bus channel.
100///
101/// Drop unsubscribes from everything automatically. While the handle is
102/// alive, [`recv`](Self::recv) / [`recv_timeout`](Self::recv_timeout) /
103/// [`try_recv`](Self::try_recv) drain queued [`PubsubFrame`]s in arrival
104/// order.
105///
106/// **Threading.** `Subscription` is `Send + Sync` —
107/// `Arc<Subscription>` works, so multiple async tasks (or
108/// `spawn_blocking` jobs) can share one subscription and call `recv`
109/// concurrently. The underlying `std::sync::mpsc::Receiver` is
110/// !Sync, so we wrap it (and the matching ack `Sender`) in a `Mutex`;
111/// concurrent `recv` callers serialise on that lock, with each call
112/// receiving a *different* frame in arrival order (single-consumer
113/// semantics — NOT broadcast fanout). `try_recv` is non-blocking even
114/// under contention: if the lock is held by a blocking `recv`,
115/// `try_recv` returns `Ok(None)` rather than waiting.
116///
117/// If you need broadcast fanout (every subscriber sees every message),
118/// open a separate `Subscription` per consumer — they're cheap.
119#[allow(missing_debug_implementations)]
120pub struct Subscription {
121    inner: Arc<RwLock<Inner>>,
122    // Keeps the AOF/reaper alive as long as a Subscription does — so
123    // dropping every `Store` clone while a subscriber is still active
124    // leaves the keyspace intact until the subscriber also goes away.
125    _guard: Arc<crate::store::DropGuard>,
126    // `Receiver<T>` is `Send + !Sync`; wrap so `Subscription: Sync`.
127    // Hot path (recv) acquires + holds the lock during the blocking
128    // wait — single consumer at a time; concurrent recv callers
129    // serialise and each get a different frame. See type-level
130    // doc-comment for the trade-off.
131    receiver: Mutex<Receiver<PubsubFrame>>,
132    // `Sender<T>` is also !Sync (Send + Clone but cannot be shared by
133    // reference across threads). Wrap so the ack-frame path (called
134    // from subscribe/unsubscribe / Drop) can run from any thread.
135    sender: Mutex<Sender<PubsubFrame>>,
136    id: u64,
137    channels: HashSet<Vec<u8>>,
138    patterns: HashSet<Vec<u8>>,
139}
140
141impl Subscription {
142    pub(crate) fn new(inner: Arc<RwLock<Inner>>, guard: Arc<crate::store::DropGuard>) -> Self {
143        let (sender, receiver) = channel();
144        let id = inner
145            .write()
146            .unwrap_or_else(std::sync::PoisonError::into_inner)
147            .bus
148            .alloc_id();
149        Self {
150            inner,
151            _guard: guard,
152            receiver: Mutex::new(receiver),
153            sender: Mutex::new(sender),
154            id,
155            channels: HashSet::new(),
156            patterns: HashSet::new(),
157        }
158    }
159
160    /// Clone of the inbound `Sender`. Used both for ack frames (Subscribe /
161    /// Unsubscribe / ...) and to register a sender clone inside
162    /// `PubsubBus`. Calling this acquires the sender lock briefly (~20 ns).
163    fn sender_clone(&self) -> Sender<PubsubFrame> {
164        self.sender
165            .lock()
166            .unwrap_or_else(std::sync::PoisonError::into_inner)
167            .clone()
168    }
169
170    /// `SUBSCRIBE channel [channel ...]`. Per-channel `Subscribe` acks are
171    /// enqueued onto the receive queue in order.
172    pub fn subscribe(&mut self, channels: &[&[u8]]) {
173        let s = self.sender_clone();
174        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
175        for ch in channels {
176            let owned = ch.to_vec();
177            let added = g.bus.add_channel(self.id, &s, owned.clone());
178            if added {
179                self.channels.insert(owned.clone());
180            }
181            let count = g.bus.count_for(self.id);
182            let _ = s.send(PubsubFrame::Subscribe {
183                channel: owned,
184                count,
185            });
186        }
187    }
188
189    /// `PSUBSCRIBE pattern [pattern ...]`. Patterns use Redis glob syntax
190    /// (`*`, `?`, `[abc]`).
191    pub fn psubscribe(&mut self, patterns: &[&[u8]]) {
192        let s = self.sender_clone();
193        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
194        for pat in patterns {
195            let owned = pat.to_vec();
196            let added = g.bus.add_pattern(self.id, &s, owned.clone());
197            if added {
198                self.patterns.insert(owned.clone());
199            }
200            let count = g.bus.count_for(self.id);
201            let _ = s.send(PubsubFrame::Psubscribe {
202                pattern: owned,
203                count,
204            });
205        }
206    }
207
208    /// `UNSUBSCRIBE [channel ...]`. Empty `channels` removes every channel
209    /// subscription this handle holds (matching the Redis wire shape:
210    /// individual ack frames for each channel that was actually removed,
211    /// or a single `Unsubscribe { channel: None }` if none were held).
212    pub fn unsubscribe(&mut self, channels: &[&[u8]]) {
213        if channels.is_empty() {
214            self.drain_channel_subs();
215            return;
216        }
217        let s = self.sender_clone();
218        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
219        for ch in channels {
220            let owned = ch.to_vec();
221            let _ = g.bus.remove_channel(self.id, &owned);
222            self.channels.remove(&owned);
223            let count = g.bus.count_for(self.id);
224            let _ = s.send(PubsubFrame::Unsubscribe {
225                channel: Some(owned),
226                count,
227            });
228        }
229    }
230
231    /// `PUNSUBSCRIBE [pattern ...]`. Empty `patterns` removes every pattern.
232    pub fn punsubscribe(&mut self, patterns: &[&[u8]]) {
233        if patterns.is_empty() {
234            self.drain_pattern_subs();
235            return;
236        }
237        let s = self.sender_clone();
238        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
239        for pat in patterns {
240            let owned = pat.to_vec();
241            let _ = g.bus.remove_pattern(self.id, &owned);
242            self.patterns.remove(&owned);
243            let count = g.bus.count_for(self.id);
244            let _ = s.send(PubsubFrame::Punsubscribe {
245                pattern: Some(owned),
246                count,
247            });
248        }
249    }
250
251    fn drain_channel_subs(&mut self) {
252        let s = self.sender_clone();
253        let owned: Vec<Vec<u8>> = self.channels.drain().collect();
254        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
255        if owned.is_empty() {
256            let count = g.bus.count_for(self.id);
257            let _ = s.send(PubsubFrame::Unsubscribe { channel: None, count });
258            return;
259        }
260        for ch in owned {
261            let _ = g.bus.remove_channel(self.id, &ch);
262            let count = g.bus.count_for(self.id);
263            let _ = s.send(PubsubFrame::Unsubscribe {
264                channel: Some(ch),
265                count,
266            });
267        }
268    }
269
270    fn drain_pattern_subs(&mut self) {
271        let s = self.sender_clone();
272        let owned: Vec<Vec<u8>> = self.patterns.drain().collect();
273        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
274        if owned.is_empty() {
275            let count = g.bus.count_for(self.id);
276            let _ = s.send(PubsubFrame::Punsubscribe { pattern: None, count });
277            return;
278        }
279        for p in owned {
280            let _ = g.bus.remove_pattern(self.id, &p);
281            let count = g.bus.count_for(self.id);
282            let _ = s.send(PubsubFrame::Punsubscribe {
283                pattern: Some(p),
284                count,
285            });
286        }
287    }
288
289    /// Block until one frame is queued. `Err(io::ErrorKind::UnexpectedEof)`
290    /// once the underlying bus tears down (last `Store` clone dropped).
291    ///
292    /// Acquires the receiver mutex for the entire blocking wait — other
293    /// `recv`/`recv_timeout` callers serialise behind this one. Concurrent
294    /// `try_recv` calls return `Ok(None)` while a `recv` is blocked (no
295    /// wait on the lock); see the type-level doc for the trade-off.
296    pub fn recv(&self) -> KevyResult<PubsubFrame> {
297        let g = self.receiver.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
298        g.recv().map_err(|_| KevyError::Closed)
299    }
300
301    /// Bounded blocking recv. `Err(KevyError::TimedOut)` when `dur`
302    /// elapses; `Err(KevyError::Closed)` when the bus is gone.
303    pub fn recv_timeout(&self, dur: Duration) -> KevyResult<PubsubFrame> {
304        let g = self.receiver.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
305        g.recv_timeout(dur).map_err(|e| match e {
306            RecvTimeoutError::Timeout => KevyError::TimedOut,
307            RecvTimeoutError::Disconnected => KevyError::Closed,
308        })
309    }
310
311    /// Non-blocking recv. `Ok(None)` if the queue is empty;
312    /// `Err(KevyError::Closed)` when the bus is gone.
313    ///
314    /// Uses `try_lock` so a concurrent blocking `recv` doesn't make
315    /// `try_recv` itself block — lock contention is reported as `Ok(None)`
316    /// (semantically: "no frame available right now"). Same shape callers
317    /// already handle for an empty queue.
318    pub fn try_recv(&self) -> KevyResult<Option<PubsubFrame>> {
319        let Ok(g) = self.receiver.try_lock() else {
320            return Ok(None);
321        };
322        match g.try_recv() {
323            Ok(f) => Ok(Some(f)),
324            Err(TryRecvError::Empty) => Ok(None),
325            Err(TryRecvError::Disconnected) => Err(KevyError::Closed),
326        }
327    }
328}
329
330impl std::fmt::Debug for Subscription {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        f.debug_struct("Subscription")
333            .field("id", &self.id)
334            .field("channels", &self.channels.len())
335            .field("patterns", &self.patterns.len())
336            .finish_non_exhaustive()
337    }
338}
339
340impl Drop for Subscription {
341    fn drop(&mut self) {
342        // Best-effort cleanup. Recover from poison (a panic elsewhere left the
343        // bus intact) so our entries are always removed.
344        let mut g = self.inner.write().unwrap_or_else(std::sync::PoisonError::into_inner);
345        g.bus.remove_all_for(self.id);
346    }
347}
348
349#[cfg(test)]
350#[path = "pubsub_tests.rs"]
351mod tests;