Skip to main content

ruststream_lapin/
publisher.rs

1//! The live half of publishing: the publishers a policy pairs into.
2//!
3//! Each of them exists only from a [`ConnectedLapinBroker`], so it always has a connection; the
4//! declaration half (what to publish and how) lives in [`crate::publish_policy`].
5
6// Without the `testing` feature a connection link has one variant, so a `match` on it has a
7// single arm; the matches stay so that the in-process arm has its place when the feature is on.
8#![cfg_attr(
9    not(feature = "testing"),
10    allow(clippy::infallible_destructuring_match)
11)]
12
13use std::future::{Future, ready};
14use std::sync::{Arc, Mutex};
15
16use bytes::Bytes;
17use lapin::options::{BasicPublishOptions, ConfirmSelectOptions};
18use lapin::{BasicProperties, Channel};
19use lapin::{Confirmation, PublisherConfirm};
20use ruststream::{
21    HeaderMap, Lend, OutgoingFor, OutgoingMessage, Publisher, Take, TransactionalPublisher,
22};
23
24use crate::broker::{AmqpConnection, ConnectedLapinBroker, Link};
25use crate::channel::ChannelCell;
26use crate::convert;
27use crate::error::AmqpError;
28#[cfg(feature = "testing")]
29use crate::in_process::Bus;
30use crate::publish_policy::PublishOptions;
31use crate::publish_step::LapinPublishOptions;
32
33/// One buffered publish: everything the flush needs to rebuild the frame the caller asked for.
34///
35/// The per-message settings are kept as the call site left them, not resolved against the policy
36/// yet: the flush builds the frame, and resolving twice would let a policy change between the
37/// buffering and the commit go unnoticed.
38#[derive(Debug, Clone)]
39pub(crate) struct Buffered {
40    pub(crate) routing_key: String,
41    pub(crate) payload: Bytes,
42    pub(crate) headers: HeaderMap,
43    pub(crate) options: LapinPublishOptions,
44}
45
46impl Buffered {
47    /// A message a lending publisher buffered: the framework reuses that buffer for the next
48    /// message, so the bytes are copied out of it. The header map is the publish's own and is
49    /// taken with the rest of the message.
50    pub(crate) fn lent(msg: OutgoingMessage<'_>, options: Option<&LapinPublishOptions>) -> Self {
51        let (routing_key, payload, headers) = msg.into_parts();
52        Self {
53            routing_key: routing_key.to_owned(),
54            payload: Bytes::copy_from_slice(payload),
55            headers,
56            options: options.copied().unwrap_or_default(),
57        }
58    }
59
60    /// A message a transaction was handed: the buffer is the transaction's to keep, so the
61    /// payload and the header map are taken as the framework wrote them.
62    pub(crate) fn taken(msg: OutgoingFor<'_, Take>, options: Option<&LapinPublishOptions>) -> Self {
63        let (routing_key, payload, headers) = msg.into_parts();
64        Self {
65            routing_key: routing_key.to_owned(),
66            payload: payload.freeze(),
67            headers,
68            options: options.copied().unwrap_or_default(),
69        }
70    }
71}
72
73pub(crate) async fn do_publish(
74    conn: &AmqpConnection,
75    channel: &Channel,
76    exchange: &str,
77    routing_key: &str,
78    payload: &[u8],
79    properties: BasicProperties,
80) -> Result<PublisherConfirm, AmqpError> {
81    #[cfg(feature = "testing")]
82    conn.routes().published(exchange, routing_key);
83    #[cfg(not(feature = "testing"))]
84    let _ = conn;
85    channel
86        .basic_publish(
87            convert::short(exchange, "exchange name")?,
88            convert::short(routing_key, "routing key")?,
89            BasicPublishOptions::default(),
90            payload,
91            properties,
92        )
93        .await
94        .map_err(AmqpError::publish)
95}
96
97/// The live fire-and-forget publisher, on the connection's shared publish channel. Cheap to
98/// clone.
99///
100/// Paired from [`LapinPublish`](crate::LapinPublish), so it always has a connection. It aliases
101/// that connection, though, and may outlive it: after the broker shuts down every publish
102/// reports [`AmqpError::Closed`] instead of silently succeeding against a dead connection.
103#[derive(Debug, Clone)]
104pub struct LapinPublisher {
105    link: Link,
106    options: PublishOptions,
107}
108
109impl LapinPublisher {
110    pub(crate) fn new(connected: &ConnectedLapinBroker, options: PublishOptions) -> Self {
111        Self {
112            link: connected.link().clone(),
113            options,
114        }
115    }
116}
117
118impl Publisher for LapinPublisher {
119    /// `basic_publish` takes the payload as `&[u8]` and copies it into the frame it sends, so
120    /// this publisher reads the bytes and keeps nothing.
121    type Payload = Lend;
122    type Error = AmqpError;
123    type Options = LapinPublishOptions;
124
125    /// Publishes `msg` without waiting for a broker confirm.
126    ///
127    /// The AMQP basic properties are `options` resolved over the policy's defaults; a call that
128    /// adjusted nothing publishes with the policy's own settings.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`AmqpError::Closed`] once the broker has shut down and
133    /// [`AmqpError::Publish`] when the channel rejects the frame.
134    ///
135    /// # Cancel safety
136    ///
137    /// Not cancel safe: dropping the future may leave the message published or not.
138    async fn publish(
139        &self,
140        msg: OutgoingMessage<'_>,
141        options: Option<&Self::Options>,
142    ) -> Result<(), Self::Error> {
143        let conn = match &self.link {
144            Link::Amqp(conn) => conn,
145            #[cfg(feature = "testing")]
146            Link::InProcess(bus) => {
147                return bus.publish(
148                    &self.options.exchange,
149                    msg.name(),
150                    msg.payload(),
151                    msg.headers(),
152                    &self.options.resolve(options),
153                );
154            }
155        };
156        let channel = conn.live_publish_channel(msg.name()).await?;
157        let properties =
158            convert::properties_for_publish(msg.headers(), &self.options.resolve(options))?;
159        // Without confirm_select on the channel the returned confirm resolves to NotRequested;
160        // dropping it does not lose anything.
161        let _confirm = do_publish(
162            conn,
163            &channel,
164            &self.options.exchange,
165            msg.name(),
166            msg.payload(),
167            properties,
168        )
169        .await?;
170        Ok(())
171    }
172}
173
174/// The live publisher that awaits broker confirms for every message.
175///
176/// Outside a transaction each [`publish`](Publisher::publish) resolves only once the broker
177/// confirmed the message. Confirms buffer client-side, so this publisher offers both transaction
178/// kinds:
179///
180/// * owned ([`OwnedTransactions`](ruststream::OwnedTransactions), the natural fit): every
181///   [`transaction`](ruststream::OwnedTransactions::transaction) call opens an independent
182///   [`ConfirmsTransaction`](crate::ConfirmsTransaction) that owns its buffer, so any number can
183///   be open on one handle and the handle keeps publishing directly meanwhile;
184/// * borrowed ([`TransactionalPublisher`]): the handle carries one buffer between
185///   [`begin_transaction`](TransactionalPublisher::begin_transaction) and
186///   [`commit`](TransactionalPublisher::commit), so a second begin while one is open errors.
187///
188/// Either way `commit` publishes the buffer in order and awaits all confirms, and `abort`
189/// discards it without touching the broker.
190///
191/// Clones share one confirm channel and one handle-level transaction buffer. Like every live
192/// publisher it aliases the connection and may outlive it: after shutdown every operation
193/// reports [`AmqpError::Closed`].
194#[derive(Debug, Clone)]
195pub struct ConfirmsPublisher {
196    link: Link,
197    options: PublishOptions,
198    channel: Arc<ChannelCell<Channel>>,
199    txn: Arc<Mutex<Option<Vec<Buffered>>>>,
200}
201
202impl ConfirmsPublisher {
203    pub(crate) fn new(connected: &ConnectedLapinBroker, options: PublishOptions) -> Self {
204        Self {
205            link: connected.link().clone(),
206            options,
207            channel: Arc::new(ChannelCell::new()),
208            txn: Arc::new(Mutex::new(None)),
209        }
210    }
211
212    /// The confirm channel, opened on first use and opened again once the broker closes it.
213    ///
214    /// Why lazily and not at pairing time: pairing is a synchronous constructor call (see
215    /// [`LapinPublishPolicy`]), and a publisher that never publishes should hold no channel. Why
216    /// again: a rejected publish closes the channel, and a publisher that kept the dead one would
217    /// never send anything through this handle again.
218    async fn channel(&self, conn: &AmqpConnection, target: &str) -> Result<Channel, AmqpError> {
219        self.channel
220            .get(|| async {
221                let channel = conn
222                    .live_connection(target)?
223                    .create_channel()
224                    .await
225                    .map_err(AmqpError::publish)?;
226                channel
227                    .confirm_select(ConfirmSelectOptions::default())
228                    .await
229                    .map_err(AmqpError::publish)?;
230                Ok(channel)
231            })
232            .await
233    }
234
235    /// Publishes `buffered` in order on the confirm channel and awaits every confirm.
236    ///
237    /// The flush of the owned transaction kind ([`ConfirmsTransaction`]), whose contract loses
238    /// the buffer on a failed commit - redelivery of the inputs is the recovery path - so it
239    /// needs no bookkeeping about what was sent. The borrowed kind keeps its own flush: its
240    /// buffer is shared with the handle, which is state this one does not have.
241    pub(crate) async fn flush_owned(&self, buffered: &[Buffered]) -> Result<(), AmqpError> {
242        // The whole buffer rides one channel; the first routing key names the flush in any
243        // connection-level diagnostic.
244        let Some(first) = buffered.first() else {
245            return Ok(());
246        };
247        let conn = match &self.link {
248            Link::Amqp(conn) => conn,
249            #[cfg(feature = "testing")]
250            Link::InProcess(bus) => return self.options.flush_in_process(bus, buffered),
251        };
252        conn.ensure_live(&first.routing_key)?;
253        let channel = self.channel(conn, &first.routing_key).await?;
254
255        let mut confirms = Vec::with_capacity(buffered.len());
256        for entry in buffered {
257            let properties = convert::properties_for_publish(
258                &entry.headers,
259                &self.options.resolve(Some(&entry.options)),
260            )?;
261            let confirm = do_publish(
262                conn,
263                &channel,
264                &self.options.exchange,
265                &entry.routing_key,
266                &entry.payload,
267                properties,
268            )
269            .await?;
270            confirms.push((&entry.routing_key, confirm));
271        }
272        for (routing_key, confirm) in confirms {
273            let confirmation = confirm.await.map_err(AmqpError::publish)?;
274            confirmation_ok(&confirmation, routing_key)?;
275        }
276        Ok(())
277    }
278
279    async fn publish_confirmed(
280        &self,
281        routing_key: &str,
282        payload: &[u8],
283        headers: &HeaderMap,
284        options: Option<&LapinPublishOptions>,
285    ) -> Result<(), AmqpError> {
286        let conn = match &self.link {
287            Link::Amqp(conn) => conn,
288            #[cfg(feature = "testing")]
289            Link::InProcess(bus) => {
290                return bus.publish(
291                    &self.options.exchange,
292                    routing_key,
293                    payload,
294                    headers,
295                    &self.options.resolve(options),
296                );
297            }
298        };
299        conn.ensure_live(routing_key)?;
300        let channel = self.channel(conn, routing_key).await?;
301        let properties = convert::properties_for_publish(headers, &self.options.resolve(options))?;
302        let confirm = do_publish(
303            conn,
304            &channel,
305            &self.options.exchange,
306            routing_key,
307            payload,
308            properties,
309        )
310        .await?
311        .await
312        .map_err(AmqpError::publish)?;
313        confirmation_ok(&confirm, routing_key)
314    }
315}
316
317fn confirmation_ok(confirmation: &Confirmation, routing_key: &str) -> Result<(), AmqpError> {
318    if confirmation.is_nack() {
319        return Err(AmqpError::Publish(
320            format!("the broker negatively confirmed the publish to {routing_key:?}").into(),
321        ));
322    }
323    Ok(())
324}
325
326impl Publisher for ConfirmsPublisher {
327    /// `basic_publish` takes the payload as `&[u8]` and copies it into the frame it sends, so
328    /// this publisher reads the bytes and keeps nothing.
329    type Payload = Lend;
330    type Error = AmqpError;
331    type Options = LapinPublishOptions;
332
333    /// Publishes `msg`, awaiting the broker confirm (or buffering inside a transaction).
334    ///
335    /// The AMQP basic properties are `options` resolved over the policy's defaults. Inside a
336    /// transaction they are buffered with the message and resolved at the commit.
337    ///
338    /// # Errors
339    ///
340    /// Returns [`AmqpError::Closed`] once the broker has shut down and [`AmqpError::Publish`]
341    /// when the channel rejects the frame or the broker returns a negative confirm.
342    ///
343    /// # Cancel safety
344    ///
345    /// Not cancel safe outside a transaction: dropping the future may leave the message
346    /// published but unconfirmed. Inside a transaction buffering is synchronous and dropping the
347    /// future is harmless.
348    async fn publish(
349        &self,
350        msg: OutgoingMessage<'_>,
351        options: Option<&Self::Options>,
352    ) -> Result<(), Self::Error> {
353        {
354            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
355            if let Some(buffer) = txn.as_mut() {
356                buffer.push(Buffered::lent(msg, options));
357                return Ok(());
358            }
359        }
360        self.publish_confirmed(msg.name(), msg.payload(), msg.headers(), options)
361            .await
362    }
363}
364
365impl TransactionalPublisher for ConfirmsPublisher {
366    /// Opens the buffering transaction.
367    ///
368    /// # Errors
369    ///
370    /// Returns [`AmqpError::Transaction`] when a transaction is already open on this handle;
371    /// the open transaction is left untouched.
372    fn begin_transaction(&self) -> impl Future<Output = Result<(), Self::Error>> {
373        let already_open = {
374            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
375            let open = txn.is_some();
376            if !open {
377                *txn = Some(Vec::new());
378            }
379            open
380        };
381        if already_open {
382            return ready(Err(AmqpError::Transaction(
383                "a transaction is already open on this confirms publisher; commit or abort it \
384                 before beginning another"
385                    .to_owned(),
386            )));
387        }
388        ready(Ok(()))
389    }
390
391    /// Publishes the buffered messages in order and awaits every confirm.
392    ///
393    /// # Errors
394    ///
395    /// Returns [`AmqpError::Transaction`] when no transaction is open, and
396    /// [`AmqpError::Publish`] when any message fails to publish or the broker returns a negative
397    /// confirm. Messages already flushed stay published: publisher confirms give durability per
398    /// message, not atomicity across them (use [`ServerTxPublish`](crate::ServerTxPublish) for
399    /// that).
400    async fn commit(&self) -> Result<(), Self::Error> {
401        let buffered = {
402            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
403            txn.take()
404        };
405        let Some(buffered) = buffered else {
406            return Err(AmqpError::Transaction(
407                "commit with no open transaction on this confirms publisher".to_owned(),
408            ));
409        };
410        if buffered.is_empty() {
411            return Ok(());
412        }
413
414        let conn = match &self.link {
415            Link::Amqp(conn) => conn,
416            #[cfg(feature = "testing")]
417            Link::InProcess(bus) => return self.options.flush_in_process(bus, &buffered),
418        };
419        let target = buffered[0].routing_key.as_str();
420        conn.ensure_live(target)?;
421        let channel = self.channel(conn, target).await?;
422        let mut confirms = Vec::with_capacity(buffered.len());
423        for entry in &buffered {
424            let properties = convert::properties_for_publish(
425                &entry.headers,
426                &self.options.resolve(Some(&entry.options)),
427            )?;
428            let confirm = do_publish(
429                conn,
430                &channel,
431                &self.options.exchange,
432                &entry.routing_key,
433                &entry.payload,
434                properties,
435            )
436            .await?;
437            confirms.push((&entry.routing_key, confirm));
438        }
439        for (routing_key, confirm) in confirms {
440            let confirmation = confirm.await.map_err(AmqpError::publish)?;
441            confirmation_ok(&confirmation, routing_key)?;
442        }
443        Ok(())
444    }
445
446    /// Discards the buffered messages without publishing anything.
447    ///
448    /// # Errors
449    ///
450    /// Returns [`AmqpError::Transaction`] when no transaction is open.
451    fn abort(&self) -> impl Future<Output = Result<(), Self::Error>> {
452        let discarded = self
453            .txn
454            .lock()
455            .expect("transaction buffer mutex poisoned")
456            .take();
457        if discarded.is_none() {
458            return ready(Err(AmqpError::Transaction(
459                "abort with no open transaction on this confirms publisher".to_owned(),
460            )));
461        }
462        ready(Ok(()))
463    }
464}
465
466/// The live publisher backed by AMQP server transactions (`tx.select` / `tx.commit` /
467/// `tx.rollback`).
468///
469/// Between [`begin_transaction`](TransactionalPublisher::begin_transaction) and
470/// [`commit`](TransactionalPublisher::commit) messages accumulate on the broker inside the
471/// channel transaction and become visible atomically at commit;
472/// [`abort`](TransactionalPublisher::abort) rolls them back server-side. Outside a transaction
473/// [`publish`](Publisher::publish) behaves like the fire-and-forget publisher.
474///
475/// Only the borrowed transaction kind ([`TransactionalPublisher`]) applies here, unlike
476/// [`ConfirmsPublisher`]: `tx.select` puts the channel itself into transactional mode, so the
477/// transaction is channel state with exactly one instance, and there is no buffer for an owned
478/// [`Transaction`](ruststream::Transaction) value to own.
479///
480/// Clones share the transactional channel and its open/closed state. Interleaving `publish`
481/// and `begin_transaction`/`commit` from concurrent tasks is not supported: which side of the
482/// transaction boundary a concurrent publish lands on would be a race either way. Like every
483/// live publisher it aliases the connection and may outlive it: after shutdown every operation
484/// reports [`AmqpError::Closed`].
485#[derive(Debug, Clone)]
486pub struct ServerTxPublisher {
487    link: Link,
488    options: PublishOptions,
489    channel: Arc<ChannelCell<Channel>>,
490    open: Arc<Mutex<bool>>,
491    /// The in-process stand for the messages a server stages inside the channel transaction.
492    #[cfg(feature = "testing")]
493    staged: Arc<Mutex<Vec<Buffered>>>,
494}
495
496impl ServerTxPublisher {
497    pub(crate) fn new(connected: &ConnectedLapinBroker, options: PublishOptions) -> Self {
498        Self {
499            link: connected.link().clone(),
500            options,
501            channel: Arc::new(ChannelCell::new()),
502            open: Arc::new(Mutex::new(false)),
503            #[cfg(feature = "testing")]
504            staged: Arc::new(Mutex::new(Vec::new())),
505        }
506    }
507
508    /// The transactional channel, opened on first use; see [`ConfirmsPublisher::channel`] for
509    /// why it is not opened at pairing time.
510    ///
511    /// A channel that died while a transaction was open is handed back as it is: the transaction
512    /// lives in the channel, so a fresh one would commit nothing and report success for the
513    /// publishes it never carried. Outside a transaction there is no such state, and a dead
514    /// channel is replaced like any other publisher's.
515    async fn tx_channel(&self, conn: &AmqpConnection, target: &str) -> Result<Channel, AmqpError> {
516        if self.is_open() {
517            return self.channel.held().ok_or_else(|| {
518                AmqpError::Transaction(
519                    "the transactional channel is gone; the open transaction cannot be published \
520                     into or committed"
521                        .to_owned(),
522                )
523            });
524        }
525        self.channel
526            .get(|| async {
527                let channel = conn
528                    .live_connection(target)?
529                    .create_channel()
530                    .await
531                    .map_err(AmqpError::publish)?;
532                channel.tx_select().await.map_err(AmqpError::publish)?;
533                Ok(channel)
534            })
535            .await
536    }
537
538    fn is_open(&self) -> bool {
539        *self.open.lock().expect("transaction state mutex poisoned")
540    }
541
542    fn set_open(&self, open: bool) {
543        *self.open.lock().expect("transaction state mutex poisoned") = open;
544    }
545}
546
547impl Publisher for ServerTxPublisher {
548    /// `basic_publish` takes the payload as `&[u8]` and copies it into the frame it sends, so
549    /// this publisher reads the bytes and keeps nothing.
550    type Payload = Lend;
551    type Error = AmqpError;
552    type Options = LapinPublishOptions;
553
554    /// Publishes `msg`: into the open server transaction, or plainly when none is open.
555    ///
556    /// The AMQP basic properties are `options` resolved over the policy's defaults, either way.
557    ///
558    /// # Errors
559    ///
560    /// Returns [`AmqpError::Closed`] once the broker has shut down and [`AmqpError::Publish`]
561    /// when the channel rejects the frame.
562    ///
563    /// # Cancel safety
564    ///
565    /// Not cancel safe: dropping the future may leave the message queued in the transaction or
566    /// not.
567    async fn publish(
568        &self,
569        msg: OutgoingMessage<'_>,
570        options: Option<&Self::Options>,
571    ) -> Result<(), Self::Error> {
572        let conn = match &self.link {
573            Link::Amqp(conn) => conn,
574            #[cfg(feature = "testing")]
575            Link::InProcess(bus) => {
576                let resolved = self.options.resolve(options);
577                if !self.is_open() {
578                    return bus.publish(
579                        &self.options.exchange,
580                        msg.name(),
581                        msg.payload(),
582                        msg.headers(),
583                        &resolved,
584                    );
585                }
586                // The frame is checked as the server checks it when it stages the message.
587                bus.check(&self.options.exchange, msg.name(), msg.headers(), &resolved)?;
588                self.staged
589                    .lock()
590                    .expect("staged transaction mutex poisoned")
591                    .push(Buffered::lent(msg, options));
592                return Ok(());
593            }
594        };
595        let properties =
596            convert::properties_for_publish(msg.headers(), &self.options.resolve(options))?;
597        // The transaction is channel state, so an open one is published into the channel that
598        // holds it; outside one the connection's shared channel carries the message.
599        let channel = if self.is_open() {
600            conn.ensure_live(msg.name())?;
601            self.tx_channel(conn, msg.name()).await?.clone()
602        } else {
603            conn.live_publish_channel(msg.name()).await?
604        };
605        let _confirm = do_publish(
606            conn,
607            &channel,
608            &self.options.exchange,
609            msg.name(),
610            msg.payload(),
611            properties,
612        )
613        .await?;
614        Ok(())
615    }
616}
617
618/// The transaction target named in diagnostics: server transactions are a property of the
619/// channel, not of one routing key.
620const TX_TARGET: &str = "the transactional channel";
621
622impl TransactionalPublisher for ServerTxPublisher {
623    /// Opens a server transaction (`tx.select` on first use).
624    ///
625    /// # Errors
626    ///
627    /// Returns [`AmqpError::Transaction`] when a transaction is already open on this handle
628    /// (the open transaction is left untouched), [`AmqpError::Closed`] once the broker has shut
629    /// down, and [`AmqpError::Publish`] when the transactional channel cannot be set up.
630    async fn begin_transaction(&self) -> Result<(), Self::Error> {
631        if self.is_open() {
632            return Err(AmqpError::Transaction(
633                "a transaction is already open on this server-transactional publisher; commit or \
634                 abort it before beginning another"
635                    .to_owned(),
636            ));
637        }
638        match &self.link {
639            Link::Amqp(conn) => {
640                conn.ensure_live(TX_TARGET)?;
641                self.tx_channel(conn, TX_TARGET).await?;
642            }
643            #[cfg(feature = "testing")]
644            Link::InProcess(bus) => bus.ensure_live(TX_TARGET)?,
645        }
646        self.set_open(true);
647        Ok(())
648    }
649
650    /// Commits the open server transaction.
651    ///
652    /// # Errors
653    ///
654    /// Returns [`AmqpError::Transaction`] when no transaction is open, and
655    /// [`AmqpError::Publish`] when `tx.commit` fails; the transaction state on the broker is
656    /// then unknown (the channel may be closed) and the publisher should be discarded.
657    async fn commit(&self) -> Result<(), Self::Error> {
658        if !self.is_open() {
659            return Err(AmqpError::Transaction(
660                "commit with no open transaction on this server-transactional publisher".to_owned(),
661            ));
662        }
663        let conn = match &self.link {
664            Link::Amqp(conn) => conn,
665            #[cfg(feature = "testing")]
666            Link::InProcess(bus) => {
667                bus.ensure_live(TX_TARGET)?;
668                let staged = std::mem::take(
669                    &mut *self
670                        .staged
671                        .lock()
672                        .expect("staged transaction mutex poisoned"),
673                );
674                self.set_open(false);
675                // The server takes an oversized body into the transaction and refuses the commit,
676                // which then publishes none of it.
677                for entry in &staged {
678                    Bus::check_size(&entry.routing_key, entry.payload.len())?;
679                }
680                // The commit makes the whole transaction visible at once, as `tx.commit` does.
681                return self.options.flush_in_process(bus, &staged);
682            }
683        };
684        conn.ensure_live(TX_TARGET)?;
685        let channel = self.tx_channel(conn, TX_TARGET).await?;
686        channel.tx_commit().await.map_err(AmqpError::publish)?;
687        self.set_open(false);
688        Ok(())
689    }
690
691    /// Rolls back the open server transaction.
692    ///
693    /// # Errors
694    ///
695    /// Returns [`AmqpError::Transaction`] when no transaction is open, and
696    /// [`AmqpError::Publish`] when `tx.rollback` fails.
697    async fn abort(&self) -> Result<(), Self::Error> {
698        if !self.is_open() {
699            return Err(AmqpError::Transaction(
700                "abort with no open transaction on this server-transactional publisher".to_owned(),
701            ));
702        }
703        let conn = match &self.link {
704            Link::Amqp(conn) => conn,
705            #[cfg(feature = "testing")]
706            Link::InProcess(bus) => {
707                bus.ensure_live(TX_TARGET)?;
708                self.staged
709                    .lock()
710                    .expect("staged transaction mutex poisoned")
711                    .clear();
712                self.set_open(false);
713                return Ok(());
714            }
715        };
716        conn.ensure_live(TX_TARGET)?;
717        let channel = self.tx_channel(conn, TX_TARGET).await?;
718        channel.tx_rollback().await.map_err(AmqpError::publish)?;
719        self.set_open(false);
720        Ok(())
721    }
722}
723
724#[cfg(test)]
725mod tests {
726    use ruststream::{BytesMut, OutgoingFor, OutgoingMessage, Take};
727
728    use super::Buffered;
729
730    /// Content equality cannot tell a hand-over from a copy, so the buffer the framework wrote is
731    /// identified by its address.
732    #[test]
733    fn a_buffered_message_keeps_the_buffer_the_framework_wrote() {
734        let payload = BytesMut::from(&br#"{"id":1}"#[..]);
735        let written = payload.as_ptr();
736        let msg: OutgoingFor<'_, Take> = OutgoingMessage::produced("orders", payload);
737
738        let buffered = Buffered::taken(msg, None);
739
740        assert_eq!(
741            buffered.payload.as_ptr(),
742            written,
743            "a transaction keeps the message until the commit, so it takes the buffer rather \
744             than copying out of it"
745        );
746    }
747}