1use std::collections::BTreeMap;
16use std::future::Future;
17use std::pin::Pin;
18
19use either::Either;
20use futures::stream::select_with_strategy;
21use futures::{Stream, stream};
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
58const 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
67pub(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 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 external_table: ExternalStorageTable,
117
118 upstream: Executor,
120
121 output_indices: Vec<usize>,
123
124 output_columns: Vec<ColumnDesc>,
126
127 state_impl: CdcBackfillState<S>,
129
130 progress: Option<CreateMviewProgressReporter>,
133
134 metrics: CdcBackfillMetrics,
135
136 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 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 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 let mut current_pk_pos: Option<OwnedRow>;
314
315 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 yield Message::Barrier(first_barrier);
322
323 let mut state_impl = self.state_impl;
326
327 state_impl.init_epoch(first_barrier_epoch).await?;
328
329 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 let mut total_snapshot_row_count = state.row_count as u64;
337
338 let (timestamp_handling, timestamptz_handling, time_handling, bigint_unsigned_handling) =
339 get_cdc_json_parse_handling_from_properties(&self.properties);
340 let handle_toast_columns: bool =
342 self.external_table.table_type() == &ExternalCdcTableType::Postgres;
343 let mut upstream = transform_upstream(
345 upstream,
346 self.output_columns.clone(),
347 timestamp_handling,
348 timestamptz_handling,
349 time_handling,
350 bigint_unsigned_handling,
351 handle_toast_columns,
352 )
353 .boxed()
354 .peekable();
355 let mut last_binlog_offset = state.last_cdc_offset.clone();
356
357 if need_backfill {
378 let mut table_reader: Option<ExternalTableReaderImpl> = None;
382 let external_table = self.external_table.clone();
383 let actor_id = self.actor_ctx.id;
384 let fragment_id = self.actor_ctx.fragment_id;
385 let mut future = Box::pin(async move {
386 let backoff = get_infinite_backoff_strategy();
387 tokio_retry::Retry::spawn(backoff, || async {
388 match external_table.create_table_reader().await {
389 Ok(reader) => Ok(reader),
390 Err(e) => {
391 tracing::warn!(error = %e.as_report(), actor_id = %actor_id, fragment_id = %fragment_id, "failed to create cdc table reader, retrying...");
392 Err(e)
393 }
394 }
395 })
396 .instrument(tracing::info_span!("create_cdc_table_reader_with_retry"))
397 .await
398 .expect("Retry create cdc table reader until success.")
399 });
400 loop {
401 if let Some(msg) =
402 build_reader_and_poll_upstream(&mut upstream, &mut table_reader, &mut future)
403 .await?
404 {
405 if let Some(msg) = mapping_message(msg, &self.output_indices) {
406 match msg {
407 Message::Barrier(barrier) => {
408 state_impl.commit_state(barrier.epoch).await?;
410 yield Message::Barrier(barrier);
411 }
412 Message::Chunk(_) => {
413 }
415 Message::Watermark(_) => {
416 }
418 }
419 }
420 } else {
421 assert!(table_reader.is_some(), "table reader must created");
422 tracing::info!(
423 %table_id,
424 upstream_table_name,
425 "table reader created successfully"
426 );
427 break;
428 }
429 }
430
431 let upstream_table_reader = UpstreamTableReader::new(
432 self.external_table.clone(),
433 table_reader.expect("table reader must created"),
434 );
435
436 if last_binlog_offset.is_none() {
437 static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
439 tokio::sync::Semaphore::const_new(10);
440
441 let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
442 last_binlog_offset = upstream_table_reader.current_cdc_offset().await?;
443 }
444
445 let offset_parse_func = upstream_table_reader.reader.get_cdc_offset_parser();
446 let mut consumed_binlog_offset: Option<CdcOffset> = None;
447
448 let pk_needs_unsigned_i64_compare = {
452 let schema = self.external_table.schema();
453 let pk_names: Vec<String> = pk_indices
454 .iter()
455 .map(|&i| schema.fields[i].name.clone())
456 .collect();
457 upstream_table_reader
458 .reader
459 .pk_column_unsigned_i64_compare_flags(&pk_names)?
460 };
461
462 tracing::info!(
463 %table_id,
464 upstream_table_name,
465 initial_binlog_offset = ?last_binlog_offset,
466 ?current_pk_pos,
467 is_finished = state.is_finished,
468 is_snapshot_paused,
469 snapshot_row_count = total_snapshot_row_count,
470 rate_limit = self.rate_limit_rps,
471 disable_backfill = self.options.disable_backfill,
472 snapshot_barrier_interval = self.options.snapshot_barrier_interval,
473 snapshot_batch_size = self.options.snapshot_batch_size,
474 "start cdc backfill",
475 );
476
477 let _ = Pin::new(&mut upstream).peek().await;
480
481 #[for_await]
483 for msg in upstream.by_ref() {
484 match msg? {
485 Message::Barrier(barrier) => {
486 match barrier.mutation.as_deref() {
487 Some(crate::executor::Mutation::Pause) => {
488 is_snapshot_paused = true;
489 tracing::info!(
490 %table_id,
491 upstream_table_name,
492 "snapshot is paused by barrier"
493 );
494 }
495 Some(crate::executor::Mutation::Resume) => {
496 is_snapshot_paused = false;
497 tracing::info!(
498 %table_id,
499 upstream_table_name,
500 "snapshot is resumed by barrier"
501 );
502 }
503 _ => {
504 }
506 }
507 state_impl.commit_state(barrier.epoch).await?;
509 yield Message::Barrier(barrier);
510 break;
511 }
512 Message::Chunk(ref chunk) => {
513 last_binlog_offset = get_cdc_chunk_last_offset(&offset_parse_func, chunk)?;
514 }
515 Message::Watermark(_) => {
516 }
518 }
519 }
520
521 tracing::info!(%table_id,
522 upstream_table_name,
523 initial_binlog_offset = ?last_binlog_offset,
524 ?current_pk_pos,
525 is_snapshot_paused,
526 "start cdc backfill loop");
527
528 let mut upstream_chunk_buffer: Vec<StreamChunk> = vec![];
530
531 'backfill_loop: loop {
532 let mut should_bypass_snapshot_stream_patch =
533 self.rate_limit_rps.is_some_and(|val| val == 0);
534 let left_upstream = upstream.by_ref().map(Either::Left);
535
536 let mut snapshot_read_row_cnt: usize = 0;
537 let read_args = SnapshotReadArgs::new(
538 current_pk_pos.clone(),
539 self.rate_limit_rps,
540 pk_indices.clone(),
541 additional_columns.clone(),
542 schema_table_name.clone(),
543 external_database_name.clone(),
544 );
545 let right_snapshot = pin!(
546 upstream_table_reader
547 .snapshot_read_full_table(read_args, self.options.snapshot_batch_size)
548 .map(Either::Right)
549 );
550 let (right_snapshot, snapshot_valve) = pausable(right_snapshot);
551 if is_snapshot_paused {
552 snapshot_valve.pause();
553 }
554
555 let mut backfill_stream =
557 select_with_strategy(left_upstream, right_snapshot, |_: &mut ()| {
558 stream::PollNext::Left
559 });
560
561 let mut cur_barrier_snapshot_processed_rows: u64 = 0;
562 let mut cur_barrier_upstream_processed_rows: u64 = 0;
563 let mut barrier_count: u32 = 0;
564 let mut pending_barrier = None;
565
566 #[for_await]
567 for either in &mut backfill_stream {
568 match either {
569 Either::Left(msg) => {
571 match msg? {
572 Message::Barrier(barrier) => {
573 barrier_count += 1;
575 let can_start_new_snapshot =
576 barrier_count == self.options.snapshot_barrier_interval;
577 let mut needs_rebuild_snapshot = false;
578
579 if let Some(mutation) = barrier.mutation.as_deref() {
580 use crate::executor::Mutation;
581 match mutation {
582 Mutation::Pause => {
583 is_snapshot_paused = true;
584 snapshot_valve.pause();
585 }
586 Mutation::Resume => {
587 is_snapshot_paused = false;
588 snapshot_valve.resume();
589 }
590 Mutation::Throttle(some) => {
591 if let Some(entry) =
592 some.get(&self.actor_ctx.fragment_id)
593 && entry.throttle_type()
594 == ThrottleType::Backfill
595 && entry.rate_limit != self.rate_limit_rps
596 {
597 should_bypass_snapshot_stream_patch = self
600 .rate_limit_rps
601 .is_some_and(|val| val == 0);
602 self.rate_limit_rps = entry.rate_limit;
603 needs_rebuild_snapshot = true;
604 }
605 }
606 mutation if mutation.is_stop(self.actor_ctx.id) => {
607 tracing::info!(
609 %table_id,
610 upstream_table_name,
611 "CdcBackfill has been dropped due to config change"
612 );
613 yield Message::Barrier(barrier);
614 break 'backfill_loop;
615 }
616 _ => (),
617 }
618 }
619
620 if can_start_new_snapshot || needs_rebuild_snapshot {
623 pending_barrier = Some(barrier);
625 tracing::debug!(
626 %table_id,
627 ?current_pk_pos,
628 ?snapshot_read_row_cnt,
629 "Prepare to start a new snapshot"
630 );
631 break;
633 } else {
634 let (
636 emitted_upstream_chunks,
637 consumed_upstream_row_count,
638 consumed_binlog_offset,
639 ) = Self::consume_upstream_chunk_buffer(
640 &offset_parse_func,
641 &mut upstream_chunk_buffer,
642 current_pk_pos.as_ref(),
643 PkCompareInfo {
644 indices: &pk_indices,
645 order: &pk_order,
646 needs_unsigned_i64_compare:
647 &pk_needs_unsigned_i64_compare,
648 },
649 &last_binlog_offset,
650 &self.output_indices,
651 )?;
652 cur_barrier_upstream_processed_rows +=
653 consumed_upstream_row_count;
654 if let Some(consumed_binlog_offset) = consumed_binlog_offset
655 {
656 last_binlog_offset = Some(consumed_binlog_offset);
657 }
658 for chunk in emitted_upstream_chunks {
659 yield Message::Chunk(chunk);
660 }
661
662 Self::report_metrics(
663 &self.metrics,
664 cur_barrier_snapshot_processed_rows,
665 cur_barrier_upstream_processed_rows,
666 );
667
668 state_impl
670 .mutate_state(
671 current_pk_pos.clone(),
672 last_binlog_offset.clone(),
673 total_snapshot_row_count,
674 false,
675 )
676 .await?;
677
678 state_impl.commit_state(barrier.epoch).await?;
679
680 yield Message::Barrier(barrier);
682 }
683 }
684 Message::Chunk(chunk) => {
685 if chunk.cardinality() == 0 {
687 continue;
688 }
689
690 let chunk_binlog_offset =
691 get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
692
693 tracing::trace!(
694 "recv changelog chunk: chunk_offset {:?}, capactiy {}",
695 chunk_binlog_offset,
696 chunk.capacity()
697 );
698
699 if let Some(last_binlog_offset) = last_binlog_offset.as_ref()
703 && let Some(chunk_offset) = chunk_binlog_offset
704 && chunk_offset < *last_binlog_offset
705 {
706 tracing::trace!(
707 "skip changelog chunk: chunk_offset {:?}, capacity {}",
708 chunk_offset,
709 chunk.capacity()
710 );
711 continue;
712 }
713 upstream_chunk_buffer.push(chunk.compact_vis());
715 }
716 Message::Watermark(_) => {
717 }
719 }
720 }
721 Either::Right(msg) => {
723 match msg? {
724 None => {
725 tracing::info!(
726 %table_id,
727 ?last_binlog_offset,
728 ?current_pk_pos,
729 "snapshot read stream ends"
730 );
731 for chunk in upstream_chunk_buffer.drain(..) {
736 yield Message::Chunk(mapping_chunk(
737 chunk,
738 &self.output_indices,
739 ));
740 }
741
742 break 'backfill_loop;
744 }
745 Some(chunk) => {
746 current_pk_pos = Some(get_new_pos(&chunk, &pk_indices));
750
751 tracing::trace!(
752 "got a snapshot chunk: len {}, current_pk_pos {:?}",
753 chunk.cardinality(),
754 current_pk_pos
755 );
756 let chunk_cardinality = chunk.cardinality() as u64;
757 cur_barrier_snapshot_processed_rows += chunk_cardinality;
758 total_snapshot_row_count += chunk_cardinality;
759 yield Message::Chunk(mapping_chunk(
760 chunk,
761 &self.output_indices,
762 ));
763 }
764 }
765 }
766 }
767 }
768
769 assert!(pending_barrier.is_some(), "pending_barrier must exist");
770 let pending_barrier = pending_barrier.unwrap();
771
772 let (_, mut snapshot_stream) = backfill_stream.into_inner();
778 if !should_bypass_snapshot_stream_patch && is_snapshot_paused {
780 snapshot_valve.resume();
781 }
782 if !should_bypass_snapshot_stream_patch
783 && let Some(msg) = snapshot_stream
784 .next()
785 .instrument_await("consume_snapshot_stream_once")
786 .await
787 {
788 let Either::Right(msg) = msg else {
789 bail!("BUG: snapshot_read contains upstream messages");
790 };
791 match msg? {
792 None => {
793 tracing::info!(
794 %table_id,
795 ?last_binlog_offset,
796 ?current_pk_pos,
797 "snapshot read stream ends in the force emit branch"
798 );
799 for chunk in upstream_chunk_buffer.drain(..) {
802 yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
803 }
804
805 state_impl
807 .mutate_state(
808 current_pk_pos.clone(),
809 last_binlog_offset.clone(),
810 total_snapshot_row_count,
811 true,
812 )
813 .await?;
814
815 state_impl.commit_state(pending_barrier.epoch).await?;
817 yield Message::Barrier(pending_barrier);
818 break 'backfill_loop;
820 }
821 Some(_) if is_snapshot_paused => {
822 }
824 Some(chunk) => {
825 current_pk_pos = Some(get_new_pos(&chunk, &pk_indices));
827
828 let row_count = chunk.cardinality() as u64;
829 cur_barrier_snapshot_processed_rows += row_count;
830 total_snapshot_row_count += row_count;
831 snapshot_read_row_cnt += row_count as usize;
832
833 tracing::debug!(
834 %table_id,
835 ?current_pk_pos,
836 ?snapshot_read_row_cnt,
837 "force emit a snapshot chunk"
838 );
839 yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
840 }
841 }
842 }
843
844 if let Some(current_pos) = ¤t_pk_pos {
847 for chunk in upstream_chunk_buffer.drain(..) {
848 cur_barrier_upstream_processed_rows += chunk.cardinality() as u64;
849
850 consumed_binlog_offset =
853 get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
854
855 yield Message::Chunk(mapping_chunk(
856 mark_cdc_chunk(
857 &offset_parse_func,
858 chunk,
859 current_pos,
860 &pk_indices,
861 &pk_order,
862 &pk_needs_unsigned_i64_compare,
863 last_binlog_offset.clone(),
864 )?,
865 &self.output_indices,
866 ));
867 }
868 } else {
869 upstream_chunk_buffer.clear();
872 }
873
874 if consumed_binlog_offset.is_some() {
876 last_binlog_offset.clone_from(&consumed_binlog_offset);
877 }
878
879 Self::report_metrics(
880 &self.metrics,
881 cur_barrier_snapshot_processed_rows,
882 cur_barrier_upstream_processed_rows,
883 );
884
885 state_impl
887 .mutate_state(
888 current_pk_pos.clone(),
889 last_binlog_offset.clone(),
890 total_snapshot_row_count,
891 false,
892 )
893 .await?;
894
895 state_impl.commit_state(pending_barrier.epoch).await?;
896 yield Message::Barrier(pending_barrier);
897 }
898 upstream_table_reader.disconnect().await?;
899 } else if self.options.disable_backfill {
900 tracing::info!(
902 %table_id,
903 upstream_table_name,
904 "CdcBackfill has been disabled"
905 );
906 state_impl
907 .mutate_state(
908 current_pk_pos.clone(),
909 last_binlog_offset.clone(),
910 total_snapshot_row_count,
911 true,
912 )
913 .await?;
914 }
915
916 tracing::info!(
917 %table_id,
918 upstream_table_name,
919 "CdcBackfill has already finished and will forward messages directly to the downstream"
920 );
921
922 while let Some(Ok(msg)) = upstream.next().await {
925 if let Some(msg) = mapping_message(msg, &self.output_indices) {
926 if let Message::Barrier(barrier) = &msg {
928 state_impl
931 .mutate_state(
932 current_pk_pos.clone(),
933 last_binlog_offset.clone(),
934 total_snapshot_row_count,
935 true,
936 )
937 .await?;
938 state_impl.commit_state(barrier.epoch).await?;
939
940 if let Some(progress) = self.progress.as_mut() {
942 progress.finish(barrier.epoch, total_snapshot_row_count);
943 }
944 yield msg;
945 break;
947 }
948 yield msg;
949 }
950 }
951
952 #[for_await]
956 for msg in upstream {
957 if let Some(msg) = mapping_message(msg?, &self.output_indices) {
960 if let Message::Barrier(barrier) = &msg {
961 state_impl.commit_state(barrier.epoch).await?;
963 }
964 yield msg;
965 }
966 }
967 }
968}
969
970pub(crate) async fn build_reader_and_poll_upstream(
971 upstream: &mut (impl Stream<Item = StreamExecutorResult<Message>> + Unpin),
972 table_reader: &mut Option<ExternalTableReaderImpl>,
973 future: &mut Pin<Box<impl Future<Output = ExternalTableReaderImpl>>>,
974) -> StreamExecutorResult<Option<Message>> {
975 if table_reader.is_some() {
976 return Ok(None);
977 }
978 tokio::select! {
979 biased;
980 reader = &mut *future => {
981 *table_reader = Some(reader);
982 Ok(None)
983 }
984 msg = upstream.next() => {
985 msg.transpose()
986 }
987 }
988}
989
990#[try_stream(ok = Message, error = StreamExecutorError)]
991pub async fn transform_upstream(
992 upstream: BoxedMessageStream,
993 output_columns: Vec<ColumnDesc>,
994 timestamp_handling: Option<TimestampHandling>,
995 timestamptz_handling: Option<TimestamptzHandling>,
996 time_handling: Option<TimeHandling>,
997 bigint_unsigned_handling: Option<BigintUnsignedHandlingMode>,
998 handle_toast_columns: bool,
999) {
1000 let props = SpecificParserConfig {
1001 encoding_config: EncodingProperties::Json(JsonProperties {
1002 use_schema_registry: false,
1003 timestamp_handling,
1004 timestamptz_handling,
1005 time_handling,
1006 bigint_unsigned_handling,
1007 handle_toast_columns,
1008 }),
1009 protocol_config: ProtocolProperties::Debezium(DebeziumProps::default()),
1011 };
1012
1013 let columns_with_meta = output_columns
1015 .iter()
1016 .map(SourceColumnDesc::from)
1017 .collect_vec();
1018 let mut parser = DebeziumParser::new(
1019 props,
1020 columns_with_meta.clone(),
1021 Arc::new(SourceContext::dummy()),
1022 )
1023 .await
1024 .map_err(StreamExecutorError::connector_error)?;
1025
1026 pin_mut!(upstream);
1027 #[for_await]
1028 for msg in upstream {
1029 let mut msg = msg?;
1030 if let Message::Chunk(chunk) = &mut msg {
1031 let parsed_chunk = parse_debezium_chunk(&mut parser, chunk).await?;
1032 let _ = std::mem::replace(chunk, parsed_chunk);
1033 }
1034 yield msg;
1035 }
1036}
1037
1038async fn parse_debezium_chunk(
1039 parser: &mut DebeziumParser,
1040 chunk: &StreamChunk,
1041) -> StreamExecutorResult<StreamChunk> {
1042 let mut builder = SourceStreamChunkBuilder::new(
1049 parser.columns().to_vec(),
1050 SourceCtrlOpts {
1051 chunk_size: chunk.capacity(),
1052 split_txn: false,
1053 },
1054 );
1055
1056 let payloads = chunk.data_chunk().project(&[0]);
1060 let offsets = chunk.data_chunk().project(&[1]).compact_vis();
1061
1062 for payload in payloads.rows() {
1064 let ScalarRefImpl::Jsonb(jsonb_ref) = payload.datum_at(0).expect("payload must exist")
1065 else {
1066 panic!("payload must be jsonb");
1067 };
1068
1069 parser
1070 .parse_inner(
1071 None,
1072 Some(jsonb_ref.to_string().as_bytes().to_vec()),
1073 builder.row_writer(),
1074 )
1075 .await
1076 .unwrap();
1077 }
1078 builder.finish_current_chunk();
1079
1080 let parsed_chunk = {
1081 let mut iter = builder.consume_ready_chunks();
1082 assert_eq!(1, iter.len());
1083 iter.next().unwrap()
1084 };
1085 assert_eq!(parsed_chunk.capacity(), chunk.capacity()); let (ops, mut columns, vis) = parsed_chunk.into_inner();
1087 columns.extend(offsets.into_parts().0);
1091
1092 Ok(StreamChunk::from_parts(
1093 ops,
1094 DataChunk::from_parts(columns.into(), vis),
1095 ))
1096}
1097
1098impl<S: StateStore> Execute for CdcBackfillExecutor<S> {
1099 fn execute(self: Box<Self>) -> BoxedMessageStream {
1100 self.execute_inner().boxed()
1101 }
1102}
1103
1104#[cfg(test)]
1105mod tests {
1106 use std::collections::BTreeMap;
1107 use std::str::FromStr;
1108
1109 use futures::{StreamExt, pin_mut};
1110 use risingwave_common::array::{Array, DataChunk, Op, StreamChunk};
1111 use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, Schema, TableId};
1112 use risingwave_common::row::{OwnedRow, Row};
1113 use risingwave_common::types::{DataType, Datum, JsonbVal, ScalarImpl};
1114 use risingwave_common::util::epoch::test_epoch;
1115 use risingwave_common::util::iter_util::ZipEqFast;
1116 use risingwave_common::util::sort_util::OrderType;
1117 use risingwave_connector::source::cdc::CdcScanOptions;
1118 use risingwave_connector::source::cdc::external::mock_external_table::MockExternalTableReader;
1119 use risingwave_connector::source::cdc::external::mysql::MySqlOffset;
1120 use risingwave_connector::source::cdc::external::{
1121 CdcOffset, ExternalCdcTableType, ExternalTableConfig, SchemaTableName,
1122 };
1123 use risingwave_storage::memory::MemoryStateStore;
1124
1125 use super::PkCompareInfo;
1126 use crate::common::table::test_utils::gen_pbtable;
1127 use crate::executor::backfill::cdc::cdc_backfill::transform_upstream;
1128 use crate::executor::backfill::cdc::state::CdcBackfillState;
1129 use crate::executor::monitor::StreamingMetrics;
1130 use crate::executor::prelude::StateTable;
1131 use crate::executor::source::default_source_internal_table;
1132 use crate::executor::test_utils::MockSource;
1133 use crate::executor::{
1134 ActorContext, Barrier, CdcBackfillExecutor, ExternalStorageTable, Message,
1135 };
1136
1137 #[tokio::test]
1138 async fn test_transform_upstream_chunk() {
1139 let schema = Schema::new(vec![
1140 Field::unnamed(DataType::Jsonb), Field::unnamed(DataType::Varchar), Field::unnamed(DataType::Varchar), ]);
1144 let stream_key = vec![1];
1145 let (mut tx, source) = MockSource::channel();
1146 let source = source.into_executor(schema.clone(), stream_key.clone());
1147 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 } }"#;
1149
1150 let datums: Vec<Datum> = vec![
1151 Some(JsonbVal::from_str(payload).unwrap().into()),
1152 Some("file: 1.binlog, pos: 100".to_owned().into()),
1153 Some("mydb.orders".to_owned().into()),
1154 ];
1155
1156 let mut builders = schema.create_array_builders(8);
1157 for (builder, datum) in builders.iter_mut().zip_eq_fast(datums.iter()) {
1158 builder.append(datum.clone());
1159 }
1160 let columns = builders
1161 .into_iter()
1162 .map(|builder| builder.finish().into())
1163 .collect();
1164
1165 let chunk = StreamChunk::from_parts(vec![Op::Insert], DataChunk::new(columns, 1));
1167
1168 tx.push_chunk(chunk);
1169 let upstream = Box::new(source).execute();
1170
1171 let columns = vec![
1173 ColumnDesc::named("O_ORDERKEY", ColumnId::new(1), DataType::Int64),
1174 ColumnDesc::named("O_CUSTKEY", ColumnId::new(2), DataType::Int64),
1175 ColumnDesc::named("O_ORDERSTATUS", ColumnId::new(3), DataType::Varchar),
1176 ColumnDesc::named("O_TOTALPRICE", ColumnId::new(4), DataType::Decimal),
1177 ColumnDesc::named("O_ORDERDATE", ColumnId::new(5), DataType::Date),
1178 ColumnDesc::named("commit_ts", ColumnId::new(6), DataType::Timestamptz),
1179 ];
1180
1181 let parsed_stream = transform_upstream(upstream, columns, None, None, None, None, false);
1182 pin_mut!(parsed_stream);
1183 let message = parsed_stream
1184 .next()
1185 .await
1186 .expect("transform stream should yield the input chunk")
1187 .expect("transforming the CDC chunk should succeed");
1188 let Message::Chunk(chunk) = message else {
1189 panic!("expected a transformed chunk");
1190 };
1191 assert_eq!(
1192 chunk
1193 .rows()
1194 .map(|(op, row)| (op, row.to_owned_row()))
1195 .collect::<Vec<_>>(),
1196 vec![(
1197 Op::Insert,
1198 OwnedRow::new(vec![
1199 Some(ScalarImpl::Int64(5)),
1200 Some(ScalarImpl::Int64(44485)),
1201 Some(ScalarImpl::Utf8("F".into())),
1202 Some(ScalarImpl::Decimal("144659.20".parse().unwrap())),
1203 Some(ScalarImpl::Date("1994-07-30".parse().unwrap())),
1204 None,
1205 Some(ScalarImpl::Utf8("file: 1.binlog, pos: 100".into())),
1206 ]),
1207 )]
1208 );
1209 }
1210
1211 #[tokio::test]
1212 async fn test_build_reader_and_poll_upstream() {
1213 let actor_context = ActorContext::for_test(1);
1214 let external_storage_table = ExternalStorageTable::for_test_undefined();
1215 let schema = Schema::new(vec![
1216 Field::unnamed(DataType::Jsonb), Field::unnamed(DataType::Varchar), Field::unnamed(DataType::Varchar), ]);
1220 let stream_key = vec![1];
1221 let (mut tx, source) = MockSource::channel();
1222 let source = source.into_executor(schema.clone(), stream_key.clone());
1223 let output_indices = vec![1, 0, 4]; let output_columns = vec![
1225 ColumnDesc::named("O_ORDERKEY", ColumnId::new(1), DataType::Int64),
1226 ColumnDesc::named("O_CUSTKEY", ColumnId::new(2), DataType::Int64),
1227 ColumnDesc::named("O_ORDERSTATUS", ColumnId::new(3), DataType::Varchar),
1228 ColumnDesc::named("O_TOTALPRICE", ColumnId::new(4), DataType::Decimal),
1229 ColumnDesc::named("O_DUMMY", ColumnId::new(5), DataType::Int64),
1230 ColumnDesc::named("commit_ts", ColumnId::new(6), DataType::Timestamptz),
1231 ];
1232 let store = MemoryStateStore::new();
1233 let state_table =
1234 StateTable::from_table_catalog(&default_source_internal_table(0x2333), store, None)
1235 .await;
1236 let cdc = CdcBackfillExecutor::new(
1237 actor_context,
1238 external_storage_table,
1239 source,
1240 output_indices,
1241 output_columns,
1242 None,
1243 StreamingMetrics::unused().into(),
1244 state_table,
1245 None,
1246 CdcScanOptions {
1247 disable_backfill: true,
1250 ..CdcScanOptions::default()
1251 },
1252 BTreeMap::default(),
1253 );
1254 let s = cdc.execute_inner();
1258 pin_mut!(s);
1259
1260 tx.send_barrier(Barrier::new_test_barrier(test_epoch(8)));
1262 {
1264 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 } }"#;
1265 let datums: Vec<Datum> = vec![
1266 Some(JsonbVal::from_str(payload).unwrap().into()),
1267 Some("file: 1.binlog, pos: 100".to_owned().into()),
1268 Some("mydb.orders".to_owned().into()),
1269 ];
1270 let mut builders = schema.create_array_builders(8);
1271 for (builder, datum) in builders.iter_mut().zip_eq_fast(datums.iter()) {
1272 builder.append(datum.clone());
1273 }
1274 let columns = builders
1275 .into_iter()
1276 .map(|builder| builder.finish().into())
1277 .collect();
1278 let chunk = StreamChunk::from_parts(vec![Op::Insert], DataChunk::new(columns, 1));
1280
1281 tx.push_chunk(chunk);
1282 }
1283 let _first_barrier = s.next().await.unwrap();
1284 let upstream_change_log = s.next().await.unwrap().unwrap();
1285 let Message::Chunk(chunk) = upstream_change_log else {
1286 panic!("expect chunk");
1287 };
1288 assert_eq!(chunk.columns().len(), 3);
1289 assert_eq!(chunk.rows().count(), 1);
1290 assert_eq!(
1291 chunk.columns()[0].as_int64().iter().collect::<Vec<_>>(),
1292 vec![Some(44485)]
1293 );
1294 assert_eq!(
1295 chunk.columns()[1].as_int64().iter().collect::<Vec<_>>(),
1296 vec![Some(5)]
1297 );
1298 assert_eq!(
1299 chunk.columns()[2].as_int64().iter().collect::<Vec<_>>(),
1300 vec![Some(100)]
1301 );
1302 }
1303
1304 #[tokio::test]
1305 async fn test_disabled_cdc_backfill_finishes_without_table_reader() {
1306 let actor_context = ActorContext::for_test(1);
1307 let external_storage_table = ExternalStorageTable::new(
1308 TableId::new(1234),
1309 SchemaTableName {
1310 schema_name: "public".to_owned(),
1311 table_name: "orders".to_owned(),
1312 },
1313 "db".to_owned(),
1314 ExternalTableConfig::default(),
1315 ExternalCdcTableType::Undefined,
1316 Schema::new(vec![Field::with_name(DataType::Int64, "id")]),
1317 vec![OrderType::ascending()],
1318 vec![0],
1319 );
1320 let schema = Schema::new(vec![
1321 Field::unnamed(DataType::Jsonb),
1322 Field::unnamed(DataType::Varchar),
1323 Field::unnamed(DataType::Varchar),
1324 ]);
1325 let stream_key = vec![1];
1326 let (tx, source) = MockSource::channel();
1327 let source = source.into_executor(schema, stream_key);
1328 let output_columns = vec![ColumnDesc::named("id", ColumnId::new(1), DataType::Int64)];
1329 let memory_state_store = MemoryStateStore::new();
1330 let state_table = create_cdc_state_table(memory_state_store.clone()).await;
1331 let cdc = CdcBackfillExecutor::new(
1332 actor_context,
1333 external_storage_table,
1334 source,
1335 vec![0],
1336 output_columns,
1337 None,
1338 StreamingMetrics::unused().into(),
1339 state_table,
1340 None,
1341 CdcScanOptions {
1342 disable_backfill: true,
1343 ..CdcScanOptions::default()
1344 },
1345 BTreeMap::default(),
1346 );
1347 let executor = cdc.execute_inner();
1348 pin_mut!(executor);
1349
1350 tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
1351 assert!(matches!(
1352 executor.next().await.unwrap().unwrap(),
1353 Message::Barrier(_)
1354 ));
1355
1356 tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
1357 assert!(matches!(
1358 executor.next().await.unwrap().unwrap(),
1359 Message::Barrier(_)
1360 ));
1361
1362 let mut restored_state = CdcBackfillState::new(
1363 TableId::new(1234),
1364 create_cdc_state_table(memory_state_store).await,
1365 5,
1366 );
1367 restored_state
1368 .init_epoch(Barrier::new_test_barrier(test_epoch(2)).epoch)
1369 .await
1370 .unwrap();
1371 let state = restored_state.restore_state().await.unwrap();
1372 assert!(state.is_finished);
1373 assert_eq!(state.last_cdc_offset, None);
1374 }
1375
1376 #[tokio::test]
1377 async fn test_unfinished_cdc_backfill_rejects_null_offset_on_restore() {
1378 let memory_state_store = MemoryStateStore::new();
1379 let mut state_writer = CdcBackfillState::new(
1380 TableId::new(1234),
1381 create_cdc_state_table(memory_state_store.clone()).await,
1382 5,
1383 );
1384 state_writer
1385 .init_epoch(Barrier::new_test_barrier(test_epoch(1)).epoch)
1386 .await
1387 .unwrap();
1388 state_writer
1389 .mutate_state(
1390 Some(OwnedRow::new(vec![Some(ScalarImpl::Int64(10))])),
1391 None,
1392 10,
1393 false,
1394 )
1395 .await
1396 .unwrap();
1397 state_writer
1398 .commit_state(Barrier::new_test_barrier(test_epoch(2)).epoch)
1399 .await
1400 .unwrap();
1401
1402 let mut restored_state = CdcBackfillState::new(
1403 TableId::new(1234),
1404 create_cdc_state_table(memory_state_store).await,
1405 5,
1406 );
1407 restored_state
1408 .init_epoch(Barrier::new_test_barrier(test_epoch(2)).epoch)
1409 .await
1410 .unwrap();
1411
1412 assert!(restored_state.restore_state().await.is_err());
1413 }
1414
1415 fn create_raw_cdc_chunk(rows: &[(&str, &str)]) -> StreamChunk {
1416 let schema = Schema::new(vec![
1417 Field::unnamed(DataType::Jsonb),
1418 Field::unnamed(DataType::Varchar),
1419 ]);
1420 let mut builders = schema.create_array_builders(rows.len());
1421 for (payload, offset) in rows {
1422 let payload_datum: Datum = Some(JsonbVal::from_str(payload).unwrap().into());
1423 let offset_datum: Datum = Some((*offset).into());
1424 builders[0].append(payload_datum);
1425 builders[1].append(offset_datum);
1426 }
1427 let columns = builders
1428 .into_iter()
1429 .map(|builder| builder.finish().into())
1430 .collect();
1431 StreamChunk::from_parts(
1432 vec![Op::Insert; rows.len()],
1433 DataChunk::new(columns, rows.len()),
1434 )
1435 }
1436
1437 async fn create_cdc_state_table(store: MemoryStateStore) -> StateTable<MemoryStateStore> {
1438 let state_schema = Schema::new(vec![
1439 Field::with_name(DataType::Varchar, "split_id"),
1440 Field::with_name(DataType::Int64, "id"),
1441 Field::with_name(DataType::Boolean, "backfill_finished"),
1442 Field::with_name(DataType::Int64, "row_count"),
1443 Field::with_name(DataType::Jsonb, "cdc_offset"),
1444 ]);
1445 let column_descs = vec![
1446 ColumnDesc::unnamed(ColumnId::from(0), state_schema[0].data_type.clone()),
1447 ColumnDesc::unnamed(ColumnId::from(1), state_schema[1].data_type.clone()),
1448 ColumnDesc::unnamed(ColumnId::from(2), state_schema[2].data_type.clone()),
1449 ColumnDesc::unnamed(ColumnId::from(3), state_schema[3].data_type.clone()),
1450 ColumnDesc::unnamed(ColumnId::from(4), state_schema[4].data_type.clone()),
1451 ];
1452
1453 StateTable::from_table_catalog(
1454 &gen_pbtable(
1455 TableId::from(0x42),
1456 column_descs,
1457 vec![OrderType::ascending()],
1458 vec![0],
1459 0,
1460 ),
1461 store,
1462 None,
1463 )
1464 .await
1465 }
1466
1467 #[test]
1468 fn test_consume_upstream_chunk_buffer_retains_future_rows() {
1469 let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1470 &[
1471 (
1472 Op::Insert,
1473 OwnedRow::new(vec![
1474 Some(ScalarImpl::Int64(1)),
1475 Some(ScalarImpl::Int64(100)),
1476 Some(ScalarImpl::Utf8(
1477 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1478 .into(),
1479 )),
1480 ]),
1481 ),
1482 (
1483 Op::Insert,
1484 OwnedRow::new(vec![
1485 Some(ScalarImpl::Int64(6)),
1486 Some(ScalarImpl::Int64(600)),
1487 Some(ScalarImpl::Utf8(
1488 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1489 .into(),
1490 )),
1491 ]),
1492 ),
1493 ],
1494 &[DataType::Int64, DataType::Int64, DataType::Varchar],
1495 )];
1496
1497 let (emitted_chunks, drained_row_count, drained_offset) =
1498 CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1499 &MockExternalTableReader::get_cdc_offset_parser(),
1500 &mut upstream_chunk_buffer,
1501 Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1502 PkCompareInfo {
1503 indices: &[0],
1504 order: &[OrderType::ascending()],
1505 needs_unsigned_i64_compare: &[false],
1506 },
1507 &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 2))),
1508 &[0, 1],
1509 )
1510 .unwrap();
1511
1512 assert_eq!(drained_row_count, 1);
1513 assert_eq!(
1514 drained_offset,
1515 Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3)))
1516 );
1517 assert_eq!(emitted_chunks.len(), 1);
1518 assert_eq!(emitted_chunks[0].rows().count(), 1);
1519 assert_eq!(
1520 emitted_chunks[0].rows().next().unwrap().1.to_owned_row(),
1521 OwnedRow::new(vec![
1522 Some(ScalarImpl::Int64(1)),
1523 Some(ScalarImpl::Int64(100))
1524 ])
1525 );
1526
1527 assert_eq!(upstream_chunk_buffer.len(), 1);
1528 assert_eq!(upstream_chunk_buffer[0].rows().count(), 1);
1529 assert_eq!(
1530 upstream_chunk_buffer[0]
1531 .rows()
1532 .next()
1533 .unwrap()
1534 .1
1535 .to_owned_row(),
1536 OwnedRow::new(vec![
1537 Some(ScalarImpl::Int64(6)),
1538 Some(ScalarImpl::Int64(600)),
1539 Some(ScalarImpl::Utf8(
1540 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1541 .into(),
1542 )),
1543 ])
1544 );
1545 }
1546
1547 #[test]
1548 fn test_consume_buffer_uses_unsigned_bigint_pk_order() {
1549 let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1550 &[
1551 (
1552 Op::Insert,
1553 OwnedRow::new(vec![
1554 Some(ScalarImpl::Int64(4)),
1555 Some(ScalarImpl::Int64(400)),
1556 Some(ScalarImpl::Utf8(
1557 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1558 .into(),
1559 )),
1560 ]),
1561 ),
1562 (
1563 Op::Insert,
1564 OwnedRow::new(vec![
1565 Some(ScalarImpl::Int64(-1)),
1567 Some(ScalarImpl::Int64(900)),
1568 Some(ScalarImpl::Utf8(
1569 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1570 .into(),
1571 )),
1572 ]),
1573 ),
1574 ],
1575 &[DataType::Int64, DataType::Int64, DataType::Varchar],
1576 )];
1577
1578 let (emitted_chunks, drained_row_count, drained_offset) =
1579 CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1580 &MockExternalTableReader::get_cdc_offset_parser(),
1581 &mut upstream_chunk_buffer,
1582 Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1583 PkCompareInfo {
1584 indices: &[0],
1585 order: &[OrderType::ascending()],
1586 needs_unsigned_i64_compare: &[true],
1587 },
1588 &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 2))),
1589 &[0, 1],
1590 )
1591 .unwrap();
1592
1593 assert_eq!(drained_row_count, 1);
1594 assert_eq!(
1595 drained_offset,
1596 Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3)))
1597 );
1598 assert_eq!(emitted_chunks.len(), 1);
1599 assert_eq!(
1600 emitted_chunks[0].rows().next().unwrap().1.to_owned_row(),
1601 OwnedRow::new(vec![
1602 Some(ScalarImpl::Int64(4)),
1603 Some(ScalarImpl::Int64(400))
1604 ])
1605 );
1606
1607 assert_eq!(upstream_chunk_buffer.len(), 1);
1608 assert_eq!(
1609 upstream_chunk_buffer[0]
1610 .rows()
1611 .next()
1612 .unwrap()
1613 .1
1614 .to_owned_row(),
1615 OwnedRow::new(vec![
1616 Some(ScalarImpl::Int64(-1)),
1617 Some(ScalarImpl::Int64(900)),
1618 Some(ScalarImpl::Utf8(
1619 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1620 .into(),
1621 )),
1622 ])
1623 );
1624 }
1625
1626 #[test]
1627 fn test_consume_buffer_non_monotonic_pk_in_chunk() {
1628 let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1629 &[
1630 (
1631 Op::Insert,
1632 OwnedRow::new(vec![
1633 Some(ScalarImpl::Int64(1)),
1634 Some(ScalarImpl::Int64(100)),
1635 Some(ScalarImpl::Utf8(
1636 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1637 .into(),
1638 )),
1639 ]),
1640 ),
1641 (
1642 Op::Insert,
1643 OwnedRow::new(vec![
1644 Some(ScalarImpl::Int64(6)),
1645 Some(ScalarImpl::Int64(600)),
1646 Some(ScalarImpl::Utf8(
1647 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1648 .into(),
1649 )),
1650 ]),
1651 ),
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 ],
1664 &[DataType::Int64, DataType::Int64, DataType::Varchar],
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(), 1);
1688 assert_eq!(emitted_chunks[0].rows().count(), 2);
1689 assert_eq!(
1690 emitted_chunks[0]
1691 .rows()
1692 .map(|(_, row)| row.to_owned_row())
1693 .collect::<Vec<_>>(),
1694 vec![
1695 OwnedRow::new(vec![
1696 Some(ScalarImpl::Int64(1)),
1697 Some(ScalarImpl::Int64(100))
1698 ]),
1699 OwnedRow::new(vec![
1700 Some(ScalarImpl::Int64(2)),
1701 Some(ScalarImpl::Int64(200))
1702 ]),
1703 ]
1704 );
1705
1706 assert_eq!(upstream_chunk_buffer.len(), 1);
1707 assert_eq!(upstream_chunk_buffer[0].rows().count(), 1);
1708 assert_eq!(
1709 upstream_chunk_buffer[0]
1710 .rows()
1711 .next()
1712 .unwrap()
1713 .1
1714 .to_owned_row(),
1715 OwnedRow::new(vec![
1716 Some(ScalarImpl::Int64(6)),
1717 Some(ScalarImpl::Int64(600)),
1718 Some(ScalarImpl::Utf8(
1719 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1720 .into(),
1721 )),
1722 ])
1723 );
1724 }
1725
1726 #[test]
1727 fn test_consume_buffer_processes_chunks_after_future_row() {
1728 let mut upstream_chunk_buffer = vec![
1729 StreamChunk::from_rows(
1730 &[
1731 (
1732 Op::Insert,
1733 OwnedRow::new(vec![
1734 Some(ScalarImpl::Int64(1)),
1735 Some(ScalarImpl::Int64(100)),
1736 Some(ScalarImpl::Utf8(
1737 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1738 .into(),
1739 )),
1740 ]),
1741 ),
1742 (
1743 Op::Insert,
1744 OwnedRow::new(vec![
1745 Some(ScalarImpl::Int64(6)),
1746 Some(ScalarImpl::Int64(600)),
1747 Some(ScalarImpl::Utf8(
1748 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1749 .into(),
1750 )),
1751 ]),
1752 ),
1753 ],
1754 &[DataType::Int64, DataType::Int64, DataType::Varchar],
1755 ),
1756 StreamChunk::from_rows(
1757 &[(
1758 Op::Insert,
1759 OwnedRow::new(vec![
1760 Some(ScalarImpl::Int64(2)),
1761 Some(ScalarImpl::Int64(200)),
1762 Some(ScalarImpl::Utf8(
1763 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
1764 .into(),
1765 )),
1766 ]),
1767 )],
1768 &[DataType::Int64, DataType::Int64, DataType::Varchar],
1769 ),
1770 ];
1771
1772 let (emitted_chunks, drained_row_count, drained_offset) =
1773 CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1774 &MockExternalTableReader::get_cdc_offset_parser(),
1775 &mut upstream_chunk_buffer,
1776 Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1777 PkCompareInfo {
1778 indices: &[0],
1779 order: &[OrderType::ascending()],
1780 needs_unsigned_i64_compare: &[false],
1781 },
1782 &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 2))),
1783 &[0, 1],
1784 )
1785 .unwrap();
1786
1787 assert_eq!(drained_row_count, 2);
1788 assert_eq!(
1789 drained_offset,
1790 Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3)))
1791 );
1792 assert_eq!(emitted_chunks.len(), 2);
1793 assert_eq!(emitted_chunks[0].rows().count(), 1);
1794 assert_eq!(emitted_chunks[1].rows().count(), 1);
1795 assert_eq!(
1796 emitted_chunks[1].rows().next().unwrap().1.to_owned_row(),
1797 OwnedRow::new(vec![
1798 Some(ScalarImpl::Int64(2)),
1799 Some(ScalarImpl::Int64(200))
1800 ])
1801 );
1802
1803 assert_eq!(upstream_chunk_buffer.len(), 1);
1804 assert_eq!(
1805 upstream_chunk_buffer[0]
1806 .rows()
1807 .next()
1808 .unwrap()
1809 .1
1810 .to_owned_row(),
1811 OwnedRow::new(vec![
1812 Some(ScalarImpl::Int64(6)),
1813 Some(ScalarImpl::Int64(600)),
1814 Some(ScalarImpl::Utf8(
1815 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1816 .into(),
1817 )),
1818 ])
1819 );
1820 }
1821
1822 #[test]
1823 fn test_consume_buffer_advances_offset_after_preceding_rows_are_emitted() {
1824 let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1825 &[
1826 (
1827 Op::Insert,
1828 OwnedRow::new(vec![
1829 Some(ScalarImpl::Int64(6)),
1830 Some(ScalarImpl::Int64(600)),
1831 Some(ScalarImpl::Utf8(
1832 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1833 .into(),
1834 )),
1835 ]),
1836 ),
1837 (
1838 Op::Insert,
1839 OwnedRow::new(vec![
1840 Some(ScalarImpl::Int64(2)),
1841 Some(ScalarImpl::Int64(200)),
1842 Some(ScalarImpl::Utf8(
1843 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
1844 .into(),
1845 )),
1846 ]),
1847 ),
1848 ],
1849 &[DataType::Int64, DataType::Int64, DataType::Varchar],
1850 )];
1851
1852 let (_, drained_row_count, drained_offset) =
1853 CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1854 &MockExternalTableReader::get_cdc_offset_parser(),
1855 &mut upstream_chunk_buffer,
1856 Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(6))])),
1857 PkCompareInfo {
1858 indices: &[0],
1859 order: &[OrderType::ascending()],
1860 needs_unsigned_i64_compare: &[false],
1861 },
1862 &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3))),
1863 &[0, 1],
1864 )
1865 .unwrap();
1866
1867 assert_eq!(drained_row_count, 2);
1868 assert_eq!(
1869 drained_offset,
1870 Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 5)))
1871 );
1872 assert!(upstream_chunk_buffer.is_empty());
1873 }
1874
1875 #[tokio::test]
1876 async fn test_cdc_backfill_persists_buffered_offset_on_checkpoint() {
1877 let memory_state_store = MemoryStateStore::new();
1878 let state_table = create_cdc_state_table(memory_state_store.clone()).await;
1879
1880 let (mut tx, source) = MockSource::channel();
1881 let source = source.into_executor(
1882 Schema::new(vec![
1883 Field::unnamed(DataType::Jsonb),
1884 Field::unnamed(DataType::Varchar),
1885 ]),
1886 vec![0],
1887 );
1888
1889 let external_table = ExternalStorageTable::new(
1890 TableId::new(1234),
1891 SchemaTableName {
1892 schema_name: "public".to_owned(),
1893 table_name: "mock_table".to_owned(),
1894 },
1895 "mydb".to_owned(),
1896 ExternalTableConfig::default(),
1897 ExternalCdcTableType::Mock,
1898 Schema::new(vec![
1899 Field::with_name(DataType::Int64, "id"),
1900 Field::with_name(DataType::Float64, "price"),
1901 ]),
1902 vec![OrderType::ascending()],
1903 vec![0],
1904 );
1905 let output_columns = vec![
1906 ColumnDesc::named("id", ColumnId::new(1), DataType::Int64),
1907 ColumnDesc::named("price", ColumnId::new(2), DataType::Float64),
1908 ];
1909
1910 let executor = CdcBackfillExecutor::new(
1911 ActorContext::for_test(0x1a),
1912 external_table,
1913 source,
1914 vec![0, 1],
1915 output_columns,
1916 None,
1917 StreamingMetrics::unused().into(),
1918 state_table,
1919 None,
1920 CdcScanOptions {
1921 snapshot_barrier_interval: 10,
1922 ..Default::default()
1923 },
1924 BTreeMap::default(),
1925 )
1926 .execute_inner();
1927 pin_mut!(executor);
1928
1929 tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
1930 assert!(matches!(
1931 executor.next().await.unwrap().unwrap(),
1932 Message::Barrier(_)
1933 ));
1934
1935 tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
1936 assert!(matches!(
1937 executor.next().await.unwrap().unwrap(),
1938 Message::Barrier(_)
1939 ));
1940
1941 assert!(matches!(
1942 executor.next().await.unwrap().unwrap(),
1943 Message::Chunk(_)
1944 ));
1945
1946 tx.push_chunk(create_raw_cdc_chunk(&[
1947 (
1948 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 } }"#,
1949 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
1950 ),
1951 (
1952 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 } }"#,
1953 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#,
1954 ),
1955 ]));
1956 tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
1957
1958 assert!(matches!(
1959 executor.next().await.unwrap().unwrap(),
1960 Message::Chunk(_)
1961 ));
1962 assert!(matches!(
1963 executor.next().await.unwrap().unwrap(),
1964 Message::Barrier(_)
1965 ));
1966
1967 let mut restored_state = CdcBackfillState::new(
1968 TableId::new(1234),
1969 create_cdc_state_table(memory_state_store).await,
1970 5,
1971 );
1972 restored_state
1973 .init_epoch(Barrier::new_test_barrier(test_epoch(3)).epoch)
1974 .await
1975 .unwrap();
1976 let state = restored_state.restore_state().await.unwrap();
1977 assert_eq!(
1978 state.last_cdc_offset,
1979 Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 4)))
1980 );
1981 }
1982}