1use std::fmt::{Debug, Formatter};
17use std::sync::Arc;
18
19use bytes::Bytes;
20use fred::clients::Client;
21use fred::interfaces::{ClientLike, PubsubInterface};
22use fred::types::{Message, MessageKind};
23use futures::Stream;
24use futures::stream::unfold;
25use ruststream::codec::Codec;
26use ruststream::{
27 AckError, Headers, IncomingMessage, OutgoingMessage, PairError, Partitioned, PublishPolicy,
28 Publisher, SubscriptionSource,
29};
30use tokio::sync::broadcast::{Receiver, error::RecvError};
31
32use crate::broker::{ConnectedRedisBroker, RedisCore};
33use crate::envelope::{SharedEnvelope, frame, unframe};
34use crate::{error::RedisError, message::PARTITION_KEY_HEADER};
35
36#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
38pub enum PubSubMode {
39 #[default]
41 Classic,
42 Sharded,
44}
45
46#[derive(Clone)]
59#[must_use]
60pub struct RedisPubSub {
61 channel: String,
62 mode: PubSubMode,
63 pattern: bool,
64 codec: Option<SharedEnvelope>,
65}
66
67impl Debug for RedisPubSub {
68 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("RedisPubSub")
70 .field("channel", &self.channel)
71 .field("mode", &self.mode)
72 .field("pattern", &self.pattern)
73 .field("codec", &self.codec.is_some())
74 .finish()
75 }
76}
77
78impl RedisPubSub {
79 pub fn new(channel: impl Into<String>) -> Self {
81 Self {
82 channel: channel.into(),
83 mode: PubSubMode::default(),
84 pattern: false,
85 codec: None,
86 }
87 }
88
89 pub const fn mode(mut self, mode: PubSubMode) -> Self {
91 self.mode = mode;
92 self
93 }
94
95 pub const fn pattern(mut self) -> Self {
98 self.pattern = true;
99 self
100 }
101
102 pub fn codec(mut self, codec: impl Codec + 'static) -> Self {
105 self.codec = Some(Arc::new(codec));
106 self
107 }
108
109 #[must_use]
111 pub fn channel(&self) -> &str {
112 &self.channel
113 }
114
115 pub(crate) const fn delivery_mode(&self) -> PubSubMode {
116 self.mode
117 }
118
119 pub(crate) const fn is_pattern(&self) -> bool {
120 self.pattern
121 }
122
123 pub(crate) fn codec_handle(&self) -> Option<SharedEnvelope> {
124 self.codec.clone()
125 }
126
127 pub(crate) fn validate(&self) -> Result<(), RedisError> {
128 if self.pattern && matches!(self.mode, PubSubMode::Sharded) {
129 return Err(RedisError::InvalidOptions(
130 "pattern subscriptions are classic-only; sharded pub/sub has no PSUBSCRIBE"
131 .to_owned(),
132 ));
133 }
134 Ok(())
135 }
136}
137
138impl SubscriptionSource<ConnectedRedisBroker> for RedisPubSub {
139 type Subscriber = RedisPubSubSubscriber;
140
141 fn name(&self) -> &str {
142 self.channel()
143 }
144
145 async fn subscribe(
146 self,
147 connected: &ConnectedRedisBroker,
148 ) -> Result<Self::Subscriber, RedisError> {
149 connected.subscribe_pubsub(self).await
150 }
151}
152
153#[cfg(feature = "testing")]
154impl SubscriptionSource<crate::testing::ConnectedRedisTestBroker> for RedisPubSub {
155 type Subscriber = crate::testing::RedisTestSubscriber;
156
157 fn name(&self) -> &str {
158 self.channel()
159 }
160
161 async fn subscribe(
162 self,
163 connected: &crate::testing::ConnectedRedisTestBroker,
164 ) -> Result<Self::Subscriber, RedisError> {
165 connected.subscribe(self.channel()).await
166 }
167}
168
169pub struct RedisPubSubSubscriber {
172 client: Client,
173 rx: Receiver<Message>,
174 codec: Option<SharedEnvelope>,
175}
176
177impl Debug for RedisPubSubSubscriber {
178 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
179 f.debug_struct("RedisPubSubSubscriber")
180 .finish_non_exhaustive()
181 }
182}
183
184impl RedisPubSubSubscriber {
185 pub(crate) fn new(
186 client: Client,
187 rx: Receiver<Message>,
188 codec: Option<SharedEnvelope>,
189 ) -> Self {
190 Self { client, rx, codec }
191 }
192}
193
194impl Drop for RedisPubSubSubscriber {
195 fn drop(&mut self) {
196 let client = self.client.clone();
199 tokio::spawn(async move {
200 let _ = client.quit().await;
201 });
202 }
203}
204
205fn to_message(msg: &Message, codec: Option<&SharedEnvelope>) -> RedisPubSubMessage {
206 let raw = msg.value.as_bytes().unwrap_or(&[]);
207 let (payload, headers) = unframe(codec, raw);
208 RedisPubSubMessage {
209 channel: msg.channel.to_string(),
210 pattern: matches!(msg.kind, MessageKind::PMessage),
213 payload,
214 headers,
215 }
216}
217
218impl ruststream::Subscriber for RedisPubSubSubscriber {
219 type Message = RedisPubSubMessage;
220 type Error = RedisError;
221
222 fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
230 let codec = self.codec.clone();
231 unfold((&mut self.rx, codec), |(rx, codec)| async move {
232 loop {
233 match rx.recv().await {
234 Ok(msg) => {
235 let message = to_message(&msg, codec.as_ref());
236 return Some((Ok(message), (rx, codec)));
237 }
238 Err(RecvError::Lagged(_)) => {}
240 Err(RecvError::Closed) => return None,
241 }
242 }
243 })
244 }
245}
246
247pub struct RedisPubSubMessage {
249 channel: String,
250 pattern: bool,
252 payload: Bytes,
253 headers: Headers,
254}
255
256impl Debug for RedisPubSubMessage {
257 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
258 f.debug_struct("RedisPubSubMessage")
259 .field("channel", &self.channel)
260 .field("pattern", &self.pattern)
261 .field("payload_len", &self.payload.len())
262 .finish_non_exhaustive()
263 }
264}
265
266impl RedisPubSubMessage {
267 #[must_use]
272 pub fn channel(&self) -> &str {
273 &self.channel
274 }
275
276 #[must_use]
279 pub fn from_pattern(&self) -> bool {
280 self.pattern
281 }
282}
283
284impl IncomingMessage for RedisPubSubMessage {
285 fn payload(&self) -> &[u8] {
286 &self.payload
287 }
288
289 fn headers(&self) -> &Headers {
290 &self.headers
291 }
292
293 async fn ack(self) -> Result<(), AckError> {
294 Err(AckError::Unsupported)
295 }
296
297 async fn nack(self, _requeue: bool) -> Result<(), AckError> {
298 Err(AckError::Unsupported)
299 }
300}
301
302impl Partitioned for RedisPubSubMessage {
303 fn partition_key(&self) -> Option<&[u8]> {
304 self.headers().get(PARTITION_KEY_HEADER)
305 }
306}
307
308#[derive(Clone, Default)]
324#[must_use]
325pub struct RedisPubSubPublish {
326 mode: PubSubMode,
327 codec: Option<SharedEnvelope>,
328}
329
330impl Debug for RedisPubSubPublish {
331 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
332 f.debug_struct("RedisPubSubPublish")
333 .field("mode", &self.mode)
334 .field("codec", &self.codec.is_some())
335 .finish()
336 }
337}
338
339impl RedisPubSubPublish {
340 pub fn new() -> Self {
342 Self::default()
343 }
344
345 pub const fn mode(mut self, mode: PubSubMode) -> Self {
347 self.mode = mode;
348 self
349 }
350
351 pub fn codec(mut self, codec: impl Codec + 'static) -> Self {
354 self.codec = Some(Arc::new(codec));
355 self
356 }
357}
358
359impl PublishPolicy<ConnectedRedisBroker> for RedisPubSubPublish {
360 type Live = RedisPubSubPublisher;
361
362 async fn pair(self, connected: &ConnectedRedisBroker) -> Result<Self::Live, PairError> {
363 Ok(connected.pubsub_publisher(self))
364 }
365}
366
367#[derive(Clone)]
375pub struct RedisPubSubPublisher {
376 core: Arc<RedisCore>,
377 mode: PubSubMode,
378 codec: Option<SharedEnvelope>,
379}
380
381impl Debug for RedisPubSubPublisher {
382 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
383 f.debug_struct("RedisPubSubPublisher")
384 .field("mode", &self.mode)
385 .field("codec", &self.codec.is_some())
386 .finish_non_exhaustive()
387 }
388}
389
390impl RedisPubSubPublisher {
391 pub(crate) fn new(core: Arc<RedisCore>, publish: RedisPubSubPublish) -> Self {
392 Self {
393 core,
394 mode: publish.mode,
395 codec: publish.codec,
396 }
397 }
398}
399
400impl Publisher for RedisPubSubPublisher {
401 type Error = RedisError;
402
403 async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
404 let pool = self.core.pool()?;
405 let client = pool.next();
406 let channel = msg.name().to_owned();
407 let body = frame(self.codec.as_ref(), msg.payload(), msg.headers());
408 let _: i64 = match self.mode {
409 PubSubMode::Classic => client.publish(channel, body).await,
410 PubSubMode::Sharded => client.spublish(channel, body).await,
411 }
412 .map_err(RedisError::publish)?;
413 Ok(())
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use crate::context::PubSubContext;
421 use ruststream::BuildContext;
422
423 #[test]
424 fn build_context_reads_channel_and_pattern_flag() {
425 let exact = RedisPubSubMessage {
426 channel: "events".to_owned(),
427 pattern: false,
428 payload: Bytes::from_static(b"{}"),
429 headers: Headers::new(),
430 };
431 let cx = PubSubContext::build(&exact);
432 assert_eq!(cx.channel(), "events");
433 assert!(!cx.from_pattern());
434
435 let matched = RedisPubSubMessage {
436 channel: "events.user".to_owned(),
437 pattern: true,
438 payload: Bytes::from_static(b"{}"),
439 headers: Headers::new(),
440 };
441 assert!(PubSubContext::build(&matched).from_pattern());
442 }
443
444 #[test]
445 fn pattern_with_sharded_is_rejected() {
446 let err = RedisPubSub::new("e.*")
447 .mode(PubSubMode::Sharded)
448 .pattern()
449 .validate()
450 .unwrap_err();
451 assert!(matches!(err, RedisError::InvalidOptions(msg) if msg.contains("classic-only")));
452 }
453
454 #[test]
455 fn classic_pattern_validates() {
456 RedisPubSub::new("e.*").pattern().validate().expect("ok");
457 }
458}