1use std::collections::BTreeMap;
16
17use either::Either;
18use futures::stream;
19use futures::stream::select_with_strategy;
20use itertools::Itertools;
21use risingwave_common::bitmap::BitmapBuilder;
22use risingwave_common::catalog::{ColumnDesc, Field};
23use risingwave_common::row::RowDeserializer;
24use risingwave_common::util::iter_util::ZipEqFast;
25use risingwave_common::util::sort_util::{OrderType, cmp_datum};
26use risingwave_connector::source::cdc::CdcScanOptions;
27use risingwave_connector::source::cdc::external::{
28 CdcOffset, ExternalCdcTableType, ExternalTableReaderImpl,
29};
30use risingwave_connector::source::{CdcTableSnapshotSplit, CdcTableSnapshotSplitRaw};
31use risingwave_pb::common::ThrottleType;
32use rw_futures_util::pausable;
33use thiserror_ext::AsReport;
34use tracing::Instrument;
35
36use crate::executor::backfill::cdc::cdc_backfill::{
37 build_reader_and_poll_upstream, get_cdc_json_parse_handling_from_properties, transform_upstream,
38};
39use crate::executor::backfill::cdc::state_v2::ParallelizedCdcBackfillState;
40use crate::executor::backfill::cdc::upstream_table::external::ExternalStorageTable;
41use crate::executor::backfill::cdc::upstream_table::snapshot::{
42 SplitSnapshotReadArgs, UpstreamTableRead, UpstreamTableReader,
43};
44use crate::executor::backfill::utils::{get_cdc_chunk_last_offset, mapping_chunk, mapping_message};
45use crate::executor::prelude::*;
46use crate::executor::source::get_infinite_backoff_strategy;
47use crate::task::cdc_progress::CdcProgressReporter;
48pub struct ParallelizedCdcBackfillExecutor<S: StateStore> {
49 actor_ctx: ActorContextRef,
50
51 external_table: ExternalStorageTable,
53
54 upstream: Executor,
56
57 output_indices: Vec<usize>,
59
60 output_columns: Vec<ColumnDesc>,
62
63 rate_limit_rps: Option<u32>,
65
66 options: CdcScanOptions,
67
68 state_table: StateTable<S>,
69
70 properties: BTreeMap<String, String>,
71
72 progress: Option<CdcProgressReporter>,
73}
74
75impl<S: StateStore> ParallelizedCdcBackfillExecutor<S> {
76 #[expect(clippy::too_many_arguments)]
77 pub fn new(
78 actor_ctx: ActorContextRef,
79 external_table: ExternalStorageTable,
80 upstream: Executor,
81 output_indices: Vec<usize>,
82 output_columns: Vec<ColumnDesc>,
83 _metrics: Arc<StreamingMetrics>,
84 state_table: StateTable<S>,
85 rate_limit_rps: Option<u32>,
86 options: CdcScanOptions,
87 properties: BTreeMap<String, String>,
88 progress: Option<CdcProgressReporter>,
89 ) -> Self {
90 Self {
91 actor_ctx,
92 external_table,
93 upstream,
94 output_indices,
95 output_columns,
96 rate_limit_rps,
97 options,
98 state_table,
99 properties,
100 progress,
101 }
102 }
103
104 #[try_stream(ok = Message, error = StreamExecutorError)]
105 async fn execute_inner(mut self) {
106 assert!(!self.options.disable_backfill);
107 let pk_indices = self.external_table.pk_indices().to_vec();
109 let table_id = self.external_table.table_id();
110 let upstream_table_name = self.external_table.qualified_table_name();
111 let schema_table_name = self.external_table.schema_table_name().clone();
112 let external_database_name = self.external_table.database_name().to_owned();
113 let additional_columns = self
114 .output_columns
115 .iter()
116 .filter(|col| col.additional_column.column_type.is_some())
117 .cloned()
118 .collect_vec();
119 assert!(
120 (self.options.backfill_split_pk_column_index as usize) < pk_indices.len(),
121 "split pk column index {} out of bound",
122 self.options.backfill_split_pk_column_index
123 );
124 let snapshot_split_column_index =
125 pk_indices[self.options.backfill_split_pk_column_index as usize];
126 let cdc_table_snapshot_split_column =
127 vec![self.external_table.schema().fields[snapshot_split_column_index].clone()];
128
129 let mut upstream = self.upstream.execute();
130 let first_barrier = expect_first_barrier(&mut upstream).await?;
132 let (timestamp_handling, timestamptz_handling, time_handling, bigint_unsigned_handling) =
138 get_cdc_json_parse_handling_from_properties(&self.properties);
139 let handle_toast_columns: bool =
141 self.external_table.table_type() == &ExternalCdcTableType::Postgres;
142 let mut upstream = transform_upstream(
143 upstream,
144 self.output_columns.clone(),
145 timestamp_handling,
146 timestamptz_handling,
147 time_handling,
148 bigint_unsigned_handling,
149 handle_toast_columns,
150 )
151 .boxed();
152 let mut next_reset_barrier = Some(first_barrier);
153 let mut is_reset = false;
154 let mut state_impl = ParallelizedCdcBackfillState::new(self.state_table);
155 let mut upstream_chunk_buffer: Vec<StreamChunk> = vec![];
157
158 'with_cdc_table_snapshot_splits: loop {
160 assert!(upstream_chunk_buffer.is_empty());
161 let reset_barrier = next_reset_barrier.take().unwrap();
162 let all_snapshot_splits = match reset_barrier.mutation.as_deref() {
163 Some(Mutation::Add(add)) => &add.actor_cdc_table_snapshot_splits.splits,
164
165 Some(Mutation::Update(update)) => &update.actor_cdc_table_snapshot_splits.splits,
166 _ => {
167 return Err(anyhow::anyhow!("ParallelizedCdcBackfillExecutor expects either Mutation::Add or Mutation::Update to initialize CDC table snapshot splits.").into());
168 }
169 };
170 let mut actor_snapshot_splits = vec![];
171 let mut generation = None;
172 if let Some((splits, snapshot_generation)) = all_snapshot_splits.get(&self.actor_ctx.id)
174 {
175 actor_snapshot_splits = splits
176 .iter()
177 .map(|s: &CdcTableSnapshotSplitRaw| {
178 let de = RowDeserializer::new(
179 cdc_table_snapshot_split_column
180 .iter()
181 .map(Field::data_type)
182 .collect_vec(),
183 );
184 let left_bound_inclusive =
185 de.deserialize(s.left_bound_inclusive.as_ref()).unwrap();
186 let right_bound_exclusive =
187 de.deserialize(s.right_bound_exclusive.as_ref()).unwrap();
188 CdcTableSnapshotSplit {
189 split_id: s.split_id,
190 left_bound_inclusive,
191 right_bound_exclusive,
192 }
193 })
194 .collect();
195 generation = Some(*snapshot_generation);
196 }
197 tracing::debug!(?actor_snapshot_splits, ?generation, "actor splits");
198 assert_consecutive_splits(&actor_snapshot_splits);
199
200 let mut is_snapshot_paused = reset_barrier.is_pause_on_startup();
201 let barrier_epoch = reset_barrier.epoch;
202 yield Message::Barrier(reset_barrier);
203 if !is_reset {
204 state_impl.init_epoch(barrier_epoch).await?;
205 is_reset = true;
206 tracing::info!(%table_id, "Initialize executor.");
207 } else {
208 tracing::info!(%table_id, "Reset executor.");
209 }
210
211 let mut current_actor_bounds = None;
212 let mut actor_cdc_offset_high: Option<CdcOffset> = None;
213 let mut actor_cdc_offset_low: Option<CdcOffset> = None;
214 let mut next_split_idx = actor_snapshot_splits.len();
216 for (idx, split) in actor_snapshot_splits.iter().enumerate() {
217 let state = state_impl.restore_state(split.split_id).await?;
218 if !state.is_finished {
219 next_split_idx = idx;
220 break;
221 }
222 extends_current_actor_bound(&mut current_actor_bounds, split);
223 if let Some(ref cdc_offset) = state.cdc_offset_low {
224 if let Some(ref cur) = actor_cdc_offset_low {
225 if *cur > *cdc_offset {
226 actor_cdc_offset_low = state.cdc_offset_low.clone();
227 }
228 } else {
229 actor_cdc_offset_low = state.cdc_offset_low.clone();
230 }
231 }
232 if let Some(ref cdc_offset) = state.cdc_offset_high {
233 if let Some(ref cur) = actor_cdc_offset_high {
234 if *cur < *cdc_offset {
235 actor_cdc_offset_high = state.cdc_offset_high.clone();
236 }
237 } else {
238 actor_cdc_offset_high = state.cdc_offset_high.clone();
239 }
240 }
241 }
242 for split in actor_snapshot_splits.iter().skip(next_split_idx) {
243 state_impl
245 .mutate_state(split.split_id, false, 0, None, None)
246 .await?;
247 }
248 let mut should_report_actor_backfill_progress = if next_split_idx > 0 {
249 Some((
250 actor_snapshot_splits[0].split_id,
251 actor_snapshot_splits[next_split_idx - 1].split_id,
252 ))
253 } else {
254 None
255 };
256
257 let mut table_reader: Option<ExternalTableReaderImpl> = None;
260 let external_table = self.external_table.clone();
261 let mut future = Box::pin(async move {
262 let backoff = get_infinite_backoff_strategy();
263 tokio_retry::Retry::spawn(backoff, || async {
264 match external_table.create_table_reader().await {
265 Ok(reader) => Ok(reader),
266 Err(e) => {
267 tracing::warn!(error = %e.as_report(), "failed to create cdc table reader, retrying...");
268 Err(e)
269 }
270 }
271 })
272 .instrument(tracing::info_span!("create_cdc_table_reader_with_retry"))
273 .await
274 .expect("Retry create cdc table reader until success.")
275 });
276 loop {
277 if let Some(msg) =
278 build_reader_and_poll_upstream(&mut upstream, &mut table_reader, &mut future)
279 .await?
280 {
281 if let Some(msg) = mapping_message(msg, &self.output_indices) {
282 match msg {
283 Message::Barrier(barrier) => {
284 state_impl.commit_state(barrier.epoch).await?;
285 if is_reset_barrier(&barrier, self.actor_ctx.id) {
286 next_reset_barrier = Some(barrier);
287 continue 'with_cdc_table_snapshot_splits;
288 }
289 yield Message::Barrier(barrier);
290 }
291 Message::Chunk(chunk) => {
292 if chunk.cardinality() == 0 {
293 continue;
294 }
295 if let Some(filtered_chunk) = filter_stream_chunk(
296 chunk,
297 ¤t_actor_bounds,
298 snapshot_split_column_index,
299 ) && filtered_chunk.cardinality() > 0
300 {
301 yield Message::Chunk(filtered_chunk);
302 }
303 }
304 Message::Watermark(_) => {
305 }
307 }
308 }
309 } else {
310 assert!(table_reader.is_some(), "table reader must created");
311 tracing::info!(
312 %table_id,
313 upstream_table_name,
314 "table reader created successfully"
315 );
316 break;
317 }
318 }
319 let upstream_table_reader = UpstreamTableReader::new(
320 self.external_table.clone(),
321 table_reader.expect("table reader must created"),
322 );
323 let offset_parse_func = upstream_table_reader.reader.get_cdc_offset_parser();
325
326 for split in actor_snapshot_splits.iter().skip(next_split_idx) {
328 tracing::info!(
329 %table_id,
330 upstream_table_name,
331 ?split,
332 is_snapshot_paused,
333 "start cdc backfill split"
334 );
335 let finished_split_bounds = current_actor_bounds.clone();
336 let current_split_bounds = Some((
337 split.left_bound_inclusive.clone(),
338 split.right_bound_exclusive.clone(),
339 ));
340 extends_current_actor_bound(&mut current_actor_bounds, split);
341
342 let split_cdc_offset_low = {
343 static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
345 tokio::sync::Semaphore::const_new(10);
346
347 let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
348 upstream_table_reader.current_cdc_offset().await?
349 };
350 if let Some(ref cdc_offset) = split_cdc_offset_low {
351 if let Some(ref cur) = actor_cdc_offset_low {
352 if *cur > *cdc_offset {
353 actor_cdc_offset_low = split_cdc_offset_low.clone();
354 }
355 } else {
356 actor_cdc_offset_low = split_cdc_offset_low.clone();
357 }
358 }
359 let mut split_cdc_offset_high = None;
360
361 let left_upstream = upstream.by_ref().map(Either::Left);
362 let read_args = SplitSnapshotReadArgs::new(
363 split.left_bound_inclusive.clone(),
364 split.right_bound_exclusive.clone(),
365 cdc_table_snapshot_split_column.clone(),
366 self.rate_limit_rps,
367 additional_columns.clone(),
368 schema_table_name.clone(),
369 external_database_name.clone(),
370 );
371 let right_snapshot = pin!(
372 upstream_table_reader
373 .snapshot_read_table_split(read_args)
374 .map(Either::Right)
375 );
376 let (right_snapshot, snapshot_valve) = pausable(right_snapshot);
377 if is_snapshot_paused {
378 snapshot_valve.pause();
379 }
380 let mut backfill_stream =
381 select_with_strategy(left_upstream, right_snapshot, |_: &mut ()| {
382 stream::PollNext::Left
383 });
384 let mut row_count: u64 = 0;
385 #[for_await]
386 for either in &mut backfill_stream {
387 match either {
388 Either::Left(msg) => {
390 match msg? {
391 Message::Barrier(barrier) => {
392 state_impl.commit_state(barrier.epoch).await?;
393 if let Some(mutation) = barrier.mutation.as_deref() {
394 use crate::executor::Mutation;
395 match mutation {
396 Mutation::Pause => {
397 is_snapshot_paused = true;
398 snapshot_valve.pause();
399 }
400 Mutation::Resume => {
401 is_snapshot_paused = false;
402 snapshot_valve.resume();
403 }
404 Mutation::Throttle(some) => {
405 if let Some(entry) =
409 some.get(&self.actor_ctx.fragment_id)
410 && entry.throttle_type()
411 == ThrottleType::Backfill
412 && entry.rate_limit != self.rate_limit_rps
413 {
414 self.rate_limit_rps = entry.rate_limit;
416 }
417 }
418 mutation if mutation.is_stop(self.actor_ctx.id) => {
419 tracing::info!(
420 %table_id,
421 upstream_table_name,
422 "CdcBackfill has been dropped due to config change"
423 );
424 for chunk in upstream_chunk_buffer.drain(..) {
425 yield Message::Chunk(chunk);
426 }
427 yield Message::Barrier(barrier);
428 let () = futures::future::pending().await;
429 unreachable!();
430 }
431 _ => (),
432 }
433 }
434 if is_reset_barrier(&barrier, self.actor_ctx.id) {
435 next_reset_barrier = Some(barrier);
436 for chunk in upstream_chunk_buffer.drain(..) {
437 yield Message::Chunk(chunk);
438 }
439 continue 'with_cdc_table_snapshot_splits;
440 }
441 if let Some(split_range) =
442 should_report_actor_backfill_progress.take()
443 && let Some(ref progress) = self.progress
444 {
445 progress.update(
446 self.actor_ctx.fragment_id,
447 self.actor_ctx.id,
448 barrier.epoch,
449 generation.expect("should have set generation when having progress to report"),
450 split_range,
451 );
452 }
453 yield Message::Barrier(barrier);
455 }
456 Message::Chunk(chunk) => {
457 if chunk.cardinality() == 0 {
459 continue;
460 }
461
462 let chunk = mapping_chunk(chunk, &self.output_indices);
475 let (finished_chunk, current_chunk) =
476 split_finished_and_current_chunk(
477 chunk,
478 &finished_split_bounds,
479 ¤t_split_bounds,
480 snapshot_split_column_index,
481 );
482 if let Some(finished_chunk) = finished_chunk
483 && finished_chunk.cardinality() > 0
484 {
485 yield Message::Chunk(finished_chunk);
486 }
487 if let Some(filtered_chunk) = current_chunk
488 && filtered_chunk.cardinality() > 0
489 {
490 upstream_chunk_buffer.push(filtered_chunk);
492 }
493 }
494 Message::Watermark(_) => {
495 }
497 }
498 }
499 Either::Right(msg) => {
501 match msg? {
502 None => {
503 tracing::info!(
504 %table_id,
505 split_id = split.split_id,
506 "snapshot read stream ends"
507 );
508 for chunk in upstream_chunk_buffer.drain(..) {
509 yield Message::Chunk(chunk);
510 }
511
512 split_cdc_offset_high = {
513 static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
515 tokio::sync::Semaphore::const_new(10);
516
517 let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
518 upstream_table_reader.current_cdc_offset().await?
519 };
520 if let Some(ref cdc_offset) = split_cdc_offset_high {
521 if let Some(ref cur) = actor_cdc_offset_high {
522 if *cur < *cdc_offset {
523 actor_cdc_offset_high =
524 split_cdc_offset_high.clone();
525 }
526 } else {
527 actor_cdc_offset_high = split_cdc_offset_high.clone();
528 }
529 }
530 break;
532 }
533 Some(chunk) => {
534 let chunk_cardinality = chunk.cardinality() as u64;
535 row_count = row_count.saturating_add(chunk_cardinality);
536 yield Message::Chunk(mapping_chunk(
537 chunk,
538 &self.output_indices,
539 ));
540 }
541 }
542 }
543 }
544 }
545 state_impl
547 .mutate_state(
548 split.split_id,
549 true,
550 row_count,
551 split_cdc_offset_low,
552 split_cdc_offset_high,
553 )
554 .await?;
555 if let Some((_, right_split)) = &mut should_report_actor_backfill_progress {
556 assert!(
557 *right_split < split.split_id,
558 "{} {}",
559 *right_split,
560 split.split_id
561 );
562 *right_split = split.split_id;
563 } else {
564 should_report_actor_backfill_progress = Some((split.split_id, split.split_id));
565 }
566 }
567
568 upstream_table_reader.disconnect().await?;
569 tracing::info!(
570 %table_id,
571 upstream_table_name,
572 "CdcBackfill has already finished and will forward messages directly to the downstream"
573 );
574
575 let mut should_report_actor_backfill_done = false;
576 #[for_await]
580 for msg in &mut upstream {
581 let msg = msg?;
582 match msg {
583 Message::Barrier(barrier) => {
584 state_impl.commit_state(barrier.epoch).await?;
585 if is_reset_barrier(&barrier, self.actor_ctx.id) {
586 next_reset_barrier = Some(barrier);
587 continue 'with_cdc_table_snapshot_splits;
588 }
589 if let Some(split_range) = should_report_actor_backfill_progress.take()
590 && let Some(ref progress) = self.progress
591 {
592 progress.update(
593 self.actor_ctx.fragment_id,
594 self.actor_ctx.id,
595 barrier.epoch,
596 generation.expect(
597 "should have set generation when having progress to report",
598 ),
599 split_range,
600 );
601 }
602 if should_report_actor_backfill_done {
603 should_report_actor_backfill_done = false;
604 assert!(!actor_snapshot_splits.is_empty());
605 if let Some(ref progress) = self.progress {
606 progress.finish(
607 self.actor_ctx.fragment_id,
608 self.actor_ctx.id,
609 barrier.epoch,
610 generation.expect(
611 "should have set generation when having progress to report",
612 ),
613 (
614 actor_snapshot_splits[0].split_id,
615 actor_snapshot_splits[actor_snapshot_splits.len() - 1]
616 .split_id,
617 ),
618 );
619 }
620 }
621 yield Message::Barrier(barrier);
622 }
623 Message::Chunk(chunk) => {
624 if actor_snapshot_splits.is_empty() {
625 continue;
626 }
627 if chunk.cardinality() == 0 {
628 continue;
629 }
630
631 let chunk_cdc_offset =
632 get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
633 if let Some(high) = actor_cdc_offset_high.as_ref() {
644 if state_impl.is_legacy_state() {
645 actor_cdc_offset_high = None;
647 should_report_actor_backfill_done = true;
648 } else if let Some(ref chunk_offset) = chunk_cdc_offset
649 && *chunk_offset >= *high
650 {
651 actor_cdc_offset_high = None;
653 should_report_actor_backfill_done = true;
654 }
655 }
656 let chunk = mapping_chunk(chunk, &self.output_indices);
657 if let Some(filtered_chunk) = filter_stream_chunk(
658 chunk,
659 ¤t_actor_bounds,
660 snapshot_split_column_index,
661 ) && filtered_chunk.cardinality() > 0
662 {
663 yield Message::Chunk(filtered_chunk);
664 }
665 }
666 msg @ Message::Watermark(_) => {
667 if let Some(msg) = mapping_message(msg, &self.output_indices) {
668 yield msg;
669 }
670 }
671 }
672 }
673 }
674 }
675}
676
677fn split_finished_and_current_chunk(
678 chunk: StreamChunk,
679 finished_split_bounds: &Option<(OwnedRow, OwnedRow)>,
680 current_split_bounds: &Option<(OwnedRow, OwnedRow)>,
681 snapshot_split_column_index: usize,
682) -> (Option<StreamChunk>, Option<StreamChunk>) {
683 let finished_chunk = filter_stream_chunk(
684 chunk.clone(),
685 finished_split_bounds,
686 snapshot_split_column_index,
687 )
688 .map(StreamChunk::compact_vis);
689 let current_chunk =
690 filter_stream_chunk(chunk, current_split_bounds, snapshot_split_column_index)
691 .map(StreamChunk::compact_vis);
692 (finished_chunk, current_chunk)
693}
694
695fn filter_stream_chunk(
696 chunk: StreamChunk,
697 bound: &Option<(OwnedRow, OwnedRow)>,
698 snapshot_split_column_index: usize,
699) -> Option<StreamChunk> {
700 let Some((left, right)) = bound else {
701 return None;
702 };
703 assert_eq!(left.len(), 1, "multiple split columns is not supported yet");
704 assert_eq!(
705 right.len(),
706 1,
707 "multiple split columns is not supported yet"
708 );
709 let left_split_key = left.datum_at(0);
710 let right_split_key = right.datum_at(0);
711 let is_leftmost_bound = is_leftmost_bound(left);
712 let is_rightmost_bound = is_rightmost_bound(right);
713 if is_leftmost_bound && is_rightmost_bound {
714 return Some(chunk);
715 }
716 let mut new_bitmap = BitmapBuilder::with_capacity(chunk.capacity());
717 let (ops, columns, visibility) = chunk.into_inner();
718 for (row_split_key, v) in columns[snapshot_split_column_index]
719 .iter()
720 .zip_eq_fast(visibility.iter())
721 {
722 if !v {
723 new_bitmap.append(false);
724 continue;
725 }
726 let mut is_in_range = true;
727 if !is_leftmost_bound {
728 is_in_range = cmp_datum(
729 row_split_key,
730 left_split_key,
731 OrderType::ascending_nulls_first(),
732 )
733 .is_ge();
734 }
735 if is_in_range && !is_rightmost_bound {
736 is_in_range = cmp_datum(
737 row_split_key,
738 right_split_key,
739 OrderType::ascending_nulls_first(),
740 )
741 .is_lt();
742 }
743 if !is_in_range {
744 tracing::trace!(?row_split_key, ?left_split_key, ?right_split_key, snapshot_split_column_index, data_type = ?columns[snapshot_split_column_index].data_type(), "filter out row")
745 }
746 new_bitmap.append(is_in_range);
747 }
748 Some(StreamChunk::with_visibility(
749 ops,
750 columns,
751 new_bitmap.finish(),
752 ))
753}
754
755fn is_leftmost_bound(row: &OwnedRow) -> bool {
756 row.iter().all(|d| d.is_none())
757}
758
759fn is_rightmost_bound(row: &OwnedRow) -> bool {
760 row.iter().all(|d| d.is_none())
761}
762
763impl<S: StateStore> Execute for ParallelizedCdcBackfillExecutor<S> {
764 fn execute(self: Box<Self>) -> BoxedMessageStream {
765 self.execute_inner().boxed()
766 }
767}
768
769fn extends_current_actor_bound(
770 current: &mut Option<(OwnedRow, OwnedRow)>,
771 split: &CdcTableSnapshotSplit,
772) {
773 if current.is_none() {
774 *current = Some((
775 split.left_bound_inclusive.clone(),
776 split.right_bound_exclusive.clone(),
777 ));
778 } else {
779 current.as_mut().unwrap().1 = split.right_bound_exclusive.clone();
780 }
781}
782
783fn is_reset_barrier(barrier: &Barrier, actor_id: ActorId) -> bool {
784 match barrier.mutation.as_deref() {
785 Some(Mutation::Update(update)) => update
786 .actor_cdc_table_snapshot_splits
787 .splits
788 .contains_key(&actor_id),
789 _ => false,
790 }
791}
792
793fn assert_consecutive_splits(actor_snapshot_splits: &[CdcTableSnapshotSplit]) {
794 for i in 1..actor_snapshot_splits.len() {
795 assert_eq!(
796 actor_snapshot_splits[i].split_id,
797 actor_snapshot_splits[i - 1].split_id + 1,
798 "{:?}",
799 actor_snapshot_splits
800 );
801 assert!(
802 cmp_datum(
803 actor_snapshot_splits[i - 1]
804 .right_bound_exclusive
805 .datum_at(0),
806 actor_snapshot_splits[i].right_bound_exclusive.datum_at(0),
807 OrderType::ascending_nulls_last(),
808 )
809 .is_lt()
810 );
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use risingwave_common::array::StreamChunk;
817 use risingwave_common::row::OwnedRow;
818 use risingwave_common::types::ScalarImpl;
819
820 use crate::executor::backfill::cdc::cdc_backill_v2::{
821 filter_stream_chunk, split_finished_and_current_chunk,
822 };
823
824 #[test]
825 fn test_filter_stream_chunk() {
826 use risingwave_common::array::StreamChunkTestExt;
827 let chunk = StreamChunk::from_pretty(
828 " I I
829 + 1 6
830 - 2 .
831 U- 3 7
832 U+ 4 .",
833 );
834 let bound = None;
835 let c = filter_stream_chunk(chunk.clone(), &bound, 0);
836 assert!(c.is_none());
837
838 let bound = Some((OwnedRow::new(vec![None]), OwnedRow::new(vec![None])));
839 let c = filter_stream_chunk(chunk.clone(), &bound, 0);
840 assert_eq!(c.unwrap().compact_vis(), chunk);
841
842 let bound = Some((
843 OwnedRow::new(vec![None]),
844 OwnedRow::new(vec![Some(ScalarImpl::Int64(3))]),
845 ));
846 let c = filter_stream_chunk(chunk.clone(), &bound, 0);
847 assert_eq!(
848 c.unwrap().compact_vis(),
849 StreamChunk::from_pretty(
850 " I I
851 + 1 6
852 - 2 .",
853 )
854 );
855
856 let bound = Some((
857 OwnedRow::new(vec![Some(ScalarImpl::Int64(3))]),
858 OwnedRow::new(vec![None]),
859 ));
860 let c = filter_stream_chunk(chunk.clone(), &bound, 0);
861 assert_eq!(
862 c.unwrap().compact_vis(),
863 StreamChunk::from_pretty(
864 " I I
865 U- 3 7
866 U+ 4 .",
867 )
868 );
869
870 let bound = Some((
871 OwnedRow::new(vec![Some(ScalarImpl::Int64(2))]),
872 OwnedRow::new(vec![Some(ScalarImpl::Int64(4))]),
873 ));
874 let c = filter_stream_chunk(chunk.clone(), &bound, 0);
875 assert_eq!(
876 c.unwrap().compact_vis(),
877 StreamChunk::from_pretty(
878 " I I
879 - 2 .
880 U- 3 7",
881 )
882 );
883
884 let bound = None;
886 let c = filter_stream_chunk(chunk.clone(), &bound, 1);
887 assert!(c.is_none());
888
889 let bound = Some((OwnedRow::new(vec![None]), OwnedRow::new(vec![None])));
890 let c = filter_stream_chunk(chunk.clone(), &bound, 1);
891 assert_eq!(c.unwrap().compact_vis(), chunk);
892
893 let bound = Some((
894 OwnedRow::new(vec![None]),
895 OwnedRow::new(vec![Some(ScalarImpl::Int64(7))]),
896 ));
897 let c = filter_stream_chunk(chunk.clone(), &bound, 1);
898 assert_eq!(
899 c.unwrap().compact_vis(),
900 StreamChunk::from_pretty(
901 " I I
902 + 1 6
903 - 2 .
904 U+ 4 .",
905 )
906 );
907
908 let bound = Some((
909 OwnedRow::new(vec![Some(ScalarImpl::Int64(7))]),
910 OwnedRow::new(vec![None]),
911 ));
912 let c = filter_stream_chunk(chunk, &bound, 1);
913 assert_eq!(
914 c.unwrap().compact_vis(),
915 StreamChunk::from_pretty(
916 " I I
917 U- 3 7",
918 )
919 );
920 }
921
922 #[test]
923 fn test_split_finished_and_current_chunk() {
924 use risingwave_common::array::StreamChunkTestExt;
925
926 let chunk = StreamChunk::from_pretty(
927 " I I
928 + 1 11
929 + 6 10
930 + 199 40",
931 );
932 let finished_split_bounds = Some((
933 OwnedRow::new(vec![Some(ScalarImpl::Int64(1))]),
934 OwnedRow::new(vec![Some(ScalarImpl::Int64(6))]),
935 ));
936 let current_split_bounds = Some((
937 OwnedRow::new(vec![Some(ScalarImpl::Int64(6))]),
938 OwnedRow::new(vec![Some(ScalarImpl::Int64(100))]),
939 ));
940
941 let (finished_chunk, current_chunk) = split_finished_and_current_chunk(
942 chunk,
943 &finished_split_bounds,
944 ¤t_split_bounds,
945 0,
946 );
947
948 assert_eq!(
949 finished_chunk.unwrap(),
950 StreamChunk::from_pretty(
951 " I I
952 + 1 11",
953 )
954 );
955 assert_eq!(
956 current_chunk.unwrap(),
957 StreamChunk::from_pretty(
958 " I I
959 + 6 10",
960 )
961 );
962 }
963}