Skip to main content

risingwave_stream/executor/source/
source_executor.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
55/// A constant to multiply when calculating the maximum time to wait for a barrier. This is due to
56/// some latencies in network and cost in meta.
57pub 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    /// Streaming source for external
102    stream_source_core: StreamSourceCore<S>,
103
104    /// Metrics for monitor.
105    metrics: Arc<StreamingMetrics>,
106
107    /// Keep the MySQL CDC state file sequence metric alive while this executor owns the split.
108    mysql_cdc_state_binlog_file_seq_guard: Option<LabelGuardedIntGauge>,
109
110    /// Receiver of barrier channel.
111    barrier_receiver: Option<UnboundedReceiver<Barrier>>,
112
113    /// System parameter reader to read barrier interval
114    system_params: SystemParamsReaderRef,
115
116    /// Rate limit in rows/s.
117    rate_limit_rps: Option<u32>,
118
119    is_shared_non_cdc: bool,
120
121    /// Local barrier manager for reporting source load finished events
122    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    /// `source_id | source_name | actor_id | fragment_id`
191    #[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    /// - `should_trim_state`: whether to trim state for dropped splits.
202    ///
203    ///   For scaling, the connector splits can be migrated to other actors, but
204    ///   won't be added or removed. Actors should not trim states for splits that
205    ///   are moved to other actors.
206    ///
207    ///   For source split change, split will not be migrated and we can trim states
208    ///   for deleted splits.
209    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    /// Returns `true` if split changed. Otherwise `false`.
246    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        // Checks added splits
271        for (split_id, split) in target_splits {
272            if let Some(s) = core.latest_split_info.get(&split_id) {
273                // For existing splits, we should use the latest offset from the cache.
274                // `target_splits` is from meta and contains the initial offset.
275                target_state.insert(split_id, s.clone());
276            } else {
277                split_changed = true;
278                // write new assigned split to state cache. snapshot is base on cache.
279
280                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        // Checks dropped splits
298        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                // trim dropped splits' state
323                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    /// Rebuild stream if there is a err in stream
338    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        // Replace the source reader with a new one of the new state.
376        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    /// Handle `InjectSourceOffsets` mutation by updating split offsets in place.
385    /// Returns `true` if any offsets were injected and a stream rebuild is needed.
386    ///
387    /// This is an UNSAFE operation that can cause data duplication or loss
388    /// depending on the correctness of the provided offsets.
389    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        // First, filter to only offsets for splits this actor owns
401        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        // Update splits and prepare JSON states for persistence
416        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                // Update the split's offset in place
432                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                // Mark this split as updated for persistence
443                self.stream_source_core
444                    .updated_splits_in_epoch
445                    .insert(split_id.clone().into(), split.clone());
446                // Parse the offset as JSON and store it
447                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            // Store the injected offsets as JSON in the state table
463            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            // Record metrics for CDC sources before moving cache
502            let source_id = core.source_id.to_string();
503            for split_impl in &cache {
504                // Extract and record CDC-specific metrics based on split type
505                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        // commit anyway, even if no message saved
550        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    /// try mem table spill
560    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    /// Report CDC source offset updated only the first time.
579    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        // Report CDC source offset updated only the first time
588        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            // Report only once, then ignore all subsequent offset updates
595            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            // Mark as reported to prevent any future reports, even if offset changes
607            *must_report_cdc_offset_once = false;
608        }
609    }
610
611    /// A source executor with a stream source receives:
612    /// 1. Barrier messages
613    /// 2. Data from external source
614    /// and acts accordingly.
615    #[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        // must_report_cdc_offset is true if and only if the source is a CDC source.
631        // must_wait_cdc_offset_before_report is true if and only if the source is a MySQL or SQL Server CDC source.
632        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                // CDC source must reach this branch.
635                tracing::debug!(?splits, "boot with splits");
636                // Skip report for non-CDC.
637                let must_report_cdc_offset_once = splits.iter().any(|split| split.is_cdc_split());
638                // Only for MySQL and SQL Server CDC, we need to wait for the offset to be non-empty before reporting.
639                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        // Build source description from the builder.
660        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                    // if state store is non-empty, we consider it's initialized.
697                    is_uninitialized = false;
698                } else {
699                    // This is a new split, not in state table.
700                    // make sure it is written to state table later.
701                    // Then even it receives no messages, we can observe it in state table.
702                    core.updated_splits_in_epoch.insert(ele.id(), ele.clone());
703                }
704            }
705        }
706
707        // init in-memory split states with persisted state if any
708        core.init_split_state(boot_state.clone());
709
710        // Return the ownership of `stream_source_core` to the source executor.
711        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        // Build the source stream reader.
720        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            // make sure it is written to state table later.
729            // Then even it receives no messages, we can observe it in state table.
730            self.stream_source_core
731                .updated_splits_in_epoch
732                .extend(latest_splits.into_iter().map(|s| (s.id(), s)));
733        }
734        // Merge the chunks from source and the barriers into a single stream. We prioritize
735        // barriers over source data chunks here.
736        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 the first barrier requires us to pause on startup, pause the stream.
744        if is_pause_on_startup {
745            tracing::info!("source paused on startup");
746            stream.pause_stream();
747            command_paused = true;
748        }
749
750        // We allow data to flow for `WAIT_BARRIER_MULTIPLE_TIMES` * `expected_barrier_latency_ms`
751        // milliseconds, considering some other latencies like network and cost in Meta.
752        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                // This branch will be preferred.
776                Either::Left(Message::Barrier(barrier)) => {
777                    last_barrier_time = Instant::now();
778
779                    if self_paused {
780                        self_paused = false;
781                        // command_paused has a higher priority.
782                        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                                    // rebuild the stream reader with new props
826                                    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                                    // suppose the connector props change will not involve state change
833                                    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                                    // recreate from latest_split_info
869                                    self.rebuild_stream_reader(&source_desc, &mut stream)?;
870                                }
871                            }
872                            Mutation::ResetSource { source_id } => {
873                                // Note: RESET SOURCE only clears the offset, does NOT pause the source.
874                                // When offset is None, after recovery/restart, Debezium will automatically
875                                // enter recovery mode and fetch the latest offset from upstream.
876                                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                                    // Step 1: Collect all current splits and clear their offsets
884                                    let splits_with_cleared_offset: Vec<SplitImpl> = self.stream_source_core
885                                        .latest_split_info
886                                        .values()
887                                        .map(|split| {
888                                            // Clone the split and clear its offset
889                                            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                                        // Step 2: Write splits back to state table with offset = None
959                                        self.stream_source_core
960                                            .split_state_store
961                                            .set_states(splits_with_cleared_offset.clone())
962                                            .await?;
963
964                                        // Step 3: Update in-memory split info with cleared offsets
965                                        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                                        // Trigger a rebuild to apply the new offsets
1003                                        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                    // when handle a checkpoint barrier, spawn a task to wait for epoch commit notification
1034                    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                    // For the source executor, the message we receive from this arm
1058                    // should always be barrier message.
1059                    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                                // Each Pulsar chunk comes from one split, so all rows have the same
1077                                // split ID.
1078                                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                        // Exceeds the max wait barrier time, the source will be paused.
1102                        // Currently we can guarantee the
1103                        // source is not paused since it received stream
1104                        // chunks.
1105                        self_paused = true;
1106                        tracing::warn!(
1107                            "source paused, wait barrier for {:?}",
1108                            last_barrier_time.elapsed()
1109                        );
1110                        stream.pause_stream();
1111
1112                        // Only update `max_wait_barrier_time_ms` to capture
1113                        // `barrier_interval_ms`
1114                        // changes here to avoid frequently accessing the shared
1115                        // `system_params`.
1116                        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        // The source executor should only be stopped by the actor when finding a `Stop` mutation.
1143        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            // cdc source only has one split
1210            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    /// Send the current task and reset to an empty one for the next epoch.
1222    /// Uses `reset_for_next_epoch()` to clone the client handle (e.g. `PubSub` `Subscription`)
1223    /// from the current task, avoiding network I/O on the checkpoint hot path.
1224    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
1232/// A worker used to do some work after each checkpoint epoch is committed.
1233///
1234/// # Usage Cases
1235///
1236/// Typically there are 2 issues related with ack on checkpoint:
1237///
1238/// 1. Correctness (at-least-once), or don't let upstream clean uncommitted data.
1239///    For message queueing semantics (delete after ack), we should ack to avoid redelivery,
1240///    and only ack after checkpoint to avoid data loss.
1241///
1242/// 2. Allow upstream to clean data after commit.
1243///
1244/// See also <https://github.com/risingwavelabs/risingwave/issues/16736#issuecomment-2109379790>
1245///
1246/// ## CDC
1247///
1248/// Commit last consumed offset to upstream DB, so that old data can be discarded.
1249///
1250/// ## Google Pub/Sub
1251///
1252/// Due to queueing semantics.
1253/// Although Pub/Sub supports `retain_acked_messages` and `seek` functionality,
1254/// it's quite limited unlike Kafka.
1255///
1256/// See also <https://cloud.google.com/pubsub/docs/subscribe-best-practices#process-messages>
1257struct 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            // poll the rx and wait for the epoch commit
1271            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                        // The task must not be committed since the associated epoch may not succeed.
1285                        // The wait_checkpoint_rx lifetime is tied to the lifecycle of the source executor actor.
1286                        // The old actor must be dropped before any subsequent recovery can succeed; see PartialGraphState::abort_and_wait_actors
1287                        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                            // Run task with callback to record LSN after successful commit
1295                            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(&registry, 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(&registry, 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        // This datagen will generate 3 rows at one time.
1560        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        // Consume barrier.
1614        executor.next().await.unwrap().unwrap();
1615
1616        // Consume data chunk.
1617        let msg = executor.next().await.unwrap().unwrap();
1618
1619        // Row id will not be filled here.
1620        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        // Consume barrier.
1703        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(); // barrier
1742
1743        epoch.inc_epoch();
1744        let barrier = Barrier::new_test_barrier(epoch);
1745        barrier_tx.send(barrier).unwrap();
1746
1747        ready_chunks.next().await.unwrap(); // barrier
1748
1749        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        // there must exist state for new add partition
1756        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        // receive all
1779        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(); // barrier
1793
1794        epoch.inc_epoch();
1795        let barrier = Barrier::new_test_barrier(epoch);
1796        barrier_tx.send(barrier).unwrap();
1797
1798        ready_chunks.next().await.unwrap(); // barrier
1799
1800        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}