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