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
6use std::sync::{Arc, Mutex};
7
8use bytes::Bytes;
9use lapin::options::{BasicPublishOptions, ConfirmSelectOptions};
10use lapin::{BasicProperties, Channel};
11use lapin::{Confirmation, PublisherConfirm};
12use ruststream::{Headers, OutgoingMessage, Publisher, TransactionalPublisher};
13use tokio::sync::OnceCell;
14
15use crate::broker::{AmqpConnection, ConnectedLapinBroker};
16use crate::convert;
17use crate::error::AmqpError;
18use crate::publish_policy::PublishOptions;
19
20/// One buffered publish: routing key, payload, headers.
21pub(crate) type Buffered = (String, Bytes, Headers);
22
23pub(crate) async fn do_publish(
24    channel: &Channel,
25    exchange: &str,
26    routing_key: &str,
27    payload: &[u8],
28    properties: BasicProperties,
29) -> Result<PublisherConfirm, AmqpError> {
30    channel
31        .basic_publish(
32            convert::short(exchange, "exchange name")?,
33            convert::short(routing_key, "routing key")?,
34            BasicPublishOptions::default(),
35            payload,
36            properties,
37        )
38        .await
39        .map_err(AmqpError::publish)
40}
41
42/// The live fire-and-forget publisher, on the connection's shared publish channel. Cheap to
43/// clone.
44///
45/// Paired from [`LapinPublish`](crate::LapinPublish), so it always has a connection. It aliases
46/// that connection, though, and may outlive it: after the broker shuts down every publish
47/// reports [`AmqpError::Closed`] instead of silently succeeding against a dead connection.
48#[derive(Debug, Clone)]
49pub struct LapinPublisher {
50    conn: Arc<AmqpConnection>,
51    options: PublishOptions,
52}
53
54impl LapinPublisher {
55    pub(crate) fn new(connected: &ConnectedLapinBroker, options: PublishOptions) -> Self {
56        Self {
57            conn: Arc::clone(connected.connection()),
58            options,
59        }
60    }
61}
62
63impl Publisher for LapinPublisher {
64    type Error = AmqpError;
65
66    /// Publishes `msg` without waiting for a broker confirm.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`AmqpError::Closed`] once the broker has shut down and
71    /// [`AmqpError::Publish`] when the channel rejects the frame.
72    ///
73    /// # Cancel safety
74    ///
75    /// Not cancel safe: dropping the future may leave the message published or not.
76    async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
77        let channel = self.conn.live_publish_channel(msg.name())?;
78        let properties = convert::properties_for_publish(msg.headers(), self.options.persistent)?;
79        // Without confirm_select on the channel the returned confirm resolves to NotRequested;
80        // dropping it does not lose anything.
81        let _confirm = do_publish(
82            channel,
83            &self.options.exchange,
84            msg.name(),
85            msg.payload(),
86            properties,
87        )
88        .await?;
89        Ok(())
90    }
91}
92
93/// The live publisher that awaits broker confirms for every message.
94///
95/// Outside a transaction each [`publish`](Publisher::publish) resolves only once the broker
96/// confirmed the message. Confirms buffer client-side, so this publisher offers both transaction
97/// kinds:
98///
99/// * owned ([`OwnedTransactions`](ruststream::OwnedTransactions), the natural fit): every
100///   [`transaction`](ruststream::OwnedTransactions::transaction) call opens an independent
101///   [`ConfirmsTransaction`](crate::ConfirmsTransaction) that owns its buffer, so any number can
102///   be open on one handle and the handle keeps publishing directly meanwhile;
103/// * borrowed ([`TransactionalPublisher`]): the handle carries one buffer between
104///   [`begin_transaction`](TransactionalPublisher::begin_transaction) and
105///   [`commit`](TransactionalPublisher::commit), so a second begin while one is open errors.
106///
107/// Either way `commit` publishes the buffer in order and awaits all confirms, and `abort`
108/// discards it without touching the broker.
109///
110/// Clones share one confirm channel and one handle-level transaction buffer. Like every live
111/// publisher it aliases the connection and may outlive it: after shutdown every operation
112/// reports [`AmqpError::Closed`].
113#[derive(Debug, Clone)]
114pub struct ConfirmsPublisher {
115    conn: Arc<AmqpConnection>,
116    options: PublishOptions,
117    channel: Arc<OnceCell<Channel>>,
118    txn: Arc<Mutex<Option<Vec<Buffered>>>>,
119}
120
121impl ConfirmsPublisher {
122    pub(crate) fn new(connected: &ConnectedLapinBroker, options: PublishOptions) -> Self {
123        Self {
124            conn: Arc::clone(connected.connection()),
125            options,
126            channel: Arc::new(OnceCell::new()),
127            txn: Arc::new(Mutex::new(None)),
128        }
129    }
130
131    /// The confirm channel, opened on first use.
132    ///
133    /// Why lazily and not at pairing time: pairing is a synchronous constructor call (see
134    /// [`LapinPublishPolicy`]), and a publisher that never publishes should hold no channel.
135    async fn channel(&self, target: &str) -> Result<&Channel, AmqpError> {
136        self.channel
137            .get_or_try_init(|| async {
138                let channel = self
139                    .conn
140                    .live_connection(target)?
141                    .create_channel()
142                    .await
143                    .map_err(AmqpError::publish)?;
144                channel
145                    .confirm_select(ConfirmSelectOptions::default())
146                    .await
147                    .map_err(AmqpError::publish)?;
148                Ok(channel)
149            })
150            .await
151    }
152
153    /// Publishes `buffered` in order on the confirm channel and awaits every confirm.
154    ///
155    /// The flush of the owned transaction kind ([`ConfirmsTransaction`]), whose contract loses
156    /// the buffer on a failed commit - redelivery of the inputs is the recovery path - so it
157    /// needs no bookkeeping about what was sent. The borrowed kind keeps its own flush: its
158    /// buffer is shared with the handle, which is state this one does not have.
159    pub(crate) async fn flush_owned(&self, buffered: &[Buffered]) -> Result<(), AmqpError> {
160        // The whole buffer rides one channel; the first routing key names the flush in any
161        // connection-level diagnostic.
162        let Some((first_key, _, _)) = buffered.first() else {
163            return Ok(());
164        };
165        self.conn.ensure_live(first_key)?;
166        let channel = self.channel(first_key).await?;
167
168        let mut confirms = Vec::with_capacity(buffered.len());
169        for (routing_key, payload, headers) in buffered {
170            let properties = convert::properties_for_publish(headers, self.options.persistent)?;
171            let confirm = do_publish(
172                channel,
173                &self.options.exchange,
174                routing_key,
175                payload,
176                properties,
177            )
178            .await?;
179            confirms.push((routing_key, confirm));
180        }
181        for (routing_key, confirm) in confirms {
182            let confirmation = confirm.await.map_err(AmqpError::publish)?;
183            confirmation_ok(&confirmation, routing_key)?;
184        }
185        Ok(())
186    }
187
188    async fn publish_confirmed(
189        &self,
190        routing_key: &str,
191        payload: &[u8],
192        headers: &Headers,
193    ) -> Result<(), AmqpError> {
194        self.conn.ensure_live(routing_key)?;
195        let channel = self.channel(routing_key).await?;
196        let properties = convert::properties_for_publish(headers, self.options.persistent)?;
197        let confirm = do_publish(
198            channel,
199            &self.options.exchange,
200            routing_key,
201            payload,
202            properties,
203        )
204        .await?
205        .await
206        .map_err(AmqpError::publish)?;
207        confirmation_ok(&confirm, routing_key)
208    }
209}
210
211fn confirmation_ok(confirmation: &Confirmation, routing_key: &str) -> Result<(), AmqpError> {
212    if confirmation.is_nack() {
213        return Err(AmqpError::Publish(
214            format!("the broker negatively confirmed the publish to {routing_key:?}").into(),
215        ));
216    }
217    Ok(())
218}
219
220impl Publisher for ConfirmsPublisher {
221    type Error = AmqpError;
222
223    /// Publishes `msg`, awaiting the broker confirm (or buffering inside a transaction).
224    ///
225    /// # Errors
226    ///
227    /// Returns [`AmqpError::Closed`] once the broker has shut down and [`AmqpError::Publish`]
228    /// when the channel rejects the frame or the broker returns a negative confirm.
229    ///
230    /// # Cancel safety
231    ///
232    /// Not cancel safe outside a transaction: dropping the future may leave the message
233    /// published but unconfirmed. Inside a transaction buffering is synchronous and dropping the
234    /// future is harmless.
235    async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
236        {
237            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
238            if let Some(buffer) = txn.as_mut() {
239                buffer.push((
240                    msg.name().to_owned(),
241                    Bytes::copy_from_slice(msg.payload()),
242                    msg.headers().clone(),
243                ));
244                return Ok(());
245            }
246        }
247        self.publish_confirmed(msg.name(), msg.payload(), msg.headers())
248            .await
249    }
250}
251
252impl TransactionalPublisher for ConfirmsPublisher {
253    /// Opens the buffering transaction.
254    ///
255    /// # Errors
256    ///
257    /// Returns [`AmqpError::Transaction`] when a transaction is already open on this handle;
258    /// the open transaction is left untouched.
259    async fn begin_transaction(&self) -> Result<(), Self::Error> {
260        let already_open = {
261            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
262            let open = txn.is_some();
263            if !open {
264                *txn = Some(Vec::new());
265            }
266            open
267        };
268        if already_open {
269            return Err(AmqpError::Transaction(
270                "a transaction is already open on this confirms publisher; commit or abort it \
271                 before beginning another"
272                    .to_owned(),
273            ));
274        }
275        Ok(())
276    }
277
278    /// Publishes the buffered messages in order and awaits every confirm.
279    ///
280    /// # Errors
281    ///
282    /// Returns [`AmqpError::Transaction`] when no transaction is open, and
283    /// [`AmqpError::Publish`] when any message fails to publish or the broker returns a negative
284    /// confirm. Messages already flushed stay published: publisher confirms give durability per
285    /// message, not atomicity across them (use [`ServerTxPublish`] for that).
286    async fn commit(&self) -> Result<(), Self::Error> {
287        let buffered = {
288            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
289            txn.take()
290        };
291        let Some(buffered) = buffered else {
292            return Err(AmqpError::Transaction(
293                "commit with no open transaction on this confirms publisher".to_owned(),
294            ));
295        };
296        if buffered.is_empty() {
297            return Ok(());
298        }
299
300        let target = buffered[0].0.as_str();
301        self.conn.ensure_live(target)?;
302        let channel = self.channel(target).await?;
303        let mut confirms = Vec::with_capacity(buffered.len());
304        for (routing_key, payload, headers) in &buffered {
305            let properties = convert::properties_for_publish(headers, self.options.persistent)?;
306            let confirm = do_publish(
307                channel,
308                &self.options.exchange,
309                routing_key,
310                payload,
311                properties,
312            )
313            .await?;
314            confirms.push((routing_key, confirm));
315        }
316        for (routing_key, confirm) in confirms {
317            let confirmation = confirm.await.map_err(AmqpError::publish)?;
318            confirmation_ok(&confirmation, routing_key)?;
319        }
320        Ok(())
321    }
322
323    /// Discards the buffered messages without publishing anything.
324    ///
325    /// # Errors
326    ///
327    /// Returns [`AmqpError::Transaction`] when no transaction is open.
328    async fn abort(&self) -> Result<(), Self::Error> {
329        let discarded = self
330            .txn
331            .lock()
332            .expect("transaction buffer mutex poisoned")
333            .take();
334        if discarded.is_none() {
335            return Err(AmqpError::Transaction(
336                "abort with no open transaction on this confirms publisher".to_owned(),
337            ));
338        }
339        Ok(())
340    }
341}
342
343/// The live publisher backed by AMQP server transactions (`tx.select` / `tx.commit` /
344/// `tx.rollback`).
345///
346/// Between [`begin_transaction`](TransactionalPublisher::begin_transaction) and
347/// [`commit`](TransactionalPublisher::commit) messages accumulate on the broker inside the
348/// channel transaction and become visible atomically at commit;
349/// [`abort`](TransactionalPublisher::abort) rolls them back server-side. Outside a transaction
350/// [`publish`](Publisher::publish) behaves like the fire-and-forget publisher.
351///
352/// Only the borrowed transaction kind ([`TransactionalPublisher`]) applies here, unlike
353/// [`ConfirmsPublisher`]: `tx.select` puts the channel itself into transactional mode, so the
354/// transaction is channel state with exactly one instance, and there is no buffer for an owned
355/// [`Transaction`](ruststream::Transaction) value to own.
356///
357/// Clones share the transactional channel and its open/closed state. Interleaving `publish`
358/// and `begin_transaction`/`commit` from concurrent tasks is not supported: which side of the
359/// transaction boundary a concurrent publish lands on would be a race either way. Like every
360/// live publisher it aliases the connection and may outlive it: after shutdown every operation
361/// reports [`AmqpError::Closed`].
362#[derive(Debug, Clone)]
363pub struct ServerTxPublisher {
364    conn: Arc<AmqpConnection>,
365    options: PublishOptions,
366    channel: Arc<OnceCell<Channel>>,
367    open: Arc<Mutex<bool>>,
368}
369
370impl ServerTxPublisher {
371    pub(crate) fn new(connected: &ConnectedLapinBroker, options: PublishOptions) -> Self {
372        Self {
373            conn: Arc::clone(connected.connection()),
374            options,
375            channel: Arc::new(OnceCell::new()),
376            open: Arc::new(Mutex::new(false)),
377        }
378    }
379
380    /// The transactional channel, opened on first use; see [`ConfirmsPublisher::channel`] for
381    /// why it is not opened at pairing time.
382    async fn tx_channel(&self, target: &str) -> Result<&Channel, AmqpError> {
383        self.channel
384            .get_or_try_init(|| async {
385                let channel = self
386                    .conn
387                    .live_connection(target)?
388                    .create_channel()
389                    .await
390                    .map_err(AmqpError::publish)?;
391                channel.tx_select().await.map_err(AmqpError::publish)?;
392                Ok(channel)
393            })
394            .await
395    }
396
397    fn is_open(&self) -> bool {
398        *self.open.lock().expect("transaction state mutex poisoned")
399    }
400
401    fn set_open(&self, open: bool) {
402        *self.open.lock().expect("transaction state mutex poisoned") = open;
403    }
404}
405
406impl Publisher for ServerTxPublisher {
407    type Error = AmqpError;
408
409    /// Publishes `msg`: into the open server transaction, or plainly when none is open.
410    ///
411    /// # Errors
412    ///
413    /// Returns [`AmqpError::Closed`] once the broker has shut down and [`AmqpError::Publish`]
414    /// when the channel rejects the frame.
415    ///
416    /// # Cancel safety
417    ///
418    /// Not cancel safe: dropping the future may leave the message queued in the transaction or
419    /// not.
420    async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
421        let properties = convert::properties_for_publish(msg.headers(), self.options.persistent)?;
422        let channel = if self.is_open() {
423            self.conn.ensure_live(msg.name())?;
424            self.tx_channel(msg.name()).await?
425        } else {
426            self.conn.live_publish_channel(msg.name())?
427        };
428        let _confirm = do_publish(
429            channel,
430            &self.options.exchange,
431            msg.name(),
432            msg.payload(),
433            properties,
434        )
435        .await?;
436        Ok(())
437    }
438}
439
440/// The transaction target named in diagnostics: server transactions are a property of the
441/// channel, not of one routing key.
442const TX_TARGET: &str = "the transactional channel";
443
444impl TransactionalPublisher for ServerTxPublisher {
445    /// Opens a server transaction (`tx.select` on first use).
446    ///
447    /// # Errors
448    ///
449    /// Returns [`AmqpError::Transaction`] when a transaction is already open on this handle
450    /// (the open transaction is left untouched), [`AmqpError::Closed`] once the broker has shut
451    /// down, and [`AmqpError::Publish`] when the transactional channel cannot be set up.
452    async fn begin_transaction(&self) -> Result<(), Self::Error> {
453        if self.is_open() {
454            return Err(AmqpError::Transaction(
455                "a transaction is already open on this server-transactional publisher; commit or \
456                 abort it before beginning another"
457                    .to_owned(),
458            ));
459        }
460        self.conn.ensure_live(TX_TARGET)?;
461        self.tx_channel(TX_TARGET).await?;
462        self.set_open(true);
463        Ok(())
464    }
465
466    /// Commits the open server transaction.
467    ///
468    /// # Errors
469    ///
470    /// Returns [`AmqpError::Transaction`] when no transaction is open, and
471    /// [`AmqpError::Publish`] when `tx.commit` fails; the transaction state on the broker is
472    /// then unknown (the channel may be closed) and the publisher should be discarded.
473    async fn commit(&self) -> Result<(), Self::Error> {
474        if !self.is_open() {
475            return Err(AmqpError::Transaction(
476                "commit with no open transaction on this server-transactional publisher".to_owned(),
477            ));
478        }
479        self.conn.ensure_live(TX_TARGET)?;
480        let channel = self.tx_channel(TX_TARGET).await?;
481        channel.tx_commit().await.map_err(AmqpError::publish)?;
482        self.set_open(false);
483        Ok(())
484    }
485
486    /// Rolls back the open server transaction.
487    ///
488    /// # Errors
489    ///
490    /// Returns [`AmqpError::Transaction`] when no transaction is open, and
491    /// [`AmqpError::Publish`] when `tx.rollback` fails.
492    async fn abort(&self) -> Result<(), Self::Error> {
493        if !self.is_open() {
494            return Err(AmqpError::Transaction(
495                "abort with no open transaction on this server-transactional publisher".to_owned(),
496            ));
497        }
498        self.conn.ensure_live(TX_TARGET)?;
499        let channel = self.tx_channel(TX_TARGET).await?;
500        channel.tx_rollback().await.map_err(AmqpError::publish)?;
501        self.set_open(false);
502        Ok(())
503    }
504}