Skip to main content

risingwave_stream/executor/backfill/cdc/
cdc_backill_v2.rs

1// Copyright 2025 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::BTreeMap;
16
17use either::Either;
18use futures::stream;
19use futures::stream::select_with_strategy;
20use itertools::Itertools;
21use risingwave_common::bitmap::BitmapBuilder;
22use risingwave_common::catalog::{ColumnDesc, Field};
23use risingwave_common::row::RowDeserializer;
24use risingwave_common::util::iter_util::ZipEqFast;
25use risingwave_common::util::sort_util::{OrderType, cmp_datum};
26use risingwave_connector::source::cdc::CdcScanOptions;
27use risingwave_connector::source::cdc::external::{
28    CdcOffset, ExternalCdcTableType, ExternalTableReaderImpl,
29};
30use risingwave_connector::source::{CdcTableSnapshotSplit, CdcTableSnapshotSplitRaw};
31use risingwave_pb::common::ThrottleType;
32use rw_futures_util::pausable;
33use thiserror_ext::AsReport;
34use tracing::Instrument;
35
36use crate::executor::backfill::cdc::cdc_backfill::{
37    build_reader_and_poll_upstream, get_cdc_json_parse_handling_from_properties, transform_upstream,
38};
39use crate::executor::backfill::cdc::state_v2::ParallelizedCdcBackfillState;
40use crate::executor::backfill::cdc::upstream_table::external::ExternalStorageTable;
41use crate::executor::backfill::cdc::upstream_table::snapshot::{
42    SplitSnapshotReadArgs, UpstreamTableRead, UpstreamTableReader,
43};
44use crate::executor::backfill::utils::{get_cdc_chunk_last_offset, mapping_chunk, mapping_message};
45use crate::executor::prelude::*;
46use crate::executor::source::get_infinite_backoff_strategy;
47use crate::task::cdc_progress::CdcProgressReporter;
48pub struct ParallelizedCdcBackfillExecutor<S: StateStore> {
49    actor_ctx: ActorContextRef,
50
51    /// The external table to be backfilled
52    external_table: ExternalStorageTable,
53
54    /// Upstream changelog stream which may contain metadata columns, e.g. `_rw_offset`
55    upstream: Executor,
56
57    /// The column indices need to be forwarded to the downstream from the upstream and table scan.
58    output_indices: Vec<usize>,
59
60    /// The schema of output chunk, including additional columns if any
61    output_columns: Vec<ColumnDesc>,
62
63    /// Rate limit in rows/s.
64    rate_limit_rps: Option<u32>,
65
66    options: CdcScanOptions,
67
68    state_table: StateTable<S>,
69
70    properties: BTreeMap<String, String>,
71
72    progress: Option<CdcProgressReporter>,
73}
74
75impl<S: StateStore> ParallelizedCdcBackfillExecutor<S> {
76    #[expect(clippy::too_many_arguments)]
77    pub fn new(
78        actor_ctx: ActorContextRef,
79        external_table: ExternalStorageTable,
80        upstream: Executor,
81        output_indices: Vec<usize>,
82        output_columns: Vec<ColumnDesc>,
83        _metrics: Arc<StreamingMetrics>,
84        state_table: StateTable<S>,
85        rate_limit_rps: Option<u32>,
86        options: CdcScanOptions,
87        properties: BTreeMap<String, String>,
88        progress: Option<CdcProgressReporter>,
89    ) -> Self {
90        Self {
91            actor_ctx,
92            external_table,
93            upstream,
94            output_indices,
95            output_columns,
96            rate_limit_rps,
97            options,
98            state_table,
99            properties,
100            progress,
101        }
102    }
103
104    #[try_stream(ok = Message, error = StreamExecutorError)]
105    async fn execute_inner(mut self) {
106        assert!(!self.options.disable_backfill);
107        // The indices to primary key columns
108        let pk_indices = self.external_table.pk_indices().to_vec();
109        let table_id = self.external_table.table_id();
110        let upstream_table_name = self.external_table.qualified_table_name();
111        let schema_table_name = self.external_table.schema_table_name().clone();
112        let external_database_name = self.external_table.database_name().to_owned();
113        let additional_columns = self
114            .output_columns
115            .iter()
116            .filter(|col| col.additional_column.column_type.is_some())
117            .cloned()
118            .collect_vec();
119        assert!(
120            (self.options.backfill_split_pk_column_index as usize) < pk_indices.len(),
121            "split pk column index {} out of bound",
122            self.options.backfill_split_pk_column_index
123        );
124        let snapshot_split_column_index =
125            pk_indices[self.options.backfill_split_pk_column_index as usize];
126        let cdc_table_snapshot_split_column =
127            vec![self.external_table.schema().fields[snapshot_split_column_index].clone()];
128
129        let mut upstream = self.upstream.execute();
130        // Poll the upstream to get the first barrier.
131        let first_barrier = expect_first_barrier(&mut upstream).await?;
132        // Make sure to use mapping_message after transform_upstream.
133
134        // If user sets debezium.time.precision.mode to "connect", it means the user can guarantee
135        // that the upstream data precision is MilliSecond. In this case, we don't use GuessNumberUnit
136        // mode to guess precision, but use Milli mode directly, which can handle extreme timestamps.
137        let (timestamp_handling, timestamptz_handling, time_handling, bigint_unsigned_handling) =
138            get_cdc_json_parse_handling_from_properties(&self.properties);
139        // Only postgres-cdc connector may trigger TOAST.
140        let handle_toast_columns: bool =
141            self.external_table.table_type() == &ExternalCdcTableType::Postgres;
142        let mut upstream = transform_upstream(
143            upstream,
144            self.output_columns.clone(),
145            timestamp_handling,
146            timestamptz_handling,
147            time_handling,
148            bigint_unsigned_handling,
149            handle_toast_columns,
150        )
151        .boxed();
152        let mut next_reset_barrier = Some(first_barrier);
153        let mut is_reset = false;
154        let mut state_impl = ParallelizedCdcBackfillState::new(self.state_table);
155        // The buffered chunks have already been mapped.
156        let mut upstream_chunk_buffer: Vec<StreamChunk> = vec![];
157
158        // Need reset on CDC table snapshot splits reschedule.
159        'with_cdc_table_snapshot_splits: loop {
160            assert!(upstream_chunk_buffer.is_empty());
161            let reset_barrier = next_reset_barrier.take().unwrap();
162            let all_snapshot_splits = match reset_barrier.mutation.as_deref() {
163                Some(Mutation::Add(add)) => &add.actor_cdc_table_snapshot_splits.splits,
164
165                Some(Mutation::Update(update)) => &update.actor_cdc_table_snapshot_splits.splits,
166                _ => {
167                    return Err(anyhow::anyhow!("ParallelizedCdcBackfillExecutor expects either Mutation::Add or Mutation::Update to initialize CDC table snapshot splits.").into());
168                }
169            };
170            let mut actor_snapshot_splits = vec![];
171            let mut generation = None;
172            // TODO(zw): optimization: remove consumed splits to reduce barrier size for downstream.
173            if let Some((splits, snapshot_generation)) = all_snapshot_splits.get(&self.actor_ctx.id)
174            {
175                actor_snapshot_splits = splits
176                    .iter()
177                    .map(|s: &CdcTableSnapshotSplitRaw| {
178                        let de = RowDeserializer::new(
179                            cdc_table_snapshot_split_column
180                                .iter()
181                                .map(Field::data_type)
182                                .collect_vec(),
183                        );
184                        let left_bound_inclusive =
185                            de.deserialize(s.left_bound_inclusive.as_ref()).unwrap();
186                        let right_bound_exclusive =
187                            de.deserialize(s.right_bound_exclusive.as_ref()).unwrap();
188                        CdcTableSnapshotSplit {
189                            split_id: s.split_id,
190                            left_bound_inclusive,
191                            right_bound_exclusive,
192                        }
193                    })
194                    .collect();
195                generation = Some(*snapshot_generation);
196            }
197            tracing::debug!(?actor_snapshot_splits, ?generation, "actor splits");
198            assert_consecutive_splits(&actor_snapshot_splits);
199
200            let mut is_snapshot_paused = reset_barrier.is_pause_on_startup();
201            let barrier_epoch = reset_barrier.epoch;
202            yield Message::Barrier(reset_barrier);
203            if !is_reset {
204                state_impl.init_epoch(barrier_epoch).await?;
205                is_reset = true;
206                tracing::info!(%table_id, "Initialize executor.");
207            } else {
208                tracing::info!(%table_id, "Reset executor.");
209            }
210
211            let mut current_actor_bounds = None;
212            let mut actor_cdc_offset_high: Option<CdcOffset> = None;
213            let mut actor_cdc_offset_low: Option<CdcOffset> = None;
214            // Find next split that need backfill.
215            let mut next_split_idx = actor_snapshot_splits.len();
216            for (idx, split) in actor_snapshot_splits.iter().enumerate() {
217                let state = state_impl.restore_state(split.split_id).await?;
218                if !state.is_finished {
219                    next_split_idx = idx;
220                    break;
221                }
222                extends_current_actor_bound(&mut current_actor_bounds, split);
223                if let Some(ref cdc_offset) = state.cdc_offset_low {
224                    if let Some(ref cur) = actor_cdc_offset_low {
225                        if *cur > *cdc_offset {
226                            actor_cdc_offset_low = state.cdc_offset_low.clone();
227                        }
228                    } else {
229                        actor_cdc_offset_low = state.cdc_offset_low.clone();
230                    }
231                }
232                if let Some(ref cdc_offset) = state.cdc_offset_high {
233                    if let Some(ref cur) = actor_cdc_offset_high {
234                        if *cur < *cdc_offset {
235                            actor_cdc_offset_high = state.cdc_offset_high.clone();
236                        }
237                    } else {
238                        actor_cdc_offset_high = state.cdc_offset_high.clone();
239                    }
240                }
241            }
242            for split in actor_snapshot_splits.iter().skip(next_split_idx) {
243                // Initialize state so that overall progress can be measured.
244                state_impl
245                    .mutate_state(split.split_id, false, 0, None, None)
246                    .await?;
247            }
248            let mut should_report_actor_backfill_progress = if next_split_idx > 0 {
249                Some((
250                    actor_snapshot_splits[0].split_id,
251                    actor_snapshot_splits[next_split_idx - 1].split_id,
252                ))
253            } else {
254                None
255            };
256
257            // After init the state table and forward the initial barrier to downstream,
258            // we now try to create the table reader with retry.
259            let mut table_reader: Option<ExternalTableReaderImpl> = None;
260            let external_table = self.external_table.clone();
261            let actor_id = self.actor_ctx.id;
262            let fragment_id = self.actor_ctx.fragment_id;
263            let mut future = Box::pin(async move {
264                let backoff = get_infinite_backoff_strategy();
265                tokio_retry::Retry::spawn(backoff, || async {
266                    match external_table.create_table_reader().await {
267                        Ok(reader) => Ok(reader),
268                        Err(e) => {
269                            tracing::warn!(error = %e.as_report(), actor_id = %actor_id, fragment_id = %fragment_id, "failed to create cdc table reader, retrying...");
270                            Err(e)
271                        }
272                    }
273                })
274                    .instrument(tracing::info_span!("create_cdc_table_reader_with_retry"))
275                    .await
276                    .expect("Retry create cdc table reader until success.")
277            });
278            loop {
279                if let Some(msg) =
280                    build_reader_and_poll_upstream(&mut upstream, &mut table_reader, &mut future)
281                        .await?
282                {
283                    if let Some(msg) = mapping_message(msg, &self.output_indices) {
284                        match msg {
285                            Message::Barrier(barrier) => {
286                                state_impl.commit_state(barrier.epoch).await?;
287                                if is_reset_barrier(&barrier, self.actor_ctx.id) {
288                                    next_reset_barrier = Some(barrier);
289                                    continue 'with_cdc_table_snapshot_splits;
290                                }
291                                yield Message::Barrier(barrier);
292                            }
293                            Message::Chunk(chunk) => {
294                                if chunk.cardinality() == 0 {
295                                    continue;
296                                }
297                                if let Some(filtered_chunk) = filter_stream_chunk(
298                                    chunk,
299                                    &current_actor_bounds,
300                                    snapshot_split_column_index,
301                                ) && filtered_chunk.cardinality() > 0
302                                {
303                                    yield Message::Chunk(filtered_chunk);
304                                }
305                            }
306                            Message::Watermark(_) => {
307                                // Ignore watermark, like the `CdcBackfillExecutor`.
308                            }
309                        }
310                    }
311                } else {
312                    assert!(table_reader.is_some(), "table reader must created");
313                    tracing::info!(
314                        %table_id,
315                        upstream_table_name,
316                        "table reader created successfully"
317                    );
318                    break;
319                }
320            }
321            let upstream_table_reader = UpstreamTableReader::new(
322                self.external_table.clone(),
323                table_reader.expect("table reader must created"),
324            );
325            // let mut upstream = upstream.peekable();
326            let offset_parse_func = upstream_table_reader.reader.get_cdc_offset_parser();
327
328            // Backfill snapshot splits sequentially.
329            for split in actor_snapshot_splits.iter().skip(next_split_idx) {
330                tracing::info!(
331                    %table_id,
332                    upstream_table_name,
333                    ?split,
334                    is_snapshot_paused,
335                    "start cdc backfill split"
336                );
337                let finished_split_bounds = current_actor_bounds.clone();
338                let current_split_bounds = Some((
339                    split.left_bound_inclusive.clone(),
340                    split.right_bound_exclusive.clone(),
341                ));
342                extends_current_actor_bound(&mut current_actor_bounds, split);
343
344                let split_cdc_offset_low = {
345                    // Limit concurrent CDC connections globally to 10 using a semaphore.
346                    static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
347                        tokio::sync::Semaphore::const_new(10);
348
349                    let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
350                    upstream_table_reader.current_cdc_offset().await?
351                };
352                if let Some(ref cdc_offset) = split_cdc_offset_low {
353                    if let Some(ref cur) = actor_cdc_offset_low {
354                        if *cur > *cdc_offset {
355                            actor_cdc_offset_low = split_cdc_offset_low.clone();
356                        }
357                    } else {
358                        actor_cdc_offset_low = split_cdc_offset_low.clone();
359                    }
360                }
361                let mut split_cdc_offset_high = None;
362
363                let left_upstream = upstream.by_ref().map(Either::Left);
364                let read_args = SplitSnapshotReadArgs::new(
365                    split.left_bound_inclusive.clone(),
366                    split.right_bound_exclusive.clone(),
367                    cdc_table_snapshot_split_column.clone(),
368                    self.rate_limit_rps,
369                    additional_columns.clone(),
370                    schema_table_name.clone(),
371                    external_database_name.clone(),
372                );
373                let right_snapshot = pin!(
374                    upstream_table_reader
375                        .snapshot_read_table_split(read_args)
376                        .map(Either::Right)
377                );
378                let (right_snapshot, snapshot_valve) = pausable(right_snapshot);
379                if is_snapshot_paused {
380                    snapshot_valve.pause();
381                }
382                let mut backfill_stream =
383                    select_with_strategy(left_upstream, right_snapshot, |_: &mut ()| {
384                        stream::PollNext::Left
385                    });
386                let mut row_count: u64 = 0;
387                #[for_await]
388                for either in &mut backfill_stream {
389                    match either {
390                        // Upstream
391                        Either::Left(msg) => {
392                            match msg? {
393                                Message::Barrier(barrier) => {
394                                    state_impl.commit_state(barrier.epoch).await?;
395                                    if let Some(mutation) = barrier.mutation.as_deref() {
396                                        use crate::executor::Mutation;
397                                        match mutation {
398                                            Mutation::Pause => {
399                                                is_snapshot_paused = true;
400                                                snapshot_valve.pause();
401                                            }
402                                            Mutation::Resume => {
403                                                is_snapshot_paused = false;
404                                                snapshot_valve.resume();
405                                            }
406                                            Mutation::Throttle(some) => {
407                                                // TODO(zw): optimization: improve throttle.
408                                                // 1. Handle rate limit 0. Currently, to resume the process, the actor must be rebuilt.
409                                                // 2. Apply new rate limit immediately.
410                                                if let Some(entry) =
411                                                    some.get(&self.actor_ctx.fragment_id)
412                                                    && entry.throttle_type()
413                                                        == ThrottleType::Backfill
414                                                    && entry.rate_limit != self.rate_limit_rps
415                                                {
416                                                    // The new rate limit will take effect since next split.
417                                                    self.rate_limit_rps = entry.rate_limit;
418                                                }
419                                            }
420                                            mutation if mutation.is_stop(self.actor_ctx.id) => {
421                                                tracing::info!(
422                                                    %table_id,
423                                                    upstream_table_name,
424                                                    "CdcBackfill has been dropped due to config change"
425                                                );
426                                                for chunk in upstream_chunk_buffer.drain(..) {
427                                                    yield Message::Chunk(chunk);
428                                                }
429                                                yield Message::Barrier(barrier);
430                                                let () = futures::future::pending().await;
431                                                unreachable!();
432                                            }
433                                            _ => (),
434                                        }
435                                    }
436                                    if is_reset_barrier(&barrier, self.actor_ctx.id) {
437                                        next_reset_barrier = Some(barrier);
438                                        for chunk in upstream_chunk_buffer.drain(..) {
439                                            yield Message::Chunk(chunk);
440                                        }
441                                        continue 'with_cdc_table_snapshot_splits;
442                                    }
443                                    if let Some(split_range) =
444                                        should_report_actor_backfill_progress.take()
445                                        && let Some(ref progress) = self.progress
446                                    {
447                                        progress.update(
448                                            self.actor_ctx.fragment_id,
449                                            self.actor_ctx.id,
450                                            barrier.epoch,
451                                            generation.expect("should have set generation when having progress to report"),
452                                            split_range,
453                                        );
454                                    }
455                                    // emit barrier and continue to consume the backfill stream
456                                    yield Message::Barrier(barrier);
457                                }
458                                Message::Chunk(chunk) => {
459                                    // skip empty upstream chunk
460                                    if chunk.cardinality() == 0 {
461                                        continue;
462                                    }
463
464                                    // TODO(zw): re-enable
465                                    // let chunk_cdc_offset =
466                                    //     get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
467                                    // if *self.external_table.table_type()
468                                    //     == ExternalCdcTableType::Postgres
469                                    //     && let Some(cur) = actor_cdc_offset_low.as_ref()
470                                    //     && let Some(chunk_offset) = chunk_cdc_offset
471                                    //     && chunk_offset < *cur
472                                    // {
473                                    //     continue;
474                                    // }
475
476                                    let chunk = mapping_chunk(chunk, &self.output_indices);
477                                    let (finished_chunk, current_chunk) =
478                                        split_finished_and_current_chunk(
479                                            chunk,
480                                            &finished_split_bounds,
481                                            &current_split_bounds,
482                                            snapshot_split_column_index,
483                                        );
484                                    if let Some(finished_chunk) = finished_chunk
485                                        && finished_chunk.cardinality() > 0
486                                    {
487                                        yield Message::Chunk(finished_chunk);
488                                    }
489                                    if let Some(filtered_chunk) = current_chunk
490                                        && filtered_chunk.cardinality() > 0
491                                    {
492                                        // Buffer only rows that overlap the split currently being backfilled.
493                                        upstream_chunk_buffer.push(filtered_chunk);
494                                    }
495                                }
496                                Message::Watermark(_) => {
497                                    // Ignore watermark during backfill, like the `CdcBackfillExecutor`.
498                                }
499                            }
500                        }
501                        // Snapshot read
502                        Either::Right(msg) => {
503                            match msg? {
504                                None => {
505                                    tracing::info!(
506                                        %table_id,
507                                        split_id = split.split_id,
508                                        "snapshot read stream ends"
509                                    );
510                                    for chunk in upstream_chunk_buffer.drain(..) {
511                                        yield Message::Chunk(chunk);
512                                    }
513
514                                    split_cdc_offset_high = {
515                                        // Limit concurrent CDC connections globally to 10 using a semaphore.
516                                        static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
517                                            tokio::sync::Semaphore::const_new(10);
518
519                                        let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
520                                        upstream_table_reader.current_cdc_offset().await?
521                                    };
522                                    if let Some(ref cdc_offset) = split_cdc_offset_high {
523                                        if let Some(ref cur) = actor_cdc_offset_high {
524                                            if *cur < *cdc_offset {
525                                                actor_cdc_offset_high =
526                                                    split_cdc_offset_high.clone();
527                                            }
528                                        } else {
529                                            actor_cdc_offset_high = split_cdc_offset_high.clone();
530                                        }
531                                    }
532                                    // Next split.
533                                    break;
534                                }
535                                Some(chunk) => {
536                                    let chunk_cardinality = chunk.cardinality() as u64;
537                                    row_count = row_count.saturating_add(chunk_cardinality);
538                                    yield Message::Chunk(mapping_chunk(
539                                        chunk,
540                                        &self.output_indices,
541                                    ));
542                                }
543                            }
544                        }
545                    }
546                }
547                // Mark current split backfill as finished. The state will be persisted by next barrier.
548                state_impl
549                    .mutate_state(
550                        split.split_id,
551                        true,
552                        row_count,
553                        split_cdc_offset_low,
554                        split_cdc_offset_high,
555                    )
556                    .await?;
557                if let Some((_, right_split)) = &mut should_report_actor_backfill_progress {
558                    assert!(
559                        *right_split < split.split_id,
560                        "{} {}",
561                        *right_split,
562                        split.split_id
563                    );
564                    *right_split = split.split_id;
565                } else {
566                    should_report_actor_backfill_progress = Some((split.split_id, split.split_id));
567                }
568            }
569
570            upstream_table_reader.disconnect().await?;
571            tracing::info!(
572                %table_id,
573                upstream_table_name,
574                "CdcBackfill has already finished and will forward messages directly to the downstream"
575            );
576
577            let mut should_report_actor_backfill_done = false;
578            // After backfill progress finished
579            // we can forward messages directly to the downstream,
580            // as backfill is finished.
581            #[for_await]
582            for msg in &mut upstream {
583                let msg = msg?;
584                match msg {
585                    Message::Barrier(barrier) => {
586                        state_impl.commit_state(barrier.epoch).await?;
587                        if is_reset_barrier(&barrier, self.actor_ctx.id) {
588                            next_reset_barrier = Some(barrier);
589                            continue 'with_cdc_table_snapshot_splits;
590                        }
591                        if let Some(split_range) = should_report_actor_backfill_progress.take()
592                            && let Some(ref progress) = self.progress
593                        {
594                            progress.update(
595                                self.actor_ctx.fragment_id,
596                                self.actor_ctx.id,
597                                barrier.epoch,
598                                generation.expect(
599                                    "should have set generation when having progress to report",
600                                ),
601                                split_range,
602                            );
603                        }
604                        if should_report_actor_backfill_done {
605                            should_report_actor_backfill_done = false;
606                            assert!(!actor_snapshot_splits.is_empty());
607                            if let Some(ref progress) = self.progress {
608                                progress.finish(
609                                    self.actor_ctx.fragment_id,
610                                    self.actor_ctx.id,
611                                    barrier.epoch,
612                                    generation.expect(
613                                        "should have set generation when having progress to report",
614                                    ),
615                                    (
616                                        actor_snapshot_splits[0].split_id,
617                                        actor_snapshot_splits[actor_snapshot_splits.len() - 1]
618                                            .split_id,
619                                    ),
620                                );
621                            }
622                        }
623                        yield Message::Barrier(barrier);
624                    }
625                    Message::Chunk(chunk) => {
626                        if actor_snapshot_splits.is_empty() {
627                            continue;
628                        }
629                        if chunk.cardinality() == 0 {
630                            continue;
631                        }
632
633                        let chunk_cdc_offset =
634                            get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
635                        // // TODO(zw): re-enable
636                        // if *self.external_table.table_type() == ExternalCdcTableType::Postgres
637                        //     && let Some(cur) = actor_cdc_offset_low.as_ref()
638                        //     && let Some(ref chunk_offset) = chunk_cdc_offset
639                        //     && *chunk_offset < *cur
640                        // {
641                        //     continue;
642                        // }
643
644                        // should_report_actor_backfill_done is set to true at most once.
645                        if let Some(high) = actor_cdc_offset_high.as_ref() {
646                            if state_impl.is_legacy_state() {
647                                // Since the legacy state does not track CDC offsets, report backfill completion immediately.
648                                actor_cdc_offset_high = None;
649                                should_report_actor_backfill_done = true;
650                            } else if let Some(ref chunk_offset) = chunk_cdc_offset
651                                && *chunk_offset >= *high
652                            {
653                                // Report backfill completion once the latest CDC offset exceeds the highest offset tracked during the backfill.
654                                actor_cdc_offset_high = None;
655                                should_report_actor_backfill_done = true;
656                            }
657                        }
658                        let chunk = mapping_chunk(chunk, &self.output_indices);
659                        if let Some(filtered_chunk) = filter_stream_chunk(
660                            chunk,
661                            &current_actor_bounds,
662                            snapshot_split_column_index,
663                        ) && filtered_chunk.cardinality() > 0
664                        {
665                            yield Message::Chunk(filtered_chunk);
666                        }
667                    }
668                    msg @ Message::Watermark(_) => {
669                        if let Some(msg) = mapping_message(msg, &self.output_indices) {
670                            yield msg;
671                        }
672                    }
673                }
674            }
675        }
676    }
677}
678
679fn split_finished_and_current_chunk(
680    chunk: StreamChunk,
681    finished_split_bounds: &Option<(OwnedRow, OwnedRow)>,
682    current_split_bounds: &Option<(OwnedRow, OwnedRow)>,
683    snapshot_split_column_index: usize,
684) -> (Option<StreamChunk>, Option<StreamChunk>) {
685    let finished_chunk = filter_stream_chunk(
686        chunk.clone(),
687        finished_split_bounds,
688        snapshot_split_column_index,
689    )
690    .map(StreamChunk::compact_vis);
691    let current_chunk =
692        filter_stream_chunk(chunk, current_split_bounds, snapshot_split_column_index)
693            .map(StreamChunk::compact_vis);
694    (finished_chunk, current_chunk)
695}
696
697fn filter_stream_chunk(
698    chunk: StreamChunk,
699    bound: &Option<(OwnedRow, OwnedRow)>,
700    snapshot_split_column_index: usize,
701) -> Option<StreamChunk> {
702    let Some((left, right)) = bound else {
703        return None;
704    };
705    assert_eq!(left.len(), 1, "multiple split columns is not supported yet");
706    assert_eq!(
707        right.len(),
708        1,
709        "multiple split columns is not supported yet"
710    );
711    let left_split_key = left.datum_at(0);
712    let right_split_key = right.datum_at(0);
713    let is_leftmost_bound = is_leftmost_bound(left);
714    let is_rightmost_bound = is_rightmost_bound(right);
715    if is_leftmost_bound && is_rightmost_bound {
716        return Some(chunk);
717    }
718    let mut new_bitmap = BitmapBuilder::with_capacity(chunk.capacity());
719    let (ops, columns, visibility) = chunk.into_inner();
720    for (row_split_key, v) in columns[snapshot_split_column_index]
721        .iter()
722        .zip_eq_fast(visibility.iter())
723    {
724        if !v {
725            new_bitmap.append(false);
726            continue;
727        }
728        let mut is_in_range = true;
729        if !is_leftmost_bound {
730            is_in_range = cmp_datum(
731                row_split_key,
732                left_split_key,
733                OrderType::ascending_nulls_first(),
734            )
735            .is_ge();
736        }
737        if is_in_range && !is_rightmost_bound {
738            is_in_range = cmp_datum(
739                row_split_key,
740                right_split_key,
741                OrderType::ascending_nulls_first(),
742            )
743            .is_lt();
744        }
745        if !is_in_range {
746            tracing::trace!(?row_split_key, ?left_split_key, ?right_split_key, snapshot_split_column_index, data_type = ?columns[snapshot_split_column_index].data_type(), "filter out row")
747        }
748        new_bitmap.append(is_in_range);
749    }
750    Some(StreamChunk::with_visibility(
751        ops,
752        columns,
753        new_bitmap.finish(),
754    ))
755}
756
757fn is_leftmost_bound(row: &OwnedRow) -> bool {
758    row.iter().all(|d| d.is_none())
759}
760
761fn is_rightmost_bound(row: &OwnedRow) -> bool {
762    row.iter().all(|d| d.is_none())
763}
764
765impl<S: StateStore> Execute for ParallelizedCdcBackfillExecutor<S> {
766    fn execute(self: Box<Self>) -> BoxedMessageStream {
767        self.execute_inner().boxed()
768    }
769}
770
771fn extends_current_actor_bound(
772    current: &mut Option<(OwnedRow, OwnedRow)>,
773    split: &CdcTableSnapshotSplit,
774) {
775    if current.is_none() {
776        *current = Some((
777            split.left_bound_inclusive.clone(),
778            split.right_bound_exclusive.clone(),
779        ));
780    } else {
781        current.as_mut().unwrap().1 = split.right_bound_exclusive.clone();
782    }
783}
784
785fn is_reset_barrier(barrier: &Barrier, actor_id: ActorId) -> bool {
786    match barrier.mutation.as_deref() {
787        Some(Mutation::Update(update)) => update
788            .actor_cdc_table_snapshot_splits
789            .splits
790            .contains_key(&actor_id),
791        _ => false,
792    }
793}
794
795fn assert_consecutive_splits(actor_snapshot_splits: &[CdcTableSnapshotSplit]) {
796    for i in 1..actor_snapshot_splits.len() {
797        assert_eq!(
798            actor_snapshot_splits[i].split_id,
799            actor_snapshot_splits[i - 1].split_id + 1,
800            "{:?}",
801            actor_snapshot_splits
802        );
803        assert!(
804            cmp_datum(
805                actor_snapshot_splits[i - 1]
806                    .right_bound_exclusive
807                    .datum_at(0),
808                actor_snapshot_splits[i].right_bound_exclusive.datum_at(0),
809                OrderType::ascending_nulls_last(),
810            )
811            .is_lt()
812        );
813    }
814}
815
816#[cfg(test)]
817mod tests {
818    use risingwave_common::array::StreamChunk;
819    use risingwave_common::row::OwnedRow;
820    use risingwave_common::types::ScalarImpl;
821
822    use crate::executor::backfill::cdc::cdc_backill_v2::{
823        filter_stream_chunk, split_finished_and_current_chunk,
824    };
825
826    #[test]
827    fn test_filter_stream_chunk() {
828        use risingwave_common::array::StreamChunkTestExt;
829        let chunk = StreamChunk::from_pretty(
830            "  I I
831             + 1 6
832             - 2 .
833            U- 3 7
834            U+ 4 .",
835        );
836        let bound = None;
837        let c = filter_stream_chunk(chunk.clone(), &bound, 0);
838        assert!(c.is_none());
839
840        let bound = Some((OwnedRow::new(vec![None]), OwnedRow::new(vec![None])));
841        let c = filter_stream_chunk(chunk.clone(), &bound, 0);
842        assert_eq!(c.unwrap().compact_vis(), chunk);
843
844        let bound = Some((
845            OwnedRow::new(vec![None]),
846            OwnedRow::new(vec![Some(ScalarImpl::Int64(3))]),
847        ));
848        let c = filter_stream_chunk(chunk.clone(), &bound, 0);
849        assert_eq!(
850            c.unwrap().compact_vis(),
851            StreamChunk::from_pretty(
852                "  I I
853             + 1 6
854             - 2 .",
855            )
856        );
857
858        let bound = Some((
859            OwnedRow::new(vec![Some(ScalarImpl::Int64(3))]),
860            OwnedRow::new(vec![None]),
861        ));
862        let c = filter_stream_chunk(chunk.clone(), &bound, 0);
863        assert_eq!(
864            c.unwrap().compact_vis(),
865            StreamChunk::from_pretty(
866                "  I I
867            U- 3 7
868            U+ 4 .",
869            )
870        );
871
872        let bound = Some((
873            OwnedRow::new(vec![Some(ScalarImpl::Int64(2))]),
874            OwnedRow::new(vec![Some(ScalarImpl::Int64(4))]),
875        ));
876        let c = filter_stream_chunk(chunk.clone(), &bound, 0);
877        assert_eq!(
878            c.unwrap().compact_vis(),
879            StreamChunk::from_pretty(
880                "  I I
881             - 2 .
882            U- 3 7",
883            )
884        );
885
886        // Test NULL value.
887        let bound = None;
888        let c = filter_stream_chunk(chunk.clone(), &bound, 1);
889        assert!(c.is_none());
890
891        let bound = Some((OwnedRow::new(vec![None]), OwnedRow::new(vec![None])));
892        let c = filter_stream_chunk(chunk.clone(), &bound, 1);
893        assert_eq!(c.unwrap().compact_vis(), chunk);
894
895        let bound = Some((
896            OwnedRow::new(vec![None]),
897            OwnedRow::new(vec![Some(ScalarImpl::Int64(7))]),
898        ));
899        let c = filter_stream_chunk(chunk.clone(), &bound, 1);
900        assert_eq!(
901            c.unwrap().compact_vis(),
902            StreamChunk::from_pretty(
903                "  I I
904             + 1 6
905             - 2 .
906            U+ 4 .",
907            )
908        );
909
910        let bound = Some((
911            OwnedRow::new(vec![Some(ScalarImpl::Int64(7))]),
912            OwnedRow::new(vec![None]),
913        ));
914        let c = filter_stream_chunk(chunk, &bound, 1);
915        assert_eq!(
916            c.unwrap().compact_vis(),
917            StreamChunk::from_pretty(
918                "  I I
919            U- 3 7",
920            )
921        );
922    }
923
924    #[test]
925    fn test_split_finished_and_current_chunk() {
926        use risingwave_common::array::StreamChunkTestExt;
927
928        let chunk = StreamChunk::from_pretty(
929            "  I I
930             + 1 11
931             + 6 10
932             + 199 40",
933        );
934        let finished_split_bounds = Some((
935            OwnedRow::new(vec![Some(ScalarImpl::Int64(1))]),
936            OwnedRow::new(vec![Some(ScalarImpl::Int64(6))]),
937        ));
938        let current_split_bounds = Some((
939            OwnedRow::new(vec![Some(ScalarImpl::Int64(6))]),
940            OwnedRow::new(vec![Some(ScalarImpl::Int64(100))]),
941        ));
942
943        let (finished_chunk, current_chunk) = split_finished_and_current_chunk(
944            chunk,
945            &finished_split_bounds,
946            &current_split_bounds,
947            0,
948        );
949
950        assert_eq!(
951            finished_chunk.unwrap(),
952            StreamChunk::from_pretty(
953                "  I I
954                 + 1 11",
955            )
956        );
957        assert_eq!(
958            current_chunk.unwrap(),
959            StreamChunk::from_pretty(
960                "  I I
961                 + 6 10",
962            )
963        );
964    }
965}