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