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}