1use 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
57const METADATA_STATE_LEN: usize = 4;
59
60pub(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 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 external_table: ExternalStorageTable,
110
111 upstream: Executor,
113
114 output_indices: Vec<usize>,
116
117 output_columns: Vec<ColumnDesc>,
119
120 state_impl: CdcBackfillState<S>,
122
123 progress: Option<CreateMviewProgressReporter>,
126
127 metrics: CdcBackfillMetrics,
128
129 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 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 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 let mut current_pk_pos: Option<OwnedRow>;
304
305 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 yield Message::Barrier(first_barrier);
312
313 let mut state_impl = self.state_impl;
316
317 state_impl.init_epoch(first_barrier_epoch).await?;
318
319 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 let mut total_snapshot_row_count = state.row_count as u64;
327
328 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 let handle_toast_columns: bool =
353 self.external_table.table_type() == &ExternalCdcTableType::Postgres;
354 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 state_impl.commit_state(barrier.epoch).await?;
375 yield Message::Barrier(barrier);
376 }
377 Message::Chunk(chunk) => {
378 if need_backfill {
379 } else {
381 yield Message::Chunk(chunk);
383 }
384 }
385 Message::Watermark(_) => {
386 }
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 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 if need_backfill {
458 let _ = Pin::new(&mut upstream).peek().await;
461
462 #[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 }
487 }
488 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 }
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 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 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 Either::Left(msg) => {
552 match msg? {
553 Message::Barrier(barrier) => {
554 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 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 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 if can_start_new_snapshot || needs_rebuild_snapshot {
604 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;
614 } else {
615 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 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 yield Message::Barrier(barrier);
659 }
660 }
661 Message::Chunk(chunk) => {
662 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 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 upstream_chunk_buffer.push(chunk.compact_vis());
692 }
693 Message::Watermark(_) => {
694 }
696 }
697 }
698 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 for chunk in upstream_chunk_buffer.drain(..) {
713 yield Message::Chunk(mapping_chunk(
714 chunk,
715 &self.output_indices,
716 ));
717 }
718
719 break 'backfill_loop;
721 }
722 Some(chunk) => {
723 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 let (_, mut snapshot_stream) = backfill_stream.into_inner();
755 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 for chunk in upstream_chunk_buffer.drain(..) {
779 yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
780 }
781
782 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 state_impl.commit_state(pending_barrier.epoch).await?;
794 yield Message::Barrier(pending_barrier);
795 break 'backfill_loop;
797 }
798 Some(_) if is_snapshot_paused => {
799 }
801 Some(chunk) => {
802 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 let Some(current_pos) = ¤t_pk_pos {
824 for chunk in upstream_chunk_buffer.drain(..) {
825 cur_barrier_upstream_processed_rows += chunk.cardinality() as u64;
826
827 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 upstream_chunk_buffer.clear();
848 }
849
850 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 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 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 while let Some(Ok(msg)) = upstream.next().await {
902 if let Some(msg) = mapping_message(msg, &self.output_indices) {
903 if let Message::Barrier(barrier) = &msg {
905 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 if let Some(progress) = self.progress.as_mut() {
919 progress.finish(barrier.epoch, total_snapshot_row_count);
920 }
921 yield msg;
922 break;
924 }
925 yield msg;
926 }
927 }
928
929 #[for_await]
933 for msg in upstream {
934 if let Some(msg) = mapping_message(msg?, &self.output_indices) {
937 if let Message::Barrier(barrier) = &msg {
938 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 protocol_config: ProtocolProperties::Debezium(DebeziumProps::default()),
988 };
989
990 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 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 let payloads = chunk.data_chunk().project(&[0]);
1037 let offsets = chunk.data_chunk().project(&[1]).compact_vis();
1038
1039 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()); let (ops, mut columns, vis) = parsed_chunk.into_inner();
1064 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), Field::unnamed(DataType::Varchar), Field::unnamed(DataType::Varchar), ]);
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#"{ "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 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 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 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), Field::unnamed(DataType::Varchar), Field::unnamed(DataType::Varchar), ]);
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]; 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 disable_backfill: true,
1206 ..CdcScanOptions::default()
1207 },
1208 BTreeMap::default(),
1209 );
1210 let s = cdc.execute_inner();
1214 pin_mut!(s);
1215
1216 tx.send_barrier(Barrier::new_test_barrier(test_epoch(8)));
1218 {
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 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}