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}