Skip to main content

netget/server/redis/
pubsub.rs

1//! Pub/sub for one Redis server: which connection is subscribed to what, and delivery of a
2//! published message to every connection whose subscription matches.
3//!
4//! This is protocol mechanics, not storage. Nothing here outlives the connections that put it
5//! here: a subscription is a fact about a live socket, and it is removed with the socket. No
6//! message is retained — one that finds no subscriber is gone, as in Redis. What the model
7//! decides is policy: whether a `SUBSCRIBE` is honoured, whether a `PUBLISH` is delivered and
8//! what it answers, and what the server publishes on its own (`redis_publish_message`).
9//!
10//! # Ordering
11//!
12//! Every byte a connection's read loop writes goes through that connection's [`Outbox`], and
13//! so does every message delivered to it, so one writer task owns the socket and frames never
14//! interleave. A subscription is registered and its confirmation queued under the hub lock,
15//! and a publish queues its messages under the same lock, so a subscriber reads the
16//! `subscribe` confirmation before any message on that channel — the order every client
17//! library relies on to know its subscription is live.
18//!
19//! # Bounds
20//!
21//! Everything a peer can grow is capped, and every cap refuses rather than truncates:
22//!
23//! | Bound | Value | On breach |
24//! |---|---|---|
25//! | [`MAX_CHANNEL_BYTES`] | 1 KiB | the command is refused |
26//! | [`MAX_PATTERN_BYTES`] | 256 B | the command is refused |
27//! | [`MAX_MESSAGE_BYTES`] | 1 MiB | the `PUBLISH` is refused, nothing is delivered |
28//! | [`MAX_SUBSCRIPTIONS_PER_CONNECTION`] | 1024 | the whole command is refused |
29//! | [`MAX_SUBSCRIPTIONS_PER_SERVER`] | 65 536 | the whole command is refused |
30//! | [`MAX_PATTERNS_PER_SERVER`] | 1024 distinct | the whole command is refused |
31//! | [`SUBSCRIBER_OUTPUT_LIMIT_BYTES`] | 8 MiB queued | the subscriber is disconnected |
32//!
33//! A publish costs, per distinct pattern, at most `pattern bytes × channel bytes` matcher
34//! steps (`utils::redis_keys::glob_match` is iterative and never backtracks past the last
35//! `*`), which is why both lengths and the number of distinct patterns are capped.
36//!
37//! The last row is Redis's `client-output-buffer-limit pubsub` hard limit: a subscriber that
38//! does not read is closed rather than allowed to make the server buffer without bound. Redis
39//! ships `32mb 8mb 60`; this server applies the hard limit only, at 8 MiB, because 256
40//! connections at 32 MiB each is 8 GiB of somebody else's messages.
41use super::resp_value::Resp;
42use crate::utils::redis_keys::glob_match;
43use bytes::Bytes;
44use std::collections::{BTreeSet, HashMap, HashSet};
45use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
46use std::sync::{Arc, Mutex};
47use tokio::sync::{mpsc, oneshot, watch};
48use tracing::warn;
49
50/// Longest channel name accepted, in bytes.
51pub const MAX_CHANNEL_BYTES: usize = 1024;
52/// Longest pattern accepted, in bytes.
53pub const MAX_PATTERN_BYTES: usize = 256;
54/// Largest message a `PUBLISH` (or `redis_publish_message`) may carry.
55pub const MAX_MESSAGE_BYTES: usize = 1024 * 1024;
56/// Channels, patterns and shard channels one connection may hold together.
57pub const MAX_SUBSCRIPTIONS_PER_CONNECTION: usize = 1024;
58/// Subscriptions of every kind across every connection of one server.
59pub const MAX_SUBSCRIPTIONS_PER_SERVER: usize = 65_536;
60/// Distinct patterns across one server — the number of matcher runs one publish costs.
61pub const MAX_PATTERNS_PER_SERVER: usize = 1024;
62/// Delivered-but-unwritten bytes one subscriber may have queued before it is disconnected.
63pub const SUBSCRIBER_OUTPUT_LIMIT_BYTES: usize = 8 * 1024 * 1024;
64
65/// Which namespace a subscription lives in.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum Kind {
68    /// `SUBSCRIBE` / `PUBLISH`.
69    Channel,
70    /// `PSUBSCRIBE`: a glob over channel names.
71    Pattern,
72    /// `SSUBSCRIBE` / `SPUBLISH`: a shard channel, routed by hash slot in a cluster.
73    Shard,
74}
75
76impl Kind {
77    fn subscribe_word(self) -> &'static str {
78        match self {
79            Kind::Channel => "subscribe",
80            Kind::Pattern => "psubscribe",
81            Kind::Shard => "ssubscribe",
82        }
83    }
84    fn unsubscribe_word(self) -> &'static str {
85        match self {
86            Kind::Channel => "unsubscribe",
87            Kind::Pattern => "punsubscribe",
88            Kind::Shard => "sunsubscribe",
89        }
90    }
91    fn max_name_bytes(self) -> usize {
92        match self {
93            Kind::Pattern => MAX_PATTERN_BYTES,
94            _ => MAX_CHANNEL_BYTES,
95        }
96    }
97}
98
99// ============================================================================
100// Outbox: the single writer of one connection
101// ============================================================================
102
103/// One frame on its way to a connection's socket.
104pub struct OutFrame {
105    pub bytes: Bytes,
106    /// A delivered pub/sub message, counted against [`SUBSCRIBER_OUTPUT_LIMIT_BYTES`]. Replies
107    /// are not: the read loop waits for each reply to be written before reading the next
108    /// command, so they can never pile up.
109    pub counted: bool,
110    /// Told when the frame has been written (or failed to be).
111    pub ack: Option<oneshot::Sender<std::io::Result<()>>>,
112}
113
114/// The sending side of a connection's writer task, plus the slow-consumer accounting.
115#[derive(Clone)]
116pub struct Outbox {
117    tx: mpsc::UnboundedSender<OutFrame>,
118    queued: Arc<AtomicUsize>,
119    limit: usize,
120    killed: Arc<watch::Sender<bool>>,
121}
122
123/// What the writer task needs: the receiving side and the shared counter.
124pub struct OutboxReceiver {
125    pub rx: mpsc::UnboundedReceiver<OutFrame>,
126    pub queued: Arc<AtomicUsize>,
127}
128
129impl Outbox {
130    pub fn new() -> (Outbox, OutboxReceiver) {
131        Self::with_limit(SUBSCRIBER_OUTPUT_LIMIT_BYTES)
132    }
133
134    pub fn with_limit(limit: usize) -> (Outbox, OutboxReceiver) {
135        let (tx, rx) = mpsc::unbounded_channel();
136        let queued = Arc::new(AtomicUsize::new(0));
137        let (killed, _) = watch::channel(false);
138        (
139            Outbox {
140                tx,
141                queued: queued.clone(),
142                limit,
143                killed: Arc::new(killed),
144            },
145            OutboxReceiver { rx, queued },
146        )
147    }
148
149    /// Resolves once the connection has been marked for closing — a slow consumer, or a write
150    /// that failed. The read loop selects on it, and so does every wait for a reply to be
151    /// written, so a peer that has stopped reading cannot park either of them.
152    pub async fn closing(&self) {
153        let mut rx = self.killed.subscribe();
154        let _ = rx.wait_for(|k| *k).await;
155    }
156
157    pub fn is_closing(&self) -> bool {
158        *self.killed.borrow()
159    }
160
161    /// Queue a reply and wait until it is on the socket. The read loop's only write path, so a
162    /// peer that stops reading still stalls its own command stream exactly as a direct write
163    /// would have.
164    pub async fn send_reply(&self, bytes: Vec<u8>) -> std::io::Result<()> {
165        let (ack, done) = oneshot::channel();
166        self.tx
167            .send(OutFrame {
168                bytes: Bytes::from(bytes),
169                counted: false,
170                ack: Some(ack),
171            })
172            .map_err(|_| std::io::Error::other("connection writer has stopped"))?;
173        tokio::select! {
174            written = done => written
175                .unwrap_or_else(|_| Err(std::io::Error::other("connection writer has stopped"))),
176            _ = self.closing() => Err(std::io::Error::other("connection is closing")),
177        }
178    }
179
180    /// Queue a frame without waiting: subscription confirmations, which must keep their place
181    /// relative to messages, and are queued under the hub lock.
182    fn queue_reply(&self, bytes: Bytes) {
183        let _ = self.tx.send(OutFrame {
184            bytes,
185            counted: false,
186            ack: None,
187        });
188    }
189
190    /// Queue a delivered message, unless that would take this subscriber past its output
191    /// limit, in which case the subscriber is marked for disconnection.
192    fn deliver(&self, bytes: Bytes) -> Delivery {
193        if self.is_closing() {
194            return Delivery::Gone;
195        }
196        let len = bytes.len();
197        let before = self.queued.fetch_add(len, Ordering::AcqRel);
198        if before + len > self.limit {
199            self.queued.fetch_sub(len, Ordering::AcqRel);
200            self.mark_killed();
201            return Delivery::Overflow;
202        }
203        if self
204            .tx
205            .send(OutFrame {
206                bytes,
207                counted: true,
208                ack: None,
209            })
210            .is_err()
211        {
212            self.queued.fetch_sub(len, Ordering::AcqRel);
213            return Delivery::Gone;
214        }
215        Delivery::Queued
216    }
217
218    /// Mark the connection for closing and wake its read loop.
219    pub fn mark_killed(&self) {
220        self.killed.send_replace(true);
221    }
222
223    /// Bytes of delivered messages waiting to be written.
224    pub fn queued_bytes(&self) -> usize {
225        self.queued.load(Ordering::Acquire)
226    }
227}
228
229/// What happened to one delivery.
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231enum Delivery {
232    Queued,
233    /// Past the output limit: the subscriber is being disconnected.
234    Overflow,
235    /// The connection is already closing.
236    Gone,
237}
238
239// ============================================================================
240// The hub
241// ============================================================================
242
243struct Subscriber {
244    outbox: Outbox,
245    /// The connection's RESP version, read at delivery time: a message is a push on RESP3 and
246    /// an array on RESP2.
247    proto: Arc<AtomicU8>,
248    channels: BTreeSet<Vec<u8>>,
249    patterns: BTreeSet<Vec<u8>>,
250    shard: BTreeSet<Vec<u8>>,
251}
252
253impl Subscriber {
254    fn set(&self, kind: Kind) -> &BTreeSet<Vec<u8>> {
255        match kind {
256            Kind::Channel => &self.channels,
257            Kind::Pattern => &self.patterns,
258            Kind::Shard => &self.shard,
259        }
260    }
261    fn set_mut(&mut self, kind: Kind) -> &mut BTreeSet<Vec<u8>> {
262        match kind {
263            Kind::Channel => &mut self.channels,
264            Kind::Pattern => &mut self.patterns,
265            Kind::Shard => &mut self.shard,
266        }
267    }
268    fn total(&self) -> usize {
269        self.channels.len() + self.patterns.len() + self.shard.len()
270    }
271    /// The count Redis reports in a confirmation: channels plus patterns, or shard channels
272    /// alone for the `s` family.
273    fn reported(&self, kind: Kind) -> i64 {
274        match kind {
275            Kind::Shard => self.shard.len() as i64,
276            _ => (self.channels.len() + self.patterns.len()) as i64,
277        }
278    }
279}
280
281#[derive(Default)]
282struct HubInner {
283    subscribers: HashMap<u32, Subscriber>,
284    channels: HashMap<Vec<u8>, BTreeSet<u32>>,
285    patterns: HashMap<Vec<u8>, BTreeSet<u32>>,
286    shard: HashMap<Vec<u8>, BTreeSet<u32>>,
287    total: usize,
288}
289
290impl HubInner {
291    fn index_mut(&mut self, kind: Kind) -> &mut HashMap<Vec<u8>, BTreeSet<u32>> {
292        match kind {
293            Kind::Channel => &mut self.channels,
294            Kind::Pattern => &mut self.patterns,
295            Kind::Shard => &mut self.shard,
296        }
297    }
298    fn index(&self, kind: Kind) -> &HashMap<Vec<u8>, BTreeSet<u32>> {
299        match kind {
300            Kind::Channel => &self.channels,
301            Kind::Pattern => &self.patterns,
302            Kind::Shard => &self.shard,
303        }
304    }
305
306    fn drop_subscriber(&mut self, conn: u32) {
307        let Some(sub) = self.subscribers.remove(&conn) else {
308            return;
309        };
310        for kind in [Kind::Channel, Kind::Pattern, Kind::Shard] {
311            for name in sub.set(kind) {
312                let index = self.index_mut(kind);
313                if let Some(set) = index.get_mut(name) {
314                    set.remove(&conn);
315                    if set.is_empty() {
316                        index.remove(name);
317                    }
318                }
319            }
320        }
321        self.total -= sub.total();
322    }
323}
324
325/// The subscription registry of one server.
326#[derive(Default)]
327pub struct PubSubHub {
328    inner: Mutex<HubInner>,
329}
330
331/// A message frame for one subscriber's protocol version.
332fn message_frame(
333    kind: Kind,
334    pattern: Option<&[u8]>,
335    channel: &[u8],
336    message: &[u8],
337    proto: u8,
338) -> Bytes {
339    let mut items = vec![Resp::bulk(match (kind, pattern) {
340        (Kind::Shard, _) => "smessage",
341        (_, Some(_)) => "pmessage",
342        _ => "message",
343    })];
344    if let Some(p) = pattern {
345        items.push(Resp::bulk(p));
346    }
347    items.push(Resp::bulk(channel));
348    items.push(Resp::bulk(message));
349    Bytes::from(Resp::Push(items).encode(proto))
350}
351
352fn confirmation(word: &str, name: Option<&[u8]>, count: i64, proto: u8) -> Bytes {
353    Bytes::from(
354        Resp::Push(vec![
355            Resp::bulk(word),
356            name.map(Resp::bulk).unwrap_or(Resp::Null),
357            Resp::Int(count),
358        ])
359        .encode(proto),
360    )
361}
362
363/// Check one name against its length bound. `Err` is the RESP error to send.
364pub fn check_name(kind: Kind, name: &[u8]) -> Result<(), String> {
365    if name.len() > kind.max_name_bytes() {
366        let what = if kind == Kind::Pattern {
367            "pattern"
368        } else {
369            "channel name"
370        };
371        return Err(format!(
372            "ERR {what} longer than {} bytes",
373            kind.max_name_bytes()
374        ));
375    }
376    Ok(())
377}
378
379/// Check a message against [`MAX_MESSAGE_BYTES`].
380pub fn check_message(message: &[u8]) -> Result<(), String> {
381    if message.len() > MAX_MESSAGE_BYTES {
382        return Err(format!("ERR message longer than {MAX_MESSAGE_BYTES} bytes"));
383    }
384    Ok(())
385}
386
387impl PubSubHub {
388    fn lock(&self) -> std::sync::MutexGuard<'_, HubInner> {
389        self.inner
390            .lock()
391            .unwrap_or_else(|poisoned| poisoned.into_inner())
392    }
393
394    /// Whether `names` could be subscribed by `conn` without breaking a bound. Called before
395    /// the model is asked, so a request that can never be honoured costs no model call, and
396    /// again inside [`Self::subscribe`] because the hub may have changed while it was asked.
397    pub fn check_subscribe(&self, conn: u32, kind: Kind, names: &[Vec<u8>]) -> Result<(), String> {
398        let inner = self.lock();
399        Self::check_locked(&inner, conn, kind, names)
400    }
401
402    fn check_locked(
403        inner: &HubInner,
404        conn: u32,
405        kind: Kind,
406        names: &[Vec<u8>],
407    ) -> Result<(), String> {
408        for name in names {
409            check_name(kind, name)?;
410        }
411        let existing = inner.subscribers.get(&conn);
412        let new: HashSet<&Vec<u8>> = names
413            .iter()
414            .filter(|n| existing.is_none_or(|s| !s.set(kind).contains(*n)))
415            .collect();
416        let held = existing.map(Subscriber::total).unwrap_or(0);
417        if held + new.len() > MAX_SUBSCRIPTIONS_PER_CONNECTION {
418            return Err(format!(
419                "ERR max subscriptions per connection reached ({MAX_SUBSCRIPTIONS_PER_CONNECTION})"
420            ));
421        }
422        if inner.total + new.len() > MAX_SUBSCRIPTIONS_PER_SERVER {
423            return Err(format!(
424                "ERR max subscriptions per server reached ({MAX_SUBSCRIPTIONS_PER_SERVER})"
425            ));
426        }
427        if kind == Kind::Pattern {
428            let distinct_new = new
429                .iter()
430                .filter(|p| !inner.patterns.contains_key(**p))
431                .count();
432            if inner.patterns.len() + distinct_new > MAX_PATTERNS_PER_SERVER {
433                return Err(format!(
434                    "ERR max distinct patterns per server reached ({MAX_PATTERNS_PER_SERVER})"
435                ));
436            }
437        }
438        Ok(())
439    }
440
441    /// Subscribe `conn` to `names` and queue one confirmation per name on its outbox, in
442    /// order, under the hub lock. All or nothing: a bound breached by any name refuses the
443    /// whole command, and `Err` is the RESP error to send.
444    pub fn subscribe(
445        &self,
446        conn: u32,
447        outbox: &Outbox,
448        proto: &Arc<AtomicU8>,
449        kind: Kind,
450        names: &[Vec<u8>],
451    ) -> Result<(), String> {
452        let mut inner = self.lock();
453        Self::check_locked(&inner, conn, kind, names)?;
454        let version = proto.load(Ordering::Relaxed);
455        inner.subscribers.entry(conn).or_insert_with(|| Subscriber {
456            outbox: outbox.clone(),
457            proto: proto.clone(),
458            channels: BTreeSet::new(),
459            patterns: BTreeSet::new(),
460            shard: BTreeSet::new(),
461        });
462        for name in names {
463            let added = inner
464                .subscribers
465                .get_mut(&conn)
466                .map(|s| s.set_mut(kind).insert(name.clone()))
467                .unwrap_or(false);
468            if added {
469                inner.total += 1;
470                inner
471                    .index_mut(kind)
472                    .entry(name.clone())
473                    .or_default()
474                    .insert(conn);
475            }
476            let count = inner
477                .subscribers
478                .get(&conn)
479                .map(|s| s.reported(kind))
480                .unwrap_or(0);
481            outbox.queue_reply(confirmation(
482                kind.subscribe_word(),
483                Some(name),
484                count,
485                version,
486            ));
487        }
488        Ok(())
489    }
490
491    /// Unsubscribe `conn` from `names`, or from everything of `kind` when `names` is empty,
492    /// queuing one confirmation per name. With nothing to unsubscribe from, Redis still
493    /// answers once, with a null name and the current count; so does this.
494    pub fn unsubscribe(
495        &self,
496        conn: u32,
497        outbox: &Outbox,
498        proto: u8,
499        kind: Kind,
500        names: &[Vec<u8>],
501    ) {
502        let mut inner = self.lock();
503        let targets: Vec<Vec<u8>> = if names.is_empty() {
504            inner
505                .subscribers
506                .get(&conn)
507                .map(|s| s.set(kind).iter().cloned().collect())
508                .unwrap_or_default()
509        } else {
510            names.to_vec()
511        };
512        if targets.is_empty() {
513            let count = inner
514                .subscribers
515                .get(&conn)
516                .map(|s| s.reported(kind))
517                .unwrap_or(0);
518            outbox.queue_reply(confirmation(kind.unsubscribe_word(), None, count, proto));
519            return;
520        }
521        for name in targets {
522            let removed = inner
523                .subscribers
524                .get_mut(&conn)
525                .map(|s| s.set_mut(kind).remove(&name))
526                .unwrap_or(false);
527            if removed {
528                inner.total -= 1;
529                let index = inner.index_mut(kind);
530                if let Some(set) = index.get_mut(&name) {
531                    set.remove(&conn);
532                    if set.is_empty() {
533                        index.remove(&name);
534                    }
535                }
536            }
537            let count = inner
538                .subscribers
539                .get(&conn)
540                .map(|s| s.reported(kind))
541                .unwrap_or(0);
542            outbox.queue_reply(confirmation(
543                kind.unsubscribe_word(),
544                Some(&name),
545                count,
546                proto,
547            ));
548        }
549        if inner.subscribers.get(&conn).is_some_and(|s| s.total() == 0) {
550            inner.subscribers.remove(&conn);
551        }
552    }
553
554    /// Forget every subscription `conn` holds, without confirmations: the connection closed,
555    /// or sent `RESET`.
556    pub fn remove_connection(&self, conn: u32) {
557        self.lock().drop_subscriber(conn);
558    }
559
560    /// Subscriptions `conn` holds (channels + patterns + shard channels). Above zero, a RESP2
561    /// connection is in subscribed mode.
562    pub fn subscription_count(&self, conn: u32) -> usize {
563        self.lock()
564            .subscribers
565            .get(&conn)
566            .map(Subscriber::total)
567            .unwrap_or(0)
568    }
569
570    /// How many deliveries a publish would make right now, for the model's event.
571    pub fn receivers(&self, channel: &[u8], shard: bool) -> usize {
572        let inner = self.lock();
573        if shard {
574            return inner.shard.get(channel).map(BTreeSet::len).unwrap_or(0);
575        }
576        inner.channels.get(channel).map(BTreeSet::len).unwrap_or(0)
577            + inner
578                .patterns
579                .iter()
580                .filter(|(p, _)| glob_match(p, channel))
581                .map(|(_, subs)| subs.len())
582                .sum::<usize>()
583    }
584
585    /// Deliver `message` on `channel` to every matching subscription and return how many
586    /// deliveries were queued — Redis's `PUBLISH` reply, where a connection subscribed to the
587    /// channel and to a matching pattern receives (and counts) twice.
588    ///
589    /// A subscriber the delivery would take past [`SUBSCRIBER_OUTPUT_LIMIT_BYTES`] is not
590    /// counted, is dropped from the hub, and its connection is woken to close.
591    pub fn publish(&self, channel: &[u8], message: &[u8], shard: bool) -> usize {
592        let mut inner = self.lock();
593        let mut delivered = 0;
594        let mut slow: Vec<u32> = Vec::new();
595        let mut gone: Vec<u32> = Vec::new();
596        // Each frame shape is encoded once per protocol version, not once per subscriber.
597        let mut cache: HashMap<(Option<Vec<u8>>, u8), Bytes> = HashMap::new();
598        let kind = if shard { Kind::Shard } else { Kind::Channel };
599        let mut targets: Vec<(u32, Option<Vec<u8>>)> = inner
600            .index(kind)
601            .get(channel)
602            .map(|subs| subs.iter().map(|c| (*c, None)).collect())
603            .unwrap_or_default();
604        if !shard {
605            let mut matched: Vec<(&Vec<u8>, &BTreeSet<u32>)> = inner
606                .patterns
607                .iter()
608                .filter(|(p, _)| glob_match(p, channel))
609                .collect();
610            matched.sort_by(|a, b| a.0.cmp(b.0));
611            for (pattern, subs) in matched {
612                targets.extend(subs.iter().map(|c| (*c, Some(pattern.clone()))));
613            }
614        }
615        for (conn, pattern) in targets {
616            let Some(sub) = inner.subscribers.get(&conn) else {
617                continue;
618            };
619            let proto = sub.proto.load(Ordering::Relaxed);
620            let frame = cache
621                .entry((pattern.clone(), proto))
622                .or_insert_with(|| message_frame(kind, pattern.as_deref(), channel, message, proto))
623                .clone();
624            match sub.outbox.deliver(frame) {
625                Delivery::Queued => delivered += 1,
626                Delivery::Overflow if !slow.contains(&conn) => slow.push(conn),
627                Delivery::Gone if !gone.contains(&conn) => gone.push(conn),
628                _ => {}
629            }
630        }
631        for conn in gone {
632            inner.drop_subscriber(conn);
633        }
634        for conn in slow {
635            warn!(
636                "Redis connection {} decision=slow_consumer_disconnected: more than {} bytes of \
637                 pub/sub messages queued unread; dropping its subscriptions and closing it \
638                 (Redis's client-output-buffer-limit pubsub)",
639                conn, SUBSCRIBER_OUTPUT_LIMIT_BYTES
640            );
641            inner.drop_subscriber(conn);
642        }
643        delivered
644    }
645
646    /// `PUBSUB CHANNELS` / `SHARDCHANNELS`: active channels, optionally filtered by a pattern.
647    pub fn active_channels(&self, shard: bool, pattern: Option<&[u8]>) -> Vec<Vec<u8>> {
648        let inner = self.lock();
649        let index = if shard { &inner.shard } else { &inner.channels };
650        let mut names: Vec<Vec<u8>> = index
651            .keys()
652            .filter(|c| pattern.is_none_or(|p| glob_match(p, c)))
653            .cloned()
654            .collect();
655        names.sort();
656        names
657    }
658
659    /// `PUBSUB NUMSUB` / `SHARDNUMSUB`: subscribers of each named channel.
660    pub fn numsub(&self, shard: bool, channels: &[Vec<u8>]) -> Vec<(Vec<u8>, usize)> {
661        let inner = self.lock();
662        let index = if shard { &inner.shard } else { &inner.channels };
663        channels
664            .iter()
665            .map(|c| (c.clone(), index.get(c).map(BTreeSet::len).unwrap_or(0)))
666            .collect()
667    }
668
669    /// `PUBSUB NUMPAT`: distinct patterns subscribed across the server.
670    pub fn numpat(&self) -> usize {
671        self.lock().patterns.len()
672    }
673
674    /// Subscriptions across the server, for `list_redis_subscriptions`.
675    pub fn snapshot(&self) -> serde_json::Value {
676        let inner = self.lock();
677        let names = |index: &HashMap<Vec<u8>, BTreeSet<u32>>| {
678            let mut v: Vec<serde_json::Value> = index
679                .iter()
680                .map(|(name, subs)| {
681                    serde_json::json!({
682                        "name": String::from_utf8_lossy(name),
683                        "subscribers": subs.len(),
684                    })
685                })
686                .collect();
687            v.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
688            v
689        };
690        serde_json::json!({
691            "channels": names(&inner.channels),
692            "patterns": names(&inner.patterns),
693            "shard_channels": names(&inner.shard),
694            "subscribed_connections": inner.subscribers.len(),
695            "total_subscriptions": inner.total,
696        })
697    }
698}