Skip to main content

ruststream_lapin/
publisher.rs

1//! The publishers: fire-and-forget, confirm-transactional, and server-transactional.
2
3use std::sync::{Arc, Mutex};
4
5use bytes::Bytes;
6use lapin::options::{BasicPublishOptions, ConfirmSelectOptions};
7use lapin::{BasicProperties, Channel};
8use lapin::{Confirmation, PublisherConfirm};
9use ruststream::{Headers, OutgoingMessage, Publisher, TransactionalPublisher};
10use tokio::sync::OnceCell;
11
12use crate::broker::SharedConn;
13use crate::convert;
14use crate::error::AmqpError;
15
16/// One buffered publish: routing key, payload, headers.
17type Buffered = (String, Bytes, Headers);
18
19pub(crate) async fn do_publish(
20    channel: &Channel,
21    exchange: &str,
22    routing_key: &str,
23    payload: &[u8],
24    properties: BasicProperties,
25) -> Result<PublisherConfirm, AmqpError> {
26    channel
27        .basic_publish(
28            convert::short(exchange, "exchange name")?,
29            convert::short(routing_key, "routing key")?,
30            BasicPublishOptions::default(),
31            payload,
32            properties,
33        )
34        .await
35        .map_err(AmqpError::publish)
36}
37
38/// Fire-and-forget publisher on the broker's shared publish channel.
39///
40/// [`OutgoingMessage::name`] is the routing key; the target exchange is a property of the
41/// publisher (the default exchange unless [`exchange`](Self::exchange) says otherwise). On the
42/// default exchange the routing key addresses the queue with that name.
43///
44/// Messages are published persistent (delivery mode 2) unless
45/// [`persistent(false)`](Self::persistent) opts out.
46///
47/// Obtained from [`LapinBroker::publisher`](crate::LapinBroker::publisher); usable before
48/// `Broker::connect` resolves the connection (publishing earlier returns
49/// [`AmqpError::NotConnected`]).
50#[derive(Debug, Clone)]
51pub struct LapinPublisher {
52    conn: SharedConn,
53    exchange: String,
54    persistent: bool,
55}
56
57impl LapinPublisher {
58    pub(crate) fn new(conn: SharedConn) -> Self {
59        Self {
60            conn,
61            exchange: String::new(),
62            persistent: true,
63        }
64    }
65
66    /// Publishes to `exchange` instead of the default exchange.
67    #[must_use]
68    pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
69        self.exchange = exchange.into();
70        self
71    }
72
73    /// Whether messages are marked persistent (delivery mode 2). Defaults to `true`.
74    #[must_use]
75    pub fn persistent(mut self, persistent: bool) -> Self {
76        self.persistent = persistent;
77        self
78    }
79
80    /// Upgrades to a publisher that awaits broker confirms, with buffering transactions.
81    ///
82    /// The recommended transactional publisher: durable and much faster than AMQP server
83    /// transactions.
84    #[must_use]
85    pub fn confirms(self) -> ConfirmsPublisher {
86        ConfirmsPublisher {
87            conn: self.conn,
88            exchange: self.exchange,
89            persistent: self.persistent,
90            channel: Arc::new(OnceCell::new()),
91            txn: Arc::new(Mutex::new(None)),
92        }
93    }
94
95    /// Upgrades to a publisher backed by AMQP server transactions (`tx.select`).
96    ///
97    /// Server-side atomicity, at the cost of a synchronous commit round trip that is
98    /// significantly slower than [`confirms`](Self::confirms).
99    #[must_use]
100    pub fn server_tx(self) -> ServerTxPublisher {
101        ServerTxPublisher {
102            conn: self.conn,
103            exchange: self.exchange,
104            persistent: self.persistent,
105            channel: Arc::new(OnceCell::new()),
106            open: Arc::new(Mutex::new(false)),
107        }
108    }
109}
110
111impl Publisher for LapinPublisher {
112    type Error = AmqpError;
113
114    /// Publishes `msg` without waiting for a broker confirm.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`AmqpError::NotConnected`] before `Broker::connect` resolves the connection and
119    /// [`AmqpError::Publish`] when the channel rejects the frame.
120    ///
121    /// # Cancel safety
122    ///
123    /// Not cancel safe: dropping the future may leave the message published or not.
124    async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
125        let state = self.conn.get().ok_or(AmqpError::NotConnected)?;
126        let properties = convert::properties_for_publish(msg.headers(), self.persistent)?;
127        // Without confirm_select on the channel the returned confirm resolves to NotRequested;
128        // dropping it does not lose anything.
129        let _confirm = do_publish(
130            state.publish_channel(),
131            &self.exchange,
132            msg.name(),
133            msg.payload(),
134            properties,
135        )
136        .await?;
137        Ok(())
138    }
139}
140
141/// A publisher that awaits broker confirms for every message.
142///
143/// Outside a transaction each [`publish`](Publisher::publish) resolves only once the broker
144/// confirmed the message. Between
145/// [`begin_transaction`](TransactionalPublisher::begin_transaction) and
146/// [`commit`](TransactionalPublisher::commit) messages buffer in memory; `commit` publishes them
147/// in order and awaits all confirms, and [`abort`](TransactionalPublisher::abort) discards the
148/// buffer without touching the broker.
149///
150/// Clones share one confirm channel and one transaction buffer.
151#[derive(Debug, Clone)]
152pub struct ConfirmsPublisher {
153    conn: SharedConn,
154    exchange: String,
155    persistent: bool,
156    channel: Arc<OnceCell<Channel>>,
157    txn: Arc<Mutex<Option<Vec<Buffered>>>>,
158}
159
160impl ConfirmsPublisher {
161    async fn channel(&self) -> Result<&Channel, AmqpError> {
162        self.channel
163            .get_or_try_init(|| async {
164                let state = self.conn.get().ok_or(AmqpError::NotConnected)?;
165                let channel = state
166                    .connection()
167                    .create_channel()
168                    .await
169                    .map_err(AmqpError::publish)?;
170                channel
171                    .confirm_select(ConfirmSelectOptions::default())
172                    .await
173                    .map_err(AmqpError::publish)?;
174                Ok(channel)
175            })
176            .await
177    }
178
179    async fn publish_confirmed(
180        &self,
181        routing_key: &str,
182        payload: &[u8],
183        headers: &Headers,
184    ) -> Result<(), AmqpError> {
185        let channel = self.channel().await?;
186        let properties = convert::properties_for_publish(headers, self.persistent)?;
187        let confirm = do_publish(channel, &self.exchange, routing_key, payload, properties)
188            .await?
189            .await
190            .map_err(AmqpError::publish)?;
191        confirmation_ok(&confirm, routing_key)
192    }
193}
194
195fn confirmation_ok(confirmation: &Confirmation, routing_key: &str) -> Result<(), AmqpError> {
196    if confirmation.is_nack() {
197        return Err(AmqpError::Publish(
198            format!("the broker negatively confirmed the publish to {routing_key:?}").into(),
199        ));
200    }
201    Ok(())
202}
203
204impl Publisher for ConfirmsPublisher {
205    type Error = AmqpError;
206
207    /// Publishes `msg`, awaiting the broker confirm (or buffering inside a transaction).
208    ///
209    /// # Errors
210    ///
211    /// Returns [`AmqpError::NotConnected`] before `Broker::connect` resolves the connection and
212    /// [`AmqpError::Publish`] when the channel rejects the frame or the broker returns a
213    /// negative confirm.
214    ///
215    /// # Cancel safety
216    ///
217    /// Not cancel safe outside a transaction: dropping the future may leave the message
218    /// published but unconfirmed. Inside a transaction buffering is synchronous and dropping the
219    /// future is harmless.
220    async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
221        {
222            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
223            if let Some(buffer) = txn.as_mut() {
224                buffer.push((
225                    msg.name().to_owned(),
226                    Bytes::copy_from_slice(msg.payload()),
227                    msg.headers().clone(),
228                ));
229                return Ok(());
230            }
231        }
232        self.publish_confirmed(msg.name(), msg.payload(), msg.headers())
233            .await
234    }
235}
236
237impl TransactionalPublisher for ConfirmsPublisher {
238    /// Opens the buffering transaction; a no-op when one is already open.
239    ///
240    /// # Errors
241    ///
242    /// Never fails today; the signature leaves room for transport errors.
243    async fn begin_transaction(&self) -> Result<(), Self::Error> {
244        self.txn
245            .lock()
246            .expect("transaction buffer mutex poisoned")
247            .get_or_insert_with(Vec::new);
248        Ok(())
249    }
250
251    /// Publishes the buffered messages in order and awaits every confirm.
252    ///
253    /// # Errors
254    ///
255    /// Returns [`AmqpError::Publish`] when any message fails to publish or the broker returns a
256    /// negative confirm. Messages already flushed stay published: publisher confirms give
257    /// durability per message, not atomicity across them (use
258    /// [`server_tx`](LapinPublisher::server_tx) for that).
259    async fn commit(&self) -> Result<(), Self::Error> {
260        let buffered = {
261            let mut txn = self.txn.lock().expect("transaction buffer mutex poisoned");
262            txn.take()
263        };
264        let Some(buffered) = buffered else {
265            return Ok(());
266        };
267        if buffered.is_empty() {
268            return Ok(());
269        }
270
271        let channel = self.channel().await?;
272        let mut confirms = Vec::with_capacity(buffered.len());
273        for (routing_key, payload, headers) in &buffered {
274            let properties = convert::properties_for_publish(headers, self.persistent)?;
275            let confirm =
276                do_publish(channel, &self.exchange, routing_key, payload, properties).await?;
277            confirms.push((routing_key, confirm));
278        }
279        for (routing_key, confirm) in confirms {
280            let confirmation = confirm.await.map_err(AmqpError::publish)?;
281            confirmation_ok(&confirmation, routing_key)?;
282        }
283        Ok(())
284    }
285
286    /// Discards the buffered messages without publishing anything.
287    ///
288    /// # Errors
289    ///
290    /// Never fails today; the signature leaves room for transport errors.
291    async fn abort(&self) -> Result<(), Self::Error> {
292        self.txn
293            .lock()
294            .expect("transaction buffer mutex poisoned")
295            .take();
296        Ok(())
297    }
298}
299
300/// A publisher backed by AMQP server transactions (`tx.select` / `tx.commit` / `tx.rollback`).
301///
302/// Between [`begin_transaction`](TransactionalPublisher::begin_transaction) and
303/// [`commit`](TransactionalPublisher::commit) messages accumulate on the broker inside the
304/// channel transaction and become visible atomically at commit;
305/// [`abort`](TransactionalPublisher::abort) rolls them back server-side. Outside a transaction
306/// [`publish`](Publisher::publish) behaves like the fire-and-forget publisher.
307///
308/// Clones share the transactional channel and its open/closed state. Interleaving `publish`
309/// and `begin_transaction`/`commit` from concurrent tasks is not supported: which side of the
310/// transaction boundary a concurrent publish lands on would be a race either way.
311#[derive(Debug, Clone)]
312pub struct ServerTxPublisher {
313    conn: SharedConn,
314    exchange: String,
315    persistent: bool,
316    channel: Arc<OnceCell<Channel>>,
317    open: Arc<Mutex<bool>>,
318}
319
320impl ServerTxPublisher {
321    async fn tx_channel(&self) -> Result<&Channel, AmqpError> {
322        self.channel
323            .get_or_try_init(|| async {
324                let state = self.conn.get().ok_or(AmqpError::NotConnected)?;
325                let channel = state
326                    .connection()
327                    .create_channel()
328                    .await
329                    .map_err(AmqpError::publish)?;
330                channel.tx_select().await.map_err(AmqpError::publish)?;
331                Ok(channel)
332            })
333            .await
334    }
335
336    fn is_open(&self) -> bool {
337        *self.open.lock().expect("transaction state mutex poisoned")
338    }
339
340    fn set_open(&self, open: bool) {
341        *self.open.lock().expect("transaction state mutex poisoned") = open;
342    }
343}
344
345impl Publisher for ServerTxPublisher {
346    type Error = AmqpError;
347
348    /// Publishes `msg`: into the open server transaction, or plainly when none is open.
349    ///
350    /// # Errors
351    ///
352    /// Returns [`AmqpError::NotConnected`] before `Broker::connect` resolves the connection and
353    /// [`AmqpError::Publish`] when the channel rejects the frame.
354    ///
355    /// # Cancel safety
356    ///
357    /// Not cancel safe: dropping the future may leave the message queued in the transaction or
358    /// not.
359    async fn publish(&self, msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
360        let properties = convert::properties_for_publish(msg.headers(), self.persistent)?;
361        let channel = if self.is_open() {
362            self.tx_channel().await?
363        } else {
364            let state = self.conn.get().ok_or(AmqpError::NotConnected)?;
365            state.publish_channel()
366        };
367        let _confirm = do_publish(
368            channel,
369            &self.exchange,
370            msg.name(),
371            msg.payload(),
372            properties,
373        )
374        .await?;
375        Ok(())
376    }
377}
378
379impl TransactionalPublisher for ServerTxPublisher {
380    /// Opens a server transaction (`tx.select` on first use); a no-op when one is open.
381    ///
382    /// # Errors
383    ///
384    /// Returns [`AmqpError::NotConnected`] before `Broker::connect` resolves the connection and
385    /// [`AmqpError::Publish`] when the transactional channel cannot be set up.
386    async fn begin_transaction(&self) -> Result<(), Self::Error> {
387        self.tx_channel().await?;
388        self.set_open(true);
389        Ok(())
390    }
391
392    /// Commits the open server transaction; a no-op when none is open.
393    ///
394    /// # Errors
395    ///
396    /// Returns [`AmqpError::Publish`] when `tx.commit` fails; the transaction state on the
397    /// broker is then unknown (the channel may be closed) and the publisher should be discarded.
398    async fn commit(&self) -> Result<(), Self::Error> {
399        if !self.is_open() {
400            return Ok(());
401        }
402        let channel = self.tx_channel().await?;
403        channel.tx_commit().await.map_err(AmqpError::publish)?;
404        self.set_open(false);
405        Ok(())
406    }
407
408    /// Rolls back the open server transaction; a no-op when none is open.
409    ///
410    /// # Errors
411    ///
412    /// Returns [`AmqpError::Publish`] when `tx.rollback` fails.
413    async fn abort(&self) -> Result<(), Self::Error> {
414        if !self.is_open() {
415            return Ok(());
416        }
417        let channel = self.tx_channel().await?;
418        channel.tx_rollback().await.map_err(AmqpError::publish)?;
419        self.set_open(false);
420        Ok(())
421    }
422}