queuey_rabbitmq/backend.rs
1//! The [`Backend`] implementation.
2
3use std::{
4 sync::{
5 Arc,
6 atomic::{AtomicU64, Ordering},
7 },
8 time::{Duration, SystemTime, UNIX_EPOCH},
9};
10
11use async_trait::async_trait;
12use futures::StreamExt;
13use lapin::{
14 Channel, Connection, Consumer,
15 message::Delivery as LapinDelivery,
16 options::{BasicAckOptions, BasicConsumeOptions, BasicQosOptions, BasicRejectOptions},
17 types::{FieldTable, MAX_SHORT_STRING_LENGTH},
18};
19use queuey_core::{Backend, Delivery, DeliveryStream, Envelope, QueueConfig, Result};
20use tracing::{debug, error, info, warn};
21
22use crate::{
23 codec,
24 connection::{ConnectionHandle, REPLY_SUCCESS, declare_topology},
25 delivery::RabbitMqDelivery,
26 error::{RabbitMqError, amqp, short_string},
27 options::RabbitMqOptions,
28 publisher::{Hold, Publisher},
29 reconnect::{Attempt, Rebuilding},
30 topology,
31};
32
33/// A [`Backend`] backed by a single RabbitMQ connection.
34///
35/// * One [`Connection`].
36/// * One publishing [`Channel`] in confirm mode, shared behind a
37/// [`tokio::sync::Mutex`]. Every publish (enqueue, retry, defer, dead-letter)
38/// is `mandatory` and waits for the broker's confirmation. The channel is
39/// reopened lazily if a channel exception closed it. Nothing is ever
40/// *declared* on it.
41/// * One long-lived [`Channel`] for the hold queue declarations every retry,
42/// delayed publish and [`defer`](Backend::defer) makes on demand. A
43/// declaration is the one thing the broker routinely refuses
44/// (`PRECONDITION_FAILED` closes the channel it ran on), so it is kept away
45/// from the publishes it would otherwise take down with it.
46/// * One fresh [`Channel`] per [`consume`](Backend::consume) call, so each
47/// consumer gets its own `basic_qos` prefetch window and a failure on one
48/// consumer cannot take down the others.
49/// * One short-lived channel per [`declare`](Backend::declare) call, so a
50/// rejected declaration (e.g. re-declaring an existing queue with different
51/// arguments, which RabbitMQ answers with `PRECONDITION_FAILED` and closes the
52/// channel) cannot poison the other channels.
53///
54/// # Reconnection
55///
56/// The connection above is not one socket but a slot. When it drops, the first
57/// operation to notice dials a replacement, the others queue behind it, every
58/// queue this backend declared is re-declared on it, and the consumer streams
59/// resubscribe and carry on yielding. Nothing above the backend sees an error:
60/// a publish issued during the outage waits, and
61/// [`Worker::run`](queuey_core::Worker::run) keeps running.
62///
63/// What does *not* survive is anything already in flight. The broker requeues
64/// every unacknowledged delivery when a connection drops, so a job whose handler
65/// was mid-run is delivered again on the new connection, and when the first run
66/// finishes and tries to settle, that settle fails (the worker counts it in
67/// [`WorkerHandle::settle_failures`](queuey_core::WorkerHandle::settle_failures)).
68/// Handlers were already required to tolerate this — the contract is
69/// at-least-once — but an outage is when it stops being theoretical.
70///
71/// See [`RabbitMqOptions::reconnect`] to bound the attempts or turn it off, and
72/// [`topology`] for the queues this creates.
73#[derive(Debug)]
74pub struct RabbitMqBackend {
75 connection: Arc<ConnectionHandle>,
76 publisher: Publisher,
77 options: Arc<RabbitMqOptions>,
78}
79
80impl RabbitMqBackend {
81 /// Connect to `uri` with [`RabbitMqOptions::default`].
82 ///
83 /// ```no_run
84 /// # async fn example() -> queuey_core::Result<()> {
85 /// use queuey_rabbitmq::RabbitMqBackend;
86 ///
87 /// let backend = RabbitMqBackend::connect("amqp://guest:guest@localhost:5672/%2f").await?;
88 /// # Ok(()) }
89 /// ```
90 pub async fn connect(uri: &str) -> Result<Self> {
91 Self::with_options(uri, RabbitMqOptions::default()).await
92 }
93
94 /// Connect to `uri` with explicit options.
95 ///
96 /// Only the *first* connection is made here, and it is not retried: a
97 /// process that cannot reach its broker at startup should fail loudly rather
98 /// than block its caller in a backoff loop. Every connection after this one
99 /// is [`RabbitMqOptions::reconnect`]'s business.
100 pub async fn with_options(uri: &str, options: RabbitMqOptions) -> Result<Self> {
101 let options = Arc::new(options);
102 let connection = ConnectionHandle::connect(uri, Arc::clone(&options)).await?;
103 let live = connection.ensure_connected().await?;
104
105 let channel = Publisher::open_confirm_channel(&live).await?;
106 // A second, non-confirm channel used only for the on-demand hold queue
107 // declarations: a refused declaration closes the channel it ran on, and
108 // that must never be the channel every publish shares.
109 let declare_channel = live.create_channel().await.map_err(amqp)?;
110
111 info!(
112 channel = channel.id(),
113 declare_channel = declare_channel.id(),
114 reconnects = connection.reconnects(),
115 "rabbitmq backend connected"
116 );
117
118 Ok(Self {
119 publisher: Publisher::new(
120 Arc::clone(&connection),
121 channel,
122 declare_channel,
123 Arc::clone(&options),
124 ),
125 connection,
126 options,
127 })
128 }
129
130 /// Whether the backend currently holds a live connection.
131 ///
132 /// A `false` does not mean the backend is broken: with reconnection on (the
133 /// default) the next operation waits for a replacement. It is here for
134 /// health endpoints and dashboards that want to report the gap rather than
135 /// cause one, and it never itself triggers a reconnect.
136 #[must_use]
137 pub fn is_connected(&self) -> bool {
138 self.connection.is_connected()
139 }
140
141 /// The options this backend was built with.
142 #[must_use]
143 pub fn options(&self) -> &RabbitMqOptions {
144 &self.options
145 }
146
147 /// The name of the dead-letter queue backing `queue`.
148 #[must_use]
149 pub fn dead_queue_name(&self, queue: &str) -> String {
150 topology::dead_queue_name(queue, &self.options.dead_suffix)
151 }
152
153 /// The name of the hold queue that `ttl_ms`-long waits of `queue` happen in.
154 ///
155 /// There is one per distinct rounded delay, shared by retries and
156 /// deferrals, and it is created on demand by whatever schedules the wait
157 /// rather than by [`declare`](Backend::declare); see [`topology`].
158 #[must_use]
159 pub fn deferred_queue_name(&self, queue: &str, ttl_ms: u32) -> String {
160 topology::deferred_queue_name(queue, &self.options.deferred_suffix, ttl_ms)
161 }
162
163 /// Declare `q` and (optionally) `q.dead` on `channel`, and remember the
164 /// config.
165 ///
166 /// Hold queues are *not* declared here: their names depend on the delays
167 /// jobs actually ask for, so they are created on demand by
168 /// [`publish`](Backend::publish) with a delay, [`defer`](Backend::defer)
169 /// and the delivery's `retry` / `defer`, and deleted again by the broker
170 /// once idle.
171 ///
172 /// The declaration itself is [`declare_topology`], shared with the replay a
173 /// reconnect performs, so what comes back after an outage is exactly what
174 /// was created before it.
175 async fn declare_one(&self, channel: &Channel, config: &QueueConfig) -> Result<()> {
176 declare_topology(channel, config, &self.options).await?;
177
178 // Only after the declarations landed: a retry or deferral onto this
179 // queue now knows whether its hold queue has to be durable, and with how
180 // many priority levels the returning job will be ordered. This is also
181 // the record a reconnect replays.
182 self.connection.remember(config);
183
184 debug!(queue = %config.name, "topology declared");
185 Ok(())
186 }
187}
188
189#[async_trait]
190impl Backend for RabbitMqBackend {
191 async fn declare(&self, queues: &[QueueConfig]) -> Result<()> {
192 if queues.is_empty() {
193 return Ok(());
194 }
195 // Up front, before a single queue exists: a name that leaves no room for
196 // its hold queues would otherwise declare fine and then fail one retry
197 // at a time, in production, on the day someone picks a long delay.
198 // Nothing is created when this fails.
199 for config in queues {
200 check_deferrable_name(&config.name, &self.options.deferred_suffix)?;
201 }
202 let channel = self
203 .connection
204 .ensure_connected()
205 .await?
206 .create_channel()
207 .await
208 .map_err(amqp)?;
209 let result: Result<()> = async {
210 for config in queues {
211 self.declare_one(&channel, config).await?;
212 }
213 Ok(())
214 }
215 .await;
216
217 // Best-effort cleanup: the declaration result is what matters.
218 if channel.status().connected()
219 && let Err(error) = channel.close(REPLY_SUCCESS, "OK".into()).await
220 {
221 debug!(%error, "closing the declaration channel failed");
222 }
223 result
224 }
225
226 /// Publish `envelope` to its queue, or with `delay` into the hold queue
227 /// that releases it onto its queue afterwards.
228 ///
229 /// A delayed publish is held exactly like a retry: the delay is rounded up
230 /// to [`retry_granularity`](RabbitMqOptions::retry_granularity), the job
231 /// returns at the priority the envelope carries (`0` for a fresh envelope,
232 /// so it joins the back of the queue), and the same two rules as for
233 /// [`defer`](Backend::defer) apply: the queue must have been declared
234 /// through this backend, and the delay must not exceed
235 /// [`MAX_DEFERRAL_MS`](topology::MAX_DEFERRAL_MS). An undelayed publish
236 /// has neither restriction.
237 async fn publish(&self, envelope: &Envelope, delay: Option<Duration>) -> Result<()> {
238 match delay {
239 None => self.publisher.publish_envelope(envelope).await,
240 Some(delay) => {
241 self.publisher
242 .publish_held(envelope, delay, Hold::Retry)
243 .await
244 }
245 }
246 }
247
248 /// Hold `envelope` for `delay`, then put it back on its own queue.
249 ///
250 /// # The queue must have been declared through this backend
251 ///
252 /// Deferring onto a queue this backend instance never
253 /// [`declare`](Backend::declare)d is [`Error::UnknownQueue`], not a
254 /// best-effort publish. A hold queue has to know its main queue's durability
255 /// and dead-letter it back by name, and neither can be guessed: a transient
256 /// hold queue in front of a durable queue loses jobs on a restart, and a TTL
257 /// expiry into a queue that does not exist is dropped by the broker in
258 /// silence. Unlike a `mandatory` publish, nothing is returned and nothing
259 /// is reported.
260 ///
261 /// `Producer::new` and `WorkerBuilder::build` declare the whole queue set,
262 /// so anything built through them can defer. `Producer::new_undeclared`
263 /// deliberately does not, so a producer built that way can enqueue but
264 /// cannot defer, or enqueue with a delay, until something in the process
265 /// declares the queue.
266 ///
267 /// # The delay has a ceiling
268 ///
269 /// Delays are rounded up to
270 /// [`deferred_granularity`](RabbitMqOptions::deferred_granularity) and
271 /// capped at [`MAX_DEFERRAL_MS`](topology::MAX_DEFERRAL_MS) (~24.8 days);
272 /// a longer one is an error rather than a shorter wait.
273 ///
274 /// [`Error::UnknownQueue`]: queuey_core::Error::UnknownQueue
275 async fn defer(&self, envelope: &Envelope, delay: Duration) -> Result<()> {
276 self.publisher
277 .publish_held(envelope, delay, Hold::Deferral)
278 .await
279 }
280
281 /// Subscribe to `queue` and stream its deliveries.
282 ///
283 /// The stream outlives the connection it started on: if the connection
284 /// drops, it waits for a reconnect and resubscribes rather than ending, so
285 /// [`Worker::run`](queuey_core::Worker::run) keeps going across a broker
286 /// restart. It ends only when the backend is [`close`](Backend::close)d, or
287 /// when reconnection is disabled or exhausted, which is what
288 /// [`Error::ConsumerStopped`](queuey_core::Error::ConsumerStopped) is for.
289 ///
290 /// Deliveries the worker was still holding when the connection dropped are
291 /// requeued by the broker and delivered again here; settling them on the old
292 /// connection fails. See the type-level docs.
293 async fn consume(&self, queue: &QueueConfig) -> Result<DeliveryStream> {
294 let connection = self.connection.ensure_connected().await?;
295 let generation = self.connection.generation();
296 let (channel, consumer) = subscribe(&connection, queue).await?;
297
298 let stream: DeliveryStream = Box::pin(delivery_stream(ConsumeState {
299 consumer,
300 // Held purely to keep the consumer's channel open for as long as
301 // the stream lives, and replaced wholesale on a resubscribe.
302 channel,
303 connection: Arc::clone(&self.connection),
304 publisher: self.publisher.clone(),
305 config: queue.clone(),
306 generation,
307 }));
308 Ok(stream)
309 }
310
311 /// Close the connection and every channel on it, for good.
312 ///
313 /// The handle is marked closing *first*, before a single channel goes down,
314 /// so that the consumers watching for a dropped connection see a deliberate
315 /// shutdown instead of an outage and end their streams rather than racing to
316 /// reconnect. Nothing reopens the connection afterwards.
317 async fn close(&self) -> Result<()> {
318 self.connection.mark_closing();
319 if let Err(error) = self.publisher.close().await {
320 debug!(%error, "closing the publishing channel failed");
321 }
322 self.connection.close().await?;
323 info!("rabbitmq backend closed");
324 Ok(())
325 }
326}
327
328/// Refuse a queue name whose *longest* hold queue name would not fit in an AMQP
329/// short string.
330///
331/// A queue name is valid at up to 255 bytes, but a hold queue appends the suffix
332/// and up to ten digits of TTL, so a perfectly legal `q` can have illegal hold
333/// queues. Checking the worst case,
334/// [`MAX_DEFERRAL_MS`](topology::MAX_DEFERRAL_MS), the longest TTL this backend
335/// will ever produce, means the answer does not depend on which delays happen
336/// to be used, so a name that passes `declare` can always be deferred on.
337fn check_deferrable_name(queue: &str, deferred_suffix: &str) -> Result<()> {
338 let hold = topology::deferred_queue_name(queue, deferred_suffix, topology::MAX_DEFERRAL_MS);
339 if hold.len() <= MAX_SHORT_STRING_LENGTH {
340 return Ok(());
341 }
342 Err(RabbitMqError::DeferredNameTooLong {
343 queue: queue.to_owned(),
344 length: hold.len(),
345 hold,
346 limit: MAX_SHORT_STRING_LENGTH,
347 }
348 .into_core())
349}
350
351/// Whether an error from `Connection::close` only says "already closing".
352///
353/// lapin flips every channel to `Closing` as soon as a connection close starts.
354/// A consumer channel dropped just before (a finished [`DeliveryStream`]) still
355/// has its own deferred `channel.close` queued; that command then fails the
356/// channel state check and lapin reports it through the connection-close
357/// promise, even though the connection does shut down normally. Nothing is
358/// lost and nothing is left open, so this is not an error worth surfacing.
359pub(crate) fn is_benign_close_error(error: &lapin::Error) -> bool {
360 use lapin::{ChannelState, ConnectionState, ErrorKind};
361 matches!(
362 error.kind(),
363 ErrorKind::InvalidChannelState(ChannelState::Closing | ChannelState::Closed, _)
364 | ErrorKind::InvalidConnectionState(ConnectionState::Closing | ConnectionState::Closed)
365 )
366}
367
368/// Open a channel on `connection` and start consuming `config`'s queue on it.
369///
370/// One channel per consumer: the `basic_qos` prefetch window is per channel, and
371/// a failure on one consumer must not take down the others. Shared by
372/// [`consume`](Backend::consume) and by the resubscribe a dropped connection
373/// triggers, so a recovered consumer is configured exactly like a fresh one.
374async fn subscribe(connection: &Connection, config: &QueueConfig) -> Result<(Channel, Consumer)> {
375 let channel = connection.create_channel().await.map_err(amqp)?;
376 channel
377 .basic_qos(config.prefetch, BasicQosOptions { global: false })
378 .await
379 .map_err(amqp)?;
380
381 let tag = consumer_tag(&config.name);
382 let consumer = channel
383 .basic_consume(
384 short_string(&config.name)?,
385 short_string(&tag)?,
386 BasicConsumeOptions {
387 no_local: false,
388 no_ack: false,
389 exclusive: false,
390 nowait: false,
391 },
392 FieldTable::default(),
393 )
394 .await
395 .map_err(amqp)?;
396
397 info!(queue = %config.name, prefetch = config.prefetch, %tag, "consuming");
398 Ok((channel, consumer))
399}
400
401/// Everything the consumer stream needs to keep alive between polls.
402struct ConsumeState {
403 consumer: Consumer,
404 /// Kept alive so the consumer's channel outlives the call that opened it,
405 /// and replaced on every resubscribe.
406 channel: Channel,
407 connection: Arc<ConnectionHandle>,
408 publisher: Publisher,
409 /// The full config, not just the name: a resubscribe needs the prefetch too.
410 config: QueueConfig,
411 /// Connection generation this subscription was made on, so a consumer can
412 /// tell "my connection died" from "somebody already replaced it".
413 generation: u64,
414}
415
416/// What a consumer should do after its subscription stopped producing.
417enum Resubscribed {
418 /// Back on a live connection; keep polling.
419 Yes,
420 /// The stream is over on purpose: the backend is closing, or reconnection is
421 /// switched off.
422 Stop,
423}
424
425/// Turn a lapin [`Consumer`] into a core [`DeliveryStream`].
426///
427/// Bodies that do not decode as an [`Envelope`] are disposed of in place (see
428/// [`discard_malformed`]) and never surface as stream items: a poison message
429/// must not stall or kill a worker.
430///
431/// A subscription that stops producing, whether it errors or simply ends, is
432/// rebuilt rather than propagated (see [`recover`]), so a dropped connection is
433/// a pause in this stream instead of the end of it. The stream still terminates
434/// on a deliberate close, and still yields an error when reconnection is off or
435/// has given up, which is what makes
436/// [`Error::ConsumerStopped`](queuey_core::Error::ConsumerStopped) reachable.
437fn delivery_stream(
438 state: ConsumeState,
439) -> impl futures::Stream<Item = Result<Box<dyn Delivery>>> + Send {
440 futures::stream::unfold(state, |mut state| async move {
441 loop {
442 let next = match state.consumer.next().await {
443 Some(Ok(delivery)) => Some(delivery),
444 // lapin always follows an error with end-of-stream, so the
445 // consumer is finished either way; both cases are the same
446 // question, which is whether this consumer can be rebuilt.
447 Some(Err(error)) => {
448 warn!(
449 queue = %state.config.name,
450 %error,
451 "consumer failed"
452 );
453 None
454 }
455 None => None,
456 };
457
458 let Some(delivery) = next else {
459 match recover(&mut state).await {
460 Ok(Resubscribed::Yes) => continue,
461 Ok(Resubscribed::Stop) => return None,
462 Err(error) => return Some((Err(error), state)),
463 }
464 };
465
466 match Envelope::from_bytes(&delivery.data) {
467 Ok(envelope) => {
468 let boxed: Box<dyn Delivery> = Box::new(RabbitMqDelivery::new(
469 envelope,
470 delivery.acker.clone(),
471 state.publisher.clone(),
472 ));
473 return Some((Ok(boxed), state));
474 }
475 Err(error) => {
476 warn!(
477 queue = %state.config.name,
478 delivery_tag = delivery.delivery_tag,
479 bytes = delivery.data.len(),
480 %error,
481 "dropping message whose body is not a valid envelope"
482 );
483 discard_malformed(&state.publisher, &state.config.name, &delivery).await;
484 }
485 }
486 }
487 })
488}
489
490/// Put a stopped consumer back on a live connection.
491///
492/// Returns [`Resubscribed::Stop`] when the stream is meant to end (the backend
493/// is closing, or reconnection is disabled), and an error when reconnection was
494/// tried and gave up. Both end the stream; the worker distinguishes them,
495/// because only one of the two is a failure.
496///
497/// The retry loop here is not the same one as
498/// [`ConnectionHandle::ensure_connected`]'s, and it is needed as well as that
499/// one: the connection can be perfectly healthy while `basic_consume` still
500/// fails, most obviously when the queue itself is gone (deleted by an operator,
501/// or lost to a broker restart and not restored because the redeclare after the
502/// reconnect was refused). Without a delay of its own that case would spin as
503/// fast as the broker can say no, so failures here are paced by the same
504/// [`ReconnectPolicy`](crate::ReconnectPolicy) and counted against the same
505/// attempt limit.
506async fn recover(state: &mut ConsumeState) -> Result<Resubscribed> {
507 if state.connection.is_closing() {
508 debug!(queue = %state.config.name, "consumer stopped; the backend is closing");
509 return Ok(Resubscribed::Stop);
510 }
511 let Some(policy) = state.connection.policy() else {
512 info!(
513 queue = %state.config.name,
514 "consumer stopped and reconnection is disabled; ending the stream"
515 );
516 return Ok(Resubscribed::Stop);
517 };
518
519 // The old channel is usually dead already, but a broker-side `basic.cancel`
520 // (a deleted queue) leaves it open with no consumer on it.
521 close_consumer_channel(&state.channel).await;
522
523 let mut failures: u32 = 0;
524 loop {
525 // `ensure_connected` enforces the same limit on the connection itself
526 // and blocks here until the broker is back.
527 let connection = state.connection.ensure_connected().await?;
528 let generation = state.connection.generation();
529
530 match subscribe(&connection, &state.config).await {
531 Ok((channel, consumer)) => {
532 info!(
533 queue = %state.config.name,
534 generation,
535 previous_generation = state.generation,
536 attempts = failures + 1,
537 "consumer resubscribed"
538 );
539 state.channel = channel;
540 state.consumer = consumer;
541 state.generation = generation;
542 return Ok(Resubscribed::Yes);
543 }
544 Err(error) => {
545 failures += 1;
546 let Some(delay) =
547 policy.next_delay(Attempt::after(Rebuilding::Consumer, failures, &error))
548 else {
549 error!(
550 queue = %state.config.name,
551 %error,
552 attempts = failures,
553 "giving up on resubscribing the consumer; the policy declined another \
554 attempt"
555 );
556 return Err(error);
557 };
558 warn!(
559 queue = %state.config.name,
560 %error,
561 failures,
562 ?delay,
563 "resubscribing the consumer failed; will try again"
564 );
565 state.connection.sleep_unless_closing(delay).await?;
566 }
567 }
568 }
569}
570
571/// Close a consumer's channel before it is replaced, best-effort.
572///
573/// Only reachable for a channel the broker left open (a `basic.cancel` after its
574/// queue was deleted); after a connection drop the channel is already gone and
575/// this is a no-op. Leaking it would hold a channel per outage on a
576/// long-running worker.
577async fn close_consumer_channel(channel: &Channel) {
578 if !channel.status().connected() {
579 return;
580 }
581 if let Err(error) = channel.close(REPLY_SUCCESS, "OK".into()).await {
582 debug!(%error, channel = channel.id(), "closing a replaced consumer channel failed");
583 }
584}
585
586/// Get an undecodable message off the queue without failing the stream.
587///
588/// The message is always settled if the broker will let us settle it, in this
589/// order:
590///
591/// 1. Copy the raw bytes to `q.dead` with an `x-death-reason` header and ack the
592/// original, so the payload survives for inspection. Skipped when
593/// [`RabbitMqOptions::declare_dead_letter_queues`] is off, because this
594/// backend then does not own `q.dead` and the `mandatory` publish would only
595/// come back unroutable.
596/// 2. `basic_reject(requeue = false)`, which discards the message (or hands it
597/// to the queue's own dead-letter exchange) rather than letting it be
598/// redelivered forever. This also runs when step 1 failed *after* the publish
599/// landed but the ack did not.
600/// 3. If even the reject fails, log at `ERROR` and move on. The message then
601/// stays unacknowledged and keeps one prefetch slot until the consumer
602/// channel closes, at which point the broker requeues it. Nothing better is
603/// available: settling it needs the very channel that just refused.
604async fn discard_malformed(publisher: &Publisher, queue: &str, delivery: &LapinDelivery) {
605 if publisher.options().declare_dead_letter_queues {
606 match publisher
607 .publish_malformed(queue, &delivery.data, codec::REASON_MALFORMED)
608 .await
609 {
610 Ok(()) => match delivery.acker.ack(BasicAckOptions::default()).await {
611 Ok(true) => return,
612 Ok(false) => {
613 warn!(
614 queue,
615 delivery_tag = delivery.delivery_tag,
616 "a malformed message was already settled; nothing left to ack"
617 );
618 return;
619 }
620 Err(error) => {
621 warn!(%error, queue, delivery_tag = delivery.delivery_tag,
622 "acking a malformed message failed; falling back to reject");
623 }
624 },
625 Err(error) => {
626 warn!(%error, queue, "forwarding a malformed message to the dead-letter queue failed");
627 }
628 }
629 }
630
631 match delivery
632 .acker
633 .reject(BasicRejectOptions { requeue: false })
634 .await
635 {
636 Ok(true) => {}
637 Ok(false) => {
638 warn!(
639 queue,
640 delivery_tag = delivery.delivery_tag,
641 "a malformed message was already settled; nothing left to reject"
642 );
643 }
644 Err(error) => {
645 error!(
646 %error, queue, delivery_tag = delivery.delivery_tag,
647 "rejecting a malformed message failed; it stays unacknowledged and holds a \
648 prefetch slot until the consumer channel closes"
649 );
650 }
651 }
652}
653
654/// A consumer tag that is unique within this process and short enough for AMQP.
655///
656/// RabbitMQ only requires uniqueness per channel, and this backend opens a fresh
657/// channel per consumer, but a readable, globally distinct tag makes the
658/// management UI far easier to reason about.
659fn consumer_tag(queue: &str) -> String {
660 static NEXT: AtomicU64 = AtomicU64::new(0);
661 let sequence = NEXT.fetch_add(1, Ordering::Relaxed);
662 let nanos = SystemTime::now()
663 .duration_since(UNIX_EPOCH)
664 .map(|since| since.as_nanos())
665 .unwrap_or_default();
666 // Leave ample room for the fixed parts inside the 255-byte AMQP limit.
667 let queue = codec::truncate_at_boundary(queue, 160);
668 format!("queuey.{queue}.{nanos:x}.{sequence}")
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674
675 #[test]
676 fn closing_state_errors_are_benign_on_close() {
677 use lapin::{ChannelState, ConnectionState, ErrorKind};
678 for kind in [
679 ErrorKind::InvalidChannelState(ChannelState::Closing, "channel.close"),
680 ErrorKind::InvalidChannelState(ChannelState::Closed, "channel.close"),
681 ErrorKind::InvalidConnectionState(ConnectionState::Closing),
682 ErrorKind::InvalidConnectionState(ConnectionState::Closed),
683 ] {
684 assert!(is_benign_close_error(&lapin::Error::from(kind)));
685 }
686 }
687
688 #[test]
689 fn other_state_errors_are_not_benign_on_close() {
690 use lapin::{ChannelState, ConnectionState, ErrorKind};
691 for kind in [
692 ErrorKind::InvalidChannelState(ChannelState::Initial, "channel.close"),
693 ErrorKind::InvalidChannelState(ChannelState::Error, "channel.close"),
694 ErrorKind::InvalidConnectionState(ConnectionState::Error),
695 ErrorKind::InvalidChannel(7),
696 ] {
697 assert!(!is_benign_close_error(&lapin::Error::from(kind)));
698 }
699 }
700
701 #[test]
702 fn consumer_tags_are_unique_and_name_the_queue() {
703 let first = consumer_tag("myapp.emails");
704 let second = consumer_tag("myapp.emails");
705 assert_ne!(first, second);
706 assert!(first.starts_with("queuey.myapp.emails."));
707 assert!(second.starts_with("queuey.myapp.emails."));
708 }
709
710 #[test]
711 fn consumer_tags_fit_in_a_short_string() {
712 let tag = consumer_tag(&"q".repeat(1000));
713 assert!(
714 tag.len() <= MAX_SHORT_STRING_LENGTH,
715 "len was {}",
716 tag.len()
717 );
718 assert!(short_string(&tag).is_ok());
719 }
720
721 #[test]
722 fn a_queue_name_with_room_for_its_hold_queues_is_accepted() {
723 assert!(check_deferrable_name("myapp.emails", topology::DEFAULT_DEFERRED_SUFFIX).is_ok());
724 // The longest name that still fits: 255 - len(".deferred.") - 10 digits.
725 let longest = "q".repeat(MAX_SHORT_STRING_LENGTH - ".deferred.".len() - 10);
726 assert_eq!(longest.len(), 235);
727 assert!(check_deferrable_name(&longest, topology::DEFAULT_DEFERRED_SUFFIX).is_ok());
728 }
729
730 #[test]
731 fn a_queue_name_that_leaves_no_room_for_hold_queues_is_refused_at_declare() {
732 // 250 bytes is a perfectly legal queue name (`short_string` takes it),
733 // but `{q}.deferred.2147483647` is 270 bytes, so every deferral on it
734 // would fail. Better to say so once, at declare time.
735 let name = "q".repeat(250);
736 assert!(
737 short_string(&name).is_ok(),
738 "the queue name itself is legal"
739 );
740
741 let error = check_deferrable_name(&name, topology::DEFAULT_DEFERRED_SUFFIX)
742 .expect_err("a name with no room for hold queues must be refused");
743 let text = error.to_string();
744 assert!(
745 text.contains("leaves no room for its hold queues"),
746 "{text}"
747 );
748 assert!(text.contains("270 bytes"), "{text}");
749 assert!(text.contains("255-byte"), "{text}");
750 }
751
752 #[test]
753 fn the_hold_queue_name_check_uses_the_configured_suffix() {
754 let name = "q".repeat(240);
755 // 240 + 10 (".deferred.") + 10 digits = 260: refused.
756 assert!(check_deferrable_name(&name, topology::DEFAULT_DEFERRED_SUFFIX).is_err());
757 // 240 + 2 ("-h") + 1 (".") + 10 digits = 253: accepted.
758 assert!(check_deferrable_name(&name, "-h").is_ok());
759 }
760
761 #[test]
762 fn declare_options_never_auto_delete() {
763 let durable = topology::declare_options(true);
764 assert!(durable.durable);
765 assert!(!durable.auto_delete);
766 assert!(!durable.exclusive);
767 assert!(!durable.passive);
768 assert!(!durable.nowait);
769
770 let transient = topology::declare_options(false);
771 assert!(!transient.durable);
772 assert!(!transient.auto_delete);
773 }
774}