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::SqlServerCdc(debezium_split) => {
927 if let Some(sqlserver_split) = debezium_split.sql_server_split.as_mut() {
928 tracing::info!(
929 split_id = ?sqlserver_split.inner.split_id,
930 old_offset = ?sqlserver_split.inner.start_offset,
931 "Clearing SQL Server CDC offset"
932 );
933 sqlserver_split.inner.start_offset = None;
934 }
935 }
936 _ => {
937 tracing::warn!(
938 "RESET SOURCE called on non-CDC split type"
939 );
940 }
941 }
942 new_split
943 })
944 .collect();
945
946 if !splits_with_cleared_offset.is_empty() {
947 tracing::info!(
948 actor_id = %self.actor_ctx.id,
949 split_count = splits_with_cleared_offset.len(),
950 "Updating state table with cleared offsets"
951 );
952
953 self.stream_source_core
955 .split_state_store
956 .set_states(splits_with_cleared_offset.clone())
957 .await?;
958
959 for split in splits_with_cleared_offset {
961 self.stream_source_core
962 .latest_split_info
963 .insert(split.id(), split.clone());
964 self.stream_source_core
965 .updated_splits_in_epoch
966 .insert(split.id(), split);
967 }
968
969 tracing::info!(
970 actor_id = %self.actor_ctx.id,
971 source_id = source_id.as_raw_id(),
972 "RESET SOURCE completed: offset cleared (set to None). \
973 Trigger recovery/restart to fetch latest offset from upstream."
974 );
975 } else {
976 tracing::warn!(
977 actor_id = %self.actor_ctx.id,
978 "No splits found to reset - source may not be initialized yet"
979 );
980 }
981 } else {
982 tracing::debug!(
983 actor_id = %self.actor_ctx.id,
984 target_source_id = source_id.as_raw_id(),
985 current_source_id = self.stream_source_core.source_id.as_raw_id(),
986 "ResetSource mutation for different source, ignoring"
987 );
988 }
989 }
990
991 Mutation::InjectSourceOffsets {
992 source_id,
993 split_offsets,
994 } => {
995 if *source_id == self.stream_source_core.source_id {
996 if self.handle_inject_source_offsets(split_offsets).await? {
997 split_change = Some((
999 &source_desc,
1000 &mut stream,
1001 ApplyMutationAfterBarrier::ConnectorPropsChange,
1002 ));
1003 }
1004 } else {
1005 tracing::debug!(
1006 actor_id = %self.actor_ctx.id,
1007 target_source_id = source_id.as_raw_id(),
1008 current_source_id = self.stream_source_core.source_id.as_raw_id(),
1009 "InjectSourceOffsets mutation for different source, ignoring"
1010 );
1011 }
1012 }
1013
1014 _ => {}
1015 }
1016 }
1017
1018 let updated_splits = self.persist_state_and_clear_cache(epoch).await?;
1019
1020 self.maybe_report_cdc_source_offset(
1021 &updated_splits,
1022 epoch,
1023 source_id,
1024 &mut must_report_cdc_offset_once,
1025 must_wait_cdc_offset_before_report,
1026 );
1027
1028 if let Some(task_builder) = &mut wait_checkpoint_task_builder {
1029 task_builder.on_barrier(&barrier, updated_splits);
1030 }
1031
1032 let barrier_epoch = barrier.epoch;
1033 yield Message::Barrier(barrier);
1034
1035 if let Some((source_desc, stream, to_apply_mutation)) = split_change {
1036 self.apply_split_change_after_yield_barrier(
1037 barrier_epoch,
1038 source_desc,
1039 stream,
1040 to_apply_mutation,
1041 )
1042 .await?;
1043 }
1044 }
1045 Either::Left(_) => {
1046 unreachable!();
1049 }
1050
1051 Either::Right(event) => {
1052 let (chunk, latest_state) = match event {
1053 SourceReaderEventWithState::Progress(latest_state) => {
1054 self.apply_latest_split_state(latest_state);
1055 continue;
1056 }
1057 SourceReaderEventWithState::Data((chunk, latest_state)) => {
1058 (chunk, latest_state)
1059 }
1060 };
1061
1062 if let Some(task_builder) = &mut wait_checkpoint_task_builder {
1063 if let Some(pulsar_message_id_idx) = pulsar_message_id_idx {
1064 if chunk.capacity() > 0 {
1065 let (_, row, _) = chunk.row_at(0);
1068 let split_id = SplitId::from(
1069 row.datum_at(split_idx).unwrap().into_utf8().to_owned(),
1070 );
1071 let pulsar_message_id_col = chunk.column_at(pulsar_message_id_idx);
1072 task_builder.update_task_on_chunk(
1073 self.actor_ctx.id,
1074 source_id,
1075 Some(&split_id),
1076 pulsar_message_id_col.clone(),
1077 );
1078 }
1079 } else {
1080 let offset_col = chunk.column_at(offset_idx);
1081 task_builder.update_task_on_chunk(
1082 self.actor_ctx.id,
1083 source_id,
1084 None,
1085 offset_col.clone(),
1086 );
1087 }
1088 }
1089 if last_barrier_time.elapsed().as_millis() > max_wait_barrier_time_ms {
1090 self_paused = true;
1095 tracing::warn!(
1096 "source paused, wait barrier for {:?}",
1097 last_barrier_time.elapsed()
1098 );
1099 stream.pause_stream();
1100
1101 max_wait_barrier_time_ms = self.system_params.load().barrier_interval_ms()
1106 as u128
1107 * WAIT_BARRIER_MULTIPLE_TIMES;
1108 }
1109
1110 self.apply_latest_split_state(latest_state);
1111
1112 let card = chunk.cardinality();
1113 if card == 0 {
1114 continue;
1115 }
1116 source_output_row_count.inc_by(card as u64);
1117 let to_remove_col_indices =
1118 if let Some(pulsar_message_id_idx) = pulsar_message_id_idx {
1119 vec![split_idx, offset_idx, pulsar_message_id_idx]
1120 } else {
1121 vec![split_idx, offset_idx]
1122 };
1123 let chunk =
1124 prune_additional_cols(&chunk, &to_remove_col_indices, &source_desc.columns);
1125 yield Message::Chunk(chunk);
1126 self.try_flush_data().await?;
1127 }
1128 }
1129 }
1130
1131 tracing::error!(
1133 actor_id = %self.actor_ctx.id,
1134 "source executor exited unexpectedly"
1135 )
1136 }
1137}
1138
1139#[derive(Debug, Clone)]
1140enum ApplyMutationAfterBarrier<'a> {
1141 SplitChange {
1142 target_splits: Vec<SplitImpl>,
1143 should_trim_state: bool,
1144 split_change_count: &'a LabelGuardedMetric<GenericCounter<AtomicU64>>,
1145 },
1146 ConnectorPropsChange,
1147}
1148
1149impl<S: StateStore> Execute for SourceExecutor<S> {
1150 fn execute(self: Box<Self>) -> BoxedMessageStream {
1151 self.execute_inner().boxed()
1152 }
1153}
1154
1155impl<S: StateStore> Debug for SourceExecutor<S> {
1156 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1157 f.debug_struct("SourceExecutor")
1158 .field("source_id", &self.stream_source_core.source_id)
1159 .field("column_ids", &self.stream_source_core.column_ids)
1160 .finish()
1161 }
1162}
1163
1164struct WaitCheckpointTaskBuilder {
1165 wait_checkpoint_tx: UnboundedSender<(Epoch, WaitCheckpointTask)>,
1166 building_task: WaitCheckpointTask,
1167}
1168
1169impl WaitCheckpointTaskBuilder {
1170 fn update_task_on_chunk(
1171 &mut self,
1172 actor_id: ActorId,
1173 source_id: SourceId,
1174 pulsar_split_id: Option<&SplitId>,
1175 offset_col: ArrayRef,
1176 ) {
1177 match &mut self.building_task {
1178 WaitCheckpointTask::AckPubsubMessage(_, arrays) => {
1179 arrays.push(offset_col);
1180 }
1181 WaitCheckpointTask::AckNatsJetStream(_, arrays, _) => {
1182 arrays.push(offset_col);
1183 }
1184 WaitCheckpointTask::AckPulsarMessage(arrays) => {
1185 let split_id = pulsar_split_id.expect("Pulsar chunk must have a split ID");
1186 let pulsar_ack_channel_id =
1187 build_pulsar_ack_channel_id(source_id, split_id, actor_id);
1188 arrays.push((pulsar_ack_channel_id, offset_col));
1189 }
1190 WaitCheckpointTask::CommitCdcOffset(_) => {}
1191 }
1192 }
1193
1194 fn on_barrier(&mut self, barrier: &Barrier, updated_splits: HashMap<SplitId, SplitImpl>) {
1196 self.update_task_on_barrier(updated_splits);
1197 if barrier.kind.is_checkpoint() {
1198 tracing::debug!("epoch to wait {:?}", barrier.epoch);
1199 self.send(Epoch(barrier.epoch.prev));
1200 }
1201 }
1202
1203 fn update_task_on_barrier(&mut self, updated_splits: HashMap<SplitId, SplitImpl>) {
1204 if let WaitCheckpointTask::CommitCdcOffset(offsets) = &mut self.building_task
1205 && !updated_splits.is_empty()
1206 {
1207 assert_eq!(1, updated_splits.len());
1209 for (split_id, split_impl) in updated_splits {
1210 if split_impl.is_cdc_split() {
1211 *offsets = Some((split_id, split_impl.get_cdc_split_offset()));
1212 } else {
1213 unreachable!()
1214 }
1215 }
1216 }
1217 }
1218
1219 fn send(&mut self, epoch: Epoch) {
1223 let new_task = self.building_task.reset_for_next_epoch();
1224 self.wait_checkpoint_tx
1225 .send((epoch, std::mem::replace(&mut self.building_task, new_task)))
1226 .expect("wait_checkpoint_tx send should succeed");
1227 }
1228}
1229
1230struct WaitCheckpointWorker<S: StateStore> {
1256 wait_checkpoint_rx: UnboundedReceiver<(Epoch, WaitCheckpointTask)>,
1257 state_store: S,
1258 table_id: TableId,
1259 source_id: SourceId,
1260 source_name: String,
1261 metrics: Arc<StreamingMetrics>,
1262}
1263
1264impl<S: StateStore> WaitCheckpointWorker<S> {
1265 pub async fn run(mut self) {
1266 tracing::debug!("wait epoch worker start success");
1267 loop {
1268 match self.wait_checkpoint_rx.recv().await {
1270 Some((epoch, task)) => {
1271 tracing::debug!("start to wait epoch {}", epoch.0);
1272 let ret = self
1273 .state_store
1274 .try_wait_epoch(
1275 HummockReadEpoch::Committed(epoch.0),
1276 TryWaitEpochOptions {
1277 table_id: self.table_id,
1278 },
1279 )
1280 .await;
1281 if self.wait_checkpoint_rx.is_closed() {
1282 tracing::debug!(epoch = epoch.0, "Drop stale wait checkpoint task.");
1286 break;
1287 }
1288 match ret {
1289 Ok(()) => {
1290 tracing::debug!(epoch = epoch.0, "wait epoch success");
1291
1292 task.run_with_on_commit_success(
1294 self.source_id,
1295 &self.source_name,
1296 |source_id: u64, offset| {
1297 if let Some(lsn_value) =
1298 extract_postgres_lsn_from_offset_str(offset)
1299 {
1300 self.metrics
1301 .pg_cdc_jni_commit_offset_lsn
1302 .with_guarded_label_values(&[&source_id.to_string()])
1303 .set(lsn_value as i64);
1304 }
1305 if let Some(lsn_value) =
1306 extract_sql_server_commit_lsn_from_offset_str(offset)
1307 {
1308 self.metrics
1309 .sqlserver_cdc_jni_commit_offset_lsn
1310 .with_guarded_label_values(&[&source_id.to_string()])
1311 .set(lsn_u128_to_i64(lsn_value));
1312 }
1313 },
1314 )
1315 .await;
1316 }
1317 Err(e) => {
1318 tracing::error!(
1319 error = %e.as_report(),
1320 "wait epoch {} failed", epoch.0
1321 );
1322 }
1323 }
1324 }
1325 None => {
1326 tracing::error!("wait epoch rx closed");
1327 break;
1328 }
1329 }
1330 }
1331 }
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use maplit::{btreemap, convert_args, hashmap};
1337 use prometheus::Registry;
1338 use prometheus::core::Collector;
1339 use risingwave_common::array::{Array, BytesArray};
1340 use risingwave_common::catalog::{ColumnId, Field};
1341 use risingwave_common::config::MetricLevel;
1342 use risingwave_common::id::SourceId;
1343 use risingwave_common::system_param::local_manager::LocalSystemParamsManager;
1344 use risingwave_common::test_prelude::StreamChunkTestExt;
1345 use risingwave_common::util::epoch::{EpochExt, test_epoch};
1346 use risingwave_connector::source::cdc::{DebeziumCdcSplit, Mysql};
1347 use risingwave_connector::source::datagen::DatagenSplit;
1348 use risingwave_connector::source::reader::desc::test_utils::create_source_desc_builder;
1349 use risingwave_pb::catalog::StreamSourceInfo;
1350 use risingwave_pb::plan_common::PbRowFormatType;
1351 use risingwave_pb::stream_plan::barrier::BarrierKind;
1352 use risingwave_storage::memory::MemoryStateStore;
1353 use tokio::sync::mpsc::unbounded_channel;
1354 use tracing_test::traced_test;
1355
1356 use super::*;
1357 use crate::executor::AddMutation;
1358 use crate::executor::source::{SourceStateTableHandler, default_source_internal_table};
1359 use crate::task::LocalBarrierManager;
1360
1361 const MOCK_SOURCE_NAME: &str = "mock_source";
1362
1363 fn message_ids<const N: usize>(values: [Option<&[u8]>; N]) -> ArrayRef {
1364 BytesArray::from_iter(values).into_ref()
1365 }
1366
1367 #[test]
1368 fn test_build_pulsar_ack_task_for_interleaved_splits() {
1369 let (wait_checkpoint_tx, _wait_checkpoint_rx) = unbounded_channel();
1370 let mut builder = WaitCheckpointTaskBuilder {
1371 wait_checkpoint_tx,
1372 building_task: WaitCheckpointTask::AckPulsarMessage(vec![]),
1373 };
1374 let actor_id = ActorId::new(11);
1375 let source_id = SourceId::new(7);
1376 let split_a = SplitId::from("persistent://public/default/topic-a");
1377 let split_b = SplitId::from("persistent://public/default/topic-b");
1378
1379 builder.update_task_on_chunk(
1380 actor_id,
1381 source_id,
1382 Some(&split_a),
1383 message_ids([Some(b"a-0")]),
1384 );
1385 builder.update_task_on_chunk(
1386 actor_id,
1387 source_id,
1388 Some(&split_b),
1389 message_ids([Some(b"b-0")]),
1390 );
1391 builder.update_task_on_chunk(
1392 actor_id,
1393 source_id,
1394 Some(&split_a),
1395 message_ids([Some(b"a-1")]),
1396 );
1397
1398 let WaitCheckpointTask::AckPulsarMessage(ack_arrays) = builder.building_task else {
1399 unreachable!();
1400 };
1401 let channel_ids = ack_arrays
1402 .into_iter()
1403 .map(|(channel_id, _)| channel_id)
1404 .collect_vec();
1405
1406 assert_eq!(
1407 channel_ids,
1408 vec![
1409 build_pulsar_ack_channel_id(source_id, &split_a, actor_id),
1410 build_pulsar_ack_channel_id(source_id, &split_b, actor_id),
1411 build_pulsar_ack_channel_id(source_id, &split_a, actor_id),
1412 ]
1413 );
1414 }
1415
1416 #[test]
1417 fn test_build_cdc_commit_task_across_non_checkpoint_barriers() {
1418 let (wait_checkpoint_tx, mut wait_checkpoint_rx) = unbounded_channel();
1419 let mut builder = WaitCheckpointTaskBuilder {
1420 wait_checkpoint_tx,
1421 building_task: WaitCheckpointTask::CommitCdcOffset(None),
1422 };
1423 let split = SplitImpl::PostgresCdc(DebeziumCdcSplit::new(1, Some("offset".to_owned())));
1424
1425 let non_checkpoint = Barrier {
1426 kind: BarrierKind::Barrier,
1427 ..Barrier::new_test_barrier(test_epoch(1))
1428 };
1429 builder.on_barrier(&non_checkpoint, hashmap! { split.id() => split.clone() });
1430 assert!(wait_checkpoint_rx.try_recv().is_err());
1431
1432 let checkpoint = Barrier::new_test_barrier(test_epoch(2));
1433 builder.on_barrier(&checkpoint, HashMap::new());
1434 let (epoch, task) = wait_checkpoint_rx.try_recv().unwrap();
1435 assert_eq!(epoch.0, checkpoint.epoch.prev);
1436 let WaitCheckpointTask::CommitCdcOffset(Some((split_id, offset))) = task else {
1437 panic!("the offset persisted at the non-checkpoint barrier should be committed");
1438 };
1439 assert_eq!(split_id, split.id());
1440 assert_eq!(offset, "offset");
1441
1442 builder.on_barrier(&Barrier::new_test_barrier(test_epoch(3)), HashMap::new());
1443 let (_, task) = wait_checkpoint_rx.try_recv().unwrap();
1444 assert!(matches!(task, WaitCheckpointTask::CommitCdcOffset(None)));
1445 }
1446
1447 #[tokio::test]
1448 async fn test_recovered_mysql_cdc_state_file_seq_metric_is_initialized()
1449 -> StreamExecutorResult<()> {
1450 let mut state_table_handler = SourceStateTableHandler::from_table_catalog(
1451 &default_source_internal_table(0x2333),
1452 MemoryStateStore::new(),
1453 )
1454 .await;
1455 let offset = r#"{"sourceOffset":{"file":"binlog.000123","pos":45678}}"#;
1456 let persisted_split = SplitImpl::MysqlCdc(DebeziumCdcSplit::<Mysql>::new(
1457 1001,
1458 Some(offset.to_owned()),
1459 ));
1460 let uninitialized_split = SplitImpl::MysqlCdc(DebeziumCdcSplit::<Mysql>::new(1001, None));
1461 let epoch_1 = EpochPair::new_test_epoch(test_epoch(1));
1462 let epoch_2 = EpochPair::new_test_epoch(test_epoch(2));
1463 let epoch_3 = EpochPair::new_test_epoch(test_epoch(3));
1464
1465 state_table_handler.init_epoch(epoch_1).await?;
1466 state_table_handler
1467 .set_states(vec![persisted_split.clone()])
1468 .await?;
1469 state_table_handler
1470 .state_table_mut()
1471 .commit_for_test(epoch_2)
1472 .await?;
1473 state_table_handler
1474 .state_table_mut()
1475 .commit_for_test(epoch_3)
1476 .await?;
1477
1478 let recovered_split = state_table_handler
1479 .new_committed_reader(epoch_3)
1480 .await?
1481 .try_recover_from_state_store(&uninitialized_split)
1482 .await?
1483 .unwrap();
1484 assert_eq!(recovered_split, persisted_split);
1485
1486 let registry = Registry::new();
1487 let metrics = StreamingMetrics::new(®istry, MetricLevel::Debug);
1488 let mut metric_guard = None;
1489 update_mysql_cdc_state_file_seq_metric(&mut metric_guard, &metrics, "1", &recovered_split);
1490
1491 for _ in 0..2 {
1492 let file_seq = metrics
1493 .mysql_cdc_state_binlog_file_seq
1494 .collect()
1495 .pop()
1496 .unwrap();
1497 assert_eq!(
1498 123.0,
1499 file_seq.get_metric()[0]
1500 .get_gauge()
1501 .as_ref()
1502 .unwrap()
1503 .value()
1504 );
1505 }
1506
1507 Ok(())
1508 }
1509
1510 #[test]
1511 fn test_mysql_cdc_state_file_seq_metric_is_cleared() {
1512 let registry = Registry::new();
1513 let metrics = StreamingMetrics::new(®istry, MetricLevel::Debug);
1514 let mut metric_guard = None;
1515 let split_with_offset = SplitImpl::MysqlCdc(DebeziumCdcSplit::<Mysql>::new(
1516 1001,
1517 Some(r#"{"sourceOffset":{"file":"binlog.000123","pos":45678}}"#.to_owned()),
1518 ));
1519 let split_without_offset = SplitImpl::MysqlCdc(DebeziumCdcSplit::<Mysql>::new(1001, None));
1520
1521 update_mysql_cdc_state_file_seq_metric(
1522 &mut metric_guard,
1523 &metrics,
1524 "1",
1525 &split_with_offset,
1526 );
1527 assert!(metric_guard.is_some());
1528
1529 update_mysql_cdc_state_file_seq_metric(
1530 &mut metric_guard,
1531 &metrics,
1532 "1",
1533 &split_without_offset,
1534 );
1535 assert!(metric_guard.is_none());
1536
1537 drop(metrics.mysql_cdc_state_binlog_file_seq.collect());
1538 assert!(
1539 metrics
1540 .mysql_cdc_state_binlog_file_seq
1541 .collect()
1542 .pop()
1543 .unwrap()
1544 .get_metric()
1545 .is_empty()
1546 );
1547
1548 update_mysql_cdc_state_file_seq_metric(
1549 &mut metric_guard,
1550 &metrics,
1551 "1",
1552 &split_with_offset,
1553 );
1554 assert!(metric_guard.is_some());
1555
1556 clear_mysql_cdc_state_file_seq_metric_if_unassigned(&mut metric_guard, &HashMap::new());
1557 assert!(metric_guard.is_none());
1558
1559 drop(metrics.mysql_cdc_state_binlog_file_seq.collect());
1560 assert!(
1561 metrics
1562 .mysql_cdc_state_binlog_file_seq
1563 .collect()
1564 .pop()
1565 .unwrap()
1566 .get_metric()
1567 .is_empty()
1568 );
1569 }
1570
1571 #[tokio::test]
1572 async fn test_source_executor() {
1573 let source_id = 0.into();
1574 let schema = Schema {
1575 fields: vec![Field::with_name(DataType::Int32, "sequence_int")],
1576 };
1577 let row_id_index = None;
1578 let source_info = StreamSourceInfo {
1579 row_format: PbRowFormatType::Native as i32,
1580 ..Default::default()
1581 };
1582 let (barrier_tx, barrier_rx) = unbounded_channel::<Barrier>();
1583 let column_ids = vec![0].into_iter().map(ColumnId::from).collect();
1584
1585 let properties = convert_args!(btreemap!(
1587 "connector" => "datagen",
1588 "datagen.rows.per.second" => "3",
1589 "fields.sequence_int.kind" => "sequence",
1590 "fields.sequence_int.start" => "11",
1591 "fields.sequence_int.end" => "11111",
1592 ));
1593 let source_desc_builder =
1594 create_source_desc_builder(&schema, row_id_index, source_info, properties, vec![]);
1595 let split_state_store = SourceStateTableHandler::from_table_catalog(
1596 &default_source_internal_table(0x2333),
1597 MemoryStateStore::new(),
1598 )
1599 .await;
1600 let core = StreamSourceCore::<MemoryStateStore> {
1601 source_id,
1602 column_ids,
1603 source_desc_builder: Some(source_desc_builder),
1604 latest_split_info: HashMap::new(),
1605 split_state_store,
1606 updated_splits_in_epoch: HashMap::new(),
1607 source_name: MOCK_SOURCE_NAME.to_owned(),
1608 };
1609
1610 let system_params_manager = LocalSystemParamsManager::for_test();
1611
1612 let executor = SourceExecutor::new(
1613 ActorContext::for_test(0),
1614 core,
1615 Arc::new(StreamingMetrics::unused()),
1616 barrier_rx,
1617 system_params_manager.get_params(),
1618 None,
1619 false,
1620 LocalBarrierManager::for_test(),
1621 );
1622 let mut executor = executor.boxed().execute();
1623
1624 let init_barrier =
1625 Barrier::new_test_barrier(test_epoch(1)).with_mutation(Mutation::Add(AddMutation {
1626 splits: hashmap! {
1627 ActorId::default() => vec![
1628 SplitImpl::Datagen(DatagenSplit {
1629 split_index: 0,
1630 split_num: 1,
1631 start_offset: None,
1632 }),
1633 ],
1634 },
1635 ..Default::default()
1636 }));
1637 barrier_tx.send(init_barrier).unwrap();
1638
1639 executor.next().await.unwrap().unwrap();
1641
1642 let msg = executor.next().await.unwrap().unwrap();
1644
1645 assert_eq!(
1647 msg.into_chunk().unwrap(),
1648 StreamChunk::from_pretty(
1649 " i
1650 + 11
1651 + 12
1652 + 13"
1653 )
1654 );
1655 }
1656
1657 #[traced_test]
1658 #[tokio::test]
1659 async fn test_split_change_mutation() {
1660 let source_id = SourceId::new(0);
1661 let schema = Schema {
1662 fields: vec![Field::with_name(DataType::Int32, "v1")],
1663 };
1664 let row_id_index = None;
1665 let source_info = StreamSourceInfo {
1666 row_format: PbRowFormatType::Native as i32,
1667 ..Default::default()
1668 };
1669 let properties = convert_args!(btreemap!(
1670 "connector" => "datagen",
1671 "fields.v1.kind" => "sequence",
1672 "fields.v1.start" => "11",
1673 "fields.v1.end" => "11111",
1674 ));
1675
1676 let source_desc_builder =
1677 create_source_desc_builder(&schema, row_id_index, source_info, properties, vec![]);
1678 let mem_state_store = MemoryStateStore::new();
1679
1680 let column_ids = vec![ColumnId::from(0)];
1681 let (barrier_tx, barrier_rx) = unbounded_channel::<Barrier>();
1682 let split_state_store = SourceStateTableHandler::from_table_catalog(
1683 &default_source_internal_table(0x2333),
1684 mem_state_store.clone(),
1685 )
1686 .await;
1687
1688 let core = StreamSourceCore::<MemoryStateStore> {
1689 source_id,
1690 column_ids: column_ids.clone(),
1691 source_desc_builder: Some(source_desc_builder),
1692 latest_split_info: HashMap::new(),
1693 split_state_store,
1694 updated_splits_in_epoch: HashMap::new(),
1695 source_name: MOCK_SOURCE_NAME.to_owned(),
1696 };
1697
1698 let system_params_manager = LocalSystemParamsManager::for_test();
1699
1700 let executor = SourceExecutor::new(
1701 ActorContext::for_test(0),
1702 core,
1703 Arc::new(StreamingMetrics::unused()),
1704 barrier_rx,
1705 system_params_manager.get_params(),
1706 None,
1707 false,
1708 LocalBarrierManager::for_test(),
1709 );
1710 let mut handler = executor.boxed().execute();
1711
1712 let mut epoch = test_epoch(1);
1713 let init_barrier =
1714 Barrier::new_test_barrier(epoch).with_mutation(Mutation::Add(AddMutation {
1715 splits: hashmap! {
1716 ActorId::default() => vec![
1717 SplitImpl::Datagen(DatagenSplit {
1718 split_index: 0,
1719 split_num: 3,
1720 start_offset: None,
1721 }),
1722 ],
1723 },
1724 ..Default::default()
1725 }));
1726 barrier_tx.send(init_barrier).unwrap();
1727
1728 handler
1730 .next()
1731 .await
1732 .unwrap()
1733 .unwrap()
1734 .into_barrier()
1735 .unwrap();
1736
1737 let mut ready_chunks = handler.ready_chunks(10);
1738
1739 let _ = ready_chunks.next().await.unwrap();
1740
1741 let new_assignment = vec![
1742 SplitImpl::Datagen(DatagenSplit {
1743 split_index: 0,
1744 split_num: 3,
1745 start_offset: None,
1746 }),
1747 SplitImpl::Datagen(DatagenSplit {
1748 split_index: 1,
1749 split_num: 3,
1750 start_offset: None,
1751 }),
1752 SplitImpl::Datagen(DatagenSplit {
1753 split_index: 2,
1754 split_num: 3,
1755 start_offset: None,
1756 }),
1757 ];
1758
1759 epoch.inc_epoch();
1760 let change_split_mutation =
1761 Barrier::new_test_barrier(epoch).with_mutation(Mutation::SourceChangeSplit(hashmap! {
1762 ActorId::default() => new_assignment.clone()
1763 }));
1764
1765 barrier_tx.send(change_split_mutation).unwrap();
1766
1767 let _ = ready_chunks.next().await.unwrap(); epoch.inc_epoch();
1770 let barrier = Barrier::new_test_barrier(epoch);
1771 barrier_tx.send(barrier).unwrap();
1772
1773 ready_chunks.next().await.unwrap(); let mut source_state_handler = SourceStateTableHandler::from_table_catalog(
1776 &default_source_internal_table(0x2333),
1777 mem_state_store.clone(),
1778 )
1779 .await;
1780
1781 source_state_handler
1783 .init_epoch(EpochPair::new_test_epoch(epoch))
1784 .await
1785 .unwrap();
1786 source_state_handler
1787 .get(&new_assignment[1].id())
1788 .await
1789 .unwrap()
1790 .unwrap();
1791
1792 tokio::time::sleep(Duration::from_millis(100)).await;
1793
1794 let _ = ready_chunks.next().await.unwrap();
1795
1796 epoch.inc_epoch();
1797 let barrier = Barrier::new_test_barrier(epoch).with_mutation(Mutation::Pause);
1798 barrier_tx.send(barrier).unwrap();
1799
1800 epoch.inc_epoch();
1801 let barrier = Barrier::new_test_barrier(epoch).with_mutation(Mutation::Resume);
1802 barrier_tx.send(barrier).unwrap();
1803
1804 ready_chunks.next().await.unwrap();
1806
1807 let prev_assignment = new_assignment;
1808 let new_assignment = vec![prev_assignment[2].clone()];
1809
1810 epoch.inc_epoch();
1811 let drop_split_mutation =
1812 Barrier::new_test_barrier(epoch).with_mutation(Mutation::SourceChangeSplit(hashmap! {
1813 ActorId::default() => new_assignment.clone()
1814 }));
1815
1816 barrier_tx.send(drop_split_mutation).unwrap();
1817
1818 ready_chunks.next().await.unwrap(); epoch.inc_epoch();
1821 let barrier = Barrier::new_test_barrier(epoch);
1822 barrier_tx.send(barrier).unwrap();
1823
1824 ready_chunks.next().await.unwrap(); let mut source_state_handler = SourceStateTableHandler::from_table_catalog(
1827 &default_source_internal_table(0x2333),
1828 mem_state_store.clone(),
1829 )
1830 .await;
1831
1832 let new_epoch = EpochPair::new_test_epoch(epoch);
1833 source_state_handler.init_epoch(new_epoch).await.unwrap();
1834
1835 let committed_reader = source_state_handler
1836 .new_committed_reader(new_epoch)
1837 .await
1838 .unwrap();
1839 assert!(
1840 committed_reader
1841 .try_recover_from_state_store(&prev_assignment[0])
1842 .await
1843 .unwrap()
1844 .is_none()
1845 );
1846
1847 assert!(
1848 committed_reader
1849 .try_recover_from_state_store(&prev_assignment[1])
1850 .await
1851 .unwrap()
1852 .is_none()
1853 );
1854
1855 assert!(
1856 committed_reader
1857 .try_recover_from_state_store(&prev_assignment[2])
1858 .await
1859 .unwrap()
1860 .is_some()
1861 );
1862 }
1863}