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