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::{CdcOffset, ExternalCdcTableType};
28use risingwave_connector::source::{CdcTableSnapshotSplit, CdcTableSnapshotSplitRaw};
29use rw_futures_util::pausable;
30use thiserror_ext::AsReport;
31
32use crate::executor::backfill::cdc::cdc_backfill::{
33 get_cdc_json_parse_handling_from_properties, transform_upstream,
34};
35use crate::executor::backfill::cdc::state_v2::{CdcStateRecord, ParallelizedCdcBackfillState};
36use crate::executor::backfill::cdc::upstream_table::external::ExternalStorageTable;
37use crate::executor::backfill::cdc::upstream_table::reader::{
38 build_reader_and_poll_upstream, create_table_reader_with_retry,
39};
40use crate::executor::backfill::cdc::upstream_table::snapshot::{
41 SplitSnapshotReadArgs, UpstreamTableRead, UpstreamTableReader,
42};
43use crate::executor::backfill::utils::{get_cdc_chunk_last_offset, mapping_chunk, mapping_message};
44use crate::executor::prelude::*;
45use crate::task::ActorId;
46use crate::task::cdc_progress::CdcProgressReporter;
47
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
75enum SnapshotAttemptState {
76 Reading,
77 Failed,
78 Finished(CdcOffset),
79}
80
81impl<S: StateStore> ParallelizedCdcBackfillExecutor<S> {
82 #[expect(clippy::too_many_arguments)]
83 pub fn new(
84 actor_ctx: ActorContextRef,
85 external_table: ExternalStorageTable,
86 upstream: Executor,
87 output_indices: Vec<usize>,
88 output_columns: Vec<ColumnDesc>,
89 _metrics: Arc<StreamingMetrics>,
90 state_table: StateTable<S>,
91 rate_limit_rps: Option<u32>,
92 options: CdcScanOptions,
93 properties: BTreeMap<String, String>,
94 progress: Option<CdcProgressReporter>,
95 ) -> Self {
96 Self {
97 actor_ctx,
98 external_table,
99 upstream,
100 output_indices,
101 output_columns,
102 rate_limit_rps,
103 options,
104 state_table,
105 properties,
106 progress,
107 }
108 }
109
110 #[try_stream(ok = Message, error = StreamExecutorError)]
111 async fn execute_inner(mut self) {
112 assert!(!self.options.disable_backfill);
113 let pk_indices = self.external_table.pk_indices().to_vec();
115 let table_id = self.external_table.table_id();
116 let upstream_table_name = self.external_table.qualified_table_name();
117 let schema_table_name = self.external_table.schema_table_name().clone();
118 let external_database_name = self.external_table.database_name().to_owned();
119 let additional_columns = self
120 .output_columns
121 .iter()
122 .filter(|col| col.additional_column.column_type.is_some())
123 .cloned()
124 .collect_vec();
125 assert!(
126 (self.options.backfill_split_pk_column_index as usize) < pk_indices.len(),
127 "split pk column index {} out of bound",
128 self.options.backfill_split_pk_column_index
129 );
130 let snapshot_split_column_index =
131 pk_indices[self.options.backfill_split_pk_column_index as usize];
132 let snapshot_split_column_in_output_index = self
133 .output_indices
134 .iter()
135 .position(|&idx| idx == snapshot_split_column_index)
136 .expect("snapshot split column must be present in CDC backfill output");
137 let cdc_table_snapshot_split_column =
138 vec![self.external_table.schema().fields[snapshot_split_column_index].clone()];
139
140 let mut upstream = self.upstream.execute();
141 let first_barrier = expect_first_barrier(&mut upstream).await?;
143 let (timestamp_handling, timestamptz_handling, time_handling, bigint_unsigned_handling) =
149 get_cdc_json_parse_handling_from_properties(&self.properties);
150 let handle_toast_columns: bool =
152 self.external_table.table_type() == &ExternalCdcTableType::Postgres;
153 let mut upstream = transform_upstream(
154 upstream,
155 self.output_columns.clone(),
156 timestamp_handling,
157 timestamptz_handling,
158 time_handling,
159 bigint_unsigned_handling,
160 handle_toast_columns,
161 )
162 .boxed();
163 let mut next_reset_barrier = Some(first_barrier);
164 let mut is_reset = false;
165 let mut state_impl = ParallelizedCdcBackfillState::new(self.state_table);
166 let mut upstream_chunk_buffer: Vec<StreamChunk> = vec![];
168
169 'with_cdc_table_snapshot_splits: loop {
171 assert!(upstream_chunk_buffer.is_empty());
172 let reset_barrier = next_reset_barrier.take().unwrap();
173 let all_snapshot_splits = match reset_barrier.mutation.as_deref() {
174 Some(Mutation::Add(add)) => &add.actor_cdc_table_snapshot_splits.splits,
175
176 Some(Mutation::Update(update)) => &update.actor_cdc_table_snapshot_splits.splits,
177 _ => {
178 return Err(anyhow::anyhow!("ParallelizedCdcBackfillExecutor expects either Mutation::Add or Mutation::Update to initialize CDC table snapshot splits.").into());
179 }
180 };
181 let mut actor_snapshot_splits = vec![];
182 let mut generation = None;
183 if let Some((splits, snapshot_generation)) = all_snapshot_splits.get(&self.actor_ctx.id)
185 {
186 actor_snapshot_splits = splits
187 .iter()
188 .map(|s: &CdcTableSnapshotSplitRaw| {
189 let de = RowDeserializer::new(
190 cdc_table_snapshot_split_column
191 .iter()
192 .map(Field::data_type)
193 .collect_vec(),
194 );
195 let left_bound_inclusive =
196 de.deserialize(s.left_bound_inclusive.as_ref()).unwrap();
197 let right_bound_exclusive =
198 de.deserialize(s.right_bound_exclusive.as_ref()).unwrap();
199 CdcTableSnapshotSplit {
200 split_id: s.split_id,
201 left_bound_inclusive,
202 right_bound_exclusive,
203 }
204 })
205 .collect();
206 generation = Some(*snapshot_generation);
207 }
208 tracing::debug!(?actor_snapshot_splits, ?generation, "actor splits");
209 assert_consecutive_splits(&actor_snapshot_splits);
210
211 let mut is_snapshot_paused = reset_barrier.is_pause_on_startup();
212 let barrier_epoch = reset_barrier.epoch;
213 yield Message::Barrier(reset_barrier);
214 if !is_reset {
215 state_impl.init_epoch(barrier_epoch).await?;
216 is_reset = true;
217 tracing::info!(%table_id, "Initialize executor.");
218 } else {
219 tracing::info!(%table_id, "Reset executor.");
220 }
221
222 let mut current_actor_bounds = None;
223 let mut actor_cdc_offset_high: Option<CdcOffset> = None;
224 let mut actor_cdc_offset_low: Option<CdcOffset> = None;
225 let mut split_states = Vec::with_capacity(actor_snapshot_splits.len());
226
227 for split in &actor_snapshot_splits {
228 let state = state_impl.restore_state(split.split_id).await?;
229 extends_current_actor_bound(&mut current_actor_bounds, split);
230
231 if state.is_finished {
232 if let Some(cdc_offset) = state.cdc_offset_low.as_ref()
233 && actor_cdc_offset_low
234 .as_ref()
235 .is_none_or(|current| current > cdc_offset)
236 {
237 actor_cdc_offset_low = Some(cdc_offset.clone());
238 }
239
240 if let Some(cdc_offset) = state.cdc_offset_high.as_ref()
241 && actor_cdc_offset_high
242 .as_ref()
243 .is_none_or(|current| current < cdc_offset)
244 {
245 actor_cdc_offset_high = Some(cdc_offset.clone());
246 }
247 }
248
249 split_states.push(state);
250 }
251
252 let next_split_idx = split_states.iter().position(|state| !state.is_finished);
253 let finished_prefix_len = next_split_idx.unwrap_or(actor_snapshot_splits.len());
254 let mut should_report_actor_backfill_progress = (finished_prefix_len > 0).then(|| {
255 (
256 actor_snapshot_splits[0].split_id,
257 actor_snapshot_splits[finished_prefix_len - 1].split_id,
258 )
259 });
260
261 for split in &actor_snapshot_splits {
262 state_impl.init_state_if_absent(split.split_id).await?;
264 }
265
266 let offset_parse_func = self.external_table.table_type().get_cdc_offset_parser()?;
267
268 if let Some(next_split_idx) = next_split_idx {
272 let external_table = self.external_table.clone();
273 let actor_id = self.actor_ctx.id;
274 let fragment_id = self.actor_ctx.fragment_id;
275 let mut future = Box::pin(create_table_reader_with_retry(
276 external_table,
277 actor_id,
278 fragment_id,
279 ));
280
281 let table_reader = loop {
282 match build_reader_and_poll_upstream(&mut upstream, &mut future).await? {
283 Either::Left(msg) => match msg {
284 Message::Barrier(barrier) => {
285 for chunk in &upstream_chunk_buffer {
286 yield Message::Chunk(chunk.clone());
287 }
288
289 state_impl.commit_state(barrier.epoch).await?;
290
291 if let Some(mutation) = barrier.mutation.as_deref() {
292 match mutation {
293 Mutation::Pause => {
294 is_snapshot_paused = true;
295 }
296 Mutation::Resume => {
297 is_snapshot_paused = false;
298 }
299 Mutation::Throttle(_) => {
300 if let Some(entry) = mutation.backfill_throttle_config(
301 self.actor_ctx.fragment_id,
302 ) {
303 self.rate_limit_rps = entry.rate_limit;
304 }
305 }
306 _ => (),
307 }
308 }
309
310 if is_reset_barrier(&barrier, self.actor_ctx.id) {
311 upstream_chunk_buffer.clear();
312 next_reset_barrier = Some(barrier);
313 continue 'with_cdc_table_snapshot_splits;
314 }
315 yield Message::Barrier(barrier);
316 }
317 Message::Chunk(chunk) => {
318 if !chunk.has_visible_rows() {
319 continue;
320 }
321
322 let chunk = mapping_chunk(chunk, &self.output_indices);
323 let (forwarded_chunk, buffered_chunk) = route_cdc_chunk(
324 chunk,
325 &actor_snapshot_splits,
326 &split_states,
327 next_split_idx,
328 snapshot_split_column_in_output_index,
329 );
330
331 if let Some(forwarded_chunk) = forwarded_chunk {
332 yield Message::Chunk(forwarded_chunk);
333 }
334
335 if let Some(buffered_chunk) = buffered_chunk {
336 upstream_chunk_buffer.push(buffered_chunk);
337 }
338 }
339 Message::Watermark(_) => {
340 }
342 },
343 Either::Right(table_reader) => break table_reader,
344 }
345 };
346 tracing::info!(
347 %table_id,
348 upstream_table_name,
349 "table reader created successfully"
350 );
351
352 let mut upstream_table_reader =
353 UpstreamTableReader::new(self.external_table.clone(), table_reader);
354
355 for (split_idx, (split, restored_state)) in actor_snapshot_splits
357 .iter()
358 .zip_eq_fast(split_states.iter())
359 .enumerate()
360 .skip(next_split_idx)
361 {
362 if restored_state.is_finished {
363 extend_backfill_progress(
364 &mut should_report_actor_backfill_progress,
365 split.split_id,
366 );
367
368 continue;
369 }
370 tracing::info!(
371 %table_id,
372 upstream_table_name,
373 ?split,
374 is_snapshot_paused,
375 "start cdc backfill split"
376 );
377 let mut durable_row_count = restored_state.row_count as u64;
378 let mut split_cdc_offset_low = restored_state.cdc_offset_low.clone();
379
380 let split_cdc_offset_high = 'backfill_loop: loop {
381 let mut attempt_row_count = 0_u64;
384
385 if split_cdc_offset_low.is_none() {
386 static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
387 tokio::sync::Semaphore::const_new(10);
388 let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
389 split_cdc_offset_low =
390 upstream_table_reader.current_cdc_offset().await?;
391 }
392
393 if let Some(ref cdc_offset) = split_cdc_offset_low
394 && actor_cdc_offset_low
395 .as_ref()
396 .is_none_or(|cur| cur > cdc_offset)
397 {
398 actor_cdc_offset_low = split_cdc_offset_low.clone();
399 }
400
401 let attempt_state = {
402 let left_upstream = upstream.by_ref().map(Either::Left);
403 let read_args = SplitSnapshotReadArgs::new(
404 split.left_bound_inclusive.clone(),
405 split.right_bound_exclusive.clone(),
406 cdc_table_snapshot_split_column.clone(),
407 self.rate_limit_rps,
408 additional_columns.clone(),
409 schema_table_name.clone(),
410 external_database_name.clone(),
411 );
412 let right_snapshot = pin!(
413 upstream_table_reader
414 .snapshot_read_table_split(read_args)
415 .map(Either::Right)
416 );
417 let (right_snapshot, snapshot_valve) = pausable(right_snapshot);
418
419 if is_snapshot_paused {
420 snapshot_valve.pause();
421 }
422
423 let mut backfill_stream = select_with_strategy(
424 left_upstream,
425 right_snapshot,
426 |_: &mut ()| stream::PollNext::Left,
427 );
428 let mut attempt_state = SnapshotAttemptState::Reading;
429
430 #[for_await]
431 'backfill_stream: for either in &mut backfill_stream {
432 match either {
433 Either::Left(upstream_message) => match upstream_message? {
434 Message::Barrier(barrier) => {
435 for chunk in &upstream_chunk_buffer {
439 yield Message::Chunk(chunk.clone());
440 }
441
442 durable_row_count =
443 durable_row_count.max(attempt_row_count);
444
445 state_impl
446 .mutate_state(
447 split.split_id,
448 false,
449 durable_row_count,
450 split_cdc_offset_low.clone(),
451 None,
452 )
453 .await?;
454 state_impl.commit_state(barrier.epoch).await?;
455
456 if let Some(mutation) = barrier.mutation.as_deref() {
457 use crate::executor::Mutation;
458 match mutation {
459 Mutation::Pause => {
460 is_snapshot_paused = true;
461 snapshot_valve.pause();
462 }
463 Mutation::Resume => {
464 is_snapshot_paused = false;
465 snapshot_valve.resume();
466 }
467 Mutation::Throttle(_) => {
468 if let Some(entry) = mutation
472 .backfill_throttle_config(
473 self.actor_ctx.fragment_id,
474 )
475 {
476 self.rate_limit_rps = entry.rate_limit;
478 }
479 }
480 mutation
481 if mutation.is_stop(self.actor_ctx.id) =>
482 {
483 tracing::info!(
484 %table_id,
485 upstream_table_name,
486 "CdcBackfill has been dropped due to config change"
487 );
488
489 yield Message::Barrier(barrier);
490
491 let () = futures::future::pending().await;
492 unreachable!();
493 }
494 _ => (),
495 }
496 }
497
498 if is_reset_barrier(&barrier, self.actor_ctx.id) {
499 upstream_chunk_buffer.clear();
502 next_reset_barrier = Some(barrier);
503
504 continue 'with_cdc_table_snapshot_splits;
506 }
507
508 if let (Some(split_range), Some(progress)) = (
509 should_report_actor_backfill_progress.take(),
510 self.progress.as_ref(),
511 ) {
512 progress.update(
513 self.actor_ctx.fragment_id,
514 self.actor_ctx.id,
515 barrier.epoch,
516 generation.expect("should have set generation when having progress to report"),
517 split_range,
518 );
519 }
520
521 yield Message::Barrier(barrier);
522
523 if matches!(attempt_state, SnapshotAttemptState::Failed)
524 {
525 break 'backfill_stream;
526 }
527 }
528 Message::Chunk(chunk) => {
529 if !chunk.has_visible_rows() {
530 continue 'backfill_stream;
531 }
532
533 let chunk = mapping_chunk(chunk, &self.output_indices);
547 let (forwarded_chunk, buffered_chunk) = route_cdc_chunk(
548 chunk,
549 &actor_snapshot_splits,
550 &split_states,
551 split_idx,
552 snapshot_split_column_in_output_index,
553 );
554
555 if let Some(forwarded_chunk) = forwarded_chunk {
556 yield Message::Chunk(forwarded_chunk);
557 }
558
559 if let Some(buffered_chunk) = buffered_chunk {
560 upstream_chunk_buffer.push(buffered_chunk);
561 }
562 }
563 Message::Watermark(_) => {
564 }
566 },
567 Either::Right(snapshot) => {
568 if matches!(attempt_state, SnapshotAttemptState::Failed) {
570 continue 'backfill_stream;
571 }
572
573 match snapshot {
574 Ok(None) => {
575 tracing::info!(
576 %table_id,
577 split_id = split.split_id,
578 "snapshot read stream ends"
579 );
580
581 for chunk in upstream_chunk_buffer.drain(..) {
582 yield Message::Chunk(chunk);
583 }
584
585 durable_row_count = attempt_row_count;
588
589 static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
591 tokio::sync::Semaphore::const_new(10);
592 let _permit =
593 CDC_CONN_SEMAPHORE.acquire().await.unwrap();
594 let high = upstream_table_reader
595 .current_cdc_offset()
596 .await?
597 .expect(
598 "CDC offset must be available after snapshot completion",
599 );
600 attempt_state =
601 SnapshotAttemptState::Finished(high);
602
603 break 'backfill_stream;
604 }
605 Ok(Some(chunk)) => {
606 attempt_row_count = attempt_row_count
607 .saturating_add(chunk.cardinality() as u64);
608
609 yield Message::Chunk(mapping_chunk(
610 chunk,
611 &self.output_indices,
612 ));
613 }
614 Err(error) => {
615 attempt_state = SnapshotAttemptState::Failed;
616 tracing::warn!(
617 error = %error.as_report(),
618 %table_id,
619 upstream_table_name,
620 "failed to read CDC snapshot; rebuilding reader after a barrier"
621 );
622 }
623 }
624 }
625 }
626 }
627 attempt_state
628 };
629
630 match attempt_state {
631 SnapshotAttemptState::Finished(split_cdc_offset_high) => {
632 break 'backfill_loop split_cdc_offset_high;
633 }
634 SnapshotAttemptState::Failed => {
635 if let Err(error) = upstream_table_reader.disconnect().await {
636 tracing::warn!(
637 error = %error.as_report(),
638 %table_id,
639 upstream_table_name,
640 "failed to disconnect CDC snapshot reader; continuing with rebuild"
641 );
642 }
643
644 let mut future = Box::pin(create_table_reader_with_retry(
645 self.external_table.clone(),
646 self.actor_ctx.id,
647 self.actor_ctx.fragment_id,
648 ));
649 let table_reader = loop {
650 match build_reader_and_poll_upstream(&mut upstream, &mut future)
651 .await?
652 {
653 Either::Left(msg) => match msg {
654 Message::Barrier(barrier) => {
655 for chunk in &upstream_chunk_buffer {
656 yield Message::Chunk(chunk.clone());
657 }
658
659 state_impl.commit_state(barrier.epoch).await?;
660
661 if let Some(mutation) = barrier.mutation.as_deref()
662 {
663 use crate::executor::Mutation;
664 match mutation {
665 Mutation::Pause => {
666 is_snapshot_paused = true;
667 }
668 Mutation::Resume => {
669 is_snapshot_paused = false;
670 }
671 Mutation::Throttle(_) => {
672 if let Some(entry) = mutation
673 .backfill_throttle_config(
674 self.actor_ctx.fragment_id,
675 )
676 {
677 self.rate_limit_rps =
678 entry.rate_limit;
679 }
680 }
681 mutation
682 if mutation
683 .is_stop(self.actor_ctx.id) =>
684 {
685 yield Message::Barrier(barrier);
686 let () =
687 futures::future::pending().await;
688 unreachable!();
689 }
690 _ => (),
691 }
692 }
693
694 if is_reset_barrier(&barrier, self.actor_ctx.id) {
695 upstream_chunk_buffer.clear();
696 next_reset_barrier = Some(barrier);
697
698 continue 'with_cdc_table_snapshot_splits;
699 }
700
701 yield Message::Barrier(barrier);
702 }
703 Message::Chunk(chunk) => {
704 let chunk =
705 mapping_chunk(chunk, &self.output_indices);
706 let (forwarded_chunk, buffered_chunk) =
707 route_cdc_chunk(
708 chunk,
709 &actor_snapshot_splits,
710 &split_states,
711 split_idx,
712 snapshot_split_column_in_output_index,
713 );
714
715 if let Some(forwarded_chunk) = forwarded_chunk {
716 yield Message::Chunk(forwarded_chunk);
717 }
718
719 if let Some(buffered_chunk) = buffered_chunk {
720 upstream_chunk_buffer.push(buffered_chunk);
721 }
722 }
723 Message::Watermark(_) => {}
724 },
725 Either::Right(table_reader) => {
726 break table_reader;
727 }
728 }
729 };
730
731 upstream_table_reader = UpstreamTableReader::new(
732 self.external_table.clone(),
733 table_reader,
734 );
735
736 tracing::info!(
737 %table_id,
738 upstream_table_name,
739 "CDC table reader rebuilt successfully"
740 );
741
742 continue 'backfill_loop;
743 }
744 SnapshotAttemptState::Reading => {
745 unreachable!(
746 "backfill stream must not end while the snapshot attempt is still reading"
747 );
748 }
749 }
750 };
751
752 if actor_cdc_offset_high
753 .as_ref()
754 .is_none_or(|cur| cur < &split_cdc_offset_high)
755 {
756 actor_cdc_offset_high = Some(split_cdc_offset_high.clone());
757 }
758
759 state_impl
761 .mutate_state(
762 split.split_id,
763 true,
764 durable_row_count,
765 split_cdc_offset_low.clone(),
766 Some(split_cdc_offset_high),
767 )
768 .await?;
769
770 extend_backfill_progress(
771 &mut should_report_actor_backfill_progress,
772 split.split_id,
773 );
774 }
775
776 upstream_table_reader.disconnect().await?;
777 }
778 tracing::info!(
779 %table_id,
780 upstream_table_name,
781 "CdcBackfill has already finished and will forward messages directly to the downstream"
782 );
783
784 let mut should_report_actor_backfill_done = false;
785 #[for_await]
789 for msg in &mut upstream {
790 let msg = msg?;
791 match msg {
792 Message::Barrier(barrier) => {
793 state_impl.commit_state(barrier.epoch).await?;
794 if is_reset_barrier(&barrier, self.actor_ctx.id) {
795 next_reset_barrier = Some(barrier);
796 continue 'with_cdc_table_snapshot_splits;
797 }
798 if let Some(split_range) = should_report_actor_backfill_progress.take()
799 && let Some(ref progress) = self.progress
800 {
801 progress.update(
802 self.actor_ctx.fragment_id,
803 self.actor_ctx.id,
804 barrier.epoch,
805 generation.expect(
806 "should have set generation when having progress to report",
807 ),
808 split_range,
809 );
810 }
811 if should_report_actor_backfill_done {
812 should_report_actor_backfill_done = false;
813 assert!(!actor_snapshot_splits.is_empty());
814 if let Some(ref progress) = self.progress {
815 progress.finish(
816 self.actor_ctx.fragment_id,
817 self.actor_ctx.id,
818 barrier.epoch,
819 generation.expect(
820 "should have set generation when having progress to report",
821 ),
822 (
823 actor_snapshot_splits[0].split_id,
824 actor_snapshot_splits[actor_snapshot_splits.len() - 1]
825 .split_id,
826 ),
827 );
828 }
829 }
830 yield Message::Barrier(barrier);
831 }
832 Message::Chunk(chunk) => {
833 if actor_snapshot_splits.is_empty() || !chunk.has_visible_rows() {
834 continue;
835 }
836
837 let chunk_cdc_offset =
838 get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
839 if let Some(high) = actor_cdc_offset_high.as_ref() {
850 if state_impl.is_legacy_state() {
851 actor_cdc_offset_high = None;
853 should_report_actor_backfill_done = true;
854 } else if let Some(ref chunk_offset) = chunk_cdc_offset
855 && *chunk_offset >= *high
856 {
857 actor_cdc_offset_high = None;
859 should_report_actor_backfill_done = true;
860 }
861 }
862 let chunk = mapping_chunk(chunk, &self.output_indices);
863 if let Some(filtered_chunk) = filter_stream_chunk(
864 chunk,
865 ¤t_actor_bounds,
866 snapshot_split_column_in_output_index,
867 ) {
868 yield Message::Chunk(filtered_chunk);
869 }
870 }
871 msg @ Message::Watermark(_) => {
872 if let Some(msg) = mapping_message(msg, &self.output_indices) {
873 yield msg;
874 }
875 }
876 }
877 }
878 }
879 }
880}
881
882fn route_cdc_chunk(
893 chunk: StreamChunk,
894 splits: &[CdcTableSnapshotSplit],
895 states: &[CdcStateRecord],
896 current_split_idx: usize,
897 snapshot_split_column_index: usize,
898) -> (Option<StreamChunk>, Option<StreamChunk>) {
899 let mut forwarded = BitmapBuilder::zeroed(chunk.capacity());
900 let mut buffered = BitmapBuilder::zeroed(chunk.capacity());
901
902 for (_, row) in chunk.rows() {
903 let split_key = row.datum_at(snapshot_split_column_index);
904
905 let Ok(split_idx) = splits.binary_search_by(|split| {
906 let right_bound_le_key = !is_rightmost_bound(&split.right_bound_exclusive)
907 && cmp_datum(
908 split.right_bound_exclusive.datum_at(0),
909 split_key,
910 OrderType::ascending_nulls_first(),
911 )
912 .is_le();
913
914 let left_bound_gt_key = !is_leftmost_bound(&split.left_bound_inclusive)
915 && cmp_datum(
916 split.left_bound_inclusive.datum_at(0),
917 split_key,
918 OrderType::ascending_nulls_first(),
919 )
920 .is_gt();
921
922 if right_bound_le_key {
923 std::cmp::Ordering::Less
925 } else if left_bound_gt_key {
926 std::cmp::Ordering::Greater
928 } else {
929 std::cmp::Ordering::Equal
931 }
932 }) else {
933 continue;
935 };
936
937 if split_idx == current_split_idx {
939 buffered.set(row.index(), true);
940 continue;
941 }
942
943 let state = &states[split_idx];
944 let is_completed = split_idx < current_split_idx || state.is_finished;
948 let is_partially_completed = state.row_count > 0;
949 if is_completed || is_partially_completed {
950 forwarded.set(row.index(), true);
951 }
952
953 }
956
957 let forwarded = forwarded.finish();
958 let forwarded_chunk = forwarded
959 .any()
960 .then(|| chunk.clone_with_vis(forwarded).compact_vis());
961
962 let buffered = buffered.finish();
963 let buffered_chunk = buffered
964 .any()
965 .then(|| chunk.clone_with_vis(buffered).compact_vis());
966
967 (forwarded_chunk, buffered_chunk)
968}
969
970fn extend_backfill_progress(progress: &mut Option<(i64, i64)>, split_id: i64) {
971 if let Some((_, right)) = progress {
972 assert!(*right < split_id);
973 *right = split_id;
974 } else {
975 *progress = Some((split_id, split_id));
976 }
977}
978
979fn filter_stream_chunk(
986 chunk: StreamChunk,
987 bound: &Option<(OwnedRow, OwnedRow)>,
988 snapshot_split_column_index: usize,
989) -> Option<StreamChunk> {
990 let Some((left, right)) = bound else {
991 return None;
992 };
993 assert_eq!(left.len(), 1, "multiple split columns is not supported yet");
994 assert_eq!(
995 right.len(),
996 1,
997 "multiple split columns is not supported yet"
998 );
999 let left_split_key = left.datum_at(0);
1000 let right_split_key = right.datum_at(0);
1001 let is_leftmost_bound = is_leftmost_bound(left);
1002 let is_rightmost_bound = is_rightmost_bound(right);
1003 if is_leftmost_bound && is_rightmost_bound {
1004 return chunk.has_visible_rows().then_some(chunk);
1005 }
1006 let mut new_bitmap = BitmapBuilder::with_capacity(chunk.capacity());
1007 let (ops, columns, visibility) = chunk.into_inner();
1008 for (row_split_key, v) in columns[snapshot_split_column_index]
1009 .iter()
1010 .zip_eq_fast(visibility.iter())
1011 {
1012 if !v {
1013 new_bitmap.append(false);
1014 continue;
1015 }
1016 let mut is_in_range = true;
1017 if !is_leftmost_bound {
1018 is_in_range = cmp_datum(
1019 row_split_key,
1020 left_split_key,
1021 OrderType::ascending_nulls_first(),
1022 )
1023 .is_ge();
1024 }
1025 if is_in_range && !is_rightmost_bound {
1026 is_in_range = cmp_datum(
1027 row_split_key,
1028 right_split_key,
1029 OrderType::ascending_nulls_first(),
1030 )
1031 .is_lt();
1032 }
1033 if !is_in_range {
1034 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")
1035 }
1036 new_bitmap.append(is_in_range);
1037 }
1038
1039 let visibility = new_bitmap.finish();
1040
1041 visibility
1042 .any()
1043 .then_some(StreamChunk::with_visibility(ops, columns, visibility))
1044}
1045
1046fn is_leftmost_bound(row: &OwnedRow) -> bool {
1048 row.iter().all(|d| d.is_none())
1049}
1050
1051fn is_rightmost_bound(row: &OwnedRow) -> bool {
1053 row.iter().all(|d| d.is_none())
1054}
1055
1056impl<S: StateStore> Execute for ParallelizedCdcBackfillExecutor<S> {
1057 fn execute(self: Box<Self>) -> BoxedMessageStream {
1058 self.execute_inner().boxed()
1059 }
1060}
1061
1062fn extends_current_actor_bound(
1063 current: &mut Option<(OwnedRow, OwnedRow)>,
1064 split: &CdcTableSnapshotSplit,
1065) {
1066 if current.is_none() {
1067 *current = Some((
1068 split.left_bound_inclusive.clone(),
1069 split.right_bound_exclusive.clone(),
1070 ));
1071 } else {
1072 current.as_mut().unwrap().1 = split.right_bound_exclusive.clone();
1073 }
1074}
1075
1076fn is_reset_barrier(barrier: &Barrier, actor_id: ActorId) -> bool {
1077 match barrier.mutation.as_deref() {
1078 Some(Mutation::Update(update)) => update
1079 .actor_cdc_table_snapshot_splits
1080 .splits
1081 .contains_key(&actor_id),
1082 _ => false,
1083 }
1084}
1085
1086fn assert_consecutive_splits(actor_snapshot_splits: &[CdcTableSnapshotSplit]) {
1087 for i in 1..actor_snapshot_splits.len() {
1088 assert_eq!(
1089 actor_snapshot_splits[i].split_id,
1090 actor_snapshot_splits[i - 1].split_id + 1,
1091 "{:?}",
1092 actor_snapshot_splits
1093 );
1094 assert!(
1095 cmp_datum(
1096 actor_snapshot_splits[i - 1]
1097 .right_bound_exclusive
1098 .datum_at(0),
1099 actor_snapshot_splits[i].right_bound_exclusive.datum_at(0),
1100 OrderType::ascending_nulls_last(),
1101 )
1102 .is_lt()
1103 );
1104 }
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109 use std::collections::BTreeMap;
1110 use std::sync::Arc;
1111 use std::time::Duration;
1112
1113 use futures::StreamExt;
1114 use risingwave_common::array::StreamChunk;
1115 use risingwave_common::catalog::{
1116 CdcKeyComparison, ColumnDesc, ColumnId, Field, Schema, TableId,
1117 };
1118 use risingwave_common::row::{OwnedRow, Row};
1119 use risingwave_common::types::{DataType, ScalarImpl};
1120 use risingwave_common::util::epoch::{EpochExt, test_epoch};
1121 use risingwave_common::util::sort_util::OrderType;
1122 use risingwave_connector::source::CdcTableSnapshotSplitRaw;
1123 use risingwave_connector::source::cdc::external::{
1124 ExternalCdcTableType, ExternalTableConfig, SchemaTableName,
1125 };
1126 use risingwave_connector::source::cdc::{
1127 CdcScanOptions, CdcTableSnapshotSplitAssignmentWithGeneration,
1128 };
1129 use risingwave_storage::memory::MemoryStateStore;
1130
1131 use super::*;
1132 use crate::common::table::state_table::StateTable;
1133 use crate::common::table::test_utils::gen_pbtable;
1134 use crate::executor::monitor::StreamingMetrics;
1135 use crate::executor::test_utils::MockSource;
1136 use crate::executor::{AddMutation, Execute, Mutation};
1137
1138 async fn create_parallel_cdc_state_table(
1139 store: MemoryStateStore,
1140 ) -> StateTable<MemoryStateStore> {
1141 let state_schema = Schema::new(vec![
1142 Field::with_name(DataType::Int64, "split_id"),
1143 Field::with_name(DataType::Boolean, "backfill_finished"),
1144 Field::with_name(DataType::Int64, "row_count"),
1145 Field::with_name(DataType::Jsonb, "cdc_offset_low"),
1146 Field::with_name(DataType::Jsonb, "cdc_offset_high"),
1147 ]);
1148 let column_descs = state_schema
1149 .fields
1150 .iter()
1151 .enumerate()
1152 .map(|(idx, field)| {
1153 ColumnDesc::unnamed(ColumnId::new(idx as i32), field.data_type.clone())
1154 })
1155 .collect();
1156
1157 StateTable::from_table_catalog(
1158 &gen_pbtable(
1159 TableId::new(0x42),
1160 column_descs,
1161 vec![OrderType::ascending()],
1162 vec![0],
1163 0,
1164 ),
1165 store,
1166 None,
1167 )
1168 .await
1169 }
1170
1171 #[tokio::test(start_paused = true)]
1172 async fn test_rebuilds_reader_after_snapshot_error() {
1173 let (mut tx, source) = MockSource::channel();
1174
1175 let source = source.into_executor(
1176 Schema::new(vec![
1177 Field::unnamed(DataType::Jsonb),
1178 Field::unnamed(DataType::Varchar),
1179 ]),
1180 vec![0],
1181 );
1182 let external_table = ExternalStorageTable::new(
1183 TableId::new(1234),
1184 SchemaTableName {
1185 schema_name: "public".to_owned(),
1186 table_name: "mock_table".to_owned(),
1187 },
1188 "mydb".to_owned(),
1189 ExternalTableConfig::default(),
1190 ExternalCdcTableType::Mock,
1191 Schema::new(vec![
1192 Field::with_name(DataType::Int64, "id"),
1193 Field::with_name(DataType::Float64, "price"),
1194 ]),
1195 vec![OrderType::ascending()],
1196 Some(vec![CdcKeyComparison::Native]),
1197 vec![0],
1198 )
1199 .with_mock_snapshot_errors([1, 0]);
1200 let external_table_for_assertion = external_table.clone();
1201
1202 let state_table = create_parallel_cdc_state_table(MemoryStateStore::new()).await;
1203
1204 let actor_id = 0x1a.into();
1205 let mut executor = ParallelizedCdcBackfillExecutor::new(
1206 ActorContext::for_test(actor_id),
1207 external_table,
1208 source,
1209 vec![0, 1],
1210 vec![
1211 ColumnDesc::named("id", ColumnId::new(1), DataType::Int64),
1212 ColumnDesc::named("price", ColumnId::new(2), DataType::Float64),
1213 ],
1214 Arc::new(StreamingMetrics::unused()),
1215 state_table,
1216 Some(4),
1217 CdcScanOptions {
1218 backfill_parallelism: 1,
1219 backfill_num_rows_per_split: 100,
1220 ..Default::default()
1221 },
1222 BTreeMap::new(),
1223 None,
1224 )
1225 .boxed()
1226 .execute();
1227
1228 let mut curr_epoch = test_epoch(1);
1229 let snapshot_splits = [(
1230 actor_id,
1231 (
1232 vec![CdcTableSnapshotSplitRaw {
1233 split_id: 1,
1234 left_bound_inclusive: OwnedRow::new(vec![Some(ScalarImpl::Int64(1))])
1235 .value_serialize(),
1236 right_bound_exclusive: OwnedRow::new(vec![Some(ScalarImpl::Int64(10))])
1237 .value_serialize(),
1238 }],
1239 10,
1240 ),
1241 )]
1242 .into_iter()
1243 .collect();
1244
1245 tx.send_barrier(
1247 Barrier::new_test_barrier(curr_epoch).with_mutation(Mutation::Add(AddMutation {
1248 actor_cdc_table_snapshot_splits: CdcTableSnapshotSplitAssignmentWithGeneration {
1249 splits: snapshot_splits,
1250 },
1251 ..Default::default()
1252 })),
1253 );
1254
1255 assert!(matches!(
1256 executor.next().await.unwrap().unwrap(),
1257 Message::Barrier(Barrier { epoch, .. }) if epoch.curr == curr_epoch
1258 ));
1259
1260 assert!(
1264 tokio::time::timeout(Duration::from_millis(50), executor.next())
1265 .await
1266 .is_err()
1267 );
1268 assert_eq!(external_table_for_assertion.mock_reader_create_count(), 1);
1269
1270 curr_epoch.inc_epoch();
1271 tx.push_barrier(curr_epoch, false);
1272 assert!(matches!(
1273 executor.next().await.unwrap().unwrap(),
1274 Message::Barrier(Barrier { epoch, .. }) if epoch.curr == curr_epoch
1275 ));
1276
1277 assert!(matches!(
1279 executor.next().await.unwrap().unwrap(),
1280 Message::Chunk(_)
1281 ));
1282
1283 assert_eq!(external_table_for_assertion.mock_reader_create_count(), 2);
1284 }
1285
1286 #[test]
1287 fn test_filter_stream_chunk() {
1288 use risingwave_common::array::StreamChunkTestExt;
1289 let chunk = StreamChunk::from_pretty(
1290 " I I
1291 + 1 6
1292 - 2 .
1293 U- 3 7
1294 U+ 4 .",
1295 );
1296 let bound = None;
1297 let c = filter_stream_chunk(chunk.clone(), &bound, 0);
1298 assert!(c.is_none());
1299
1300 let bound = Some((OwnedRow::new(vec![None]), OwnedRow::new(vec![None])));
1301 let c = filter_stream_chunk(chunk.clone(), &bound, 0);
1302 assert_eq!(c.unwrap().compact_vis(), chunk);
1303
1304 let bound = Some((
1305 OwnedRow::new(vec![None]),
1306 OwnedRow::new(vec![Some(ScalarImpl::Int64(3))]),
1307 ));
1308 let c = filter_stream_chunk(chunk.clone(), &bound, 0);
1309 assert_eq!(
1310 c.unwrap().compact_vis(),
1311 StreamChunk::from_pretty(
1312 " I I
1313 + 1 6
1314 - 2 .",
1315 )
1316 );
1317
1318 let bound = Some((
1319 OwnedRow::new(vec![Some(ScalarImpl::Int64(3))]),
1320 OwnedRow::new(vec![None]),
1321 ));
1322 let c = filter_stream_chunk(chunk.clone(), &bound, 0);
1323 assert_eq!(
1324 c.unwrap().compact_vis(),
1325 StreamChunk::from_pretty(
1326 " I I
1327 U- 3 7
1328 U+ 4 .",
1329 )
1330 );
1331
1332 let bound = Some((
1333 OwnedRow::new(vec![Some(ScalarImpl::Int64(2))]),
1334 OwnedRow::new(vec![Some(ScalarImpl::Int64(4))]),
1335 ));
1336 let c = filter_stream_chunk(chunk.clone(), &bound, 0);
1337 assert_eq!(
1338 c.unwrap().compact_vis(),
1339 StreamChunk::from_pretty(
1340 " I I
1341 - 2 .
1342 U- 3 7",
1343 )
1344 );
1345
1346 let bound = None;
1348 let c = filter_stream_chunk(chunk.clone(), &bound, 1);
1349 assert!(c.is_none());
1350
1351 let bound = Some((OwnedRow::new(vec![None]), OwnedRow::new(vec![None])));
1352 let c = filter_stream_chunk(chunk.clone(), &bound, 1);
1353 assert_eq!(c.unwrap().compact_vis(), chunk);
1354
1355 let bound = Some((
1356 OwnedRow::new(vec![None]),
1357 OwnedRow::new(vec![Some(ScalarImpl::Int64(7))]),
1358 ));
1359 let c = filter_stream_chunk(chunk.clone(), &bound, 1);
1360 assert_eq!(
1361 c.unwrap().compact_vis(),
1362 StreamChunk::from_pretty(
1363 " I I
1364 + 1 6
1365 - 2 .
1366 U+ 4 .",
1367 )
1368 );
1369
1370 let bound = Some((
1371 OwnedRow::new(vec![Some(ScalarImpl::Int64(7))]),
1372 OwnedRow::new(vec![None]),
1373 ));
1374 let c = filter_stream_chunk(chunk, &bound, 1);
1375 assert_eq!(
1376 c.unwrap().compact_vis(),
1377 StreamChunk::from_pretty(
1378 " I I
1379 U- 3 7",
1380 )
1381 );
1382 }
1383
1384 #[test]
1385 fn test_route_cdc_chunk() {
1386 use risingwave_common::array::StreamChunkTestExt;
1387
1388 let bound = |value: i64| OwnedRow::new(vec![Some(value.into())]);
1389 let splits = [(0, 100), (100, 200), (200, 300), (300, 400), (400, 500)]
1390 .into_iter()
1391 .enumerate()
1392 .map(|(idx, (left, right))| CdcTableSnapshotSplit {
1393 split_id: idx as i64,
1394 left_bound_inclusive: bound(left),
1395 right_bound_exclusive: bound(right),
1396 })
1397 .collect_vec();
1398
1399 let states = [
1401 CdcStateRecord {
1402 is_finished: true,
1403 ..Default::default()
1404 },
1405 CdcStateRecord {
1406 row_count: 1,
1407 ..Default::default()
1408 },
1409 CdcStateRecord {
1410 is_finished: true,
1411 ..Default::default()
1412 },
1413 CdcStateRecord {
1414 row_count: 1,
1415 ..Default::default()
1416 },
1417 CdcStateRecord::default(),
1418 ];
1419
1420 let chunk = StreamChunk::from_pretty(
1433 " I I
1434 + -1 1
1435 + 50 5
1436 + 150 15
1437 - 151 16
1438 - 250 25
1439 - 350 35
1440 + 450 45
1441 + 500 50",
1442 );
1443 let (forwarded_chunk, buffered_chunk) = route_cdc_chunk(chunk, &splits, &states, 1, 0);
1444
1445 assert_eq!(
1446 forwarded_chunk.unwrap(),
1447 StreamChunk::from_pretty(
1448 " I I
1449 + 50 5
1450 - 250 25
1451 - 350 35",
1452 )
1453 );
1454
1455 assert_eq!(
1456 buffered_chunk.unwrap(),
1457 StreamChunk::from_pretty(
1458 " I I
1459 + 150 15
1460 - 151 16",
1461 )
1462 );
1463 }
1464}