Skip to main content

ruststream_lapin/queue/
classic.rs

1//! The classic queue descriptor.
2
3use std::future::ready;
4use std::num::NonZeroU16;
5use std::time::Duration;
6
7use lapin::types::{AMQPValue, FieldTable};
8#[cfg(feature = "asyncapi")]
9use ruststream::asyncapi::Bindings;
10use ruststream::runtime::IntoSource;
11use ruststream::{AddressedCopies, RedeliveryAddress, RedeliveryAddressed, SubscriptionSource};
12
13use super::spec::{
14    Binding, DEAD_LETTER_EXCHANGE, DEAD_LETTER_ROUTING_KEY, QueueKind, QueueSpec, queue_settings,
15};
16#[cfg(feature = "asyncapi")]
17use crate::bindings::{consumer_operation, queue_channel};
18use crate::broker::ConnectedLapinBroker;
19use crate::delay::Delay;
20use crate::error::AmqpError;
21use crate::exchange::RabbitExchange;
22use crate::subscriber::LapinSubscriber;
23
24/// Describes one classic-queue subscription: the queue, its expected settings, and its bindings.
25///
26/// The classic queue is the single-node implementation, and it counts nothing: a delivery says
27/// only that it has been seen before, never how often. So a registration's `max_attempts(n)` is
28/// the runtime's to apply, counted with the framework's own header on the copies it publishes,
29/// and `out_retry(policy)` names the publisher those copies leave through. A queue that should
30/// end a poison loop on the server instead is a [`RabbitQuorumQueue`](crate::RabbitQuorumQueue).
31///
32/// Descriptors describe the EXPECTED topology for routing; by default nothing is created on the
33/// broker (managing infrastructure is the user's job). Opt in to declaration per broker with
34/// [`declare_topology(true)`](crate::LapinBroker::declare_topology).
35///
36/// # Examples
37///
38/// ```
39/// use ruststream_lapin::prelude::*;
40/// use serde::Deserialize;
41///
42/// #[derive(Deserialize)]
43/// struct OrderPlaced {
44///     id: u64,
45/// }
46///
47/// #[subscriber(RabbitQueue::new("orders")
48///     .bind(RabbitExchange::topic("events"), "order.*")
49///     .dead_letter_exchange("dead-letters"))]
50/// async fn on_order(event: &OrderPlaced) -> HandlerOutcome {
51///     if event.id == 0 {
52///         // Rejected without requeue: the queue's dead-letter exchange takes it.
53///         return HandlerOutcome::drop();
54///     }
55///     HandlerOutcome::ack()
56/// }
57///
58/// fn app() -> RustStream {
59///     let broker = LapinBroker::new("amqp://localhost:5672").declare_topology(true);
60///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
61///         b.include(on_order);
62///     })
63/// }
64/// ```
65///
66/// A slice parameter asks for a batch, and the mount site says how big it may get:
67///
68/// ```
69/// use std::time::Duration;
70///
71/// use ruststream_lapin::prelude::*;
72/// use serde::Deserialize;
73///
74/// #[derive(Deserialize)]
75/// struct Settlement {
76///     id: u64,
77/// }
78///
79/// #[subscriber(RabbitQueue::new("settlements")
80///     .prefetch(nonzero!(64))
81///     .batch_wait(Duration::from_millis(200)))]
82/// async fn on_settlements(settlements: &[Settlement]) -> HandlerOutcome {
83///     tracing::info!(count = settlements.len(), "settling");
84///     HandlerOutcome::ack()
85/// }
86///
87/// fn app() -> RustStream {
88///     let broker = LapinBroker::new("amqp://localhost:5672").declare_topology(true);
89///     RustStream::new(AppInfo::new("settlements", "0.1.0")).with_broker(broker, |b| {
90///         b.include(on_settlements.batch(nonzero!(32)));
91///     })
92/// }
93/// ```
94///
95/// The runtime caps the retries of a classic queue and publishes each copy through the
96/// `out_retry` policy, here the confirms one:
97///
98/// ```
99/// use ruststream_lapin::prelude::*;
100/// use serde::Deserialize;
101///
102/// #[derive(Deserialize)]
103/// struct Refund {
104///     id: u64,
105/// }
106///
107/// #[subscriber(RabbitQueue::new("refunds"))]
108/// async fn on_refund(refund: &Refund) -> HandlerOutcome {
109///     tracing::warn!(refund.id, "the provider is down, trying again");
110///     HandlerOutcome::retry()
111/// }
112///
113/// fn app() -> RustStream {
114///     let broker = LapinBroker::new("amqp://localhost:5672").declare_topology(true);
115///     RustStream::new(AppInfo::new("refunds", "0.1.0")).with_broker(broker, |b| {
116///         b.include(on_refund)
117///             .max_attempts(nonzero!(5u32))
118///             .dead_letter("refunds.dead")
119///             .out_retry(TransactionalPublish::default());
120///     })
121/// }
122/// ```
123#[derive(Debug, Clone, PartialEq)]
124pub struct RabbitQueue {
125    spec: QueueSpec,
126}
127
128impl RabbitQueue {
129    /// Describes the classic queue `name` with the defaults: durable, shared, not auto-deleted.
130    #[must_use]
131    pub fn new(name: impl Into<String>) -> Self {
132        Self {
133            spec: QueueSpec::new(QueueKind::Classic, name.into()),
134        }
135    }
136
137    /// Whether the queue survives a broker restart. Defaults to `true`.
138    ///
139    /// `RabbitMQ` 4 denies transient non-exclusive queues, so a queue declared `durable(false)`
140    /// has to be [`exclusive`](Self::exclusive) too.
141    #[must_use]
142    pub fn durable(mut self, durable: bool) -> Self {
143        self.spec.durable = durable;
144        self
145    }
146
147    /// Whether the queue is exclusive to this connection. Defaults to `false`.
148    #[must_use]
149    pub fn exclusive(mut self, exclusive: bool) -> Self {
150        self.spec.exclusive = exclusive;
151        self
152    }
153
154    /// Whether the queue is deleted when its last consumer disconnects. Defaults to `false`.
155    #[must_use]
156    pub fn auto_delete(mut self, auto_delete: bool) -> Self {
157        self.spec.auto_delete = auto_delete;
158        self
159    }
160
161    pub(crate) const fn spec(&self) -> &QueueSpec {
162        &self.spec
163    }
164}
165
166queue_settings!(RabbitQueue);
167
168/// The descriptor is its own source, so the manual path's `subscriber(source, body)` takes a
169/// `RabbitQueue` where the attribute path writes it in the decorator.
170impl IntoSource for RabbitQueue {
171    type Source = Self;
172
173    fn into_source(self) -> Self {
174        self
175    }
176}
177
178impl SubscriptionSource<ConnectedLapinBroker> for RabbitQueue {
179    type Subscriber = LapinSubscriber;
180
181    /// The service publishes the copies of a retry, and the queue name is where they go: on the
182    /// default exchange a routing key addresses the queue that carries it.
183    ///
184    /// A classic queue keeps no counter and has no delivery limit, so there is nothing on the
185    /// server to end a retry loop; the runtime counts and the mount site names the publisher the
186    /// copies leave through with `out_retry(policy)`.
187    type Copies = AddressedCopies;
188
189    fn name(&self) -> &str {
190        &self.spec.name
191    }
192
193    async fn subscribe(
194        self,
195        connected: &ConnectedLapinBroker,
196    ) -> Result<Self::Subscriber, AmqpError> {
197        connected.subscribe(self).await
198    }
199
200    /// The queue this subscription consumes, as the `amqp` binding names its settings.
201    #[cfg(feature = "asyncapi")]
202    fn channel_bindings(&self) -> Bindings {
203        queue_channel(&self.spec)
204    }
205
206    /// The consumer's own half: this crate acknowledges by hand on every subscription.
207    #[cfg(feature = "asyncapi")]
208    fn operation_bindings(&self) -> Bindings {
209        consumer_operation()
210    }
211}
212
213/// The queue name: on the default exchange a routing key addresses the queue that carries it, so
214/// that is where the runtime's retry copy reaches this subscription again.
215///
216/// The answer holds for a retry publisher on the default exchange, which is what
217/// [`LapinPublish`](crate::LapinPublish) is unless configured otherwise. A publisher aimed at a
218/// topic or direct exchange reaches the queue only through a binding under this name, so bind it
219/// there or leave the retry publisher on the default exchange.
220impl RedeliveryAddressed<ConnectedLapinBroker> for RabbitQueue {
221    fn redelivery_address(
222        &self,
223        _connected: &ConnectedLapinBroker,
224    ) -> impl Future<Output = Result<RedeliveryAddress, AmqpError>> + Send {
225        // The name is on the descriptor, so nothing has to be asked of the broker.
226        ready(Ok(RedeliveryAddress::new(self.spec.name.clone())))
227    }
228}