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