1use std::collections::{HashMap, HashSet};
16use std::time::Duration;
17
18use anyhow::anyhow;
19use either::Either;
20use itertools::Itertools;
21use prometheus::core::{AtomicU64, GenericCounter};
22use risingwave_common::array::ArrayRef;
23use risingwave_common::catalog::TableId;
24use risingwave_common::metrics::{GLOBAL_ERROR_METRICS, LabelGuardedIntGauge, LabelGuardedMetric};
25use risingwave_common::system_param::local_manager::SystemParamsReaderRef;
26use risingwave_common::system_param::reader::SystemParamsRead;
27use risingwave_common::types::JsonbVal;
28use risingwave_common::util::epoch::{Epoch, EpochPair};
29use risingwave_connector::dispatch_cdc_source_type;
30use risingwave_connector::source::cdc::split::{
31 extract_postgres_lsn_from_offset_str, extract_sql_server_commit_lsn_from_offset_str,
32};
33use risingwave_connector::source::cdc::{CdcSourceRuntimeFlags, CdcSourceType};
34use risingwave_connector::source::reader::desc::{SourceDesc, SourceDescBuilder};
35use risingwave_connector::source::reader::reader::SourceReader;
36use risingwave_connector::source::{
37 ConnectorState, SplitId, SplitImpl, SplitMetaData, WaitCheckpointTask,
38 build_pulsar_ack_channel_id,
39};
40use risingwave_hummock_sdk::HummockReadEpoch;
41use risingwave_pb::common::ThrottleType;
42use risingwave_pb::id::SourceId;
43use risingwave_storage::store::TryWaitEpochOptions;
44use thiserror_ext::AsReport;
45use tokio::sync::mpsc;
46use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
47use tokio::time::Instant;
48
49use super::executor_core::StreamSourceCore;
50use super::{barrier_to_message_stream, get_split_offset_col_idx, prune_additional_cols};
51use crate::executor::UpdateMutation;
52use crate::executor::prelude::*;
53use crate::executor::source::reader_stream::{SourceReaderEventWithState, StreamReaderBuilder};
54use crate::executor::stream_reader::StreamReaderWithPause;
55use crate::task::LocalBarrierManager;
56
57pub const WAIT_BARRIER_MULTIPLE_TIMES: u128 = 5;
60
61fn lsn_u128_to_i64(lsn: u128) -> i64 {
62 lsn.min(i64::MAX as u128) as i64
63}
64
65fn update_mysql_cdc_state_file_seq_metric(
66 metric_guard: &mut Option<LabelGuardedIntGauge>,
67 metrics: &StreamingMetrics,
68 source_id: &str,
69 split_impl: &SplitImpl,
70) {
71 let SplitImpl::MysqlCdc(mysql_split) = split_impl else {
72 return;
73 };
74
75 if let Some((file_seq, _)) = mysql_split.mysql_binlog_offset() {
76 metric_guard
77 .get_or_insert_with(|| {
78 metrics
79 .mysql_cdc_state_binlog_file_seq
80 .with_guarded_label_values(&[source_id])
81 })
82 .set(file_seq as i64);
83 } else {
84 *metric_guard = None;
85 }
86}
87
88fn clear_mysql_cdc_state_file_seq_metric_if_unassigned(
89 metric_guard: &mut Option<LabelGuardedIntGauge>,
90 target_state: &HashMap<SplitId, SplitImpl>,
91) {
92 if !target_state
93 .values()
94 .any(|split| matches!(split, SplitImpl::MysqlCdc(_)))
95 {
96 *metric_guard = None;
97 }
98}
99
100fn update_mariadb_cdc_state_metrics(
101 metric_guards: &mut Option<(LabelGuardedIntGauge, LabelGuardedIntGauge)>,
102 metrics: &StreamingMetrics,
103 source_id: &str,
104 split_impl: &SplitImpl,
105) {
106 let SplitImpl::MariadbCdc(mariadb_split) = split_impl else {
107 return;
108 };
109
110 if let Some((file_seq, position)) = mariadb_split.mariadb_binlog_offset() {
111 let (file_seq_guard, position_guard) = metric_guards.get_or_insert_with(|| {
112 (
113 metrics
114 .mariadb_cdc_state_binlog_file_seq
115 .with_guarded_label_values(&[source_id]),
116 metrics
117 .mariadb_cdc_state_binlog_position
118 .with_guarded_label_values(&[source_id]),
119 )
120 });
121 file_seq_guard.set(file_seq as i64);
122 position_guard.set(position as i64);
123 } else {
124 *metric_guards = None;
125 }
126}
127
128fn clear_mariadb_cdc_state_metrics_if_unassigned(
129 metric_guards: &mut Option<(LabelGuardedIntGauge, LabelGuardedIntGauge)>,
130 target_state: &HashMap<SplitId, SplitImpl>,
131) {
132 if !target_state
133 .values()
134 .any(|split| matches!(split, SplitImpl::MariadbCdc(_)))
135 {
136 *metric_guards = None;
137 }
138}
139
140pub struct SourceExecutor<S: StateStore> {
141 actor_ctx: ActorContextRef,
142
143 stream_source_core: StreamSourceCore<S>,
145
146 metrics: Arc<StreamingMetrics>,
148
149 mysql_cdc_state_binlog_file_seq_guard: Option<LabelGuardedIntGauge>,
151 mariadb_cdc_state_metric_guards: Option<(LabelGuardedIntGauge, LabelGuardedIntGauge)>,
153
154 barrier_receiver: Option<UnboundedReceiver<Barrier>>,
156
157 system_params: SystemParamsReaderRef,
159
160 rate_limit_rps: Option<u32>,
162
163 is_shared_non_cdc: bool,
164
165 barrier_manager: LocalBarrierManager,
167}
168
169impl<S: StateStore> SourceExecutor<S> {
170 #[expect(clippy::too_many_arguments)]
171 pub fn new(
172 actor_ctx: ActorContextRef,
173 stream_source_core: StreamSourceCore<S>,
174 metrics: Arc<StreamingMetrics>,
175 barrier_receiver: UnboundedReceiver<Barrier>,
176 system_params: SystemParamsReaderRef,
177 rate_limit_rps: Option<u32>,
178 is_shared_non_cdc: bool,
179 barrier_manager: LocalBarrierManager,
180 ) -> Self {
181 Self {
182 actor_ctx,
183 stream_source_core,
184 metrics,
185 mysql_cdc_state_binlog_file_seq_guard: None,
186 mariadb_cdc_state_metric_guards: None,
187 barrier_receiver: Some(barrier_receiver),
188 system_params,
189 rate_limit_rps,
190 is_shared_non_cdc,
191 barrier_manager,
192 }
193 }
194
195 fn stream_reader_builder(&self, source_desc: SourceDesc) -> StreamReaderBuilder {
196 StreamReaderBuilder {
197 source_desc,
198 rate_limit: self.rate_limit_rps,
199 source_id: self.stream_source_core.source_id,
200 source_name: self.stream_source_core.source_name.clone(),
201 is_auto_schema_change_enable: self.is_auto_schema_change_enable(),
202 actor_ctx: self.actor_ctx.clone(),
203 reader_stream: None,
204 }
205 }
206
207 async fn spawn_wait_checkpoint_worker(
208 core: &StreamSourceCore<S>,
209 source_reader: SourceReader,
210 metrics: Arc<StreamingMetrics>,
211 ) -> StreamExecutorResult<Option<WaitCheckpointTaskBuilder>> {
212 let Some(initial_task) = source_reader.create_wait_checkpoint_task().await? else {
213 return Ok(None);
214 };
215 let (wait_checkpoint_tx, wait_checkpoint_rx) = mpsc::unbounded_channel();
216 let wait_checkpoint_worker = WaitCheckpointWorker {
217 wait_checkpoint_rx,
218 state_store: core.split_state_store.state_table().state_store().clone(),
219 table_id: core.split_state_store.state_table().table_id(),
220 source_id: core.source_id,
221 source_name: core.source_name.clone(),
222 metrics,
223 };
224 tokio::spawn(wait_checkpoint_worker.run());
225 Ok(Some(WaitCheckpointTaskBuilder {
226 wait_checkpoint_tx,
227 building_task: initial_task,
228 }))
229 }
230
231 fn is_auto_schema_change_enable(&self) -> bool {
232 self.actor_ctx.config.developer.enable_auto_schema_change
233 }
234
235 #[inline]
237 fn get_metric_labels(&self) -> [String; 4] {
238 [
239 self.actor_ctx.id.to_string(),
240 self.stream_source_core.source_id.to_string(),
241 self.stream_source_core.source_name.clone(),
242 self.actor_ctx.fragment_id.to_string(),
243 ]
244 }
245
246 async fn apply_split_change_after_yield_barrier<const BIASED: bool>(
255 &mut self,
256 barrier_epoch: EpochPair,
257 source_desc: &SourceDesc,
258 stream: &mut StreamReaderWithPause<BIASED, SourceReaderEventWithState>,
259 apply_mutation: ApplyMutationAfterBarrier<'_>,
260 ) -> StreamExecutorResult<()> {
261 {
262 let mut should_rebuild_stream = false;
263 match apply_mutation {
264 ApplyMutationAfterBarrier::SplitChange {
265 target_splits,
266 should_trim_state,
267 split_change_count,
268 } => {
269 split_change_count.inc();
270 if self
271 .update_state_if_changed(barrier_epoch, target_splits, should_trim_state)
272 .await?
273 {
274 should_rebuild_stream = true;
275 }
276 }
277 ApplyMutationAfterBarrier::ConnectorPropsChange => {
278 should_rebuild_stream = true;
279 }
280 }
281
282 if should_rebuild_stream {
283 self.rebuild_stream_reader(source_desc, stream)?;
284 }
285 }
286
287 Ok(())
288 }
289
290 async fn update_state_if_changed(
292 &mut self,
293 barrier_epoch: EpochPair,
294 target_splits: Vec<SplitImpl>,
295 should_trim_state: bool,
296 ) -> StreamExecutorResult<bool> {
297 let mysql_file_seq_metric_guard = &mut self.mysql_cdc_state_binlog_file_seq_guard;
298 let mariadb_metric_guards = &mut self.mariadb_cdc_state_metric_guards;
299 let core = &mut self.stream_source_core;
300
301 let target_splits: HashMap<_, _> = target_splits
302 .into_iter()
303 .map(|split| (split.id(), split))
304 .collect();
305
306 let mut target_state: HashMap<SplitId, SplitImpl> =
307 HashMap::with_capacity(target_splits.len());
308
309 let mut split_changed = false;
310
311 let committed_reader = core
312 .split_state_store
313 .new_committed_reader(barrier_epoch)
314 .await?;
315
316 for (split_id, split) in target_splits {
318 if let Some(s) = core.latest_split_info.get(&split_id) {
319 target_state.insert(split_id, s.clone());
322 } else {
323 split_changed = true;
324 let initial_state = if let Some(recover_state) = committed_reader
327 .try_recover_from_state_store(&split)
328 .await?
329 {
330 recover_state
331 } else {
332 split
333 };
334
335 core.updated_splits_in_epoch
336 .entry(split_id.clone())
337 .or_insert_with(|| initial_state.clone());
338
339 target_state.insert(split_id, initial_state);
340 }
341 }
342
343 for existing_split_id in core.latest_split_info.keys() {
345 if !target_state.contains_key(existing_split_id) {
346 tracing::info!("split dropping detected: {}", existing_split_id);
347 split_changed = true;
348 }
349 }
350
351 if split_changed {
352 tracing::info!(
353 actor_id = %self.actor_ctx.id,
354 state = ?target_state,
355 "apply split change"
356 );
357
358 core.updated_splits_in_epoch
359 .retain(|split_id, _| target_state.contains_key(split_id));
360
361 let dropped_splits = core
362 .latest_split_info
363 .extract_if(|split_id, _| !target_state.contains_key(split_id))
364 .map(|(_, split)| split)
365 .collect_vec();
366
367 if should_trim_state && !dropped_splits.is_empty() {
368 core.split_state_store.trim_state(&dropped_splits).await?;
370 }
371
372 core.latest_split_info = target_state;
373 }
374
375 clear_mysql_cdc_state_file_seq_metric_if_unassigned(
376 mysql_file_seq_metric_guard,
377 &core.latest_split_info,
378 );
379 clear_mariadb_cdc_state_metrics_if_unassigned(
380 mariadb_metric_guards,
381 &core.latest_split_info,
382 );
383
384 Ok(split_changed)
385 }
386
387 fn rebuild_stream_reader_from_error<const BIASED: bool>(
389 &mut self,
390 source_desc: &SourceDesc,
391 stream: &mut StreamReaderWithPause<BIASED, SourceReaderEventWithState>,
392 e: StreamExecutorError,
393 ) -> StreamExecutorResult<()> {
394 let core = &mut self.stream_source_core;
395 tracing::error!(
396 error = ?e.as_report(),
397 actor_id = %self.actor_ctx.id,
398 source_id = %core.source_id,
399 "stream source reader error",
400 );
401 GLOBAL_ERROR_METRICS.user_source_error.report([
402 e.variant_name().to_owned(),
403 core.source_id.to_string(),
404 core.source_name.clone(),
405 self.actor_ctx.fragment_id.to_string(),
406 ]);
407
408 self.rebuild_stream_reader(source_desc, stream)
409 }
410
411 fn rebuild_stream_reader<const BIASED: bool>(
412 &mut self,
413 source_desc: &SourceDesc,
414 stream: &mut StreamReaderWithPause<BIASED, SourceReaderEventWithState>,
415 ) -> StreamExecutorResult<()> {
416 let core = &mut self.stream_source_core;
417 let target_state: Vec<SplitImpl> = core.latest_split_info.values().cloned().collect();
418
419 tracing::info!(
420 "actor {:?} apply source split change to {:?}",
421 self.actor_ctx.id,
422 target_state
423 );
424
425 let reader_stream_builder = self.stream_reader_builder(source_desc.clone());
427 let reader_stream = reader_stream_builder.into_retry_stream(Some(target_state), false);
428
429 stream.replace_data_stream(reader_stream);
430
431 Ok(())
432 }
433
434 async fn handle_inject_source_offsets(
440 &mut self,
441 split_offsets: &HashMap<String, String>,
442 ) -> StreamExecutorResult<bool> {
443 tracing::warn!(
444 actor_id = %self.actor_ctx.id,
445 source_id = %self.stream_source_core.source_id,
446 num_offsets = split_offsets.len(),
447 "UNSAFE: Injecting source offsets - this may cause data duplication or loss"
448 );
449
450 let offsets_to_inject: Vec<(String, String)> = {
452 let owned_splits: HashSet<&str> = self
453 .stream_source_core
454 .latest_split_info
455 .keys()
456 .map(|s| s.as_ref())
457 .collect();
458 split_offsets
459 .iter()
460 .filter(|(split_id, _)| owned_splits.contains(split_id.as_str()))
461 .map(|(k, v)| (k.clone(), v.clone()))
462 .collect()
463 };
464
465 let mut json_states: Vec<(String, JsonbVal)> = Vec::new();
467 let mut failed_splits: Vec<String> = Vec::new();
468
469 for (split_id, offset) in &offsets_to_inject {
470 if let Some(split) = self
471 .stream_source_core
472 .latest_split_info
473 .get_mut(split_id.as_str())
474 {
475 tracing::info!(
476 actor_id = %self.actor_ctx.id,
477 split_id = %split_id,
478 offset = %offset,
479 "Injecting offset for owned split"
480 );
481 if let Err(e) = split.update_in_place(offset.clone()) {
483 tracing::error!(
484 actor_id = %self.actor_ctx.id,
485 split_id = %split_id,
486 error = ?e.as_report(),
487 "Failed to update split offset"
488 );
489 failed_splits.push(split_id.clone());
490 continue;
491 }
492 self.stream_source_core
494 .updated_splits_in_epoch
495 .insert(split_id.clone().into(), split.clone());
496 let json_value: serde_json::Value = serde_json::from_str(offset)
498 .unwrap_or_else(|_| serde_json::json!({ "offset": offset }));
499 json_states.push((split_id.clone(), JsonbVal::from(json_value)));
500 }
501 }
502
503 if !failed_splits.is_empty() {
504 return Err(StreamExecutorError::connector_error(anyhow!(
505 "failed to inject offsets for splits: {:?}",
506 failed_splits
507 )));
508 }
509
510 let num_injected = json_states.len();
511 if num_injected > 0 {
512 self.stream_source_core
514 .split_state_store
515 .set_states_json(json_states)
516 .await?;
517
518 tracing::info!(
519 actor_id = %self.actor_ctx.id,
520 source_id = %self.stream_source_core.source_id,
521 num_injected = num_injected,
522 "Offset injection completed for owned splits, triggering rebuild"
523 );
524 Ok(true)
525 } else {
526 tracing::info!(
527 actor_id = %self.actor_ctx.id,
528 source_id = %self.stream_source_core.source_id,
529 "No owned splits to inject offsets for"
530 );
531 Ok(false)
532 }
533 }
534
535 async fn persist_state_and_clear_cache(
536 &mut self,
537 epoch: EpochPair,
538 ) -> StreamExecutorResult<HashMap<SplitId, SplitImpl>> {
539 let core = &mut self.stream_source_core;
540 let mysql_file_seq_metric_guard = &mut self.mysql_cdc_state_binlog_file_seq_guard;
541 let mariadb_metric_guards = &mut self.mariadb_cdc_state_metric_guards;
542
543 let cache = core
544 .updated_splits_in_epoch
545 .values()
546 .map(|split_impl| split_impl.to_owned())
547 .collect_vec();
548
549 if !cache.is_empty() {
550 tracing::debug!(state = ?cache, "take snapshot");
551
552 let source_id = core.source_id.to_string();
554 for split_impl in &cache {
555 match split_impl {
557 SplitImpl::PostgresCdc(pg_split) => {
558 if let Some(lsn_value) = pg_split.pg_lsn() {
559 self.metrics
560 .pg_cdc_state_table_lsn
561 .with_guarded_label_values(&[&source_id])
562 .set(lsn_value as i64);
563 }
564 }
565 SplitImpl::MysqlCdc(mysql_split) => {
566 update_mysql_cdc_state_file_seq_metric(
567 mysql_file_seq_metric_guard,
568 &self.metrics,
569 &source_id,
570 split_impl,
571 );
572 if let Some((_, position)) = mysql_split.mysql_binlog_offset() {
573 self.metrics
574 .mysql_cdc_state_binlog_position
575 .with_guarded_label_values(&[&source_id])
576 .set(position as i64);
577 }
578 }
579 SplitImpl::MariadbCdc(_) => {
580 update_mariadb_cdc_state_metrics(
581 mariadb_metric_guards,
582 &self.metrics,
583 &source_id,
584 split_impl,
585 );
586 }
587 SplitImpl::SqlServerCdc(sqlserver_split) => {
588 if let Some(lsn) = sqlserver_split.sql_server_change_lsn() {
589 self.metrics
590 .sqlserver_cdc_state_change_lsn
591 .with_guarded_label_values(&[&source_id])
592 .set(lsn_u128_to_i64(lsn));
593 }
594 if let Some(lsn) = sqlserver_split.sql_server_commit_lsn() {
595 self.metrics
596 .sqlserver_cdc_state_commit_lsn
597 .with_guarded_label_values(&[&source_id])
598 .set(lsn_u128_to_i64(lsn));
599 }
600 }
601 _ => {}
602 }
603 }
604
605 core.split_state_store.set_states(cache).await?;
606 }
607
608 core.split_state_store.commit(epoch).await?;
610
611 let updated_splits = core.updated_splits_in_epoch.clone();
612
613 core.updated_splits_in_epoch.clear();
614
615 Ok(updated_splits)
616 }
617
618 async fn try_flush_data(&mut self) -> StreamExecutorResult<()> {
620 let core = &mut self.stream_source_core;
621 core.split_state_store.try_flush().await?;
622
623 Ok(())
624 }
625
626 fn apply_latest_split_state(&mut self, latest_state: HashMap<SplitId, SplitImpl>) {
627 for (split_id, new_split_impl) in &latest_state {
628 if let Some(split_impl) = self.stream_source_core.latest_split_info.get_mut(split_id) {
629 *split_impl = new_split_impl.clone();
630 }
631 }
632 self.stream_source_core
633 .updated_splits_in_epoch
634 .extend(latest_state);
635 }
636
637 fn maybe_report_cdc_source_offset(
639 &self,
640 updated_splits: &HashMap<SplitId, SplitImpl>,
641 epoch: EpochPair,
642 source_id: SourceId,
643 must_report_cdc_offset_once: &mut bool,
644 must_wait_cdc_offset_before_report: bool,
645 ) {
646 if *must_report_cdc_offset_once
648 && (!must_wait_cdc_offset_before_report
649 || updated_splits
650 .values()
651 .any(|split| split.is_cdc_split() && !split.get_cdc_split_offset().is_empty()))
652 {
653 self.barrier_manager.report_cdc_source_offset_updated(
655 epoch,
656 self.actor_ctx.id,
657 source_id,
658 );
659 tracing::info!(
660 actor_id = %self.actor_ctx.id,
661 source_id = %source_id,
662 epoch = ?epoch,
663 "Reported CDC source offset updated to meta (first time only)"
664 );
665 *must_report_cdc_offset_once = false;
667 }
668 }
669
670 #[try_stream(ok = Message, error = StreamExecutorError)]
675 async fn execute_inner(mut self) {
676 let mut barrier_receiver = self.barrier_receiver.take().unwrap();
677 let first_barrier = barrier_receiver
678 .recv()
679 .instrument_await("source_recv_first_barrier")
680 .await
681 .ok_or_else(|| {
682 anyhow!(
683 "failed to receive the first barrier, actor_id: {:?}, source_id: {:?}",
684 self.actor_ctx.id,
685 self.stream_source_core.source_id
686 )
687 })?;
688 let first_epoch = first_barrier.epoch;
689 let (mut boot_state, mut must_report_cdc_offset_once, must_wait_cdc_offset_before_report) =
691 if let Some(splits) = first_barrier.initial_split_assignment(self.actor_ctx.id) {
692 tracing::debug!(?splits, "boot with splits");
694 let must_report_cdc_offset_once = splits.iter().any(|split| split.is_cdc_split());
696 let must_wait_cdc_offset_before_report = must_report_cdc_offset_once
697 && splits.iter().any(|split| {
698 dispatch_cdc_source_type!(
699 CdcSourceType::from(split),
700 T,
701 T::NEEDS_NON_EMPTY_INITIAL_OFFSET,
702 false
703 )
704 });
705 (
706 splits.to_vec(),
707 must_report_cdc_offset_once,
708 must_wait_cdc_offset_before_report,
709 )
710 } else {
711 (Vec::default(), true, true)
712 };
713 let is_pause_on_startup = first_barrier.is_pause_on_startup();
714 let mut is_uninitialized = first_barrier.is_newly_added(self.actor_ctx.id);
715
716 yield Message::Barrier(first_barrier);
717
718 let mut core = self.stream_source_core;
719 let source_id = core.source_id;
720
721 let source_desc_builder: SourceDescBuilder = core.source_desc_builder.take().unwrap();
723 let mut source_desc = source_desc_builder
724 .build()
725 .map_err(StreamExecutorError::connector_error)?;
726
727 let mut wait_checkpoint_task_builder = Self::spawn_wait_checkpoint_worker(
728 &core,
729 source_desc.source.clone(),
730 self.metrics.clone(),
731 )
732 .await?;
733
734 let (Some(split_idx), Some(offset_idx), pulsar_message_id_idx) =
735 get_split_offset_col_idx(&source_desc.columns)
736 else {
737 unreachable!("Partition and offset columns must be set.");
738 };
739
740 core.split_state_store.init_epoch(first_epoch).await?;
741 {
742 let source_id = source_id.to_string();
743 let committed_reader = core
744 .split_state_store
745 .new_committed_reader(first_epoch)
746 .await?;
747 for ele in &mut boot_state {
748 if let Some(recover_state) =
749 committed_reader.try_recover_from_state_store(ele).await?
750 {
751 *ele = recover_state;
752 update_mysql_cdc_state_file_seq_metric(
753 &mut self.mysql_cdc_state_binlog_file_seq_guard,
754 &self.metrics,
755 &source_id,
756 ele,
757 );
758 update_mariadb_cdc_state_metrics(
759 &mut self.mariadb_cdc_state_metric_guards,
760 &self.metrics,
761 &source_id,
762 ele,
763 );
764 is_uninitialized = false;
766 } else {
767 core.updated_splits_in_epoch.insert(ele.id(), ele.clone());
771 }
772 }
773 }
774
775 core.init_split_state(boot_state.clone());
777
778 self.stream_source_core = core;
780
781 let recover_state: ConnectorState = (!boot_state.is_empty()).then_some(boot_state);
782 tracing::debug!(state = ?recover_state, "start with state");
783
784 let barrier_stream = barrier_to_message_stream(barrier_receiver).boxed();
785 let mut reader_stream_builder = self.stream_reader_builder(source_desc.clone());
786 let mut latest_splits = None;
787 if is_uninitialized {
789 let create_split_reader_result = reader_stream_builder
790 .fetch_latest_splits(recover_state.clone(), self.is_shared_non_cdc)
791 .await?;
792 latest_splits = create_split_reader_result.latest_splits;
793 }
794
795 if let Some(latest_splits) = latest_splits {
796 self.stream_source_core
799 .updated_splits_in_epoch
800 .extend(latest_splits.into_iter().map(|s| (s.id(), s)));
801 }
802 let mut stream = StreamReaderWithPause::<true, SourceReaderEventWithState>::new(
805 barrier_stream,
806 reader_stream_builder
807 .into_retry_stream(recover_state, is_uninitialized && self.is_shared_non_cdc),
808 );
809 let mut command_paused = false;
810
811 if is_pause_on_startup {
813 tracing::info!("source paused on startup");
814 stream.pause_stream();
815 command_paused = true;
816 }
817
818 let mut max_wait_barrier_time_ms =
821 self.system_params.load().barrier_interval_ms() as u128 * WAIT_BARRIER_MULTIPLE_TIMES;
822 let mut last_barrier_time = Instant::now();
823 let mut self_paused = false;
824
825 let source_output_row_count = self
826 .metrics
827 .source_output_row_count
828 .with_guarded_label_values(&self.get_metric_labels());
829
830 let source_split_change_count = self
831 .metrics
832 .source_split_change_count
833 .with_guarded_label_values(&self.get_metric_labels());
834
835 while let Some(msg) = stream.next().await {
836 let Ok(msg) = msg else {
837 tokio::time::sleep(Duration::from_millis(1000)).await;
838 self.rebuild_stream_reader_from_error(&source_desc, &mut stream, msg.unwrap_err())?;
839 continue;
840 };
841
842 match msg {
843 Either::Left(Message::Barrier(barrier)) => {
845 last_barrier_time = Instant::now();
846
847 if self_paused {
848 self_paused = false;
849 if !command_paused {
851 stream.resume_stream();
852 }
853 }
854
855 let epoch = barrier.epoch;
856 let mut split_change = None;
857
858 if let Some(mutation) = barrier.mutation.as_deref() {
859 match mutation {
860 Mutation::Pause => {
861 command_paused = true;
862 stream.pause_stream()
863 }
864 Mutation::Resume => {
865 command_paused = false;
866 stream.resume_stream()
867 }
868 Mutation::SourceChangeSplit(actor_splits) => {
869 tracing::info!(
870 actor_id = %self.actor_ctx.id,
871 actor_splits = ?actor_splits,
872 "source change split received"
873 );
874
875 split_change = actor_splits.get(&self.actor_ctx.id).cloned().map(
876 |target_splits| {
877 (
878 &source_desc,
879 &mut stream,
880 ApplyMutationAfterBarrier::SplitChange {
881 target_splits,
882 should_trim_state: true,
883 split_change_count: &source_split_change_count,
884 },
885 )
886 },
887 );
888 }
889
890 Mutation::ConnectorPropsChange(maybe_mutation) => {
891 if let Some(new_props) = maybe_mutation.get(&source_id.as_raw_id())
892 {
893 tracing::info!(
895 actor_id = %self.actor_ctx.id,
896 source_id = %source_id,
897 "updating source connector properties",
898 );
899 source_desc.update_reader(new_props.clone())?;
900 split_change = Some((
902 &source_desc,
903 &mut stream,
904 ApplyMutationAfterBarrier::ConnectorPropsChange,
905 ));
906 }
907 }
908
909 Mutation::Update(UpdateMutation { actor_splits, .. }) => {
910 split_change = actor_splits.get(&self.actor_ctx.id).cloned().map(
911 |target_splits| {
912 (
913 &source_desc,
914 &mut stream,
915 ApplyMutationAfterBarrier::SplitChange {
916 target_splits,
917 should_trim_state: false,
918 split_change_count: &source_split_change_count,
919 },
920 )
921 },
922 );
923 }
924 Mutation::Throttle(fragment_to_apply) => {
925 if let Some(entry) =
926 fragment_to_apply.get(&self.actor_ctx.fragment_id)
927 && entry.throttle_type() == ThrottleType::Source
928 && entry.rate_limit != self.rate_limit_rps
929 {
930 tracing::info!(
931 "updating rate limit from {:?} to {:?}",
932 self.rate_limit_rps,
933 entry.rate_limit
934 );
935 self.rate_limit_rps = entry.rate_limit;
936 self.rebuild_stream_reader(&source_desc, &mut stream)?;
938 }
939 }
940 Mutation::ResetSource { source_id } => {
941 if *source_id == self.stream_source_core.source_id {
945 tracing::info!(
946 actor_id = %self.actor_ctx.id,
947 source_id = source_id.as_raw_id(),
948 "Resetting CDC source: clearing offset (set to None)"
949 );
950
951 let splits_with_cleared_offset: Vec<SplitImpl> = self.stream_source_core
953 .latest_split_info
954 .values()
955 .map(|split| {
956 let mut new_split = split.clone();
958 match &mut new_split {
959 SplitImpl::MysqlCdc(debezium_split) => {
960 if let Some(mysql_split) = debezium_split.mysql_split.as_mut() {
961 tracing::info!(
962 split_id = ?mysql_split.inner.split_id,
963 old_offset = ?mysql_split.inner.start_offset,
964 "Clearing MySQL CDC offset"
965 );
966 mysql_split.inner.start_offset = None;
967 }
968 }
969 SplitImpl::MariadbCdc(debezium_split) => {
970 if let Some(mariadb_split) = debezium_split.mariadb_split.as_mut() {
971 tracing::info!(
972 split_id = ?mariadb_split.inner.split_id,
973 old_offset = ?mariadb_split.inner.start_offset,
974 "Clearing MariaDB CDC offset"
975 );
976 mariadb_split.inner.start_offset = None;
977 }
978 }
979 SplitImpl::PostgresCdc(debezium_split) => {
980 if let Some(pg_split) = debezium_split.postgres_split.as_mut() {
981 tracing::info!(
982 split_id = ?pg_split.inner.split_id,
983 old_offset = ?pg_split.inner.start_offset,
984 "Clearing PostgreSQL CDC offset"
985 );
986 pg_split.inner.start_offset = None;
987 }
988 }
989 SplitImpl::MongodbCdc(debezium_split) => {
990 if let Some(mongo_split) = debezium_split.mongodb_split.as_mut() {
991 tracing::info!(
992 split_id = ?mongo_split.inner.split_id,
993 old_offset = ?mongo_split.inner.start_offset,
994 "Clearing MongoDB CDC offset"
995 );
996 mongo_split.inner.start_offset = None;
997 }
998 }
999 SplitImpl::SqlServerCdc(debezium_split) => {
1000 if let Some(sqlserver_split) = debezium_split.sql_server_split.as_mut() {
1001 tracing::info!(
1002 split_id = ?sqlserver_split.inner.split_id,
1003 old_offset = ?sqlserver_split.inner.start_offset,
1004 "Clearing SQL Server CDC offset"
1005 );
1006 sqlserver_split.inner.start_offset = None;
1007 }
1008 }
1009 _ => {
1010 tracing::warn!(
1011 "RESET SOURCE called on non-CDC split type"
1012 );
1013 }
1014 }
1015 new_split
1016 })
1017 .collect();
1018
1019 if !splits_with_cleared_offset.is_empty() {
1020 tracing::info!(
1021 actor_id = %self.actor_ctx.id,
1022 split_count = splits_with_cleared_offset.len(),
1023 "Updating state table with cleared offsets"
1024 );
1025
1026 self.stream_source_core
1028 .split_state_store
1029 .set_states(splits_with_cleared_offset.clone())
1030 .await?;
1031
1032 for split in splits_with_cleared_offset {
1034 self.stream_source_core
1035 .latest_split_info
1036 .insert(split.id(), split.clone());
1037 self.stream_source_core
1038 .updated_splits_in_epoch
1039 .insert(split.id(), split);
1040 }
1041
1042 tracing::info!(
1043 actor_id = %self.actor_ctx.id,
1044 source_id = source_id.as_raw_id(),
1045 "RESET SOURCE completed: offset cleared (set to None). \
1046 Trigger recovery/restart to fetch latest offset from upstream."
1047 );
1048 } else {
1049 tracing::warn!(
1050 actor_id = %self.actor_ctx.id,
1051 "No splits found to reset - source may not be initialized yet"
1052 );
1053 }
1054 } else {
1055 tracing::debug!(
1056 actor_id = %self.actor_ctx.id,
1057 target_source_id = source_id.as_raw_id(),
1058 current_source_id = self.stream_source_core.source_id.as_raw_id(),
1059 "ResetSource mutation for different source, ignoring"
1060 );
1061 }
1062 }
1063
1064 Mutation::InjectSourceOffsets {
1065 source_id,
1066 split_offsets,
1067 } => {
1068 if *source_id == self.stream_source_core.source_id {
1069 if self.handle_inject_source_offsets(split_offsets).await? {
1070 split_change = Some((
1072 &source_desc,
1073 &mut stream,
1074 ApplyMutationAfterBarrier::ConnectorPropsChange,
1075 ));
1076 }
1077 } else {
1078 tracing::debug!(
1079 actor_id = %self.actor_ctx.id,
1080 target_source_id = source_id.as_raw_id(),
1081 current_source_id = self.stream_source_core.source_id.as_raw_id(),
1082 "InjectSourceOffsets mutation for different source, ignoring"
1083 );
1084 }
1085 }
1086
1087 _ => {}
1088 }
1089 }
1090
1091 let updated_splits = self.persist_state_and_clear_cache(epoch).await?;
1092
1093 self.maybe_report_cdc_source_offset(
1094 &updated_splits,
1095 epoch,
1096 source_id,
1097 &mut must_report_cdc_offset_once,
1098 must_wait_cdc_offset_before_report,
1099 );
1100
1101 if let Some(task_builder) = &mut wait_checkpoint_task_builder {
1102 task_builder.on_barrier(&barrier, updated_splits);
1103 }
1104
1105 let barrier_epoch = barrier.epoch;
1106 yield Message::Barrier(barrier);
1107
1108 if let Some((source_desc, stream, to_apply_mutation)) = split_change {
1109 self.apply_split_change_after_yield_barrier(
1110 barrier_epoch,
1111 source_desc,
1112 stream,
1113 to_apply_mutation,
1114 )
1115 .await?;
1116 }
1117 }
1118 Either::Left(_) => {
1119 unreachable!();
1122 }
1123
1124 Either::Right(event) => {
1125 let (chunk, latest_state) = match event {
1126 SourceReaderEventWithState::Progress(latest_state) => {
1127 self.apply_latest_split_state(latest_state);
1128 continue;
1129 }
1130 SourceReaderEventWithState::Data((chunk, latest_state)) => {
1131 (chunk, latest_state)
1132 }
1133 };
1134
1135 if let Some(task_builder) = &mut wait_checkpoint_task_builder {
1136 if let Some(pulsar_message_id_idx) = pulsar_message_id_idx {
1137 if chunk.capacity() > 0 {
1138 let (_, row, _) = chunk.row_at(0);
1141 let split_id = SplitId::from(
1142 row.datum_at(split_idx).unwrap().into_utf8().to_owned(),
1143 );
1144 let pulsar_message_id_col = chunk.column_at(pulsar_message_id_idx);
1145 task_builder.update_task_on_chunk(
1146 self.actor_ctx.id,
1147 source_id,
1148 Some(&split_id),
1149 pulsar_message_id_col.clone(),
1150 );
1151 }
1152 } else {
1153 let offset_col = chunk.column_at(offset_idx);
1154 task_builder.update_task_on_chunk(
1155 self.actor_ctx.id,
1156 source_id,
1157 None,
1158 offset_col.clone(),
1159 );
1160 }
1161 }
1162 if last_barrier_time.elapsed().as_millis() > max_wait_barrier_time_ms {
1163 self_paused = true;
1168 tracing::warn!(
1169 "source paused, wait barrier for {:?}",
1170 last_barrier_time.elapsed()
1171 );
1172 stream.pause_stream();
1173
1174 max_wait_barrier_time_ms = self.system_params.load().barrier_interval_ms()
1179 as u128
1180 * WAIT_BARRIER_MULTIPLE_TIMES;
1181 }
1182
1183 self.apply_latest_split_state(latest_state);
1184
1185 let card = chunk.cardinality();
1186 if card == 0 {
1187 continue;
1188 }
1189 source_output_row_count.inc_by(card as u64);
1190 let to_remove_col_indices =
1191 if let Some(pulsar_message_id_idx) = pulsar_message_id_idx {
1192 vec![split_idx, offset_idx, pulsar_message_id_idx]
1193 } else {
1194 vec![split_idx, offset_idx]
1195 };
1196 let chunk =
1197 prune_additional_cols(&chunk, &to_remove_col_indices, &source_desc.columns);
1198 yield Message::Chunk(chunk);
1199 self.try_flush_data().await?;
1200 }
1201 }
1202 }
1203
1204 tracing::error!(
1206 actor_id = %self.actor_ctx.id,
1207 "source executor exited unexpectedly"
1208 )
1209 }
1210}
1211
1212#[derive(Debug, Clone)]
1213enum ApplyMutationAfterBarrier<'a> {
1214 SplitChange {
1215 target_splits: Vec<SplitImpl>,
1216 should_trim_state: bool,
1217 split_change_count: &'a LabelGuardedMetric<GenericCounter<AtomicU64>>,
1218 },
1219 ConnectorPropsChange,
1220}
1221
1222impl<S: StateStore> Execute for SourceExecutor<S> {
1223 fn execute(self: Box<Self>) -> BoxedMessageStream {
1224 self.execute_inner().boxed()
1225 }
1226}
1227
1228impl<S: StateStore> Debug for SourceExecutor<S> {
1229 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1230 f.debug_struct("SourceExecutor")
1231 .field("source_id", &self.stream_source_core.source_id)
1232 .field("column_ids", &self.stream_source_core.column_ids)
1233 .finish()
1234 }
1235}
1236
1237struct WaitCheckpointTaskBuilder {
1238 wait_checkpoint_tx: UnboundedSender<(Epoch, WaitCheckpointTask)>,
1239 building_task: WaitCheckpointTask,
1240}
1241
1242impl WaitCheckpointTaskBuilder {
1243 fn update_task_on_chunk(
1244 &mut self,
1245 actor_id: ActorId,
1246 source_id: SourceId,
1247 pulsar_split_id: Option<&SplitId>,
1248 offset_col: ArrayRef,
1249 ) {
1250 match &mut self.building_task {
1251 WaitCheckpointTask::AckPubsubMessage(_, arrays) => {
1252 arrays.push(offset_col);
1253 }
1254 WaitCheckpointTask::AckNatsJetStream(_, arrays, _) => {
1255 arrays.push(offset_col);
1256 }
1257 WaitCheckpointTask::AckPulsarMessage(arrays) => {
1258 let split_id = pulsar_split_id.expect("Pulsar chunk must have a split ID");
1259 let pulsar_ack_channel_id =
1260 build_pulsar_ack_channel_id(source_id, split_id, actor_id);
1261 arrays.push((pulsar_ack_channel_id, offset_col));
1262 }
1263 WaitCheckpointTask::CommitCdcOffset(_) => {}
1264 }
1265 }
1266
1267 fn on_barrier(&mut self, barrier: &Barrier, updated_splits: HashMap<SplitId, SplitImpl>) {
1269 self.update_task_on_barrier(updated_splits);
1270 if barrier.kind.is_checkpoint() {
1271 tracing::debug!("epoch to wait {:?}", barrier.epoch);
1272 self.send(Epoch(barrier.epoch.prev));
1273 }
1274 }
1275
1276 fn update_task_on_barrier(&mut self, updated_splits: HashMap<SplitId, SplitImpl>) {
1277 if let WaitCheckpointTask::CommitCdcOffset(offsets) = &mut self.building_task
1278 && !updated_splits.is_empty()
1279 {
1280 assert_eq!(1, updated_splits.len());
1282 for (split_id, split_impl) in updated_splits {
1283 if split_impl.is_cdc_split() {
1284 *offsets = Some((split_id, split_impl.get_cdc_split_offset()));
1285 } else {
1286 unreachable!()
1287 }
1288 }
1289 }
1290 }
1291
1292 fn send(&mut self, epoch: Epoch) {
1296 let new_task = self.building_task.reset_for_next_epoch();
1297 self.wait_checkpoint_tx
1298 .send((epoch, std::mem::replace(&mut self.building_task, new_task)))
1299 .expect("wait_checkpoint_tx send should succeed");
1300 }
1301}
1302
1303struct WaitCheckpointWorker<S: StateStore> {
1329 wait_checkpoint_rx: UnboundedReceiver<(Epoch, WaitCheckpointTask)>,
1330 state_store: S,
1331 table_id: TableId,
1332 source_id: SourceId,
1333 source_name: String,
1334 metrics: Arc<StreamingMetrics>,
1335}
1336
1337impl<S: StateStore> WaitCheckpointWorker<S> {
1338 pub async fn run(mut self) {
1339 tracing::debug!("wait epoch worker start success");
1340 loop {
1341 match self.wait_checkpoint_rx.recv().await {
1343 Some((epoch, task)) => {
1344 tracing::debug!("start to wait epoch {}", epoch.0);
1345 let ret = self
1346 .state_store
1347 .try_wait_epoch(
1348 HummockReadEpoch::Committed(epoch.0),
1349 TryWaitEpochOptions {
1350 table_id: self.table_id,
1351 },
1352 )
1353 .await;
1354 if self.wait_checkpoint_rx.is_closed() {
1355 tracing::debug!(epoch = epoch.0, "Drop stale wait checkpoint task.");
1359 break;
1360 }
1361 match ret {
1362 Ok(()) => {
1363 tracing::debug!(epoch = epoch.0, "wait epoch success");
1364
1365 task.run_with_on_commit_success(
1367 self.source_id,
1368 &self.source_name,
1369 |source_id: u64, offset| {
1370 if let Some(lsn_value) =
1371 extract_postgres_lsn_from_offset_str(offset)
1372 {
1373 self.metrics
1374 .pg_cdc_jni_commit_offset_lsn
1375 .with_guarded_label_values(&[&source_id.to_string()])
1376 .set(lsn_value as i64);
1377 }
1378 if let Some(lsn_value) =
1379 extract_sql_server_commit_lsn_from_offset_str(offset)
1380 {
1381 self.metrics
1382 .sqlserver_cdc_jni_commit_offset_lsn
1383 .with_guarded_label_values(&[&source_id.to_string()])
1384 .set(lsn_u128_to_i64(lsn_value));
1385 }
1386 },
1387 )
1388 .await;
1389 }
1390 Err(e) => {
1391 tracing::error!(
1392 error = %e.as_report(),
1393 "wait epoch {} failed", epoch.0
1394 );
1395 }
1396 }
1397 }
1398 None => {
1399 tracing::error!("wait epoch rx closed");
1400 break;
1401 }
1402 }
1403 }
1404 }
1405}
1406
1407#[cfg(test)]
1408mod tests {
1409 use maplit::{btreemap, convert_args, hashmap};
1410 use prometheus::Registry;
1411 use prometheus::core::Collector;
1412 use risingwave_common::array::{Array, BytesArray};
1413 use risingwave_common::catalog::{ColumnId, Field};
1414 use risingwave_common::config::MetricLevel;
1415 use risingwave_common::id::SourceId;
1416 use risingwave_common::system_param::local_manager::LocalSystemParamsManager;
1417 use risingwave_common::test_prelude::StreamChunkTestExt;
1418 use risingwave_common::util::epoch::{EpochExt, test_epoch};
1419 use risingwave_connector::source::cdc::{DebeziumCdcSplit, Mariadb, Mysql};
1420 use risingwave_connector::source::datagen::DatagenSplit;
1421 use risingwave_connector::source::reader::desc::test_utils::create_source_desc_builder;
1422 use risingwave_pb::catalog::StreamSourceInfo;
1423 use risingwave_pb::plan_common::PbRowFormatType;
1424 use risingwave_pb::stream_plan::barrier::BarrierKind;
1425 use risingwave_storage::memory::MemoryStateStore;
1426 use tokio::sync::mpsc::unbounded_channel;
1427 use tracing_test::traced_test;
1428
1429 use super::*;
1430 use crate::executor::AddMutation;
1431 use crate::executor::source::{SourceStateTableHandler, default_source_internal_table};
1432 use crate::task::LocalBarrierManager;
1433
1434 const MOCK_SOURCE_NAME: &str = "mock_source";
1435
1436 fn message_ids<const N: usize>(values: [Option<&[u8]>; N]) -> ArrayRef {
1437 BytesArray::from_iter(values).into_ref()
1438 }
1439
1440 #[test]
1441 fn test_build_pulsar_ack_task_for_interleaved_splits() {
1442 let (wait_checkpoint_tx, _wait_checkpoint_rx) = unbounded_channel();
1443 let mut builder = WaitCheckpointTaskBuilder {
1444 wait_checkpoint_tx,
1445 building_task: WaitCheckpointTask::AckPulsarMessage(vec![]),
1446 };
1447 let actor_id = ActorId::new(11);
1448 let source_id = SourceId::new(7);
1449 let split_a = SplitId::from("persistent://public/default/topic-a");
1450 let split_b = SplitId::from("persistent://public/default/topic-b");
1451
1452 builder.update_task_on_chunk(
1453 actor_id,
1454 source_id,
1455 Some(&split_a),
1456 message_ids([Some(b"a-0")]),
1457 );
1458 builder.update_task_on_chunk(
1459 actor_id,
1460 source_id,
1461 Some(&split_b),
1462 message_ids([Some(b"b-0")]),
1463 );
1464 builder.update_task_on_chunk(
1465 actor_id,
1466 source_id,
1467 Some(&split_a),
1468 message_ids([Some(b"a-1")]),
1469 );
1470
1471 let WaitCheckpointTask::AckPulsarMessage(ack_arrays) = builder.building_task else {
1472 unreachable!();
1473 };
1474 let channel_ids = ack_arrays
1475 .into_iter()
1476 .map(|(channel_id, _)| channel_id)
1477 .collect_vec();
1478
1479 assert_eq!(
1480 channel_ids,
1481 vec![
1482 build_pulsar_ack_channel_id(source_id, &split_a, actor_id),
1483 build_pulsar_ack_channel_id(source_id, &split_b, actor_id),
1484 build_pulsar_ack_channel_id(source_id, &split_a, actor_id),
1485 ]
1486 );
1487 }
1488
1489 #[test]
1490 fn test_build_cdc_commit_task_across_non_checkpoint_barriers() {
1491 let (wait_checkpoint_tx, mut wait_checkpoint_rx) = unbounded_channel();
1492 let mut builder = WaitCheckpointTaskBuilder {
1493 wait_checkpoint_tx,
1494 building_task: WaitCheckpointTask::CommitCdcOffset(None),
1495 };
1496 let split = SplitImpl::PostgresCdc(DebeziumCdcSplit::new(1, Some("offset".to_owned())));
1497
1498 let non_checkpoint = Barrier {
1499 kind: BarrierKind::Barrier,
1500 ..Barrier::new_test_barrier(test_epoch(1))
1501 };
1502 builder.on_barrier(&non_checkpoint, hashmap! { split.id() => split.clone() });
1503 assert!(wait_checkpoint_rx.try_recv().is_err());
1504
1505 let checkpoint = Barrier::new_test_barrier(test_epoch(2));
1506 builder.on_barrier(&checkpoint, HashMap::new());
1507 let (epoch, task) = wait_checkpoint_rx.try_recv().unwrap();
1508 assert_eq!(epoch.0, checkpoint.epoch.prev);
1509 let WaitCheckpointTask::CommitCdcOffset(Some((split_id, offset))) = task else {
1510 panic!("the offset persisted at the non-checkpoint barrier should be committed");
1511 };
1512 assert_eq!(split_id, split.id());
1513 assert_eq!(offset, "offset");
1514
1515 builder.on_barrier(&Barrier::new_test_barrier(test_epoch(3)), HashMap::new());
1516 let (_, task) = wait_checkpoint_rx.try_recv().unwrap();
1517 assert!(matches!(task, WaitCheckpointTask::CommitCdcOffset(None)));
1518 }
1519
1520 #[tokio::test]
1521 async fn test_recovered_mysql_cdc_state_file_seq_metric_is_initialized()
1522 -> StreamExecutorResult<()> {
1523 let mut state_table_handler = SourceStateTableHandler::from_table_catalog(
1524 &default_source_internal_table(0x2333),
1525 MemoryStateStore::new(),
1526 )
1527 .await;
1528 let offset = r#"{"sourceOffset":{"file":"binlog.000123","pos":45678}}"#;
1529 let persisted_split = SplitImpl::MysqlCdc(DebeziumCdcSplit::<Mysql>::new(
1530 1001,
1531 Some(offset.to_owned()),
1532 ));
1533 let uninitialized_split = SplitImpl::MysqlCdc(DebeziumCdcSplit::<Mysql>::new(1001, None));
1534 let epoch_1 = EpochPair::new_test_epoch(test_epoch(1));
1535 let epoch_2 = EpochPair::new_test_epoch(test_epoch(2));
1536 let epoch_3 = EpochPair::new_test_epoch(test_epoch(3));
1537
1538 state_table_handler.init_epoch(epoch_1).await?;
1539 state_table_handler
1540 .set_states(vec![persisted_split.clone()])
1541 .await?;
1542 state_table_handler
1543 .state_table_mut()
1544 .commit_for_test(epoch_2)
1545 .await?;
1546 state_table_handler
1547 .state_table_mut()
1548 .commit_for_test(epoch_3)
1549 .await?;
1550
1551 let recovered_split = state_table_handler
1552 .new_committed_reader(epoch_3)
1553 .await?
1554 .try_recover_from_state_store(&uninitialized_split)
1555 .await?
1556 .unwrap();
1557 assert_eq!(recovered_split, persisted_split);
1558
1559 let registry = Registry::new();
1560 let metrics = StreamingMetrics::new(®istry, MetricLevel::Debug);
1561 let mut metric_guard = None;
1562 update_mysql_cdc_state_file_seq_metric(&mut metric_guard, &metrics, "1", &recovered_split);
1563
1564 for _ in 0..2 {
1565 let file_seq = metrics
1566 .mysql_cdc_state_binlog_file_seq
1567 .collect()
1568 .pop()
1569 .unwrap();
1570 assert_eq!(
1571 123.0,
1572 file_seq.get_metric()[0]
1573 .get_gauge()
1574 .as_ref()
1575 .unwrap()
1576 .value()
1577 );
1578 }
1579
1580 Ok(())
1581 }
1582
1583 #[tokio::test]
1584 async fn test_recovered_mariadb_cdc_state_metrics_are_initialized() -> StreamExecutorResult<()>
1585 {
1586 let mut state_table_handler = SourceStateTableHandler::from_table_catalog(
1587 &default_source_internal_table(0x2333),
1588 MemoryStateStore::new(),
1589 )
1590 .await;
1591 let offset = r#"{"sourceOffset":{"file":"binlog.000123","pos":45678}}"#;
1592 let persisted_split = SplitImpl::MariadbCdc(DebeziumCdcSplit::<Mariadb>::new(
1593 1001,
1594 Some(offset.to_owned()),
1595 ));
1596 let uninitialized_split =
1597 SplitImpl::MariadbCdc(DebeziumCdcSplit::<Mariadb>::new(1001, None));
1598 let epoch_1 = EpochPair::new_test_epoch(test_epoch(1));
1599 let epoch_2 = EpochPair::new_test_epoch(test_epoch(2));
1600 let epoch_3 = EpochPair::new_test_epoch(test_epoch(3));
1601
1602 state_table_handler.init_epoch(epoch_1).await?;
1603 state_table_handler
1604 .set_states(vec![persisted_split.clone()])
1605 .await?;
1606 state_table_handler
1607 .state_table_mut()
1608 .commit_for_test(epoch_2)
1609 .await?;
1610 state_table_handler
1611 .state_table_mut()
1612 .commit_for_test(epoch_3)
1613 .await?;
1614
1615 let recovered_split = state_table_handler
1616 .new_committed_reader(epoch_3)
1617 .await?
1618 .try_recover_from_state_store(&uninitialized_split)
1619 .await?
1620 .unwrap();
1621 assert_eq!(recovered_split, persisted_split);
1622
1623 let registry = Registry::new();
1624 let metrics = StreamingMetrics::new(®istry, MetricLevel::Debug);
1625 let mut metric_guard = None;
1626 update_mariadb_cdc_state_metrics(&mut metric_guard, &metrics, "1", &recovered_split);
1627
1628 for _ in 0..3 {
1630 for (metric, expected) in [
1631 (&metrics.mariadb_cdc_state_binlog_file_seq, 123.0),
1632 (&metrics.mariadb_cdc_state_binlog_position, 45678.0),
1633 ] {
1634 let family = metric.collect().pop().unwrap();
1635 assert_eq!(family.get_metric().len(), 1);
1636 assert_eq!(
1637 family.get_metric()[0].get_gauge().as_ref().unwrap().value(),
1638 expected
1639 );
1640 }
1641 }
1642
1643 Ok(())
1644 }
1645
1646 #[test]
1647 fn test_mariadb_cdc_state_metrics_are_cleared() {
1648 let registry = Registry::new();
1649 let metrics = StreamingMetrics::new(®istry, MetricLevel::Debug);
1650 let mut metric_guards = None;
1651 let split_with_offset = SplitImpl::MariadbCdc(DebeziumCdcSplit::<Mariadb>::new(
1652 1001,
1653 Some(r#"{"sourceOffset":{"file":"binlog.000123","pos":45678}}"#.to_owned()),
1654 ));
1655 let assigned = hashmap! { split_with_offset.id() => split_with_offset.clone() };
1656 for offset in [None, Some("invalid".to_owned())] {
1657 update_mariadb_cdc_state_metrics(&mut metric_guards, &metrics, "1", &split_with_offset);
1658 clear_mariadb_cdc_state_metrics_if_unassigned(&mut metric_guards, &assigned);
1659 assert!(metric_guards.is_some());
1660 let split = SplitImpl::MariadbCdc(DebeziumCdcSplit::<Mariadb>::new(1001, offset));
1661 update_mariadb_cdc_state_metrics(&mut metric_guards, &metrics, "1", &split);
1662 assert!(metric_guards.is_none());
1663 for metric in [
1664 &metrics.mariadb_cdc_state_binlog_file_seq,
1665 &metrics.mariadb_cdc_state_binlog_position,
1666 ] {
1667 drop(metric.collect());
1668 assert!(metric.collect().pop().unwrap().get_metric().is_empty());
1669 }
1670 }
1671 update_mariadb_cdc_state_metrics(&mut metric_guards, &metrics, "1", &split_with_offset);
1672 clear_mariadb_cdc_state_metrics_if_unassigned(&mut metric_guards, &HashMap::new());
1673 assert!(metric_guards.is_none());
1674 for metric in [
1675 &metrics.mariadb_cdc_state_binlog_file_seq,
1676 &metrics.mariadb_cdc_state_binlog_position,
1677 ] {
1678 drop(metric.collect());
1679 assert!(metric.collect().pop().unwrap().get_metric().is_empty());
1680 }
1681 }
1682
1683 #[test]
1684 fn test_mysql_cdc_state_file_seq_metric_is_cleared() {
1685 let registry = Registry::new();
1686 let metrics = StreamingMetrics::new(®istry, MetricLevel::Debug);
1687 let mut metric_guard = None;
1688 let split_with_offset = SplitImpl::MysqlCdc(DebeziumCdcSplit::<Mysql>::new(
1689 1001,
1690 Some(r#"{"sourceOffset":{"file":"binlog.000123","pos":45678}}"#.to_owned()),
1691 ));
1692 let split_without_offset = SplitImpl::MysqlCdc(DebeziumCdcSplit::<Mysql>::new(1001, None));
1693
1694 update_mysql_cdc_state_file_seq_metric(
1695 &mut metric_guard,
1696 &metrics,
1697 "1",
1698 &split_with_offset,
1699 );
1700 assert!(metric_guard.is_some());
1701
1702 update_mysql_cdc_state_file_seq_metric(
1703 &mut metric_guard,
1704 &metrics,
1705 "1",
1706 &split_without_offset,
1707 );
1708 assert!(metric_guard.is_none());
1709
1710 drop(metrics.mysql_cdc_state_binlog_file_seq.collect());
1711 assert!(
1712 metrics
1713 .mysql_cdc_state_binlog_file_seq
1714 .collect()
1715 .pop()
1716 .unwrap()
1717 .get_metric()
1718 .is_empty()
1719 );
1720
1721 update_mysql_cdc_state_file_seq_metric(
1722 &mut metric_guard,
1723 &metrics,
1724 "1",
1725 &split_with_offset,
1726 );
1727 assert!(metric_guard.is_some());
1728
1729 clear_mysql_cdc_state_file_seq_metric_if_unassigned(&mut metric_guard, &HashMap::new());
1730 assert!(metric_guard.is_none());
1731
1732 drop(metrics.mysql_cdc_state_binlog_file_seq.collect());
1733 assert!(
1734 metrics
1735 .mysql_cdc_state_binlog_file_seq
1736 .collect()
1737 .pop()
1738 .unwrap()
1739 .get_metric()
1740 .is_empty()
1741 );
1742 }
1743
1744 #[tokio::test]
1745 async fn test_source_executor() {
1746 let source_id = 0.into();
1747 let schema = Schema {
1748 fields: vec![Field::with_name(DataType::Int32, "sequence_int")],
1749 };
1750 let row_id_index = None;
1751 let source_info = StreamSourceInfo {
1752 row_format: PbRowFormatType::Native as i32,
1753 ..Default::default()
1754 };
1755 let (barrier_tx, barrier_rx) = unbounded_channel::<Barrier>();
1756 let column_ids = vec![0].into_iter().map(ColumnId::from).collect();
1757
1758 let properties = convert_args!(btreemap!(
1760 "connector" => "datagen",
1761 "datagen.rows.per.second" => "3",
1762 "fields.sequence_int.kind" => "sequence",
1763 "fields.sequence_int.start" => "11",
1764 "fields.sequence_int.end" => "11111",
1765 ));
1766 let source_desc_builder =
1767 create_source_desc_builder(&schema, row_id_index, source_info, properties, vec![]);
1768 let split_state_store = SourceStateTableHandler::from_table_catalog(
1769 &default_source_internal_table(0x2333),
1770 MemoryStateStore::new(),
1771 )
1772 .await;
1773 let core = StreamSourceCore::<MemoryStateStore> {
1774 source_id,
1775 column_ids,
1776 source_desc_builder: Some(source_desc_builder),
1777 latest_split_info: HashMap::new(),
1778 split_state_store,
1779 updated_splits_in_epoch: HashMap::new(),
1780 source_name: MOCK_SOURCE_NAME.to_owned(),
1781 };
1782
1783 let system_params_manager = LocalSystemParamsManager::for_test();
1784
1785 let executor = SourceExecutor::new(
1786 ActorContext::for_test(0),
1787 core,
1788 Arc::new(StreamingMetrics::unused()),
1789 barrier_rx,
1790 system_params_manager.get_params(),
1791 None,
1792 false,
1793 LocalBarrierManager::for_test(),
1794 );
1795 let mut executor = executor.boxed().execute();
1796
1797 let init_barrier =
1798 Barrier::new_test_barrier(test_epoch(1)).with_mutation(Mutation::Add(AddMutation {
1799 splits: hashmap! {
1800 ActorId::default() => vec![
1801 SplitImpl::Datagen(DatagenSplit {
1802 split_index: 0,
1803 split_num: 1,
1804 start_offset: None,
1805 }),
1806 ],
1807 },
1808 ..Default::default()
1809 }));
1810 barrier_tx.send(init_barrier).unwrap();
1811
1812 executor.next().await.unwrap().unwrap();
1814
1815 let msg = executor.next().await.unwrap().unwrap();
1817
1818 assert_eq!(
1820 msg.into_chunk().unwrap(),
1821 StreamChunk::from_pretty(
1822 " i
1823 + 11
1824 + 12
1825 + 13"
1826 )
1827 );
1828 }
1829
1830 #[traced_test]
1831 #[tokio::test]
1832 async fn test_split_change_mutation() {
1833 let source_id = SourceId::new(0);
1834 let schema = Schema {
1835 fields: vec![Field::with_name(DataType::Int32, "v1")],
1836 };
1837 let row_id_index = None;
1838 let source_info = StreamSourceInfo {
1839 row_format: PbRowFormatType::Native as i32,
1840 ..Default::default()
1841 };
1842 let properties = convert_args!(btreemap!(
1843 "connector" => "datagen",
1844 "fields.v1.kind" => "sequence",
1845 "fields.v1.start" => "11",
1846 "fields.v1.end" => "11111",
1847 ));
1848
1849 let source_desc_builder =
1850 create_source_desc_builder(&schema, row_id_index, source_info, properties, vec![]);
1851 let mem_state_store = MemoryStateStore::new();
1852
1853 let column_ids = vec![ColumnId::from(0)];
1854 let (barrier_tx, barrier_rx) = unbounded_channel::<Barrier>();
1855 let split_state_store = SourceStateTableHandler::from_table_catalog(
1856 &default_source_internal_table(0x2333),
1857 mem_state_store.clone(),
1858 )
1859 .await;
1860
1861 let core = StreamSourceCore::<MemoryStateStore> {
1862 source_id,
1863 column_ids: column_ids.clone(),
1864 source_desc_builder: Some(source_desc_builder),
1865 latest_split_info: HashMap::new(),
1866 split_state_store,
1867 updated_splits_in_epoch: HashMap::new(),
1868 source_name: MOCK_SOURCE_NAME.to_owned(),
1869 };
1870
1871 let system_params_manager = LocalSystemParamsManager::for_test();
1872
1873 let executor = SourceExecutor::new(
1874 ActorContext::for_test(0),
1875 core,
1876 Arc::new(StreamingMetrics::unused()),
1877 barrier_rx,
1878 system_params_manager.get_params(),
1879 None,
1880 false,
1881 LocalBarrierManager::for_test(),
1882 );
1883 let mut handler = executor.boxed().execute();
1884
1885 let mut epoch = test_epoch(1);
1886 let init_barrier =
1887 Barrier::new_test_barrier(epoch).with_mutation(Mutation::Add(AddMutation {
1888 splits: hashmap! {
1889 ActorId::default() => vec![
1890 SplitImpl::Datagen(DatagenSplit {
1891 split_index: 0,
1892 split_num: 3,
1893 start_offset: None,
1894 }),
1895 ],
1896 },
1897 ..Default::default()
1898 }));
1899 barrier_tx.send(init_barrier).unwrap();
1900
1901 handler
1903 .next()
1904 .await
1905 .unwrap()
1906 .unwrap()
1907 .into_barrier()
1908 .unwrap();
1909
1910 let mut ready_chunks = handler.ready_chunks(10);
1911
1912 let _ = ready_chunks.next().await.unwrap();
1913
1914 let new_assignment = vec![
1915 SplitImpl::Datagen(DatagenSplit {
1916 split_index: 0,
1917 split_num: 3,
1918 start_offset: None,
1919 }),
1920 SplitImpl::Datagen(DatagenSplit {
1921 split_index: 1,
1922 split_num: 3,
1923 start_offset: None,
1924 }),
1925 SplitImpl::Datagen(DatagenSplit {
1926 split_index: 2,
1927 split_num: 3,
1928 start_offset: None,
1929 }),
1930 ];
1931
1932 epoch.inc_epoch();
1933 let change_split_mutation =
1934 Barrier::new_test_barrier(epoch).with_mutation(Mutation::SourceChangeSplit(hashmap! {
1935 ActorId::default() => new_assignment.clone()
1936 }));
1937
1938 barrier_tx.send(change_split_mutation).unwrap();
1939
1940 let _ = ready_chunks.next().await.unwrap(); epoch.inc_epoch();
1943 let barrier = Barrier::new_test_barrier(epoch);
1944 barrier_tx.send(barrier).unwrap();
1945
1946 ready_chunks.next().await.unwrap(); let mut source_state_handler = SourceStateTableHandler::from_table_catalog(
1949 &default_source_internal_table(0x2333),
1950 mem_state_store.clone(),
1951 )
1952 .await;
1953
1954 source_state_handler
1956 .init_epoch(EpochPair::new_test_epoch(epoch))
1957 .await
1958 .unwrap();
1959 source_state_handler
1960 .get(&new_assignment[1].id())
1961 .await
1962 .unwrap()
1963 .unwrap();
1964
1965 tokio::time::sleep(Duration::from_millis(100)).await;
1966
1967 let _ = ready_chunks.next().await.unwrap();
1968
1969 epoch.inc_epoch();
1970 let barrier = Barrier::new_test_barrier(epoch).with_mutation(Mutation::Pause);
1971 barrier_tx.send(barrier).unwrap();
1972
1973 epoch.inc_epoch();
1974 let barrier = Barrier::new_test_barrier(epoch).with_mutation(Mutation::Resume);
1975 barrier_tx.send(barrier).unwrap();
1976
1977 ready_chunks.next().await.unwrap();
1979
1980 let prev_assignment = new_assignment;
1981 let new_assignment = vec![prev_assignment[2].clone()];
1982
1983 epoch.inc_epoch();
1984 let drop_split_mutation =
1985 Barrier::new_test_barrier(epoch).with_mutation(Mutation::SourceChangeSplit(hashmap! {
1986 ActorId::default() => new_assignment.clone()
1987 }));
1988
1989 barrier_tx.send(drop_split_mutation).unwrap();
1990
1991 ready_chunks.next().await.unwrap(); epoch.inc_epoch();
1994 let barrier = Barrier::new_test_barrier(epoch);
1995 barrier_tx.send(barrier).unwrap();
1996
1997 ready_chunks.next().await.unwrap(); let mut source_state_handler = SourceStateTableHandler::from_table_catalog(
2000 &default_source_internal_table(0x2333),
2001 mem_state_store.clone(),
2002 )
2003 .await;
2004
2005 let new_epoch = EpochPair::new_test_epoch(epoch);
2006 source_state_handler.init_epoch(new_epoch).await.unwrap();
2007
2008 let committed_reader = source_state_handler
2009 .new_committed_reader(new_epoch)
2010 .await
2011 .unwrap();
2012 assert!(
2013 committed_reader
2014 .try_recover_from_state_store(&prev_assignment[0])
2015 .await
2016 .unwrap()
2017 .is_none()
2018 );
2019
2020 assert!(
2021 committed_reader
2022 .try_recover_from_state_store(&prev_assignment[1])
2023 .await
2024 .unwrap()
2025 .is_none()
2026 );
2027
2028 assert!(
2029 committed_reader
2030 .try_recover_from_state_store(&prev_assignment[2])
2031 .await
2032 .unwrap()
2033 .is_some()
2034 );
2035 }
2036}