Skip to main content

risingwave_stream/executor/backfill/cdc/
cdc_backfill.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::BTreeMap;
16use std::future::Future;
17use std::pin::Pin;
18
19use either::Either;
20use futures::stream;
21use futures::stream::select_with_strategy;
22use itertools::Itertools;
23use risingwave_common::array::{DataChunk, Op};
24use risingwave_common::bail;
25use risingwave_common::bitmap::BitmapBuilder;
26use risingwave_common::catalog::ColumnDesc;
27use risingwave_common::row::RowExt;
28use risingwave_common::util::sort_util::{OrderType, cmp_datum_iter};
29use risingwave_connector::parser::{
30    BigintUnsignedHandlingMode, ByteStreamSourceParser, DebeziumParser, DebeziumProps,
31    EncodingProperties, JsonProperties, ProtocolProperties, SourceStreamChunkBuilder,
32    SpecificParserConfig, TimeHandling, TimestampHandling, TimestamptzHandling,
33};
34use risingwave_connector::source::cdc::CdcScanOptions;
35use risingwave_connector::source::cdc::external::{
36    CdcOffset, ExternalCdcTableType, ExternalTableReaderImpl,
37};
38use risingwave_connector::source::{SourceColumnDesc, SourceContext, SourceCtrlOpts};
39use risingwave_pb::common::ThrottleType;
40use rw_futures_util::pausable;
41use thiserror_ext::AsReport;
42use tracing::Instrument;
43
44use crate::executor::backfill::cdc::state::CdcBackfillState;
45use crate::executor::backfill::cdc::upstream_table::external::ExternalStorageTable;
46use crate::executor::backfill::cdc::upstream_table::snapshot::{
47    SnapshotReadArgs, UpstreamTableRead, UpstreamTableReader,
48};
49use crate::executor::backfill::utils::{
50    get_cdc_chunk_last_offset, get_new_pos, mapping_chunk, mapping_message, mark_cdc_chunk,
51};
52use crate::executor::monitor::CdcBackfillMetrics;
53use crate::executor::prelude::*;
54use crate::executor::source::get_infinite_backoff_strategy;
55use crate::task::CreateMviewProgressReporter;
56
57/// `split_id`, `is_finished`, `row_count`, `cdc_offset` all occupy 1 column each.
58const METADATA_STATE_LEN: usize = 4;
59
60// The TimestampHandling/TimestamptzHandling/TimeHandling parser's behavior depends on the debezium.time.precision.mode setting:
61// - If left unset, Debezium defaults to time.precision.mode=microseconds (per debezium.properties), and the parser uses Micro.
62// - If set to "connect", Debezium uses time.precision.mode=connect, and the parser uses Milli by design.
63// - If set to any other value, Debezium applies that specific value, and the default parser is used to maintain backward compatibility.
64pub(crate) fn get_cdc_json_parse_handling_from_properties(
65    properties: &BTreeMap<String, String>,
66) -> (
67    Option<TimestampHandling>,
68    Option<TimestamptzHandling>,
69    Option<TimeHandling>,
70    Option<BigintUnsignedHandlingMode>,
71) {
72    let (timestamp_handling, timestamptz_handling, time_handling) = match properties
73        .get("debezium.time.precision.mode")
74    {
75        None => (
76            Some(TimestampHandling::Micro),
77            Some(TimestamptzHandling::Micro),
78            Some(TimeHandling::Micro),
79        ),
80        Some(m) if m == "connect" => (
81            Some(TimestampHandling::Milli),
82            Some(TimestamptzHandling::Milli),
83            Some(TimeHandling::Milli),
84        ),
85        Some(other) => {
86            // backward compatibility.
87            tracing::warn!(
88                "Unsupported debezium.time.precision.mode = {other}, fall back to default parser."
89            );
90            (None, None, None)
91        }
92    };
93    let bigint_unsigned_handling = properties
94        .get("debezium.bigint.unsigned.handling.mode")
95        .is_some_and(|v| v == "precise")
96        .then_some(BigintUnsignedHandlingMode::Precise);
97    (
98        timestamp_handling,
99        timestamptz_handling,
100        time_handling,
101        bigint_unsigned_handling,
102    )
103}
104
105pub struct CdcBackfillExecutor<S: StateStore> {
106    actor_ctx: ActorContextRef,
107
108    /// The external table to be backfilled
109    external_table: ExternalStorageTable,
110
111    /// Upstream changelog stream which may contain metadata columns, e.g. `_rw_offset`
112    upstream: Executor,
113
114    /// The column indices need to be forwarded to the downstream from the upstream and table scan.
115    output_indices: Vec<usize>,
116
117    /// The schema of output chunk, including additional columns if any
118    output_columns: Vec<ColumnDesc>,
119
120    /// State table of the `CdcBackfill` executor
121    state_impl: CdcBackfillState<S>,
122
123    // TODO: introduce a CdcBackfillProgress to report finish to Meta
124    // This object is just a stub right now
125    progress: Option<CreateMviewProgressReporter>,
126
127    metrics: CdcBackfillMetrics,
128
129    /// Rate limit in rows/s.
130    rate_limit_rps: Option<u32>,
131
132    options: CdcScanOptions,
133
134    properties: BTreeMap<String, String>,
135}
136
137impl<S: StateStore> CdcBackfillExecutor<S> {
138    #[expect(clippy::too_many_arguments)]
139    pub fn new(
140        actor_ctx: ActorContextRef,
141        external_table: ExternalStorageTable,
142        upstream: Executor,
143        output_indices: Vec<usize>,
144        output_columns: Vec<ColumnDesc>,
145        progress: Option<CreateMviewProgressReporter>,
146        metrics: Arc<StreamingMetrics>,
147        state_table: StateTable<S>,
148        rate_limit_rps: Option<u32>,
149        options: CdcScanOptions,
150        properties: BTreeMap<String, String>,
151    ) -> Self {
152        let pk_indices = external_table.pk_indices();
153        let upstream_table_id = external_table.table_id();
154        let state_impl = CdcBackfillState::new(
155            upstream_table_id,
156            state_table,
157            pk_indices.len() + METADATA_STATE_LEN,
158        );
159
160        let metrics = metrics.new_cdc_backfill_metrics(external_table.table_id(), actor_ctx.id);
161        Self {
162            actor_ctx,
163            external_table,
164            upstream,
165            output_indices,
166            output_columns,
167            state_impl,
168            progress,
169            metrics,
170            rate_limit_rps,
171            options,
172            properties,
173        }
174    }
175
176    fn report_metrics(
177        metrics: &CdcBackfillMetrics,
178        snapshot_processed_row_count: u64,
179        upstream_processed_row_count: u64,
180    ) {
181        metrics
182            .cdc_backfill_snapshot_read_row_count
183            .inc_by(snapshot_processed_row_count);
184
185        metrics
186            .cdc_backfill_upstream_output_row_count
187            .inc_by(upstream_processed_row_count);
188    }
189
190    fn consume_upstream_chunk_buffer(
191        offset_parse_func: &risingwave_connector::source::cdc::external::CdcOffsetParseFunc,
192        upstream_chunk_buffer: &mut Vec<StreamChunk>,
193        current_pk_pos: Option<&OwnedRow>,
194        pk_indices: &[usize],
195        pk_order: &[OrderType],
196        last_binlog_offset: &Option<CdcOffset>,
197        output_indices: &[usize],
198    ) -> StreamExecutorResult<(Vec<StreamChunk>, u64, Option<CdcOffset>)> {
199        let Some(current_pos) = current_pk_pos else {
200            return Ok((vec![], 0, None));
201        };
202
203        let buffered_chunks = std::mem::take(upstream_chunk_buffer);
204        let mut emitted_chunks = Vec::with_capacity(buffered_chunks.len());
205        let mut upstream_processed_row_count = 0;
206        let mut consumed_binlog_offset = None;
207        let mut retained_chunks = Vec::with_capacity(buffered_chunks.len());
208        let mut can_advance_consumed_offset = true;
209
210        for chunk in buffered_chunks {
211            let mut emitted_vis = BitmapBuilder::zeroed(chunk.capacity());
212            let mut retained_vis = BitmapBuilder::zeroed(chunk.capacity());
213            for idx in 0..chunk.capacity() {
214                let (op, row, visible) = chunk.row_at(idx);
215                if !visible {
216                    // Buffered chunks may carry sparse visibility from previous filtering rounds.
217                    continue;
218                }
219                let event_offset = (*offset_parse_func)(
220                    row.iter()
221                        .last()
222                        .flatten()
223                        .expect("cdc offset must exist")
224                        .into_utf8(),
225                )?;
226                let in_binlog_range = last_binlog_offset
227                    .as_ref()
228                    .is_none_or(|binlog_low| *binlog_low <= event_offset);
229
230                let row_pk = row.project(pk_indices);
231                let reached_current_pos =
232                    cmp_datum_iter(row_pk.iter(), current_pos.iter(), pk_order.iter().copied())
233                        .is_le();
234                if !in_binlog_range {
235                    continue;
236                }
237                let should_emit = reached_current_pos;
238
239                match op {
240                    Op::Insert | Op::Delete => {
241                        if should_emit {
242                            emitted_vis.set(idx, true);
243                            if can_advance_consumed_offset {
244                                consumed_binlog_offset = Some(event_offset);
245                            }
246                            upstream_processed_row_count += 1;
247                        } else {
248                            retained_vis.set(idx, true);
249                            can_advance_consumed_offset = false;
250                        }
251                    }
252                    Op::UpdateDelete | Op::UpdateInsert => {
253                        unreachable!("CDC buffered chunks should not contain update pairs")
254                    }
255                }
256            }
257
258            let emitted_vis = emitted_vis.finish();
259            if emitted_vis.count_ones() > 0 {
260                emitted_chunks.push(mapping_chunk(
261                    chunk.clone_with_vis(emitted_vis),
262                    output_indices,
263                ));
264            }
265
266            let retained_vis = retained_vis.finish();
267            if retained_vis.count_ones() > 0 {
268                retained_chunks.push(chunk.clone_with_vis(retained_vis));
269            }
270        }
271
272        *upstream_chunk_buffer = retained_chunks;
273
274        Ok((
275            emitted_chunks,
276            upstream_processed_row_count,
277            consumed_binlog_offset,
278        ))
279    }
280
281    #[try_stream(ok = Message, error = StreamExecutorError)]
282    async fn execute_inner(mut self) {
283        // The indices to primary key columns
284        let pk_indices = self.external_table.pk_indices().to_vec();
285        let pk_order = self.external_table.pk_order_types().to_vec();
286
287        let table_id = self.external_table.table_id();
288        let upstream_table_name = self.external_table.qualified_table_name();
289        let schema_table_name = self.external_table.schema_table_name().clone();
290        let external_database_name = self.external_table.database_name().to_owned();
291
292        let additional_columns = self
293            .output_columns
294            .iter()
295            .filter(|col| col.additional_column.column_type.is_some())
296            .cloned()
297            .collect_vec();
298
299        let mut upstream = self.upstream.execute();
300
301        // Current position of the upstream_table storage primary key.
302        // `None` means it starts from the beginning.
303        let mut current_pk_pos: Option<OwnedRow>;
304
305        // Poll the upstream to get the first barrier.
306        let first_barrier = expect_first_barrier(&mut upstream).await?;
307
308        let mut is_snapshot_paused = first_barrier.is_pause_on_startup();
309        let first_barrier_epoch = first_barrier.epoch;
310        // The first barrier message should be propagated.
311        yield Message::Barrier(first_barrier);
312
313        // Check whether this parallelism has been assigned splits,
314        // if not, we should bypass the backfill directly.
315        let mut state_impl = self.state_impl;
316
317        state_impl.init_epoch(first_barrier_epoch).await?;
318
319        // restore backfill state
320        let state = state_impl.restore_state().await?;
321        current_pk_pos = state.current_pk_pos.clone();
322
323        let need_backfill = !self.options.disable_backfill && !state.is_finished;
324
325        // Keep track of rows from the snapshot.
326        let mut total_snapshot_row_count = state.row_count as u64;
327
328        // After init the state table and forward the initial barrier to downstream,
329        // we now try to create the table reader with retry.
330        // If backfill hasn't finished, we can ignore upstream cdc events before we create the table reader;
331        // If backfill is finished, we should forward the upstream cdc events to downstream.
332        let mut table_reader: Option<ExternalTableReaderImpl> = None;
333        let external_table = self.external_table.clone();
334        let mut future = Box::pin(async move {
335            let backoff = get_infinite_backoff_strategy();
336            tokio_retry::Retry::spawn(backoff, || async {
337                match external_table.create_table_reader().await {
338                    Ok(reader) => Ok(reader),
339                    Err(e) => {
340                        tracing::warn!(error = %e.as_report(), "failed to create cdc table reader, retrying...");
341                        Err(e)
342                    }
343                }
344            })
345            .instrument(tracing::info_span!("create_cdc_table_reader_with_retry"))
346            .await
347            .expect("Retry create cdc table reader until success.")
348        });
349        let (timestamp_handling, timestamptz_handling, time_handling, bigint_unsigned_handling) =
350            get_cdc_json_parse_handling_from_properties(&self.properties);
351        // Only postgres-cdc connector may trigger TOAST.
352        let handle_toast_columns: bool =
353            self.external_table.table_type() == &ExternalCdcTableType::Postgres;
354        // Make sure to use mapping_message after transform_upstream.
355        let mut upstream = transform_upstream(
356            upstream,
357            self.output_columns.clone(),
358            timestamp_handling,
359            timestamptz_handling,
360            time_handling,
361            bigint_unsigned_handling,
362            handle_toast_columns,
363        )
364        .boxed();
365        loop {
366            if let Some(msg) =
367                build_reader_and_poll_upstream(&mut upstream, &mut table_reader, &mut future)
368                    .await?
369            {
370                if let Some(msg) = mapping_message(msg, &self.output_indices) {
371                    match msg {
372                        Message::Barrier(barrier) => {
373                            // commit state to bump the epoch of state table
374                            state_impl.commit_state(barrier.epoch).await?;
375                            yield Message::Barrier(barrier);
376                        }
377                        Message::Chunk(chunk) => {
378                            if need_backfill {
379                                // ignore chunk if we need backfill, since we can read the data from the snapshot
380                            } else {
381                                // forward the chunk to downstream
382                                yield Message::Chunk(chunk);
383                            }
384                        }
385                        Message::Watermark(_) => {
386                            // ignore watermark
387                        }
388                    }
389                }
390            } else {
391                assert!(table_reader.is_some(), "table reader must created");
392                tracing::info!(
393                    %table_id,
394                    upstream_table_name,
395                    "table reader created successfully"
396                );
397                break;
398            }
399        }
400
401        let upstream_table_reader = UpstreamTableReader::new(
402            self.external_table.clone(),
403            table_reader.expect("table reader must created"),
404        );
405
406        let mut upstream = upstream.peekable();
407
408        let mut last_binlog_offset: Option<CdcOffset> = {
409            // Limit concurrent CDC connections globally to 10 using a semaphore.
410            static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
411                tokio::sync::Semaphore::const_new(10);
412
413            let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
414            state
415                .last_cdc_offset
416                .map_or(upstream_table_reader.current_cdc_offset().await?, Some)
417        };
418
419        let offset_parse_func = upstream_table_reader.reader.get_cdc_offset_parser();
420        let mut consumed_binlog_offset: Option<CdcOffset> = None;
421
422        tracing::info!(
423            %table_id,
424            upstream_table_name,
425            initial_binlog_offset = ?last_binlog_offset,
426            ?current_pk_pos,
427            is_finished = state.is_finished,
428            is_snapshot_paused,
429            snapshot_row_count = total_snapshot_row_count,
430            rate_limit = self.rate_limit_rps,
431            disable_backfill = self.options.disable_backfill,
432            snapshot_barrier_interval = self.options.snapshot_barrier_interval,
433            snapshot_batch_size = self.options.snapshot_batch_size,
434            "start cdc backfill",
435        );
436
437        // CDC Backfill Algorithm:
438        //
439        // When the first barrier comes from upstream:
440        //  - read the current binlog offset as `binlog_low`
441        //  - start a snapshot read upon upstream table and iterate over the snapshot read stream
442        //  - buffer the changelog event from upstream
443        //
444        // When a new barrier comes from upstream:
445        //  - read the current binlog offset as `binlog_high`
446        //  - for each row of the upstream change log, forward it to downstream if it in the range
447        //    of [binlog_low, binlog_high] and its pk <= `current_pos`, otherwise keep buffering it
448        //    until `current_pos` catches up
449        //  - reconstruct the whole backfill stream with upstream changelog and a new table snapshot
450        //
451        // When a chunk comes from snapshot, we forward it to the downstream and raise
452        // `current_pos`.
453        // When we reach the end of the snapshot read stream, it means backfill has been
454        // finished.
455        //
456        // Once the backfill loop ends, we forward the upstream directly to the downstream.
457        if need_backfill {
458            // drive the upstream changelog first to ensure we can receive timely changelog event,
459            // otherwise the upstream changelog may be blocked by the snapshot read stream
460            let _ = Pin::new(&mut upstream).peek().await;
461
462            // wait for a barrier to make sure the backfill starts after upstream source
463            #[for_await]
464            for msg in upstream.by_ref() {
465                match msg? {
466                    Message::Barrier(barrier) => {
467                        match barrier.mutation.as_deref() {
468                            Some(crate::executor::Mutation::Pause) => {
469                                is_snapshot_paused = true;
470                                tracing::info!(
471                                    %table_id,
472                                    upstream_table_name,
473                                    "snapshot is paused by barrier"
474                                );
475                            }
476                            Some(crate::executor::Mutation::Resume) => {
477                                is_snapshot_paused = false;
478                                tracing::info!(
479                                    %table_id,
480                                    upstream_table_name,
481                                    "snapshot is resumed by barrier"
482                                );
483                            }
484                            _ => {
485                                // ignore other mutations
486                            }
487                        }
488                        // commit state just to bump the epoch of state table
489                        state_impl.commit_state(barrier.epoch).await?;
490                        yield Message::Barrier(barrier);
491                        break;
492                    }
493                    Message::Chunk(ref chunk) => {
494                        last_binlog_offset = get_cdc_chunk_last_offset(&offset_parse_func, chunk)?;
495                    }
496                    Message::Watermark(_) => {
497                        // Ignore watermark
498                    }
499                }
500            }
501
502            tracing::info!(%table_id,
503                upstream_table_name,
504                initial_binlog_offset = ?last_binlog_offset,
505                ?current_pk_pos,
506                is_snapshot_paused,
507                "start cdc backfill loop");
508
509            // the buffer will be drained when a barrier comes
510            let mut upstream_chunk_buffer: Vec<StreamChunk> = vec![];
511
512            'backfill_loop: loop {
513                let mut should_bypass_snapshot_stream_patch =
514                    self.rate_limit_rps.is_some_and(|val| val == 0);
515                let left_upstream = upstream.by_ref().map(Either::Left);
516
517                let mut snapshot_read_row_cnt: usize = 0;
518                let read_args = SnapshotReadArgs::new(
519                    current_pk_pos.clone(),
520                    self.rate_limit_rps,
521                    pk_indices.clone(),
522                    additional_columns.clone(),
523                    schema_table_name.clone(),
524                    external_database_name.clone(),
525                );
526                let right_snapshot = pin!(
527                    upstream_table_reader
528                        .snapshot_read_full_table(read_args, self.options.snapshot_batch_size)
529                        .map(Either::Right)
530                );
531                let (right_snapshot, snapshot_valve) = pausable(right_snapshot);
532                if is_snapshot_paused {
533                    snapshot_valve.pause();
534                }
535
536                // Prefer to select upstream, so we can stop snapshot stream when barrier comes.
537                let mut backfill_stream =
538                    select_with_strategy(left_upstream, right_snapshot, |_: &mut ()| {
539                        stream::PollNext::Left
540                    });
541
542                let mut cur_barrier_snapshot_processed_rows: u64 = 0;
543                let mut cur_barrier_upstream_processed_rows: u64 = 0;
544                let mut barrier_count: u32 = 0;
545                let mut pending_barrier = None;
546
547                #[for_await]
548                for either in &mut backfill_stream {
549                    match either {
550                        // Upstream
551                        Either::Left(msg) => {
552                            match msg? {
553                                Message::Barrier(barrier) => {
554                                    // increase the barrier count and check whether need to start a new snapshot
555                                    barrier_count += 1;
556                                    let can_start_new_snapshot =
557                                        barrier_count == self.options.snapshot_barrier_interval;
558                                    let mut needs_rebuild_snapshot = false;
559
560                                    if let Some(mutation) = barrier.mutation.as_deref() {
561                                        use crate::executor::Mutation;
562                                        match mutation {
563                                            Mutation::Pause => {
564                                                is_snapshot_paused = true;
565                                                snapshot_valve.pause();
566                                            }
567                                            Mutation::Resume => {
568                                                is_snapshot_paused = false;
569                                                snapshot_valve.resume();
570                                            }
571                                            Mutation::Throttle(some) => {
572                                                if let Some(entry) =
573                                                    some.get(&self.actor_ctx.fragment_id)
574                                                    && entry.throttle_type()
575                                                        == ThrottleType::Backfill
576                                                    && entry.rate_limit != self.rate_limit_rps
577                                                {
578                                                    // If self.rate_limit_rps is 0, the snapshot stream does not establish a snapshot_read.
579                                                    // Consequently, the later snapshot stream patch is bypassed.
580                                                    should_bypass_snapshot_stream_patch = self
581                                                        .rate_limit_rps
582                                                        .is_some_and(|val| val == 0);
583                                                    self.rate_limit_rps = entry.rate_limit;
584                                                    needs_rebuild_snapshot = true;
585                                                }
586                                            }
587                                            mutation if mutation.is_stop(self.actor_ctx.id) => {
588                                                // the actor has been dropped, exit the backfill loop
589                                                tracing::info!(
590                                                    %table_id,
591                                                    upstream_table_name,
592                                                    "CdcBackfill has been dropped due to config change"
593                                                );
594                                                yield Message::Barrier(barrier);
595                                                break 'backfill_loop;
596                                            }
597                                            _ => (),
598                                        }
599                                    }
600
601                                    // when processing a barrier, check whether can start a new snapshot
602                                    // if the number of barriers reaches the snapshot interval
603                                    if can_start_new_snapshot || needs_rebuild_snapshot {
604                                        // staging the barrier
605                                        pending_barrier = Some(barrier);
606                                        tracing::debug!(
607                                            %table_id,
608                                            ?current_pk_pos,
609                                            ?snapshot_read_row_cnt,
610                                            "Prepare to start a new snapshot"
611                                        );
612                                        // Break the loop for consuming snapshot and prepare to start a new snapshot
613                                        break;
614                                    } else {
615                                        // Drain the in-memory buffer to ensure no data is lost during the recovery process.
616                                        let (
617                                            emitted_upstream_chunks,
618                                            consumed_upstream_row_count,
619                                            consumed_binlog_offset,
620                                        ) = Self::consume_upstream_chunk_buffer(
621                                            &offset_parse_func,
622                                            &mut upstream_chunk_buffer,
623                                            current_pk_pos.as_ref(),
624                                            &pk_indices,
625                                            &pk_order,
626                                            &last_binlog_offset,
627                                            &self.output_indices,
628                                        )?;
629                                        cur_barrier_upstream_processed_rows +=
630                                            consumed_upstream_row_count;
631                                        if let Some(consumed_binlog_offset) = consumed_binlog_offset
632                                        {
633                                            last_binlog_offset = Some(consumed_binlog_offset);
634                                        }
635                                        for chunk in emitted_upstream_chunks {
636                                            yield Message::Chunk(chunk);
637                                        }
638
639                                        Self::report_metrics(
640                                            &self.metrics,
641                                            cur_barrier_snapshot_processed_rows,
642                                            cur_barrier_upstream_processed_rows,
643                                        );
644
645                                        // update and persist current backfill progress
646                                        state_impl
647                                            .mutate_state(
648                                                current_pk_pos.clone(),
649                                                last_binlog_offset.clone(),
650                                                total_snapshot_row_count,
651                                                false,
652                                            )
653                                            .await?;
654
655                                        state_impl.commit_state(barrier.epoch).await?;
656
657                                        // emit barrier and continue consume the backfill stream
658                                        yield Message::Barrier(barrier);
659                                    }
660                                }
661                                Message::Chunk(chunk) => {
662                                    // skip empty upstream chunk
663                                    if chunk.cardinality() == 0 {
664                                        continue;
665                                    }
666
667                                    let chunk_binlog_offset =
668                                        get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
669
670                                    tracing::trace!(
671                                        "recv changelog chunk: chunk_offset {:?}, capactiy {}",
672                                        chunk_binlog_offset,
673                                        chunk.capacity()
674                                    );
675
676                                    // Since we don't need changelog before the
677                                    // `last_binlog_offset`, skip the chunk that *only* contains
678                                    // events before `last_binlog_offset`.
679                                    if let Some(last_binlog_offset) = last_binlog_offset.as_ref()
680                                        && let Some(chunk_offset) = chunk_binlog_offset
681                                        && chunk_offset < *last_binlog_offset
682                                    {
683                                        tracing::trace!(
684                                            "skip changelog chunk: chunk_offset {:?}, capacity {}",
685                                            chunk_offset,
686                                            chunk.capacity()
687                                        );
688                                        continue;
689                                    }
690                                    // Buffer the upstream chunk.
691                                    upstream_chunk_buffer.push(chunk.compact_vis());
692                                }
693                                Message::Watermark(_) => {
694                                    // Ignore watermark during backfill.
695                                }
696                            }
697                        }
698                        // Snapshot read
699                        Either::Right(msg) => {
700                            match msg? {
701                                None => {
702                                    tracing::info!(
703                                        %table_id,
704                                        ?last_binlog_offset,
705                                        ?current_pk_pos,
706                                        "snapshot read stream ends"
707                                    );
708                                    // If the snapshot read stream ends, it means all historical
709                                    // data has been loaded.
710                                    // We should not mark the chunk anymore,
711                                    // otherwise, we will ignore some rows in the buffer.
712                                    for chunk in upstream_chunk_buffer.drain(..) {
713                                        yield Message::Chunk(mapping_chunk(
714                                            chunk,
715                                            &self.output_indices,
716                                        ));
717                                    }
718
719                                    // backfill has finished, exit the backfill loop and persist the state when we recv a barrier
720                                    break 'backfill_loop;
721                                }
722                                Some(chunk) => {
723                                    // Raise the current position.
724                                    // As snapshot read streams are ordered by pk, so we can
725                                    // just use the last row to update `current_pos`.
726                                    current_pk_pos = Some(get_new_pos(&chunk, &pk_indices));
727
728                                    tracing::trace!(
729                                        "got a snapshot chunk: len {}, current_pk_pos {:?}",
730                                        chunk.cardinality(),
731                                        current_pk_pos
732                                    );
733                                    let chunk_cardinality = chunk.cardinality() as u64;
734                                    cur_barrier_snapshot_processed_rows += chunk_cardinality;
735                                    total_snapshot_row_count += chunk_cardinality;
736                                    yield Message::Chunk(mapping_chunk(
737                                        chunk,
738                                        &self.output_indices,
739                                    ));
740                                }
741                            }
742                        }
743                    }
744                }
745
746                assert!(pending_barrier.is_some(), "pending_barrier must exist");
747                let pending_barrier = pending_barrier.unwrap();
748
749                // The snapshot stream patch:
750                // Here we have to ensure the snapshot stream is consumed at least once,
751                // since the barrier event can kick in anytime.
752                // Otherwise, the result set of the new snapshot stream may become empty.
753                // It maybe a cancellation bug of the mysql driver.
754                let (_, mut snapshot_stream) = backfill_stream.into_inner();
755                // Resume the snapshot stream so that the snapshot stream patch won't block.
756                if !should_bypass_snapshot_stream_patch && is_snapshot_paused {
757                    snapshot_valve.resume();
758                }
759                if !should_bypass_snapshot_stream_patch
760                    && let Some(msg) = snapshot_stream
761                        .next()
762                        .instrument_await("consume_snapshot_stream_once")
763                        .await
764                {
765                    let Either::Right(msg) = msg else {
766                        bail!("BUG: snapshot_read contains upstream messages");
767                    };
768                    match msg? {
769                        None => {
770                            tracing::info!(
771                                %table_id,
772                                ?last_binlog_offset,
773                                ?current_pk_pos,
774                                "snapshot read stream ends in the force emit branch"
775                            );
776                            // End of the snapshot read stream.
777                            // Consume the buffered upstream chunk without filtering by `binlog_low`.
778                            for chunk in upstream_chunk_buffer.drain(..) {
779                                yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
780                            }
781
782                            // mark backfill has finished
783                            state_impl
784                                .mutate_state(
785                                    current_pk_pos.clone(),
786                                    last_binlog_offset.clone(),
787                                    total_snapshot_row_count,
788                                    true,
789                                )
790                                .await?;
791
792                            // commit state because we have received a barrier message
793                            state_impl.commit_state(pending_barrier.epoch).await?;
794                            yield Message::Barrier(pending_barrier);
795                            // end of backfill loop, since backfill has finished
796                            break 'backfill_loop;
797                        }
798                        Some(_) if is_snapshot_paused => {
799                            // Since the snapshot stream is paused, drop the chunk.
800                        }
801                        Some(chunk) => {
802                            // Raise the current pk position.
803                            current_pk_pos = Some(get_new_pos(&chunk, &pk_indices));
804
805                            let row_count = chunk.cardinality() as u64;
806                            cur_barrier_snapshot_processed_rows += row_count;
807                            total_snapshot_row_count += row_count;
808                            snapshot_read_row_cnt += row_count as usize;
809
810                            tracing::debug!(
811                                %table_id,
812                                ?current_pk_pos,
813                                ?snapshot_read_row_cnt,
814                                "force emit a snapshot chunk"
815                            );
816                            yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
817                        }
818                    }
819                }
820
821                // If the number of barriers reaches the snapshot interval,
822                // consume the buffered upstream chunks.
823                if let Some(current_pos) = &current_pk_pos {
824                    for chunk in upstream_chunk_buffer.drain(..) {
825                        cur_barrier_upstream_processed_rows += chunk.cardinality() as u64;
826
827                        // record the consumed binlog offset that will be
828                        // persisted later
829                        consumed_binlog_offset =
830                            get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
831
832                        yield Message::Chunk(mapping_chunk(
833                            mark_cdc_chunk(
834                                &offset_parse_func,
835                                chunk,
836                                current_pos,
837                                &pk_indices,
838                                &pk_order,
839                                last_binlog_offset.clone(),
840                            )?,
841                            &self.output_indices,
842                        ));
843                    }
844                } else {
845                    // If no current_pos, means we did not process any snapshot yet.
846                    // we can just ignore the upstream buffer chunk in that case.
847                    upstream_chunk_buffer.clear();
848                }
849
850                // Update last seen binlog offset
851                if consumed_binlog_offset.is_some() {
852                    last_binlog_offset.clone_from(&consumed_binlog_offset);
853                }
854
855                Self::report_metrics(
856                    &self.metrics,
857                    cur_barrier_snapshot_processed_rows,
858                    cur_barrier_upstream_processed_rows,
859                );
860
861                // update and persist current backfill progress
862                state_impl
863                    .mutate_state(
864                        current_pk_pos.clone(),
865                        last_binlog_offset.clone(),
866                        total_snapshot_row_count,
867                        false,
868                    )
869                    .await?;
870
871                state_impl.commit_state(pending_barrier.epoch).await?;
872                yield Message::Barrier(pending_barrier);
873            }
874        } else if self.options.disable_backfill {
875            // If backfill is disabled, we just mark the backfill as finished
876            tracing::info!(
877                %table_id,
878                upstream_table_name,
879                "CdcBackfill has been disabled"
880            );
881            state_impl
882                .mutate_state(
883                    current_pk_pos.clone(),
884                    last_binlog_offset.clone(),
885                    total_snapshot_row_count,
886                    true,
887                )
888                .await?;
889        }
890
891        upstream_table_reader.disconnect().await?;
892
893        tracing::info!(
894            %table_id,
895            upstream_table_name,
896            "CdcBackfill has already finished and will forward messages directly to the downstream"
897        );
898
899        // Wait for first barrier to come after backfill is finished.
900        // So we can update our progress + persist the status.
901        while let Some(Ok(msg)) = upstream.next().await {
902            if let Some(msg) = mapping_message(msg, &self.output_indices) {
903                // If not finished then we need to update state, otherwise no need.
904                if let Message::Barrier(barrier) = &msg {
905                    // finalized the backfill state
906                    // TODO: unify `mutate_state` and `commit_state` into one method
907                    state_impl
908                        .mutate_state(
909                            current_pk_pos.clone(),
910                            last_binlog_offset.clone(),
911                            total_snapshot_row_count,
912                            true,
913                        )
914                        .await?;
915                    state_impl.commit_state(barrier.epoch).await?;
916
917                    // mark progress as finished
918                    if let Some(progress) = self.progress.as_mut() {
919                        progress.finish(barrier.epoch, total_snapshot_row_count);
920                    }
921                    yield msg;
922                    // break after the state have been saved
923                    break;
924                }
925                yield msg;
926            }
927        }
928
929        // After backfill progress finished
930        // we can forward messages directly to the downstream,
931        // as backfill is finished.
932        #[for_await]
933        for msg in upstream {
934            // upstream offsets will be removed from the message before forwarding to
935            // downstream
936            if let Some(msg) = mapping_message(msg?, &self.output_indices) {
937                if let Message::Barrier(barrier) = &msg {
938                    // commit state just to bump the epoch of state table
939                    state_impl.commit_state(barrier.epoch).await?;
940                }
941                yield msg;
942            }
943        }
944    }
945}
946
947pub(crate) async fn build_reader_and_poll_upstream(
948    upstream: &mut BoxedMessageStream,
949    table_reader: &mut Option<ExternalTableReaderImpl>,
950    future: &mut Pin<Box<impl Future<Output = ExternalTableReaderImpl>>>,
951) -> StreamExecutorResult<Option<Message>> {
952    if table_reader.is_some() {
953        return Ok(None);
954    }
955    tokio::select! {
956        biased;
957        reader = &mut *future => {
958            *table_reader = Some(reader);
959            Ok(None)
960        }
961        msg = upstream.next() => {
962            msg.transpose()
963        }
964    }
965}
966
967#[try_stream(ok = Message, error = StreamExecutorError)]
968pub async fn transform_upstream(
969    upstream: BoxedMessageStream,
970    output_columns: Vec<ColumnDesc>,
971    timestamp_handling: Option<TimestampHandling>,
972    timestamptz_handling: Option<TimestamptzHandling>,
973    time_handling: Option<TimeHandling>,
974    bigint_unsigned_handling: Option<BigintUnsignedHandlingMode>,
975    handle_toast_columns: bool,
976) {
977    let props = SpecificParserConfig {
978        encoding_config: EncodingProperties::Json(JsonProperties {
979            use_schema_registry: false,
980            timestamp_handling,
981            timestamptz_handling,
982            time_handling,
983            bigint_unsigned_handling,
984            handle_toast_columns,
985        }),
986        // the cdc message is generated internally so the key must exist.
987        protocol_config: ProtocolProperties::Debezium(DebeziumProps::default()),
988    };
989
990    // convert to source column desc to feed into parser
991    let columns_with_meta = output_columns
992        .iter()
993        .map(SourceColumnDesc::from)
994        .collect_vec();
995    let mut parser = DebeziumParser::new(
996        props,
997        columns_with_meta.clone(),
998        Arc::new(SourceContext::dummy()),
999    )
1000    .await
1001    .map_err(StreamExecutorError::connector_error)?;
1002
1003    pin_mut!(upstream);
1004    #[for_await]
1005    for msg in upstream {
1006        let mut msg = msg?;
1007        if let Message::Chunk(chunk) = &mut msg {
1008            let parsed_chunk = parse_debezium_chunk(&mut parser, chunk).await?;
1009            let _ = std::mem::replace(chunk, parsed_chunk);
1010        }
1011        yield msg;
1012    }
1013}
1014
1015async fn parse_debezium_chunk(
1016    parser: &mut DebeziumParser,
1017    chunk: &StreamChunk,
1018) -> StreamExecutorResult<StreamChunk> {
1019    // here we transform the input chunk in `(payload varchar, _rw_offset varchar, _rw_table_name varchar)` schema
1020    // to chunk with downstream table schema `info.schema` of MergeNode contains the schema of the
1021    // table job with `_rw_offset` in the end
1022    // see `gen_create_table_plan_for_cdc_source` for details
1023
1024    // use `SourceStreamChunkBuilder` for convenience
1025    let mut builder = SourceStreamChunkBuilder::new(
1026        parser.columns().to_vec(),
1027        SourceCtrlOpts {
1028            chunk_size: chunk.capacity(),
1029            split_txn: false,
1030        },
1031    );
1032
1033    // The schema of input chunk `(payload varchar, _rw_offset varchar, _rw_table_name varchar, _row_id)`
1034    // We should use the debezium parser to parse the first column,
1035    // then chain the parsed row with `_rw_offset` row to get a new row.
1036    let payloads = chunk.data_chunk().project(&[0]);
1037    let offsets = chunk.data_chunk().project(&[1]).compact_vis();
1038
1039    // TODO: preserve the transaction semantics
1040    for payload in payloads.rows() {
1041        let ScalarRefImpl::Jsonb(jsonb_ref) = payload.datum_at(0).expect("payload must exist")
1042        else {
1043            panic!("payload must be jsonb");
1044        };
1045
1046        parser
1047            .parse_inner(
1048                None,
1049                Some(jsonb_ref.to_string().as_bytes().to_vec()),
1050                builder.row_writer(),
1051            )
1052            .await
1053            .unwrap();
1054    }
1055    builder.finish_current_chunk();
1056
1057    let parsed_chunk = {
1058        let mut iter = builder.consume_ready_chunks();
1059        assert_eq!(1, iter.len());
1060        iter.next().unwrap()
1061    };
1062    assert_eq!(parsed_chunk.capacity(), chunk.capacity()); // each payload is expected to generate one row
1063    let (ops, mut columns, vis) = parsed_chunk.into_inner();
1064    // note that `vis` is not necessarily the same as the original chunk's visibilities
1065
1066    // concat the rows in the parsed chunk with the `_rw_offset` column
1067    columns.extend(offsets.into_parts().0);
1068
1069    Ok(StreamChunk::from_parts(
1070        ops,
1071        DataChunk::from_parts(columns.into(), vis),
1072    ))
1073}
1074
1075impl<S: StateStore> Execute for CdcBackfillExecutor<S> {
1076    fn execute(self: Box<Self>) -> BoxedMessageStream {
1077        self.execute_inner().boxed()
1078    }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083    use std::collections::BTreeMap;
1084    use std::str::FromStr;
1085
1086    use futures::{StreamExt, pin_mut};
1087    use risingwave_common::array::{Array, DataChunk, Op, StreamChunk};
1088    use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, Schema, TableId};
1089    use risingwave_common::row::{OwnedRow, Row};
1090    use risingwave_common::types::{DataType, Datum, JsonbVal, ScalarImpl};
1091    use risingwave_common::util::epoch::test_epoch;
1092    use risingwave_common::util::iter_util::ZipEqFast;
1093    use risingwave_common::util::sort_util::OrderType;
1094    use risingwave_connector::source::cdc::CdcScanOptions;
1095    use risingwave_connector::source::cdc::external::mock_external_table::MockExternalTableReader;
1096    use risingwave_connector::source::cdc::external::mysql::MySqlOffset;
1097    use risingwave_connector::source::cdc::external::{
1098        CdcOffset, ExternalCdcTableType, ExternalTableConfig, SchemaTableName,
1099    };
1100    use risingwave_storage::memory::MemoryStateStore;
1101
1102    use crate::common::table::test_utils::gen_pbtable;
1103    use crate::executor::backfill::cdc::cdc_backfill::transform_upstream;
1104    use crate::executor::backfill::cdc::state::CdcBackfillState;
1105    use crate::executor::monitor::StreamingMetrics;
1106    use crate::executor::prelude::StateTable;
1107    use crate::executor::source::default_source_internal_table;
1108    use crate::executor::test_utils::MockSource;
1109    use crate::executor::{
1110        ActorContext, Barrier, CdcBackfillExecutor, ExternalStorageTable, Message,
1111    };
1112
1113    #[tokio::test]
1114    async fn test_transform_upstream_chunk() {
1115        let schema = Schema::new(vec![
1116            Field::unnamed(DataType::Jsonb),   // debezium json payload
1117            Field::unnamed(DataType::Varchar), // _rw_offset
1118            Field::unnamed(DataType::Varchar), // _rw_table_name
1119        ]);
1120        let stream_key = vec![1];
1121        let (mut tx, source) = MockSource::channel();
1122        let source = source.into_executor(schema.clone(), stream_key.clone());
1123        // let payload = r#"{"before": null,"after":{"O_ORDERKEY": 5, "O_CUSTKEY": 44485, "O_ORDERSTATUS": "F", "O_TOTALPRICE": "144659.20", "O_ORDERDATE": "1994-07-30" },"source":{"version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002", "ts_ms": 1695277757000, "snapshot": "last", "db": "mydb", "sequence": null, "table": "orders_new", "server_id": 0, "gtid": null, "file": "binlog.000008", "pos": 3693, "row": 0, "thread": null, "query": null},"op":"r","ts_ms":1695277757017,"transaction":null}"#.to_string();
1124        let payload = r#"{ "payload": { "before": null, "after": { "O_ORDERKEY": 5, "O_CUSTKEY": 44485, "O_ORDERSTATUS": "F", "O_TOTALPRICE": "144659.20", "O_ORDERDATE": "1994-07-30" }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002", "ts_ms": 1695277757000, "snapshot": "last", "db": "mydb", "sequence": null, "table": "orders_new", "server_id": 0, "gtid": null, "file": "binlog.000008", "pos": 3693, "row": 0, "thread": null, "query": null }, "op": "r", "ts_ms": 1695277757017, "transaction": null } }"#;
1125
1126        let datums: Vec<Datum> = vec![
1127            Some(JsonbVal::from_str(payload).unwrap().into()),
1128            Some("file: 1.binlog, pos: 100".to_owned().into()),
1129            Some("mydb.orders".to_owned().into()),
1130        ];
1131
1132        println!("datums: {:?}", datums[1]);
1133
1134        let mut builders = schema.create_array_builders(8);
1135        for (builder, datum) in builders.iter_mut().zip_eq_fast(datums.iter()) {
1136            builder.append(datum.clone());
1137        }
1138        let columns = builders
1139            .into_iter()
1140            .map(|builder| builder.finish().into())
1141            .collect();
1142
1143        // one row chunk
1144        let chunk = StreamChunk::from_parts(vec![Op::Insert], DataChunk::new(columns, 1));
1145
1146        tx.push_chunk(chunk);
1147        let upstream = Box::new(source).execute();
1148
1149        // schema to the debezium parser
1150        let columns = vec![
1151            ColumnDesc::named("O_ORDERKEY", ColumnId::new(1), DataType::Int64),
1152            ColumnDesc::named("O_CUSTKEY", ColumnId::new(2), DataType::Int64),
1153            ColumnDesc::named("O_ORDERSTATUS", ColumnId::new(3), DataType::Varchar),
1154            ColumnDesc::named("O_TOTALPRICE", ColumnId::new(4), DataType::Decimal),
1155            ColumnDesc::named("O_ORDERDATE", ColumnId::new(5), DataType::Date),
1156            ColumnDesc::named("commit_ts", ColumnId::new(6), DataType::Timestamptz),
1157        ];
1158
1159        let parsed_stream = transform_upstream(upstream, columns, None, None, None, None, false);
1160        pin_mut!(parsed_stream);
1161        // the output chunk must contain the offset column
1162        if let Some(message) = parsed_stream.next().await {
1163            println!("chunk: {:#?}", message.unwrap());
1164        }
1165    }
1166
1167    #[tokio::test]
1168    async fn test_build_reader_and_poll_upstream() {
1169        let actor_context = ActorContext::for_test(1);
1170        let external_storage_table = ExternalStorageTable::for_test_undefined();
1171        let schema = Schema::new(vec![
1172            Field::unnamed(DataType::Jsonb),   // debezium json payload
1173            Field::unnamed(DataType::Varchar), // _rw_offset
1174            Field::unnamed(DataType::Varchar), // _rw_table_name
1175        ]);
1176        let stream_key = vec![1];
1177        let (mut tx, source) = MockSource::channel();
1178        let source = source.into_executor(schema.clone(), stream_key.clone());
1179        let output_indices = vec![1, 0, 4]; //reorder
1180        let output_columns = vec![
1181            ColumnDesc::named("O_ORDERKEY", ColumnId::new(1), DataType::Int64),
1182            ColumnDesc::named("O_CUSTKEY", ColumnId::new(2), DataType::Int64),
1183            ColumnDesc::named("O_ORDERSTATUS", ColumnId::new(3), DataType::Varchar),
1184            ColumnDesc::named("O_TOTALPRICE", ColumnId::new(4), DataType::Decimal),
1185            ColumnDesc::named("O_DUMMY", ColumnId::new(5), DataType::Int64),
1186            ColumnDesc::named("commit_ts", ColumnId::new(6), DataType::Timestamptz),
1187        ];
1188        let store = MemoryStateStore::new();
1189        let state_table =
1190            StateTable::from_table_catalog(&default_source_internal_table(0x2333), store, None)
1191                .await;
1192        let cdc = CdcBackfillExecutor::new(
1193            actor_context,
1194            external_storage_table,
1195            source,
1196            output_indices,
1197            output_columns,
1198            None,
1199            StreamingMetrics::unused().into(),
1200            state_table,
1201            None,
1202            CdcScanOptions {
1203                // We want to mark backfill as finished. However it's not straightforward to do so.
1204                // Here we disable_backfill instead.
1205                disable_backfill: true,
1206                ..CdcScanOptions::default()
1207            },
1208            BTreeMap::default(),
1209        );
1210        // cdc.state_impl.init_epoch(EpochPair::new(test_epoch(4), test_epoch(3))).await.unwrap();
1211        // cdc.state_impl.mutate_state(None, None, 0, true).await.unwrap();
1212        // cdc.state_impl.commit_state(EpochPair::new(test_epoch(5), test_epoch(4))).await.unwrap();
1213        let s = cdc.execute_inner();
1214        pin_mut!(s);
1215
1216        // send first barrier
1217        tx.send_barrier(Barrier::new_test_barrier(test_epoch(8)));
1218        // send chunk
1219        {
1220            let payload = r#"{ "payload": { "before": null, "after": { "O_ORDERKEY": 5, "O_CUSTKEY": 44485, "O_ORDERSTATUS": "F", "O_TOTALPRICE": "144659.20", "O_DUMMY": 100 }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002", "ts_ms": 1695277757000, "snapshot": "last", "db": "mydb", "sequence": null, "table": "orders_new", "server_id": 0, "gtid": null, "file": "binlog.000008", "pos": 3693, "row": 0, "thread": null, "query": null }, "op": "r", "ts_ms": 1695277757017, "transaction": null } }"#;
1221            let datums: Vec<Datum> = vec![
1222                Some(JsonbVal::from_str(payload).unwrap().into()),
1223                Some("file: 1.binlog, pos: 100".to_owned().into()),
1224                Some("mydb.orders".to_owned().into()),
1225            ];
1226            let mut builders = schema.create_array_builders(8);
1227            for (builder, datum) in builders.iter_mut().zip_eq_fast(datums.iter()) {
1228                builder.append(datum.clone());
1229            }
1230            let columns = builders
1231                .into_iter()
1232                .map(|builder| builder.finish().into())
1233                .collect();
1234            // one row chunk
1235            let chunk = StreamChunk::from_parts(vec![Op::Insert], DataChunk::new(columns, 1));
1236
1237            tx.push_chunk(chunk);
1238        }
1239        let _first_barrier = s.next().await.unwrap();
1240        let upstream_change_log = s.next().await.unwrap().unwrap();
1241        let Message::Chunk(chunk) = upstream_change_log else {
1242            panic!("expect chunk");
1243        };
1244        assert_eq!(chunk.columns().len(), 3);
1245        assert_eq!(chunk.rows().count(), 1);
1246        assert_eq!(
1247            chunk.columns()[0].as_int64().iter().collect::<Vec<_>>(),
1248            vec![Some(44485)]
1249        );
1250        assert_eq!(
1251            chunk.columns()[1].as_int64().iter().collect::<Vec<_>>(),
1252            vec![Some(5)]
1253        );
1254        assert_eq!(
1255            chunk.columns()[2].as_int64().iter().collect::<Vec<_>>(),
1256            vec![Some(100)]
1257        );
1258    }
1259
1260    fn create_raw_cdc_chunk(rows: &[(&str, &str)]) -> StreamChunk {
1261        let schema = Schema::new(vec![
1262            Field::unnamed(DataType::Jsonb),
1263            Field::unnamed(DataType::Varchar),
1264        ]);
1265        let mut builders = schema.create_array_builders(rows.len());
1266        for (payload, offset) in rows {
1267            let payload_datum: Datum = Some(JsonbVal::from_str(payload).unwrap().into());
1268            let offset_datum: Datum = Some((*offset).into());
1269            builders[0].append(payload_datum);
1270            builders[1].append(offset_datum);
1271        }
1272        let columns = builders
1273            .into_iter()
1274            .map(|builder| builder.finish().into())
1275            .collect();
1276        StreamChunk::from_parts(
1277            vec![Op::Insert; rows.len()],
1278            DataChunk::new(columns, rows.len()),
1279        )
1280    }
1281
1282    async fn create_cdc_state_table(store: MemoryStateStore) -> StateTable<MemoryStateStore> {
1283        let state_schema = Schema::new(vec![
1284            Field::with_name(DataType::Varchar, "split_id"),
1285            Field::with_name(DataType::Int64, "id"),
1286            Field::with_name(DataType::Boolean, "backfill_finished"),
1287            Field::with_name(DataType::Int64, "row_count"),
1288            Field::with_name(DataType::Jsonb, "cdc_offset"),
1289        ]);
1290        let column_descs = vec![
1291            ColumnDesc::unnamed(ColumnId::from(0), state_schema[0].data_type.clone()),
1292            ColumnDesc::unnamed(ColumnId::from(1), state_schema[1].data_type.clone()),
1293            ColumnDesc::unnamed(ColumnId::from(2), state_schema[2].data_type.clone()),
1294            ColumnDesc::unnamed(ColumnId::from(3), state_schema[3].data_type.clone()),
1295            ColumnDesc::unnamed(ColumnId::from(4), state_schema[4].data_type.clone()),
1296        ];
1297
1298        StateTable::from_table_catalog(
1299            &gen_pbtable(
1300                TableId::from(0x42),
1301                column_descs,
1302                vec![OrderType::ascending()],
1303                vec![0],
1304                0,
1305            ),
1306            store,
1307            None,
1308        )
1309        .await
1310    }
1311
1312    #[test]
1313    fn test_consume_upstream_chunk_buffer_retains_future_rows() {
1314        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1315            &[
1316                (
1317                    Op::Insert,
1318                    OwnedRow::new(vec![
1319                        Some(ScalarImpl::Int64(1)),
1320                        Some(ScalarImpl::Int64(100)),
1321                        Some(ScalarImpl::Utf8(
1322                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1323                                .into(),
1324                        )),
1325                    ]),
1326                ),
1327                (
1328                    Op::Insert,
1329                    OwnedRow::new(vec![
1330                        Some(ScalarImpl::Int64(6)),
1331                        Some(ScalarImpl::Int64(600)),
1332                        Some(ScalarImpl::Utf8(
1333                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1334                                .into(),
1335                        )),
1336                    ]),
1337                ),
1338            ],
1339            &[DataType::Int64, DataType::Int64, DataType::Varchar],
1340        )];
1341
1342        let (emitted_chunks, drained_row_count, drained_offset) =
1343            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1344                &MockExternalTableReader::get_cdc_offset_parser(),
1345                &mut upstream_chunk_buffer,
1346                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1347                &[0],
1348                &[OrderType::ascending()],
1349                &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 2))),
1350                &[0, 1],
1351            )
1352            .unwrap();
1353
1354        assert_eq!(drained_row_count, 1);
1355        assert_eq!(
1356            drained_offset,
1357            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3)))
1358        );
1359        assert_eq!(emitted_chunks.len(), 1);
1360        assert_eq!(emitted_chunks[0].rows().count(), 1);
1361        assert_eq!(
1362            emitted_chunks[0].rows().next().unwrap().1.to_owned_row(),
1363            OwnedRow::new(vec![
1364                Some(ScalarImpl::Int64(1)),
1365                Some(ScalarImpl::Int64(100))
1366            ])
1367        );
1368
1369        assert_eq!(upstream_chunk_buffer.len(), 1);
1370        assert_eq!(upstream_chunk_buffer[0].rows().count(), 1);
1371        assert_eq!(
1372            upstream_chunk_buffer[0]
1373                .rows()
1374                .next()
1375                .unwrap()
1376                .1
1377                .to_owned_row(),
1378            OwnedRow::new(vec![
1379                Some(ScalarImpl::Int64(6)),
1380                Some(ScalarImpl::Int64(600)),
1381                Some(ScalarImpl::Utf8(
1382                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1383                        .into(),
1384                )),
1385            ])
1386        );
1387    }
1388
1389    #[test]
1390    fn test_consume_buffer_non_monotonic_pk_in_chunk() {
1391        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1392            &[
1393                (
1394                    Op::Insert,
1395                    OwnedRow::new(vec![
1396                        Some(ScalarImpl::Int64(1)),
1397                        Some(ScalarImpl::Int64(100)),
1398                        Some(ScalarImpl::Utf8(
1399                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1400                                .into(),
1401                        )),
1402                    ]),
1403                ),
1404                (
1405                    Op::Insert,
1406                    OwnedRow::new(vec![
1407                        Some(ScalarImpl::Int64(6)),
1408                        Some(ScalarImpl::Int64(600)),
1409                        Some(ScalarImpl::Utf8(
1410                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1411                                .into(),
1412                        )),
1413                    ]),
1414                ),
1415                (
1416                    Op::Insert,
1417                    OwnedRow::new(vec![
1418                        Some(ScalarImpl::Int64(2)),
1419                        Some(ScalarImpl::Int64(200)),
1420                        Some(ScalarImpl::Utf8(
1421                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
1422                                .into(),
1423                        )),
1424                    ]),
1425                ),
1426            ],
1427            &[DataType::Int64, DataType::Int64, DataType::Varchar],
1428        )];
1429
1430        let (emitted_chunks, drained_row_count, drained_offset) =
1431            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1432                &MockExternalTableReader::get_cdc_offset_parser(),
1433                &mut upstream_chunk_buffer,
1434                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1435                &[0],
1436                &[OrderType::ascending()],
1437                &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 2))),
1438                &[0, 1],
1439            )
1440            .unwrap();
1441
1442        assert_eq!(drained_row_count, 2);
1443        assert_eq!(
1444            drained_offset,
1445            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3)))
1446        );
1447        assert_eq!(emitted_chunks.len(), 1);
1448        assert_eq!(emitted_chunks[0].rows().count(), 2);
1449        assert_eq!(
1450            emitted_chunks[0]
1451                .rows()
1452                .map(|(_, row)| row.to_owned_row())
1453                .collect::<Vec<_>>(),
1454            vec![
1455                OwnedRow::new(vec![
1456                    Some(ScalarImpl::Int64(1)),
1457                    Some(ScalarImpl::Int64(100))
1458                ]),
1459                OwnedRow::new(vec![
1460                    Some(ScalarImpl::Int64(2)),
1461                    Some(ScalarImpl::Int64(200))
1462                ]),
1463            ]
1464        );
1465
1466        assert_eq!(upstream_chunk_buffer.len(), 1);
1467        assert_eq!(upstream_chunk_buffer[0].rows().count(), 1);
1468        assert_eq!(
1469            upstream_chunk_buffer[0]
1470                .rows()
1471                .next()
1472                .unwrap()
1473                .1
1474                .to_owned_row(),
1475            OwnedRow::new(vec![
1476                Some(ScalarImpl::Int64(6)),
1477                Some(ScalarImpl::Int64(600)),
1478                Some(ScalarImpl::Utf8(
1479                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1480                        .into(),
1481                )),
1482            ])
1483        );
1484    }
1485
1486    #[test]
1487    fn test_consume_buffer_processes_chunks_after_future_row() {
1488        let mut upstream_chunk_buffer = vec![
1489            StreamChunk::from_rows(
1490                &[
1491                    (
1492                        Op::Insert,
1493                        OwnedRow::new(vec![
1494                            Some(ScalarImpl::Int64(1)),
1495                            Some(ScalarImpl::Int64(100)),
1496                            Some(ScalarImpl::Utf8(
1497                                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1498                                    .into(),
1499                            )),
1500                        ]),
1501                    ),
1502                    (
1503                        Op::Insert,
1504                        OwnedRow::new(vec![
1505                            Some(ScalarImpl::Int64(6)),
1506                            Some(ScalarImpl::Int64(600)),
1507                            Some(ScalarImpl::Utf8(
1508                                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1509                                    .into(),
1510                            )),
1511                        ]),
1512                    ),
1513                ],
1514                &[DataType::Int64, DataType::Int64, DataType::Varchar],
1515            ),
1516            StreamChunk::from_rows(
1517                &[(
1518                    Op::Insert,
1519                    OwnedRow::new(vec![
1520                        Some(ScalarImpl::Int64(2)),
1521                        Some(ScalarImpl::Int64(200)),
1522                        Some(ScalarImpl::Utf8(
1523                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
1524                                .into(),
1525                        )),
1526                    ]),
1527                )],
1528                &[DataType::Int64, DataType::Int64, DataType::Varchar],
1529            ),
1530        ];
1531
1532        let (emitted_chunks, drained_row_count, drained_offset) =
1533            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1534                &MockExternalTableReader::get_cdc_offset_parser(),
1535                &mut upstream_chunk_buffer,
1536                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1537                &[0],
1538                &[OrderType::ascending()],
1539                &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 2))),
1540                &[0, 1],
1541            )
1542            .unwrap();
1543
1544        assert_eq!(drained_row_count, 2);
1545        assert_eq!(
1546            drained_offset,
1547            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3)))
1548        );
1549        assert_eq!(emitted_chunks.len(), 2);
1550        assert_eq!(emitted_chunks[0].rows().count(), 1);
1551        assert_eq!(emitted_chunks[1].rows().count(), 1);
1552        assert_eq!(
1553            emitted_chunks[1].rows().next().unwrap().1.to_owned_row(),
1554            OwnedRow::new(vec![
1555                Some(ScalarImpl::Int64(2)),
1556                Some(ScalarImpl::Int64(200))
1557            ])
1558        );
1559
1560        assert_eq!(upstream_chunk_buffer.len(), 1);
1561        assert_eq!(
1562            upstream_chunk_buffer[0]
1563                .rows()
1564                .next()
1565                .unwrap()
1566                .1
1567                .to_owned_row(),
1568            OwnedRow::new(vec![
1569                Some(ScalarImpl::Int64(6)),
1570                Some(ScalarImpl::Int64(600)),
1571                Some(ScalarImpl::Utf8(
1572                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1573                        .into(),
1574                )),
1575            ])
1576        );
1577    }
1578
1579    #[test]
1580    fn test_consume_buffer_advances_offset_after_preceding_rows_are_emitted() {
1581        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1582            &[
1583                (
1584                    Op::Insert,
1585                    OwnedRow::new(vec![
1586                        Some(ScalarImpl::Int64(6)),
1587                        Some(ScalarImpl::Int64(600)),
1588                        Some(ScalarImpl::Utf8(
1589                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1590                                .into(),
1591                        )),
1592                    ]),
1593                ),
1594                (
1595                    Op::Insert,
1596                    OwnedRow::new(vec![
1597                        Some(ScalarImpl::Int64(2)),
1598                        Some(ScalarImpl::Int64(200)),
1599                        Some(ScalarImpl::Utf8(
1600                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
1601                                .into(),
1602                        )),
1603                    ]),
1604                ),
1605            ],
1606            &[DataType::Int64, DataType::Int64, DataType::Varchar],
1607        )];
1608
1609        let (_, drained_row_count, drained_offset) =
1610            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1611                &MockExternalTableReader::get_cdc_offset_parser(),
1612                &mut upstream_chunk_buffer,
1613                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(6))])),
1614                &[0],
1615                &[OrderType::ascending()],
1616                &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3))),
1617                &[0, 1],
1618            )
1619            .unwrap();
1620
1621        assert_eq!(drained_row_count, 2);
1622        assert_eq!(
1623            drained_offset,
1624            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 5)))
1625        );
1626        assert!(upstream_chunk_buffer.is_empty());
1627    }
1628
1629    #[tokio::test]
1630    async fn test_cdc_backfill_persists_buffered_offset_on_checkpoint() {
1631        let memory_state_store = MemoryStateStore::new();
1632        let state_table = create_cdc_state_table(memory_state_store.clone()).await;
1633
1634        let (mut tx, source) = MockSource::channel();
1635        let source = source.into_executor(
1636            Schema::new(vec![
1637                Field::unnamed(DataType::Jsonb),
1638                Field::unnamed(DataType::Varchar),
1639            ]),
1640            vec![0],
1641        );
1642
1643        let external_table = ExternalStorageTable::new(
1644            TableId::new(1234),
1645            SchemaTableName {
1646                schema_name: "public".to_owned(),
1647                table_name: "mock_table".to_owned(),
1648            },
1649            "mydb".to_owned(),
1650            ExternalTableConfig::default(),
1651            ExternalCdcTableType::Mock,
1652            Schema::new(vec![
1653                Field::with_name(DataType::Int64, "id"),
1654                Field::with_name(DataType::Float64, "price"),
1655            ]),
1656            vec![OrderType::ascending()],
1657            vec![0],
1658        );
1659        let output_columns = vec![
1660            ColumnDesc::named("id", ColumnId::new(1), DataType::Int64),
1661            ColumnDesc::named("price", ColumnId::new(2), DataType::Float64),
1662        ];
1663
1664        let executor = CdcBackfillExecutor::new(
1665            ActorContext::for_test(0x1a),
1666            external_table,
1667            source,
1668            vec![0, 1],
1669            output_columns,
1670            None,
1671            StreamingMetrics::unused().into(),
1672            state_table,
1673            None,
1674            CdcScanOptions {
1675                snapshot_barrier_interval: 10,
1676                ..Default::default()
1677            },
1678            BTreeMap::default(),
1679        )
1680        .execute_inner();
1681        pin_mut!(executor);
1682
1683        tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
1684        assert!(matches!(
1685            executor.next().await.unwrap().unwrap(),
1686            Message::Barrier(_)
1687        ));
1688
1689        tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
1690        assert!(matches!(
1691            executor.next().await.unwrap().unwrap(),
1692            Message::Barrier(_)
1693        ));
1694
1695        assert!(matches!(
1696            executor.next().await.unwrap().unwrap(),
1697            Message::Chunk(_)
1698        ));
1699
1700        tx.push_chunk(create_raw_cdc_chunk(&[
1701            (
1702                r#"{ "payload": { "before": null, "after": { "id": 1, "price": 10.01 }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002" }, "op": "r", "ts_ms": 1695277757017, "transaction": null } }"#,
1703                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
1704            ),
1705            (
1706                r#"{ "payload": { "before": null, "after": { "id": 6, "price": 66.06 }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002" }, "op": "r", "ts_ms": 1695277757017, "transaction": null } }"#,
1707                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#,
1708            ),
1709        ]));
1710        tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
1711
1712        assert!(matches!(
1713            executor.next().await.unwrap().unwrap(),
1714            Message::Chunk(_)
1715        ));
1716        assert!(matches!(
1717            executor.next().await.unwrap().unwrap(),
1718            Message::Barrier(_)
1719        ));
1720
1721        let mut restored_state = CdcBackfillState::new(
1722            TableId::new(1234),
1723            create_cdc_state_table(memory_state_store).await,
1724            5,
1725        );
1726        restored_state
1727            .init_epoch(Barrier::new_test_barrier(test_epoch(3)).epoch)
1728            .await
1729            .unwrap();
1730        let state = restored_state.restore_state().await.unwrap();
1731        assert_eq!(
1732            state.last_cdc_offset,
1733            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 4)))
1734        );
1735    }
1736}