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}