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