Skip to main content

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