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