Skip to main content

ruststream_fred/
pubsub.rs

1//! Redis Pub/Sub transport: fire-and-forget fan-out with no acknowledgement.
2//!
3//! Unlike Streams, Pub/Sub has no durability, no consumer groups, and no ack: a message reaches
4//! whichever subscribers are connected at publish time, and `ack` / `nack` report
5//! [`AckError::Unsupported`]. Two delivery modes exist, explicit because they do not interoperate:
6//!
7//! * [`PubSubMode::Classic`] - `SUBSCRIBE` / `PUBLISH`, broadcast to every node; supports patterns
8//!   (`PSUBSCRIBE`). The only option on standalone and sentinel.
9//! * [`PubSubMode::Sharded`] - `SSUBSCRIBE` / `SPUBLISH` (Redis 7+), slot-local so it scales across
10//!   a cluster, but has no pattern support.
11//!
12//! A channel and a pattern are two descriptors, [`RedisPubSub`] and [`RedisPubSubPattern`],
13//! because they differ in more than a flag: a pattern reads many channels and names none, so
14//! sharded delivery has no meaning there and neither does a destination a retry copy could go to.
15//!
16//! Headers travel in a frame around the payload: a binary frame by default, or a readable
17//! codec-serialized envelope when a codec is set with [`RedisPubSub::codec`] /
18//! [`RedisPubSubPublish::codec`]. Both framings are lossless; the envelope writes a field whose
19//! bytes are valid UTF-8 as text and any other bytes as themselves.
20
21use std::fmt::{Debug, Formatter};
22use std::future::{Future, ready};
23use std::num::NonZeroUsize;
24use std::sync::Arc;
25
26use bytes::Bytes;
27use fred::clients::{Client, Pool};
28use fred::interfaces::{ClientLike, PubsubInterface};
29use fred::types::{Message, MessageKind};
30use futures::Stream;
31use futures::stream::unfold;
32#[cfg(feature = "asyncapi")]
33use ruststream::asyncapi::Bindings;
34use ruststream::codec::Codec;
35use ruststream::{
36    AckError, AddressedCopies, BatchSubscriber, BufferedSubscriber, HeaderMap, IncomingMessage,
37    Lend, NamedCopies, OutgoingMessage, PairError, Partitioned, PublishPolicy, Publisher,
38    RedeliveryAddress, RedeliveryAddressed, RetryDeclaration, SubscriptionSource,
39};
40use tokio::runtime::Handle;
41use tokio::sync::broadcast::Receiver;
42use tokio::sync::broadcast::error::{RecvError, TryRecvError};
43use tracing::warn;
44
45use crate::broker::{ConnectedRedisBroker, RedisCore};
46use crate::envelope::{SharedEnvelope, frame, unframe};
47use crate::loopback::{InFlight, Tap};
48use crate::partition::{RedisPublishOptions, resolved_headers};
49use crate::pipeline::{PubSubForm, RoundMessage, Window};
50use crate::publisher::joins_round;
51use crate::route::Route;
52use crate::{error::RedisError, message::PARTITION_KEY_HEADER};
53
54/// This form's publish policy, [`RedisPubSubPublish`], under the mount-site name every form gives
55/// its own. Its options, the delivery mode and the framing codec, still chain off it.
56pub use crate::pubsub::RedisPubSubPublish as Publish;
57
58/// The core prelude plus everything a Redis Pub/Sub service writes.
59///
60/// The broker, the [`RedisPubSub`] descriptor with its [`PubSubMode`], this form's [`Publish`]
61/// policy, and its per-delivery context. Pub/Sub carries no core capability traits.
62///
63/// # Examples
64///
65/// ```
66/// # mod demo {
67/// use ruststream_fred::pubsub::prelude::*;
68/// use serde::{Deserialize, Serialize};
69///
70/// #[derive(Deserialize)]
71/// struct Event {
72///     kind: String,
73/// }
74///
75/// #[derive(Serialize, Outgoing)]
76/// #[outgoing(name = "events.seen")]
77/// struct Seen {
78///     kind: String,
79/// }
80///
81/// #[subscriber(RedisPubSub::new("events").mode(PubSubMode::Sharded), reply)]
82/// async fn on_event(event: &Event) -> Seen {
83///     Seen {
84///         kind: event.kind.clone(),
85///     }
86/// }
87///
88/// #[ruststream::app]
89/// fn app() -> impl App {
90///     RustStream::new(AppInfo::new("events", "0.1.0")).with_broker(
91///         RedisBroker::cluster(["10.0.0.1:6379"]),
92///         |b| {
93///             // Sharded in, sharded out: an `SPUBLISH` reaches only sharded subscribers.
94///             b.include(on_event)
95///                 .out_reply(Publish::new().mode(PubSubMode::Sharded));
96///         },
97///     )
98/// }
99/// # }
100/// # fn main() {}
101/// ```
102///
103/// Two vocabularies that do not mix. A handler body imports `ruststream::prelude::*` and bounds an
104/// injected slot with the broker capability trait it needs (`Out<impl Publisher>`); a routes file
105/// globs this prelude and names the policy by its mount-site word, the same word on every form.
106///
107/// A file that also globs another form's prelude sees an ambiguous `Publish`; use
108/// [`crate::prelude`] and write `pubsub::Publish` there.
109pub mod prelude {
110    pub use ruststream::prelude::*;
111
112    pub use super::{PubSubMode, Publish, RedisPubSub, RedisPubSubPattern};
113    // `keys` arrives as the module, not as a glob: its members are short words a service also uses
114    // for its own types, and `Ctx<keys::Channel>` reads as what it is at the use site.
115    pub use crate::context::{PipelineContext, PoolContext, PubSubContext, keys};
116    pub use crate::pipeline::{Bindable, InRound, RedisPipelineSteps};
117    pub use crate::{PARTITION_KEY_HEADER, RedisBroker, RedisPublishOptions, RedisPublishSteps};
118
119    #[cfg(any(
120        feature = "tls-rustls",
121        feature = "tls-rustls-ring",
122        feature = "tls-native-tls"
123    ))]
124    pub use crate::{TlsConfig, TlsConnector};
125}
126
127/// Pub/Sub delivery mode. Defaults to [`Classic`](Self::Classic).
128#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
129pub enum PubSubMode {
130    /// `SUBSCRIBE` / `PUBLISH`: cluster-wide broadcast, pattern-capable, does not scale by slot.
131    #[default]
132    Classic,
133    /// `SSUBSCRIBE` / `SPUBLISH` (Redis 7+): slot-local sharded delivery, no patterns.
134    Sharded,
135}
136
137impl PubSubMode {
138    /// The word the generated `AsyncAPI` document names this mode by.
139    #[cfg(feature = "asyncapi")]
140    pub(crate) const fn as_str(self) -> &'static str {
141        match self {
142            Self::Classic => "classic",
143            Self::Sharded => "sharded",
144        }
145    }
146}
147
148/// Describes one Pub/Sub subscription on a single channel, against a [`ConnectedRedisBroker`].
149///
150/// A glob is [`RedisPubSubPattern`] instead.
151///
152/// # Examples
153///
154/// ```
155/// # mod demo {
156/// use ruststream_fred::pubsub::prelude::*;
157/// use serde::Deserialize;
158///
159/// #[derive(Deserialize)]
160/// struct Event {
161///     kind: String,
162/// }
163///
164/// // Classic: `SUBSCRIBE`, broadcast to every node of a cluster.
165/// #[subscriber(RedisPubSub::new("events"))]
166/// async fn on_event(event: &Event) -> HandlerOutcome {
167///     println!("event: {}", event.kind);
168///     HandlerOutcome::ack()
169/// }
170///
171/// // Sharded: `SSUBSCRIBE`, served by the node that owns the channel's slot.
172/// #[subscriber(RedisPubSub::new("metrics").mode(PubSubMode::Sharded))]
173/// async fn on_metric(event: &Event) -> HandlerOutcome {
174///     println!("metric: {}", event.kind);
175///     HandlerOutcome::ack()
176/// }
177///
178/// #[ruststream::app]
179/// fn app() -> impl App {
180///     RustStream::new(AppInfo::new("events", "0.1.0")).with_broker(
181///         RedisBroker::cluster(["10.0.0.1:6379"]),
182///         |b| {
183///             b.include(on_event);
184///             b.include(on_metric);
185///         },
186///     )
187/// }
188/// # }
189/// # fn main() {}
190/// ```
191#[derive(Clone)]
192#[must_use]
193pub struct RedisPubSub {
194    channel: String,
195    mode: PubSubMode,
196    codec: Option<SharedEnvelope>,
197    /// Where the mount site sends a spent delivery, taken from its retry declaration when the
198    /// subscription opens, so the broker publishes to that name as the channel it is.
199    dead_letter: Option<String>,
200    /// `None` keeps the capacity `fred` gives a client's broadcast channels by default.
201    buffer: Option<NonZeroUsize>,
202}
203
204impl Debug for RedisPubSub {
205    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
206        f.debug_struct("RedisPubSub")
207            .field("channel", &self.channel)
208            .field("mode", &self.mode)
209            .field("codec", &self.codec.is_some())
210            .field("dead_letter", &self.dead_letter)
211            .field("buffer", &self.buffer)
212            .finish()
213    }
214}
215
216impl RedisPubSub {
217    /// A subscription on the exact `channel`.
218    pub fn new(channel: impl Into<String>) -> Self {
219        Self {
220            channel: channel.into(),
221            mode: PubSubMode::default(),
222            codec: None,
223            dead_letter: None,
224            buffer: None,
225        }
226    }
227
228    /// Sets the delivery mode. Defaults to [`PubSubMode::Classic`].
229    pub const fn mode(mut self, mode: PubSubMode) -> Self {
230        self.mode = mode;
231        self
232    }
233
234    /// Decodes the header/payload envelope with `codec` (must match the publisher). Without it the
235    /// default binary framing is used. Either way the payload arrives as it was published.
236    pub fn codec(mut self, codec: impl Codec + 'static) -> Self {
237        self.codec = Some(Arc::new(codec));
238        self
239    }
240
241    /// Sets how many messages the subscription holds that its handler has not taken yet.
242    ///
243    /// Redis pushes a Pub/Sub message the moment it is published, and the subscription keeps it
244    /// until the handler asks for the next one. The buffer bounds how many may wait. Once it is
245    /// full, each new message drops the oldest, and the subscription logs a warning with the
246    /// channel and the count it lost. The default is `fred`'s own
247    /// (`PerformanceConfig::broadcast_channel_capacity`, 32 messages in `fred` 10.1).
248    ///
249    /// Raise it for a handler that falls behind bursts: set it to the largest burst the handler
250    /// must absorb. The room is reserved when the subscription opens, about 600 bytes per message
251    /// of buffer whatever the payloads weigh, because `fred` sizes all eight of a client's
252    /// notification channels by it.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// # mod demo {
258    /// use ruststream::nonzero;
259    /// use ruststream_fred::pubsub::prelude::*;
260    /// # #[derive(serde::Deserialize)]
261    /// # struct Event { kind: String }
262    ///
263    /// // A handler that falls behind a burst of up to ten thousand events loses none of them.
264    /// #[subscriber(RedisPubSub::new("events").buffer(nonzero!(10_000)))]
265    /// async fn on_event(event: &Event) -> HandlerOutcome {
266    ///     println!("event {}", event.kind);
267    ///     HandlerOutcome::ack()
268    /// }
269    /// #
270    /// # fn app() -> RustStream {
271    /// #     RustStream::new(AppInfo::new("events", "0.1.0")).with_broker(
272    /// #         RedisBroker::standalone("redis://localhost:6379"),
273    /// #         |b| {
274    /// #             b.include(on_event);
275    /// #         },
276    /// #     )
277    /// # }
278    /// # }
279    /// ```
280    pub const fn buffer(mut self, messages: NonZeroUsize) -> Self {
281        self.buffer = Some(messages);
282        self
283    }
284
285    /// The channel this subscription listens on.
286    #[must_use]
287    pub fn channel(&self) -> &str {
288        &self.channel
289    }
290
291    pub(crate) const fn buffer_size(&self) -> Option<NonZeroUsize> {
292        self.buffer
293    }
294
295    pub(crate) const fn delivery_mode(&self) -> PubSubMode {
296        self.mode
297    }
298
299    pub(crate) fn codec_handle(&self) -> Option<SharedEnvelope> {
300        self.codec.clone()
301    }
302
303    /// How a name this subscription reads or dead-letters to is written: a publish in its mode
304    /// and its framing.
305    pub(crate) fn route(&self) -> Route {
306        Route::Channel {
307            mode: self.mode,
308            envelope: self.codec.clone(),
309        }
310    }
311
312    /// The dead-letter destination the mount site declared, once the registration has handed its
313    /// declaration over.
314    pub(crate) fn dead_letter(&self) -> Option<&str> {
315        self.dead_letter.as_deref()
316    }
317
318    /// Takes the dead-letter destination out of the mount site's declaration.
319    fn with_declaration(mut self, declaration: &RetryDeclaration) -> Self {
320        self.dead_letter = declaration.dead_letter().map(str::to_owned);
321        self
322    }
323
324    /// Where a publisher reaches this subscription again, shared by both broker forms.
325    fn redelivery_channel(&self) -> RedeliveryAddress {
326        RedeliveryAddress::new(self.channel.clone())
327    }
328
329    /// What this subscription adds to its channel in the generated `AsyncAPI` document, shared by
330    /// both broker forms: the delivery mode it subscribes in and how headers are framed.
331    #[cfg(feature = "asyncapi")]
332    fn describe(&self) -> Bindings {
333        crate::asyncapi::channel(&crate::asyncapi::PubSubSubscription::new(
334            self.mode.as_str(),
335            false,
336            crate::asyncapi::Envelope::of(self.codec.as_ref()),
337        ))
338    }
339}
340
341/// Describes one Pub/Sub subscription on a glob pattern (`PSUBSCRIBE`), against a
342/// [`ConnectedRedisBroker`].
343///
344/// Patterns are classic-only: sharded Pub/Sub routes by slot and has no `PSUBSCRIBE`, so this
345/// descriptor carries no mode to get wrong.
346///
347/// # Examples
348///
349/// ```
350/// # mod demo {
351/// use ruststream_fred::pubsub::prelude::*;
352/// use serde::Deserialize;
353///
354/// #[derive(Deserialize)]
355/// struct Event {
356///     kind: String,
357/// }
358///
359/// #[subscriber(RedisPubSubPattern::new("events.*"))]
360/// async fn on_any_event(event: &Event, Ctx(channel): Ctx<keys::Channel>) -> HandlerOutcome {
361///     println!("{} on {channel}", event.kind);
362///     HandlerOutcome::ack()
363/// }
364///
365/// #[ruststream::app]
366/// fn app() -> impl App {
367///     RustStream::new(AppInfo::new("events", "0.1.0")).with_broker(
368///         RedisBroker::standalone("redis://localhost:6379"),
369///         |b| {
370///             // A glob is no channel a `PUBLISH` can name, so the mount site says where a
371///             // retry copy goes.
372///             b.include(on_any_event)
373///                 .out_retry(Publish::default())
374///                 .to("events.retry");
375///         },
376///     )
377/// }
378/// # }
379/// # fn main() {}
380/// ```
381///
382/// Sharded Pub/Sub has no pattern subscription, so a pattern takes no mode:
383///
384/// ```compile_fail
385/// use ruststream_fred::{PubSubMode, RedisPubSubPattern};
386///
387/// let events = RedisPubSubPattern::new("events.*").mode(PubSubMode::Sharded);
388/// ```
389#[derive(Clone)]
390#[must_use]
391pub struct RedisPubSubPattern {
392    pattern: String,
393    codec: Option<SharedEnvelope>,
394    /// `None` keeps the capacity `fred` gives a client's broadcast channels by default.
395    buffer: Option<NonZeroUsize>,
396}
397
398impl Debug for RedisPubSubPattern {
399    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
400        f.debug_struct("RedisPubSubPattern")
401            .field("pattern", &self.pattern)
402            .field("codec", &self.codec.is_some())
403            .field("buffer", &self.buffer)
404            .finish()
405    }
406}
407
408impl RedisPubSubPattern {
409    /// A subscription on every channel matching the glob `pattern`.
410    pub fn new(pattern: impl Into<String>) -> Self {
411        Self {
412            pattern: pattern.into(),
413            codec: None,
414            buffer: None,
415        }
416    }
417
418    /// Decodes the header/payload envelope with `codec` (must match the publisher). Without it the
419    /// default binary framing is used. Either way the payload arrives as it was published.
420    pub fn codec(mut self, codec: impl Codec + 'static) -> Self {
421        self.codec = Some(Arc::new(codec));
422        self
423    }
424
425    /// Sets how many messages the subscription holds that its handler has not taken yet.
426    ///
427    /// Redis pushes a Pub/Sub message the moment it is published, and the subscription keeps it
428    /// until the handler asks for the next one. The buffer bounds how many may wait. Once it is
429    /// full, each new message drops the oldest, and the subscription logs a warning with the
430    /// channel and the count it lost. The default is `fred`'s own
431    /// (`PerformanceConfig::broadcast_channel_capacity`, 32 messages in `fred` 10.1).
432    ///
433    /// Raise it for a handler that falls behind bursts: set it to the largest burst the handler
434    /// must absorb. The room is reserved when the subscription opens, about 600 bytes per message
435    /// of buffer whatever the payloads weigh, because `fred` sizes all eight of a client's
436    /// notification channels by it.
437    ///
438    /// # Examples
439    ///
440    /// ```
441    /// # mod demo {
442    /// use ruststream::nonzero;
443    /// use ruststream_fred::pubsub::prelude::*;
444    /// # #[derive(serde::Deserialize)]
445    /// # struct Event { kind: String }
446    ///
447    /// // A handler that falls behind a burst of up to ten thousand events loses none of them.
448    /// #[subscriber(RedisPubSubPattern::new("events.*").buffer(nonzero!(10_000)))]
449    /// async fn on_event(event: &Event) -> HandlerOutcome {
450    ///     println!("event {}", event.kind);
451    ///     HandlerOutcome::ack()
452    /// }
453    /// #
454    /// # fn app() -> RustStream {
455    /// #     RustStream::new(AppInfo::new("events", "0.1.0")).with_broker(
456    /// #         RedisBroker::standalone("redis://localhost:6379"),
457    /// #         |b| {
458    /// #             b.include(on_event);
459    /// #         },
460    /// #     )
461    /// # }
462    /// # }
463    /// ```
464    pub const fn buffer(mut self, messages: NonZeroUsize) -> Self {
465        self.buffer = Some(messages);
466        self
467    }
468
469    /// The glob this subscription matches channels against.
470    #[must_use]
471    pub fn pattern(&self) -> &str {
472        &self.pattern
473    }
474
475    pub(crate) const fn buffer_size(&self) -> Option<NonZeroUsize> {
476        self.buffer
477    }
478
479    pub(crate) fn codec_handle(&self) -> Option<SharedEnvelope> {
480        self.codec.clone()
481    }
482
483    /// Patterns are classic-only, so the mode in the document is the only one they have.
484    #[cfg(feature = "asyncapi")]
485    fn describe(&self) -> Bindings {
486        crate::asyncapi::channel(&crate::asyncapi::PubSubSubscription::new(
487            PubSubMode::Classic.as_str(),
488            true,
489            crate::asyncapi::Envelope::of(self.codec.as_ref()),
490        ))
491    }
492}
493
494impl SubscriptionSource<ConnectedRedisBroker> for RedisPubSub {
495    type Subscriber = RedisPubSubSubscriber;
496    /// Pub/Sub cannot acknowledge, so every retry is a copy this process publishes, and the
497    /// channel is where it goes.
498    type Copies = AddressedCopies;
499
500    fn name(&self) -> &str {
501        self.channel()
502    }
503
504    fn declare_retry(self, declaration: &RetryDeclaration) -> Self {
505        self.with_declaration(declaration)
506    }
507
508    async fn subscribe(
509        self,
510        connected: &ConnectedRedisBroker,
511    ) -> Result<Self::Subscriber, RedisError> {
512        connected.subscribe_pubsub(self).await
513    }
514
515    #[cfg(feature = "asyncapi")]
516    fn channel_bindings(&self) -> Bindings {
517        self.describe()
518    }
519}
520
521/// The channel itself.
522///
523/// The answer is about the address, not about the command: reaching a
524/// [`PubSubMode::Sharded`] subscription still takes a publisher in the same mode, which is the
525/// policy the mount site names with `out_retry`.
526impl RedeliveryAddressed<ConnectedRedisBroker> for RedisPubSub {
527    fn redelivery_address(
528        &self,
529        _connected: &ConnectedRedisBroker,
530    ) -> impl Future<Output = Result<RedeliveryAddress, RedisError>> {
531        ready(Ok(self.redelivery_channel()))
532    }
533}
534
535/// A pattern subscription reads every channel the glob matches, and a glob is not a channel a
536/// `PUBLISH` can name, so the mount site names where a retry copy goes: `.out_retry(policy).to(..)`
537/// for a fixed channel, or a transform that reads the delivery's own channel.
538impl SubscriptionSource<ConnectedRedisBroker> for RedisPubSubPattern {
539    type Subscriber = RedisPubSubSubscriber;
540    type Copies = NamedCopies;
541
542    fn name(&self) -> &str {
543        self.pattern()
544    }
545
546    async fn subscribe(
547        self,
548        connected: &ConnectedRedisBroker,
549    ) -> Result<Self::Subscriber, RedisError> {
550        connected.subscribe_pubsub_pattern(self).await
551    }
552
553    #[cfg(feature = "asyncapi")]
554    fn channel_bindings(&self) -> Bindings {
555        self.describe()
556    }
557}
558
559/// A Pub/Sub subscription backed by a dedicated `fred` client, so its message stream and channel
560/// state are isolated from other subscribers and from the publishing pool.
561///
562/// Pub/Sub delivers one message at a time, so the batches a [`BatchSubscriber`] hands out are
563/// assembled on the client by the core's [`BufferedSubscriber`]: it fills a batch up to the size
564/// the mount site asked for and closes a partial one on its own deadline.
565pub struct RedisPubSubSubscriber(BufferedSubscriber<PubSubWire>);
566
567impl Debug for RedisPubSubSubscriber {
568    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
569        f.debug_struct("RedisPubSubSubscriber")
570            .finish_non_exhaustive()
571    }
572}
573
574impl RedisPubSubSubscriber {
575    pub(crate) fn new(wire: PubSubWire) -> Self {
576        Self(BufferedSubscriber::new(wire))
577    }
578}
579
580impl ruststream::Subscriber for RedisPubSubSubscriber {
581    type Message = RedisPubSubMessage;
582    type Error = RedisError;
583
584    /// Yields one message per Pub/Sub delivery.
585    ///
586    /// # Cancel safety
587    ///
588    /// Dropping the returned stream between items is safe. What arrives while no stream is polling
589    /// waits in the subscription's [buffer](crate::RedisPubSub::buffer); past it the oldest is
590    /// lost, and while the subscription is disconnected everything is, as Redis Pub/Sub keeps
591    /// nothing for a subscriber that is not there.
592    fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
593        self.0.stream()
594    }
595}
596
597impl BatchSubscriber for RedisPubSubSubscriber {
598    type Batch = Vec<RedisPubSubMessage>;
599
600    /// Yields batches of at most `size` deliveries, assembled as they arrive.
601    ///
602    /// # Cancel safety
603    ///
604    /// Same as [`Subscriber::stream`](ruststream::Subscriber::stream). A batch abandoned mid-fill
605    /// loses the deliveries it holds, as any Pub/Sub delivery nobody is polling for is lost.
606    fn batches(
607        &mut self,
608        size: NonZeroUsize,
609    ) -> impl Stream<Item = Result<Self::Batch, Self::Error>> + Send + '_ {
610        self.0.batches(size)
611    }
612}
613
614/// The wire side of a Pub/Sub subscription: the dedicated client and the channel it feeds.
615pub(crate) struct PubSubWire {
616    /// The channel or the glob it reads, for the warning a lost message raises.
617    name: String,
618    client: Client,
619    rx: Receiver<Message>,
620    codec: Option<SharedEnvelope>,
621    /// The broker's pool, handed to every delivery for `Ctx<keys::FredPool>`.
622    pool: Pool,
623    /// The subscription's registration with the in-process server; empty on a real connection.
624    tap: Tap,
625    /// The broker's runtime, where the dedicated client is closed once the subscription stops.
626    runtime: Handle,
627}
628
629impl Debug for PubSubWire {
630    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
631        f.debug_struct("PubSubWire").finish_non_exhaustive()
632    }
633}
634
635impl PubSubWire {
636    pub(crate) fn new(
637        name: String,
638        client: Client,
639        rx: Receiver<Message>,
640        codec: Option<SharedEnvelope>,
641        pool: Pool,
642        tap: Tap,
643        runtime: Handle,
644    ) -> Self {
645        Self {
646            name,
647            client,
648            rx,
649            codec,
650            pool,
651            tap,
652            runtime,
653        }
654    }
655
656    /// The window's client: a connection of the pool the flushes go out on.
657    pub(crate) fn round_client(&self) -> Client {
658        self.pool.next().clone()
659    }
660
661    /// Yields one delivery per message, each with a slot in `window`, telling the window how
662    /// many more have arrived behind it, and reports a failed flush of the window once.
663    pub(crate) fn round_stream<'a>(
664        &'a mut self,
665        window: &'a Arc<Window<PubSubForm>>,
666    ) -> impl Stream<Item = Result<RoundMessage<PubSubForm>, RedisError>> + Send + 'a {
667        let codec = self.codec.clone();
668        let (pool, tap, name) = (&self.pool, &self.tap, self.name.as_str());
669        unfold((&mut self.rx, codec), move |(rx, codec)| async move {
670            loop {
671                if let Some(failure) = window.take_failure() {
672                    return Some((Err(failure), (rx, codec)));
673                }
674                match rx.recv().await {
675                    Ok(msg) => {
676                        let delivery = to_message(&msg, codec.as_ref(), pool, tap);
677                        window.yielded(rx.len());
678                        let round = window.open();
679                        return Some((
680                            Ok(RoundMessage::new(delivery, Arc::clone(window), round)),
681                            (rx, codec),
682                        ));
683                    }
684                    // The receiver fell behind the broadcast buffer; skip the gap and keep reading.
685                    Err(RecvError::Lagged(skipped)) => lagged(name, tap, skipped),
686                    Err(RecvError::Closed) => return None,
687                }
688            }
689        })
690    }
691}
692
693/// Reports the messages a subscription that fell behind its buffer lost, and forgets them.
694fn lagged(name: &str, tap: &Tap, skipped: u64) {
695    warn!(
696        subscription = name,
697        lost = skipped,
698        "a Redis Pub/Sub subscription fell behind its buffer and lost the oldest messages; raise \
699         its buffer or speed up its handler"
700    );
701    #[cfg(feature = "testing")]
702    tap.discard(usize::try_from(skipped).unwrap_or(usize::MAX));
703    #[cfg(not(feature = "testing"))]
704    let _ = tap;
705}
706
707impl PubSubWire {
708    /// Yields one batch of up to `size` messages: the first one waited for, and those already
709    /// arrived behind it. The batch shares one slot of `window`.
710    pub(crate) fn round_batches<'a>(
711        &'a mut self,
712        window: &'a Arc<Window<PubSubForm>>,
713        size: NonZeroUsize,
714    ) -> impl Stream<Item = Result<Vec<RoundMessage<PubSubForm>>, RedisError>> + Send + 'a {
715        window.fill_at(size.get());
716        let codec = self.codec.clone();
717        let (pool, tap, name) = (&self.pool, &self.tap, self.name.as_str());
718        unfold((&mut self.rx, codec), move |(rx, codec)| async move {
719            loop {
720                if let Some(failure) = window.take_failure() {
721                    return Some((Err(failure), (rx, codec)));
722                }
723                let first = match rx.recv().await {
724                    Ok(msg) => msg,
725                    Err(RecvError::Lagged(skipped)) => {
726                        lagged(name, tap, skipped);
727                        continue;
728                    }
729                    Err(RecvError::Closed) => return None,
730                };
731                let mut messages = vec![first];
732                while messages.len() < size.get() {
733                    match rx.try_recv() {
734                        Ok(msg) => messages.push(msg),
735                        Err(TryRecvError::Lagged(skipped)) => lagged(name, tap, skipped),
736                        Err(_) => break,
737                    }
738                }
739                let members = u32::try_from(messages.len()).unwrap_or(u32::MAX);
740                for _ in 0..members {
741                    window.yielded(rx.len());
742                }
743                let round = window.open_batch(members);
744                let batch = messages
745                    .iter()
746                    .map(|msg| {
747                        RoundMessage::new(
748                            to_message(msg, codec.as_ref(), pool, tap),
749                            Arc::clone(window),
750                            round.clone(),
751                        )
752                    })
753                    .collect();
754                return Some((Ok(batch), (rx, codec)));
755            }
756        })
757    }
758}
759
760impl Drop for PubSubWire {
761    fn drop(&mut self) {
762        // What reached the subscription and was never read leaves with it: the in-process server
763        // counted each message in flight when it sent it.
764        #[cfg(feature = "testing")]
765        {
766            let mut unread = 0_usize;
767            loop {
768                match self.rx.try_recv() {
769                    Ok(_) => unread += 1,
770                    Err(TryRecvError::Lagged(skipped)) => {
771                        unread =
772                            unread.saturating_add(usize::try_from(skipped).unwrap_or(usize::MAX));
773                    }
774                    Err(_) => break,
775                }
776            }
777            self.tap.discard(unread);
778        }
779        // The dedicated client owns a background connection task; close it on a detached task since
780        // `drop` cannot await, on the broker's runtime, which is there wherever the drop happens.
781        let client = self.client.clone();
782        self.runtime.spawn(async move {
783            let _ = client.quit().await;
784        });
785    }
786}
787
788fn to_message(
789    msg: &Message,
790    codec: Option<&SharedEnvelope>,
791    pool: &Pool,
792    tap: &Tap,
793) -> RedisPubSubMessage {
794    let raw = msg.value.as_bytes().unwrap_or(&[]);
795    let (payload, headers) = unframe(codec, raw);
796    RedisPubSubMessage {
797        channel: msg.channel.to_string(),
798        // `PMessage` is the delivery kind for a `PSUBSCRIBE` match; the message's own channel is the
799        // concrete one matched, which differs from the subscription's glob pattern.
800        pattern: matches!(msg.kind, MessageKind::PMessage),
801        payload,
802        headers,
803        pool: pool.clone(),
804        flight: tap.delivered(),
805    }
806}
807
808impl ruststream::Subscriber for PubSubWire {
809    type Message = RedisPubSubMessage;
810    type Error = RedisError;
811
812    fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
813        let codec = self.codec.clone();
814        let (pool, tap, name) = (&self.pool, &self.tap, self.name.as_str());
815        unfold((&mut self.rx, codec), move |(rx, codec)| async move {
816            loop {
817                match rx.recv().await {
818                    Ok(msg) => {
819                        let message = to_message(&msg, codec.as_ref(), pool, tap);
820                        return Some((Ok(message), (rx, codec)));
821                    }
822                    // The receiver fell behind the broadcast buffer; skip the gap and keep reading.
823                    Err(RecvError::Lagged(skipped)) => lagged(name, tap, skipped),
824                    Err(RecvError::Closed) => return None,
825                }
826            }
827        })
828    }
829}
830
831/// A Pub/Sub delivery. `ack` / `nack` are unsupported (Pub/Sub has no acknowledgement).
832pub struct RedisPubSubMessage {
833    channel: String,
834    /// Whether this delivery arrived through a `PSUBSCRIBE` pattern match (vs an exact subscribe).
835    pattern: bool,
836    payload: Bytes,
837    headers: HeaderMap,
838    /// The broker's pool, which `Ctx<keys::FredPool>` hands the handler.
839    pool: Pool,
840    /// The test harness's count of this delivery; empty outside the in-process mode.
841    #[cfg_attr(
842        not(feature = "testing"),
843        allow(
844            dead_code,
845            reason = "held for its release on drop, which only `testing` gives"
846        )
847    )]
848    flight: InFlight,
849}
850
851impl Debug for RedisPubSubMessage {
852    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
853        f.debug_struct("RedisPubSubMessage")
854            .field("channel", &self.channel)
855            .field("pattern", &self.pattern)
856            .field("payload_len", &self.payload.len())
857            .finish_non_exhaustive()
858    }
859}
860
861impl RedisPubSubMessage {
862    /// The channel this message arrived on.
863    ///
864    /// On a [`RedisPubSubPattern`] subscription this is the concrete channel the message was
865    /// published to, which differs from the glob the subscription registered.
866    #[must_use]
867    pub fn channel(&self) -> &str {
868        &self.channel
869    }
870
871    /// Whether this delivery arrived through a `PSUBSCRIBE` pattern match rather than an exact
872    /// channel subscribe.
873    #[must_use]
874    pub fn from_pattern(&self) -> bool {
875        self.pattern
876    }
877
878    /// The broker's connection pool, for the per-delivery context.
879    pub(crate) const fn pool(&self) -> &Pool {
880        &self.pool
881    }
882
883    /// Takes the harness's count of this delivery, for a window to hold until its flush.
884    #[cfg(feature = "testing")]
885    pub(crate) fn take_flight(&mut self) -> InFlight {
886        std::mem::take(&mut self.flight)
887    }
888}
889
890impl IncomingMessage for RedisPubSubMessage {
891    fn payload(&self) -> &[u8] {
892        &self.payload
893    }
894
895    fn headers(&self) -> &HeaderMap {
896        &self.headers
897    }
898
899    // See `RedisMessage`: the keyed worker lanes read the key from here, not from `Partitioned`.
900    fn partition_key(&self) -> Option<&[u8]> {
901        Partitioned::partition_key(self)
902    }
903
904    fn ack(self) -> impl Future<Output = Result<(), AckError>> {
905        ready(Err(AckError::Unsupported))
906    }
907
908    fn nack(self, _requeue: bool) -> impl Future<Output = Result<(), AckError>> {
909        ready(Err(AckError::Unsupported))
910    }
911}
912
913impl Partitioned for RedisPubSubMessage {
914    fn partition_key(&self) -> Option<&[u8]> {
915        self.headers().get(PARTITION_KEY_HEADER)
916    }
917}
918
919/// The declaration half of the Pub/Sub publisher: delivery mode and envelope codec, no connection.
920///
921/// Constructible anywhere (in a router definition, in configuration), it pairs into a
922/// [`RedisPubSubPublisher`] against a [`ConnectedRedisBroker`]. The publish mode must match how
923/// subscribers subscribed: a sharded publish only reaches sharded subscribers.
924///
925/// # Examples
926///
927/// ```
928/// # mod demo {
929/// use ruststream_fred::stream::prelude::*;
930/// use ruststream_fred::{PubSubMode, RedisPubSubPublish};
931/// use serde::{Deserialize, Serialize};
932///
933/// #[derive(Deserialize)]
934/// struct Order {
935///     id: u64,
936/// }
937///
938/// #[derive(Serialize, Outgoing)]
939/// #[outgoing(name = "orders.live")]
940/// struct Placed {
941///     id: u64,
942/// }
943///
944/// #[subscriber(RedisStream::new("orders").group("workers"), reply)]
945/// async fn announce(order: &Order) -> Placed {
946///     Placed { id: order.id }
947/// }
948///
949/// #[ruststream::app]
950/// fn app() -> impl App {
951///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
952///         RedisBroker::cluster(["10.0.0.1:6379"]),
953///         |b| {
954///             // Read from a durable stream, broadcast to whoever is listening with `SPUBLISH`.
955///             b.include(announce)
956///                 .out_reply(RedisPubSubPublish::new().mode(PubSubMode::Sharded));
957///         },
958///     )
959/// }
960/// # }
961/// # fn main() {}
962/// ```
963#[derive(Clone, Default)]
964#[must_use]
965pub struct RedisPubSubPublish {
966    mode: PubSubMode,
967    codec: Option<SharedEnvelope>,
968}
969
970impl Debug for RedisPubSubPublish {
971    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
972        f.debug_struct("RedisPubSubPublish")
973            .field("mode", &self.mode)
974            .field("codec", &self.codec.is_some())
975            .finish()
976    }
977}
978
979impl RedisPubSubPublish {
980    /// A classic-mode policy with the default binary framing. Equivalent to [`Self::default`].
981    pub fn new() -> Self {
982        Self::default()
983    }
984
985    /// Sets the publish mode. Defaults to [`PubSubMode::Classic`].
986    pub const fn mode(mut self, mode: PubSubMode) -> Self {
987        self.mode = mode;
988        self
989    }
990
991    /// Serializes the header/payload envelope with `codec` (must match the subscriber), which makes
992    /// the wire value readable while the data is text. Without it the default binary framing is
993    /// used. Either way a payload that is not text is published byte for byte.
994    pub fn codec(mut self, codec: impl Codec + 'static) -> Self {
995        self.codec = Some(Arc::new(codec));
996        self
997    }
998}
999
1000impl PublishPolicy<ConnectedRedisBroker> for RedisPubSubPublish {
1001    type Live = RedisPubSubPublisher;
1002
1003    fn pair(
1004        self,
1005        connected: &ConnectedRedisBroker,
1006    ) -> impl Future<Output = Result<Self::Live, PairError>> {
1007        ready(Ok(connected.pubsub_publisher(self)))
1008    }
1009
1010    /// The channel a `PUBLISH` goes out on, the delivery mode it goes out in, and how headers are
1011    /// framed beside the payload.
1012    #[cfg(feature = "asyncapi")]
1013    fn channel_bindings(&self, channel: &str) -> Bindings {
1014        crate::asyncapi::channel(&crate::asyncapi::Publish::pubsub(
1015            channel,
1016            self.mode.as_str(),
1017            crate::asyncapi::Envelope::of(self.codec.as_ref()),
1018        ))
1019    }
1020}
1021
1022/// Publishes Pub/Sub messages with `PUBLISH` (classic) or `SPUBLISH` (sharded): a
1023/// [`RedisPubSubPublish`] policy paired with a connection.
1024///
1025/// Obtain it from
1026/// [`ConnectedRedisBroker::pubsub_publisher`](crate::ConnectedRedisBroker::pubsub_publisher), or
1027/// by pairing the policy. Like every publisher here it may outlive the connection, so publishing
1028/// after shutdown reports [`RedisError::ShutDown`].
1029#[derive(Clone)]
1030pub struct RedisPubSubPublisher {
1031    core: Arc<RedisCore>,
1032    mode: PubSubMode,
1033    codec: Option<SharedEnvelope>,
1034    /// What `pipeline.bind(&out)` names this publisher and its clones by.
1035    round_name: u64,
1036}
1037
1038impl Debug for RedisPubSubPublisher {
1039    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1040        f.debug_struct("RedisPubSubPublisher")
1041            .field("mode", &self.mode)
1042            .field("codec", &self.codec.is_some())
1043            .finish_non_exhaustive()
1044    }
1045}
1046
1047impl RedisPubSubPublisher {
1048    /// What `pipeline.bind(&out)` names this publisher and its clones by.
1049    pub(crate) const fn round_name(&self) -> u64 {
1050        self.round_name
1051    }
1052
1053    pub(crate) fn new(core: Arc<RedisCore>, publish: RedisPubSubPublish) -> Self {
1054        Self {
1055            round_name: core.rounds().publisher(),
1056            core,
1057            mode: publish.mode,
1058            codec: publish.codec,
1059        }
1060    }
1061}
1062
1063impl Publisher for RedisPubSubPublisher {
1064    /// The payload is read into an envelope of this crate's own before it reaches the client, so
1065    /// there is nothing to keep: the publish is lent the bytes where they already are.
1066    type Payload = Lend;
1067
1068    type Error = RedisError;
1069    /// `PUBLISH` and `SPUBLISH` take a channel and a payload and nothing else, and which of the
1070    /// two is issued is the publisher's mode, fixed by the policy, because a sharded publish only
1071    /// reaches sharded subscribers. What a call site still says is the partition key, framed into
1072    /// the payload with the message's other headers.
1073    type Options = RedisPublishOptions;
1074
1075    async fn publish(
1076        &self,
1077        msg: OutgoingMessage<'_>,
1078        options: Option<&Self::Options>,
1079    ) -> Result<(), Self::Error> {
1080        let (channel, payload, headers) = msg.into_parts();
1081        if let Some(round) = self
1082            .core
1083            .rounds()
1084            .joined(self.round_name, joins_round(options))
1085        {
1086            let body = frame(
1087                self.codec.as_ref(),
1088                payload,
1089                &resolved_headers(headers, options),
1090            );
1091            return round
1092                .publish(channel, body, self.mode == PubSubMode::Sharded)
1093                .await
1094                .map_err(RedisError::publish);
1095        }
1096        send(
1097            &self.core,
1098            self.mode,
1099            self.codec.as_ref(),
1100            channel,
1101            payload,
1102            resolved_headers(headers, options),
1103        )
1104        .await
1105    }
1106}
1107
1108/// Publishes one framed message on `channel` in `mode`: the Pub/Sub publish, shared by
1109/// [`RedisPubSubPublisher`] and the broker's default publisher.
1110pub(crate) async fn send(
1111    core: &RedisCore,
1112    mode: PubSubMode,
1113    codec: Option<&SharedEnvelope>,
1114    channel: &str,
1115    payload: &[u8],
1116    headers: HeaderMap,
1117) -> Result<(), RedisError> {
1118    let pool = core.pool()?;
1119    let client = pool.next();
1120    let body = frame(codec, payload, &headers);
1121    let channel = channel.to_owned();
1122    let _: i64 = match mode {
1123        PubSubMode::Classic => client.publish(channel, body).await,
1124        PubSubMode::Sharded => client.spublish(channel, body).await,
1125    }
1126    .map_err(RedisError::publish)?;
1127    Ok(())
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132    use super::*;
1133    use crate::context::PubSubContext;
1134    use fred::types::config::Config;
1135    use ruststream::BuildContext;
1136
1137    /// An unconnected pool (just client structs); `Pool::new` opens no sockets.
1138    fn offline_pool() -> Pool {
1139        Pool::new(Config::default(), None, None, None, 1).expect("offline pool")
1140    }
1141
1142    #[test]
1143    fn build_context_reads_channel_and_pattern_flag() {
1144        let exact = RedisPubSubMessage {
1145            channel: "events".to_owned(),
1146            pattern: false,
1147            payload: Bytes::from_static(b"{}"),
1148            headers: HeaderMap::new(),
1149            pool: offline_pool(),
1150            flight: InFlight::default(),
1151        };
1152        let cx = PubSubContext::build(&exact);
1153        assert_eq!(cx.channel(), "events");
1154        assert!(!cx.from_pattern());
1155
1156        let matched = RedisPubSubMessage {
1157            channel: "events.user".to_owned(),
1158            pattern: true,
1159            payload: Bytes::from_static(b"{}"),
1160            headers: HeaderMap::new(),
1161            pool: offline_pool(),
1162            flight: InFlight::default(),
1163        };
1164        assert!(PubSubContext::build(&matched).from_pattern());
1165    }
1166
1167    /// A channel is one destination a `PUBLISH` names, whatever the delivery mode.
1168    #[test]
1169    fn a_channel_addresses_its_copies_to_itself() {
1170        assert_eq!(
1171            RedisPubSub::new("events").redelivery_channel().as_str(),
1172            "events"
1173        );
1174        assert_eq!(
1175            RedisPubSub::new("events")
1176                .mode(PubSubMode::Sharded)
1177                .redelivery_channel()
1178                .as_str(),
1179            "events"
1180        );
1181    }
1182}