1use 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
43pub 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
55struct 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 self.reader.abort();
74 }
75}
76
77#[derive(Debug)]
79struct AckLedger {
80 generation: u64,
81 handed_off: u64,
82 acked: u64,
83 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 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 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
160struct Decoder {
162 deserializer: Box<dyn RecordDeserializer>,
163 payload: SchemaRef,
164 output: SchemaRef,
165 config: RabbitMqSourceConfig,
166}
167
168struct 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 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 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 #[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 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 #[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
433async 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 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 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 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 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 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
645fn 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 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 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;