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