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