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 client = create_pg_client(&pg_conn, None)
248 .await
249 .context("failed to create the PostgreSQL client")?;
250
251 let query = "SELECT confirmed_flush_lsn, pg_current_wal_lsn() \
252 FROM pg_replication_slots WHERE slot_name = $1";
253 let row = client
254 .query_opt(query, &[&slot_name])
255 .await
256 .context("failed to query PostgreSQL LSNs")?;
257 match row {
258 Some(row) => {
259 let confirmed_flush_lsn: Option<PgLsn> = row.get(0);
260 let upstream_max_lsn: PgLsn = row.get(1);
261 Ok(Some((
262 confirmed_flush_lsn.map(Into::into),
263 upstream_max_lsn.into(),
264 slot_name.clone(),
265 )))
266 }
267 None => {
268 tracing::warn!("no replication slot found with name: {}", slot_name);
269 Ok(None)
270 }
271 }
272 }
273
274 async fn query_sql_server_lsns(&self) -> ConnectorResult<Option<(String, String)>> {
276 let hostname = self
277 .properties
278 .get("hostname")
279 .ok_or_else(|| anyhow!("missing `hostname` in CDC properties"))?;
280 let port = self
281 .properties
282 .get("port")
283 .ok_or_else(|| anyhow!("missing `port` in CDC properties"))?
284 .parse::<u16>()
285 .context("failed to parse `port` as a u16")?;
286 let username = self
287 .properties
288 .get("username")
289 .ok_or_else(|| anyhow!("missing `username` in CDC properties"))?;
290 let password = self
291 .properties
292 .get("password")
293 .ok_or_else(|| anyhow!("missing `password` in CDC properties"))?;
294 let database = self
295 .properties
296 .get("database.name")
297 .ok_or_else(|| anyhow!("missing `database.name` in CDC properties"))?;
298
299 let mut config = Config::new();
300 config.host(hostname);
301 config.port(port);
302 config.database(database);
303 config.authentication(tiberius::AuthMethod::sql_server(username, password));
304 config.trust_cert();
305
306 let mut client = SqlServerClient::new_with_config(config).await?;
307 let row = client
308 .inner_client
309 .simple_query(
310 "SELECT \
311 sys.fn_cdc_get_max_lsn() AS max_lsn, \
312 (SELECT MIN(sys.fn_cdc_get_min_lsn(capture_instance)) FROM cdc.change_tables) AS min_lsn"
313 .to_owned(),
314 )
315 .await?
316 .into_row()
317 .await?
318 .ok_or_else(|| anyhow!("No result returned when querying SQL Server max/min LSN"))?;
319
320 let lsn_bytes_to_hex = |bytes: &[u8]| -> ConnectorResult<String> {
321 if bytes.len() != 10 {
322 return Err(anyhow!(
323 "SQL Server LSN should be 10 bytes, got {} bytes",
324 bytes.len()
325 )
326 .into());
327 }
328 let mut hex_string = String::with_capacity(22);
329 for byte in &bytes[0..4] {
330 hex_string.push_str(&format!("{:02x}", byte));
331 }
332 hex_string.push(':');
333 for byte in &bytes[4..8] {
334 hex_string.push_str(&format!("{:02x}", byte));
335 }
336 hex_string.push(':');
337 for byte in &bytes[8..10] {
338 hex_string.push_str(&format!("{:02x}", byte));
339 }
340 Ok(hex_string)
341 };
342
343 let max_lsn = row
344 .try_get::<&[u8], usize>(0)?
345 .map(lsn_bytes_to_hex)
346 .transpose()?
347 .ok_or_else(|| anyhow!("SQL Server max_lsn is NULL"))?;
348 let min_lsn = row
349 .try_get::<&[u8], usize>(1)?
350 .map(lsn_bytes_to_hex)
351 .transpose()?
352 .ok_or_else(|| anyhow!("SQL Server min_lsn is NULL"))?;
353
354 Ok(Some((min_lsn, max_lsn)))
355 }
356
357 async fn monitor_sql_server_lsns(&mut self) -> ConnectorResult<()> {
358 let lsns = self.query_sql_server_lsns().await.with_context(|| {
359 format!(
360 "failed to query SQL Server LSNs for source {}",
361 self.source_id
362 )
363 })?;
364 if let Some((min_lsn, max_lsn)) = lsns {
365 let labels = vec![self.source_id.to_string()];
366
367 if let Some(value) = Self::sql_server_lsn_to_i64(&min_lsn) {
368 get_or_create_guarded_int_gauge(
369 &mut self.sqlserver_cdc_upstream_min_lsn,
370 &self.metrics.sqlserver_cdc_upstream_min_lsn,
371 &labels,
372 )
373 .set(value);
374 }
375 if let Some(value) = Self::sql_server_lsn_to_i64(&max_lsn) {
376 get_or_create_guarded_int_gauge(
377 &mut self.sqlserver_cdc_upstream_max_lsn,
378 &self.metrics.sqlserver_cdc_upstream_max_lsn,
379 &labels,
380 )
381 .set(value);
382 }
383 }
384
385 Ok(())
386 }
387}
388
389pub trait ListCdcSplits {
390 type CdcSourceType: CdcSourceTypeTrait;
391 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>>;
393}
394
395#[async_trait]
397pub trait CdcMonitor {
398 async fn monitor_cdc(&mut self) -> ConnectorResult<()>;
399}
400
401#[async_trait]
402impl<T: CdcSourceTypeTrait> CdcMonitor for DebeziumSplitEnumerator<T> {
403 default async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
404 Ok(())
405 }
406}
407
408impl DebeziumSplitEnumerator<Mysql> {
409 async fn monitor_mysql_binlog_files(&mut self) -> ConnectorResult<()> {
410 let hostname = self
412 .properties
413 .get("hostname")
414 .map(|s| s.as_str())
415 .ok_or_else(|| {
416 anyhow::anyhow!("missing required property 'hostname' for MySQL CDC source")
417 })?;
418 let port = self
419 .properties
420 .get("port")
421 .map(|s| s.as_str())
422 .ok_or_else(|| {
423 anyhow::anyhow!("missing required property 'port' for MySQL CDC source")
424 })?;
425
426 let binlog_files = self.query_binlog_files().await.with_context(|| {
428 format!(
429 "failed to query binlog files for MySQL CDC source {} ({}:{})",
430 self.source_id, hostname, port
431 )
432 })?;
433 if let Some((oldest_file, oldest_size)) = binlog_files.first()
434 && let Some(seq) = extract_binlog_file_seq(oldest_file)
435 {
436 let labels = vec![hostname.to_owned(), port.to_owned()];
437 get_or_create_guarded_int_gauge(
438 &mut self.mysql_cdc_binlog_file_seq_min,
439 &self.metrics.mysql_cdc_binlog_file_seq_min,
440 &labels,
441 )
442 .set(seq as i64);
443 tracing::debug!(
444 "MySQL CDC source {} ({}:{}): oldest binlog = {}, seq = {}, size = {}",
445 self.source_id,
446 hostname,
447 port,
448 oldest_file,
449 seq,
450 oldest_size
451 );
452 }
453 if let Some((newest_file, newest_size)) = binlog_files.last()
454 && let Some(seq) = extract_binlog_file_seq(newest_file)
455 {
456 let labels = vec![hostname.to_owned(), port.to_owned()];
457 get_or_create_guarded_int_gauge(
458 &mut self.mysql_cdc_binlog_file_seq_max,
459 &self.metrics.mysql_cdc_binlog_file_seq_max,
460 &labels,
461 )
462 .set(seq as i64);
463 tracing::debug!(
464 "MySQL CDC source {} ({}:{}): newest binlog = {}, seq = {}, size = {}",
465 self.source_id,
466 hostname,
467 port,
468 newest_file,
469 seq,
470 newest_size
471 );
472 }
473 tracing::debug!(
474 "MySQL CDC source {} ({}:{}): total {} binlog files",
475 self.source_id,
476 hostname,
477 port,
478 binlog_files.len()
479 );
480 Ok(())
481 }
482
483 async fn query_binlog_files(&self) -> ConnectorResult<Vec<(String, u64)>> {
485 let hostname = self
487 .properties
488 .get("hostname")
489 .ok_or_else(|| anyhow::anyhow!("missing `hostname` in CDC properties"))?;
490 let port = self
491 .properties
492 .get("port")
493 .ok_or_else(|| anyhow::anyhow!("missing `port` in CDC properties"))?
494 .parse::<u16>()
495 .context("failed to parse `port` as a u16")?;
496 let username = self
497 .properties
498 .get("username")
499 .ok_or_else(|| anyhow::anyhow!("missing `username` in CDC properties"))?;
500 let password = self
501 .properties
502 .get("password")
503 .ok_or_else(|| anyhow::anyhow!("missing `password` in CDC properties"))?;
504 let database = self
505 .properties
506 .get("database.name")
507 .ok_or_else(|| anyhow::anyhow!("missing `database.name` in CDC properties"))?;
508
509 let ssl_mode = self
511 .properties
512 .get("ssl.mode")
513 .and_then(|s| s.parse().ok())
514 .unwrap_or(SslMode::Preferred);
515
516 let pool =
518 build_mysql_connection_pool(hostname, port, username, password, database, ssl_mode);
519 let mut conn = pool
520 .get_conn()
521 .await
522 .context("failed to connect to MySQL")?;
523
524 let rows: Vec<Row> = conn
529 .query("SHOW BINARY LOGS")
530 .await
531 .context("failed to execute `SHOW BINARY LOGS`")?;
532 let query_result = rows
533 .into_iter()
534 .map(|mut row| -> ConnectorResult<(String, u64)> {
535 let log_name = row
536 .take_opt::<String, _>(0)
537 .transpose()
538 .context("`SHOW BINARY LOGS`: failed to decode `Log_name`")?
539 .ok_or_else(|| anyhow!("`SHOW BINARY LOGS`: missing `Log_name` column"))?;
540 let file_size = row
541 .take_opt::<u64, _>(1)
542 .transpose()
543 .context("`SHOW BINARY LOGS`: failed to decode `File_size`")?
544 .ok_or_else(|| anyhow!("`SHOW BINARY LOGS`: missing `File_size` column"))?;
545 Ok((log_name, file_size))
546 })
547 .collect::<ConnectorResult<Vec<_>>>()?;
548
549 drop(conn);
550 pool.disconnect().await.ok();
551
552 Ok(query_result)
553 }
554}
555
556impl ListCdcSplits for DebeziumSplitEnumerator<Mysql> {
557 type CdcSourceType = Mysql;
558
559 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
560 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
562 self.source_id.as_raw_id(),
563 None,
564 None,
565 )]
566 }
567}
568
569#[async_trait]
570impl CdcMonitor for DebeziumSplitEnumerator<Mysql> {
571 async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
572 self.monitor_mysql_binlog_files().await?;
574 Ok(())
575 }
576}
577
578impl ListCdcSplits for DebeziumSplitEnumerator<Postgres> {
579 type CdcSourceType = Postgres;
580
581 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
582 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
584 self.source_id.as_raw_id(),
585 None,
586 None,
587 )]
588 }
589}
590
591#[async_trait]
592impl CdcMonitor for DebeziumSplitEnumerator<Postgres> {
593 async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
594 self.monitor_postgres_confirmed_flush_lsn().await?;
596 Ok(())
597 }
598}
599
600impl ListCdcSplits for DebeziumSplitEnumerator<Citus> {
601 type CdcSourceType = Citus;
602
603 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
604 self.worker_node_addrs
605 .iter()
606 .enumerate()
607 .map(|(id, addr)| {
608 DebeziumCdcSplit::<Self::CdcSourceType>::new(
609 id as u32,
610 None,
611 Some(addr.to_string()),
612 )
613 })
614 .collect_vec()
615 }
616}
617impl ListCdcSplits for DebeziumSplitEnumerator<Mongodb> {
618 type CdcSourceType = Mongodb;
619
620 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
621 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
623 self.source_id.as_raw_id(),
624 None,
625 None,
626 )]
627 }
628}
629
630impl ListCdcSplits for DebeziumSplitEnumerator<SqlServer> {
631 type CdcSourceType = SqlServer;
632
633 fn list_cdc_splits(&mut self) -> Vec<DebeziumCdcSplit<Self::CdcSourceType>> {
634 vec![DebeziumCdcSplit::<Self::CdcSourceType>::new(
635 self.source_id.as_raw_id(),
636 None,
637 None,
638 )]
639 }
640}
641
642#[async_trait]
643impl CdcMonitor for DebeziumSplitEnumerator<SqlServer> {
644 async fn monitor_cdc(&mut self) -> ConnectorResult<()> {
645 self.monitor_sql_server_lsns().await
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use prometheus::core::Collector;
652 use risingwave_common::metrics::LabelGuardedIntGaugeVec;
653
654 use super::get_or_create_guarded_int_gauge;
655
656 #[test]
657 fn cached_guarded_metric_survives_repeated_collections() {
658 let metric_vec = LabelGuardedIntGaugeVec::test_int_gauge_vec::<2>();
659 let labels = vec!["source_id".to_owned(), "slot_name".to_owned()];
660 let mut metric = None;
661
662 get_or_create_guarded_int_gauge(&mut metric, &metric_vec, &labels).set(1);
663
664 assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
665 assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
666
667 drop(metric);
668 assert_eq!(1, metric_vec.collect().pop().unwrap().get_metric().len());
669 assert_eq!(0, metric_vec.collect().pop().unwrap().get_metric().len());
670 }
671}