Skip to main content

laminar_connectors/rabbitmq/source/
mod.rs

1//! RabbitMQ source: one manual-acknowledgement consumer on an existing (or opt-in declared)
2//! classic or quorum queue.
3//!
4//! Deliveries are handed to the pipeline unacknowledged and acknowledged only after the
5//! checkpoint that contains them is durable. Delivery tags increase monotonically per channel and
6//! deliveries are handed off in tag order, so a cumulative `basic.ack(multiple)` at the highest
7//! handed-off tag covered by a committed checkpoint acknowledges exactly the committed messages.
8//! A lost channel ends this generation: the broker requeues every unacknowledged delivery and
9//! the engine restarts from its last durable checkpoint (at-least-once; replay may duplicate).
10
11use std::collections::VecDeque;
12use std::sync::Arc;
13
14use arrow_array::{ArrayRef, RecordBatch};
15use arrow_schema::SchemaRef;
16use async_trait::async_trait;
17use futures_util::StreamExt;
18use lapin::message::Delivery;
19use lapin::options::{
20    BasicAckOptions, BasicCancelOptions, BasicConsumeOptions, BasicQosOptions, BasicRejectOptions,
21};
22use lapin::types::ShortString;
23use lapin::{Channel, Connection, Consumer};
24use tokio::sync::{mpsc, Notify};
25
26use super::config::{DecodeErrorPolicy, RabbitMqSourceConfig};
27use super::connection::{close, connect, prepare_queue};
28use super::metrics::{saturating, SourceMetrics};
29use crate::checkpoint::SourceCheckpoint;
30use crate::config::ConnectorConfig;
31use crate::connector::{
32    ConnectorTaskAdmission, ConnectorTaskOwner, ConnectorTaskTracker, SourceBatch, SourceConnector,
33    SourceConsistency, SourceContract, SourceInputMode, SourcePosition, SourceStart,
34    SourceTopology,
35};
36use crate::error::{ConnectorError, SerdeError};
37use crate::serde::{self, Format, RecordDeserializer};
38
39const GENERATION: &str = "generation";
40const DELIVERY_TAG: &str = "delivery_tag";
41const CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
42
43/// RabbitMQ AMQP 0-9-1 queue source.
44pub struct RabbitMqSource {
45    schema: SchemaRef,
46    registry: Option<prometheus::Registry>,
47    config: Option<RabbitMqSourceConfig>,
48    running: Option<Running>,
49    failure: Option<String>,
50    data_ready: Arc<Notify>,
51    task_owner: ConnectorTaskOwner,
52    task_tracker: ConnectorTaskTracker,
53}
54
55/// Resources of one channel generation; dropping it closes the channel and connection, which
56/// makes the broker requeue every unacknowledged delivery.
57struct Running {
58    connection: Connection,
59    channel: Channel,
60    consumer_tag: ShortString,
61    deliveries: mpsc::Receiver<Result<Delivery, String>>,
62    reader: tokio::task::JoinHandle<()>,
63    carry: Option<Delivery>,
64    decoder: Arc<Decoder>,
65    ledger: AckLedger,
66    metrics: SourceMetrics,
67    tasks: ConnectorTaskAdmission,
68}
69
70impl Drop for Running {
71    fn drop(&mut self) {
72        // The reader owns the consumer, which keeps the channel open until it is dropped.
73        self.reader.abort();
74    }
75}
76
77/// Positions of one channel generation, in delivery tags.
78#[derive(Debug)]
79struct AckLedger {
80    generation: u64,
81    handed_off: u64,
82    acked: u64,
83    /// `(last tag, deliveries)` per handed-off batch not yet acknowledged.
84    batches: VecDeque<(u64, usize)>,
85}
86
87impl AckLedger {
88    fn new(generation: u64) -> Self {
89        Self {
90            generation,
91            handed_off: 0,
92            acked: 0,
93            batches: VecDeque::new(),
94        }
95    }
96
97    fn hand_off(&mut self, last_tag: u64, deliveries: usize) {
98        debug_assert!(
99            last_tag > self.handed_off,
100            "delivery tags increase per channel"
101        );
102        self.handed_off = last_tag;
103        self.batches.push_back((last_tag, deliveries));
104    }
105
106    fn unacked(&self) -> usize {
107        self.batches.iter().map(|(_, count)| count).sum()
108    }
109
110    /// The tag to acknowledge cumulatively for a committed cursor, if any.
111    ///
112    /// A cursor from another generation names a channel that no longer exists; its deliveries
113    /// were requeued when that channel closed, so there is nothing to acknowledge.
114    fn commit_target(&self, cursor: &SourceCheckpoint) -> Result<Option<u64>, ConnectorError> {
115        let (Some(generation), Some(tag)) = (
116            cursor.get_offset(GENERATION),
117            cursor.get_offset(DELIVERY_TAG),
118        ) else {
119            return Ok(None);
120        };
121        let generation = u64::from_str_radix(generation, 16).map_err(|_| corrupt_cursor())?;
122        let tag: u64 = tag.parse().map_err(|_| corrupt_cursor())?;
123        if generation != self.generation || tag <= self.acked {
124            return Ok(None);
125        }
126        if tag > self.handed_off {
127            return Err(ConnectorError::InvalidState {
128                expected: format!("a committed delivery tag at most {}", self.handed_off),
129                actual: format!("committed delivery tag {tag}"),
130            });
131        }
132        Ok(Some(tag))
133    }
134
135    /// Record a cumulative acknowledgement, returning how many deliveries it settled.
136    fn acknowledge(&mut self, tag: u64) -> usize {
137        self.acked = tag;
138        let mut settled = 0;
139        while self.batches.front().is_some_and(|(last, _)| *last <= tag) {
140            settled += self.batches.pop_front().map_or(0, |(_, count)| count);
141        }
142        settled
143    }
144
145    fn cursor(&self) -> SourceCheckpoint {
146        let mut cursor = SourceCheckpoint::new();
147        cursor.set_offset(GENERATION, format!("{:016x}", self.generation));
148        cursor.set_offset(DELIVERY_TAG, self.handed_off.to_string());
149        cursor
150    }
151}
152
153fn corrupt_cursor() -> ConnectorError {
154    ConnectorError::InvalidState {
155        expected: "a RabbitMQ source cursor".into(),
156        actual: "an unparseable generation or delivery tag".into(),
157    }
158}
159
160/// Payload codec plus the metadata columns appended after it.
161struct Decoder {
162    deserializer: Box<dyn RecordDeserializer>,
163    payload: SchemaRef,
164    output: SchemaRef,
165    config: RabbitMqSourceConfig,
166}
167
168/// One decoded poll: the output batch and the tags, ascending, of messages the codec rejected.
169struct Decoded {
170    records: Option<RecordBatch>,
171    rejected: Vec<u64>,
172}
173
174impl Decoder {
175    fn decode(&self, deliveries: &[Delivery]) -> Result<Decoded, ConnectorError> {
176        let payloads: Vec<&[u8]> = deliveries
177            .iter()
178            .map(|delivery| delivery.data.as_slice())
179            .collect();
180        match self
181            .deserializer
182            .deserialize_batch(&payloads, &self.payload)
183        {
184            Ok(batch) => {
185                let all: Vec<&Delivery> = deliveries.iter().collect();
186                let counts = if self.config.metadata.is_empty() || batch.num_rows() == all.len() {
187                    None
188                } else {
189                    Some(self.row_counts(&payloads)?)
190                };
191                let records = self.with_metadata(batch, &all, counts.as_deref())?;
192                Ok(Decoded {
193                    records: Some(records),
194                    rejected: Vec::new(),
195                })
196            }
197            Err(error) if self.config.on_decode_error == DecodeErrorPolicy::Fail => {
198                Err(ConnectorError::Serde(error))
199            }
200            Err(_) => self.isolate(deliveries),
201        }
202    }
203
204    /// Decode each message alone so undecodable ones can be rejected and the rest handed off.
205    fn isolate(&self, deliveries: &[Delivery]) -> Result<Decoded, ConnectorError> {
206        let mut good = Vec::with_capacity(deliveries.len());
207        let mut batches = Vec::with_capacity(deliveries.len());
208        let mut counts = Vec::with_capacity(deliveries.len());
209        let mut rejected = Vec::new();
210        for delivery in deliveries {
211            match self
212                .deserializer
213                .deserialize_batch(&[delivery.data.as_slice()], &self.payload)
214            {
215                Ok(batch) => {
216                    counts.push(batch.num_rows());
217                    batches.push(batch);
218                    good.push(delivery);
219                }
220                Err(_) => rejected.push(delivery.delivery_tag),
221            }
222        }
223        if good.is_empty() {
224            return Ok(Decoded {
225                records: None,
226                rejected,
227            });
228        }
229        let payload =
230            arrow_select::concat::concat_batches(&self.payload, &batches).map_err(|error| {
231                ConnectorError::Serde(SerdeError::MalformedInput(error.to_string()))
232            })?;
233        let records = self.with_metadata(payload, &good, Some(&counts))?;
234        Ok(Decoded {
235            records: Some(records),
236            rejected,
237        })
238    }
239
240    fn row_counts(&self, payloads: &[&[u8]]) -> Result<Vec<usize>, ConnectorError> {
241        payloads
242            .iter()
243            .map(|payload| {
244                self.deserializer
245                    .deserialize_batch(&[payload], &self.payload)
246                    .map(|batch| batch.num_rows())
247                    .map_err(ConnectorError::Serde)
248            })
249            .collect()
250    }
251
252    /// Append metadata, repeating a message's values once per row its payload decoded to.
253    fn with_metadata(
254        &self,
255        payload: RecordBatch,
256        deliveries: &[&Delivery],
257        counts: Option<&[usize]>,
258    ) -> Result<RecordBatch, ConnectorError> {
259        if self.config.metadata.is_empty() {
260            return Ok(payload);
261        }
262        let per_row: Vec<&Delivery> = match counts {
263            None => deliveries.to_vec(),
264            Some(counts) => deliveries
265                .iter()
266                .zip(counts)
267                .flat_map(|(delivery, count)| std::iter::repeat_n(*delivery, *count))
268                .collect(),
269        };
270        if per_row.len() != payload.num_rows() {
271            return Err(ConnectorError::Serde(SerdeError::RecordCountMismatch {
272                expected: per_row.len(),
273                got: payload.num_rows(),
274            }));
275        }
276        let mut columns: Vec<ArrayRef> = payload.columns().to_vec();
277        columns.extend(self.config.metadata.arrays(&per_row)?);
278        RecordBatch::try_new(Arc::clone(&self.output), columns)
279            .map_err(|error| ConnectorError::Serde(SerdeError::MalformedInput(error.to_string())))
280    }
281}
282
283impl RabbitMqSource {
284    /// Create an unstarted source; metrics register on `registry` at start.
285    #[must_use]
286    pub fn new(schema: SchemaRef, registry: Option<&prometheus::Registry>) -> Self {
287        let (task_owner, task_tracker) = ConnectorTaskOwner::new();
288        Self {
289            schema,
290            registry: registry.cloned(),
291            config: None,
292            running: None,
293            failure: None,
294            data_ready: Arc::new(Notify::new()),
295            task_owner,
296            task_tracker,
297        }
298    }
299
300    /// End this generation; every later poll repeats the cause as a terminal error.
301    fn fail(&mut self, cause: String) -> ConnectorError {
302        tracing::error!(cause = %cause, "RabbitMQ source generation ended");
303        self.running = None;
304        self.failure.get_or_insert(cause.clone());
305        terminal(cause)
306    }
307
308    /// The payload codec and the schemas of the committed contract.
309    #[cfg_attr(not(feature = "fpml"), allow(clippy::unused_async))]
310    async fn decoder(
311        &self,
312        config: &ConnectorConfig,
313        parsed: &RabbitMqSourceConfig,
314    ) -> Result<Decoder, ConnectorError> {
315        #[cfg(feature = "fpml")]
316        if parsed.format == Format::Fpml {
317            use crate::schema::traits::FormatDecoder;
318            let guard = self.task_owner.track().ok_or_else(retired)?;
319            let decoder = crate::fpml::message::start_decoder(
320                config,
321                config.schema_binding(),
322                "RabbitMQ",
323                self.registry.as_ref(),
324                guard,
325            )
326            .await?;
327            let payload = decoder.output_schema();
328            parsed.metadata.validate_payload(&payload)?;
329            return Ok(Decoder {
330                deserializer: Box::new(crate::fpml::message::MessageDecoder(Arc::new(decoder))),
331                output: parsed.metadata.output_schema(&payload),
332                payload,
333                config: parsed.clone(),
334            });
335        }
336        let (payload, output) = self.resolve_output(config, parsed)?;
337        Ok(Decoder {
338            deserializer: serde::create_deserializer(parsed.format)?,
339            payload,
340            output,
341            config: parsed.clone(),
342        })
343    }
344
345    fn resolve_output(
346        &self,
347        config: &ConnectorConfig,
348        parsed: &RabbitMqSourceConfig,
349    ) -> Result<(SchemaRef, SchemaRef), ConnectorError> {
350        let declared = config
351            .arrow_schema()
352            .or_else(|| (!self.schema.fields().is_empty()).then(|| Arc::clone(&self.schema)));
353        let payload = declared
354            .map(|output| parsed.metadata.payload_schema(&output))
355            .transpose()?;
356        let binding = serde::schema_contract::reader_binding(config, parsed.format, payload)?;
357        let payload = Arc::new(binding.logical);
358        parsed.metadata.validate_payload(&payload)?;
359        let output = parsed.metadata.output_schema(&payload);
360        if config
361            .schema_binding()
362            .is_some_and(|expected| expected.logical != *output)
363        {
364            return Err(ConnectorError::SchemaMismatch(
365                "RabbitMQ reader differs from its committed contract".into(),
366            ));
367        }
368        Ok((payload, output))
369    }
370
371    async fn open(
372        &self,
373        parsed: &RabbitMqSourceConfig,
374        decoder: Decoder,
375        metrics: SourceMetrics,
376    ) -> Result<Running, ConnectorError> {
377        let connection = connect(&parsed.connection).await?;
378        let channel = connection.create_channel().await.map_err(|error| {
379            ConnectorError::ConnectionFailed(format!("RabbitMQ channel: {error}"))
380        })?;
381        prepare_queue(
382            &channel,
383            &parsed.queue,
384            parsed.declare.as_ref(),
385            parsed.binding.as_ref(),
386        )
387        .await?;
388        channel
389            .basic_qos(parsed.prefetch, BasicQosOptions { global: false })
390            .await
391            .map_err(|error| ConnectorError::ConnectionFailed(format!("basic.qos: {error}")))?;
392        let consumer = channel
393            .basic_consume(
394                parsed.queue.as_str().into(),
395                parsed.consumer_tag.clone().unwrap_or_default().into(),
396                BasicConsumeOptions {
397                    no_ack: false,
398                    exclusive: parsed.exclusive,
399                    ..BasicConsumeOptions::default()
400                },
401                parsed.consumer_arguments.clone(),
402            )
403            .await
404            .map_err(|error| {
405                ConnectorError::ConnectionFailed(format!(
406                    "basic.consume on '{}': {error}",
407                    parsed.queue
408                ))
409            })?;
410        let consumer_tag = consumer.tag();
411        let (sender, deliveries) = mpsc::channel(usize::from(parsed.prefetch));
412        let guard = self.task_owner.track().ok_or_else(retired)?;
413        let data_ready = Arc::clone(&self.data_ready);
414        let reader = tokio::spawn(async move {
415            let _guard = guard;
416            read_deliveries(consumer, sender, data_ready).await;
417        });
418        Ok(Running {
419            connection,
420            channel,
421            consumer_tag,
422            deliveries,
423            reader,
424            carry: None,
425            decoder: Arc::new(decoder),
426            ledger: AckLedger::new(rand::random()),
427            metrics,
428            tasks: self.task_owner.admission(),
429        })
430    }
431}
432
433/// Forward deliveries in tag order. Capacity equals the prefetch window, which the broker never
434/// exceeds while every delivery stays unacknowledged, so the client buffer and this channel
435/// together hold at most `prefetch.count` messages.
436async fn read_deliveries(
437    mut consumer: Consumer,
438    sender: mpsc::Sender<Result<Delivery, String>>,
439    data_ready: Arc<Notify>,
440) {
441    while let Some(item) = consumer.next().await {
442        let terminal = item.is_err();
443        if sender
444            .send(item.map_err(|error| error.to_string()))
445            .await
446            .is_err()
447        {
448            return;
449        }
450        data_ready.notify_one();
451        if terminal {
452            return;
453        }
454    }
455    let _ = sender
456        .send(Err(
457            "the broker cancelled the consumer (queue deleted or consumer timeout)".into(),
458        ))
459        .await;
460    data_ready.notify_one();
461}
462
463fn retired() -> ConnectorError {
464    ConnectorError::Internal("RabbitMQ source task generation is retired".into())
465}
466
467fn terminal(cause: String) -> ConnectorError {
468    ConnectorError::InvalidState {
469        expected: "a live RabbitMQ channel".into(),
470        actual: cause,
471    }
472}
473
474enum Collected {
475    Deliveries(Vec<Delivery>),
476    Lost(String),
477}
478
479impl Running {
480    /// Cancel the consumer before the channel closes. Dropping a still-active consumer queues a
481    /// background `basic.cancel` that fails once the channel is closing, and lapin reports that
482    /// failure from `channel.close`.
483    async fn cancel_consumer(&self) -> Result<(), ConnectorError> {
484        if !self.channel.status().connected() {
485            return Ok(());
486        }
487        let cancel = self
488            .channel
489            .basic_cancel(self.consumer_tag.clone(), BasicCancelOptions::default());
490        match tokio::time::timeout(CLOSE_TIMEOUT, cancel).await {
491            Ok(Ok(())) => Ok(()),
492            Ok(Err(error)) => Err(ConnectorError::ConnectionFailed(format!(
493                "RabbitMQ basic.cancel failed: {error}"
494            ))),
495            Err(_) => Err(ConnectorError::Timeout(
496                u64::try_from(CLOSE_TIMEOUT.as_millis()).unwrap_or(u64::MAX),
497            )),
498        }
499    }
500
501    /// Take ready deliveries within the message-count and payload-byte bounds.
502    fn collect(&mut self, max_records: usize) -> Collected {
503        let max_batch_bytes = self.decoder.config.max_batch_bytes;
504        let mut deliveries = Vec::new();
505        let mut bytes = 0_usize;
506        while deliveries.len() < max_records.max(1) {
507            let delivery = match self.carry.take() {
508                Some(delivery) => delivery,
509                None => match self.deliveries.try_recv() {
510                    Ok(Ok(delivery)) => delivery,
511                    Ok(Err(cause)) => return Collected::Lost(cause),
512                    Err(mpsc::error::TryRecvError::Empty) => break,
513                    Err(mpsc::error::TryRecvError::Disconnected) => {
514                        return Collected::Lost("the delivery reader stopped".into())
515                    }
516                },
517            };
518            if !deliveries.is_empty() && bytes.saturating_add(delivery.data.len()) > max_batch_bytes
519            {
520                self.carry = Some(delivery);
521                break;
522            }
523            bytes = bytes.saturating_add(delivery.data.len());
524            deliveries.push(delivery);
525        }
526        Collected::Deliveries(deliveries)
527    }
528
529    /// `FpML` validation blocks, so it runs on a tracked blocking thread; other codecs decode
530    /// inline.
531    async fn decode(&self, deliveries: Vec<Delivery>) -> Result<(Vec<Delivery>, Decoded), String> {
532        if self.decoder.config.format != Format::Fpml {
533            let decoded = self
534                .decoder
535                .decode(&deliveries)
536                .map_err(|error| format!("decode failed: {error}"))?;
537            return Ok((deliveries, decoded));
538        }
539        let guard = self.tasks.track().ok_or_else(|| retired().to_string())?;
540        let codec = Arc::clone(&self.decoder);
541        let (deliveries, result) = tokio::task::spawn_blocking(move || {
542            let _guard = guard;
543            let result = codec.decode(&deliveries);
544            (deliveries, result)
545        })
546        .await
547        .map_err(|error| format!("decode task failed: {error}"))?;
548        let decoded = result.map_err(|error| format!("decode failed: {error}"))?;
549        Ok((deliveries, decoded))
550    }
551
552    /// Decode and hand off ready deliveries. Any error ends the generation.
553    async fn poll(&mut self, max_records: usize) -> Result<Option<SourceBatch>, String> {
554        let deliveries = match self.collect(max_records) {
555            Collected::Deliveries(deliveries) if deliveries.is_empty() => return Ok(None),
556            Collected::Deliveries(deliveries) => deliveries,
557            Collected::Lost(cause) => return Err(cause),
558        };
559        if deliveries.iter().any(is_stream_delivery) {
560            return Err(
561                "the queue is a stream queue; stream queues do not redeliver \
562                        unacknowledged messages and are not supported by this source"
563                    .into(),
564            );
565        }
566        let config = &self.decoder.config;
567        let (accepted, oversized): (Vec<Delivery>, Vec<Delivery>) = deliveries
568            .into_iter()
569            .partition(|delivery| delivery.data.len() <= config.max_message_bytes);
570        if let (Some(first), DecodeErrorPolicy::Fail) = (oversized.first(), config.on_decode_error)
571        {
572            return Err(format!(
573                "a {}-byte message exceeds max.message.bytes = {}",
574                first.data.len(),
575                config.max_message_bytes
576            ));
577        }
578        let (accepted, decoded) = self.decode(accepted).await?;
579        let mut rejected: Vec<u64> = oversized
580            .iter()
581            .map(|delivery| delivery.delivery_tag)
582            .collect();
583        rejected.extend(&decoded.rejected);
584        for tag in &rejected {
585            self.channel
586                .basic_reject(*tag, BasicRejectOptions { requeue: false })
587                .await
588                .map_err(|error| format!("basic.reject failed: {error}"))?;
589        }
590        self.metrics.rejected.inc_by(rejected.len() as u64);
591        let handed: Vec<&Delivery> = accepted
592            .iter()
593            .filter(|delivery| {
594                decoded
595                    .rejected
596                    .binary_search(&delivery.delivery_tag)
597                    .is_err()
598            })
599            .collect();
600        let Some(last) = handed.last() else {
601            return Ok(None);
602        };
603        self.ledger.hand_off(last.delivery_tag, handed.len());
604        self.metrics.messages.inc_by(handed.len() as u64);
605        self.metrics.bytes.inc_by(
606            handed
607                .iter()
608                .map(|delivery| delivery.data.len() as u64)
609                .sum(),
610        );
611        self.metrics.redelivered.inc_by(
612            handed
613                .iter()
614                .filter(|delivery| delivery.redelivered)
615                .count() as u64,
616        );
617        self.metrics.unacked.set(saturating(self.ledger.unacked()));
618        let records = decoded.records.filter(|records| records.num_rows() > 0);
619        if let Some(records) = &records {
620            self.metrics.rows.inc_by(records.num_rows() as u64);
621        }
622        Ok(records.map(SourceBatch::new))
623    }
624
625    /// Cumulatively acknowledge the deliveries a committed cursor covers.
626    async fn acknowledge(&mut self, cursor: &SourceCheckpoint) -> Result<(), String> {
627        let Some(tag) = self
628            .ledger
629            .commit_target(cursor)
630            .map_err(|error| error.to_string())?
631        else {
632            return Ok(());
633        };
634        self.channel
635            .basic_ack(tag, BasicAckOptions { multiple: true })
636            .await
637            .map_err(|error| format!("basic.ack: {error}"))?;
638        let settled = self.ledger.acknowledge(tag);
639        self.metrics.acked.inc_by(settled as u64);
640        self.metrics.unacked.set(saturating(self.ledger.unacked()));
641        Ok(())
642    }
643}
644
645/// Stream queues attach `x-stream-offset` to every delivery and do not redeliver unacknowledged
646/// messages, so this source's recovery contract does not hold for them.
647fn is_stream_delivery(delivery: &Delivery) -> bool {
648    delivery
649        .properties
650        .headers()
651        .as_ref()
652        .is_some_and(|headers| {
653            headers
654                .inner()
655                .contains_key(&ShortString::from("x-stream-offset"))
656        })
657}
658
659#[async_trait]
660impl SourceConnector for RabbitMqSource {
661    fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
662        Some(self.task_tracker.clone())
663    }
664
665    async fn resolve_schema(
666        &mut self,
667        config: &ConnectorConfig,
668        explicit: Option<SchemaRef>,
669    ) -> Result<crate::schema::resolution::SchemaBinding, ConnectorError> {
670        use crate::schema::resolution::{logical_binding, SchemaDirection};
671        let parsed = RabbitMqSourceConfig::from_config(config)?;
672        #[cfg(feature = "fpml")]
673        if parsed.format == Format::Fpml {
674            return crate::fpml::message::source_binding(config, explicit, "RabbitMQ", |payload| {
675                parsed.metadata.validate_payload(payload)?;
676                Ok(parsed.metadata.output_schema(payload))
677            })
678            .await;
679        }
680        let reader = serde::schema_contract::reader_binding(config, parsed.format, explicit)?;
681        let payload = Arc::new(reader.logical);
682        parsed.metadata.validate_payload(&payload)?;
683        logical_binding(
684            config,
685            SchemaDirection::Source,
686            reader.origin,
687            &parsed.metadata.output_schema(&payload),
688        )
689    }
690
691    fn contract(&self, config: &ConnectorConfig) -> Result<SourceContract, ConnectorError> {
692        RabbitMqSourceConfig::from_config(config)?;
693        // The queue releases a message only when the checkpoint containing it is durable.
694        // Exactly-once is not certified: broker acknowledgement and sink commit are separate.
695        Ok(SourceContract::new(
696            SourceConsistency::CommitCoupled,
697            SourceTopology::Singleton,
698            SourceInputMode::AppendOnly,
699        ))
700    }
701
702    fn recovery_identity_options(
703        &self,
704        config: &ConnectorConfig,
705    ) -> Result<Option<std::collections::BTreeMap<String, String>>, ConnectorError> {
706        Ok(Some(RabbitMqSourceConfig::from_config(config)?.identity()))
707    }
708
709    async fn start(&mut self, request: SourceStart) -> Result<(), ConnectorError> {
710        let (config, position, _) = request.into_parts();
711        if matches!(position, SourcePosition::Initialized { .. }) {
712            return Err(ConnectorError::ConfigurationError(
713                "RabbitMQ has no sealed topology startup contract".into(),
714            ));
715        }
716        // A resume needs no positioning: every delivery the committed cut did not cover is still
717        // unacknowledged, and the broker redelivered it when the previous channel closed.
718        let parsed = RabbitMqSourceConfig::from_config(&config)?;
719        let name = config.get("laminar.source.name").unwrap_or("rabbitmq");
720        let metrics = SourceMetrics::new(name, self.registry.as_ref())?;
721        let decoder = self.decoder(&config, &parsed).await?;
722        let output = Arc::clone(&decoder.output);
723        let running = self.open(&parsed, decoder, metrics).await?;
724        tracing::info!(
725            queue = %parsed.queue,
726            prefetch = parsed.prefetch,
727            endpoints = ?parsed.connection,
728            "RabbitMQ source consuming"
729        );
730        self.schema = output;
731        self.config = Some(parsed);
732        self.failure = None;
733        self.running = Some(running);
734        Ok(())
735    }
736
737    async fn poll_batch(
738        &mut self,
739        max_records: usize,
740    ) -> Result<Option<SourceBatch>, ConnectorError> {
741        if let Some(cause) = &self.failure {
742            return Err(terminal(cause.clone()));
743        }
744        let Some(running) = self.running.as_mut() else {
745            return Ok(None);
746        };
747        match running.poll(max_records).await {
748            Ok(batch) => Ok(batch),
749            Err(cause) => Err(self.fail(cause)),
750        }
751    }
752
753    fn schema(&self) -> SchemaRef {
754        Arc::clone(&self.schema)
755    }
756
757    fn checkpoint(&self) -> SourceCheckpoint {
758        self.running
759            .as_ref()
760            .map_or_else(SourceCheckpoint::new, |running| running.ledger.cursor())
761    }
762
763    async fn notify_epoch_committed(
764        &mut self,
765        epoch: u64,
766        checkpoint: &SourceCheckpoint,
767    ) -> Result<(), ConnectorError> {
768        let Some(running) = self.running.as_mut() else {
769            return Ok(());
770        };
771        match running.acknowledge(checkpoint).await {
772            Ok(()) => Ok(()),
773            Err(cause) => Err(self.fail(format!(
774                "acknowledging committed epoch {epoch} failed; its deliveries will be \
775                 redelivered: {cause}"
776            ))),
777        }
778    }
779
780    fn data_ready_notify(&self) -> Option<Arc<Notify>> {
781        Some(Arc::clone(&self.data_ready))
782    }
783
784    async fn close(&mut self) -> Result<(), ConnectorError> {
785        let Some(running) = self.running.take() else {
786            return Ok(());
787        };
788        let cancelled = running.cancel_consumer().await;
789        running.reader.abort();
790        let closed = close(&running.channel, &running.connection, CLOSE_TIMEOUT).await;
791        cancelled.and(closed)
792    }
793}
794
795#[cfg(test)]
796mod tests;