1use anyhow::{anyhow, Result};
16use chrono::Utc;
17use log::{debug, error, info, trace, warn};
18use std::collections::HashMap;
19use std::sync::atomic::Ordering;
20use std::sync::Arc;
21use std::time::Duration;
22use tokio::sync::{oneshot, RwLock};
23use tokio::time::{interval, sleep};
24
25use super::connection::{parse_lsn, ReplicationConnection};
26use super::decoder::PgOutputDecoder;
27use super::protocol::BackendMessage;
28use super::types::{StandbyStatusUpdate, WalMessage};
29use super::{PostgresSourceConfig, ReplayState};
30use drasi_core::models::{Element, ElementMetadata, ElementReference, SourceChange};
31use drasi_lib::channels::{ComponentStatus, SourceEvent, SourceEventWrapper};
32use drasi_lib::component_graph::ComponentStatusHandle;
33use drasi_lib::sources::base::SourceBase;
34
35pub struct ReplicationStream {
36 config: PostgresSourceConfig,
37 source_id: String,
38 connection: Option<ReplicationConnection>,
39 decoder: PgOutputDecoder,
40 #[allow(dead_code)]
41 status_handle: ComponentStatusHandle,
42 base: SourceBase,
43 replay_state: Arc<ReplayState>,
44 read_lsn: u64,
45 start_lsn: Option<u64>,
46 last_feedback_time: std::time::Instant,
47 pending_transaction: Option<Vec<(SourceChange, u64)>>,
48 relations: HashMap<u32, RelationMapping>,
49 table_primary_keys: Arc<RwLock<HashMap<String, Vec<String>>>>,
50}
51
52struct RelationMapping {
53 #[allow(dead_code)]
54 table_name: String,
55 #[allow(dead_code)]
56 schema_name: String,
57 label: String,
58}
59
60impl ReplicationStream {
61 pub(crate) fn new(
62 config: PostgresSourceConfig,
63 source_id: String,
64 status_handle: ComponentStatusHandle,
65 base: SourceBase,
66 replay_state: Arc<ReplayState>,
67 start_lsn: Option<u64>,
68 ) -> Self {
69 Self {
70 config,
71 source_id,
72 connection: None,
73 decoder: PgOutputDecoder::new(),
74 status_handle,
75 base,
76 replay_state,
77 read_lsn: 0,
78 start_lsn,
79 last_feedback_time: std::time::Instant::now(),
80 pending_transaction: None,
81 relations: HashMap::new(),
82 table_primary_keys: Arc::new(RwLock::new(HashMap::new())),
83 }
84 }
85
86 pub async fn run(
91 &mut self,
92 startup_tx: Option<oneshot::Sender<std::result::Result<(), String>>>,
93 ) -> Result<()> {
94 info!("Starting replication stream for source {}", self.source_id);
95
96 if let Err(error) = self.connect_and_setup().await {
98 if let Some(tx) = startup_tx {
99 let _ = tx.send(Err(format!("{error:#}")));
100 }
101 return Err(error);
102 }
103 if let Some(tx) = startup_tx {
104 let _ = tx.send(Ok(()));
105 }
106
107 let mut keepalive_interval = interval(Duration::from_secs(10));
109
110 loop {
111 {
113 let status = self.status_handle.get_status().await;
114 if status == ComponentStatus::Stopping || status == ComponentStatus::Stopped {
115 info!("Received stop signal, shutting down replication");
116 break;
117 }
118 }
119
120 tokio::select! {
121 result = self.read_next_message() => {
123 match result {
124 Ok(Some(msg)) => {
125 if let Err(e) = self.handle_message(msg).await {
126 error!("Error handling message: {e}");
127 if let Err(e) = self.recover_connection().await {
129 error!("Failed to recover connection: {e}");
130 return Err(e);
131 }
132 }
133 }
134 Ok(None) => {
135 }
137 Err(e) => {
138 error!("Error reading message: {e}");
139 if let Err(e) = self.recover_connection().await {
141 error!("Failed to recover connection: {e}");
142 return Err(e);
143 }
144 }
145 }
146 }
147
148 _ = keepalive_interval.tick() => {
150 if let Err(e) = self.send_feedback(false).await {
151 warn!("Failed to send keepalive: {e}");
152 }
153 }
154 }
155 }
156
157 self.shutdown().await?;
159 Ok(())
160 }
161
162 async fn connect_and_setup(&mut self) -> Result<()> {
163 info!("Connecting to PostgreSQL for replication");
164
165 let mut conn = ReplicationConnection::connect(
167 &self.config.host,
168 self.config.port,
169 &self.config.database,
170 &self.config.user,
171 &self.config.password,
172 )
173 .await?;
174
175 let system_info = conn.identify_system().await?;
177 info!("Connected to PostgreSQL system: {system_info:?}");
178
179 let slot_info = conn
181 .create_replication_slot(&self.config.slot_name, false)
182 .await?;
183 info!("Using replication slot: {slot_info:?}");
184
185 let slot_lsn =
188 if !slot_info.consistent_point.is_empty() && slot_info.consistent_point != "0/0" {
189 parse_lsn(&slot_info.consistent_point)?
190 } else {
191 0
192 };
193 self.read_lsn = self.start_lsn.unwrap_or(slot_lsn);
194 self.replay_state
195 .read_lsn
196 .store(self.read_lsn, Ordering::Release);
197
198 let mut options = HashMap::new();
200 options.insert("proto_version".to_string(), "1".to_string());
201 options.insert(
202 "publication_names".to_string(),
203 self.config.publication_name.clone(),
204 );
205
206 conn.start_replication(&self.config.slot_name, Some(self.read_lsn), options)
208 .await?;
209
210 self.connection = Some(conn);
211 info!(
212 "Replication started from read LSN {:x} (slot watermark {:x})",
213 self.read_lsn, slot_lsn
214 );
215
216 Ok(())
217 }
218
219 async fn read_next_message(&mut self) -> Result<Option<BackendMessage>> {
220 if let Some(conn) = &mut self.connection {
221 match tokio::time::timeout(Duration::from_millis(100), conn.read_replication_message())
223 .await
224 {
225 Ok(Ok(msg)) => Ok(Some(msg)),
226 Ok(Err(e)) => Err(e),
227 Err(_) => Ok(None), }
229 } else {
230 Err(anyhow!("No connection available"))
231 }
232 }
233
234 async fn handle_message(&mut self, msg: BackendMessage) -> Result<()> {
235 match msg {
236 BackendMessage::CopyData(data) => {
237 self.handle_copy_data(&data).await?;
238 }
239 BackendMessage::PrimaryKeepaliveMessage {
240 wal_end,
241 timestamp: _,
242 reply,
243 } => {
244 self.read_lsn = wal_end;
245 self.replay_state
246 .read_lsn
247 .store(self.read_lsn, Ordering::Release);
248 if reply == 1 {
249 self.send_feedback(true).await?;
250 }
251 }
252 BackendMessage::ErrorResponse(err) => {
253 error!("Server error: {}", err.message);
254 return Err(anyhow!("Server error: {}", err.message));
255 }
256 _ => {
257 trace!("Ignoring message: {msg:?}");
258 }
259 }
260 Ok(())
261 }
262
263 async fn handle_copy_data(&mut self, data: &[u8]) -> Result<()> {
264 if data.is_empty() {
265 return Ok(());
266 }
267
268 let msg_type = data[0];
270
271 match msg_type {
272 b'w' => {
273 self.handle_xlog_data(&data[1..]).await?;
275 }
276 b'k' => {
277 self.handle_keepalive(&data[1..]).await?;
279 }
280 _ => {
281 warn!("Unknown copy data message type: 0x{msg_type:02x}");
282 }
283 }
284
285 Ok(())
286 }
287
288 async fn handle_xlog_data(&mut self, data: &[u8]) -> Result<()> {
289 if data.len() < 24 {
290 return Err(anyhow!("XLogData message too short: {} bytes", data.len()));
291 }
292
293 let _start_lsn = u64::from_be_bytes([
295 data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
296 ]);
297 let end_lsn = u64::from_be_bytes([
298 data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15],
299 ]);
300 let _timestamp = i64::from_be_bytes([
301 data[16], data[17], data[18], data[19], data[20], data[21], data[22], data[23],
302 ]);
303
304 self.read_lsn = end_lsn;
306 self.replay_state
307 .read_lsn
308 .store(self.read_lsn, Ordering::Release);
309
310 let wal_data = &data[24..];
312
313 if !wal_data.is_empty() {
315 let msg_type = wal_data[0];
316 debug!(
317 "Attempting to decode WAL message type: {} ({}), data length: {}",
318 msg_type as char,
319 msg_type,
320 wal_data.len()
321 );
322 }
323
324 match self.decoder.decode_message(wal_data) {
325 Ok(Some(wal_msg)) => {
326 self.process_wal_message(wal_msg).await?;
327 }
328 Ok(None) => {
329 }
331 Err(e) => {
332 if !wal_data.is_empty() {
334 debug!(
335 "Failed to decode WAL message type {} ({}): {}, data length: {}",
336 wal_data[0] as char,
337 wal_data[0],
338 e,
339 wal_data.len()
340 );
341 }
342 }
344 }
345
346 if self.last_feedback_time.elapsed() > Duration::from_secs(5) {
348 self.send_feedback(false).await?;
349 }
350
351 Ok(())
352 }
353
354 async fn handle_keepalive(&mut self, data: &[u8]) -> Result<()> {
355 if data.len() < 17 {
356 return Err(anyhow!("Keepalive message too short"));
357 }
358
359 let wal_end = u64::from_be_bytes([
360 data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
361 ]);
362 let reply = data[16];
363
364 self.read_lsn = wal_end;
365 self.replay_state
366 .read_lsn
367 .store(self.read_lsn, Ordering::Release);
368
369 if reply == 1 {
370 self.send_feedback(true).await?;
371 }
372
373 Ok(())
374 }
375
376 async fn process_wal_message(&mut self, msg: WalMessage) -> Result<()> {
377 match msg {
378 WalMessage::Begin(_) => {
379 self.pending_transaction = Some(Vec::new());
381 }
382 WalMessage::Commit(tx_info) => {
383 if let Some(changes) = self.pending_transaction.take() {
386 for (change, _) in changes {
387 self.dispatch_change(change, tx_info.commit_lsn).await;
388 }
389 debug!(
390 "Committed transaction {} with LSN {:x}",
391 tx_info.xid, tx_info.commit_lsn
392 );
393 }
394 }
395 WalMessage::Relation(relation) => {
396 let label = relation.name.clone();
399 self.relations.insert(
400 relation.id,
401 RelationMapping {
402 table_name: relation.name.clone(),
403 schema_name: relation.namespace.clone(),
404 label,
405 },
406 );
407
408 }
411 WalMessage::Insert { relation_id, tuple } => {
412 if let Some(change) = self.convert_insert(relation_id, tuple).await? {
413 if let Some(tx) = &mut self.pending_transaction {
414 tx.push((change, self.read_lsn));
415 } else {
416 self.dispatch_change(change, self.read_lsn).await;
417 }
418 }
419 }
420 WalMessage::Update {
421 relation_id,
422 old_tuple,
423 new_tuple,
424 } => {
425 if let Some(change) = self
426 .convert_update(relation_id, old_tuple, new_tuple)
427 .await?
428 {
429 if let Some(tx) = &mut self.pending_transaction {
430 tx.push((change, self.read_lsn));
431 } else {
432 self.dispatch_change(change, self.read_lsn).await;
433 }
434 }
435 }
436 WalMessage::Delete {
437 relation_id,
438 old_tuple,
439 } => {
440 if let Some(change) = self.convert_delete(relation_id, old_tuple).await? {
441 if let Some(tx) = &mut self.pending_transaction {
442 tx.push((change, self.read_lsn));
443 } else {
444 self.dispatch_change(change, self.read_lsn).await;
445 }
446 }
447 }
448 WalMessage::Truncate { relation_ids } => {
449 warn!("Truncate not yet implemented for relations: {relation_ids:?}");
450 }
451 }
452 Ok(())
453 }
454
455 async fn convert_insert(
456 &self,
457 relation_id: u32,
458 tuple: Vec<super::types::PostgresValue>,
459 ) -> Result<Option<SourceChange>> {
460 let relation = self
462 .decoder
463 .get_relation(relation_id)
464 .ok_or_else(|| anyhow!("Unknown relation {relation_id}"))?;
465
466 let mapping = self
467 .relations
468 .get(&relation_id)
469 .ok_or_else(|| anyhow!("No mapping for relation {relation_id}"))?;
470
471 let mut properties = drasi_core::models::ElementPropertyMap::new();
473 for (i, value) in tuple.iter().enumerate() {
474 if let Some(column) = relation.columns.get(i) {
475 let json_value = value.to_json();
476 if !json_value.is_null() {
477 properties.insert(
478 &column.name,
479 drasi_lib::sources::manager::convert_json_to_element_value(&json_value),
480 );
481 }
482 }
483 }
484
485 let element_id = self.generate_element_id(relation, &tuple).await?;
487
488 let element = Element::Node {
490 metadata: ElementMetadata {
491 reference: ElementReference::new(&self.source_id, &element_id),
492 labels: Arc::from([Arc::from(mapping.label.as_str())]),
493 effective_from: Utc::now().timestamp_millis() as u64,
494 },
495 properties,
496 };
497
498 Ok(Some(SourceChange::Insert { element }))
499 }
500
501 async fn convert_update(
502 &self,
503 relation_id: u32,
504 old_tuple: Option<Vec<super::types::PostgresValue>>,
505 new_tuple: Vec<super::types::PostgresValue>,
506 ) -> Result<Option<SourceChange>> {
507 let relation = self
508 .decoder
509 .get_relation(relation_id)
510 .ok_or_else(|| anyhow!("Unknown relation {relation_id}"))?;
511
512 let mapping = self
513 .relations
514 .get(&relation_id)
515 .ok_or_else(|| anyhow!("No mapping for relation {relation_id}"))?;
516
517 let element_id = self.generate_element_id(relation, &new_tuple).await?;
519
520 if old_tuple.is_none() {
521 warn!("UPDATE without old tuple for relation {relation_id}, preserving UPDATE");
522 }
523
524 let mut after_properties = drasi_core::models::ElementPropertyMap::new();
527
528 for (i, column) in relation.columns.iter().enumerate() {
530 if let Some(value) = new_tuple.get(i) {
531 let json_value = value.to_json();
532 if !json_value.is_null() {
533 after_properties.insert(
534 &column.name,
535 drasi_lib::sources::manager::convert_json_to_element_value(&json_value),
536 );
537 }
538 }
539 }
540
541 let after_element = Element::Node {
542 metadata: ElementMetadata {
543 reference: ElementReference::new(&self.source_id, &element_id),
544 labels: Arc::from([Arc::from(mapping.label.as_str())]),
545 effective_from: Utc::now().timestamp_millis() as u64,
546 },
547 properties: after_properties,
548 };
549
550 Ok(Some(SourceChange::Update {
551 element: after_element,
552 }))
553 }
554
555 async fn convert_delete(
556 &self,
557 relation_id: u32,
558 old_tuple: Vec<super::types::PostgresValue>,
559 ) -> Result<Option<SourceChange>> {
560 let relation = self
561 .decoder
562 .get_relation(relation_id)
563 .ok_or_else(|| anyhow!("Unknown relation {relation_id}"))?;
564
565 let mapping = self
566 .relations
567 .get(&relation_id)
568 .ok_or_else(|| anyhow!("No mapping for relation {relation_id}"))?;
569
570 let element_id = self.generate_element_id(relation, &old_tuple).await?;
571
572 Ok(Some(SourceChange::Delete {
573 metadata: ElementMetadata {
574 reference: ElementReference::new(&self.source_id, &element_id),
575 labels: Arc::from([Arc::from(mapping.label.as_str())]),
576 effective_from: Utc::now().timestamp_millis() as u64,
577 },
578 }))
579 }
580
581 async fn generate_element_id(
593 &self,
594 relation: &super::types::RelationInfo,
595 tuple: &[super::types::PostgresValue],
596 ) -> Result<String> {
597 let table_name = if relation.namespace == "public" {
599 relation.name.clone()
600 } else {
601 format!("{}.{}", relation.namespace, relation.name)
602 };
603
604 let primary_keys = self.table_primary_keys.read().await;
606 let pk_columns = primary_keys.get(&table_name);
607
608 let configured_keys = self
610 .config
611 .table_keys
612 .iter()
613 .find(|tk| tk.table == table_name)
614 .map(|tk| &tk.key_columns);
615
616 let key_columns = configured_keys.or(pk_columns);
618
619 if let Some(keys) = key_columns {
620 let mut key_parts = Vec::new();
621
622 for (i, column) in relation.columns.iter().enumerate() {
623 if keys.contains(&column.name) {
624 if let Some(value) = tuple.get(i) {
625 let json_val = value.to_json();
626 if !json_val.is_null() {
627 let val_str = json_val.to_string();
629 let cleaned = val_str.trim_matches('"');
630 key_parts.push(cleaned.to_string());
631 }
632 }
633 }
634 }
635
636 if !key_parts.is_empty() {
637 return Ok(format!("{}:{}", table_name, key_parts.join("_")));
639 }
640 }
641
642 warn!("No primary key value found for table '{table_name}'. Consider adding 'table_keys' configuration.");
644 Ok(format!("{}:{}", table_name, uuid::Uuid::new_v4()))
646 }
647
648 async fn send_feedback(&mut self, reply_requested: bool) -> Result<()> {
649 if let Some(conn) = &mut self.connection {
650 let confirmed_lsn = match self.base.compute_confirmed_source_position().await {
657 Some(bytes) if bytes.len() == 8 => {
658 let arr: [u8; 8] = bytes[..8].try_into().expect("length already checked");
659 u64::from_be_bytes(arr)
660 }
661 Some(bytes) => {
662 warn!(
663 "[{}] Confirmed source position has unexpected length {} (expected 8); \
664 not advancing flush_lsn",
665 self.source_id,
666 bytes.len()
667 );
668 0
669 }
670 None => 0, };
672
673 let fence = self.replay_state.effective_flush_fence();
677 let (effective_lsn, was_clamped) = if fence < u64::MAX && confirmed_lsn > fence {
678 (fence, true)
679 } else {
680 (confirmed_lsn, false)
681 };
682
683 let status = StandbyStatusUpdate {
684 write_lsn: self.read_lsn,
685 flush_lsn: effective_lsn,
686 apply_lsn: effective_lsn,
687 reply_requested,
688 };
689
690 conn.send_standby_status(status).await?;
691 self.last_feedback_time = std::time::Instant::now();
692
693 if !was_clamped && effective_lsn > 0 {
699 if let Some(confirmed_seq) = self.base.compute_confirmed_position().await {
700 self.base.prune_position_map(confirmed_seq).await;
701 }
702 }
703
704 trace!(
705 "[{}] Sent feedback: write_lsn={:x}, flush_lsn={:x}{}",
706 self.source_id,
707 self.read_lsn,
708 effective_lsn,
709 if was_clamped { " (fenced)" } else { "" }
710 );
711 }
712
713 Ok(())
714 }
715
716 async fn dispatch_change(&self, change: SourceChange, lsn: u64) {
721 let mut profiling = drasi_lib::profiling::ProfilingMetadata::new();
722 profiling.source_send_ns = Some(drasi_lib::profiling::timestamp_ns());
723
724 let mut wrapper = SourceEventWrapper::with_profiling(
725 self.source_id.clone(),
726 SourceEvent::Change(change),
727 chrono::Utc::now(),
728 profiling,
729 );
730
731 wrapper.set_source_position(super::connection::lsn_to_position_bytes(lsn));
733
734 if let Err(e) = self.base.dispatch_event(wrapper).await {
736 debug!(
737 "[{}] Failed to dispatch change (no subscribers): {}",
738 self.source_id, e
739 );
740 }
741 }
742
743 #[allow(dead_code)]
744 async fn check_stop_signal(&self) -> bool {
745 let status = self.status_handle.get_status().await;
746 status == ComponentStatus::Stopping || status == ComponentStatus::Stopped
747 }
748
749 async fn recover_connection(&mut self) -> Result<()> {
750 warn!("Attempting to recover connection");
751
752 if let Some(conn) = self.connection.take() {
754 let _ = conn.close().await;
755 }
756
757 sleep(Duration::from_secs(5)).await;
759
760 self.connect_and_setup().await?;
762
763 info!("Connection recovered successfully");
764 Ok(())
765 }
766
767 async fn shutdown(&mut self) -> Result<()> {
768 info!("Shutting down replication stream");
769
770 let _ = self.send_feedback(false).await;
772
773 if let Some(conn) = self.connection.take() {
775 conn.close().await?;
776 }
777
778 Ok(())
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use chrono::Utc;
785 use drasi_core::models::validate_effective_from;
786
787 #[test]
790 fn effective_from_uses_milliseconds() {
791 let effective_from = Utc::now().timestamp_millis() as u64;
792 assert!(
793 validate_effective_from(effective_from).is_ok(),
794 "Postgres CDC effective_from ({effective_from}) should be in millisecond range"
795 );
796 }
797
798 #[test]
800 fn effective_from_rejects_nanoseconds_pattern() {
801 let bad_effective_from = Utc::now().timestamp_nanos_opt().unwrap() as u64;
802 assert!(
803 validate_effective_from(bad_effective_from).is_err(),
804 "Nanosecond timestamp ({bad_effective_from}) should be rejected"
805 );
806 }
807}