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}