ruststream_fred/publisher.rs
1//! Publishes messages to Redis streams via `XADD`, with both transaction kinds on top: the
2//! borrowed [`TransactionalPublisher`] on the handle and the owned [`OwnedTransactions`] value.
3
4use std::fmt::{Debug, Formatter};
5use std::future::{Future, ready};
6use std::sync::{Arc, Mutex};
7
8use bytes::BytesMut;
9use fred::interfaces::{StreamsInterface, TransactionInterface};
10use fred::types::Value;
11#[cfg(feature = "asyncapi")]
12use ruststream::asyncapi::Bindings;
13use ruststream::{
14 DefaultPublish, HeaderMap, OutgoingMessage, OwnedTransactions, PairError, PublishPolicy,
15 Publisher, Take, Transaction, TransactionalPublisher,
16};
17use tracing::warn;
18
19use crate::broker::{ConnectedRedisBroker, RedisCore};
20use crate::envelope::frame;
21use crate::list::push;
22use crate::partition::{RedisPublishOptions, resolved_headers};
23use crate::pubsub::PubSubMode;
24use crate::pubsub::send;
25use crate::route::Route;
26use crate::{convert::fields_for_publish, error::RedisError};
27
28/// One buffered `XADD` (stream key plus its encoded entry fields), held while a transaction is open.
29type Buffered = (String, Vec<(String, Vec<u8>)>);
30
31/// Flushes `buffered` as one `MULTI` / `EXEC` block, in publish order.
32///
33/// The single flush path of both transaction kinds: they differ only in where the buffer lives
34/// (the handle's slot for the borrowed kind, the [`RedisTransaction`] value for the owned one),
35/// never in what a commit does.
36///
37/// # Errors
38///
39/// Returns [`RedisError::ShutDown`] when the connection is gone, or [`RedisError::Publish`] when
40/// the block is rejected.
41async fn flush_block(core: &RedisCore, buffered: Vec<Buffered>) -> Result<(), RedisError> {
42 if buffered.is_empty() {
43 return Ok(());
44 }
45 let pool = core.pool()?;
46 let txn = pool.next().multi();
47 for (key, fields) in buffered {
48 // Queued client-side by `fred`; the whole block travels on one connection at `exec`.
49 let _: () = txn
50 .xadd(key, false, None::<()>, "*", fields)
51 .await
52 .map_err(RedisError::publish)?;
53 }
54 // `abort_on_error = true`: a command the server refuses to queue discards the block instead
55 // of committing a partial one.
56 let _: Value = txn.exec(true).await.map_err(RedisError::publish)?;
57 Ok(())
58}
59
60/// The declaration half of the stream publisher: pure policy, constructible anywhere.
61///
62/// `XADD` needs no options beyond the target key, which travels on each message, so the policy is
63/// a unit marker. It pairs into a [`RedisPublisher`] against a [`ConnectedRedisBroker`], which is
64/// what makes "publishing before connect" unrepresentable.
65///
66/// # Examples
67///
68/// ```
69/// # mod demo {
70/// use ruststream_fred::stream::prelude::*;
71/// use serde::{Deserialize, Serialize};
72///
73/// #[derive(Deserialize)]
74/// struct Order {
75/// id: u64,
76/// }
77///
78/// #[derive(Serialize, Outgoing)]
79/// #[outgoing(name = "orders.accepted")]
80/// struct Accepted {
81/// id: u64,
82/// }
83///
84/// #[subscriber(RedisStream::new("orders").group("workers"), reply)]
85/// async fn accept(order: &Order) -> Accepted {
86/// Accepted { id: order.id }
87/// }
88///
89/// #[ruststream::app]
90/// fn app() -> impl App {
91/// RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
92/// RedisBroker::standalone("redis://localhost:6379"),
93/// |b| {
94/// // `Publish` is this policy under the stream prelude's name. No connection exists
95/// // yet; the runtime pairs it with the connected broker at startup.
96/// b.include(accept).out_reply(Publish);
97/// },
98/// )
99/// }
100/// # }
101/// # fn main() {}
102/// ```
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
104#[must_use]
105pub struct RedisPublish;
106
107impl PublishPolicy<ConnectedRedisBroker> for RedisPublish {
108 type Live = RedisPublisher;
109
110 fn pair(
111 self,
112 connected: &ConnectedRedisBroker,
113 ) -> impl Future<Output = Result<Self::Live, PairError>> {
114 ready(Ok(connected.publisher()))
115 }
116
117 /// An `XADD` carries its headers as entry fields and the policy has no settings of its own, so
118 /// what the document learns from here is which Redis structure the channel is and which stream
119 /// key the entries are appended to.
120 #[cfg(feature = "asyncapi")]
121 fn channel_bindings(&self, channel: &str) -> Bindings {
122 crate::asyncapi::channel(&crate::asyncapi::Publish::stream(channel))
123 }
124}
125
126/// The broker's default policy, what a registration publishes through when its mount site names
127/// none: a reply, a retry copy, a dead-letter move.
128///
129/// It writes each name the way this service reads it. A name a [`RedisList`] subscription reads,
130/// or dead-letters to, is `LPUSH`ed in that subscription's framing; a name a [`RedisPubSub`]
131/// subscription reads or dead-letters to is published on in its mode and framing; any other name,
132/// a stream key included, is `XADD`ed. So a retry copy and a dead-letter move leave through the
133/// publish of the form the delivery came from, with nothing named at the mount site.
134///
135/// The route is recorded when a subscription opens, so a name this service does not subscribe to
136/// is a stream. A pattern subscription records nothing: its mount site names both the destination
137/// and the policy of its copies.
138///
139/// # Examples
140///
141/// ```
142/// # mod demo {
143/// use ruststream_fred::RedisList;
144/// use ruststream_fred::stream::prelude::*;
145/// use serde::{Deserialize, Serialize};
146///
147/// #[derive(Deserialize)]
148/// struct Order {
149/// id: u64,
150/// }
151///
152/// #[derive(Deserialize, Serialize, Outgoing)]
153/// #[outgoing(name = "jobs")]
154/// struct Job {
155/// order: u64,
156/// }
157///
158/// #[subscriber(RedisStream::new("orders").group("planners"), reply)]
159/// async fn plan(order: &Order) -> Job {
160/// Job { order: order.id }
161/// }
162///
163/// #[subscriber(RedisList::new("jobs").reliable())]
164/// async fn work(job: &Job) -> HandlerOutcome {
165/// println!("job for order {}", job.order);
166/// HandlerOutcome::ack()
167/// }
168///
169/// #[ruststream::app]
170/// fn app() -> impl App {
171/// RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
172/// RedisBroker::standalone("redis://localhost:6379"),
173/// |b| {
174/// // The reply names no policy, so it leaves through the default one: `jobs` is
175/// // read as a list here, so the reply is an `LPUSH`.
176/// b.include(plan);
177/// b.include(work);
178/// },
179/// )
180/// }
181/// # }
182/// # fn main() {}
183/// ```
184///
185/// [`RedisList`]: crate::RedisList
186/// [`RedisPubSub`]: crate::RedisPubSub
187#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
188#[must_use]
189pub struct RedisDefaultPublish;
190
191impl PublishPolicy<ConnectedRedisBroker> for RedisDefaultPublish {
192 type Live = RedisDefaultPublisher;
193
194 fn pair(
195 self,
196 connected: &ConnectedRedisBroker,
197 ) -> impl Future<Output = Result<Self::Live, PairError>> {
198 ready(Ok(connected.default_publisher()))
199 }
200
201 /// Described as the stream publish, the family of a name no subscription of the service reads;
202 /// the document is built before any subscription opens, so a name that turns out to be read as
203 /// a list or a channel is described as a stream.
204 #[cfg(feature = "asyncapi")]
205 fn channel_bindings(&self, channel: &str) -> Bindings {
206 crate::asyncapi::channel(&crate::asyncapi::Publish::stream(channel))
207 }
208}
209
210impl DefaultPublish for ConnectedRedisBroker {
211 type Policy = RedisDefaultPublish;
212}
213
214/// The live form of [`RedisDefaultPublish`]: writes each name the way this connection's
215/// subscriptions read it. Cheap to clone.
216///
217/// Obtain it from [`ConnectedRedisBroker::default_publisher`] or by pairing the policy. Like every
218/// publisher here it may outlive the connection, so publishing after shutdown reports
219/// [`RedisError::ShutDown`].
220#[derive(Clone)]
221pub struct RedisDefaultPublisher {
222 core: Arc<RedisCore>,
223 /// What `pipeline.bind(&out)` names this publisher and its clones by.
224 round_name: u64,
225}
226
227impl Debug for RedisDefaultPublisher {
228 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
229 f.debug_struct("RedisDefaultPublisher")
230 .field("core", &self.core)
231 .finish_non_exhaustive()
232 }
233}
234
235impl RedisDefaultPublisher {
236 /// What `pipeline.bind(&out)` names this publisher and its clones by.
237 pub(crate) const fn round_name(&self) -> u64 {
238 self.round_name
239 }
240
241 pub(crate) fn new(core: Arc<RedisCore>) -> Self {
242 Self {
243 round_name: core.rounds().publisher(),
244 core,
245 }
246 }
247}
248
249impl Publisher for RedisDefaultPublisher {
250 /// As on [`RedisPublisher`]: a name nothing reads is a stream, whose `XADD` keeps the body.
251 type Payload = Take;
252
253 type Error = RedisError;
254 /// The options every publisher of this crate takes, so a partition key resolves the same
255 /// whichever family the name is written in.
256 type Options = RedisPublishOptions;
257
258 async fn publish(
259 &self,
260 msg: OutgoingMessage<'_, BytesMut>,
261 options: Option<&Self::Options>,
262 ) -> Result<(), Self::Error> {
263 let (key, payload, headers) = msg.into_parts();
264 let headers = resolved_headers(headers, options);
265 if let Some(round) = self
266 .core
267 .rounds()
268 .joined(self.round_name, joins_round(options))
269 {
270 let joined = match self.core.routes().route(key) {
271 Route::Stream => {
272 round
273 .xadd(key, fields_for_publish(Vec::from(payload), &headers))
274 .await
275 }
276 Route::List { envelope } => {
277 round
278 .lpush(key, frame(envelope.as_ref(), &payload, &headers), None)
279 .await
280 }
281 Route::Channel { mode, envelope } => {
282 round
283 .publish(
284 key,
285 frame(envelope.as_ref(), &payload, &headers),
286 mode == PubSubMode::Sharded,
287 )
288 .await
289 }
290 };
291 return joined.map_err(RedisError::publish);
292 }
293 match self.core.routes().route(key) {
294 Route::Stream => append(&self.core, key, Vec::from(payload), &headers).await,
295 Route::List { envelope } => {
296 push(&self.core, envelope.as_ref(), None, key, &payload, headers).await
297 }
298 Route::Channel { mode, envelope } => {
299 send(&self.core, mode, envelope.as_ref(), key, &payload, headers).await
300 }
301 }
302 }
303}
304
305/// Whether the call site asked this message to join the round of the delivery being handled.
306pub(crate) fn joins_round(options: Option<&RedisPublishOptions>) -> bool {
307 options.is_some_and(|options| options.join_round)
308}
309
310/// `XADD`s one entry onto the stream `key`: the stream publish, shared by [`RedisPublisher`] and
311/// [`RedisDefaultPublisher`].
312async fn append(
313 core: &RedisCore,
314 key: &str,
315 payload: Vec<u8>,
316 headers: &HeaderMap,
317) -> Result<(), RedisError> {
318 let pool = core.pool()?;
319 let _: String = pool
320 .xadd(
321 key,
322 false,
323 None::<()>,
324 "*",
325 fields_for_publish(payload, headers),
326 )
327 .await
328 .map_err(RedisError::publish)?;
329 Ok(())
330}
331
332/// The live stream publisher: [`RedisPublish`] paired with a connection. Cheap to clone.
333///
334/// [`Publisher::publish`] appends the message to the stream named by
335/// [`OutgoingMessage::name`](ruststream::OutgoingMessage::name) with `XADD <name> * ...`. The
336/// payload and headers are encoded as entry fields (see [`crate::RedisStream`] for the consuming
337/// side).
338///
339/// A publisher may outlive the connected broker it came from (it is a handle aliasing the
340/// connection), so every operation after
341/// [`shutdown`](ruststream::ConnectedBroker::shutdown) reports [`RedisError::ShutDown`] rather
342/// than running against a closed pool.
343///
344/// # Transactions
345///
346/// Both framework transaction kinds are available on standalone and sentinel topologies, and both
347/// commit the same way: the buffer is held client-side while the transaction is open and flushed
348/// as one `MULTI` / `EXEC` block, in publish order, so subscribers see the whole batch or none of
349/// it. They differ only in where that buffer lives.
350///
351/// * Borrowed ([`TransactionalPublisher`]): the handle carries one transaction.
352/// [`begin_transaction`](TransactionalPublisher::begin_transaction) claims it and starts
353/// buffering published messages, [`commit`](TransactionalPublisher::commit) flushes them, and
354/// [`abort`](TransactionalPublisher::abort) discards them. Clones of a handle share the same
355/// open transaction, and a second `begin_transaction` while one is open is rejected.
356/// * Owned ([`OwnedTransactions`]): every [`transaction`](OwnedTransactions::transaction) call
357/// returns a [`RedisTransaction`] owning its own buffer, so any number can be open on one
358/// handle concurrently and the handle keeps publishing directly meanwhile.
359///
360/// Two Redis properties apply to both kinds. Cluster supports neither, because a `MULTI` block
361/// cannot span hash slots, so opening a transaction there returns
362/// [`RedisError::InvalidOptions`]. And Redis has no rollback: a command that fails at *runtime*
363/// inside `EXEC` does not undo the commands before it. For a block of `XADD`s against stream keys
364/// that is practically limited to out-of-memory and wrong-type keys; a command the server refuses
365/// to *queue* discards the whole block.
366#[derive(Clone)]
367pub struct RedisPublisher {
368 core: Arc<RedisCore>,
369 txn: Arc<Mutex<Option<Vec<Buffered>>>>,
370 /// What `pipeline.bind(&out)` names this publisher and its clones by.
371 round_name: u64,
372}
373
374impl Debug for RedisPublisher {
375 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
376 f.debug_struct("RedisPublisher")
377 .field("core", &self.core)
378 .finish_non_exhaustive()
379 }
380}
381
382impl RedisPublisher {
383 /// What `pipeline.bind(&out)` names this publisher and its clones by.
384 pub(crate) const fn round_name(&self) -> u64 {
385 self.round_name
386 }
387
388 pub(crate) fn new(core: Arc<RedisCore>) -> Self {
389 Self {
390 round_name: core.rounds().publisher(),
391 core,
392 txn: Arc::new(Mutex::new(None)),
393 }
394 }
395
396 /// Rejects a transaction on a topology that cannot offer one. Shared by both kinds so they
397 /// answer identically.
398 fn check_transactions_supported(&self) -> Result<(), RedisError> {
399 if self.core.transactions_supported() {
400 return Ok(());
401 }
402 Err(RedisError::InvalidOptions(
403 "transactions are only supported on standalone and sentinel topologies".to_owned(),
404 ))
405 }
406
407 /// Buffers `entry` if a transaction is open and returns `true`; otherwise leaves it for an
408 /// immediate publish.
409 fn buffer_if_in_txn(&self, entry: &Buffered) -> bool {
410 let mut guard = self.txn.lock().expect("redis publisher mutex poisoned");
411 let buffered = guard.as_mut().is_some_and(|buffer| {
412 buffer.push(entry.clone());
413 true
414 });
415 drop(guard);
416 buffered
417 }
418}
419
420impl Publisher for RedisPublisher {
421 /// `XADD` hands the body to the client as the value of a field, and the client keeps it, so
422 /// the buffer the framework wrote becomes that value.
423 type Payload = Take;
424
425 type Error = RedisError;
426 /// `XADD` takes the entry id and the trim threshold per command, and this publisher fixes
427 /// both (`*` and no trim); the stream key is the message's name, not a setting. What is left
428 /// to a call site is the partition key, which leaves as an entry field like any other header.
429 type Options = RedisPublishOptions;
430
431 async fn publish(
432 &self,
433 msg: OutgoingMessage<'_, BytesMut>,
434 options: Option<&Self::Options>,
435 ) -> Result<(), Self::Error> {
436 if let Some(round) = self
437 .core
438 .rounds()
439 .joined(self.round_name, joins_round(options))
440 {
441 let (key, payload, headers) = msg.into_parts();
442 let fields =
443 fields_for_publish(Vec::from(payload), &resolved_headers(headers, options));
444 return round.xadd(key, fields).await.map_err(RedisError::publish);
445 }
446 let (key, payload, headers) = msg.into_parts();
447 let entry: Buffered = (
448 key.to_owned(),
449 fields_for_publish(Vec::from(payload), &resolved_headers(headers, options)),
450 );
451 if self.buffer_if_in_txn(&entry) {
452 return Ok(());
453 }
454 let pool = self.core.pool()?;
455 let (key, fields) = entry;
456 let _: String = pool
457 .xadd(key, false, None::<()>, "*", fields)
458 .await
459 .map_err(RedisError::publish)?;
460 Ok(())
461 }
462}
463
464impl TransactionalPublisher for RedisPublisher {
465 /// Starts buffering published messages on this handle.
466 ///
467 /// # Errors
468 ///
469 /// Returns [`RedisError::InvalidOptions`] on a cluster topology, which cannot offer
470 /// multi-key transactions, or [`RedisError::TransactionBusy`] when a transaction is already
471 /// open on this handle (the open one is left untouched).
472 fn begin_transaction(&self) -> impl Future<Output = Result<(), Self::Error>> {
473 if let Err(err) = self.check_transactions_supported() {
474 return ready(Err(err));
475 }
476 {
477 let mut guard = self.txn.lock().expect("redis publisher mutex poisoned");
478 if guard.is_some() {
479 return ready(Err(RedisError::TransactionBusy));
480 }
481 *guard = Some(Vec::new());
482 }
483 ready(Ok(()))
484 }
485
486 /// Flushes the buffered `XADD`s as one `MULTI` / `EXEC` block, in publish order, then clears
487 /// the transaction.
488 ///
489 /// # Errors
490 ///
491 /// Returns [`RedisError::NoTransaction`] when no transaction is open on this handle,
492 /// [`RedisError::ShutDown`] when the connection is gone, or [`RedisError::Publish`] if the
493 /// block is rejected. On failure the transaction is already closed: the buffer is lost, and
494 /// recovery is redelivery of the inputs rather than resubmission of the buffer.
495 async fn commit(&self) -> Result<(), Self::Error> {
496 // Taken before the flush: a failed commit has still closed the transaction.
497 let buffered = self
498 .txn
499 .lock()
500 .expect("redis publisher mutex poisoned")
501 .take()
502 .ok_or(RedisError::NoTransaction)?;
503 flush_block(&self.core, buffered).await
504 }
505
506 /// Discards the buffered messages.
507 ///
508 /// # Errors
509 ///
510 /// Returns [`RedisError::NoTransaction`] when no transaction is open on this handle.
511 fn abort(&self) -> impl Future<Output = Result<(), Self::Error>> {
512 ready(
513 self.txn
514 .lock()
515 .expect("redis publisher mutex poisoned")
516 .take()
517 .ok_or(RedisError::NoTransaction)
518 .map(|_| ()),
519 )
520 }
521}
522
523/// Owned transactions: every [`transaction`](OwnedTransactions::transaction) call opens an
524/// independent buffer-owning [`RedisTransaction`], so any number can be open concurrently on one
525/// handle, next to (and unaffected by) the handle-level [`TransactionalPublisher`] transaction.
526impl OwnedTransactions for RedisPublisher {
527 type Transaction = RedisTransaction;
528
529 /// # Errors
530 ///
531 /// Returns [`RedisError::InvalidOptions`] on a cluster topology, which cannot offer
532 /// multi-key transactions.
533 fn transaction(&self) -> impl Future<Output = Result<RedisTransaction, RedisError>> {
534 if let Err(err) = self.check_transactions_supported() {
535 return ready(Err(err));
536 }
537 // Opening allocates a buffer and never touches the connection; a connection torn down
538 // before the flush surfaces at commit, the visibility point, like the handle-level begin.
539 ready(Ok(RedisTransaction {
540 core: Arc::clone(&self.core),
541 buffered: Vec::new(),
542 settled: false,
543 }))
544 }
545}
546
547/// An owned Redis transaction, opened by [`transaction`](OwnedTransactions::transaction) on a
548/// [`RedisPublisher`].
549///
550/// A private `XADD` buffer, flushed on commit through the same `MULTI` / `EXEC` path as the
551/// handle-level kind (so the whole batch becomes visible atomically, in publish order) and
552/// discarded on abort.
553///
554/// What sets it apart from the handle-level [`TransactionalPublisher`] buffer is ownership, not
555/// the commit: any number of these can be open on one handle at a time, and the handle keeps
556/// publishing directly while they are. The buffers are independent, so settling one never touches
557/// another; only the flush itself takes a pooled connection.
558///
559/// # Examples
560///
561/// ```
562/// # mod demo {
563/// use ruststream::OutgoingMessage;
564/// use ruststream_fred::stream::prelude::*;
565/// # use serde::Deserialize;
566/// # #[derive(Deserialize)]
567/// # struct Order {
568/// # id: u64,
569/// # }
570/// #
571/// # #[subscriber(RedisStream::new("orders").group("workers"))]
572/// # async fn handle(order: &Order) -> HandlerOutcome {
573/// # println!("order {}", order.id);
574/// # HandlerOutcome::ack()
575/// # }
576///
577/// #[ruststream::app]
578/// fn app() -> impl App {
579/// RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
580/// RedisBroker::standalone("redis://localhost:6379"),
581/// |b| {
582/// b.include(handle);
583/// // Two buffers open at once on one handle; each commits as its own MULTI / EXEC.
584/// b.after_startup(TransactionalPublish, async move |publisher| {
585/// let mut orders = publisher.transaction().await?;
586/// let mut audit = publisher.transaction().await?;
587/// let seed = OutgoingMessage::new("orders", br#"{"id":0}"#.as_slice());
588/// orders.publish(seed, None).await?;
589/// let note = OutgoingMessage::new("audit", br#"{"seeded":1}"#.as_slice());
590/// audit.publish(note, None).await?;
591/// orders.commit().await?;
592/// audit.commit().await
593/// });
594/// },
595/// )
596/// }
597/// # }
598/// # fn main() {}
599/// ```
600#[must_use = "a transaction does nothing until settled with commit() or abort()"]
601pub struct RedisTransaction {
602 core: Arc<RedisCore>,
603 buffered: Vec<Buffered>,
604 settled: bool,
605}
606
607impl Debug for RedisTransaction {
608 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
609 f.debug_struct("RedisTransaction")
610 .field("buffered", &self.buffered.len())
611 .field("settled", &self.settled)
612 .finish_non_exhaustive()
613 }
614}
615
616impl Drop for RedisTransaction {
617 fn drop(&mut self) {
618 // Destructors cannot run async work, so a drop can only discard the buffer; the warning
619 // marks that as an abort the caller never wrote.
620 if !self.settled {
621 warn!(
622 target: "ruststream_fred",
623 buffered = self.buffered.len(),
624 "owned transaction dropped without commit or abort; its buffered messages are \
625 discarded"
626 );
627 }
628 }
629}
630
631impl Transaction for RedisTransaction {
632 /// The same as [`RedisPublisher`]'s: a buffered `XADD` hands the client the same value.
633 type Payload = Take;
634
635 type Error = RedisError;
636 /// The same as [`RedisPublisher`]'s: a buffered `XADD` is the same command, queued, so a
637 /// staged message takes a partition key the way a direct one does.
638 type Options = RedisPublishOptions;
639
640 /// Buffers the `XADD` locally; nothing reaches the server before [`commit`](Self::commit).
641 fn publish(
642 &mut self,
643 msg: OutgoingMessage<'_, BytesMut>,
644 options: Option<&Self::Options>,
645 ) -> impl Future<Output = Result<(), Self::Error>> {
646 let (key, payload, headers) = msg.into_parts();
647 self.buffered.push((
648 key.to_owned(),
649 fields_for_publish(Vec::from(payload), &resolved_headers(headers, options)),
650 ));
651 ready(Ok(()))
652 }
653
654 /// Flushes the buffer as one `MULTI` / `EXEC` block, in publish order.
655 ///
656 /// # Errors
657 ///
658 /// Returns [`RedisError::ShutDown`] when the connection this transaction was opened from is
659 /// gone, or [`RedisError::Publish`] when the block is rejected. A failed commit has still
660 /// consumed the transaction and its buffer is lost; redelivery of the inputs, not
661 /// resubmission of the buffer, is the recovery path.
662 async fn commit(mut self) -> Result<(), Self::Error> {
663 // Settled before the flush: a failed commit has still consumed the transaction (the
664 // buffer is lost per the Transaction contract), so the drop warning must not fire.
665 self.settled = true;
666 flush_block(&self.core, std::mem::take(&mut self.buffered)).await
667 }
668
669 /// Discards the buffer. Nothing was sent to the server, so this cannot fail.
670 fn abort(mut self) -> impl Future<Output = Result<(), Self::Error>> {
671 self.settled = true;
672 ready(Ok(()))
673 }
674}