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