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}