1use arrow_array::RecordBatch;
7use arrow_schema::SchemaRef;
8use async_trait::async_trait;
9use bytes::Bytes;
10use std::collections::{BTreeMap, VecDeque};
11use std::sync::Arc;
12use tokio::sync::Notify;
13use tokio::sync::{OwnedSemaphorePermit, Semaphore};
14
15use crate::checkpoint::SourceCheckpoint;
16use crate::config::{ConnectorConfig, ConnectorState};
17use crate::connector::{
18 ConnectorTaskOwner, ConnectorTaskTracker, SourceBatch, SourceConnector, SourceConsistency,
19 SourceContract, SourcePosition, SourceStart, SourceTopology,
20};
21use crate::error::ConnectorError;
22
23use super::changelog::{
24 events_to_record_batch, old_tuple_json_encoded_len, old_tuple_to_json, plan_record_batch,
25 tuple_json_encoded_len, tuple_to_json, CdcOperation, ChangeEvent,
26};
27use super::config::PostgresCdcConfig;
28use super::decoder::{decode_message, OldTuple, WalMessage};
29use super::lsn::Lsn;
30use super::metrics::PostgresCdcMetrics;
31use super::postgres_io::{source_config_digest, PostgresCheckpointBinding};
32use super::schema::{cdc_envelope_schema, RelationCache, RelationInfo};
33
34#[cfg(not(test))]
35const PGWIRE_IN_FLIGHT_EVENTS: usize = 1;
36#[cfg(not(test))]
37const RAW_WAL_QUEUE_CAPACITY: usize = 4_096;
38const INITIAL_BOOTSTRAP_NOT_ADMITTED: &str = "[LDB-5060] PostgreSQL CDC initial startup is not admitted until a complete, certified snapshot-to-WAL handoff is implemented";
39const CHECKPOINT_CONNECTOR: &str = "postgres-cdc";
40const CHECKPOINT_VERSION: &str = "3";
41const SYSTEM_IDENTIFIER_METADATA: &str = "system_identifier";
42const TIMELINE_ID_METADATA: &str = "timeline_id";
43const DATABASE_OID_METADATA: &str = "database_oid";
44const PUBLICATION_OID_METADATA: &str = "publication_oid";
45const PUBLICATION_DEFINITION_METADATA: &str = "publication_definition_sha256";
46const SOURCE_CONFIG_METADATA: &str = "source_config_sha256";
47const SLOT_PLUGIN_METADATA: &str = "slot_plugin";
48const SLOT_TWO_PHASE_METADATA: &str = "slot_two_phase";
49const SLOT_FAILOVER_METADATA: &str = "slot_failover";
50
51pub struct PostgresCdcSource {
68 config: PostgresCdcConfig,
70
71 state: ConnectorState,
73
74 schema: SchemaRef,
76
77 metrics: Arc<PostgresCdcMetrics>,
79
80 relation_cache: RelationCache,
82
83 committed_transactions: VecDeque<CommittedTransaction>,
85
86 buffered_event_count: usize,
88
89 buffered_event_bytes: usize,
92
93 current_txn: Option<TransactionState>,
95
96 confirmed_flush_lsn: Lsn,
98
99 write_lsn: Lsn,
101
102 polled_lsn: Lsn,
106
107 checkpoint_binding: Option<PostgresCheckpointBinding>,
109
110 #[cfg(test)]
112 pending_messages: VecDeque<Vec<u8>>,
113
114 data_ready: Arc<Notify>,
116
117 wal_rx: Option<WalPayloadRx>,
119
120 reader_handle: Option<tokio::task::JoinHandle<()>>,
122
123 reader_shutdown: Option<tokio::sync::watch::Sender<bool>>,
125
126 confirmed_lsn_tx: Option<tokio::sync::watch::Sender<u64>>,
130
131 pending_payloads: VecDeque<OwnedWalPayload>,
133
134 wal_byte_budget: Option<Arc<Semaphore>>,
136
137 wal_terminal_error: Option<WalTerminalError>,
139
140 task_owner: ConnectorTaskOwner,
142 task_tracker: ConnectorTaskTracker,
143}
144
145impl Drop for PostgresCdcSource {
146 fn drop(&mut self) {
147 if let Some(shutdown) = self.reader_shutdown.take() {
148 shutdown.send_replace(true);
149 }
150 if let Some(handle) = self.reader_handle.take() {
151 reap_postgres_reader(handle, &self.task_owner);
152 }
153 }
154}
155
156fn reap_postgres_reader(handle: tokio::task::JoinHandle<()>, task_owner: &ConnectorTaskOwner) {
157 let Some(reaper_guard) = task_owner.track() else {
158 tracing::warn!("PostgreSQL CDC task generation was sealed before reader reaping");
159 return;
160 };
161 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
162 drop(reaper_guard);
165 return;
166 };
167 drop(runtime.spawn(async move {
168 let _reaper_guard = reaper_guard;
169 if let Err(error) = handle.await {
170 tracing::debug!(%error, "PostgreSQL CDC retired reader task reaped");
171 }
172 }));
173}
174
175#[derive(Debug)]
177struct TransactionState {
178 final_lsn: Lsn,
180 commit_ts_ms: i64,
182 events: VecDeque<ChangeEvent>,
184}
185
186#[derive(Debug)]
188struct CommittedTransaction {
189 end_lsn: Lsn,
190 events: VecDeque<ChangeEvent>,
191}
192
193type WalPayloadRx = crossfire::AsyncRx<crossfire::mpsc::Array<OwnedWalPayload>>;
195
196type WalPayloadTx = crossfire::MAsyncTx<crossfire::mpsc::Array<OwnedWalPayload>>;
197
198type WalTerminalError = Arc<std::sync::Mutex<Option<String>>>;
199
200enum WalPayload {
202 Begin {
203 final_lsn: u64,
204 commit_ts_us: i64,
205 xid: u32,
206 },
207 Commit {
208 end_lsn: u64,
209 commit_ts_us: i64,
210 lsn: u64,
211 },
212 XLogData {
213 wal_end: u64,
214 data: Bytes,
215 },
216 KeepAlive {
217 wal_end: u64,
218 },
219}
220
221struct OwnedWalPayload {
222 payload: WalPayload,
223 _byte_permit: OwnedSemaphorePermit,
224 wire_bytes: Option<pgwire_replication::WireBytesGuard>,
225}
226
227fn retained_wal_payload_bytes(payload: &WalPayload) -> usize {
228 let dynamic_bytes = match payload {
229 WalPayload::XLogData { data, .. } => data.len(),
230 WalPayload::Begin { .. } | WalPayload::Commit { .. } | WalPayload::KeepAlive { .. } => 0,
231 };
232 std::mem::size_of::<OwnedWalPayload>()
233 .saturating_add(dynamic_bytes)
234 .max(1)
235}
236
237fn logical_wal_payload_bytes(payload: &WalPayload) -> usize {
238 match payload {
239 WalPayload::Begin { .. } => 1 + 8 + 8 + 4,
240 WalPayload::Commit { .. } => 1 + 1 + 8 + 8 + 8,
241 WalPayload::XLogData { data, .. } => data.len(),
242 WalPayload::KeepAlive { .. } => 0,
243 }
244}
245
246#[cfg(test)]
247async fn send_wal_or_shutdown(
248 tx: &WalPayloadTx,
249 payload: WalPayload,
250 byte_budget: &Arc<Semaphore>,
251 max_payload_bytes: usize,
252 shutdown_rx: &mut tokio::sync::watch::Receiver<bool>,
253) -> Result<bool, String> {
254 send_wal_with_wire_guard(
255 tx,
256 payload,
257 None,
258 byte_budget,
259 max_payload_bytes,
260 shutdown_rx,
261 )
262 .await
263}
264
265async fn send_wal_with_wire_guard(
266 tx: &WalPayloadTx,
267 payload: WalPayload,
268 wire_bytes: Option<pgwire_replication::WireBytesGuard>,
269 byte_budget: &Arc<Semaphore>,
270 max_payload_bytes: usize,
271 shutdown_rx: &mut tokio::sync::watch::Receiver<bool>,
272) -> Result<bool, String> {
273 if *shutdown_rx.borrow() {
274 return Ok(false);
275 }
276
277 let retained_bytes = retained_wal_payload_bytes(&payload);
278 if retained_bytes > max_payload_bytes {
279 return Err(format!(
280 "PostgreSQL CDC WAL payload exceeds the hard raw buffer limit \
281 (retained bytes: {retained_bytes}/{max_payload_bytes})"
282 ));
283 }
284 let permits = u32::try_from(retained_bytes)
285 .map_err(|_| "PostgreSQL CDC raw WAL byte budget exceeds semaphore capacity".to_string())?;
286 let permit = tokio::select! {
287 biased;
288 _ = shutdown_rx.changed() => return Ok(false),
289 result = Arc::clone(byte_budget).acquire_many_owned(permits) => result.map_err(|_| {
290 "PostgreSQL CDC raw WAL byte budget closed unexpectedly".to_string()
291 })?,
292 };
293 let owned = OwnedWalPayload {
294 payload,
295 _byte_permit: permit,
296 wire_bytes,
297 };
298 tokio::select! {
299 biased;
300 _ = shutdown_rx.changed() => Ok(false),
301 result = tx.send(owned) => Ok(result.is_ok()),
302 }
303}
304
305fn publish_terminal_wal_error(error: &WalTerminalError, message: String, data_ready: &Notify) {
306 let mut slot = error
307 .lock()
308 .unwrap_or_else(std::sync::PoisonError::into_inner);
309 if slot.is_none() {
310 *slot = Some(message);
311 }
312 drop(slot);
313 data_ready.notify_one();
314}
315
316fn take_confirmed_lsn(
317 receiver: &mut tokio::sync::watch::Receiver<u64>,
318) -> Option<pgwire_replication::Lsn> {
319 let confirmed = *receiver.borrow_and_update();
320 (confirmed > 0).then(|| pgwire_replication::Lsn::from_u64(confirmed))
321}
322
323#[cfg(not(test))]
324async fn run_wal_reader(
325 mut client: pgwire_replication::ReplicationClient,
326 wal_tx: WalPayloadTx,
327 byte_budget: Arc<Semaphore>,
328 byte_limit: usize,
329 mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
330 mut confirmed_lsn_rx: tokio::sync::watch::Receiver<u64>,
331 terminal_error: WalTerminalError,
332 data_ready: Arc<Notify>,
333 _reader_guard: crate::connector::ConnectorTaskGuard,
334) {
335 'read: loop {
336 tokio::select! {
337 biased;
338 changed = shutdown_rx.changed() => {
339 if changed.is_err() || *shutdown_rx.borrow() {
340 break 'read;
341 }
342 }
343 changed = confirmed_lsn_rx.changed() => {
344 if changed.is_err() {
345 break 'read;
346 }
347 if let Some(confirmed) = take_confirmed_lsn(&mut confirmed_lsn_rx) {
348 client.update_applied_lsn(confirmed);
349 }
350 }
351 event = client.recv() => {
352 match event {
353 Ok(Some(event)) => {
354 let payload = match event {
355 pgwire_replication::ReplicationEvent::Begin {
356 final_lsn,
357 xid,
358 commit_time_micros,
359 } => Some((
360 WalPayload::Begin {
361 final_lsn: final_lsn.as_u64(),
362 commit_ts_us: commit_time_micros,
363 xid,
364 },
365 None,
366 )),
367 pgwire_replication::ReplicationEvent::Commit {
368 end_lsn,
369 commit_time_micros,
370 lsn,
371 } => Some((
372 WalPayload::Commit {
373 end_lsn: end_lsn.as_u64(),
374 commit_ts_us: commit_time_micros,
375 lsn: lsn.as_u64(),
376 },
377 None,
378 )),
379 pgwire_replication::ReplicationEvent::XLogData {
380 wal_end,
381 data,
382 wire_bytes,
383 ..
384 } => Some((
385 WalPayload::XLogData {
386 wal_end: wal_end.as_u64(),
387 data,
388 },
389 Some(wire_bytes),
390 )),
391 pgwire_replication::ReplicationEvent::KeepAlive {
392 wal_end, ..
393 } => Some((
394 WalPayload::KeepAlive {
395 wal_end: wal_end.as_u64(),
396 },
397 None,
398 )),
399 pgwire_replication::ReplicationEvent::Message { .. } => {
400 publish_terminal_wal_error(
401 &terminal_error,
402 "PostgreSQL emitted a logical decoding message even though replication was started with messages=false"
403 .to_string(),
404 &data_ready,
405 );
406 break 'read;
407 }
408 pgwire_replication::ReplicationEvent::StoppedAt { reached } => {
409 publish_terminal_wal_error(
410 &terminal_error,
411 format!(
412 "PostgreSQL replication stopped unexpectedly at {reached}; no stop LSN was configured"
413 ),
414 &data_ready,
415 );
416 break 'read;
417 }
418 };
419 if let Some((payload, wire_bytes)) = payload {
420 match send_wal_with_wire_guard(
421 &wal_tx,
422 payload,
423 wire_bytes,
424 &byte_budget,
425 byte_limit,
426 &mut shutdown_rx,
427 )
428 .await
429 {
430 Ok(true) => data_ready.notify_one(),
431 Ok(false) => break 'read,
432 Err(message) => {
433 publish_terminal_wal_error(
434 &terminal_error,
435 message,
436 &data_ready,
437 );
438 break 'read;
439 }
440 }
441 }
442 }
443 Ok(None) => {
444 publish_terminal_wal_error(
445 &terminal_error,
446 "PostgreSQL replication stream ended unexpectedly".into(),
447 &data_ready,
448 );
449 break 'read;
450 }
451 Err(error) => {
452 publish_terminal_wal_error(
453 &terminal_error,
454 format!("PostgreSQL replication stream failed: {error}"),
455 &data_ready,
456 );
457 break 'read;
458 }
459 }
460 }
461 }
462 }
463 let _ = client.shutdown().await;
464}
465
466fn retained_event_bytes(event: &ChangeEvent) -> Result<usize, ConnectorError> {
467 planned_event_bytes(
468 event.table.capacity(),
469 event.before.as_ref().map(String::capacity),
470 event.after.as_ref().map(String::capacity),
471 )
472}
473
474fn planned_event_bytes(
475 table_bytes: usize,
476 before_bytes: Option<usize>,
477 after_bytes: Option<usize>,
478) -> Result<usize, ConnectorError> {
479 [
480 table_bytes,
481 before_bytes.unwrap_or(0),
482 after_bytes.unwrap_or(0),
483 ]
484 .into_iter()
485 .try_fold(0_usize, |total, bytes| {
486 total.checked_add(bytes).ok_or_else(|| {
487 ConnectorError::ReadError(
488 "PostgreSQL CDC decoded-event retained-byte size overflow".into(),
489 )
490 })
491 })
492}
493
494fn conservative_deque_growth_bytes(
495 len: usize,
496 capacity: usize,
497 element_size: usize,
498) -> Result<usize, ConnectorError> {
499 if len < capacity {
500 return Ok(0);
501 }
502 capacity.max(4).checked_mul(element_size).ok_or_else(|| {
503 ConnectorError::ReadError("PostgreSQL CDC container growth size overflow".into())
504 })
505}
506
507fn required_checkpoint_metadata<'a>(
508 checkpoint: &'a SourceCheckpoint,
509 key: &str,
510 context: &str,
511) -> Result<&'a str, ConnectorError> {
512 checkpoint.get_metadata(key).ok_or_else(|| {
513 ConnectorError::ConfigurationError(format!(
514 "PostgreSQL CDC {context} is missing required '{key}' metadata"
515 ))
516 })
517}
518
519fn parse_checkpoint_decimal<T>(
520 checkpoint: &SourceCheckpoint,
521 key: &str,
522 context: &str,
523) -> Result<T, ConnectorError>
524where
525 T: std::str::FromStr + ToString,
526 T::Err: std::fmt::Display,
527{
528 let value = required_checkpoint_metadata(checkpoint, key, context)?;
529 let parsed = value.parse::<T>().map_err(|error| {
530 ConnectorError::ConfigurationError(format!(
531 "PostgreSQL CDC {context} has invalid '{key}' metadata '{value}': {error}"
532 ))
533 })?;
534 if parsed.to_string() != value {
535 return Err(ConnectorError::ConfigurationError(format!(
536 "PostgreSQL CDC {context} has non-canonical '{key}' metadata '{value}'"
537 )));
538 }
539 Ok(parsed)
540}
541
542fn parse_checkpoint_bool(
543 checkpoint: &SourceCheckpoint,
544 key: &str,
545 context: &str,
546) -> Result<bool, ConnectorError> {
547 match required_checkpoint_metadata(checkpoint, key, context)? {
548 "true" => Ok(true),
549 "false" => Ok(false),
550 value => Err(ConnectorError::ConfigurationError(format!(
551 "PostgreSQL CDC {context} has invalid '{key}' metadata '{value}'"
552 ))),
553 }
554}
555
556fn parse_checkpoint_sha256(
557 checkpoint: &SourceCheckpoint,
558 key: &str,
559 context: &str,
560) -> Result<String, ConnectorError> {
561 let value = required_checkpoint_metadata(checkpoint, key, context)?;
562 if value.len() != 64
563 || !value
564 .bytes()
565 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
566 {
567 return Err(ConnectorError::ConfigurationError(format!(
568 "PostgreSQL CDC {context} has invalid '{key}' SHA-256 metadata"
569 )));
570 }
571 Ok(value.to_string())
572}
573
574fn checkpoint_binding(
575 checkpoint: &SourceCheckpoint,
576 context: &str,
577) -> Result<PostgresCheckpointBinding, ConnectorError> {
578 Ok(PostgresCheckpointBinding {
579 system_identifier: parse_checkpoint_decimal(
580 checkpoint,
581 SYSTEM_IDENTIFIER_METADATA,
582 context,
583 )?,
584 timeline_id: parse_checkpoint_decimal(checkpoint, TIMELINE_ID_METADATA, context)?,
585 database_oid: parse_checkpoint_decimal(checkpoint, DATABASE_OID_METADATA, context)?,
586 publication_oid: parse_checkpoint_decimal(checkpoint, PUBLICATION_OID_METADATA, context)?,
587 publication_definition_sha256: parse_checkpoint_sha256(
588 checkpoint,
589 PUBLICATION_DEFINITION_METADATA,
590 context,
591 )?,
592 source_config_sha256: parse_checkpoint_sha256(checkpoint, SOURCE_CONFIG_METADATA, context)?,
593 slot_plugin: required_checkpoint_metadata(checkpoint, SLOT_PLUGIN_METADATA, context)?
594 .to_string(),
595 slot_two_phase: parse_checkpoint_bool(checkpoint, SLOT_TWO_PHASE_METADATA, context)?,
596 slot_failover: parse_checkpoint_bool(checkpoint, SLOT_FAILOVER_METADATA, context)?,
597 })
598}
599
600fn validate_checkpoint_identity(
601 checkpoint: &SourceCheckpoint,
602 config: &PostgresCdcConfig,
603 context: &str,
604) -> Result<PostgresCheckpointBinding, ConnectorError> {
605 for (key, expected) in [
606 ("checkpoint_version", CHECKPOINT_VERSION),
607 ("connector", CHECKPOINT_CONNECTOR),
608 ("slot_name", config.slot_name.as_str()),
609 ("publication", config.publication.as_str()),
610 ("database", config.database.as_str()),
611 ] {
612 let actual = required_checkpoint_metadata(checkpoint, key, context)?;
613 if actual != expected {
614 return Err(ConnectorError::ConfigurationError(format!(
615 "PostgreSQL CDC {context} has '{key}' identity '{actual}', expected '{expected}'"
616 )));
617 }
618 }
619
620 let binding = checkpoint_binding(checkpoint, context)?;
621 let configured_digest = source_config_digest(config);
622 if binding.source_config_sha256 != configured_digest {
623 return Err(ConnectorError::ConfigurationError(format!(
624 "PostgreSQL CDC {context} source filter/configuration identity drifted from its checkpoint"
625 )));
626 }
627 Ok(binding)
628}
629
630fn write_checkpoint_binding(
631 checkpoint: &mut SourceCheckpoint,
632 binding: &PostgresCheckpointBinding,
633) {
634 checkpoint.set_metadata("connector", CHECKPOINT_CONNECTOR);
635 checkpoint.set_metadata("checkpoint_version", CHECKPOINT_VERSION);
636 checkpoint.set_metadata(
637 SYSTEM_IDENTIFIER_METADATA,
638 binding.system_identifier.to_string(),
639 );
640 checkpoint.set_metadata(TIMELINE_ID_METADATA, binding.timeline_id.to_string());
641 checkpoint.set_metadata(DATABASE_OID_METADATA, binding.database_oid.to_string());
642 checkpoint.set_metadata(
643 PUBLICATION_OID_METADATA,
644 binding.publication_oid.to_string(),
645 );
646 checkpoint.set_metadata(
647 PUBLICATION_DEFINITION_METADATA,
648 &binding.publication_definition_sha256,
649 );
650 checkpoint.set_metadata(SOURCE_CONFIG_METADATA, &binding.source_config_sha256);
651 checkpoint.set_metadata(SLOT_PLUGIN_METADATA, &binding.slot_plugin);
652 checkpoint.set_metadata(SLOT_TWO_PHASE_METADATA, binding.slot_two_phase.to_string());
653 checkpoint.set_metadata(SLOT_FAILOVER_METADATA, binding.slot_failover.to_string());
654}
655
656fn validate_live_binding(
657 checkpoint: &PostgresCheckpointBinding,
658 live: &PostgresCheckpointBinding,
659 context: &str,
660) -> Result<(), ConnectorError> {
661 if checkpoint != live {
662 return Err(ConnectorError::ConfigurationError(format!(
663 "PostgreSQL CDC {context} identity drifted from the live database, publication, or replication slot (checkpoint: {checkpoint:?}; live: {live:?})"
664 )));
665 }
666 Ok(())
667}
668
669impl PostgresCdcSource {
670 #[must_use]
672 pub fn new(mut config: PostgresCdcConfig, registry: Option<&prometheus::Registry>) -> Self {
673 config.normalize_table_filters();
674 let (task_owner, task_tracker) = ConnectorTaskOwner::new();
675 Self {
676 config,
677 state: ConnectorState::Created,
678 schema: cdc_envelope_schema(),
679 metrics: Arc::new(PostgresCdcMetrics::new(registry)),
680 relation_cache: RelationCache::new(),
681 committed_transactions: VecDeque::new(),
682 buffered_event_count: 0,
683 buffered_event_bytes: 0,
684 current_txn: None,
685 confirmed_flush_lsn: Lsn::ZERO,
686 write_lsn: Lsn::ZERO,
687 polled_lsn: Lsn::ZERO,
688 checkpoint_binding: None,
689 #[cfg(test)]
690 pending_messages: VecDeque::new(),
691 data_ready: Arc::new(Notify::new()),
692 wal_rx: None,
693 reader_handle: None,
694 reader_shutdown: None,
695 confirmed_lsn_tx: None,
696 pending_payloads: VecDeque::new(),
697 wal_byte_budget: None,
698 wal_terminal_error: None,
699 task_owner,
700 task_tracker,
701 }
702 }
703
704 pub fn from_config(config: &ConnectorConfig) -> Result<Self, ConnectorError> {
710 let pg_config = PostgresCdcConfig::from_config(config)?;
711 Ok(Self::new(pg_config, None))
712 }
713
714 #[must_use]
716 pub fn config(&self) -> &PostgresCdcConfig {
717 &self.config
718 }
719
720 #[must_use]
722 pub fn confirmed_flush_lsn(&self) -> Lsn {
723 self.confirmed_flush_lsn
724 }
725
726 #[must_use]
728 pub fn write_lsn(&self) -> Lsn {
729 self.write_lsn
730 }
731
732 #[must_use]
734 pub fn replication_lag_bytes(&self) -> u64 {
735 self.write_lsn.diff(self.confirmed_flush_lsn)
736 }
737
738 #[must_use]
740 pub fn relation_cache(&self) -> &RelationCache {
741 &self.relation_cache
742 }
743
744 #[must_use]
746 pub fn buffered_events(&self) -> usize {
747 self.buffered_event_count
748 }
749
750 #[cfg(test)]
751 fn enqueue_wal_data(&mut self, data: Vec<u8>) {
752 self.pending_messages.push_back(data);
753 }
754
755 #[cfg(test)]
756 fn process_pending_messages(&mut self) -> Result<(), ConnectorError> {
757 while let Some(data) = self.pending_messages.pop_front() {
758 let result = decode_message(Bytes::from(data))
759 .map_err(|error| ConnectorError::ReadError(format!("pgoutput decode: {error}")))
760 .and_then(|message| self.process_wal_message(message));
761 if let Err(error) = result {
762 self.state = ConnectorState::Failed;
763 return Err(error);
764 }
765 }
766 Ok(())
767 }
768
769 fn process_wal_message(&mut self, msg: WalMessage) -> Result<(), ConnectorError> {
771 match msg {
772 WalMessage::Begin(begin) => {
773 if self.current_txn.is_some() {
774 return Err(ConnectorError::ReadError(
775 "PostgreSQL CDC received BEGIN before the current transaction committed"
776 .into(),
777 ));
778 }
779 self.current_txn = Some(TransactionState {
780 final_lsn: begin.final_lsn,
781 commit_ts_ms: begin.commit_ts_ms,
782 events: VecDeque::new(),
783 });
784 self.reserve_committed_transaction_slot()?;
785 }
786 WalMessage::Commit(commit) => {
787 if let Err(error) = self.validate_commit_boundary(&commit) {
788 self.state = ConnectorState::Failed;
789 return Err(error);
790 }
791 self.reserve_committed_transaction_slot()?;
792 let txn = self.current_txn.take().ok_or_else(|| {
793 ConnectorError::ReadError(
794 "PostgreSQL CDC received COMMIT without an open transaction".into(),
795 )
796 })?;
797 self.committed_transactions.push_back(CommittedTransaction {
798 end_lsn: commit.end_lsn,
799 events: txn.events,
800 });
801 self.write_lsn = self.write_lsn.max(commit.end_lsn);
802 self.metrics.record_transaction();
803 self.metrics
804 .set_replication_lag_bytes(self.replication_lag_bytes());
805 }
806 WalMessage::Relation(rel) => {
807 let info = RelationInfo {
808 relation_id: rel.relation_id,
809 namespace: rel.namespace,
810 name: rel.name,
811 replica_identity: rel.replica_identity as char,
812 columns: rel.columns,
813 };
814 self.admit_relation(info)?;
815 }
816 WalMessage::Insert(ins) => {
817 self.process_insert(ins.relation_id, &ins.new_tuple)?;
818 }
819 WalMessage::Update(upd) => {
820 self.process_update(upd.relation_id, upd.old_tuple.as_ref(), &upd.new_tuple)?;
821 }
822 WalMessage::Delete(del) => {
823 self.process_delete(del.relation_id, &del.old_tuple)?;
824 }
825 WalMessage::Truncate(trunc) => {
826 let table_names: Vec<String> = trunc
827 .relation_ids
828 .iter()
829 .map(|id| {
830 self.relation_cache
831 .get(*id)
832 .map_or_else(|| Ok(format!("oid:{id}")), RelationInfo::full_name)
833 })
834 .collect::<Result<_, ConnectorError>>()?;
835 return Err(ConnectorError::ReadError(format!(
836 "TRUNCATE received on table(s): {}. \
837 Cannot produce retraction events — restart the pipeline with a fresh snapshot.",
838 table_names.join(", ")
839 )));
840 }
841 WalMessage::Origin(_) | WalMessage::Type(_) => {
842 }
845 }
846 Ok(())
847 }
848
849 fn process_insert(
850 &mut self,
851 relation_id: u32,
852 new_tuple: &super::decoder::TupleData,
853 ) -> Result<(), ConnectorError> {
854 let (lsn, ts_ms) = self.require_current_txn_context()?;
855 let (table, after_len) = {
856 let relation = self.require_relation(relation_id)?;
857 let table = relation.full_name()?;
858
859 if !self.config.should_include_table(&table) {
860 return Ok(());
861 }
862
863 let after_len = tuple_json_encoded_len(new_tuple, relation)?;
864 (table, after_len)
865 };
866 let event_bytes = planned_event_bytes(table.capacity(), None, Some(after_len))?;
867 self.reserve_current_event_slot()?;
868 self.ensure_event_capacity(event_bytes)?;
869 let after_json = tuple_to_json(new_tuple, self.require_relation(relation_id)?, after_len)?;
870
871 let event = ChangeEvent {
872 table,
873 op: CdcOperation::Insert,
874 lsn,
875 ts_ms,
876 before: None,
877 after: Some(after_json),
878 };
879
880 self.push_event(event, event_bytes)?;
881 self.metrics.record_insert();
882 Ok(())
883 }
884
885 fn process_update(
886 &mut self,
887 relation_id: u32,
888 old_tuple: Option<&OldTuple>,
889 new_tuple: &super::decoder::TupleData,
890 ) -> Result<(), ConnectorError> {
891 let (lsn, ts_ms) = self.require_current_txn_context()?;
892 let (table, before_len, after_len) = {
893 let relation = self.require_relation(relation_id)?;
894 let table = relation.full_name()?;
895
896 if !self.config.should_include_table(&table) {
897 return Ok(());
898 }
899
900 let before_len = old_tuple
901 .map(|tuple| old_tuple_json_encoded_len(tuple, relation))
902 .transpose()?;
903 let after_len = tuple_json_encoded_len(new_tuple, relation)?;
904 (table, before_len, after_len)
905 };
906 let event_bytes = planned_event_bytes(table.capacity(), before_len, Some(after_len))?;
907 self.reserve_current_event_slot()?;
908 self.ensure_event_capacity(event_bytes)?;
909 let relation = self.require_relation(relation_id)?;
910 let before_json = old_tuple
911 .zip(before_len)
912 .map(|(tuple, length)| old_tuple_to_json(tuple, relation, length))
913 .transpose()?;
914 let after_json = tuple_to_json(new_tuple, relation, after_len)?;
915
916 let event = ChangeEvent {
917 table,
918 op: CdcOperation::Update,
919 lsn,
920 ts_ms,
921 before: before_json,
922 after: Some(after_json),
923 };
924
925 self.push_event(event, event_bytes)?;
926 self.metrics.record_update();
927 Ok(())
928 }
929
930 fn process_delete(
931 &mut self,
932 relation_id: u32,
933 old_tuple: &OldTuple,
934 ) -> Result<(), ConnectorError> {
935 let (lsn, ts_ms) = self.require_current_txn_context()?;
936 let (table, before_len) = {
937 let relation = self.require_relation(relation_id)?;
938 let table = relation.full_name()?;
939
940 if !self.config.should_include_table(&table) {
941 return Ok(());
942 }
943
944 let before_len = old_tuple_json_encoded_len(old_tuple, relation)?;
945 (table, before_len)
946 };
947 let event_bytes = planned_event_bytes(table.capacity(), Some(before_len), None)?;
948 self.reserve_current_event_slot()?;
949 self.ensure_event_capacity(event_bytes)?;
950 let before_json =
951 old_tuple_to_json(old_tuple, self.require_relation(relation_id)?, before_len)?;
952
953 let event = ChangeEvent {
954 table,
955 op: CdcOperation::Delete,
956 lsn,
957 ts_ms,
958 before: Some(before_json),
959 after: None,
960 };
961
962 self.push_event(event, event_bytes)?;
963 self.metrics.record_delete();
964 Ok(())
965 }
966
967 fn require_relation(&self, relation_id: u32) -> Result<&RelationInfo, ConnectorError> {
973 self.relation_cache.get(relation_id).ok_or_else(|| {
974 ConnectorError::ReadError(format!(
975 "unknown relation ID {relation_id} (no Relation message received yet)"
976 ))
977 })
978 }
979
980 fn require_current_txn_context(&mut self) -> Result<(Lsn, i64), ConnectorError> {
981 if let Some(txn) = &self.current_txn {
982 return Ok((txn.final_lsn, txn.commit_ts_ms));
983 }
984 self.state = ConnectorState::Failed;
985 Err(ConnectorError::ReadError(
986 "PostgreSQL CDC received a row change outside a transaction".into(),
987 ))
988 }
989
990 fn validate_commit_boundary(
991 &self,
992 commit: &super::decoder::CommitMessage,
993 ) -> Result<(), ConnectorError> {
994 let transaction = self.current_txn.as_ref().ok_or_else(|| {
995 ConnectorError::ReadError(
996 "PostgreSQL CDC received COMMIT without an open transaction".into(),
997 )
998 })?;
999 if commit.commit_lsn != transaction.final_lsn {
1000 return Err(ConnectorError::ReadError(format!(
1001 "PostgreSQL CDC COMMIT LSN {} does not match BEGIN final LSN {}",
1002 commit.commit_lsn, transaction.final_lsn
1003 )));
1004 }
1005 if commit.commit_ts_ms != transaction.commit_ts_ms {
1006 return Err(ConnectorError::ReadError(format!(
1007 "PostgreSQL CDC COMMIT timestamp {} does not match BEGIN timestamp {}",
1008 commit.commit_ts_ms, transaction.commit_ts_ms
1009 )));
1010 }
1011 if commit.end_lsn < commit.commit_lsn {
1012 return Err(ConnectorError::ReadError(format!(
1013 "PostgreSQL CDC COMMIT end LSN {} is before commit LSN {}",
1014 commit.end_lsn, commit.commit_lsn
1015 )));
1016 }
1017 let last_resumable_lsn = self
1018 .committed_transactions
1019 .back()
1020 .map_or(self.polled_lsn, |transaction| {
1021 transaction.end_lsn.max(self.polled_lsn)
1022 });
1023 if commit.end_lsn < last_resumable_lsn {
1024 return Err(ConnectorError::ReadError(format!(
1025 "PostgreSQL CDC COMMIT end LSN {} is behind the last emitted or queued LSN {last_resumable_lsn}",
1026 commit.end_lsn
1027 )));
1028 }
1029 Ok(())
1030 }
1031
1032 fn event_container_retained_bytes(&self) -> Result<usize, ConnectorError> {
1033 let event_size = std::mem::size_of::<ChangeEvent>();
1034 let mut retained = self
1035 .committed_transactions
1036 .capacity()
1037 .checked_mul(std::mem::size_of::<CommittedTransaction>())
1038 .ok_or_else(|| {
1039 ConnectorError::ReadError(
1040 "PostgreSQL CDC committed-transaction container size overflow".into(),
1041 )
1042 })?;
1043 if let Some(transaction) = &self.current_txn {
1044 retained = retained
1045 .checked_add(
1046 transaction
1047 .events
1048 .capacity()
1049 .checked_mul(event_size)
1050 .ok_or_else(|| {
1051 ConnectorError::ReadError(
1052 "PostgreSQL CDC open-transaction container size overflow".into(),
1053 )
1054 })?,
1055 )
1056 .ok_or_else(|| {
1057 ConnectorError::ReadError(
1058 "PostgreSQL CDC event-container retained-byte overflow".into(),
1059 )
1060 })?;
1061 }
1062 for transaction in &self.committed_transactions {
1063 retained = retained
1064 .checked_add(
1065 transaction
1066 .events
1067 .capacity()
1068 .checked_mul(event_size)
1069 .ok_or_else(|| {
1070 ConnectorError::ReadError(
1071 "PostgreSQL CDC committed-event container size overflow".into(),
1072 )
1073 })?,
1074 )
1075 .ok_or_else(|| {
1076 ConnectorError::ReadError(
1077 "PostgreSQL CDC event-container retained-byte overflow".into(),
1078 )
1079 })?;
1080 }
1081 Ok(retained)
1082 }
1083
1084 fn decoded_retained_bytes(&self) -> Result<usize, ConnectorError> {
1085 let container_bytes = self.event_container_retained_bytes()?;
1086 let relation_bytes = self.relation_cache.retained_bytes()?;
1087 self.buffered_event_bytes
1088 .checked_add(container_bytes)
1089 .and_then(|bytes| bytes.checked_add(relation_bytes))
1090 .ok_or_else(|| {
1091 ConnectorError::ReadError(
1092 "PostgreSQL CDC decoded-stage retained-byte accounting overflow".into(),
1093 )
1094 })
1095 }
1096
1097 fn ensure_decoded_byte_limit(
1098 &mut self,
1099 additional_bytes: usize,
1100 context: &str,
1101 ) -> Result<usize, ConnectorError> {
1102 let retained_bytes = self
1103 .decoded_retained_bytes()
1104 .and_then(|bytes| {
1105 bytes.checked_add(additional_bytes).ok_or_else(|| {
1106 ConnectorError::ReadError(
1107 "PostgreSQL CDC decoded-stage retained-byte accounting overflow".into(),
1108 )
1109 })
1110 })
1111 .inspect_err(|_error| {
1112 self.state = ConnectorState::Failed;
1113 })?;
1114 let max_bytes = self.config.decoded_event_bytes();
1115 if retained_bytes > max_bytes {
1116 self.state = ConnectorState::Failed;
1117 return Err(ConnectorError::ReadError(format!(
1118 "PostgreSQL CDC {context} exceeds the hard decoded-stage buffer limit (retained bytes: {retained_bytes}/{max_bytes})"
1119 )));
1120 }
1121 Ok(retained_bytes)
1122 }
1123
1124 fn reserve_current_event_slot(&mut self) -> Result<(), ConnectorError> {
1125 let Some(transaction) = self.current_txn.as_ref() else {
1126 self.state = ConnectorState::Failed;
1127 return Err(ConnectorError::ReadError(
1128 "PostgreSQL CDC received a row change outside a transaction".into(),
1129 ));
1130 };
1131 let old_capacity = transaction.events.capacity();
1132 let growth_bytes = conservative_deque_growth_bytes(
1133 transaction.events.len(),
1134 old_capacity,
1135 std::mem::size_of::<ChangeEvent>(),
1136 )?;
1137 self.ensure_decoded_byte_limit(growth_bytes, "event-container growth")?;
1138 let Some(transaction) = self.current_txn.as_mut() else {
1139 self.state = ConnectorState::Failed;
1140 return Err(ConnectorError::Internal(
1141 "PostgreSQL CDC open transaction disappeared during container preflight".into(),
1142 ));
1143 };
1144 if let Err(error) = transaction.events.try_reserve_exact(1) {
1145 self.state = ConnectorState::Failed;
1146 return Err(ConnectorError::ReadError(format!(
1147 "PostgreSQL CDC could not reserve decoded-event storage: {error}"
1148 )));
1149 }
1150 let Some(actual_growth) = transaction
1151 .events
1152 .capacity()
1153 .checked_sub(old_capacity)
1154 .and_then(|capacity| capacity.checked_mul(std::mem::size_of::<ChangeEvent>()))
1155 else {
1156 self.state = ConnectorState::Failed;
1157 return Err(ConnectorError::Internal(
1158 "PostgreSQL CDC event-container growth accounting failed".into(),
1159 ));
1160 };
1161 if actual_growth > growth_bytes {
1162 self.state = ConnectorState::Failed;
1163 return Err(ConnectorError::Internal(format!(
1164 "PostgreSQL CDC event-container growth exceeded its conservative preflight: actual={actual_growth}, planned={growth_bytes}"
1165 )));
1166 }
1167 self.ensure_decoded_byte_limit(0, "event-container growth")?;
1168 Ok(())
1169 }
1170
1171 fn reserve_committed_transaction_slot(&mut self) -> Result<(), ConnectorError> {
1172 if self.current_txn.is_none() {
1173 self.state = ConnectorState::Failed;
1174 return Err(ConnectorError::ReadError(
1175 "PostgreSQL CDC received COMMIT without an open transaction".into(),
1176 ));
1177 }
1178 self.committed_transactions
1179 .len()
1180 .checked_add(1)
1181 .ok_or_else(|| {
1182 self.state = ConnectorState::Failed;
1183 ConnectorError::ReadError(
1184 "PostgreSQL CDC committed-transaction count overflow".into(),
1185 )
1186 })?;
1187 let old_capacity = self.committed_transactions.capacity();
1188 let growth_bytes = conservative_deque_growth_bytes(
1189 self.committed_transactions.len(),
1190 old_capacity,
1191 std::mem::size_of::<CommittedTransaction>(),
1192 )?;
1193 self.ensure_decoded_byte_limit(growth_bytes, "committed-transaction container growth")?;
1194 if let Err(error) = self.committed_transactions.try_reserve_exact(1) {
1195 self.state = ConnectorState::Failed;
1196 return Err(ConnectorError::ReadError(format!(
1197 "PostgreSQL CDC could not reserve committed-transaction storage: {error}"
1198 )));
1199 }
1200 let actual_growth = self
1201 .committed_transactions
1202 .capacity()
1203 .checked_sub(old_capacity)
1204 .and_then(|capacity| capacity.checked_mul(std::mem::size_of::<CommittedTransaction>()))
1205 .ok_or_else(|| {
1206 self.state = ConnectorState::Failed;
1207 ConnectorError::Internal(
1208 "PostgreSQL CDC committed-transaction growth accounting failed".into(),
1209 )
1210 })?;
1211 if actual_growth > growth_bytes {
1212 self.state = ConnectorState::Failed;
1213 return Err(ConnectorError::Internal(format!(
1214 "PostgreSQL CDC committed-transaction growth exceeded its conservative preflight: actual={actual_growth}, planned={growth_bytes}"
1215 )));
1216 }
1217 self.ensure_decoded_byte_limit(0, "committed-transaction container growth")?;
1218 Ok(())
1219 }
1220
1221 fn admit_relation(&mut self, info: RelationInfo) -> Result<(), ConnectorError> {
1222 let existing_bytes = self
1223 .relation_cache
1224 .get(info.relation_id)
1225 .map(RelationInfo::variable_retained_bytes)
1226 .transpose()
1227 .inspect_err(|_error| {
1228 self.state = ConnectorState::Failed;
1229 })?;
1230 let new_relation = usize::from(existing_bytes.is_none());
1231 let existing_bytes = existing_bytes.unwrap_or(0);
1232 self.relation_cache
1233 .len()
1234 .checked_add(new_relation)
1235 .ok_or_else(|| {
1236 self.state = ConnectorState::Failed;
1237 ConnectorError::ReadError("PostgreSQL CDC relation-cache count overflow".into())
1238 })?;
1239 let incoming_bytes = info.variable_retained_bytes().inspect_err(|_error| {
1240 self.state = ConnectorState::Failed;
1241 })?;
1242 let retained_growth = incoming_bytes.saturating_sub(existing_bytes);
1243 let growth_bytes = self
1244 .relation_cache
1245 .reservation_growth_bytes(info.relation_id)
1246 .inspect_err(|_error| {
1247 self.state = ConnectorState::Failed;
1248 })?;
1249 let admission_bytes = retained_growth.checked_add(growth_bytes).ok_or_else(|| {
1250 self.state = ConnectorState::Failed;
1251 ConnectorError::ReadError(
1252 "PostgreSQL CDC relation-cache admission size overflow".into(),
1253 )
1254 })?;
1255 self.ensure_decoded_byte_limit(admission_bytes, "relation-cache admission")?;
1256 let old_cache_bytes = self.relation_cache.retained_bytes()?;
1257 self.relation_cache
1258 .try_reserve_for(info.relation_id)
1259 .inspect_err(|_error| {
1260 self.state = ConnectorState::Failed;
1261 })?;
1262 let actual_growth = self
1263 .relation_cache
1264 .retained_bytes()?
1265 .checked_sub(old_cache_bytes)
1266 .ok_or_else(|| {
1267 self.state = ConnectorState::Failed;
1268 ConnectorError::Internal(
1269 "PostgreSQL CDC relation-cache growth accounting failed".into(),
1270 )
1271 })?;
1272 if actual_growth > growth_bytes {
1273 self.state = ConnectorState::Failed;
1274 return Err(ConnectorError::Internal(format!(
1275 "PostgreSQL CDC relation-cache growth exceeded its conservative preflight: actual={actual_growth}, planned={growth_bytes}"
1276 )));
1277 }
1278 self.ensure_decoded_byte_limit(retained_growth, "relation-cache admission")?;
1279 self.relation_cache.insert(info).inspect_err(|_error| {
1280 self.state = ConnectorState::Failed;
1281 })?;
1282 self.ensure_decoded_byte_limit(0, "relation-cache retention")?;
1283 Ok(())
1284 }
1285
1286 fn ensure_event_capacity(&mut self, event_bytes: usize) -> Result<(), ConnectorError> {
1287 if self.current_txn.is_none() {
1288 self.state = ConnectorState::Failed;
1289 return Err(ConnectorError::ReadError(
1290 "PostgreSQL CDC received a row change outside a transaction".into(),
1291 ));
1292 }
1293
1294 self.buffered_event_count.checked_add(1).ok_or_else(|| {
1295 self.state = ConnectorState::Failed;
1296 ConnectorError::ReadError(
1297 "PostgreSQL CDC decoded-event count accounting overflow".into(),
1298 )
1299 })?;
1300 self.ensure_decoded_byte_limit(event_bytes, "transaction")?;
1301 Ok(())
1302 }
1303
1304 fn push_event(
1305 &mut self,
1306 event: ChangeEvent,
1307 preflight_bytes: usize,
1308 ) -> Result<(), ConnectorError> {
1309 let event_bytes = retained_event_bytes(&event)?;
1310 if event_bytes > preflight_bytes {
1311 self.state = ConnectorState::Failed;
1312 return Err(ConnectorError::Internal(format!(
1313 "PostgreSQL CDC decoded event exceeded its retained-byte preflight: actual={event_bytes}, planned={preflight_bytes}"
1314 )));
1315 }
1316 self.ensure_event_capacity(event_bytes)?;
1317 let next_event_count = self.buffered_event_count.checked_add(1).ok_or_else(|| {
1318 self.state = ConnectorState::Failed;
1319 ConnectorError::Internal(
1320 "PostgreSQL CDC preflighted event-count accounting overflow".into(),
1321 )
1322 })?;
1323 let next_event_bytes = self
1324 .buffered_event_bytes
1325 .checked_add(event_bytes)
1326 .ok_or_else(|| {
1327 self.state = ConnectorState::Failed;
1328 ConnectorError::Internal(
1329 "PostgreSQL CDC preflighted retained-byte accounting overflow".into(),
1330 )
1331 })?;
1332
1333 let Some(txn) = self.current_txn.as_mut() else {
1334 self.state = ConnectorState::Failed;
1335 return Err(ConnectorError::Internal(
1336 "PostgreSQL CDC open transaction disappeared after event preflight".into(),
1337 ));
1338 };
1339 txn.events.push_back(event);
1340 self.buffered_event_count = next_event_count;
1341 self.buffered_event_bytes = next_event_bytes;
1342 Ok(())
1343 }
1344
1345 fn process_wal_payload(&mut self, payload: WalPayload) -> Result<(), ConnectorError> {
1347 use super::decoder::pg_timestamp_to_unix_ms;
1348
1349 match payload {
1350 WalPayload::Begin {
1351 final_lsn,
1352 commit_ts_us,
1353 xid,
1354 } => {
1355 let begin = super::decoder::BeginMessage {
1356 final_lsn: Lsn::new(final_lsn),
1357 commit_ts_ms: pg_timestamp_to_unix_ms(commit_ts_us).map_err(|error| {
1358 ConnectorError::ReadError(format!(
1359 "pgoutput BEGIN timestamp decode: {error}"
1360 ))
1361 })?,
1362 xid,
1363 };
1364 self.process_wal_message(WalMessage::Begin(begin))
1365 }
1366 WalPayload::Commit {
1367 end_lsn,
1368 commit_ts_us,
1369 lsn,
1370 } => {
1371 let commit = super::decoder::CommitMessage {
1372 flags: 0,
1373 commit_lsn: Lsn::new(lsn),
1374 end_lsn: Lsn::new(end_lsn),
1375 commit_ts_ms: pg_timestamp_to_unix_ms(commit_ts_us).map_err(|error| {
1376 ConnectorError::ReadError(format!(
1377 "pgoutput COMMIT timestamp decode: {error}"
1378 ))
1379 })?,
1380 };
1381 self.process_wal_message(WalMessage::Commit(commit))
1382 }
1383 WalPayload::XLogData { wal_end, data } => {
1384 let msg = decode_message(data)
1385 .map_err(|e| ConnectorError::ReadError(format!("pgoutput decode: {e}")))?;
1386 self.process_wal_message(msg)?;
1387 self.write_lsn = self.write_lsn.max(Lsn::new(wal_end));
1388 Ok(())
1389 }
1390 WalPayload::KeepAlive { wal_end } => {
1391 self.write_lsn = self.write_lsn.max(Lsn::new(wal_end));
1392 Ok(())
1393 }
1394 }
1395 }
1396
1397 fn process_owned_wal_payload(
1398 &mut self,
1399 payload: OwnedWalPayload,
1400 ) -> Result<(), ConnectorError> {
1401 let received_bytes = u64::try_from(logical_wal_payload_bytes(&payload.payload))
1402 .map_err(|_| ConnectorError::Internal("PostgreSQL CDC byte metric overflow".into()))?;
1403 self.metrics.record_bytes(received_bytes);
1404 let OwnedWalPayload {
1405 payload,
1406 _byte_permit,
1407 wire_bytes,
1408 } = payload;
1409 let result = self.process_wal_payload(payload);
1410 drop(wire_bytes);
1411 result
1412 }
1413
1414 fn fail_on_terminal_wal_error(&mut self) -> Result<(), ConnectorError> {
1415 let message = self.wal_terminal_error.as_ref().and_then(|error| {
1416 error
1417 .lock()
1418 .unwrap_or_else(std::sync::PoisonError::into_inner)
1419 .take()
1420 });
1421 if let Some(message) = message {
1422 self.state = ConnectorState::Failed;
1423 return Err(ConnectorError::ReadError(message));
1424 }
1425 Ok(())
1426 }
1427
1428 fn drain_events(&mut self, max: usize) -> Result<Option<RecordBatch>, ConnectorError> {
1436 if self.committed_transactions.is_empty() || max == 0 {
1437 return Ok(None);
1438 }
1439
1440 let mut selected_transactions = 0_usize;
1441 let mut selected_events = 0_usize;
1442 let mut resumable_lsn = self.polled_lsn;
1443 for transaction in &self.committed_transactions {
1444 let candidate_events = selected_events
1445 .checked_add(transaction.events.len())
1446 .ok_or_else(|| {
1447 self.state = ConnectorState::Failed;
1448 ConnectorError::Internal(
1449 "PostgreSQL CDC drain event-count accounting overflow".into(),
1450 )
1451 })?;
1452 if selected_events != 0 && !transaction.events.is_empty() && candidate_events > max {
1457 break;
1458 }
1459 selected_events = candidate_events;
1460 selected_transactions = selected_transactions.checked_add(1).ok_or_else(|| {
1461 self.state = ConnectorState::Failed;
1462 ConnectorError::Internal(
1463 "PostgreSQL CDC drain transaction-count accounting overflow".into(),
1464 )
1465 })?;
1466 resumable_lsn = transaction.end_lsn;
1467 }
1468
1469 if selected_events == 0 {
1470 for _ in 0..selected_transactions {
1472 self.committed_transactions.pop_front();
1473 }
1474 if self.committed_transactions.is_empty() {
1475 self.committed_transactions = VecDeque::new();
1476 }
1477 self.polled_lsn = resumable_lsn;
1478 return Ok(None);
1479 }
1480
1481 let drained_count = selected_events;
1482 let selected = self
1483 .committed_transactions
1484 .iter()
1485 .take(selected_transactions);
1486 let drained_bytes = match selected
1487 .clone()
1488 .flat_map(|transaction| transaction.events.iter())
1489 .try_fold(0_usize, |bytes, event| {
1490 bytes
1491 .checked_add(retained_event_bytes(event)?)
1492 .ok_or_else(|| {
1493 ConnectorError::Internal(
1494 "PostgreSQL CDC drained-event retained-byte accounting overflow".into(),
1495 )
1496 })
1497 }) {
1498 Ok(bytes) => bytes,
1499 Err(error) => {
1500 self.state = ConnectorState::Failed;
1501 return Err(error);
1502 }
1503 };
1504 if drained_count > self.buffered_event_count || drained_bytes > self.buffered_event_bytes {
1505 self.state = ConnectorState::Failed;
1506 return Err(ConnectorError::Internal(
1507 "PostgreSQL CDC retained-buffer accounting invariant failed".into(),
1508 ));
1509 }
1510
1511 let plan =
1512 match plan_record_batch(selected.flat_map(|transaction| transaction.events.iter())) {
1513 Ok(plan) => plan,
1514 Err(error) => {
1515 self.state = ConnectorState::Failed;
1516 return Err(error);
1517 }
1518 };
1519 let arrow_byte_limit = self.config.arrow_build_bytes();
1520 let minimum_extraction_bytes = selected_transactions
1521 .checked_mul(std::mem::size_of::<VecDeque<ChangeEvent>>())
1522 .ok_or_else(|| {
1523 self.state = ConnectorState::Failed;
1524 ConnectorError::ReadError(
1525 "PostgreSQL CDC Arrow extraction-container size overflow".into(),
1526 )
1527 })?;
1528 let minimum_arrow_bytes = plan
1529 .retained_bytes
1530 .checked_add(minimum_extraction_bytes)
1531 .ok_or_else(|| {
1532 self.state = ConnectorState::Failed;
1533 ConnectorError::ReadError(
1534 "PostgreSQL CDC Arrow build retained-byte accounting overflow".into(),
1535 )
1536 })?;
1537 if minimum_arrow_bytes > arrow_byte_limit {
1538 self.state = ConnectorState::Failed;
1539 return Err(ConnectorError::ReadError(format!(
1540 "PostgreSQL CDC Arrow batch exceeds the hard build-buffer limit (retained bytes: {minimum_arrow_bytes}/{arrow_byte_limit})"
1541 )));
1542 }
1543
1544 let mut event_groups = Vec::new();
1547 if let Err(error) = event_groups.try_reserve_exact(selected_transactions) {
1548 self.state = ConnectorState::Failed;
1549 return Err(ConnectorError::ReadError(format!(
1550 "PostgreSQL CDC could not reserve Arrow extraction storage: {error}"
1551 )));
1552 }
1553 let extraction_capacity = event_groups.capacity();
1554 let extraction_bytes = extraction_capacity
1555 .checked_mul(std::mem::size_of::<VecDeque<ChangeEvent>>())
1556 .ok_or_else(|| {
1557 self.state = ConnectorState::Failed;
1558 ConnectorError::ReadError(
1559 "PostgreSQL CDC Arrow extraction-container size overflow".into(),
1560 )
1561 })?;
1562 let planned_arrow_bytes = plan
1563 .retained_bytes
1564 .checked_add(extraction_bytes)
1565 .ok_or_else(|| {
1566 self.state = ConnectorState::Failed;
1567 ConnectorError::ReadError(
1568 "PostgreSQL CDC Arrow build retained-byte accounting overflow".into(),
1569 )
1570 })?;
1571 if planned_arrow_bytes > arrow_byte_limit {
1572 self.state = ConnectorState::Failed;
1573 return Err(ConnectorError::ReadError(format!(
1574 "PostgreSQL CDC Arrow batch exceeds the hard build-buffer limit (retained bytes: {planned_arrow_bytes}/{arrow_byte_limit})"
1575 )));
1576 }
1577
1578 for _ in 0..selected_transactions {
1579 let Some(mut transaction) = self.committed_transactions.pop_front() else {
1580 self.state = ConnectorState::Failed;
1581 return Err(ConnectorError::Internal(
1582 "PostgreSQL CDC committed transaction disappeared after drain preflight".into(),
1583 ));
1584 };
1585 event_groups.push(std::mem::take(&mut transaction.events));
1586 }
1587 let extracted_events = match event_groups.iter().try_fold(0_usize, |count, events| {
1588 count.checked_add(events.len()).ok_or_else(|| {
1589 ConnectorError::Internal(
1590 "PostgreSQL CDC Arrow extraction row-count overflow".into(),
1591 )
1592 })
1593 }) {
1594 Ok(count) => count,
1595 Err(error) => {
1596 self.state = ConnectorState::Failed;
1597 return Err(error);
1598 }
1599 };
1600 if extracted_events != selected_events || event_groups.capacity() != extraction_capacity {
1601 self.state = ConnectorState::Failed;
1602 return Err(ConnectorError::Internal(
1603 "PostgreSQL CDC Arrow extraction changed after capacity preflight".into(),
1604 ));
1605 }
1606 if self.committed_transactions.is_empty() {
1607 self.committed_transactions = VecDeque::new();
1608 }
1609
1610 let batch = match events_to_record_batch(event_groups.into_iter().flatten(), &plan) {
1611 Ok(batch) => batch,
1612 Err(error) => {
1613 self.buffered_event_count -= drained_count;
1614 self.buffered_event_bytes -= drained_bytes;
1615 self.state = ConnectorState::Failed;
1616 return Err(error);
1617 }
1618 };
1619
1620 self.buffered_event_count -= drained_count;
1621 self.buffered_event_bytes -= drained_bytes;
1622 self.polled_lsn = resumable_lsn;
1623 self.metrics.record_batch();
1624 Ok(Some(batch))
1625 }
1626}
1627
1628#[async_trait]
1629impl SourceConnector for PostgresCdcSource {
1630 fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
1631 Some(self.task_tracker.clone())
1632 }
1633
1634 fn recovery_identity_options(
1635 &self,
1636 config: &ConnectorConfig,
1637 ) -> Result<Option<BTreeMap<String, String>>, ConnectorError> {
1638 let mut parsed = if config.properties().is_empty() {
1639 self.config.clone()
1640 } else {
1641 PostgresCdcConfig::from_config(config)?
1642 };
1643 parsed.normalize_table_filters();
1644 parsed.validate()?;
1645
1646 Ok(Some(BTreeMap::from([
1647 ("database".into(), parsed.database),
1648 ("publication".into(), parsed.publication),
1649 ("slot.name".into(), parsed.slot_name),
1650 ("table.exclude".into(), parsed.table_exclude.join(",")),
1651 ("table.include".into(), parsed.table_include.join(",")),
1652 ("wire.protocol".into(), "pgoutput-v1".into()),
1653 ])))
1654 }
1655
1656 async fn start(&mut self, request: SourceStart) -> Result<(), ConnectorError> {
1657 if self.state != ConnectorState::Created {
1658 return Err(ConnectorError::InvalidState {
1659 expected: ConnectorState::Created.to_string(),
1660 actual: self.state.to_string(),
1661 });
1662 }
1663 let (config, position, _) = request.into_parts();
1664
1665 let mut parsed_config = if config.properties().is_empty() {
1669 self.config.clone()
1670 } else {
1671 PostgresCdcConfig::from_config(&config)?
1672 };
1673 parsed_config.normalize_table_filters();
1674 parsed_config.validate()?;
1675
1676 let (start_lsn, expected_binding) = match position {
1677 SourcePosition::Initial => {
1678 return Err(ConnectorError::ConfigurationError(
1679 INITIAL_BOOTSTRAP_NOT_ADMITTED.into(),
1680 ));
1681 }
1682 SourcePosition::Resume {
1683 attempt,
1684 checkpoint,
1685 } => {
1686 let context = format!("checkpoint {attempt:?}");
1687 let binding = validate_checkpoint_identity(&checkpoint, &parsed_config, &context)?;
1688 let lsn_str = checkpoint.get_offset("lsn").ok_or_else(|| {
1689 ConnectorError::ConfigurationError(format!(
1690 "PostgreSQL CDC checkpoint {attempt:?} is missing required 'lsn' offset"
1691 ))
1692 })?;
1693 let lsn = lsn_str.parse::<Lsn>().map_err(|e| {
1694 ConnectorError::ConfigurationError(format!(
1695 "invalid LSN '{lsn_str}' in PostgreSQL CDC checkpoint {attempt:?}: {e}"
1696 ))
1697 })?;
1698 (lsn, binding)
1699 }
1700 };
1701
1702 #[cfg(not(test))]
1703 {
1704 use super::postgres_io;
1705
1706 let control_driver_guard = self.task_owner.track().ok_or_else(|| {
1708 ConnectorError::Internal(
1709 "PostgreSQL CDC connector generation is already retired".into(),
1710 )
1711 })?;
1712 let control = postgres_io::connect(&parsed_config, control_driver_guard).await?;
1713
1714 let slot_inspection = postgres_io::inspect_replication_slot(
1718 control.client(),
1719 &parsed_config.slot_name,
1720 "pgoutput",
1721 &parsed_config.database,
1722 &parsed_config.publication,
1723 expected_binding.source_config_sha256.clone(),
1724 )
1725 .await;
1726 control.close().await;
1727 let slot_lsn = slot_inspection?;
1728
1729 let Some(slot) = slot_lsn.as_ref() else {
1730 return Err(ConnectorError::ConfigurationError(format!(
1731 "cannot resume PostgreSQL CDC slot '{}': the exact durable slot is missing",
1732 parsed_config.slot_name
1733 )));
1734 };
1735 validate_live_binding(&expected_binding, &slot.binding, "resume checkpoint")?;
1736 let Some(resume_slot_lsn) = slot.confirmed_flush_lsn.as_ref() else {
1737 return Err(ConnectorError::ConfigurationError(format!(
1738 "cannot resume PostgreSQL CDC slot '{}': the slot has no retained durable position",
1739 parsed_config.slot_name
1740 )));
1741 };
1742 if resume_slot_lsn.as_u64() > start_lsn.as_u64() {
1743 return Err(ConnectorError::ConfigurationError(format!(
1744 "cannot resume PostgreSQL CDC checkpoint at {}: slot '{}' has already advanced to {}; required WAL may have been reclaimed",
1745 start_lsn, parsed_config.slot_name, resume_slot_lsn
1746 )));
1747 }
1748
1749 let mut repl_config = postgres_io::build_replication_config(&parsed_config);
1751 repl_config.buffer_events = PGWIRE_IN_FLIGHT_EVENTS;
1752 if start_lsn != Lsn::ZERO {
1754 repl_config.start_lsn = pgwire_replication::Lsn::from_u64(start_lsn.as_u64());
1755 }
1756 repl_config.expected_recovery_identity =
1757 Some(pgwire_replication::ExpectedRecoveryIdentity {
1758 system_identifier: expected_binding.system_identifier,
1759 timeline_id: expected_binding.timeline_id,
1760 });
1761
1762 let replication_worker_guard = self.task_owner.track().ok_or_else(|| {
1763 ConnectorError::Internal(
1764 "PostgreSQL CDC connector generation is already retired".into(),
1765 )
1766 })?;
1767 let repl_client = match tokio::time::timeout(
1768 postgres_io::CONNECT_TIMEOUT,
1769 pgwire_replication::ReplicationClient::connect_with_worker_lifetime(
1770 repl_config,
1771 replication_worker_guard,
1772 ),
1773 )
1774 .await
1775 {
1776 Ok(Ok(client)) => client,
1777 Ok(Err(error)) => {
1778 return Err(ConnectorError::ConnectionFailed(format!(
1779 "pgwire-replication connect: {error}"
1780 )));
1781 }
1782 Err(_) => {
1783 return Err(ConnectorError::ConnectionFailed(
1784 "pgwire-replication connect timed out after 10 seconds".into(),
1785 ));
1786 }
1787 };
1788
1789 let raw_wal_byte_limit = parsed_config.raw_wal_bytes();
1791 let (wal_tx, wal_rx) =
1792 crossfire::mpsc::bounded_async::<OwnedWalPayload>(RAW_WAL_QUEUE_CAPACITY);
1793 let wal_byte_budget = Arc::new(Semaphore::new(raw_wal_byte_limit));
1797 let reader_byte_budget = Arc::clone(&wal_byte_budget);
1798 let terminal_error: WalTerminalError = Arc::new(std::sync::Mutex::new(None));
1799 let reader_terminal_error = Arc::clone(&terminal_error);
1800 let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
1801 let (confirmed_lsn_tx, confirmed_lsn_rx) =
1802 tokio::sync::watch::channel(start_lsn.as_u64());
1803 let data_ready = Arc::clone(&self.data_ready);
1804
1805 let reader_guard = self.task_owner.track().ok_or_else(|| {
1806 ConnectorError::Internal(
1807 "PostgreSQL CDC connector generation is already retired".into(),
1808 )
1809 })?;
1810
1811 let reader_handle = tokio::spawn(run_wal_reader(
1812 repl_client,
1813 wal_tx,
1814 reader_byte_budget,
1815 raw_wal_byte_limit,
1816 shutdown_rx,
1817 confirmed_lsn_rx,
1818 reader_terminal_error,
1819 data_ready,
1820 reader_guard,
1821 ));
1822
1823 self.wal_rx = Some(wal_rx);
1824 self.wal_byte_budget = Some(wal_byte_budget);
1825 self.wal_terminal_error = Some(terminal_error);
1826 self.reader_handle = Some(reader_handle);
1827 self.reader_shutdown = Some(shutdown_tx);
1828 self.confirmed_lsn_tx = Some(confirmed_lsn_tx);
1829 }
1830
1831 self.config = parsed_config;
1834 self.confirmed_flush_lsn = start_lsn;
1835 self.write_lsn = start_lsn;
1836 self.polled_lsn = start_lsn;
1837 self.checkpoint_binding = Some(expected_binding);
1838 self.metrics.set_confirmed_flush_lsn(start_lsn.as_u64());
1839 self.state = ConnectorState::Running;
1840 Ok(())
1841 }
1842
1843 async fn poll_batch(
1844 &mut self,
1845 max_records: usize,
1846 ) -> Result<Option<SourceBatch>, ConnectorError> {
1847 if self.state != ConnectorState::Running {
1848 return Err(ConnectorError::InvalidState {
1849 expected: "Running".to_string(),
1850 actual: self.state.to_string(),
1851 });
1852 }
1853
1854 {
1857 self.fail_on_terminal_wal_error()?;
1858 let high_watermark = self.config.decoded_high_watermark_bytes();
1859 let decoded_retained_bytes = self.decoded_retained_bytes().inspect_err(|_error| {
1860 self.state = ConnectorState::Failed;
1861 })?;
1862 let mut reader_closed = false;
1863 let must_finish_transaction = self.current_txn.is_some();
1864 let payload_budget = max_records.max(1);
1865 let drain_reader = must_finish_transaction || decoded_retained_bytes < high_watermark;
1866 if !drain_reader && self.pending_payloads.is_empty() {
1867 tracing::debug!(
1868 retained_bytes = decoded_retained_bytes,
1869 high_watermark,
1870 "CDC backpressure active — pausing WAL reader drain"
1871 );
1872 }
1873
1874 let mut processed_payloads = 0_usize;
1875 while processed_payloads < payload_budget {
1876 let payload = if let Some(payload) = self.pending_payloads.pop_front() {
1877 Some(payload)
1878 } else if drain_reader {
1879 match self.wal_rx.as_mut().map(|receiver| receiver.try_recv()) {
1880 Some(Ok(payload)) => Some(payload),
1881 Some(Err(crossfire::TryRecvError::Empty)) | None => None,
1882 Some(Err(crossfire::TryRecvError::Disconnected)) => {
1883 reader_closed = true;
1884 None
1885 }
1886 }
1887 } else {
1888 None
1889 };
1890 let Some(payload) = payload else {
1891 break;
1892 };
1893 if let Err(e) = self.process_owned_wal_payload(payload) {
1894 self.state = ConnectorState::Failed;
1895 return Err(e);
1896 }
1897 processed_payloads = processed_payloads.checked_add(1).ok_or_else(|| {
1898 self.state = ConnectorState::Failed;
1899 ConnectorError::Internal(
1900 "PostgreSQL CDC poll payload-count accounting overflow".into(),
1901 )
1902 })?;
1903 }
1904
1905 let reached_payload_budget = processed_payloads == payload_budget;
1909 let may_drain_more = if reached_payload_budget && self.current_txn.is_none() {
1910 self.decoded_retained_bytes().inspect_err(|_error| {
1911 self.state = ConnectorState::Failed;
1912 })? < high_watermark
1913 } else {
1914 reached_payload_budget
1915 };
1916 if may_drain_more && !reader_closed {
1917 if let Some(ref mut rx) = self.wal_rx {
1918 match rx.try_recv() {
1919 Ok(payload) => self.pending_payloads.push_back(payload),
1920 Err(crossfire::TryRecvError::Empty) => {}
1921 Err(crossfire::TryRecvError::Disconnected) => reader_closed = true,
1922 }
1923 }
1924 }
1925 if !self.pending_payloads.is_empty() {
1926 self.data_ready.notify_one();
1927 }
1928 self.fail_on_terminal_wal_error()?;
1929 if reader_closed && self.committed_transactions.is_empty() {
1930 self.state = ConnectorState::Failed;
1931 return Err(ConnectorError::ReadError(
1932 "WAL reader task terminated unexpectedly — replication stream lost".to_string(),
1933 ));
1934 }
1935 }
1936
1937 #[cfg(test)]
1938 self.process_pending_messages()?;
1939
1940 let result = match self.drain_events(max_records)? {
1943 Some(batch) => {
1944 self.metrics
1945 .set_confirmed_flush_lsn(self.confirmed_flush_lsn.as_u64());
1946 self.metrics
1947 .set_replication_lag_bytes(self.replication_lag_bytes());
1948
1949 Ok(Some(SourceBatch::new(batch)))
1950 }
1951 None => Ok(None),
1952 };
1953 let emitted_batch = matches!(&result, Ok(Some(_)));
1954 if max_records > 0 && (emitted_batch || !self.committed_transactions.is_empty()) {
1955 self.data_ready.notify_one();
1956 }
1957 result
1958 }
1959
1960 fn schema(&self) -> SchemaRef {
1961 Arc::clone(&self.schema)
1962 }
1963
1964 fn checkpoint(&self) -> SourceCheckpoint {
1965 let mut cp = SourceCheckpoint::new();
1966 cp.set_offset("lsn", self.polled_lsn.to_string());
1971 cp.set_metadata("slot_name", &self.config.slot_name);
1972 cp.set_metadata("publication", &self.config.publication);
1973 cp.set_metadata("database", &self.config.database);
1974 if let Some(binding) = &self.checkpoint_binding {
1975 write_checkpoint_binding(&mut cp, binding);
1976 }
1977 cp
1978 }
1979
1980 async fn notify_epoch_committed(
1981 &mut self,
1982 epoch: u64,
1983 checkpoint: &SourceCheckpoint,
1984 ) -> Result<(), ConnectorError> {
1985 let Some(lsn_str) = checkpoint.get_offset("lsn") else {
1990 return Ok(());
1991 };
1992 let lsn = lsn_str.parse::<Lsn>().map_err(|error| {
1993 ConnectorError::ConfigurationError(format!(
1994 "committed PostgreSQL CDC epoch {epoch} contains invalid LSN '{lsn_str}': {error}"
1995 ))
1996 })?;
1997 let context = format!("committed epoch {epoch} checkpoint");
1998 let committed_binding = validate_checkpoint_identity(checkpoint, &self.config, &context)?;
1999 let active_binding =
2000 self.checkpoint_binding
2001 .as_ref()
2002 .ok_or_else(|| ConnectorError::InvalidState {
2003 expected: "running PostgreSQL CDC checkpoint binding".into(),
2004 actual: "checkpoint binding is missing".into(),
2005 })?;
2006 validate_live_binding(&committed_binding, active_binding, &context)?;
2007 if lsn.as_u64() > self.polled_lsn.as_u64() {
2008 return Err(ConnectorError::ConfigurationError(format!(
2009 "committed PostgreSQL CDC epoch {epoch} LSN {lsn} is ahead of the source's polled LSN {}; refusing irreversible slot feedback",
2010 self.polled_lsn
2011 )));
2012 }
2013 if lsn.as_u64() < self.confirmed_flush_lsn.as_u64() {
2017 return Ok(());
2018 }
2019
2020 let tx = self
2021 .confirmed_lsn_tx
2022 .as_ref()
2023 .ok_or_else(|| ConnectorError::InvalidState {
2024 expected: "running PostgreSQL CDC confirmed-LSN feedback channel".into(),
2025 actual: "feedback channel is missing".into(),
2026 })?;
2027 tx.send(lsn.as_u64()).map_err(|_| {
2028 ConnectorError::ConnectionFailed(
2029 "PostgreSQL CDC confirmed-LSN feedback channel is closed".into(),
2030 )
2031 })?;
2032 self.confirmed_flush_lsn = lsn;
2034 self.metrics.set_confirmed_flush_lsn(lsn.as_u64());
2035 Ok(())
2036 }
2037
2038 fn contract(&self, config: &ConnectorConfig) -> Result<SourceContract, ConnectorError> {
2039 if config.properties().is_empty() {
2043 self.config.validate()?;
2044 } else {
2045 PostgresCdcConfig::from_config(config)?.validate()?;
2046 }
2047 Ok(SourceContract::new(
2048 SourceConsistency::CommitCoupled,
2049 SourceTopology::Singleton,
2050 ))
2051 }
2052
2053 fn data_ready_notify(&self) -> Option<Arc<Notify>> {
2054 Some(Arc::clone(&self.data_ready))
2055 }
2056
2057 async fn close(&mut self) -> Result<(), ConnectorError> {
2058 if let Some(tx) = self.reader_shutdown.as_ref() {
2061 tx.send_replace(true);
2062 }
2063 let detach_reader = if let Some(handle) = self.reader_handle.as_mut() {
2064 tokio::time::timeout(std::time::Duration::from_secs(5), &mut *handle)
2065 .await
2066 .is_err()
2067 } else {
2068 false
2069 };
2070 if detach_reader {
2071 tracing::warn!(
2072 "PostgreSQL CDC reader did not stop before the close deadline; its tracked reaper retains shutdown ownership"
2073 );
2074 let handle = self
2075 .reader_handle
2076 .take()
2077 .expect("reader handle was present while awaiting it");
2078 reap_postgres_reader(handle, &self.task_owner);
2079 }
2080 self.reader_handle = None;
2081 self.reader_shutdown = None;
2082 self.wal_rx = None;
2083 self.confirmed_lsn_tx = None;
2084 self.pending_payloads.clear();
2085 self.wal_byte_budget = None;
2086 self.wal_terminal_error = None;
2087
2088 self.state = ConnectorState::Closed;
2089 self.committed_transactions.clear();
2090 self.current_txn = None;
2091 self.relation_cache.clear();
2092 self.buffered_event_count = 0;
2093 self.buffered_event_bytes = 0;
2094 #[cfg(test)]
2095 self.pending_messages.clear();
2096 Ok(())
2097 }
2098}
2099
2100#[cfg(test)]
2103impl PostgresCdcSource {
2104 fn inject_event(&mut self, event: ChangeEvent) {
2106 let end_lsn = event.lsn;
2107 self.buffered_event_bytes = self
2108 .buffered_event_bytes
2109 .checked_add(retained_event_bytes(&event).expect("test event size must be valid"))
2110 .expect("test buffered-event bytes must be valid");
2111 self.committed_transactions.push_back(CommittedTransaction {
2112 end_lsn,
2113 events: VecDeque::from([event]),
2114 });
2115 self.buffered_event_count += 1;
2116 }
2117
2118 fn build_relation_message(
2120 relation_id: u32,
2121 namespace: &str,
2122 name: &str,
2123 columns: &[(u8, &str, u32, i32)], ) -> Vec<u8> {
2125 let mut buf = vec![b'R'];
2126 buf.extend_from_slice(&relation_id.to_be_bytes());
2127 buf.extend_from_slice(namespace.as_bytes());
2128 buf.push(0);
2129 buf.extend_from_slice(name.as_bytes());
2130 buf.push(0);
2131 buf.push(b'd'); buf.extend_from_slice(&(columns.len() as i16).to_be_bytes());
2133 for (flags, col_name, oid, modifier) in columns {
2134 buf.push(*flags);
2135 buf.extend_from_slice(col_name.as_bytes());
2136 buf.push(0);
2137 buf.extend_from_slice(&oid.to_be_bytes());
2138 buf.extend_from_slice(&modifier.to_be_bytes());
2139 }
2140 buf
2141 }
2142
2143 fn build_begin_message(final_lsn: u64, commit_ts_us: i64, xid: u32) -> Vec<u8> {
2145 let mut buf = vec![b'B'];
2146 buf.extend_from_slice(&final_lsn.to_be_bytes());
2147 buf.extend_from_slice(&commit_ts_us.to_be_bytes());
2148 buf.extend_from_slice(&xid.to_be_bytes());
2149 buf
2150 }
2151
2152 fn build_commit_message(commit_lsn: u64, end_lsn: u64, commit_ts_us: i64) -> Vec<u8> {
2154 let mut buf = vec![b'C'];
2155 buf.push(0); buf.extend_from_slice(&commit_lsn.to_be_bytes());
2157 buf.extend_from_slice(&end_lsn.to_be_bytes());
2158 buf.extend_from_slice(&commit_ts_us.to_be_bytes());
2159 buf
2160 }
2161
2162 fn build_insert_message(relation_id: u32, values: &[Option<&str>]) -> Vec<u8> {
2164 let mut buf = vec![b'I'];
2165 buf.extend_from_slice(&relation_id.to_be_bytes());
2166 buf.push(b'N');
2167 buf.extend_from_slice(&(values.len() as i16).to_be_bytes());
2168 for val in values {
2169 match val {
2170 Some(s) => {
2171 buf.push(b't');
2172 buf.extend_from_slice(&(s.len() as i32).to_be_bytes());
2173 buf.extend_from_slice(s.as_bytes());
2174 }
2175 None => buf.push(b'n'),
2176 }
2177 }
2178 buf
2179 }
2180
2181 fn build_delete_message(relation_id: u32, values: &[Option<&str>]) -> Vec<u8> {
2183 let mut buf = vec![b'D'];
2184 buf.extend_from_slice(&relation_id.to_be_bytes());
2185 buf.push(b'K'); buf.extend_from_slice(&(values.len() as i16).to_be_bytes());
2187 for val in values {
2188 match val {
2189 Some(s) => {
2190 buf.push(b't');
2191 buf.extend_from_slice(&(s.len() as i32).to_be_bytes());
2192 buf.extend_from_slice(s.as_bytes());
2193 }
2194 None => buf.push(b'n'),
2195 }
2196 }
2197 buf
2198 }
2199
2200 fn build_truncate_message(relation_ids: &[u32], options: u8) -> Vec<u8> {
2202 let mut buf = vec![b'T'];
2203 buf.extend_from_slice(&(relation_ids.len() as i32).to_be_bytes());
2204 buf.push(options);
2205 for id in relation_ids {
2206 buf.extend_from_slice(&id.to_be_bytes());
2207 }
2208 buf
2209 }
2210
2211 fn build_update_message(
2213 relation_id: u32,
2214 old_tuple_tag: u8,
2215 old_values: &[Option<&str>],
2216 new_values: &[Option<&str>],
2217 ) -> Vec<u8> {
2218 assert!(matches!(old_tuple_tag, b'K' | b'O'));
2219 let mut buf = vec![b'U'];
2220 buf.extend_from_slice(&relation_id.to_be_bytes());
2221 buf.push(old_tuple_tag);
2222 buf.extend_from_slice(&(old_values.len() as i16).to_be_bytes());
2223 for val in old_values {
2224 match val {
2225 Some(s) => {
2226 buf.push(b't');
2227 buf.extend_from_slice(&(s.len() as i32).to_be_bytes());
2228 buf.extend_from_slice(s.as_bytes());
2229 }
2230 None => buf.push(b'n'),
2231 }
2232 }
2233 buf.push(b'N');
2235 buf.extend_from_slice(&(new_values.len() as i16).to_be_bytes());
2236 for val in new_values {
2237 match val {
2238 Some(s) => {
2239 buf.push(b't');
2240 buf.extend_from_slice(&(s.len() as i32).to_be_bytes());
2241 buf.extend_from_slice(s.as_bytes());
2242 }
2243 None => buf.push(b'n'),
2244 }
2245 }
2246 buf
2247 }
2248}
2249
2250#[cfg(test)]
2251mod tests;