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