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