queuey_rabbitmq/topology.rs
1//! Pure helpers describing the RabbitMQ topology used by this backend.
2//!
3//! For a logical queue `q` the backend maintains two long-lived broker queues
4//! plus one short-lived *hold* queue per distinct delay:
5//!
6//! | queue | role | arguments |
7//! |---|---|---|
8//! | `q` | main work queue | `x-message-ttl` when [`QueueConfig::message_ttl`] is set, `x-max-priority` when [`QueueConfig::max_priority`] is `Some` |
9//! | `q.dead` | dead-letter queue | none |
10//! | `q.deferred.{ttl_ms}` | hold queue for one delay | `x-message-ttl = ttl_ms`, `x-dead-letter-exchange = ""`, `x-dead-letter-routing-key = q`, `x-expires = 2 * ttl_ms` |
11//!
12//! Every wait, whether a retry backoff, a delayed enqueue or a deferral, goes
13//! through a hold queue. There is no shared wait queue with per-message
14//! expirations, and this is why:
15//!
16//! # Why hold queues instead of per-message `expiration`
17//!
18//! A classic queue only ever expires the message at its *head*. Messages behind
19//! it are not examined until it has gone, so in a shared wait queue a message
20//! with a five-minute expiration at the head holds back every one-second
21//! expiration behind it (head-of-line blocking). Exponential backoff produces
22//! exactly that mix, so retries were the worst-hit case.
23//!
24//! A hold queue never sets a per-message `expiration`. Instead the delay is
25//! baked into the *name* of the queue the message waits in (`q.deferred.30000`
26//! holds every 30-second wait for `q`), and the wait is the queue-wide
27//! `x-message-ttl`. Every message in one hold queue therefore has the same TTL,
28//! so they expire in exactly the order they were published and the head is
29//! always the message that is due next. A short wait can never be stuck behind a
30//! long one, because the two live in different queues. The price is one queue
31//! per distinct delay, which is why delays are rounded up to a granularity:
32//! [`RabbitMqOptions::retry_granularity`](crate::RabbitMqOptions::retry_granularity)
33//! for retries and delayed enqueues,
34//! [`RabbitMqOptions::deferred_granularity`](crate::RabbitMqOptions::deferred_granularity)
35//! for deferrals (both default to one second), so `29.2s` and `30s` share
36//! `q.deferred.30000`.
37//!
38//! Retries and deferrals share the hold queues: the arguments depend only on the
39//! delay, and what differs between the two, the message priority, travels on the
40//! message and only matters once it is back on `q`. A retry returns at priority
41//! `0` and joins the back of the queue; a deferral returns at the queue's top
42//! priority and overtakes the backlog.
43//!
44//! # Why a hold queue's arguments depend only on its name
45//!
46//! The TTL is in the name, and every other argument is derived from it or from
47//! the main queue, so two processes running different builds of an application
48//! compute the *same* arguments for `q.deferred.30000`. That matters because
49//! RabbitMQ refuses a declaration whose arguments differ from the existing
50//! queue's (`PRECONDITION_FAILED`, which closes the declaring channel): a
51//! tunable in `x-expires` would deadlock the two processes against each other
52//! for ever, one of them unable to schedule a single wait. Hence `x-expires = 2 * ttl_ms`
53//! and nothing else: an idle hold queue deletes itself one TTL after the last
54//! publish to it, and every declare resets that timer, which is why the
55//! queue is redeclared before every publish.
56//!
57//! `x-expires` must also be strictly greater than `x-message-ttl`, or the broker
58//! could delete a queue that still owes a message. `2 * ttl_ms` satisfies that
59//! for every TTL up to [`MAX_DEFERRAL_MS`], which is why a longer delay is
60//! refused rather than silently clamped.
61//!
62//! Everything in this module is pure: it never touches a connection, so it can
63//! be unit-tested without a broker.
64
65use std::time::Duration;
66
67use lapin::{
68 options::QueueDeclareOptions,
69 types::{AMQPValue, FieldTable, LongString},
70};
71use queuey_core::QueueConfig;
72
73/// Default suffix appended to a queue name to build its dead-letter queue.
74pub const DEFAULT_DEAD_SUFFIX: &str = ".dead";
75
76/// Default infix between a queue name and a hold queue's TTL.
77///
78/// A hold queue is named `{q}{suffix}.{ttl_ms}`, so with the default a 30-second
79/// wait (retry or deferral) of `myapp.emails` happens in
80/// `myapp.emails.deferred.30000`. The name says "deferred" because deferrals
81/// were the first user; retries and delayed enqueues share the very same queues.
82pub const DEFAULT_DEFERRED_SUFFIX: &str = ".deferred";
83
84/// Header carrying the dead-lettering reason on messages routed to `q.dead`.
85pub const HEADER_DEATH_REASON: &str = "x-death-reason";
86
87/// Header carrying the originating queue name on messages routed to `q.dead`.
88pub const HEADER_ORIGINAL_QUEUE: &str = "x-original-queue";
89
90/// Header carrying the number of attempts made before dead-lettering.
91pub const HEADER_ATTEMPTS: &str = "x-attempts";
92
93/// Header mirroring [`queuey_core::Envelope::attempt`] on every publish.
94pub const HEADER_ATTEMPT: &str = "x-attempt";
95
96/// Header mirroring [`queuey_core::Envelope::deferrals`] on every publish.
97///
98/// Purely informational, like [`HEADER_ATTEMPT`]: the body is the source of
99/// truth. In particular the `x-death` header RabbitMQ stamps on a message it
100/// expired out of a hold queue is ignored.
101pub const HEADER_DEFERRALS: &str = "x-deferrals";
102
103/// Queue argument naming the exchange used for dead-lettering.
104pub const ARG_DEAD_LETTER_EXCHANGE: &str = "x-dead-letter-exchange";
105
106/// Queue argument naming the routing key used for dead-lettering.
107pub const ARG_DEAD_LETTER_ROUTING_KEY: &str = "x-dead-letter-routing-key";
108
109/// Queue argument setting a queue-wide message time-to-live in milliseconds.
110pub const ARG_MESSAGE_TTL: &str = "x-message-ttl";
111
112/// Queue argument declaring how many priority levels a queue supports.
113///
114/// Set on the main queue `q` from [`QueueConfig::max_priority`]. Never set on
115/// `q.dead` or a hold queue: those are strictly FIFO holding pens and a priority
116/// queue costs the broker an index per level.
117pub const ARG_MAX_PRIORITY: &str = "x-max-priority";
118
119/// Queue argument making the broker delete a queue after it has been unused for
120/// that many milliseconds.
121///
122/// Only used for hold queues, so a delay that is never used again does not leave
123/// an empty queue behind for ever.
124pub const ARG_EXPIRES: &str = "x-expires";
125
126/// Largest time-to-live RabbitMQ accepts, in milliseconds (~49.7 days).
127///
128/// `x-message-ttl` and `x-expires` are parsed as unsigned 32-bit millisecond
129/// counts. A larger value is refused with `PRECONDITION_FAILED`, which closes
130/// the channel, so this crate clamps a queue TTL rather than letting a huge
131/// [`Duration`] take the channel down.
132pub const MAX_TTL_MS: u32 = u32::MAX;
133
134/// Longest delay this backend will hold, in milliseconds (~24.8 days).
135///
136/// Applies to every wait that goes through a hold queue: retry backoffs, delayed
137/// enqueues and deferrals alike.
138///
139/// A hold queue is declared with `x-expires = 2 * x-message-ttl` (see the module
140/// docs for why the factor is fixed rather than configurable), and `x-expires`
141/// has the same 32-bit millisecond domain as `x-message-ttl`. Half of
142/// [`MAX_TTL_MS`] is therefore the largest TTL for which the doubled expiry
143/// still fits and still stays strictly above the TTL, so the broker can never
144/// delete a hold queue that still owes a message.
145///
146/// A longer delay is **refused**, not clamped: clamping would release the job
147/// early, and "never early" is the one timing guarantee a hold makes. See
148/// [`deferred_ttl_ms`].
149pub const MAX_DEFERRAL_MS: u32 = MAX_TTL_MS / 2;
150
151/// Name of the dead-letter queue for `queue`.
152///
153/// ```
154/// use queuey_rabbitmq::topology::{dead_queue_name, DEFAULT_DEAD_SUFFIX};
155/// assert_eq!(dead_queue_name("myapp.emails", DEFAULT_DEAD_SUFFIX), "myapp.emails.dead");
156/// ```
157#[must_use]
158pub fn dead_queue_name(queue: &str, suffix: &str) -> String {
159 format!("{queue}{suffix}")
160}
161
162/// Name of the hold queue holding `ttl_ms`-long waits of `queue`.
163///
164/// The TTL is part of the name on purpose: one queue per distinct delay is what
165/// makes a hold queue drain strictly in order (see the module docs). Retries and
166/// deferrals with the same rounded delay share the queue.
167///
168/// ```
169/// use queuey_rabbitmq::topology::{deferred_queue_name, DEFAULT_DEFERRED_SUFFIX};
170/// assert_eq!(
171/// deferred_queue_name("myapp.emails", DEFAULT_DEFERRED_SUFFIX, 30_000),
172/// "myapp.emails.deferred.30000"
173/// );
174/// ```
175#[must_use]
176pub fn deferred_queue_name(queue: &str, suffix: &str, ttl_ms: u32) -> String {
177 format!("{queue}{suffix}.{ttl_ms}")
178}
179
180/// The hold queue TTL for `delay`: `delay` rounded **up** to a whole multiple of
181/// `granularity`, at least one whole step, or [`None`] when that lands past
182/// [`MAX_DEFERRAL_MS`].
183///
184/// Rounding up is what bounds the number of hold queues: with the default
185/// one-second granularity every delay between `29.001s` and `30s` maps to
186/// `30000`, so a burst of `Retry-After: 30` responses shares one queue, and a
187/// jittered exponential backoff creates at most one queue per whole second of
188/// its range. Rounding *up* rather than to nearest guarantees the job never
189/// comes back early, and [`None`] rather than a clamp at the top end is the same
190/// guarantee: a delay this backend cannot hold is refused, never shortened. The
191/// rounding itself can push a delay over the cap, so a `delay` just under
192/// [`MAX_DEFERRAL_MS`] may still be refused.
193///
194/// `granularity` is itself clamped to `[1ms, MAX_DEFERRAL_MS]`, so a zero or
195/// absurd granularity is corrected rather than causing a division by zero or an
196/// argument RabbitMQ would refuse. Library code must never panic on a value that
197/// merely came out of a config file.
198///
199/// ```
200/// use std::time::Duration;
201/// use queuey_rabbitmq::topology::deferred_ttl_ms;
202///
203/// let second = Duration::from_secs(1);
204/// assert_eq!(deferred_ttl_ms(Duration::from_secs(30), second), Some(30_000));
205/// assert_eq!(deferred_ttl_ms(Duration::from_millis(29_200), second), Some(30_000));
206/// assert_eq!(deferred_ttl_ms(Duration::from_millis(5), second), Some(1_000));
207/// // 30 days is past what a hold queue can express.
208/// assert_eq!(deferred_ttl_ms(Duration::from_secs(30 * 86_400), second), None);
209/// ```
210#[must_use]
211pub fn deferred_ttl_ms(delay: Duration, granularity: Duration) -> Option<u32> {
212 let max = u128::from(MAX_DEFERRAL_MS);
213 let step = millis_ceil(granularity).clamp(1, max);
214 // `step <= max`, so a delay of zero still waits one whole step.
215 let ttl = millis_ceil(delay)
216 .div_ceil(step)
217 .saturating_mul(step)
218 .max(step);
219 (ttl <= max).then(|| u32::try_from(ttl).unwrap_or(MAX_DEFERRAL_MS))
220}
221
222/// Declaration arguments for the main queue `q`.
223///
224/// * `x-message-ttl` when [`QueueConfig::message_ttl`] is set (in whole
225/// milliseconds, rounded up, at least 1).
226/// * `x-max-priority` when [`QueueConfig::max_priority`] is `Some`, so deferred
227/// jobs can come back ahead of the backlog.
228///
229/// Both are *declaration* arguments, which RabbitMQ refuses to change on an
230/// existing queue (`PRECONDITION_FAILED`, which closes the declaring channel).
231/// Adding `x-max-priority` to a queue that predates this feature therefore
232/// requires deleting the queue or setting `max_priority = 0`.
233#[must_use]
234pub fn queue_args(config: &QueueConfig) -> FieldTable {
235 let mut args = FieldTable::default();
236 if let Some(ttl) = config.message_ttl {
237 args.insert(
238 ARG_MESSAGE_TTL.into(),
239 AMQPValue::LongLongInt(ttl_millis(ttl)),
240 );
241 }
242 if let Some(levels) = config.max_priority {
243 // `x-max-priority` is validated by RabbitMQ as an integer of any width;
244 // an unsigned byte is the exact domain of `QueueConfig::max_priority`.
245 args.insert(ARG_MAX_PRIORITY.into(), AMQPValue::ShortShortUInt(levels));
246 }
247 args
248}
249
250/// Declaration arguments for the hold queue `q.deferred.{ttl_ms}`.
251///
252/// * `x-message-ttl = ttl_ms`: the delay itself, queue-wide, so the queue
253/// drains in publish order.
254/// * `x-dead-letter-exchange = ""` plus `x-dead-letter-routing-key = q`: an
255/// expired message goes straight back onto the main queue.
256/// * `x-expires = 2 * ttl_ms`: an idle hold queue deletes itself one TTL after
257/// the last publish to it, so a delay that never recurs leaves nothing
258/// behind. Every declare resets that timer, which is why the queue is
259/// redeclared before every publish.
260///
261/// The arguments are a pure function of the hold queue's *name*: `ttl_ms` is in
262/// the name, and the routing key and durability come from `config`, which is the
263/// main queue the name is derived from. Nothing here is tunable, on purpose:
264/// two processes with different settings would compute different arguments for
265/// the same queue name and lock each other out with `PRECONDITION_FAILED`
266/// for ever. See the module docs.
267///
268/// Never carries `x-max-priority`: a hold queue must stay FIFO, and the priority
269/// only matters once the message is back on `q`.
270#[must_use]
271pub fn deferred_queue_args(config: &QueueConfig, ttl_ms: u32) -> FieldTable {
272 let mut args = FieldTable::default();
273 args.insert(
274 ARG_MESSAGE_TTL.into(),
275 AMQPValue::LongLongInt(ttl_ms.into()),
276 );
277 args.insert(
278 ARG_DEAD_LETTER_EXCHANGE.into(),
279 AMQPValue::LongString(LongString::from("")),
280 );
281 args.insert(
282 ARG_DEAD_LETTER_ROUTING_KEY.into(),
283 AMQPValue::LongString(LongString::from(config.name.as_str())),
284 );
285 // `ttl_ms` comes from `deferred_ttl_ms`, so it is at most `MAX_DEFERRAL_MS`
286 // and the double fits. The clamp is for a hand-rolled call with a bigger
287 // value: an `x-expires` past `u32::MAX` is a `PRECONDITION_FAILED`, not a
288 // long timeout.
289 let expires = u64::from(ttl_ms)
290 .saturating_mul(2)
291 .min(u64::from(MAX_TTL_MS));
292 args.insert(
293 ARG_EXPIRES.into(),
294 AMQPValue::LongLongInt(i64::try_from(expires).unwrap_or(i64::from(MAX_TTL_MS))),
295 );
296 args
297}
298
299/// Declaration options for a queue that is never exclusive or auto-deleted.
300///
301/// Shared by the backend's own declarations and by the on-demand hold queue
302/// declaration on the declaration channel, so the two can never disagree about
303/// the flags and provoke a `PRECONDITION_FAILED`.
304pub(crate) fn declare_options(durable: bool) -> QueueDeclareOptions {
305 QueueDeclareOptions {
306 passive: false,
307 durable,
308 exclusive: false,
309 auto_delete: false,
310 nowait: false,
311 }
312}
313
314/// Declaration arguments for the dead-letter queue `q.dead`.
315///
316/// Deliberately empty: dead-lettered messages are terminal and must not expire
317/// or be routed onwards without an operator looking at them.
318#[must_use]
319pub fn dead_queue_args(_config: &QueueConfig) -> FieldTable {
320 FieldTable::default()
321}
322
323/// Whole milliseconds for `ttl`, rounded up, clamped to `[1, MAX_TTL_MS]`.
324///
325/// The upper bound is RabbitMQ's, not this crate's: a TTL past `u32::MAX` ms is
326/// answered with `PRECONDITION_FAILED` and closes the declaring channel.
327fn ttl_millis(ttl: Duration) -> i64 {
328 let ms = ttl
329 .as_nanos()
330 .div_ceil(1_000_000)
331 .clamp(1, u128::from(MAX_TTL_MS));
332 i64::try_from(ms).unwrap_or(i64::from(MAX_TTL_MS))
333}
334
335/// Whole milliseconds in `duration`, rounded up. Unclamped, so callers can
336/// decide what their own bounds are.
337fn millis_ceil(duration: Duration) -> u128 {
338 duration.as_nanos().div_ceil(1_000_000)
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 fn config(name: &str) -> QueueConfig {
346 QueueConfig::new(name)
347 }
348
349 #[test]
350 fn dead_name_without_prefix() {
351 assert_eq!(
352 dead_queue_name("emails", DEFAULT_DEAD_SUFFIX),
353 "emails.dead"
354 );
355 }
356
357 #[test]
358 fn dead_name_with_prefix() {
359 assert_eq!(
360 dead_queue_name("myapp.emails", DEFAULT_DEAD_SUFFIX),
361 "myapp.emails.dead"
362 );
363 }
364
365 #[test]
366 fn custom_suffixes_are_honoured() {
367 assert_eq!(dead_queue_name("q", "-dlq"), "q-dlq");
368 }
369
370 /// `QueueConfig::new` is a priority queue by default; most of the existing
371 /// argument assertions predate that and only care about the TTL.
372 fn plain(name: &str) -> QueueConfig {
373 QueueConfig::new(name).max_priority(0)
374 }
375
376 #[test]
377 fn queue_args_are_empty_without_ttl_or_priorities() {
378 let args = queue_args(&plain("emails"));
379 assert!(args.inner().is_empty(), "unexpected args: {args:?}");
380 assert!(!args.contains_key(ARG_MESSAGE_TTL));
381 assert!(!args.contains_key(ARG_MAX_PRIORITY));
382 }
383
384 #[test]
385 fn queue_args_carry_message_ttl() {
386 let cfg = plain("emails").message_ttl(Duration::from_secs(30));
387 let args = queue_args(&cfg);
388 assert_eq!(
389 args.inner().get(ARG_MESSAGE_TTL),
390 Some(&AMQPValue::LongLongInt(30_000))
391 );
392 assert_eq!(args.inner().len(), 1);
393 }
394
395 #[test]
396 fn queue_args_carry_max_priority_when_the_queue_has_priorities() {
397 // The `QueueConfig` default is 10 levels.
398 let args = queue_args(&config("emails"));
399 assert_eq!(
400 args.inner().get(ARG_MAX_PRIORITY),
401 Some(&AMQPValue::ShortShortUInt(10))
402 );
403 assert_eq!(args.inner().len(), 1);
404
405 let args = queue_args(&config("emails").max_priority(255));
406 assert_eq!(
407 args.inner().get(ARG_MAX_PRIORITY),
408 Some(&AMQPValue::ShortShortUInt(255))
409 );
410 }
411
412 #[test]
413 fn queue_args_omit_max_priority_when_priorities_are_off() {
414 let cfg = config("emails").max_priority(0);
415 assert_eq!(cfg.max_priority, None);
416 assert!(!queue_args(&cfg).contains_key(ARG_MAX_PRIORITY));
417 }
418
419 #[test]
420 fn queue_args_carry_ttl_and_priority_together() {
421 let cfg = config("emails")
422 .message_ttl(Duration::from_secs(5))
423 .max_priority(3);
424 let args = queue_args(&cfg);
425 assert_eq!(
426 args.inner().get(ARG_MESSAGE_TTL),
427 Some(&AMQPValue::LongLongInt(5_000))
428 );
429 assert_eq!(
430 args.inner().get(ARG_MAX_PRIORITY),
431 Some(&AMQPValue::ShortShortUInt(3))
432 );
433 assert_eq!(args.inner().len(), 2);
434 }
435
436 #[test]
437 fn only_the_main_queue_gets_priorities() {
438 let cfg = config("emails");
439 assert!(!dead_queue_args(&cfg).contains_key(ARG_MAX_PRIORITY));
440 assert!(!deferred_queue_args(&cfg, 1_000).contains_key(ARG_MAX_PRIORITY));
441 }
442
443 #[test]
444 fn sub_millisecond_ttl_rounds_up_to_one() {
445 let cfg = config("emails").message_ttl(Duration::from_nanos(1));
446 let args = queue_args(&cfg);
447 assert_eq!(
448 args.inner().get(ARG_MESSAGE_TTL),
449 Some(&AMQPValue::LongLongInt(1))
450 );
451 }
452
453 #[test]
454 fn zero_ttl_is_clamped_to_one_millisecond() {
455 let cfg = config("emails").message_ttl(Duration::ZERO);
456 let args = queue_args(&cfg);
457 assert_eq!(
458 args.inner().get(ARG_MESSAGE_TTL),
459 Some(&AMQPValue::LongLongInt(1))
460 );
461 }
462
463 #[test]
464 fn huge_ttl_is_clamped_to_what_rabbitmq_accepts() {
465 // Not `i64::MAX`: RabbitMQ rejects anything past `u32::MAX` ms with
466 // PRECONDITION_FAILED and closes the declaring channel.
467 let cfg = config("emails").message_ttl(Duration::MAX);
468 let args = queue_args(&cfg);
469 assert_eq!(
470 args.inner().get(ARG_MESSAGE_TTL),
471 Some(&AMQPValue::LongLongInt(i64::from(MAX_TTL_MS)))
472 );
473 }
474
475 #[test]
476 fn ttl_exactly_at_the_limit_is_kept_verbatim() {
477 let cfg = config("emails").message_ttl(Duration::from_millis(u64::from(MAX_TTL_MS)));
478 let args = queue_args(&cfg);
479 assert_eq!(
480 args.inner().get(ARG_MESSAGE_TTL),
481 Some(&AMQPValue::LongLongInt(i64::from(MAX_TTL_MS)))
482 );
483 }
484
485 #[test]
486 fn ttl_one_millisecond_past_the_limit_is_clamped() {
487 let cfg = config("emails").message_ttl(Duration::from_millis(u64::from(MAX_TTL_MS) + 1));
488 let args = queue_args(&cfg);
489 assert_eq!(
490 args.inner().get(ARG_MESSAGE_TTL),
491 Some(&AMQPValue::LongLongInt(i64::from(MAX_TTL_MS)))
492 );
493 }
494
495 #[test]
496 fn dead_args_are_empty() {
497 assert!(dead_queue_args(&config("emails")).inner().is_empty());
498 }
499
500 // -- deferral ----------------------------------------------------------
501
502 const SECOND: Duration = Duration::from_secs(1);
503
504 #[test]
505 fn deferred_name_puts_the_ttl_in_the_queue_name() {
506 assert_eq!(
507 deferred_queue_name("emails", DEFAULT_DEFERRED_SUFFIX, 30_000),
508 "emails.deferred.30000"
509 );
510 assert_eq!(
511 deferred_queue_name("myapp.emails", DEFAULT_DEFERRED_SUFFIX, 1_000),
512 "myapp.emails.deferred.1000"
513 );
514 }
515
516 #[test]
517 fn deferred_name_honours_a_custom_suffix() {
518 assert_eq!(deferred_queue_name("q", "-hold", 250), "q-hold.250");
519 }
520
521 #[test]
522 fn deferred_names_differ_per_ttl_which_is_the_whole_point() {
523 let short = deferred_queue_name("q", DEFAULT_DEFERRED_SUFFIX, 1_000);
524 let long = deferred_queue_name("q", DEFAULT_DEFERRED_SUFFIX, 60_000);
525 assert_ne!(short, long);
526 }
527
528 #[test]
529 fn an_exact_multiple_of_the_granularity_is_kept_verbatim() {
530 assert_eq!(deferred_ttl_ms(SECOND, SECOND), Some(1_000));
531 assert_eq!(
532 deferred_ttl_ms(Duration::from_secs(30), SECOND),
533 Some(30_000)
534 );
535 assert_eq!(
536 deferred_ttl_ms(Duration::from_millis(750), Duration::from_millis(250)),
537 Some(750)
538 );
539 }
540
541 #[test]
542 fn a_partial_step_rounds_up_so_a_job_never_returns_early() {
543 assert_eq!(
544 deferred_ttl_ms(Duration::from_millis(29_200), SECOND),
545 Some(30_000)
546 );
547 assert_eq!(
548 deferred_ttl_ms(Duration::from_millis(1_001), SECOND),
549 Some(2_000)
550 );
551 assert_eq!(
552 deferred_ttl_ms(Duration::from_millis(1_999), SECOND),
553 Some(2_000)
554 );
555 // Sub-millisecond remainders count as a partial millisecond, then a
556 // partial step.
557 assert_eq!(
558 deferred_ttl_ms(Duration::from_micros(1_000_001), SECOND),
559 Some(2_000)
560 );
561 }
562
563 #[test]
564 fn a_delay_below_the_granularity_becomes_one_step() {
565 assert_eq!(
566 deferred_ttl_ms(Duration::from_millis(5), SECOND),
567 Some(1_000)
568 );
569 assert_eq!(
570 deferred_ttl_ms(Duration::from_nanos(1), SECOND),
571 Some(1_000)
572 );
573 // Zero is not "publish it straight back": a deferral always waits.
574 assert_eq!(deferred_ttl_ms(Duration::ZERO, SECOND), Some(1_000));
575 }
576
577 #[test]
578 fn a_delay_past_the_cap_is_refused_rather_than_released_early() {
579 // Clamping here is what the old code did, and it broke the one timing
580 // guarantee a deferral makes.
581 assert_eq!(deferred_ttl_ms(Duration::MAX, SECOND), None);
582 assert_eq!(
583 deferred_ttl_ms(Duration::from_secs(30 * 86_400), SECOND),
584 None
585 );
586 assert_eq!(
587 deferred_ttl_ms(
588 Duration::from_millis(u64::from(MAX_DEFERRAL_MS) + 1),
589 SECOND
590 ),
591 None
592 );
593 }
594
595 #[test]
596 fn the_cap_itself_is_accepted_at_a_matching_granularity() {
597 assert_eq!(
598 deferred_ttl_ms(
599 Duration::from_millis(u64::from(MAX_DEFERRAL_MS)),
600 Duration::from_millis(1)
601 ),
602 Some(MAX_DEFERRAL_MS)
603 );
604 }
605
606 #[test]
607 fn rounding_up_can_itself_cross_the_cap_and_is_then_refused() {
608 // Just under the cap, but the next whole second is past it.
609 let just_under = Duration::from_millis(u64::from(MAX_DEFERRAL_MS) - 1);
610 assert_eq!(deferred_ttl_ms(just_under, SECOND), None);
611 }
612
613 #[test]
614 fn a_zero_granularity_is_clamped_to_one_millisecond_rather_than_panicking() {
615 assert_eq!(
616 deferred_ttl_ms(Duration::from_millis(7), Duration::ZERO),
617 Some(7)
618 );
619 assert_eq!(deferred_ttl_ms(Duration::ZERO, Duration::ZERO), Some(1));
620 // Sub-millisecond granularities land in the same clamp.
621 assert_eq!(
622 deferred_ttl_ms(Duration::from_millis(7), Duration::from_nanos(1)),
623 Some(7)
624 );
625 }
626
627 #[test]
628 fn an_absurd_granularity_is_clamped_to_the_cap_not_past_it() {
629 assert_eq!(
630 deferred_ttl_ms(SECOND, Duration::MAX),
631 Some(MAX_DEFERRAL_MS)
632 );
633 }
634
635 #[test]
636 fn deferred_args_hold_for_the_ttl_then_dead_letter_onto_the_main_queue() {
637 let args = deferred_queue_args(&config("myapp.emails"), 30_000);
638 assert_eq!(
639 args.inner().get(ARG_MESSAGE_TTL),
640 Some(&AMQPValue::LongLongInt(30_000))
641 );
642 assert_eq!(
643 args.inner().get(ARG_DEAD_LETTER_EXCHANGE),
644 Some(&AMQPValue::LongString(LongString::from("")))
645 );
646 assert_eq!(
647 args.inner().get(ARG_DEAD_LETTER_ROUTING_KEY),
648 Some(&AMQPValue::LongString(LongString::from("myapp.emails")))
649 );
650 assert_eq!(
651 args.inner().get(ARG_EXPIRES),
652 Some(&AMQPValue::LongLongInt(60_000))
653 );
654 assert_eq!(args.inner().len(), 4);
655 }
656
657 #[test]
658 fn deferred_args_depend_only_on_the_hold_queue_name() {
659 // Two callers that disagree about *everything* except the main queue's
660 // name and the TTL must still compute byte-identical arguments, or the
661 // broker locks one of them out of `q.deferred.1000` for ever.
662 let one = config("q").message_ttl(Duration::from_secs(99)).prefetch(1);
663 let two = config("q").max_priority(0).prefetch(200);
664 assert_eq!(
665 deferred_queue_args(&one, 1_000),
666 deferred_queue_args(&two, 1_000)
667 );
668 }
669
670 #[test]
671 fn expires_is_always_strictly_greater_than_the_ttl() {
672 for ttl in [1_u32, 1_000, 30_000, MAX_DEFERRAL_MS] {
673 let args = deferred_queue_args(&config("q"), ttl);
674 let AMQPValue::LongLongInt(expires) = args.inner().get(ARG_EXPIRES).expect("x-expires")
675 else {
676 panic!("x-expires is not a long long int");
677 };
678 assert!(
679 *expires > i64::from(ttl),
680 "x-expires {expires} must outlive the {ttl}ms TTL"
681 );
682 assert!(
683 *expires <= i64::from(MAX_TTL_MS),
684 "x-expires {expires} is past what RabbitMQ accepts"
685 );
686 }
687 }
688
689 #[test]
690 fn expires_is_clamped_when_doubling_the_ttl_would_overflow() {
691 // Unreachable through `deferred_ttl_ms`, which caps at `MAX_DEFERRAL_MS`;
692 // guarded anyway so a hand-rolled call cannot produce an argument
693 // RabbitMQ answers with PRECONDITION_FAILED.
694 let args = deferred_queue_args(&config("q"), MAX_TTL_MS);
695 assert_eq!(
696 args.inner().get(ARG_EXPIRES),
697 Some(&AMQPValue::LongLongInt(i64::from(MAX_TTL_MS)))
698 );
699 }
700
701 #[test]
702 fn deferred_args_never_carry_a_queue_ttl_of_their_own_from_the_config() {
703 // The hold queue's TTL is the delay, not the main queue's message TTL.
704 let cfg = config("q").message_ttl(Duration::from_secs(99));
705 let args = deferred_queue_args(&cfg, 2_000);
706 assert_eq!(
707 args.inner().get(ARG_MESSAGE_TTL),
708 Some(&AMQPValue::LongLongInt(2_000))
709 );
710 }
711
712 #[test]
713 fn declare_options_are_shared_and_never_auto_delete() {
714 let durable = declare_options(true);
715 assert!(durable.durable);
716 assert!(!durable.auto_delete);
717 assert!(!durable.exclusive);
718 assert!(!durable.passive);
719 assert!(!durable.nowait);
720 assert!(!declare_options(false).durable);
721 }
722}