Skip to main content

ruststream_fred/
pubsub.rs

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