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}