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