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(anyhow!(error.error_message).context("source validation failed"));
145 }
146
147 Ok(())
148 })
149 })
150 .await
151 .context("failed to validate source")??;
152
153 tracing::debug!("validate cdc source properties success");
154 Ok(Self {
155 source_id,
156 worker_node_addrs: server_addrs,
157 metrics: context.metrics.clone(),
158 pg_cdc_upstream_max_lsn: None,
159 pg_cdc_confirmed_flush_lsn: None,
160 mysql_cdc_binlog_file_seq_min: None,
161 mysql_cdc_binlog_file_seq_max: None,
162 sqlserver_cdc_upstream_min_lsn: None,
163 sqlserver_cdc_upstream_max_lsn: None,
164 properties: properties_arc,
165 _phantom: PhantomData,
166 })
167 }
168
169 async fn list_splits(&mut self) -> ConnectorResult<Vec<DebeziumCdcSplit<T>>> {
170 Ok(self.list_cdc_splits())
171 }
172
173 async fn on_tick(&mut self) -> ConnectorResult<()> {
174 self.monitor_cdc().await
175 }
176}
177
178impl<T: CdcSourceTypeTrait> DebeziumSplitEnumerator<T> {
179 fn sql_server_lsn_to_i64(lsn: &str) -> Option<i64> {
180 parse_sql_server_lsn_str(lsn).map(|v| v.min(i64::MAX as u128) as i64)
181 }
182
183 fn pg_cdc_lsn_metric_labels(&self, slot_name: &str) -> Vec<String> {
184 vec![self.source_id.to_string(), slot_name.to_owned()]
185 }
186
187 async fn monitor_postgres_confirmed_flush_lsn(&mut self) -> ConnectorResult<()> {
188 let lsns = self.query_postgres_lsns().await.with_context(|| {
190 format!(
191 "failed to query PostgreSQL LSNs for source {}",
192 self.source_id
193 )
194 })?;
195 match lsns {
196 Some((confirmed_flush_lsn, upstream_max_lsn, slot_name)) => {
197 let labels = self.pg_cdc_lsn_metric_labels(&slot_name);
198
199 get_or_create_guarded_int_gauge(
200 &mut self.pg_cdc_upstream_max_lsn,
201 &self.metrics.pg_cdc_upstream_max_lsn,
202 &labels,
203 )
204 .set(upstream_max_lsn as i64);
205
206 if let Some(lsn) = confirmed_flush_lsn {
207 get_or_create_guarded_int_gauge(
208 &mut self.pg_cdc_confirmed_flush_lsn,
209 &self.metrics.pg_cdc_confirmed_flush_lsn,
210 &labels,
211 )
212 .set(lsn as i64);
213 tracing::debug!(
214 "Updated confirmed_flush_lsn for source {} slot {}: {}",
215 self.source_id,
216 slot_name,
217 lsn
218 );
219 } else {
220 tracing::warn!(
221 "confirmed_flush_lsn is NULL for source {} slot {}",
222 self.source_id,
223 slot_name
224 );
225 }
226 }
227 None => {
228 tracing::warn!(
229 "no replication slot was found while querying LSNs for source {}",
230 self.source_id
231 );
232 }
233 };
234 Ok(())
235 }
236
237 async fn query_postgres_lsns(&self) -> ConnectorResult<Option<(Option<u64>, u64, String)>> {
239 let pg_conn = pg_connection_config_from_properties(&self.properties)?;
240
241 let slot_name = self
242 .properties
243 .get("slot.name")
244 .ok_or_else(|| anyhow::anyhow!("missing `slot.name` in CDC properties"))?;
245
246 let application_name = format!("risingwave-postgres-source-enumerator-{}", self.source_id);
248 let client = create_pg_client(&pg_conn, None, Some(&application_name))
249 .await
250 .context("failed to create the PostgreSQL client")?;
251
252 let query = "SELECT confirmed_flush_lsn, pg_current_wal_lsn() \
253 FROM pg_replication_slots WHERE slot_name = $1";
254 let row = client
255 .query_opt(query, &[&slot_name])
256 .await
257 .context("failed to query PostgreSQL LSNs")?;
258 match row {
259 Some(row) => {
260 let confirmed_flush_lsn: Option<PgLsn> = row.get(0);
261 let upstream_max_lsn: PgLsn = row.get(1);
262 Ok(Some((
263 confirmed_flush_lsn.map(Into::into),
264 upstream_max_lsn.into(),
265 slot_name.clone(),
266 )))
267 }
268 None => {
269 tracing::warn!("no replication slot found with name: {}", slot_name);
270 Ok(None)
271 }
272 }
273 }
274
275 async fn query_sql_server_lsns(&self) -> ConnectorResult<Option<(String, String)>> {
277 let hostname = self
278 .properties
279 .get("hostname")
280 .ok_or_else(|| anyhow!("missing `hostname` in CDC properties"))?;
281 let port = self
282 .properties
283 .get("port")
284 .ok_or_else(|| anyhow!("missing `port` in CDC properties"))?
285 .parse::<u16>()
286 .context("failed to parse `port` as a u16")?;
287 let username = self
288 .properties
289 .get("username")
290 .ok_or_else(|| anyhow!("missing `username` in CDC properties"))?;
291 let password = self
292 .properties
293 .get("password")
294 .ok_or_else(|| anyhow!("missing `password` in CDC properties"))?;
295 let database = self
296 .properties
297 .get("database.name")
298 .ok_or_else(|| anyhow!("missing `database.name` in CDC properties"))?;
299
300 let mut config = Config::new();
301 config.host(hostname);
302 config.port(port);
303 config.database(database);
304 config.authentication(tiberius::AuthMethod::sql_server(username, password));
305 config.trust_cert();
306
307 let mut client = SqlServerClient::new_with_config(config).await?;
308 let row = client
309 .inner_client
310 .simple_query(
311 "SELECT \
312 sys.fn_cdc_get_max_lsn() AS max_lsn, \
313 (SELECT MIN(sys.fn_cdc_get_min_lsn(capture_instance)) FROM cdc.change_tables) AS min_lsn"
314 .to_owned(),
315 )
316 .await?
317 .into_row()
318 .await?
319 .ok_or_else(|| anyhow!("No result returned when querying SQL Server max/min LSN"))?;
320
321 let lsn_bytes_to_hex = |bytes: &[u8]| -> ConnectorResult<String> {
322 if bytes.len() != 10 {
323 return Err(anyhow!(
324 "SQL Server LSN should be 10 bytes, got {} bytes",
325 bytes.len()
326 )
327 .into());
328 }
329 let mut hex_string = String::with_capacity(22);
330 for byte in &bytes[0..4] {
331 hex_string.push_str(&format!("{:02x}", byte));
332 }
333 hex_string.push(':');
334 for byte in &bytes[4..8] {
335 hex_string.push_str(&format!("{:02x}", byte));
336 }
337 hex_string.push(':');
338 for byte in &bytes[8..10] {
339 hex_string.push_str(&format!("{:02x}", byte));
340 }
341 Ok(hex_string)
342 };
343
344 let max_lsn = row
345 .try_get::<&[u8], usize>(0)?
346 .map(lsn_bytes_to_hex)
347 .transpose()?
348 .ok_or_else(|| anyhow!("SQL Server max_lsn is NULL"))?;
349 let min_lsn = row
350 .try_get::<&[u8], usize>(1)?
351 .map(lsn_bytes_to_hex)
352 .transpose()?
353 .ok_or_else(|| anyhow!("SQL Server min_lsn is NULL"))?;
354
355 Ok(Some((min_lsn, max_lsn)))
356 }
357
358 async fn monitor_sql_server_lsns(&mut self) -> ConnectorResult<()> {
359 let lsns = self.query_sql_server_lsns().await.with_context(|| {
360 format!(
361 "failed to query SQL Server LSNs for source {}",
362 self.source_id
363 )
364 })?;
365 if let Some((min_lsn, max_lsn)) = lsns {
366 let labels = vec![self.source_id.to_string()];
367
368 if let Some(value) = Self::sql_server_lsn_to_i64(&min_lsn) {
369 get_or_create_guarded_int_gauge(
370 &mut self.sqlserver_cdc_upstream_min_lsn,
371 &self.metrics.sqlserver_cdc_upstream_min_lsn,
372 &labels,
373 )
374 .set(value);
375 }
376 if let Some(value) = Self::sql_server_lsn_to_i64(&max_lsn) {
377 get_or_create_guarded_int_gauge(
378 &mut self.sqlserver_cdc_upstream_max_lsn,
379 &self.metrics.sqlserver_cdc_upstream_max_lsn,
380 &labels,
381 )
382 .set(value);
383 }
384 }
385
386 Ok(())
387 }
388}
389
390pub trait ListCdcSplits {
391 type CdcSourceType: CdcSourceTypeTrait;
392 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>>;
394}
395
396#[async_trait]
398pub trait CdcMonitor {
399 async fn monitor_cdc(&mut self) -> ConnectorResult<()>;
400}
401
402#[async_trait]
403impl<T: CdcSourceTypeTrait> CdcMonitor for DebeziumSplitEnumerator<T> {
404 default async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
405 Ok(())
406 }
407}
408
409impl DebeziumSplitEnumerator<Mysql> {
410 async fn monitor_mysql_binlog_files(&mut self) -> ConnectorResult<()> {
411 let hostname = self
413 .properties
414 .get("hostname")
415 .map(|s| s.as_str())
416 .ok_or_else(|| {
417 anyhow::anyhow!("missing required property 'hostname' for MySQL CDC source")
418 })?;
419 let port = self
420 .properties
421 .get("port")
422 .map(|s| s.as_str())
423 .ok_or_else(|| {
424 anyhow::anyhow!("missing required property 'port' for MySQL CDC source")
425 })?;
426
427 let binlog_files = self.query_binlog_files().await.with_context(|| {
429 format!(
430 "failed to query binlog files for MySQL CDC source {} ({}:{})",
431 self.source_id, hostname, port
432 )
433 })?;
434 if let Some((oldest_file, oldest_size)) = binlog_files.first()
435 && let Some(seq) = extract_binlog_file_seq(oldest_file)
436 {
437 let labels = vec![
438 self.source_id.to_string(),
439 hostname.to_owned(),
440 port.to_owned(),
441 ];
442 get_or_create_guarded_int_gauge(
443 &mut self.mysql_cdc_binlog_file_seq_min,
444 &self.metrics.mysql_cdc_binlog_file_seq_min,
445 &labels,
446 )
447 .set(seq as i64);
448 tracing::debug!(
449 "MySQL CDC source {} ({}:{}): oldest binlog = {}, seq = {}, size = {}",
450 self.source_id,
451 hostname,
452 port,
453 oldest_file,
454 seq,
455 oldest_size
456 );
457 }
458 if let Some((newest_file, newest_size)) = binlog_files.last()
459 && let Some(seq) = extract_binlog_file_seq(newest_file)
460 {
461 let labels = vec![
462 self.source_id.to_string(),
463 hostname.to_owned(),
464 port.to_owned(),
465 ];
466 get_or_create_guarded_int_gauge(
467 &mut self.mysql_cdc_binlog_file_seq_max,
468 &self.metrics.mysql_cdc_binlog_file_seq_max,
469 &labels,
470 )
471 .set(seq as i64);
472 tracing::debug!(
473 "MySQL CDC source {} ({}:{}): newest binlog = {}, seq = {}, size = {}",
474 self.source_id,
475 hostname,
476 port,
477 newest_file,
478 seq,
479 newest_size
480 );
481 }
482 tracing::debug!(
483 "MySQL CDC source {} ({}:{}): total {} binlog files",
484 self.source_id,
485 hostname,
486 port,
487 binlog_files.len()
488 );
489 Ok(())
490 }
491
492 async fn query_binlog_files(&self) -> ConnectorResult<Vec<(String, u64)>> {
494 let hostname = self
496 .properties
497 .get("hostname")
498 .ok_or_else(|| anyhow::anyhow!("missing `hostname` in CDC properties"))?;
499 let port = self
500 .properties
501 .get("port")
502 .ok_or_else(|| anyhow::anyhow!("missing `port` in CDC properties"))?
503 .parse::<u16>()
504 .context("failed to parse `port` as a u16")?;
505 let username = self
506 .properties
507 .get("username")
508 .ok_or_else(|| anyhow::anyhow!("missing `username` in CDC properties"))?;
509 let password = self
510 .properties
511 .get("password")
512 .ok_or_else(|| anyhow::anyhow!("missing `password` in CDC properties"))?;
513 let database = self
514 .properties
515 .get("database.name")
516 .ok_or_else(|| anyhow::anyhow!("missing `database.name` in CDC properties"))?;
517
518 let ssl_mode = self
520 .properties
521 .get("ssl.mode")
522 .and_then(|s| s.parse().ok())
523 .unwrap_or(SslMode::Preferred);
524
525 let pool =
527 build_mysql_connection_pool(hostname, port, username, password, database, ssl_mode);
528 let mut conn = pool
529 .get_conn()
530 .await
531 .context("failed to connect to MySQL")?;
532
533 let rows: Vec<Row> = conn
538 .query("SHOW BINARY LOGS")
539 .await
540 .context("failed to execute `SHOW BINARY LOGS`")?;
541 let query_result = rows
542 .into_iter()
543 .map(|mut row| -> ConnectorResult<(String, u64)> {
544 let log_name = row
545 .take_opt::<String, _>(0)
546 .transpose()
547 .context("`SHOW BINARY LOGS`: failed to decode `Log_name`")?
548 .ok_or_else(|| anyhow!("`SHOW BINARY LOGS`: missing `Log_name` column"))?;
549 let file_size = row
550 .take_opt::<u64, _>(1)
551 .transpose()
552 .context("`SHOW BINARY LOGS`: failed to decode `File_size`")?
553 .ok_or_else(|| anyhow!("`SHOW BINARY LOGS`: missing `File_size` column"))?;
554 Ok((log_name, file_size))
555 })
556 .collect::<ConnectorResult<Vec<_>>>()?;
557
558 drop(conn);
559 pool.disconnect().await.ok();
560
561 Ok(query_result)
562 }
563}
564
565impl ListCdcSplits for DebeziumSplitEnumerator<Mysql> {
566 type CdcSourceType = Mysql;
567
568 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
569 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
571 self.source_id.as_raw_id(),
572 None,
573 None,
574 )]
575 }
576}
577
578#[async_trait]
579impl CdcMonitor for DebeziumSplitEnumerator<Mysql> {
580 async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
581 self.monitor_mysql_binlog_files().await?;
583 Ok(())
584 }
585}
586
587impl ListCdcSplits for DebeziumSplitEnumerator<Postgres> {
588 type CdcSourceType = Postgres;
589
590 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
591 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
593 self.source_id.as_raw_id(),
594 None,
595 None,
596 )]
597 }
598}
599
600#[async_trait]
601impl CdcMonitor for DebeziumSplitEnumerator<Postgres> {
602 async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
603 self.monitor_postgres_confirmed_flush_lsn().await?;
605 Ok(())
606 }
607}
608
609impl ListCdcSplits for DebeziumSplitEnumerator<Citus> {
610 type CdcSourceType = Citus;
611
612 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
613 self.worker_node_addrs
614 .iter()
615 .enumerate()
616 .map(|(id, addr)| {
617 DebeziumCdcSplit::<Self::CdcSourceType>::new(
618 id as u32,
619 None,
620 Some(addr.to_string()),
621 )
622 })
623 .collect_vec()
624 }
625}
626impl ListCdcSplits for DebeziumSplitEnumerator<Mongodb> {
627 type CdcSourceType = Mongodb;
628
629 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
630 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
632 self.source_id.as_raw_id(),
633 None,
634 None,
635 )]
636 }
637}
638
639impl ListCdcSplits for DebeziumSplitEnumerator<SqlServer> {
640 type CdcSourceType = SqlServer;
641
642 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
643 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
644 self.source_id.as_raw_id(),
645 None,
646 None,
647 )]
648 }
649}
650
651#[async_trait]
652impl CdcMonitor for DebeziumSplitEnumerator<SqlServer> {
653 async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
654 self.monitor_sql_server_lsns().await
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use prometheus::core::Collector;
661 use risingwave_common::metrics::LabelGuardedIntGaugeVec;
662
663 use super::get_or_create_guarded_int_gauge;
664
665 #[test]
666 fn cached_guarded_metric_survives_repeated_collections() {
667 let metric_vec = LabelGuardedIntGaugeVec::test_int_gauge_vec::<2>();
668 let labels = vec!["source_id".to_owned(), "slot_name".to_owned()];
669 let mut metric = None;
670
671 get_or_create_guarded_int_gauge(&mut metric, &metric_vec, &labels).set(1);
672
673 assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
674 assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
675
676 drop(metric);
677 assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
678 assert_eq!(0, metric_vec.collect().pop().unwrap().get_metric().len());
679 }
680}