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;