1use std::collections::BTreeMap;
16use std::marker::PhantomData;
17use std::ops::Deref;
18use std::str::FromStr;
19use std::sync::Arc;
20
21use anyhow::{Context, anyhow};
22use async_trait::async_trait;
23use itertools::Itertools;
24use mysql_async::Row;
25use mysql_async::prelude::*;
26use prost::Message;
27use risingwave_common::global_jvm::Jvm;
28use risingwave_common::id::SourceId;
29use risingwave_common::metrics::{LabelGuardedIntGauge, LabelGuardedIntGaugeVec};
30use risingwave_common::util::addr::HostAddr;
31use risingwave_jni_core::call_static_method;
32use risingwave_jni_core::jvm_runtime::execute_with_jni_env;
33use risingwave_pb::connector_service::{SourceType, ValidateSourceRequest, ValidateSourceResponse};
34use tiberius::Config;
35use tokio_postgres::types::PgLsn;
36
37use crate::connector_common::{SslMode, create_pg_client, pg_connection_config_from_properties};
38use crate::error::ConnectorResult;
39use crate::sink::sqlserver::SqlServerClient;
40use crate::source::cdc::external::mysql::build_mysql_connection_pool;
41use crate::source::cdc::split::{extract_binlog_file_seq, parse_sql_server_lsn_str};
42use crate::source::cdc::{
43 CdcProperties, CdcSourceTypeTrait, Citus, DebeziumCdcSplit, Mongodb, Mysql, Postgres,
44 SqlServer, table_schema_exclude_additional_columns,
45};
46use crate::source::monitor::metrics::EnumeratorMetrics;
47use crate::source::{SourceEnumeratorContextRef, SplitEnumerator};
48
49pub const DATABASE_SERVERS_KEY: &str = "database.servers";
50
51#[derive(Debug)]
52pub struct DebeziumSplitEnumerator<T: CdcSourceTypeTrait> {
53 source_id: SourceId,
55 worker_node_addrs: Vec<HostAddr>,
56 metrics: Arc<EnumeratorMetrics>,
57 pg_cdc_upstream_max_lsn: Option<LabelGuardedIntGauge>,
58 pg_cdc_confirmed_flush_lsn: Option<LabelGuardedIntGauge>,
59 mysql_cdc_binlog_file_seq_min: Option<LabelGuardedIntGauge>,
60 mysql_cdc_binlog_file_seq_max: Option<LabelGuardedIntGauge>,
61 sqlserver_cdc_upstream_min_lsn: Option<LabelGuardedIntGauge>,
62 sqlserver_cdc_upstream_max_lsn: Option<LabelGuardedIntGauge>,
63 properties: Arc<BTreeMap<String, String>>,
65 _phantom: PhantomData<T>,
66}
67
68fn get_or_create_guarded_int_gauge<'a>(
69 metric: &'a mut Option<LabelGuardedIntGauge>,
70 metric_vec: &LabelGuardedIntGaugeVec,
71 labels: &[String],
72) -> &'a LabelGuardedIntGauge {
73 metric.get_or_insert_with(|| metric_vec.with_guarded_label_values(labels))
74}
75
76#[async_trait]
77impl<T: CdcSourceTypeTrait> SplitEnumerator for DebeziumSplitEnumerator<T>
78where
79 Self: ListCdcSplits<CdcSourceType = T> + CdcMonitor,
80{
81 type Properties = CdcProperties<T>;
82 type Split = DebeziumCdcSplit<T>;
83
84 async fn new(
85 props: CdcProperties<T>,
86 context: SourceEnumeratorContextRef,
87 ) -> ConnectorResult<Self> {
88 let server_addrs = props
89 .properties
90 .get(DATABASE_SERVERS_KEY)
91 .map(|s| {
92 s.split(',')
93 .map(HostAddr::from_str)
94 .collect::<Result<Vec<_>, _>>()
95 })
96 .transpose()?
97 .unwrap_or_default();
98
99 assert_eq!(
100 props.get_source_type_pb(),
101 SourceType::from(T::source_type())
102 );
103
104 let jvm = Jvm::get_or_init()?;
105 let source_id = context.info.source_id;
106
107 let source_type_pb = props.get_source_type_pb();
109
110 let properties_arc = Arc::new(props.properties);
112 let properties_arc_for_validation = properties_arc.clone();
113 let table_schema_for_validation = props.table_schema;
114
115 tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
116 execute_with_jni_env(jvm, |env| {
117 let validate_source_request = ValidateSourceRequest {
118 source_id: source_id.as_raw_id() as u64,
119 source_type: source_type_pb as _,
120 properties: (*properties_arc_for_validation).clone(),
121 table_schema: Some(table_schema_exclude_additional_columns(
122 &table_schema_for_validation,
123 )),
124 is_source_job: props.is_cdc_source_job,
125 is_backfill_table: props.is_backfill_table,
126 };
127
128 let validate_source_request_bytes =
129 env.byte_array_from_slice(&Message::encode_to_vec(&validate_source_request))?;
130
131 let validate_source_response_bytes = call_static_method!(
132 env,
133 {com.risingwave.connector.source.JniSourceValidateHandler},
134 {byte[] validate(byte[] validateSourceRequestBytes)},
135 &validate_source_request_bytes
136 )?;
137
138 let validate_source_response: ValidateSourceResponse = Message::decode(
139 risingwave_jni_core::to_guarded_slice(&validate_source_response_bytes, env)?
140 .deref(),
141 )?;
142
143 if let Some(error) = validate_source_response.error {
144 return Err(
145 anyhow!(error.error_message).context("source cannot pass validation")
146 );
147 }
148
149 Ok(())
150 })
151 })
152 .await
153 .context("failed to validate source")??;
154
155 tracing::debug!("validate cdc source properties success");
156 Ok(Self {
157 source_id,
158 worker_node_addrs: server_addrs,
159 metrics: context.metrics.clone(),
160 pg_cdc_upstream_max_lsn: None,
161 pg_cdc_confirmed_flush_lsn: None,
162 mysql_cdc_binlog_file_seq_min: None,
163 mysql_cdc_binlog_file_seq_max: None,
164 sqlserver_cdc_upstream_min_lsn: None,
165 sqlserver_cdc_upstream_max_lsn: None,
166 properties: properties_arc,
167 _phantom: PhantomData,
168 })
169 }
170
171 async fn list_splits(&mut self) -> ConnectorResult<Vec<DebeziumCdcSplit<T>>> {
172 Ok(self.list_cdc_splits())
173 }
174
175 async fn on_tick(&mut self) -> ConnectorResult<()> {
176 self.monitor_cdc().await
177 }
178}
179
180impl<T: CdcSourceTypeTrait> DebeziumSplitEnumerator<T> {
181 fn sql_server_lsn_to_i64(lsn: &str) -> Option<i64> {
182 parse_sql_server_lsn_str(lsn).map(|v| v.min(i64::MAX as u128) as i64)
183 }
184
185 fn pg_cdc_lsn_metric_labels(&self, slot_name: &str) -> Vec<String> {
186 vec![self.source_id.to_string(), slot_name.to_owned()]
187 }
188
189 async fn monitor_postgres_confirmed_flush_lsn(&mut self) -> ConnectorResult<()> {
190 let lsns = self.query_postgres_lsns().await.with_context(|| {
192 format!(
193 "failed to query PostgreSQL LSNs for source {}",
194 self.source_id
195 )
196 })?;
197 match lsns {
198 Some((confirmed_flush_lsn, upstream_max_lsn, slot_name)) => {
199 let labels = self.pg_cdc_lsn_metric_labels(&slot_name);
200
201 get_or_create_guarded_int_gauge(
202 &mut self.pg_cdc_upstream_max_lsn,
203 &self.metrics.pg_cdc_upstream_max_lsn,
204 &labels,
205 )
206 .set(upstream_max_lsn as i64);
207
208 if let Some(lsn) = confirmed_flush_lsn {
209 get_or_create_guarded_int_gauge(
210 &mut self.pg_cdc_confirmed_flush_lsn,
211 &self.metrics.pg_cdc_confirmed_flush_lsn,
212 &labels,
213 )
214 .set(lsn as i64);
215 tracing::debug!(
216 "Updated confirmed_flush_lsn for source {} slot {}: {}",
217 self.source_id,
218 slot_name,
219 lsn
220 );
221 } else {
222 tracing::warn!(
223 "confirmed_flush_lsn is NULL for source {} slot {}",
224 self.source_id,
225 slot_name
226 );
227 }
228 }
229 None => {
230 tracing::warn!(
231 "No replication slot found when querying LSNs for source {}",
232 self.source_id
233 );
234 }
235 };
236 Ok(())
237 }
238
239 async fn query_postgres_lsns(&self) -> ConnectorResult<Option<(Option<u64>, u64, String)>> {
241 let pg_conn = pg_connection_config_from_properties(&self.properties)?;
242
243 let slot_name = self
244 .properties
245 .get("slot.name")
246 .ok_or_else(|| anyhow::anyhow!("slot.name not found in CDC properties"))?;
247
248 let client = create_pg_client(&pg_conn, None)
250 .await
251 .context("Failed to create PostgreSQL client")?;
252
253 let query = "SELECT confirmed_flush_lsn, pg_current_wal_lsn() \
254 FROM pg_replication_slots WHERE slot_name = $1";
255 let row = client
256 .query_opt(query, &[&slot_name])
257 .await
258 .context("PostgreSQL query LSNs error")?;
259 match row {
260 Some(row) => {
261 let confirmed_flush_lsn: Option<PgLsn> = row.get(0);
262 let upstream_max_lsn: PgLsn = row.get(1);
263 Ok(Some((
264 confirmed_flush_lsn.map(Into::into),
265 upstream_max_lsn.into(),
266 slot_name.clone(),
267 )))
268 }
269 None => {
270 tracing::warn!("No replication slot found with name: {}", slot_name);
271 Ok(None)
272 }
273 }
274 }
275
276 async fn query_sql_server_lsns(&self) -> ConnectorResult<Option<(String, String)>> {
278 let hostname = self
279 .properties
280 .get("hostname")
281 .ok_or_else(|| anyhow!("hostname not found in CDC properties"))?;
282 let port = self
283 .properties
284 .get("port")
285 .ok_or_else(|| anyhow!("port not found in CDC properties"))?
286 .parse::<u16>()
287 .context("failed to parse port as u16")?;
288 let username = self
289 .properties
290 .get("username")
291 .ok_or_else(|| anyhow!("username not found in CDC properties"))?;
292 let password = self
293 .properties
294 .get("password")
295 .ok_or_else(|| anyhow!("password not found in CDC properties"))?;
296 let database = self
297 .properties
298 .get("database.name")
299 .ok_or_else(|| anyhow!("database.name not found in CDC properties"))?;
300
301 let mut config = Config::new();
302 config.host(hostname);
303 config.port(port);
304 config.database(database);
305 config.authentication(tiberius::AuthMethod::sql_server(username, password));
306 config.trust_cert();
307
308 let mut client = SqlServerClient::new_with_config(config).await?;
309 let row = client
310 .inner_client
311 .simple_query(
312 "SELECT \
313 sys.fn_cdc_get_max_lsn() AS max_lsn, \
314 (SELECT MIN(sys.fn_cdc_get_min_lsn(capture_instance)) FROM cdc.change_tables) AS min_lsn"
315 .to_owned(),
316 )
317 .await?
318 .into_row()
319 .await?
320 .ok_or_else(|| anyhow!("No result returned when querying SQL Server max/min LSN"))?;
321
322 let lsn_bytes_to_hex = |bytes: &[u8]| -> ConnectorResult<String> {
323 if bytes.len() != 10 {
324 return Err(anyhow!(
325 "SQL Server LSN should be 10 bytes, got {} bytes",
326 bytes.len()
327 )
328 .into());
329 }
330 let mut hex_string = String::with_capacity(22);
331 for byte in &bytes[0..4] {
332 hex_string.push_str(&format!("{:02x}", byte));
333 }
334 hex_string.push(':');
335 for byte in &bytes[4..8] {
336 hex_string.push_str(&format!("{:02x}", byte));
337 }
338 hex_string.push(':');
339 for byte in &bytes[8..10] {
340 hex_string.push_str(&format!("{:02x}", byte));
341 }
342 Ok(hex_string)
343 };
344
345 let max_lsn = row
346 .try_get::<&[u8], usize>(0)?
347 .map(lsn_bytes_to_hex)
348 .transpose()?
349 .ok_or_else(|| anyhow!("SQL Server max_lsn is NULL"))?;
350 let min_lsn = row
351 .try_get::<&[u8], usize>(1)?
352 .map(lsn_bytes_to_hex)
353 .transpose()?
354 .ok_or_else(|| anyhow!("SQL Server min_lsn is NULL"))?;
355
356 Ok(Some((min_lsn, max_lsn)))
357 }
358
359 async fn monitor_sql_server_lsns(&mut self) -> ConnectorResult<()> {
360 let lsns = self.query_sql_server_lsns().await.with_context(|| {
361 format!(
362 "failed to query SQL Server LSNs for source {}",
363 self.source_id
364 )
365 })?;
366 if let Some((min_lsn, max_lsn)) = lsns {
367 let labels = vec![self.source_id.to_string()];
368
369 if let Some(value) = Self::sql_server_lsn_to_i64(&min_lsn) {
370 get_or_create_guarded_int_gauge(
371 &mut self.sqlserver_cdc_upstream_min_lsn,
372 &self.metrics.sqlserver_cdc_upstream_min_lsn,
373 &labels,
374 )
375 .set(value);
376 }
377
378 if let Some(value) = Self::sql_server_lsn_to_i64(&max_lsn) {
379 get_or_create_guarded_int_gauge(
380 &mut self.sqlserver_cdc_upstream_max_lsn,
381 &self.metrics.sqlserver_cdc_upstream_max_lsn,
382 &labels,
383 )
384 .set(value);
385 }
386 }
387
388 Ok(())
389 }
390}
391
392pub trait ListCdcSplits {
393 type CdcSourceType: CdcSourceTypeTrait;
394 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>>;
396}
397
398#[async_trait]
400pub trait CdcMonitor {
401 async fn monitor_cdc(&mut self) -> ConnectorResult<()>;
402}
403
404#[async_trait]
405impl<T: CdcSourceTypeTrait> CdcMonitor for DebeziumSplitEnumerator<T> {
406 default async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
407 Ok(())
408 }
409}
410
411impl DebeziumSplitEnumerator<Mysql> {
412 async fn monitor_mysql_binlog_files(&mut self) -> ConnectorResult<()> {
413 let hostname = self
415 .properties
416 .get("hostname")
417 .map(|s| s.as_str())
418 .ok_or_else(|| {
419 anyhow::anyhow!("missing required property 'hostname' for MySQL CDC source")
420 })?;
421 let port = self
422 .properties
423 .get("port")
424 .map(|s| s.as_str())
425 .ok_or_else(|| {
426 anyhow::anyhow!("missing required property 'port' for MySQL CDC source")
427 })?;
428
429 let binlog_files = self.query_binlog_files().await.with_context(|| {
431 format!(
432 "failed to query binlog files for MySQL CDC source {} ({}:{})",
433 self.source_id, hostname, port
434 )
435 })?;
436 if let Some((oldest_file, oldest_size)) = binlog_files.first()
437 && let Some(seq) = extract_binlog_file_seq(oldest_file)
438 {
439 let labels = vec![hostname.to_owned(), port.to_owned()];
440 get_or_create_guarded_int_gauge(
441 &mut self.mysql_cdc_binlog_file_seq_min,
442 &self.metrics.mysql_cdc_binlog_file_seq_min,
443 &labels,
444 )
445 .set(seq as i64);
446 tracing::debug!(
447 "MySQL CDC source {} ({}:{}): oldest binlog = {}, seq = {}, size = {}",
448 self.source_id,
449 hostname,
450 port,
451 oldest_file,
452 seq,
453 oldest_size
454 );
455 }
456 if let Some((newest_file, newest_size)) = binlog_files.last()
457 && let Some(seq) = extract_binlog_file_seq(newest_file)
458 {
459 let labels = vec![hostname.to_owned(), port.to_owned()];
460 get_or_create_guarded_int_gauge(
461 &mut self.mysql_cdc_binlog_file_seq_max,
462 &self.metrics.mysql_cdc_binlog_file_seq_max,
463 &labels,
464 )
465 .set(seq as i64);
466 tracing::debug!(
467 "MySQL CDC source {} ({}:{}): newest binlog = {}, seq = {}, size = {}",
468 self.source_id,
469 hostname,
470 port,
471 newest_file,
472 seq,
473 newest_size
474 );
475 }
476 tracing::debug!(
477 "MySQL CDC source {} ({}:{}): total {} binlog files",
478 self.source_id,
479 hostname,
480 port,
481 binlog_files.len()
482 );
483 Ok(())
484 }
485
486 async fn query_binlog_files(&self) -> ConnectorResult<Vec<(String, u64)>> {
488 let hostname = self
490 .properties
491 .get("hostname")
492 .ok_or_else(|| anyhow::anyhow!("hostname not found in CDC properties"))?;
493 let port = self
494 .properties
495 .get("port")
496 .ok_or_else(|| anyhow::anyhow!("port not found in CDC properties"))?
497 .parse::<u16>()
498 .context("failed to parse port as u16")?;
499 let username = self
500 .properties
501 .get("username")
502 .ok_or_else(|| anyhow::anyhow!("username not found in CDC properties"))?;
503 let password = self
504 .properties
505 .get("password")
506 .ok_or_else(|| anyhow::anyhow!("password not found in CDC properties"))?;
507 let database = self
508 .properties
509 .get("database.name")
510 .ok_or_else(|| anyhow::anyhow!("database.name not found in CDC properties"))?;
511
512 let ssl_mode = self
514 .properties
515 .get("ssl.mode")
516 .and_then(|s| s.parse().ok())
517 .unwrap_or(SslMode::Preferred);
518
519 let pool =
521 build_mysql_connection_pool(hostname, port, username, password, database, ssl_mode);
522 let mut conn = pool
523 .get_conn()
524 .await
525 .context("Failed to connect to MySQL")?;
526
527 let rows: Vec<Row> = conn
532 .query("SHOW BINARY LOGS")
533 .await
534 .context("Failed to execute SHOW BINARY LOGS")?;
535 let query_result = rows
536 .into_iter()
537 .map(|mut row| -> ConnectorResult<(String, u64)> {
538 let log_name = row
539 .take_opt::<String, _>(0)
540 .transpose()
541 .context("SHOW BINARY LOGS: failed to decode Log_name")?
542 .ok_or_else(|| anyhow!("SHOW BINARY LOGS: missing Log_name column"))?;
543 let file_size = row
544 .take_opt::<u64, _>(1)
545 .transpose()
546 .context("SHOW BINARY LOGS: failed to decode File_size")?
547 .ok_or_else(|| anyhow!("SHOW BINARY LOGS: missing File_size column"))?;
548 Ok((log_name, file_size))
549 })
550 .collect::<ConnectorResult<Vec<_>>>()?;
551
552 drop(conn);
553 pool.disconnect().await.ok();
554
555 Ok(query_result)
556 }
557}
558
559impl ListCdcSplits for DebeziumSplitEnumerator<Mysql> {
560 type CdcSourceType = Mysql;
561
562 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
563 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
565 self.source_id.as_raw_id(),
566 None,
567 None,
568 )]
569 }
570}
571
572#[async_trait]
573impl CdcMonitor for DebeziumSplitEnumerator<Mysql> {
574 async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
575 self.monitor_mysql_binlog_files().await?;
577 Ok(())
578 }
579}
580
581impl ListCdcSplits for DebeziumSplitEnumerator<Postgres> {
582 type CdcSourceType = Postgres;
583
584 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
585 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
587 self.source_id.as_raw_id(),
588 None,
589 None,
590 )]
591 }
592}
593
594#[async_trait]
595impl CdcMonitor for DebeziumSplitEnumerator<Postgres> {
596 async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
597 self.monitor_postgres_confirmed_flush_lsn().await?;
599 Ok(())
600 }
601}
602
603impl ListCdcSplits for DebeziumSplitEnumerator<Citus> {
604 type CdcSourceType = Citus;
605
606 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
607 self.worker_node_addrs
608 .iter()
609 .enumerate()
610 .map(|(id, addr)| {
611 DebeziumCdcSplit::<Self::CdcSourceType>::new(
612 id as u32,
613 None,
614 Some(addr.to_string()),
615 )
616 })
617 .collect_vec()
618 }
619}
620impl ListCdcSplits for DebeziumSplitEnumerator<Mongodb> {
621 type CdcSourceType = Mongodb;
622
623 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
624 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
626 self.source_id.as_raw_id(),
627 None,
628 None,
629 )]
630 }
631}
632
633impl ListCdcSplits for DebeziumSplitEnumerator<SqlServer> {
634 type CdcSourceType = SqlServer;
635
636 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
637 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
638 self.source_id.as_raw_id(),
639 None,
640 None,
641 )]
642 }
643}
644
645#[async_trait]
646impl CdcMonitor for DebeziumSplitEnumerator<SqlServer> {
647 async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
648 self.monitor_sql_server_lsns().await
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use prometheus::core::Collector;
655 use risingwave_common::metrics::LabelGuardedIntGaugeVec;
656
657 use super::get_or_create_guarded_int_gauge;
658
659 #[test]
660 fn cached_guarded_metric_survives_repeated_collections() {
661 let metric_vec = LabelGuardedIntGaugeVec::test_int_gauge_vec::<2>();
662 let labels = vec!["source_id".to_owned(), "slot_name".to_owned()];
663 let mut metric = None;
664
665 get_or_create_guarded_int_gauge(&mut metric, &metric_vec, &labels).set(1);
666
667 assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
668 assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
669
670 drop(metric);
671 assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
672 assert_eq!(0, metric_vec.collect().pop().unwrap().get_metric().len());
673 }
674}