Skip to main content

ruststream_fred/pipeline/
mod.rs

1//! Consumer-side pipelining: a subscription settles in a window, and a handler queues its own
2//! Redis commands into the delivery's segment of that window.
3//!
4//! `.pipeline()` where the handler is mounted opens the window (see [`RedisPipelineSteps`]): the
5//! settles of the deliveries in flight, and the commands their handlers queued through
6//! [`keys::Pipeline`](crate::context::keys::Pipeline), leave together, in one pipeline on a
7//! connection of the pool, once the read's `COUNT` has settled, once nothing is outstanding, or
8//! when the subscription stops. `.atomic()` after it makes each delivery's segment one `MULTI` /
9//! `EXEC`.
10//!
11//! What a handler queued follows the delivery's outcome: `ack` commits it with the settle, and
12//! every other outcome discards it.
13
14mod commands;
15mod descriptors;
16mod forms;
17mod message;
18mod rounds;
19mod source;
20#[cfg(all(test, feature = "testing"))]
21mod tests;
22mod window;
23
24use std::fmt::{Debug, Formatter};
25use std::marker::PhantomData;
26use std::ops::Deref;
27
28use fred::bytes_utils::Str;
29use fred::clients::{Client, Pipeline};
30use fred::error::Error;
31use fred::interfaces::ClientLike;
32use fred::types::{ClusterHash, CustomCommand, MultipleKeys, MultipleValues, Value};
33use ruststream::StartAt;
34use ruststream::runtime::{
35    Declared, ForReply, Outgoing, PublishContext, PublishTransform, Reads, SubscriberBuilder,
36    SubscriberSettings,
37};
38
39use crate::partition::RedisPublishOptions;
40
41pub use descriptors::{
42    RedisListAtomic, RedisListPipeline, RedisPubSubAtomic, RedisPubSubPatternAtomic,
43    RedisPubSubPatternPipeline, RedisPubSubPipeline, RedisStreamAtomic, RedisStreamPipeline,
44};
45pub(crate) use forms::{ListForm, PubSubForm, StreamForm};
46pub use message::RoundMessage;
47pub(crate) use rounds::Rounds;
48pub use source::PipelinedSubscriber;
49pub(crate) use window::{Form, Owner, Round, Segment, Window};
50
51/// A subscription source with a window, as the `.pipeline()` mount step makes it.
52///
53/// The descriptor is a [`RedisStream`](crate::RedisStream), a [`RedisList`](crate::RedisList), a
54/// [`RedisPubSub`](crate::RedisPubSub) or a [`RedisPubSubPattern`](crate::RedisPubSubPattern).
55/// `Mode` is [`Plain`] until `.atomic()` makes it [`Atomic`]. A service does not build one: it
56/// names the descriptor in `#[subscriber(..)]` and chains [`pipeline`](RedisPipelineSteps::pipeline)
57/// where it mounts the handler. The type shows up in the builder's type, under the names
58/// [`RedisStreamPipeline`] and its siblings.
59///
60/// # Examples
61///
62/// ```
63/// # mod demo {
64/// use ruststream_fred::stream::prelude::*;
65/// # #[derive(serde::Deserialize)]
66/// # struct Order { id: u64 }
67///
68/// #[subscriber(RedisStream::new("orders").group("workers"))]
69/// async fn record(order: &Order, Ctx(pipeline): Ctx<keys::Pipeline>) -> HandlerOutcome {
70///     if pipeline.hset("orders:by-id", (order.id.to_string(), "seen")).await.is_err() {
71///         return HandlerOutcome::retry();
72///     }
73///     HandlerOutcome::ack()
74/// }
75///
76/// fn app() -> RustStream {
77///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
78///         RedisBroker::standalone("redis://localhost:6379"),
79///         |b| {
80///             b.include(record.pipeline());
81///         },
82///     )
83/// }
84/// # }
85/// ```
86#[must_use]
87pub struct Pipelined<Descriptor, Mode = Plain> {
88    descriptor: Descriptor,
89    mode: PhantomData<fn() -> Mode>,
90}
91
92impl<Descriptor: Clone, Mode> Clone for Pipelined<Descriptor, Mode> {
93    fn clone(&self) -> Self {
94        Self {
95            descriptor: self.descriptor.clone(),
96            mode: PhantomData,
97        }
98    }
99}
100
101impl<Descriptor: Debug, Mode: WindowMode> Debug for Pipelined<Descriptor, Mode> {
102    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
103        f.debug_struct("Pipelined")
104            .field("descriptor", &self.descriptor)
105            .field("atomic", &Mode::ATOMIC)
106            .finish()
107    }
108}
109
110impl<Descriptor, Mode> Pipelined<Descriptor, Mode> {
111    pub(crate) const fn wrap(descriptor: Descriptor) -> Self {
112        Self {
113            descriptor,
114            mode: PhantomData,
115        }
116    }
117
118    /// The descriptor the window was opened on.
119    pub(crate) const fn descriptor(&self) -> &Descriptor {
120        &self.descriptor
121    }
122
123    pub(crate) fn into_descriptor(self) -> Descriptor {
124        self.descriptor
125    }
126
127    /// The same window over the descriptor `step` returns: a base setting chained at the mount
128    /// site after `.pipeline()`.
129    pub(crate) fn map(self, step: impl FnOnce(Descriptor) -> Descriptor) -> Self {
130        Self::wrap(step(self.descriptor))
131    }
132}
133
134/// The window's mode before `.atomic()`: a segment is the handler's commands, in the window's
135/// pipeline.
136#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
137pub struct Plain;
138
139/// The window's mode after `.atomic()`: a segment is `MULTI`, the handler's commands, the settle,
140/// `EXEC`.
141#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
142pub struct Atomic;
143
144mod sealed {
145    pub trait Sealed {}
146    impl Sealed for super::Plain {}
147    impl Sealed for super::Atomic {}
148}
149
150/// The two modes of a window, [`Plain`] and [`Atomic`].
151///
152/// # Examples
153///
154/// ```
155/// # mod demo {
156/// use ruststream_fred::stream::prelude::*;
157/// # #[derive(serde::Deserialize)]
158/// # struct Order { id: u64 }
159///
160/// #[subscriber(RedisStream::new("{orders}").group("workers"))]
161/// async fn invoice(order: &Order, Ctx(pipeline): Ctx<keys::Pipeline>) -> HandlerOutcome {
162///     match pipeline.lpush("{orders}:invoices", order.id.to_string()).await {
163///         Ok(()) => HandlerOutcome::ack(),
164///         Err(_) => HandlerOutcome::retry(),
165///     }
166/// }
167///
168/// fn app() -> RustStream {
169///     RustStream::new(AppInfo::new("invoices", "0.1.0")).with_broker(
170///         RedisBroker::standalone("redis://localhost:6379"),
171///         |b| {
172///             // `.pipeline()` opens the window as `Plain`, and `.atomic()` makes it `Atomic`.
173///             b.include(invoice.pipeline().atomic());
174///         },
175///     )
176/// }
177/// # }
178/// ```
179pub trait WindowMode: sealed::Sealed + Send + Sync + 'static {
180    /// Whether a segment is wrapped in `MULTI` / `EXEC`.
181    const ATOMIC: bool;
182}
183
184impl WindowMode for Plain {
185    const ATOMIC: bool = false;
186}
187
188impl WindowMode for Atomic {
189    const ATOMIC: bool = true;
190}
191
192mod windowable {
193    /// The descriptors a window opens on, and nothing else.
194    #[diagnostic::on_unimplemented(
195        message = "`.pipeline()` opens a window on a `RedisStream`, `RedisList`, `RedisPubSub` or \
196                   `RedisPubSubPattern` subscription, and this one is `{Self}`",
197        label = "no window opens here",
198        note = "a subscription has one window: chain `.pipeline()` once, on the descriptor the \
199                attribute names"
200    )]
201    pub trait Base {}
202    impl Base for crate::RedisStream {}
203    impl Base for crate::RedisList {}
204    impl Base for crate::RedisPubSub {}
205    impl Base for crate::RedisPubSubPattern {}
206}
207
208/// A subscription source a window can open on: a descriptor of this crate, possibly under the
209/// core's `start_at(..)` wrapper.
210#[diagnostic::on_unimplemented(
211    message = "`.pipeline()` opens a window on a subscription of ruststream-fred, and `{Self}` is not one",
212    label = "no window opens on this source",
213    note = "name a `RedisStream`, `RedisList`, `RedisPubSub` or `RedisPubSubPattern` in \
214            `#[subscriber(..)]`, and chain `.pipeline()` once"
215)]
216pub trait OpensWindow {
217    /// The same source with a window.
218    type Pipelined;
219
220    /// Opens the window.
221    fn open_window(self) -> Self::Pipelined;
222}
223
224impl<Descriptor: windowable::Base> OpensWindow for Descriptor {
225    type Pipelined = Pipelined<Descriptor, Plain>;
226
227    fn open_window(self) -> Self::Pipelined {
228        Pipelined::wrap(self)
229    }
230}
231
232impl<Source: OpensWindow, Position> OpensWindow for StartAt<Source, Position> {
233    type Pipelined = StartAt<Source::Pipelined, Position>;
234
235    fn open_window(self) -> Self::Pipelined {
236        self.map_inner(OpensWindow::open_window)
237    }
238}
239
240/// A source that already has a window, which is what `.atomic()` needs.
241#[diagnostic::on_unimplemented(
242    message = "`.atomic()` needs `.pipeline()` first",
243    label = "this subscription has no pipeline",
244    note = "write `.pipeline().atomic()`: a transaction is one segment of the window's pipeline, \
245            and a subscription without a window has no pipeline to put it in"
246)]
247pub trait Windowed {
248    /// The same source with an atomic window.
249    type Atomic;
250
251    /// Makes the window atomic.
252    fn into_atomic(self) -> Self::Atomic;
253}
254
255impl<Descriptor> Windowed for Pipelined<Descriptor, Plain> {
256    type Atomic = Pipelined<Descriptor, Atomic>;
257
258    fn into_atomic(self) -> Self::Atomic {
259        Pipelined::wrap(self.descriptor)
260    }
261}
262
263impl<Source: Windowed, Position> Windowed for StartAt<Source, Position> {
264    type Atomic = StartAt<Source::Atomic, Position>;
265
266    fn into_atomic(self) -> Self::Atomic {
267        self.map_inner(Windowed::into_atomic)
268    }
269}
270
271/// The builder step behind [`RedisPipelineSteps::pipeline`].
272#[diagnostic::on_unimplemented(
273    message = "`.pipeline()` opens a window on a subscription of ruststream-fred, and this \
274               registration is not one",
275    label = "no window opens on this registration",
276    note = "name a `RedisStream`, `RedisList`, `RedisPubSub` or `RedisPubSubPattern` in \
277            `#[subscriber(..)]`, and chain `.pipeline()` once"
278)]
279pub trait PipelineStep {
280    /// The builder over the pipelined source.
281    type Out;
282
283    /// Opens the window on the builder's source.
284    fn apply_pipeline(self) -> Self::Out;
285}
286
287impl<Def, Source, State, DefCodec> PipelineStep for SubscriberBuilder<Def, Source, State, DefCodec>
288where
289    Def: Declared,
290    Source: OpensWindow,
291{
292    type Out = SubscriberBuilder<Def, Source::Pipelined, State, DefCodec>;
293
294    fn apply_pipeline(self) -> Self::Out {
295        self.map_source(OpensWindow::open_window)
296    }
297}
298
299/// The builder step behind [`RedisPipelineSteps::atomic`].
300#[diagnostic::on_unimplemented(
301    message = "`.atomic()` needs `.pipeline()` first",
302    label = "this subscription has no pipeline",
303    note = "write `.pipeline().atomic()`: a transaction is one segment of the window's pipeline, \
304            and a subscription without a window has no pipeline to put it in"
305)]
306pub trait AtomicStep {
307    /// The builder over the source with an atomic window.
308    type Out;
309
310    /// Makes the window atomic.
311    fn apply_atomic(self) -> Self::Out;
312}
313
314impl<Def, Source, State, DefCodec> AtomicStep for SubscriberBuilder<Def, Source, State, DefCodec>
315where
316    Def: Declared,
317    Source: Windowed,
318{
319    type Out = SubscriberBuilder<Def, Source::Atomic, State, DefCodec>;
320
321    fn apply_atomic(self) -> Self::Out {
322        self.map_source(Windowed::into_atomic)
323    }
324}
325
326/// The window steps of a mount site: `.pipeline()` opens a consumer window on the subscription,
327/// and `.atomic()` after it makes each delivery's segment one `MULTI` / `EXEC`.
328///
329/// The steps chain on the handler where it is mounted, like the core's settings, after the
330/// descriptor the attribute named and after any base setting such as
331/// [`block`](crate::RedisSubscribeExt::block). They apply to a [`RedisStream`](crate::RedisStream),
332/// a [`RedisList`](crate::RedisList), a [`RedisPubSub`](crate::RedisPubSub) and a
333/// [`RedisPubSubPattern`](crate::RedisPubSubPattern) alike; see [the module](self) for what the
334/// window does.
335///
336/// Under `.atomic()` a delivery's segment is `MULTI`, the commands its handler queued, the
337/// delivery's settle where the form has one, and `EXEC`, sent inside the window's pipeline, so the
338/// handler's Redis side effects and its acknowledgement happen together or not at all. It buys no
339/// rollback: Redis has none, and a command that fails inside `EXEC` (a `WRONGTYPE`, say) leaves
340/// the others executed.
341///
342/// On a cluster, Redis runs a transaction only when every key of it hashes to one slot. A
343/// segment's slot is the subscription key's, so a handler's keys share it through a hash tag
344/// (`{orders}:invoices` beside a stream `{orders}`). A command for another slot is answered with a
345/// redirection inside the transaction, and Redis then refuses the whole `EXEC`, so a segment is
346/// never executed in part.
347///
348/// A handler that reads `Ctx<keys::Pipeline>` needs `.pipeline()` at its mount site. Without it
349/// the mount does not compile, and the error is the framework's general one about the handler's
350/// context not matching the subscription's (`PipelineContext: BuildContext<RedisMessage>`).
351///
352/// # Examples
353///
354/// ```
355/// # mod demo {
356/// use ruststream_fred::stream::prelude::*;
357/// # #[derive(serde::Deserialize)]
358/// # struct Order { id: u64 }
359///
360/// #[subscriber(RedisStream::new("{orders}").group("workers"))]
361/// async fn invoice(order: &Order, Ctx(pipeline): Ctx<keys::Pipeline>) -> HandlerOutcome {
362///     if pipeline.lpush("{orders}:invoices", order.id.to_string()).await.is_err() {
363///         return HandlerOutcome::retry();
364///     }
365///     HandlerOutcome::ack()
366/// }
367///
368/// fn app() -> RustStream {
369///     RustStream::new(AppInfo::new("invoices", "0.1.0")).with_broker(
370///         RedisBroker::standalone("redis://localhost:6379"),
371///         |b| {
372///             b.include(invoice.pipeline().atomic());
373///         },
374///     )
375/// }
376/// # }
377/// ```
378///
379/// A subscription without a window has nothing to make atomic:
380///
381/// ```compile_fail
382/// # mod demo {
383/// use ruststream_fred::stream::prelude::*;
384/// # #[derive(serde::Deserialize)]
385/// # struct Order { id: u64 }
386///
387/// #[subscriber(RedisStream::new("orders").group("workers"))]
388/// async fn invoice(order: &Order) -> HandlerOutcome {
389///     let _ = order.id;
390///     HandlerOutcome::ack()
391/// }
392///
393/// fn app() -> RustStream {
394///     RustStream::new(AppInfo::new("invoices", "0.1.0")).with_broker(
395///         RedisBroker::standalone("redis://localhost:6379"),
396///         |b| {
397///             b.include(invoice.atomic());
398///         },
399///     )
400/// }
401/// # }
402/// ```
403pub trait RedisPipelineSteps: Declared {
404    /// Opens a consumer window on the subscription: its settles and the commands its handlers
405    /// queue through [`keys::Pipeline`](crate::context::keys::Pipeline) leave together, in one
406    /// pipeline on a connection of the pool.
407    ///
408    /// On a stream the window flushes once the read's `COUNT` has settled, once nothing is
409    /// outstanding, or when the subscription stops: under load one `XACK` per fetched batch. A
410    /// pipelined list reads in batches, and a reliable list's `LREM` settles ride the window.
411    /// Pub/Sub, on a channel or a pattern, settles nothing, so its window carries the handlers'
412    /// commands alone, and leaves once nothing is in flight.
413    fn pipeline(self) -> <Self::Settings as PipelineStep>::Out
414    where
415        Self::Settings: PipelineStep,
416    {
417        self.declare().apply_pipeline()
418    }
419
420    /// Makes each delivery's segment one `MULTI` / `EXEC`. Follows `.pipeline()`.
421    fn atomic(self) -> <Self::Settings as AtomicStep>::Out
422    where
423        Self::Settings: AtomicStep,
424    {
425        self.declare().apply_atomic()
426    }
427}
428
429impl<D: Declared> RedisPipelineSteps for D {}
430
431mod bindable {
432    /// What a publisher of this crate is named by in a binding.
433    pub trait Named {
434        fn round_name(&self) -> u64;
435    }
436}
437
438/// A publisher of this crate, which `pipeline.bind(&out)` can bind to a delivery's round.
439///
440/// Implemented by every publisher this crate's policies pair into, and by nothing else: a
441/// publisher of another broker publishes through its own connection, which no round of this one
442/// can carry. A handler bounds its slot with it, `Out(out): Out<impl Bindable>`, to bind it.
443///
444/// # Examples
445///
446/// ```
447/// # mod demo {
448/// use ruststream_fred::stream::prelude::*;
449/// use serde::Serialize;
450/// # #[derive(serde::Deserialize)]
451/// # struct Order { id: u64 }
452/// #[derive(Serialize, Outgoing)]
453/// #[outgoing(name = "audit")]
454/// struct Audit {
455///     id: u64,
456/// }
457///
458/// /// The audit entry leaves with the delivery's acknowledgement, and not without it.
459/// #[subscriber(RedisStream::new("orders").group("workers"))]
460/// async fn record(
461///     order: &Order,
462///     Ctx(pipeline): Ctx<keys::Pipeline>,
463///     Out(out): Out<impl Bindable>,
464/// ) -> HandlerOutcome {
465///     let out = pipeline.bind(out);
466///     if out.message(&Audit { id: order.id }).publish().await.is_err() {
467///         return HandlerOutcome::retry();
468///     }
469///     HandlerOutcome::ack()
470/// }
471///
472/// fn app() -> RustStream {
473///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
474///         RedisBroker::standalone("redis://localhost:6379"),
475///         |b| {
476///             b.include(record.pipeline()).out(DefaultSlot, Publish).build();
477///         },
478///     )
479/// }
480/// # }
481/// ```
482///
483/// A publisher of another transport has no round to join, so binding one does not compile:
484///
485/// ```compile_fail,E0277
486/// use std::convert::Infallible;
487///
488/// use ruststream::{Lend, OutgoingMessage, Publisher};
489/// use ruststream_fred::pipeline::Bindable;
490///
491/// struct Elsewhere;
492///
493/// impl Publisher for Elsewhere {
494///     type Payload = Lend;
495///     type Error = Infallible;
496///     type Options = ();
497///
498///     async fn publish(
499///         &self,
500///         _msg: OutgoingMessage<'_, &[u8]>,
501///         _options: Option<&()>,
502///     ) -> Result<(), Infallible> {
503///         Ok(())
504///     }
505/// }
506///
507/// fn bindable<P: Bindable>() {}
508///
509/// bindable::<Elsewhere>();
510/// ```
511#[diagnostic::on_unimplemented(
512    message = "`{Self}` is not a publisher of ruststream-fred, so it cannot join a delivery's round",
513    label = "this slot publishes through another broker",
514    note = "bind a slot whose policy is one of this crate's (`stream::Publish`, `list::Publish`, \
515            `pubsub::Publish`), and bound the slot parameter as `Out<impl Bindable>`"
516)]
517pub trait Bindable: ruststream::Publisher + bindable::Named {}
518
519impl<T: ruststream::Publisher + bindable::Named> Bindable for T {}
520
521impl bindable::Named for crate::RedisPublisher {
522    fn round_name(&self) -> u64 {
523        self.round_name()
524    }
525}
526
527impl bindable::Named for crate::RedisListPublisher {
528    fn round_name(&self) -> u64 {
529        self.round_name()
530    }
531}
532
533impl bindable::Named for crate::RedisPubSubPublisher {
534    fn round_name(&self) -> u64 {
535        self.round_name()
536    }
537}
538
539impl bindable::Named for crate::RedisDefaultPublisher {
540    fn round_name(&self) -> u64 {
541        self.round_name()
542    }
543}
544
545/// The reply transform that puts a reply in the round of the delivery it answers.
546///
547/// A reply is published after the handler returns and before the delivery settles. Mounted on
548/// the reply position of a `.pipeline()` subscription, this transform queues it into the
549/// delivery's segment instead: it leaves with the window, after what the handler queued, and
550/// under `.atomic()` inside the delivery's `MULTI` / `EXEC`. On a subscription without a window
551/// the reply leaves at once, as it does without the transform.
552///
553/// # Examples
554///
555/// ```
556/// # mod demo {
557/// use ruststream_fred::stream::prelude::*;
558/// use serde::Serialize;
559/// # #[derive(serde::Deserialize)]
560/// # struct Order { id: u64 }
561/// #[derive(Serialize, Outgoing)]
562/// #[outgoing(name = "receipts")]
563/// struct Receipt {
564///     id: u64,
565/// }
566///
567/// #[subscriber(RedisStream::new("orders").group("workers"), reply)]
568/// async fn issue(order: &Order) -> Receipt {
569///     Receipt { id: order.id }
570/// }
571///
572/// fn app() -> RustStream {
573///     RustStream::new(AppInfo::new("receipts", "0.1.0")).with_broker(
574///         RedisBroker::standalone("redis://localhost:6379"),
575///         |b| {
576///             b.include(issue.pipeline()).out_reply(Publish).transform(InRound);
577///         },
578///     )
579/// }
580/// # }
581/// ```
582#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
583pub struct InRound;
584
585impl<C> PublishTransform<ForReply<C>, RedisPublishOptions> for InRound {
586    type Destination = Reads;
587
588    fn apply(
589        &self,
590        _out: &mut Outgoing<'_>,
591        options: &mut Option<RedisPublishOptions>,
592        _cx: &PublishContext<'_, C>,
593    ) {
594        options
595            .get_or_insert_with(RedisPublishOptions::default)
596            .join_round = true;
597    }
598}
599
600/// The delivery's own buffer in the window: what a handler queues its Redis commands into.
601///
602/// Reached as `Ctx<keys::Pipeline>` on a `.pipeline()` subscription. It carries `fred`'s command
603/// methods, each queueing the command and returning `Ok(())` once it is queued: the command runs
604/// after the handler has returned, and only if the delivery is acknowledged, so its reply is not
605/// available inside the handler. A command queued before `ack` executes before that `ack`. The
606/// window sends it; there is no method here that sends. For a command whose answer the handler needs now, take
607/// `Ctx<keys::FredPool>`.
608///
609/// The buffer is created on the first queued command, so a delivery whose handler queues nothing
610/// costs the window nothing.
611///
612/// # Examples
613///
614/// ```
615/// # mod demo {
616/// use ruststream_fred::stream::prelude::*;
617/// # #[derive(serde::Deserialize)]
618/// # struct Order { id: u64 }
619///
620/// /// Records the order and acknowledges it in one round trip with the rest of the window.
621/// #[subscriber(RedisStream::new("orders").group("workers"))]
622/// async fn record(order: &Order, Ctx(pipeline): Ctx<keys::Pipeline>) -> HandlerOutcome {
623///     if pipeline.hset("orders:by-id", (order.id.to_string(), "seen")).await.is_err() {
624///         return HandlerOutcome::retry();
625///     }
626///     HandlerOutcome::ack()
627/// }
628///
629/// fn app() -> RustStream {
630///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
631///         RedisBroker::standalone("redis://localhost:6379"),
632///         |b| {
633///             b.include(record.pipeline());
634///         },
635///     )
636/// }
637/// # }
638/// ```
639///
640/// A mount site without `.pipeline()` has no round to hand out, so the mount does not compile:
641///
642/// ```compile_fail
643/// # mod demo {
644/// use ruststream::prelude::*;
645/// use ruststream::subscriber;
646/// use ruststream_fred::context::keys;
647/// use ruststream_fred::RedisStream;
648/// # #[derive(serde::Deserialize)]
649/// # struct Order { id: u64 }
650///
651/// #[subscriber(RedisStream::new("orders").group("workers"))]
652/// async fn record(order: &Order, Ctx(pipeline): Ctx<keys::Pipeline>) -> HandlerOutcome {
653///     let _ = (order, pipeline);
654///     HandlerOutcome::ack()
655/// }
656///
657/// fn app() -> RustStream {
658///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
659///         ruststream_fred::RedisBroker::standalone("redis://localhost:6379"),
660///         |b| {
661///             b.include(record);
662///         },
663///     )
664/// }
665/// # }
666/// ```
667#[derive(Clone)]
668pub struct RedisPipeline {
669    round: Round,
670}
671
672impl Debug for RedisPipeline {
673    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
674        f.debug_struct("RedisPipeline").finish_non_exhaustive()
675    }
676}
677
678impl RedisPipeline {
679    pub(crate) const fn new(round: Round) -> Self {
680        Self { round }
681    }
682
683    /// The delivery's segment, created on first use.
684    async fn segment(&self) -> Result<Segment, Error> {
685        self.round.segment().await
686    }
687
688    /// Binds a slot's publisher to this delivery's round, and hands the slot back.
689    ///
690    /// What the slot publishes from this delivery's handler is queued into the delivery's segment
691    /// from then on: it leaves with the window, after what was queued before it, only if the
692    /// delivery is acknowledged, and under `.atomic()` inside the delivery's `MULTI` / `EXEC`.
693    /// The slot keeps its codec and its transforms. A slot publish the handler does not bind
694    /// leaves at once.
695    ///
696    /// See [`Bindable`] for a handler that binds its slot.
697    ///
698    /// # Examples
699    ///
700    /// ```
701    /// # mod demo {
702    /// use ruststream_fred::stream::prelude::*;
703    /// use serde::Serialize;
704    /// # #[derive(serde::Deserialize)]
705    /// # struct Order { id: u64 }
706    /// #[derive(Serialize, Outgoing)]
707    /// #[outgoing(name = "audit")]
708    /// struct Audit {
709    ///     id: u64,
710    /// }
711    ///
712    /// #[subscriber(RedisStream::new("orders").group("workers"))]
713    /// async fn record(
714    ///     order: &Order,
715    ///     Ctx(pipeline): Ctx<keys::Pipeline>,
716    ///     Out(out): Out<impl Bindable>,
717    /// ) -> HandlerOutcome {
718    ///     // Queued into the delivery's segment: the audit entry leaves only with the `XACK`.
719    ///     match pipeline.bind(out).message(&Audit { id: order.id }).publish().await {
720    ///         Ok(()) => HandlerOutcome::ack(),
721    ///         Err(_) => HandlerOutcome::retry(),
722    ///     }
723    /// }
724    /// #
725    /// # fn app() -> RustStream {
726    /// #     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
727    /// #         RedisBroker::standalone("redis://localhost:6379"),
728    /// #         |b| {
729    /// #             b.include(record.pipeline()).out(DefaultSlot, Publish).build();
730    /// #         },
731    /// #     )
732    /// # }
733    /// # }
734    /// ```
735    pub fn bind<'o, O>(&self, out: &'o O) -> &'o O
736    where
737        O: Deref,
738        O::Target: Bindable,
739    {
740        self.round.bind(bindable::Named::round_name(&**out));
741        out
742    }
743
744    /// Queues a Lua script run by its SHA1 digest (`EVALSHA`), with its keys and arguments.
745    ///
746    /// Queued into this delivery's segment; it runs after the handler returns, when the delivery
747    /// is acknowledged. On a cluster the command goes to the node of the first key.
748    ///
749    /// # Errors
750    ///
751    /// Returns `fred`'s error when the arguments do not convert, or when the delivery has already
752    /// settled.
753    ///
754    /// # Examples
755    ///
756    /// ```
757    /// # mod demo {
758    /// use ruststream_fred::stream::prelude::*;
759    /// # #[derive(serde::Deserialize)]
760    /// # struct Order { id: u64 }
761    ///
762    /// /// The digest of a script the service loaded with `SCRIPT LOAD` at startup.
763    /// const MARK_SEEN: &str = "a42059b356c875f0717db19a51f6aaca9ae659ea";
764    ///
765    /// #[subscriber(RedisStream::new("orders").group("workers"))]
766    /// async fn record(order: &Order, Ctx(pipeline): Ctx<keys::Pipeline>) -> HandlerOutcome {
767    ///     let id = order.id.to_string();
768    ///     match pipeline.evalsha(MARK_SEEN, vec!["orders:seen"], vec![id]).await {
769    ///         Ok(()) => HandlerOutcome::ack(),
770    ///         Err(_) => HandlerOutcome::retry(),
771    ///     }
772    /// }
773    /// #
774    /// # fn app() -> RustStream {
775    /// #     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
776    /// #         RedisBroker::standalone("redis://localhost:6379"),
777    /// #         |b| {
778    /// #             b.include(record.pipeline());
779    /// #         },
780    /// #     )
781    /// # }
782    /// # }
783    /// ```
784    pub async fn evalsha<S, K, V>(&self, hash: S, keys: K, args: V) -> Result<(), Error>
785    where
786        S: Into<Str> + Send,
787        K: Into<MultipleKeys> + Send,
788        V: TryInto<MultipleValues> + Send,
789        V::Error: Into<Error> + Send,
790    {
791        let args = args.try_into().map_err(Into::into)?;
792        self.script("EVALSHA", hash.into(), keys.into(), args).await
793    }
794
795    /// Queues a Lua script run by its source (`EVAL`), with its keys and arguments.
796    ///
797    /// Queued into this delivery's segment; it runs after the handler returns, when the delivery
798    /// is acknowledged. On a cluster the command goes to the node of the first key.
799    ///
800    /// # Errors
801    ///
802    /// Returns `fred`'s error when the arguments do not convert, or when the delivery has already
803    /// settled.
804    ///
805    /// # Examples
806    ///
807    /// ```
808    /// # mod demo {
809    /// use ruststream_fred::stream::prelude::*;
810    /// # #[derive(serde::Deserialize)]
811    /// # struct Order { id: u64 }
812    ///
813    /// const MARK_SEEN: &str = "return redis.call('SADD', KEYS[1], ARGV[1])";
814    ///
815    /// #[subscriber(RedisStream::new("orders").group("workers"))]
816    /// async fn record(order: &Order, Ctx(pipeline): Ctx<keys::Pipeline>) -> HandlerOutcome {
817    ///     let id = order.id.to_string();
818    ///     match pipeline.eval(MARK_SEEN, vec!["orders:seen"], vec![id]).await {
819    ///         Ok(()) => HandlerOutcome::ack(),
820    ///         Err(_) => HandlerOutcome::retry(),
821    ///     }
822    /// }
823    /// #
824    /// # fn app() -> RustStream {
825    /// #     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
826    /// #         RedisBroker::standalone("redis://localhost:6379"),
827    /// #         |b| {
828    /// #             b.include(record.pipeline());
829    /// #         },
830    /// #     )
831    /// # }
832    /// # }
833    /// ```
834    pub async fn eval<S, K, V>(&self, script: S, keys: K, args: V) -> Result<(), Error>
835    where
836        S: Into<Str> + Send,
837        K: Into<MultipleKeys> + Send,
838        V: TryInto<MultipleValues> + Send,
839        V::Error: Into<Error> + Send,
840    {
841        let args = args.try_into().map_err(Into::into)?;
842        self.script("EVAL", script.into(), keys.into(), args).await
843    }
844
845    /// A script run as `fred` sends one: the script or its digest, the key count, the keys and
846    /// the arguments, routed by the first key.
847    async fn script(
848        &self,
849        command: &'static str,
850        body: Str,
851        keys: MultipleKeys,
852        args: Value,
853    ) -> Result<(), Error> {
854        let keys = keys.inner();
855        let hash = keys.first().map_or(ClusterHash::Random, |key| {
856            ClusterHash::Custom(key.cluster_hash())
857        });
858        let mut values = Vec::with_capacity(2 + keys.len());
859        values.push(Value::from(body));
860        values.push(Value::from(i64::try_from(keys.len()).unwrap_or(i64::MAX)));
861        values.extend(keys.into_iter().map(Value::from));
862        match args {
863            Value::Array(args) => values.extend(args),
864            Value::Null => {}
865            arg => values.push(arg),
866        }
867        self.custom(CustomCommand::new_static(command, hash, false), values)
868            .await
869    }
870
871    /// Queues a command this facade does not name, by its name and its arguments.
872    ///
873    /// Queued into this delivery's segment; it runs after the handler returns, when the delivery
874    /// is acknowledged.
875    ///
876    /// # Errors
877    ///
878    /// Returns `fred`'s error when the arguments do not convert, or when the delivery has already
879    /// settled.
880    ///
881    /// # Examples
882    ///
883    /// ```
884    /// # mod demo {
885    /// use fred::types::{ClusterHash, CustomCommand};
886    /// use ruststream_fred::stream::prelude::*;
887    /// # #[derive(serde::Deserialize)]
888    /// # struct Order { id: u64 }
889    ///
890    /// #[subscriber(RedisStream::new("orders").group("workers"))]
891    /// async fn record(order: &Order, Ctx(pipeline): Ctx<keys::Pipeline>) -> HandlerOutcome {
892    ///     // Refreshes the order's last-access time, so an LRU eviction keeps it.
893    ///     let touch = CustomCommand::new_static("TOUCH", ClusterHash::FirstKey, false);
894    ///     match pipeline.custom(touch, vec![format!("order:{}", order.id)]).await {
895    ///         Ok(()) => HandlerOutcome::ack(),
896    ///         Err(_) => HandlerOutcome::retry(),
897    ///     }
898    /// }
899    /// #
900    /// # fn app() -> RustStream {
901    /// #     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
902    /// #         RedisBroker::standalone("redis://localhost:6379"),
903    /// #         |b| {
904    /// #             b.include(record.pipeline());
905    /// #         },
906    /// #     )
907    /// # }
908    /// # }
909    /// ```
910    pub async fn custom<T>(&self, command: CustomCommand, args: Vec<T>) -> Result<(), Error>
911    where
912        T: TryInto<Value> + Send,
913        T::Error: Into<Error> + Send,
914    {
915        match self.segment().await? {
916            Segment::Plain(segment) => segment.custom::<(), T>(command, args).await,
917            Segment::Atomic(segment) => segment.custom::<(), T>(command, args).await,
918        }
919    }
920}
921
922/// A fresh `fred` pipeline on `client`, for one segment or one flush's settles.
923pub(crate) fn pipeline_on(client: &Client) -> Pipeline<Client> {
924    client.pipeline()
925}