ruststream_lapin/subscriber.rs
1//! The subscriber: a stream of AMQP deliveries from one queue consumer, batched on the client for
2//! the handlers that take a batch.
3
4use std::num::NonZeroUsize;
5use std::sync::Arc;
6use std::time::Duration;
7
8#[cfg(feature = "testing")]
9use futures::future::Either;
10use futures::{Stream, StreamExt};
11use lapin::{Channel, Consumer};
12use ruststream::{BatchSubscriber, BufferedSubscriber, Subscriber};
13
14use crate::broker::AmqpConnection;
15use crate::delay::DelayContext;
16use crate::error::AmqpError;
17#[cfg(feature = "testing")]
18use crate::in_process::BusDeliveries;
19use crate::message::LapinMessage;
20
21/// A consumer on one queue, yielding [`LapinMessage`] deliveries.
22///
23/// Created by subscribing a [`RabbitQueue`](crate::RabbitQueue) descriptor (or a bare queue name)
24/// through [`LapinBroker`](crate::LapinBroker). The subscriber owns a dedicated channel;
25/// dropping it closes that channel and the broker redelivers whatever was unacknowledged.
26///
27/// Back-pressure: the broker stops pushing once
28/// [`prefetch`](crate::LapinBroker::prefetch) unacknowledged deliveries are in flight, so
29/// consuming slower slows the producer side down instead of buffering without bound.
30///
31/// It is a [`BatchSubscriber`] as well as a [`Subscriber`]: AMQP has no wire batch, so a batch
32/// handler's size is honoured by collecting the deliveries here (see the
33/// [impl](#impl-BatchSubscriber-for-LapinSubscriber)).
34pub struct LapinSubscriber {
35 deliveries: BufferedSubscriber<Deliveries>,
36 queue: String,
37}
38
39impl LapinSubscriber {
40 pub(crate) fn new(
41 connection: Arc<AmqpConnection>,
42 channel: Channel,
43 consumer: Consumer,
44 queue: String,
45 batch_wait: Duration,
46 delay: Option<DelayContext>,
47 ) -> Self {
48 let deliveries = Deliveries::Amqp(AmqpDeliveries {
49 connection,
50 _channel: channel,
51 consumer,
52 delay,
53 #[cfg(feature = "testing")]
54 queue: queue.clone(),
55 });
56 Self {
57 deliveries: BufferedSubscriber::new(deliveries).max_wait(batch_wait),
58 queue,
59 }
60 }
61
62 /// A subscriber on the in-process transport, batching on the client with the descriptor's
63 /// own deadline as the live subscriber does.
64 #[cfg(feature = "testing")]
65 pub(crate) fn in_process(
66 deliveries: BusDeliveries,
67 queue: String,
68 batch_wait: Duration,
69 ) -> Self {
70 Self {
71 deliveries: BufferedSubscriber::new(Deliveries::InProcess(deliveries))
72 .max_wait(batch_wait),
73 queue,
74 }
75 }
76
77 /// The queue this subscriber consumes from.
78 #[must_use]
79 pub fn queue(&self) -> &str {
80 &self.queue
81 }
82}
83
84impl std::fmt::Debug for LapinSubscriber {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("LapinSubscriber")
87 .field("queue", &self.queue)
88 .finish_non_exhaustive()
89 }
90}
91
92impl Subscriber for LapinSubscriber {
93 type Message = LapinMessage;
94 type Error = AmqpError;
95
96 /// Streams deliveries as they arrive; the stream ends when the consumer is cancelled or the
97 /// connection closes.
98 ///
99 /// # Cancel safety
100 ///
101 /// Polling is cancel safe (no delivery is lost by dropping the stream between polls), and
102 /// the stream can be re-created by calling `stream` again: deliveries buffer in the
103 /// consumer, not in the returned stream.
104 fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
105 self.deliveries.stream()
106 }
107}
108
109/// Batches are assembled on the client: AMQP delivers one `basic.deliver` at a time, so there is
110/// no wire batch to ask the broker for, and a batch closes on the registration's size or on the
111/// descriptor's [`batch_wait`](crate::RabbitQueue::batch_wait), whichever comes first.
112///
113/// A batch can only hold what the broker has already pushed, so a subscription whose
114/// [`prefetch`](crate::RabbitQueue::prefetch) window is narrower than the registration's batch size
115/// yields batches capped by the window rather than by the size.
116impl BatchSubscriber for LapinSubscriber {
117 type Batch = Vec<LapinMessage>;
118
119 /// # Cancel safety
120 ///
121 /// As cancel safe as [`stream`](Subscriber::stream) between polls; dropping the returned
122 /// stream abandons the batch being assembled, and the broker redelivers those deliveries when
123 /// the channel closes.
124 fn batches(
125 &mut self,
126 size: NonZeroUsize,
127 ) -> impl Stream<Item = Result<Self::Batch, <Self as Subscriber>::Error>> + Send + '_ {
128 self.deliveries.batches(size)
129 }
130}
131
132/// One delivery at a time, from whichever transport the broker was connected to: everything above
133/// it batches on the client.
134///
135/// Without the `testing` feature the wire consumer is the only variant, so the type is the
136/// consumer and the `match` in [`stream`](Subscriber::stream) resolves at compile time.
137// The live consumer is the larger variant on purpose: it is the one a production build has, and
138// boxing it would put an allocation on the service's own path to shrink a test build.
139#[cfg_attr(feature = "testing", allow(clippy::large_enum_variant))]
140enum Deliveries {
141 Amqp(AmqpDeliveries),
142 #[cfg(feature = "testing")]
143 InProcess(BusDeliveries),
144}
145
146#[cfg(not(feature = "testing"))]
147const _: () = assert!(size_of::<Deliveries>() == size_of::<AmqpDeliveries>());
148
149impl Subscriber for Deliveries {
150 type Message = LapinMessage;
151 type Error = AmqpError;
152
153 fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
154 match self {
155 #[cfg(not(feature = "testing"))]
156 Self::Amqp(consumer) => consumer.stream(),
157 #[cfg(feature = "testing")]
158 Self::Amqp(consumer) => Either::Left(consumer.stream()),
159 #[cfg(feature = "testing")]
160 Self::InProcess(deliveries) => Either::Right(deliveries.stream()),
161 }
162 }
163}
164
165/// The wire consumer, one `basic.deliver` at a time.
166struct AmqpDeliveries {
167 // Threaded into every delivery, so a settlement after shutdown is refused instead of waiting
168 // on an answer lapin never gives.
169 connection: Arc<AmqpConnection>,
170 // Kept alive for the lifetime of the subscription: dropping the channel cancels the
171 // consumer server-side.
172 _channel: Channel,
173 consumer: Consumer,
174 // Present only when the descriptor opted into a native delay queue; threaded into every
175 // delivery so `nack_after` can re-publish to the waiting queue.
176 delay: Option<DelayContext>,
177 // The queue a delivery came off, which the test harness reads to learn where a server-picked
178 // route went.
179 #[cfg(feature = "testing")]
180 queue: String,
181}
182
183impl AmqpDeliveries {
184 fn stream(&mut self) -> impl Stream<Item = Result<LapinMessage, AmqpError>> + Send + '_ {
185 let delay = self.delay.clone();
186 let connection = &self.connection;
187 #[cfg(feature = "testing")]
188 let queue = self.queue.as_str();
189 futures::stream::unfold(
190 (&mut self.consumer, delay),
191 move |(consumer, delay)| async move {
192 let item = consumer.next().await?;
193 let mapped = match item {
194 Ok(delivery) => Ok({
195 #[cfg(feature = "testing")]
196 connection.routes().delivered(
197 delivery.exchange.as_str(),
198 delivery.routing_key.as_str(),
199 queue,
200 );
201 LapinMessage::from_delivery(
202 delivery,
203 Some(Arc::clone(connection)),
204 delay.clone(),
205 )
206 }),
207 Err(err) => Err(AmqpError::consume(err)),
208 };
209 Some((mapped, (consumer, delay)))
210 },
211 )
212 }
213}