1use std::collections::VecDeque;
24use std::fmt::{Debug, Formatter};
25use std::future::{Future, ready};
26use std::num::NonZeroUsize;
27use std::sync::Arc;
28use std::time::Duration;
29
30use bytes::Bytes;
31use fred::clients::{Client, Pool};
32use fred::error::ErrorKind;
33use fred::interfaces::{KeysInterface, ListInterface, SortedSetsInterface};
34use fred::types::Value;
35use fred::types::lists::LMoveDirection;
36use futures::Stream;
37use futures::stream::unfold;
38#[cfg(feature = "asyncapi")]
39use ruststream::asyncapi::Bindings;
40use ruststream::codec::Codec;
41use ruststream::{
42 AckError, AddressedCopies, BatchSubscriber, BufferedSubscriber, HeaderMap, IncomingMessage,
43 Lend, PairError, Partitioned, PublishPolicy, RedeliveryAddress, RedeliveryAddressed,
44 RetryDeclaration, SubscriptionSource,
45};
46
47use crate::broker::{ConnectedRedisBroker, RedisCore};
48use crate::connection::{Connection, ReadConnection};
49use crate::envelope::{SharedEnvelope, frame, unframe};
50use crate::loopback::{InFlight, Loopback, Tap};
51use crate::partition::{RedisPublishOptions, resolved_headers};
52use crate::pipeline::{ListForm, RoundMessage, Window};
53use crate::publisher::joins_round;
54use crate::recovery::{self, RecoveryConfig};
55use crate::route::Route;
56use crate::{error::RedisError, message::PARTITION_KEY_HEADER};
57
58pub use crate::list::RedisListPublish as Publish;
61
62pub mod prelude {
109 pub use ruststream::prelude::*;
110
111 pub use super::{Publish, RedisList};
112 pub use crate::context::{PipelineContext, PoolContext, keys};
115 pub use crate::pipeline::{Bindable, InRound, RedisPipelineSteps};
116 pub use crate::{
117 PARTITION_KEY_HEADER, RedisBroker, RedisPublishOptions, RedisPublishSteps,
118 RedisSubscribeExt,
119 };
120
121 #[cfg(any(
122 feature = "tls-rustls",
123 feature = "tls-rustls-ring",
124 feature = "tls-native-tls"
125 ))]
126 pub use crate::{TlsConfig, TlsConnector};
127}
128
129const DEFAULT_BLOCK: Duration = Duration::from_secs(5);
130const PROCESSING_SUFFIX: &str = ".processing";
132
133fn block_secs(block: Duration) -> f64 {
134 block.as_secs_f64()
135}
136
137fn empty_on_timeout<T>(
141 result: Result<Option<T>, fred::error::Error>,
142) -> Result<Option<T>, RedisError> {
143 match result {
144 Ok(value) => Ok(value),
145 Err(err) if matches!(err.kind(), ErrorKind::Timeout) => Ok(None),
146 Err(err) => Err(RedisError::stream(err)),
147 }
148}
149
150#[derive(Clone)]
194#[must_use]
195pub struct RedisList {
196 key: String,
197 reliable: bool,
198 processing: Option<String>,
199 block: Option<Duration>,
200 codec: Option<SharedEnvelope>,
201 min_idle: Option<Duration>,
202 recovery_zset: Option<String>,
203 recovery_ttl: Option<Duration>,
204 dead_letter: Option<String>,
207}
208
209impl Debug for RedisList {
210 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
211 f.debug_struct("RedisList")
212 .field("key", &self.key)
213 .field("reliable", &self.reliable)
214 .field("processing", &self.processing)
215 .field("codec", &self.codec.is_some())
216 .field("recovery_zset", &self.recovery_zset)
217 .field("recovery_ttl", &self.recovery_ttl)
218 .field("dead_letter", &self.dead_letter)
219 .finish_non_exhaustive()
220 }
221}
222
223impl RedisList {
224 pub fn new(key: impl Into<String>) -> Self {
226 Self {
227 key: key.into(),
228 reliable: false,
229 processing: None,
230 block: None,
231 codec: None,
232 min_idle: None,
233 recovery_zset: None,
234 recovery_ttl: None,
235 dead_letter: None,
236 }
237 }
238
239 pub const fn reliable(mut self) -> Self {
242 self.reliable = true;
243 self
244 }
245
246 pub fn processing(mut self, key: impl Into<String>) -> Self {
248 self.processing = Some(key.into());
249 self
250 }
251
252 pub const fn block(mut self, block: Duration) -> Self {
257 self.block = Some(block);
258 self
259 }
260
261 pub fn codec(mut self, codec: impl Codec + 'static) -> Self {
264 self.codec = Some(Arc::new(codec));
265 self
266 }
267
268 pub const fn min_idle(mut self, min_idle: Duration) -> Self {
275 self.min_idle = Some(min_idle);
276 self
277 }
278
279 pub fn recovery_zset(mut self, key: impl Into<String>) -> Self {
286 self.recovery_zset = Some(key.into());
287 self.reliable = true;
288 self
289 }
290
291 pub const fn recovery_ttl(mut self, ttl: Duration) -> Self {
296 self.recovery_ttl = Some(ttl);
297 self
298 }
299
300 #[must_use]
302 pub fn key(&self) -> &str {
303 &self.key
304 }
305
306 pub(crate) fn into_key(self) -> String {
309 self.key
310 }
311
312 pub(crate) const fn is_reliable(&self) -> bool {
313 self.reliable
314 }
315
316 pub(crate) fn processing_or_default(&self) -> String {
317 self.processing
318 .clone()
319 .unwrap_or_else(|| format!("{}{PROCESSING_SUFFIX}", self.key))
320 }
321
322 pub(crate) fn block_or_default(&self) -> Duration {
323 self.block.unwrap_or(DEFAULT_BLOCK)
324 }
325
326 pub(crate) fn codec_handle(&self) -> Option<SharedEnvelope> {
327 self.codec.clone()
328 }
329
330 pub(crate) fn route(&self) -> Route {
332 Route::List {
333 envelope: self.codec.clone(),
334 }
335 }
336
337 pub(crate) fn dead_letter(&self) -> Option<&str> {
340 self.dead_letter.as_deref()
341 }
342
343 fn with_declaration(mut self, declaration: &RetryDeclaration) -> Self {
345 self.dead_letter = declaration.dead_letter().map(str::to_owned);
346 self
347 }
348
349 #[cfg(feature = "asyncapi")]
353 fn describe(&self) -> Bindings {
354 let processing = self.reliable.then(|| self.processing_or_default());
355 crate::asyncapi::channel(&crate::asyncapi::ListSubscription::new(
356 self.reliable,
357 processing.as_deref(),
358 crate::asyncapi::Envelope::of(self.codec.as_ref()),
359 ))
360 }
361
362 pub(crate) fn recovery_config(&self) -> Result<Option<RecoveryConfig>, RedisError> {
369 let Some(zset_key) = self.recovery_zset.clone() else {
370 return Ok(None);
371 };
372 let min_idle = self.min_idle.ok_or_else(|| {
373 RedisError::InvalidOptions(format!(
374 "reliable list recovery on `{}` needs a min_idle: call .min_idle(duration) \
375 alongside .recovery_zset(key)",
376 self.key
377 ))
378 })?;
379 Ok(Some(RecoveryConfig {
380 zset_key,
381 min_idle,
382 ttl: self.recovery_ttl,
383 loopback: Loopback::default(),
384 }))
385 }
386}
387
388impl SubscriptionSource<ConnectedRedisBroker> for RedisList {
389 type Subscriber = RedisListSubscriber;
390 type Copies = AddressedCopies;
392
393 fn name(&self) -> &str {
394 self.key()
395 }
396
397 fn declare_retry(self, declaration: &RetryDeclaration) -> Self {
398 self.with_declaration(declaration)
399 }
400
401 async fn subscribe(
402 self,
403 connected: &ConnectedRedisBroker,
404 ) -> Result<Self::Subscriber, RedisError> {
405 connected.subscribe_list(self).await
406 }
407
408 #[cfg(feature = "asyncapi")]
409 fn channel_bindings(&self) -> Bindings {
410 self.describe()
411 }
412}
413
414impl RedeliveryAddressed<ConnectedRedisBroker> for RedisList {
417 fn redelivery_address(
418 &self,
419 _connected: &ConnectedRedisBroker,
420 ) -> impl Future<Output = Result<RedeliveryAddress, RedisError>> {
421 ready(Ok(RedeliveryAddress::new(self.key.clone())))
422 }
423}
424
425pub struct RedisListSubscriber(BufferedSubscriber<ListWire>);
431
432impl Debug for RedisListSubscriber {
433 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
434 f.debug_tuple("RedisListSubscriber").field(&self.0).finish()
435 }
436}
437
438impl RedisListSubscriber {
439 pub(crate) fn new(wire: ListWire) -> Self {
440 Self(BufferedSubscriber::new(wire))
441 }
442}
443
444impl ruststream::Subscriber for RedisListSubscriber {
445 type Message = RedisListMessage;
446 type Error = RedisError;
447
448 fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
455 self.0.stream()
456 }
457}
458
459impl BatchSubscriber for RedisListSubscriber {
460 type Batch = Vec<RedisListMessage>;
461
462 fn batches(
470 &mut self,
471 size: NonZeroUsize,
472 ) -> impl Stream<Item = Result<Self::Batch, Self::Error>> + Send + '_ {
473 self.0.batches(size)
474 }
475}
476
477pub(crate) struct ListWire {
479 connection: Arc<Connection>,
480 reader: ReadConnection,
483 key: String,
484 reliable: bool,
485 processing: String,
486 block: Duration,
487 codec: Option<SharedEnvelope>,
488 recovery: Option<RecoveryConfig>,
489 tap: Tap,
491}
492
493impl Debug for ListWire {
494 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
495 f.debug_struct("ListWire")
496 .field("key", &self.key)
497 .field("reliable", &self.reliable)
498 .field("recovery", &self.recovery.is_some())
499 .finish_non_exhaustive()
500 }
501}
502
503impl ListWire {
504 #[allow(
505 clippy::too_many_arguments,
506 reason = "internal constructor mirroring the descriptor"
507 )]
508 pub(crate) fn new(
509 connection: Arc<Connection>,
510 reader: ReadConnection,
511 key: String,
512 reliable: bool,
513 processing: String,
514 block: Duration,
515 codec: Option<SharedEnvelope>,
516 recovery: Option<RecoveryConfig>,
517 tap: Tap,
518 ) -> Self {
519 Self {
520 connection,
521 reader,
522 key,
523 reliable,
524 processing,
525 block,
526 codec,
527 recovery,
528 tap,
529 }
530 }
531
532 fn simple_message(&self, raw: &[u8]) -> RedisListMessage {
533 let (payload, headers) = unframe(self.codec.as_ref(), raw);
534 RedisListMessage {
535 payload,
536 headers,
537 ack: None,
538 connection: Arc::clone(&self.connection),
539 flight: self.tap.delivered(),
540 }
541 }
542
543 fn reliable_message(&self, raw: Vec<u8>, recovery: Option<RecoveryHandle>) -> RedisListMessage {
544 let (payload, headers) = unframe(self.codec.as_ref(), &raw);
545 RedisListMessage {
546 payload,
547 headers,
548 connection: Arc::clone(&self.connection),
549 ack: Some(ListAck {
550 main_key: self.key.clone(),
551 processing_key: self.processing.clone(),
552 value: raw,
553 recovery,
554 }),
555 flight: self.tap.delivered(),
556 }
557 }
558
559 async fn next_entry(&self) -> Result<Option<RedisListMessage>, RedisError> {
563 #[cfg(feature = "testing")]
566 self.tap.readable().await;
567 self.connection.ensure_open()?;
570 let secs = block_secs(self.block);
571 if self.reliable {
572 if let Some(cfg) = &self.recovery {
573 recovery::sweep_orphans(self.connection.pool(), cfg, &self.key, &self.processing)
574 .await?;
575 }
576 let value: Option<Vec<u8>> = empty_on_timeout(
577 self.reader
578 .client()
579 .blmove(
580 self.key.as_str(),
581 self.processing.as_str(),
582 LMoveDirection::Right,
583 LMoveDirection::Left,
584 secs,
585 )
586 .await,
587 )?;
588 let Some(value) = value else {
589 return Ok(None);
590 };
591 let handle = match &self.recovery {
592 Some(cfg) => {
593 let member =
594 recovery::record_claim(self.connection.pool(), cfg, &value).await?;
595 Some(RecoveryHandle {
596 zset_key: cfg.zset_key.clone(),
597 member,
598 })
599 }
600 None => None,
601 };
602 Ok(Some(self.reliable_message(value, handle)))
603 } else {
604 let popped: Option<(String, Vec<u8>)> =
605 empty_on_timeout(self.reader.client().brpop(self.key.as_str(), secs).await)?;
606 Ok(popped.map(|(_, v)| self.simple_message(&v)))
607 }
608 }
609}
610
611type Claimed = (Vec<u8>, Option<RecoveryHandle>);
614
615impl ListWire {
616 pub(crate) fn round_form(&self) -> ListForm {
618 ListForm::new(
619 self.key.as_str(),
620 self.processing.as_str(),
621 self.recovery.as_ref().map(|cfg| cfg.zset_key.as_str()),
622 )
623 }
624
625 pub(crate) fn round_client(&self) -> Client {
627 self.connection.pool().next().clone()
628 }
629
630 pub(crate) fn key(&self) -> &str {
632 &self.key
633 }
634
635 async fn claim(&self, count: usize, into: &mut VecDeque<Claimed>) -> Result<(), RedisError> {
642 self.connection.ensure_open()?;
645 let before = into.len();
646 if self.reliable {
647 if let Some(cfg) = &self.recovery {
648 recovery::sweep_orphans(self.connection.pool(), cfg, &self.key, &self.processing)
649 .await?;
650 }
651 let claims = self.reader.client().pipeline();
652 for _ in 0..count {
653 claims
654 .lmove::<(), _, _>(
655 self.key.as_str(),
656 self.processing.as_str(),
657 LMoveDirection::Right,
658 LMoveDirection::Left,
659 )
660 .await
661 .map_err(RedisError::stream)?;
662 }
663 for reply in claims.try_all::<Value>().await {
664 match reply.map_err(RedisError::stream)? {
668 Value::Null => {}
669 value => into.push_back((value_bytes(value)?, None)),
670 }
671 }
672 } else {
673 let popped: Value = self
674 .reader
675 .client()
676 .rpop(self.key.as_str(), Some(count))
677 .await
678 .map_err(RedisError::stream)?;
679 if let Value::Array(values) = popped {
680 for value in values {
681 into.push_back((value_bytes(value)?, None));
682 }
683 }
684 }
685 if into.len() == before
686 && let Some(value) = self.wait_one().await?
687 {
688 into.push_back((value, None));
691 }
692 self.track(into.range_mut(before..)).await
693 }
694
695 async fn wait_one(&self) -> Result<Option<Vec<u8>>, RedisError> {
698 #[cfg(feature = "testing")]
699 self.tap.readable().await;
700 let secs = block_secs(self.block);
701 if self.reliable {
702 empty_on_timeout(
703 self.reader
704 .client()
705 .blmove(
706 self.key.as_str(),
707 self.processing.as_str(),
708 LMoveDirection::Right,
709 LMoveDirection::Left,
710 secs,
711 )
712 .await,
713 )
714 } else {
715 let popped: Option<(String, Vec<u8>)> =
716 empty_on_timeout(self.reader.client().brpop(self.key.as_str(), secs).await)?;
717 Ok(popped.map(|(_, value)| value))
718 }
719 }
720
721 async fn track<'a>(
723 &self,
724 claimed: impl Iterator<Item = &'a mut Claimed>,
725 ) -> Result<(), RedisError> {
726 let Some(cfg) = &self.recovery else {
727 return Ok(());
728 };
729 let tracking = self.connection.pool().next().pipeline();
730 let mut any = false;
731 for (value, handle) in claimed {
732 let (score, member) = cfg.tracked(value);
733 tracking
734 .zadd::<(), _, _>(
735 cfg.zset_key.as_str(),
736 None,
737 None,
738 false,
739 false,
740 (score, member.clone()),
741 )
742 .await
743 .map_err(RedisError::stream)?;
744 *handle = Some(RecoveryHandle {
745 zset_key: cfg.zset_key.clone(),
746 member,
747 });
748 any = true;
749 }
750 if !any {
751 return Ok(());
752 }
753 if let Some(ttl) = cfg.ttl_millis() {
754 tracking
755 .pexpire::<(), _>(cfg.zset_key.as_str(), ttl, None)
756 .await
757 .map_err(RedisError::stream)?;
758 }
759 for reply in tracking.try_all::<Value>().await {
760 reply.map_err(RedisError::stream)?;
761 }
762 Ok(())
763 }
764
765 fn message_of(&self, (value, recovery): Claimed) -> RedisListMessage {
766 if self.reliable {
767 self.reliable_message(value, recovery)
768 } else {
769 self.simple_message(&value)
770 }
771 }
772}
773
774fn value_bytes(value: Value) -> Result<Vec<u8>, RedisError> {
776 match value {
777 Value::Bytes(bytes) => Ok(bytes.to_vec()),
778 Value::String(text) => Ok(text.as_bytes().to_vec()),
779 other => Err(RedisError::Stream(
780 format!("a list read answered with {other:?} where an entry was expected").into(),
781 )),
782 }
783}
784
785pub struct ListReader {
788 wire: ListWire,
789 buffer: VecDeque<Claimed>,
790}
791
792impl Debug for ListReader {
793 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
794 f.debug_struct("ListReader")
795 .field("wire", &self.wire)
796 .field("buffered", &self.buffer.len())
797 .finish()
798 }
799}
800
801impl ListReader {
802 pub(crate) const fn new(wire: ListWire) -> Self {
803 Self {
804 wire,
805 buffer: VecDeque::new(),
806 }
807 }
808
809 pub(crate) fn round_stream<'a>(
812 &'a mut self,
813 window: &'a Arc<Window<ListForm>>,
814 count: usize,
815 ) -> impl Stream<Item = Result<RoundMessage<ListForm>, RedisError>> + Send + 'a {
816 unfold(self, move |reader| async move {
817 loop {
818 if let Some(failure) = window.take_failure() {
819 return Some((Err(failure), reader));
820 }
821 if let Some(claimed) = reader.buffer.pop_front() {
822 let delivery = reader.wire.message_of(claimed);
823 let round = window.open();
824 return Some((
825 Ok(RoundMessage::new(delivery, Arc::clone(window), round)),
826 reader,
827 ));
828 }
829 if let Err(err) = reader.wire.claim(count, &mut reader.buffer).await {
830 return Some((Err(err), reader));
831 }
832 window.fetched(reader.buffer.len());
833 }
834 })
835 }
836}
837
838impl ListReader {
839 pub(crate) fn round_batches<'a>(
842 &'a mut self,
843 window: &'a Arc<Window<ListForm>>,
844 size: NonZeroUsize,
845 ) -> impl Stream<Item = Result<Vec<RoundMessage<ListForm>>, RedisError>> + Send + 'a {
846 window.fill_at(size.get());
847 unfold(self, move |reader| async move {
848 loop {
849 if let Some(failure) = window.take_failure() {
850 return Some((Err(failure), reader));
851 }
852 if !reader.buffer.is_empty() {
853 let take = size.get().min(reader.buffer.len());
854 let members = u32::try_from(take).unwrap_or(u32::MAX);
855 let round = window.open_batch(members);
856 let batch = reader
857 .buffer
858 .drain(..take)
859 .collect::<Vec<_>>()
860 .into_iter()
861 .map(|claimed| {
862 RoundMessage::new(
863 reader.wire.message_of(claimed),
864 Arc::clone(window),
865 round.clone(),
866 )
867 })
868 .collect();
869 return Some((Ok(batch), reader));
870 }
871 if let Err(err) = reader.wire.claim(size.get(), &mut reader.buffer).await {
872 return Some((Err(err), reader));
873 }
874 window.fetched(reader.buffer.len());
875 }
876 })
877 }
878}
879
880impl ruststream::Subscriber for ListWire {
881 type Message = RedisListMessage;
882 type Error = RedisError;
883
884 fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
885 unfold(&*self, |s| async move {
886 loop {
887 match s.next_entry().await {
888 Ok(Some(msg)) => return Some((Ok(msg), s)),
889 Ok(None) => {}
890 Err(err) => return Some((Err(err), s)),
891 }
892 }
893 })
894 }
895}
896
897struct ListAck {
899 main_key: String,
900 processing_key: String,
901 value: Vec<u8>,
903 recovery: Option<RecoveryHandle>,
906}
907
908struct RecoveryHandle {
910 zset_key: String,
911 member: Vec<u8>,
912}
913
914pub struct RedisListMessage {
917 payload: Bytes,
918 headers: HeaderMap,
919 ack: Option<ListAck>,
920 connection: Arc<Connection>,
923 #[cfg_attr(
925 not(feature = "testing"),
926 allow(
927 dead_code,
928 reason = "held for its release on drop, which only `testing` gives"
929 )
930 )]
931 flight: InFlight,
932}
933
934impl RedisListMessage {
935 pub(crate) fn pool(&self) -> &Pool {
937 self.connection.pool()
938 }
939
940 #[cfg(feature = "testing")]
942 pub(crate) fn take_flight(&mut self) -> InFlight {
943 std::mem::take(&mut self.flight)
944 }
945
946 pub(crate) fn into_settle(self) -> Option<(Vec<u8>, Option<Vec<u8>>)> {
949 self.ack
950 .map(|handle| (handle.value, handle.recovery.map(|rec| rec.member)))
951 }
952}
953
954impl Debug for RedisListMessage {
955 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
956 f.debug_struct("RedisListMessage")
957 .field("payload_len", &self.payload.len())
958 .field("reliable", &self.ack.is_some())
959 .finish_non_exhaustive()
960 }
961}
962
963impl IncomingMessage for RedisListMessage {
964 fn payload(&self) -> &[u8] {
965 &self.payload
966 }
967
968 fn headers(&self) -> &HeaderMap {
969 &self.headers
970 }
971
972 fn partition_key(&self) -> Option<&[u8]> {
974 Partitioned::partition_key(self)
975 }
976
977 async fn ack(self) -> Result<(), AckError> {
978 let Some(handle) = self.ack else {
979 return Err(AckError::Unsupported);
980 };
981 self.connection.ensure_open_to_settle()?;
982 settle(self.connection.pool(), &handle).await
983 }
984
985 async fn nack(self, requeue: bool) -> Result<(), AckError> {
986 let Some(handle) = self.ack else {
987 return Err(AckError::Unsupported);
988 };
989 self.connection.ensure_open_to_settle()?;
990 let pool = self.connection.pool();
991 if requeue {
992 lpush(pool, handle.main_key.as_str(), handle.value.clone()).await?;
995 }
996 settle(pool, &handle).await
997 }
998}
999
1000fn ack_broker(err: fred::error::Error) -> AckError {
1001 AckError::Broker(Box::new(err))
1002}
1003
1004async fn lpush(pool: &Pool, key: &str, body: Vec<u8>) -> Result<(), AckError> {
1005 let _: i64 = pool.lpush(key, body).await.map_err(ack_broker)?;
1006 Ok(())
1007}
1008
1009async fn settle(pool: &Pool, handle: &ListAck) -> Result<(), AckError> {
1012 let _: i64 = pool
1013 .lrem(handle.processing_key.as_str(), 1, handle.value.clone())
1014 .await
1015 .map_err(ack_broker)?;
1016 if let Some(rec) = &handle.recovery {
1017 recovery::forget(pool, &rec.zset_key, &rec.member).await?;
1018 }
1019 Ok(())
1020}
1021
1022impl Partitioned for RedisListMessage {
1023 fn partition_key(&self) -> Option<&[u8]> {
1024 self.headers().get(PARTITION_KEY_HEADER)
1025 }
1026}
1027
1028#[derive(Clone, Default)]
1079#[must_use]
1080pub struct RedisListPublish {
1081 codec: Option<SharedEnvelope>,
1082 ttl: Option<Duration>,
1083}
1084
1085impl Debug for RedisListPublish {
1086 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1087 f.debug_struct("RedisListPublish")
1088 .field("codec", &self.codec.is_some())
1089 .field("ttl", &self.ttl)
1090 .finish()
1091 }
1092}
1093
1094impl RedisListPublish {
1095 pub fn new() -> Self {
1097 Self::default()
1098 }
1099
1100 pub fn codec(mut self, codec: impl Codec + 'static) -> Self {
1104 self.codec = Some(Arc::new(codec));
1105 self
1106 }
1107
1108 pub const fn ttl(mut self, ttl: Duration) -> Self {
1116 self.ttl = Some(ttl);
1117 self
1118 }
1119}
1120
1121impl PublishPolicy<ConnectedRedisBroker> for RedisListPublish {
1122 type Live = RedisListPublisher;
1123
1124 fn pair(
1125 self,
1126 connected: &ConnectedRedisBroker,
1127 ) -> impl Future<Output = Result<Self::Live, PairError>> {
1128 ready(Ok(connected.list_publisher(self)))
1129 }
1130
1131 #[cfg(feature = "asyncapi")]
1134 fn channel_bindings(&self, channel: &str) -> Bindings {
1135 crate::asyncapi::channel(&crate::asyncapi::Publish::list(
1136 channel,
1137 self.ttl
1138 .map(|ttl| u64::try_from(ttl.as_millis()).unwrap_or(u64::MAX)),
1139 crate::asyncapi::Envelope::of(self.codec.as_ref()),
1140 ))
1141 }
1142}
1143
1144#[derive(Clone)]
1152pub struct RedisListPublisher {
1153 core: Arc<RedisCore>,
1154 codec: Option<SharedEnvelope>,
1155 ttl: Option<Duration>,
1156 round_name: u64,
1158}
1159
1160impl Debug for RedisListPublisher {
1161 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1162 f.debug_struct("RedisListPublisher")
1163 .field("codec", &self.codec.is_some())
1164 .field("ttl", &self.ttl)
1165 .finish_non_exhaustive()
1166 }
1167}
1168
1169impl RedisListPublisher {
1170 pub(crate) const fn round_name(&self) -> u64 {
1172 self.round_name
1173 }
1174
1175 pub(crate) fn new(core: Arc<RedisCore>, publish: RedisListPublish) -> Self {
1176 Self {
1177 round_name: core.rounds().publisher(),
1178 core,
1179 codec: publish.codec,
1180 ttl: publish.ttl,
1181 }
1182 }
1183}
1184
1185fn ttl_millis(ttl: Duration) -> i64 {
1188 i64::try_from(ttl.as_millis()).unwrap_or(i64::MAX).max(1)
1189}
1190
1191impl ruststream::Publisher for RedisListPublisher {
1192 type Payload = Lend;
1195
1196 type Error = RedisError;
1197 type Options = RedisPublishOptions;
1201
1202 async fn publish(
1203 &self,
1204 msg: ruststream::OutgoingMessage<'_>,
1205 options: Option<&Self::Options>,
1206 ) -> Result<(), Self::Error> {
1207 let (key, payload, headers) = msg.into_parts();
1208 if let Some(round) = self
1209 .core
1210 .rounds()
1211 .joined(self.round_name, joins_round(options))
1212 {
1213 let body = frame(
1214 self.codec.as_ref(),
1215 payload,
1216 &resolved_headers(headers, options),
1217 );
1218 return round
1219 .lpush(key, body, self.ttl.map(ttl_millis))
1220 .await
1221 .map_err(RedisError::publish);
1222 }
1223 push(
1224 &self.core,
1225 self.codec.as_ref(),
1226 self.ttl,
1227 key,
1228 payload,
1229 resolved_headers(headers, options),
1230 )
1231 .await
1232 }
1233}
1234
1235pub(crate) async fn push(
1238 core: &RedisCore,
1239 codec: Option<&SharedEnvelope>,
1240 ttl: Option<Duration>,
1241 key: &str,
1242 payload: &[u8],
1243 headers: HeaderMap,
1244) -> Result<(), RedisError> {
1245 let pool = core.pool()?;
1246 let body = frame(codec, payload, &headers);
1247 let Some(ttl) = ttl else {
1248 let _: i64 = pool.lpush(key, body).await.map_err(RedisError::publish)?;
1249 return Ok(());
1250 };
1251 let pipeline = pool.next().pipeline();
1254 let _: () = pipeline
1255 .lpush(key, body)
1256 .await
1257 .map_err(RedisError::publish)?;
1258 let _: () = pipeline
1259 .pexpire(key, ttl_millis(ttl), None)
1260 .await
1261 .map_err(RedisError::publish)?;
1262 let _: Vec<Value> = pipeline.all().await.map_err(RedisError::publish)?;
1263 Ok(())
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268 use super::*;
1269
1270 #[test]
1271 fn ttl_millis_converts_and_clamps() {
1272 assert_eq!(ttl_millis(Duration::from_mins(1)), 60_000);
1273 assert_eq!(ttl_millis(Duration::from_millis(1)), 1);
1274 assert_eq!(ttl_millis(Duration::from_nanos(1)), 1);
1276 assert_eq!(ttl_millis(Duration::ZERO), 1);
1277 }
1278}