1use std::collections::BTreeMap;
16use std::pin::Pin;
17
18use either::Either;
19use futures::stream;
20use futures::stream::select_with_strategy;
21use itertools::Itertools;
22use risingwave_common::array::{DataChunk, Op};
23use risingwave_common::bail;
24use risingwave_common::bitmap::BitmapBuilder;
25use risingwave_common::catalog::{CdcKeyComparison, ColumnDesc};
26use risingwave_common::row::RowExt;
27use risingwave_common::util::sort_util::OrderType;
28use risingwave_connector::parser::{
29 BigintUnsignedHandlingMode, ByteStreamSourceParser, DebeziumParser, DebeziumProps,
30 EncodingProperties, JsonProperties, ProtocolProperties, SourceStreamChunkBuilder,
31 SpecificParserConfig, TimeHandling, TimestampHandling, TimestamptzHandling,
32};
33use risingwave_connector::source::cdc::CdcScanOptions;
34use risingwave_connector::source::cdc::external::{
35 CdcOffset, CdcOffsetParseFunc, ExternalCdcTableType, ExternalTableReaderImpl,
36};
37use risingwave_connector::source::{SourceColumnDesc, SourceContext, SourceCtrlOpts};
38use rw_futures_util::pausable;
39use thiserror_ext::AsReport;
40
41use crate::executor::backfill::cdc::state::CdcBackfillState;
42use crate::executor::backfill::cdc::upstream_table::external::ExternalStorageTable;
43use crate::executor::backfill::cdc::upstream_table::reader::{
44 build_reader_and_poll_upstream, create_table_reader_with_retry,
45};
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::{Mutation, TableId};
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
67fn can_poll_upstream_while_creating_reader(
68 current_pk_pos: Option<&OwnedRow>,
69 pk_comparisons_are_known: bool,
70) -> bool {
71 current_pk_pos.is_none() || pk_comparisons_are_known
72}
73
74enum NoSnapshotUpstreamOutcome {
76 Continue(Option<Message>),
79 Stop(Barrier),
81}
82
83#[derive(Clone, Copy)]
86struct SnapshotCursor<'a> {
87 pk_pos: &'a OwnedRow,
88 pk_needs_unsigned_i64_compare: &'a [bool],
89}
90
91struct NoSnapshotUpstreamHandler<'a, S: StateStore> {
92 actor_ctx: &'a ActorContextRef,
93 metrics: &'a CdcBackfillMetrics,
94 table_id: TableId,
95 upstream_table_name: &'a str,
96 offset_parse_func: &'a mut CdcOffsetParseFunc,
97 output_indices: &'a [usize],
98 pk_indices: &'a [usize],
99 pk_order: &'a [OrderType],
100 cursor: Option<SnapshotCursor<'a>>,
102 total_snapshot_row_count: u64,
103 state_impl: &'a mut CdcBackfillState<S>,
104 last_binlog_offset: &'a mut Option<CdcOffset>,
105 is_snapshot_paused: &'a mut bool,
106 rate_limit_rps: &'a mut Option<u32>,
107}
108
109impl<S: StateStore> NoSnapshotUpstreamHandler<'_, S> {
110 async fn handle_upstream(
111 &mut self,
112 msg: Message,
113 ) -> StreamExecutorResult<NoSnapshotUpstreamOutcome> {
114 match msg {
115 Message::Barrier(barrier) => {
116 if let Some(mutation) = barrier.mutation.as_deref() {
117 match mutation {
118 Mutation::Pause => {
119 *self.is_snapshot_paused = true;
120 tracing::info!(
121 table_id = %self.table_id,
122 upstream_table_name = self.upstream_table_name,
123 "snapshot is paused by barrier"
124 );
125 }
126 Mutation::Resume => {
127 *self.is_snapshot_paused = false;
128 tracing::info!(
129 table_id = %self.table_id,
130 upstream_table_name = self.upstream_table_name,
131 "snapshot is resumed by barrier"
132 );
133 }
134 Mutation::Throttle(_) => {
135 if let Some(entry) =
136 mutation.backfill_throttle_config(self.actor_ctx.fragment_id)
137 {
138 *self.rate_limit_rps = entry.rate_limit;
139 }
140 }
141 mutation if mutation.is_stop(self.actor_ctx.id) => {
142 tracing::info!(
143 table_id = %self.table_id,
144 upstream_table_name = self.upstream_table_name,
145 "CdcBackfill has been dropped due to config change"
146 );
147 self.state_impl.commit_state(barrier.epoch).await?;
148
149 return Ok(NoSnapshotUpstreamOutcome::Stop(barrier));
150 }
151 _ => (),
152 }
153 }
154
155 self.state_impl
157 .mutate_and_commit_state(
158 self.cursor.map(|cursor| cursor.pk_pos.clone()),
159 self.last_binlog_offset.clone(),
160 self.total_snapshot_row_count,
161 false,
162 barrier.epoch,
163 )
164 .await?;
165
166 Ok(NoSnapshotUpstreamOutcome::Continue(Some(Message::Barrier(
167 barrier,
168 ))))
169 }
170 Message::Chunk(chunk) => {
171 let Some(cursor) = self.cursor else {
172 return Ok(NoSnapshotUpstreamOutcome::Continue(None));
174 };
175
176 if !chunk.has_visible_rows() {
177 return Ok(NoSnapshotUpstreamOutcome::Continue(None));
178 }
179
180 let (chunk, consumed_offset) = CdcBackfillExecutor::<S>::filter_recovery_chunk(
181 self.offset_parse_func,
182 chunk,
183 cursor.pk_pos,
184 PkCompareInfo {
185 indices: self.pk_indices,
186 order: self.pk_order,
187 needs_unsigned_i64_compare: cursor.pk_needs_unsigned_i64_compare,
188 },
189 self.last_binlog_offset,
190 self.output_indices,
191 )?;
192
193 if let Some(consumed_offset) = consumed_offset {
194 *self.last_binlog_offset = Some(consumed_offset);
195 }
196
197 let chunk = chunk.map(|chunk| {
198 CdcBackfillExecutor::<S>::report_metrics(
199 self.metrics,
200 0,
201 chunk.cardinality() as u64,
202 );
203
204 Message::Chunk(chunk)
205 });
206
207 Ok(NoSnapshotUpstreamOutcome::Continue(chunk))
208 }
209 Message::Watermark(_) => Ok(NoSnapshotUpstreamOutcome::Continue(None)),
210 }
211 }
212
213 async fn wait_for_reader(
214 &mut self,
215 upstream: &mut (impl Stream<Item = StreamExecutorResult<Message>> + Unpin),
216 future: &mut Pin<Box<impl Future<Output = ExternalTableReaderImpl>>>,
217 ) -> StreamExecutorResult<Either<NoSnapshotUpstreamOutcome, ExternalTableReaderImpl>> {
218 match build_reader_and_poll_upstream(upstream, future).await? {
219 Either::Left(msg) => Ok(Either::Left(self.handle_upstream(msg).await?)),
220 Either::Right(table_reader) => Ok(Either::Right(table_reader)),
221 }
222 }
223}
224
225pub(crate) fn get_cdc_json_parse_handling_from_properties(
230 properties: &BTreeMap<String, String>,
231) -> (
232 Option<TimestampHandling>,
233 Option<TimestamptzHandling>,
234 Option<TimeHandling>,
235 Option<BigintUnsignedHandlingMode>,
236) {
237 let (timestamp_handling, timestamptz_handling, time_handling) = match properties
238 .get("debezium.time.precision.mode")
239 {
240 None => (
241 Some(TimestampHandling::Micro),
242 Some(TimestamptzHandling::Micro),
243 Some(TimeHandling::Micro),
244 ),
245 Some(m) if m == "connect" => (
246 Some(TimestampHandling::Milli),
247 Some(TimestamptzHandling::Milli),
248 Some(TimeHandling::Milli),
249 ),
250 Some(other) => {
251 tracing::warn!(
253 "Unsupported debezium.time.precision.mode = {other}, fall back to default parser."
254 );
255 (None, None, None)
256 }
257 };
258 let bigint_unsigned_handling = properties
259 .get("debezium.bigint.unsigned.handling.mode")
260 .is_some_and(|v| v == "precise")
261 .then_some(BigintUnsignedHandlingMode::Precise);
262 (
263 timestamp_handling,
264 timestamptz_handling,
265 time_handling,
266 bigint_unsigned_handling,
267 )
268}
269
270pub struct CdcBackfillExecutor<S: StateStore> {
271 actor_ctx: ActorContextRef,
272
273 external_table: ExternalStorageTable,
275
276 upstream: Executor,
278
279 output_indices: Vec<usize>,
281
282 output_columns: Vec<ColumnDesc>,
284
285 state_impl: CdcBackfillState<S>,
287
288 progress: Option<CreateMviewProgressReporter>,
291
292 metrics: CdcBackfillMetrics,
293
294 rate_limit_rps: Option<u32>,
296
297 options: CdcScanOptions,
298
299 properties: BTreeMap<String, String>,
300}
301
302impl<S: StateStore> CdcBackfillExecutor<S> {
303 #[expect(clippy::too_many_arguments)]
304 pub fn new(
305 actor_ctx: ActorContextRef,
306 external_table: ExternalStorageTable,
307 upstream: Executor,
308 output_indices: Vec<usize>,
309 output_columns: Vec<ColumnDesc>,
310 progress: Option<CreateMviewProgressReporter>,
311 metrics: Arc<StreamingMetrics>,
312 state_table: StateTable<S>,
313 rate_limit_rps: Option<u32>,
314 options: CdcScanOptions,
315 properties: BTreeMap<String, String>,
316 ) -> Self {
317 let pk_indices = external_table.pk_indices();
318 let upstream_table_id = external_table.table_id();
319 let state_impl = CdcBackfillState::new(
320 upstream_table_id,
321 state_table,
322 pk_indices.len() + METADATA_STATE_LEN,
323 );
324
325 let metrics = metrics.new_cdc_backfill_metrics(external_table.table_id(), actor_ctx.id);
326 Self {
327 actor_ctx,
328 external_table,
329 upstream,
330 output_indices,
331 output_columns,
332 state_impl,
333 progress,
334 metrics,
335 rate_limit_rps,
336 options,
337 properties,
338 }
339 }
340
341 fn report_metrics(
342 metrics: &CdcBackfillMetrics,
343 snapshot_processed_row_count: u64,
344 upstream_processed_row_count: u64,
345 ) {
346 metrics
347 .cdc_backfill_snapshot_read_row_count
348 .inc_by(snapshot_processed_row_count);
349
350 metrics
351 .cdc_backfill_upstream_output_row_count
352 .inc_by(upstream_processed_row_count);
353 }
354
355 fn consume_upstream_chunk_buffer(
361 offset_parse_func: &CdcOffsetParseFunc,
362 upstream_chunk_buffer: &mut Vec<StreamChunk>,
363 current_pk_pos: Option<&OwnedRow>,
364 pk_compare: PkCompareInfo<'_>,
365 last_binlog_offset: &Option<CdcOffset>,
366 output_indices: &[usize],
367 ) -> StreamExecutorResult<(Vec<StreamChunk>, u64, Option<CdcOffset>)> {
368 let Some(current_pos) = current_pk_pos else {
369 return Ok((vec![], 0, None));
370 };
371
372 let buffered_chunks = std::mem::take(upstream_chunk_buffer);
373 let mut emitted_chunks = Vec::with_capacity(buffered_chunks.len());
374 let mut upstream_processed_row_count = 0;
375 let mut consumed_binlog_offset = None;
376 let mut retained_chunks = Vec::with_capacity(buffered_chunks.len());
377 let mut can_advance_consumed_offset = true;
378
379 for chunk in buffered_chunks {
380 let mut emitted_vis = BitmapBuilder::zeroed(chunk.capacity());
381 let mut retained_vis = BitmapBuilder::zeroed(chunk.capacity());
382 for idx in 0..chunk.capacity() {
383 let (op, row, visible) = chunk.row_at(idx);
384 if !visible {
385 continue;
387 }
388 let event_offset = (*offset_parse_func)(
389 row.iter()
390 .last()
391 .flatten()
392 .expect("cdc offset must exist")
393 .into_utf8(),
394 )?;
395 let in_binlog_range = last_binlog_offset
396 .as_ref()
397 .is_none_or(|binlog_low| *binlog_low <= event_offset);
398
399 let row_pk = row.project(pk_compare.indices);
400 let reached_current_pos = cmp_pk_unsigned_aware(
401 row_pk.iter(),
402 current_pos.iter(),
403 pk_compare.order,
404 pk_compare.needs_unsigned_i64_compare,
405 )
406 .is_le();
407 if !in_binlog_range {
408 continue;
409 }
410 let should_emit = reached_current_pos;
411
412 match op {
413 Op::Insert | Op::Delete => {
414 if should_emit {
415 emitted_vis.set(idx, true);
416 if can_advance_consumed_offset {
417 consumed_binlog_offset = Some(event_offset);
418 }
419 upstream_processed_row_count += 1;
420 } else {
421 retained_vis.set(idx, true);
422 can_advance_consumed_offset = false;
423 }
424 }
425 Op::UpdateDelete | Op::UpdateInsert => {
426 unreachable!("CDC buffered chunks should not contain update pairs")
427 }
428 }
429 }
430
431 let emitted_vis = emitted_vis.finish();
432 if emitted_vis.count_ones() > 0 {
433 emitted_chunks.push(mapping_chunk(
434 chunk.clone_with_vis(emitted_vis),
435 output_indices,
436 ));
437 }
438
439 let retained_vis = retained_vis.finish();
440 if retained_vis.count_ones() > 0 {
441 retained_chunks.push(chunk.clone_with_vis(retained_vis));
442 }
443 }
444
445 *upstream_chunk_buffer = retained_chunks;
446
447 Ok((
448 emitted_chunks,
449 upstream_processed_row_count,
450 consumed_binlog_offset,
451 ))
452 }
453
454 fn filter_recovery_chunk(
458 offset_parse_func: &CdcOffsetParseFunc,
459 chunk: StreamChunk,
460 current_pos: &OwnedRow,
461 pk_compare: PkCompareInfo<'_>,
462 last_binlog_offset: &Option<CdcOffset>,
463 output_indices: &[usize],
464 ) -> StreamExecutorResult<(Option<StreamChunk>, Option<CdcOffset>)> {
465 let chunk_offset = get_cdc_chunk_last_offset(offset_parse_func, &chunk)?;
466 let chunk = mapping_chunk(
467 mark_cdc_chunk(
468 offset_parse_func,
469 chunk,
470 current_pos,
471 pk_compare.indices,
472 pk_compare.order,
473 pk_compare.needs_unsigned_i64_compare,
474 last_binlog_offset.clone(),
475 )?,
476 output_indices,
477 );
478 let chunk = (chunk.cardinality() > 0).then_some(chunk);
479 let consumed_offset = chunk_offset.filter(|chunk_offset| {
480 last_binlog_offset
481 .as_ref()
482 .is_none_or(|last| *last < *chunk_offset)
483 });
484 Ok((chunk, consumed_offset))
485 }
486
487 #[try_stream(ok = Message, error = StreamExecutorError)]
488 async fn execute_inner(mut self) {
489 let pk_indices = self.external_table.pk_indices().to_vec();
491 let pk_order = self.external_table.pk_order_types().to_vec();
492 let pk_needs_unsigned_i64_compare =
493 self.external_table.pk_comparisons().map(|comparisons| {
494 comparisons
495 .iter()
496 .map(|comparison| *comparison == CdcKeyComparison::UnsignedInt64)
497 .collect_vec()
498 });
499
500 let table_id = self.external_table.table_id();
501 let upstream_table_name = self.external_table.qualified_table_name();
502 let schema_table_name = self.external_table.schema_table_name().clone();
503 let external_database_name = self.external_table.database_name().to_owned();
504
505 let additional_columns = self
506 .output_columns
507 .iter()
508 .filter(|col| col.additional_column.column_type.is_some())
509 .cloned()
510 .collect_vec();
511
512 let mut upstream = self.upstream.execute();
513
514 let mut current_pk_pos: Option<OwnedRow>;
517
518 let first_barrier = expect_first_barrier(&mut upstream).await?;
520
521 let mut is_snapshot_paused = first_barrier.is_pause_on_startup();
522 let first_barrier_epoch = first_barrier.epoch;
523 yield Message::Barrier(first_barrier);
525
526 let mut state_impl = self.state_impl;
527
528 state_impl.init_epoch(first_barrier_epoch).await?;
529
530 let state = state_impl.restore_state().await?;
532 current_pk_pos = state.current_pk_pos.clone();
533
534 let need_backfill = !self.options.disable_backfill && !state.is_finished;
535
536 let mut total_snapshot_row_count = state.row_count as u64;
538
539 let (timestamp_handling, timestamptz_handling, time_handling, bigint_unsigned_handling) =
540 get_cdc_json_parse_handling_from_properties(&self.properties);
541 let handle_toast_columns: bool =
543 self.external_table.table_type() == &ExternalCdcTableType::Postgres;
544 let mut upstream = transform_upstream(
546 upstream,
547 self.output_columns.clone(),
548 timestamp_handling,
549 timestamptz_handling,
550 time_handling,
551 bigint_unsigned_handling,
552 handle_toast_columns,
553 )
554 .boxed()
555 .peekable();
556 let mut last_binlog_offset = state.last_cdc_offset.clone();
557
558 if need_backfill {
579 let mut offset_parse_func = self.external_table.table_type().get_cdc_offset_parser()?;
587 let mut future = Box::pin(create_table_reader_with_retry(
588 self.external_table.clone(),
589 self.actor_ctx.id,
590 self.actor_ctx.fragment_id,
591 ));
592 let table_reader = if can_poll_upstream_while_creating_reader(
593 current_pk_pos.as_ref(),
594 pk_needs_unsigned_i64_compare.is_some(),
595 ) {
596 let mut handler = NoSnapshotUpstreamHandler {
597 actor_ctx: &self.actor_ctx,
598 metrics: &self.metrics,
599 table_id,
600 upstream_table_name: &upstream_table_name,
601 offset_parse_func: &mut offset_parse_func,
602 output_indices: &self.output_indices,
603 pk_indices: &pk_indices,
604 pk_order: &pk_order,
605 cursor: current_pk_pos.as_ref().map(|pk_pos| SnapshotCursor {
606 pk_pos,
607 pk_needs_unsigned_i64_compare: pk_needs_unsigned_i64_compare
608 .as_deref()
609 .expect("checked by can_poll_upstream_while_creating_reader"),
610 }),
611 total_snapshot_row_count,
612 state_impl: &mut state_impl,
613 last_binlog_offset: &mut last_binlog_offset,
614 is_snapshot_paused: &mut is_snapshot_paused,
615 rate_limit_rps: &mut self.rate_limit_rps,
616 };
617 loop {
618 match handler.wait_for_reader(&mut upstream, &mut future).await? {
619 Either::Left(NoSnapshotUpstreamOutcome::Continue(msg)) => {
620 if let Some(msg) = msg {
621 yield msg;
622 }
623 }
624 Either::Left(NoSnapshotUpstreamOutcome::Stop(barrier)) => {
625 yield Message::Barrier(barrier);
626
627 return Ok(());
628 }
629 Either::Right(table_reader) => break table_reader,
630 }
631 }
632 } else {
633 tracing::warn!(
634 %table_id,
635 upstream_table_name,
636 "waiting for the CDC table reader to recover legacy MySQL primary-key comparison metadata; checkpoint progress is paused"
637 );
638 future.as_mut().await
639 };
640 let pk_names = self.external_table.pk_names();
641 let pk_needs_unsigned_i64_compare = match pk_needs_unsigned_i64_compare {
642 Some(comparisons) => comparisons,
643 None => {
644 let comparisons = table_reader.pk_column_comparisons(&pk_names)?;
645 assert_eq!(comparisons.len(), pk_indices.len());
646 comparisons
647 .into_iter()
648 .map(|comparison| comparison == CdcKeyComparison::UnsignedInt64)
649 .collect_vec()
650 }
651 };
652 tracing::info!(
653 %table_id,
654 upstream_table_name,
655 "table reader created successfully"
656 );
657
658 let mut upstream_table_reader =
659 UpstreamTableReader::new(self.external_table.clone(), table_reader);
660
661 if last_binlog_offset.is_none() {
662 static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
664 tokio::sync::Semaphore::const_new(10);
665
666 let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
667 last_binlog_offset = upstream_table_reader.current_cdc_offset().await?;
668 }
669
670 tracing::info!(
671 %table_id,
672 upstream_table_name,
673 initial_binlog_offset = ?last_binlog_offset,
674 ?current_pk_pos,
675 is_finished = state.is_finished,
676 is_snapshot_paused,
677 snapshot_row_count = total_snapshot_row_count,
678 rate_limit = self.rate_limit_rps,
679 disable_backfill = self.options.disable_backfill,
680 snapshot_barrier_interval = self.options.snapshot_barrier_interval,
681 snapshot_batch_size = self.options.snapshot_batch_size,
682 "start cdc backfill",
683 );
684
685 let _ = Pin::new(&mut upstream).peek().await;
688
689 let mut handler = NoSnapshotUpstreamHandler {
691 actor_ctx: &self.actor_ctx,
692 metrics: &self.metrics,
693 table_id,
694 upstream_table_name: &upstream_table_name,
695 offset_parse_func: &mut offset_parse_func,
696 output_indices: &self.output_indices,
697 pk_indices: &pk_indices,
698 pk_order: &pk_order,
699 cursor: current_pk_pos.as_ref().map(|pk_pos| SnapshotCursor {
700 pk_pos,
701 pk_needs_unsigned_i64_compare: &pk_needs_unsigned_i64_compare,
702 }),
703 total_snapshot_row_count,
704 state_impl: &mut state_impl,
705 last_binlog_offset: &mut last_binlog_offset,
706 is_snapshot_paused: &mut is_snapshot_paused,
707 rate_limit_rps: &mut self.rate_limit_rps,
708 };
709 #[for_await]
710 for msg in upstream.by_ref() {
711 match msg? {
712 Message::Chunk(chunk) if handler.cursor.is_none() => {
713 if chunk.has_visible_rows()
716 && let Some(chunk_offset) =
717 get_cdc_chunk_last_offset(handler.offset_parse_func, &chunk)?
718 && handler
719 .last_binlog_offset
720 .as_ref()
721 .is_none_or(|last| *last < chunk_offset)
722 {
723 *handler.last_binlog_offset = Some(chunk_offset);
724 }
725 }
726 msg => match handler.handle_upstream(msg).await? {
727 NoSnapshotUpstreamOutcome::Continue(Some(msg)) => {
728 let is_barrier = matches!(msg, Message::Barrier(_));
729
730 yield msg;
731
732 if is_barrier {
733 break;
734 }
735 }
736 NoSnapshotUpstreamOutcome::Continue(None) => {}
737 NoSnapshotUpstreamOutcome::Stop(barrier) => {
738 yield Message::Barrier(barrier);
739
740 return Ok(());
741 }
742 },
743 }
744 }
745
746 tracing::info!(%table_id,
747 upstream_table_name,
748 initial_binlog_offset = ?last_binlog_offset,
749 ?current_pk_pos,
750 is_snapshot_paused,
751 "start cdc backfill loop");
752
753 let mut upstream_chunk_buffer: Vec<StreamChunk> = vec![];
755 let mut needs_reader_rebuild = false;
756
757 'backfill_loop: loop {
758 if needs_reader_rebuild {
759 needs_reader_rebuild = false;
760 tracing::info!(
761 %table_id,
762 upstream_table_name,
763 "rebuilding CDC table reader after snapshot read failure"
764 );
765
766 if let Err(error) = upstream_table_reader.disconnect().await {
767 tracing::warn!(
768 error = %error.as_report(),
769 %table_id,
770 upstream_table_name,
771 "failed to disconnect CDC snapshot reader; continuing with rebuild"
772 );
773 }
774
775 let mut future = Box::pin(create_table_reader_with_retry(
776 self.external_table.clone(),
777 self.actor_ctx.id,
778 self.actor_ctx.fragment_id,
779 ));
780
781 let mut handler = NoSnapshotUpstreamHandler {
782 actor_ctx: &self.actor_ctx,
783 metrics: &self.metrics,
784 table_id,
785 upstream_table_name: &upstream_table_name,
786 offset_parse_func: &mut offset_parse_func,
787 output_indices: &self.output_indices,
788 pk_indices: &pk_indices,
789 pk_order: &pk_order,
790 cursor: current_pk_pos.as_ref().map(|pk_pos| SnapshotCursor {
791 pk_pos,
792 pk_needs_unsigned_i64_compare: &pk_needs_unsigned_i64_compare,
793 }),
794 total_snapshot_row_count,
795 state_impl: &mut state_impl,
796 last_binlog_offset: &mut last_binlog_offset,
797 is_snapshot_paused: &mut is_snapshot_paused,
798 rate_limit_rps: &mut self.rate_limit_rps,
799 };
800 let table_reader = loop {
801 match handler.wait_for_reader(&mut upstream, &mut future).await? {
802 Either::Left(NoSnapshotUpstreamOutcome::Continue(msg)) => {
803 if let Some(msg) = msg {
804 yield msg;
805 }
806 }
807 Either::Left(NoSnapshotUpstreamOutcome::Stop(barrier)) => {
808 yield Message::Barrier(barrier);
809
810 return Ok(());
811 }
812 Either::Right(table_reader) => break table_reader,
813 }
814 };
815
816 upstream_table_reader =
817 UpstreamTableReader::new(self.external_table.clone(), table_reader);
818 tracing::info!(
819 %table_id,
820 upstream_table_name,
821 "CDC table reader rebuilt successfully"
822 );
823 }
824
825 let mut should_bypass_snapshot_stream_patch =
826 self.rate_limit_rps.is_some_and(|val| val == 0);
827 let left_upstream = upstream.by_ref().map(Either::Left);
828
829 let mut snapshot_read_row_cnt: usize = 0;
830 let read_args = SnapshotReadArgs::new(
831 current_pk_pos.clone(),
832 self.rate_limit_rps,
833 pk_indices.clone(),
834 additional_columns.clone(),
835 schema_table_name.clone(),
836 external_database_name.clone(),
837 );
838 let right_snapshot = pin!(
839 upstream_table_reader
840 .snapshot_read_full_table(read_args, self.options.snapshot_batch_size)
841 .map(Either::Right)
842 );
843 let (right_snapshot, snapshot_valve) = pausable(right_snapshot);
844 if is_snapshot_paused {
845 snapshot_valve.pause();
846 }
847
848 let mut backfill_stream =
850 select_with_strategy(left_upstream, right_snapshot, |_: &mut ()| {
851 stream::PollNext::Left
852 });
853
854 let mut cur_barrier_snapshot_processed_rows: u64 = 0;
855 let mut cur_barrier_upstream_processed_rows: u64 = 0;
856 let mut barrier_count: u32 = 0;
857 let mut pending_barrier = None;
858
859 #[for_await]
860 for either in &mut backfill_stream {
861 match either {
862 Either::Left(msg) => {
864 match msg? {
865 Message::Barrier(barrier) => {
866 barrier_count += 1;
868 let mut needs_rebuild_snapshot =
869 barrier_count == self.options.snapshot_barrier_interval;
870
871 if let Some(mutation) = barrier.mutation.as_deref() {
872 match mutation {
873 Mutation::Pause => {
874 is_snapshot_paused = true;
875 snapshot_valve.pause();
876 }
877 Mutation::Resume => {
878 is_snapshot_paused = false;
879 snapshot_valve.resume();
880 }
881 Mutation::Throttle(_) => {
882 if let Some(entry) = mutation
883 .backfill_throttle_config(
884 self.actor_ctx.fragment_id,
885 )
886 && entry.rate_limit != self.rate_limit_rps
887 {
888 should_bypass_snapshot_stream_patch = self
891 .rate_limit_rps
892 .is_some_and(|val| val == 0);
893 self.rate_limit_rps = entry.rate_limit;
894 needs_rebuild_snapshot = true;
895 }
896 }
897 mutation if mutation.is_stop(self.actor_ctx.id) => {
898 tracing::info!(
900 %table_id,
901 upstream_table_name,
902 "CdcBackfill has been dropped due to config change"
903 );
904 state_impl.commit_state(barrier.epoch).await?;
905 yield Message::Barrier(barrier);
906 break 'backfill_loop;
907 }
908 _ => (),
909 }
910 }
911
912 if needs_rebuild_snapshot || needs_reader_rebuild {
915 pending_barrier = Some(barrier);
917 tracing::debug!(
918 %table_id,
919 ?current_pk_pos,
920 ?snapshot_read_row_cnt,
921 "Prepare to start a new snapshot"
922 );
923 break;
925 } else {
926 let (
928 emitted_upstream_chunks,
929 consumed_upstream_row_count,
930 consumed_binlog_offset,
931 ) = Self::consume_upstream_chunk_buffer(
932 &offset_parse_func,
933 &mut upstream_chunk_buffer,
934 current_pk_pos.as_ref(),
935 PkCompareInfo {
936 indices: &pk_indices,
937 order: &pk_order,
938 needs_unsigned_i64_compare:
939 &pk_needs_unsigned_i64_compare,
940 },
941 &last_binlog_offset,
942 &self.output_indices,
943 )?;
944 cur_barrier_upstream_processed_rows +=
945 consumed_upstream_row_count;
946 if let Some(consumed_binlog_offset) = consumed_binlog_offset
947 {
948 last_binlog_offset = Some(consumed_binlog_offset);
949 }
950 for chunk in emitted_upstream_chunks {
951 yield Message::Chunk(chunk);
952 }
953
954 Self::report_metrics(
955 &self.metrics,
956 cur_barrier_snapshot_processed_rows,
957 cur_barrier_upstream_processed_rows,
958 );
959
960 state_impl
962 .mutate_and_commit_state(
963 current_pk_pos.clone(),
964 last_binlog_offset.clone(),
965 total_snapshot_row_count,
966 false,
967 barrier.epoch,
968 )
969 .await?;
970
971 yield Message::Barrier(barrier);
973 }
974 }
975 Message::Chunk(chunk) => {
976 if chunk.cardinality() == 0 {
978 continue;
979 }
980
981 let chunk_binlog_offset =
982 get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
983
984 tracing::trace!(
985 "recv changelog chunk: chunk_offset {:?}, capactiy {}",
986 chunk_binlog_offset,
987 chunk.capacity()
988 );
989
990 if let (Some(last_binlog_offset), Some(chunk_offset)) =
994 (last_binlog_offset.as_ref(), chunk_binlog_offset)
995 && chunk_offset < *last_binlog_offset
996 {
997 tracing::trace!(
998 "skip changelog chunk: chunk_offset {:?}, capacity {}",
999 chunk_offset,
1000 chunk.capacity()
1001 );
1002 continue;
1003 }
1004
1005 upstream_chunk_buffer.push(chunk.compact_vis());
1007 }
1008 Message::Watermark(_) => {
1009 }
1011 }
1012 }
1013 Either::Right(msg) => {
1015 match msg {
1016 Ok(None) => {
1017 tracing::info!(
1018 %table_id,
1019 ?last_binlog_offset,
1020 ?current_pk_pos,
1021 "snapshot read stream ends"
1022 );
1023 for chunk in upstream_chunk_buffer.drain(..) {
1028 yield Message::Chunk(mapping_chunk(
1029 chunk,
1030 &self.output_indices,
1031 ));
1032 }
1033
1034 break 'backfill_loop;
1036 }
1037 Ok(Some(chunk)) => {
1038 current_pk_pos = Some(get_new_pos(&chunk, &pk_indices));
1042
1043 tracing::trace!(
1044 "got a snapshot chunk: len {}, current_pk_pos {:?}",
1045 chunk.cardinality(),
1046 current_pk_pos
1047 );
1048 let chunk_cardinality = chunk.cardinality() as u64;
1049 cur_barrier_snapshot_processed_rows += chunk_cardinality;
1050 total_snapshot_row_count += chunk_cardinality;
1051 yield Message::Chunk(mapping_chunk(
1052 chunk,
1053 &self.output_indices,
1054 ));
1055 }
1056 Err(error) => {
1057 needs_reader_rebuild = true;
1058 tracing::warn!(
1059 error = %error.as_report(),
1060 %table_id,
1061 upstream_table_name,
1062 "failed to read CDC snapshot; rebuilding reader after a barrier"
1063 );
1064 }
1065 }
1066 }
1067 }
1068 }
1069
1070 assert!(pending_barrier.is_some(), "pending_barrier must exist");
1071 let pending_barrier = pending_barrier.unwrap();
1072
1073 let (_, mut snapshot_stream) = backfill_stream.into_inner();
1079 if !should_bypass_snapshot_stream_patch && is_snapshot_paused {
1081 snapshot_valve.resume();
1082 }
1083 if !should_bypass_snapshot_stream_patch
1084 && let Some(msg) = snapshot_stream
1085 .next()
1086 .instrument_await("consume_snapshot_stream_once")
1087 .await
1088 {
1089 let Either::Right(msg) = msg else {
1090 bail!("BUG: snapshot_read contains upstream messages");
1091 };
1092 match msg {
1093 Ok(None) => {
1094 tracing::info!(
1095 %table_id,
1096 ?last_binlog_offset,
1097 ?current_pk_pos,
1098 "snapshot read stream ends in the force emit branch"
1099 );
1100 for chunk in upstream_chunk_buffer.drain(..) {
1103 yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
1104 }
1105
1106 state_impl
1108 .mutate_and_commit_state(
1109 current_pk_pos.clone(),
1110 last_binlog_offset.clone(),
1111 total_snapshot_row_count,
1112 true,
1113 pending_barrier.epoch,
1114 )
1115 .await?;
1116
1117 yield Message::Barrier(pending_barrier);
1118 break 'backfill_loop;
1120 }
1121 Ok(Some(_)) if is_snapshot_paused => {
1122 }
1124 Ok(Some(chunk)) => {
1125 current_pk_pos = Some(get_new_pos(&chunk, &pk_indices));
1127
1128 let row_count = chunk.cardinality() as u64;
1129 cur_barrier_snapshot_processed_rows += row_count;
1130 total_snapshot_row_count += row_count;
1131 snapshot_read_row_cnt += row_count as usize;
1132
1133 tracing::debug!(
1134 %table_id,
1135 ?current_pk_pos,
1136 ?snapshot_read_row_cnt,
1137 "force emit a snapshot chunk"
1138 );
1139 yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
1140 }
1141 Err(error) => {
1142 needs_reader_rebuild = true;
1143 tracing::warn!(
1144 error = %error.as_report(),
1145 %table_id,
1146 upstream_table_name,
1147 "failed to read CDC snapshot; rebuilding reader after a barrier"
1148 );
1149 }
1150 }
1151 }
1152
1153 let mut consumed_binlog_offset = None;
1156
1157 if let Some(current_pos) = ¤t_pk_pos {
1158 for chunk in upstream_chunk_buffer.drain(..) {
1159 cur_barrier_upstream_processed_rows += chunk.cardinality() as u64;
1160
1161 consumed_binlog_offset =
1164 get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
1165
1166 yield Message::Chunk(mapping_chunk(
1167 mark_cdc_chunk(
1168 &offset_parse_func,
1169 chunk,
1170 current_pos,
1171 &pk_indices,
1172 &pk_order,
1173 &pk_needs_unsigned_i64_compare,
1174 last_binlog_offset.clone(),
1175 )?,
1176 &self.output_indices,
1177 ));
1178 }
1179 } else {
1180 upstream_chunk_buffer.clear();
1183 }
1184
1185 if consumed_binlog_offset.is_some() {
1187 last_binlog_offset.clone_from(&consumed_binlog_offset);
1188 }
1189
1190 Self::report_metrics(
1191 &self.metrics,
1192 cur_barrier_snapshot_processed_rows,
1193 cur_barrier_upstream_processed_rows,
1194 );
1195
1196 state_impl
1198 .mutate_and_commit_state(
1199 current_pk_pos.clone(),
1200 last_binlog_offset.clone(),
1201 total_snapshot_row_count,
1202 false,
1203 pending_barrier.epoch,
1204 )
1205 .await?;
1206
1207 yield Message::Barrier(pending_barrier);
1208 }
1209 upstream_table_reader.disconnect().await?;
1210 } else if self.options.disable_backfill {
1211 tracing::info!(
1213 %table_id,
1214 upstream_table_name,
1215 "CdcBackfill has been disabled"
1216 );
1217 state_impl
1218 .mutate_state(
1219 current_pk_pos.clone(),
1220 last_binlog_offset.clone(),
1221 total_snapshot_row_count,
1222 true,
1223 )
1224 .await?;
1225 }
1226
1227 tracing::info!(
1228 %table_id,
1229 upstream_table_name,
1230 "CdcBackfill has already finished and will forward messages directly to the downstream"
1231 );
1232
1233 while let Some(Ok(msg)) = upstream.next().await {
1236 if let Some(msg) = mapping_message(msg, &self.output_indices) {
1237 if let Message::Barrier(barrier) = &msg {
1239 state_impl
1241 .mutate_and_commit_state(
1242 current_pk_pos.clone(),
1243 last_binlog_offset.clone(),
1244 total_snapshot_row_count,
1245 true,
1246 barrier.epoch,
1247 )
1248 .await?;
1249
1250 if let Some(progress) = self.progress.as_mut() {
1252 progress.finish(barrier.epoch, total_snapshot_row_count);
1253 }
1254 yield msg;
1255 break;
1257 }
1258 yield msg;
1259 }
1260 }
1261
1262 #[for_await]
1266 for msg in upstream {
1267 if let Some(msg) = mapping_message(msg?, &self.output_indices) {
1270 if let Message::Barrier(barrier) = &msg {
1271 state_impl.commit_state(barrier.epoch).await?;
1273 }
1274 yield msg;
1275 }
1276 }
1277 }
1278}
1279
1280#[try_stream(ok = Message, error = StreamExecutorError)]
1281pub async fn transform_upstream(
1282 upstream: BoxedMessageStream,
1283 output_columns: Vec<ColumnDesc>,
1284 timestamp_handling: Option<TimestampHandling>,
1285 timestamptz_handling: Option<TimestamptzHandling>,
1286 time_handling: Option<TimeHandling>,
1287 bigint_unsigned_handling: Option<BigintUnsignedHandlingMode>,
1288 handle_toast_columns: bool,
1289) {
1290 let props = SpecificParserConfig {
1291 encoding_config: EncodingProperties::Json(JsonProperties {
1292 use_schema_registry: false,
1293 timestamp_handling,
1294 timestamptz_handling,
1295 time_handling,
1296 bigint_unsigned_handling,
1297 handle_toast_columns,
1298 }),
1299 protocol_config: ProtocolProperties::Debezium(DebeziumProps::default()),
1301 };
1302
1303 let columns_with_meta = output_columns
1305 .iter()
1306 .map(SourceColumnDesc::from)
1307 .collect_vec();
1308 let mut parser = DebeziumParser::new(
1309 props,
1310 columns_with_meta.clone(),
1311 Arc::new(SourceContext::dummy()),
1312 )
1313 .await
1314 .map_err(StreamExecutorError::connector_error)?;
1315
1316 pin_mut!(upstream);
1317 #[for_await]
1318 for msg in upstream {
1319 let mut msg = msg?;
1320 if let Message::Chunk(chunk) = &mut msg {
1321 let parsed_chunk = parse_debezium_chunk(&mut parser, chunk).await?;
1322 let _ = std::mem::replace(chunk, parsed_chunk);
1323 }
1324 yield msg;
1325 }
1326}
1327
1328async fn parse_debezium_chunk(
1329 parser: &mut DebeziumParser,
1330 chunk: &StreamChunk,
1331) -> StreamExecutorResult<StreamChunk> {
1332 let mut builder = SourceStreamChunkBuilder::new(
1339 parser.columns().to_vec(),
1340 SourceCtrlOpts {
1341 chunk_size: chunk.capacity(),
1342 split_txn: false,
1343 },
1344 );
1345
1346 let payloads = chunk.data_chunk().project(&[0]);
1350 let offsets = chunk.data_chunk().project(&[1]).compact_vis();
1351
1352 for payload in payloads.rows() {
1354 let ScalarRefImpl::Jsonb(jsonb_ref) = payload.datum_at(0).expect("payload must exist")
1355 else {
1356 panic!("payload must be jsonb");
1357 };
1358
1359 parser
1360 .parse_inner(
1361 None,
1362 Some(jsonb_ref.to_string().as_bytes().to_vec()),
1363 builder.row_writer(),
1364 )
1365 .await
1366 .unwrap();
1367 }
1368 builder.finish_current_chunk();
1369
1370 let parsed_chunk = {
1371 let mut iter = builder.consume_ready_chunks();
1372 assert_eq!(1, iter.len());
1373 iter.next().unwrap()
1374 };
1375 assert_eq!(parsed_chunk.capacity(), chunk.capacity()); let (ops, mut columns, vis) = parsed_chunk.into_inner();
1377 columns.extend(offsets.into_parts().0);
1381
1382 Ok(StreamChunk::from_parts(
1383 ops,
1384 DataChunk::from_parts(columns.into(), vis),
1385 ))
1386}
1387
1388impl<S: StateStore> Execute for CdcBackfillExecutor<S> {
1389 fn execute(self: Box<Self>) -> BoxedMessageStream {
1390 self.execute_inner().boxed()
1391 }
1392}
1393
1394#[cfg(test)]
1395mod tests {
1396 use std::collections::BTreeMap;
1397 use std::str::FromStr;
1398 use std::time::Duration;
1399
1400 use either::Either;
1401 use futures::{StreamExt, pin_mut, stream};
1402 use risingwave_common::array::{Array, DataChunk, Op, StreamChunk};
1403 use risingwave_common::catalog::{
1404 CdcKeyComparison, ColumnDesc, ColumnId, Field, Schema, TableId,
1405 };
1406 use risingwave_common::row::{OwnedRow, Row};
1407 use risingwave_common::types::{DataType, Datum, JsonbVal, ScalarImpl};
1408 use risingwave_common::util::epoch::test_epoch;
1409 use risingwave_common::util::iter_util::ZipEqFast;
1410 use risingwave_common::util::sort_util::OrderType;
1411 use risingwave_connector::source::cdc::CdcScanOptions;
1412 use risingwave_connector::source::cdc::external::mock_external_table::MockExternalTableReader;
1413 use risingwave_connector::source::cdc::external::mysql::MysqlOffset;
1414 use risingwave_connector::source::cdc::external::{
1415 CdcOffset, ExternalCdcTableType, ExternalTableConfig, ExternalTableReaderImpl,
1416 SchemaTableName,
1417 };
1418 use risingwave_storage::memory::MemoryStateStore;
1419
1420 use super::{
1421 PkCompareInfo, build_reader_and_poll_upstream, can_poll_upstream_while_creating_reader,
1422 };
1423 use crate::common::table::test_utils::gen_pbtable;
1424 use crate::executor::backfill::cdc::cdc_backfill::transform_upstream;
1425 use crate::executor::backfill::cdc::state::CdcBackfillState;
1426 use crate::executor::monitor::StreamingMetrics;
1427 use crate::executor::prelude::StateTable;
1428 use crate::executor::test_utils::{MessageSender, MockSource};
1429 use crate::executor::{
1430 ActorContext, Barrier, CdcBackfillExecutor, ExternalStorageTable, Message,
1431 };
1432
1433 #[tokio::test]
1434 async fn test_transform_upstream_chunk() {
1435 let schema = Schema::new(vec![
1436 Field::unnamed(DataType::Jsonb), Field::unnamed(DataType::Varchar), Field::unnamed(DataType::Varchar), ]);
1440 let stream_key = vec![1];
1441 let (mut tx, source) = MockSource::channel();
1442 let source = source.into_executor(schema.clone(), stream_key.clone());
1443 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 } }"#;
1445
1446 let datums: Vec<Datum> = vec![
1447 Some(JsonbVal::from_str(payload).unwrap().into()),
1448 Some("file: 1.binlog, pos: 100".to_owned().into()),
1449 Some("mydb.orders".to_owned().into()),
1450 ];
1451
1452 let mut builders = schema.create_array_builders(8);
1453 for (builder, datum) in builders.iter_mut().zip_eq_fast(datums.iter()) {
1454 builder.append(datum.clone());
1455 }
1456 let columns = builders
1457 .into_iter()
1458 .map(|builder| builder.finish().into())
1459 .collect();
1460
1461 let chunk = StreamChunk::from_parts(vec![Op::Insert], DataChunk::new(columns, 1));
1463
1464 tx.push_chunk(chunk);
1465 let upstream = Box::new(source).execute();
1466
1467 let columns = vec![
1469 ColumnDesc::named("O_ORDERKEY", ColumnId::new(1), DataType::Int64),
1470 ColumnDesc::named("O_CUSTKEY", ColumnId::new(2), DataType::Int64),
1471 ColumnDesc::named("O_ORDERSTATUS", ColumnId::new(3), DataType::Varchar),
1472 ColumnDesc::named("O_TOTALPRICE", ColumnId::new(4), DataType::Decimal),
1473 ColumnDesc::named("O_ORDERDATE", ColumnId::new(5), DataType::Date),
1474 ColumnDesc::named("commit_ts", ColumnId::new(6), DataType::Timestamptz),
1475 ];
1476
1477 let parsed_stream = transform_upstream(upstream, columns, None, None, None, None, false);
1478 pin_mut!(parsed_stream);
1479 let message = parsed_stream
1480 .next()
1481 .await
1482 .expect("transform stream should yield the input chunk")
1483 .expect("transforming the CDC chunk should succeed");
1484 let Message::Chunk(chunk) = message else {
1485 panic!("expected a transformed chunk");
1486 };
1487 assert_eq!(
1488 chunk
1489 .rows()
1490 .map(|(op, row)| (op, row.to_owned_row()))
1491 .collect::<Vec<_>>(),
1492 vec![(
1493 Op::Insert,
1494 OwnedRow::new(vec![
1495 Some(ScalarImpl::Int64(5)),
1496 Some(ScalarImpl::Int64(44485)),
1497 Some(ScalarImpl::Utf8("F".into())),
1498 Some(ScalarImpl::Decimal("144659.20".parse().unwrap())),
1499 Some(ScalarImpl::Date("1994-07-30".parse().unwrap())),
1500 None,
1501 Some(ScalarImpl::Utf8("file: 1.binlog, pos: 100".into())),
1502 ]),
1503 )]
1504 );
1505 }
1506
1507 #[tokio::test]
1508 async fn test_finished_backfill_forwards_without_reader() {
1509 let actor_context = ActorContext::for_test(1);
1510 let table_id = TableId::new(1234);
1511 let external_storage_table = ExternalStorageTable::new(
1512 table_id,
1513 SchemaTableName {
1514 schema_name: "public".to_owned(),
1515 table_name: "missing_table".to_owned(),
1516 },
1517 "mydb".to_owned(),
1518 ExternalTableConfig::default(),
1519 ExternalCdcTableType::Unspecified,
1520 Schema::new(vec![Field::with_name(DataType::Int64, "O_ORDERKEY")]),
1521 vec![OrderType::ascending()],
1522 Some(vec![CdcKeyComparison::Native]),
1523 vec![0],
1524 );
1525 let schema = Schema::new(vec![
1526 Field::unnamed(DataType::Jsonb), Field::unnamed(DataType::Varchar), Field::unnamed(DataType::Varchar), ]);
1530 let stream_key = vec![1];
1531 let (mut tx, source) = MockSource::channel();
1532 let source = source
1533 .stop_on_finish(false)
1534 .into_executor(schema.clone(), stream_key.clone());
1535 let output_indices = vec![1, 0, 4]; let output_columns = vec![
1537 ColumnDesc::named("O_ORDERKEY", ColumnId::new(1), DataType::Int64),
1538 ColumnDesc::named("O_CUSTKEY", ColumnId::new(2), DataType::Int64),
1539 ColumnDesc::named("O_ORDERSTATUS", ColumnId::new(3), DataType::Varchar),
1540 ColumnDesc::named("O_TOTALPRICE", ColumnId::new(4), DataType::Decimal),
1541 ColumnDesc::named("O_DUMMY", ColumnId::new(5), DataType::Int64),
1542 ColumnDesc::named("commit_ts", ColumnId::new(6), DataType::Timestamptz),
1543 ];
1544 let store = MemoryStateStore::new();
1545 let mut persisted_state =
1546 CdcBackfillState::new(table_id, create_cdc_state_table(store.clone()).await, 5);
1547 persisted_state
1548 .init_epoch(Barrier::new_test_barrier(test_epoch(6)).epoch)
1549 .await
1550 .unwrap();
1551 persisted_state
1552 .mutate_and_commit_state(
1553 Some(OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1554 Some(CdcOffset::Mysql(MysqlOffset::new(
1555 "1.binlog".to_owned(),
1556 100,
1557 ))),
1558 1,
1559 true,
1560 Barrier::new_test_barrier(test_epoch(7)).epoch,
1561 )
1562 .await
1563 .unwrap();
1564 let state_table = create_cdc_state_table(store).await;
1565 let cdc = CdcBackfillExecutor::new(
1566 actor_context,
1567 external_storage_table,
1568 source,
1569 output_indices,
1570 output_columns,
1571 None,
1572 StreamingMetrics::unused().into(),
1573 state_table,
1574 None,
1575 CdcScanOptions::default(),
1576 BTreeMap::default(),
1577 );
1578 let s = cdc.execute_inner();
1579 pin_mut!(s);
1580
1581 tx.send_barrier(Barrier::new_test_barrier(test_epoch(8)));
1583 {
1585 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 } }"#;
1586 let datums: Vec<Datum> = vec![
1587 Some(JsonbVal::from_str(payload).unwrap().into()),
1588 Some("file: 1.binlog, pos: 100".to_owned().into()),
1589 Some("mydb.orders".to_owned().into()),
1590 ];
1591 let mut builders = schema.create_array_builders(8);
1592 for (builder, datum) in builders.iter_mut().zip_eq_fast(datums.iter()) {
1593 builder.append(datum.clone());
1594 }
1595 let columns = builders
1596 .into_iter()
1597 .map(|builder| builder.finish().into())
1598 .collect();
1599 let chunk = StreamChunk::from_parts(vec![Op::Insert], DataChunk::new(columns, 1));
1601
1602 tx.push_chunk(chunk);
1603 }
1604 let _first_barrier = s.next().await.unwrap();
1605 let upstream_change_log = s.next().await.unwrap().unwrap();
1606 let Message::Chunk(chunk) = upstream_change_log else {
1607 panic!("expect chunk");
1608 };
1609 assert_eq!(chunk.columns().len(), 3);
1610 assert_eq!(chunk.rows().count(), 1);
1611 assert_eq!(
1612 chunk.columns()[0].as_int64().iter().collect::<Vec<_>>(),
1613 vec![Some(44485)]
1614 );
1615 assert_eq!(
1616 chunk.columns()[1].as_int64().iter().collect::<Vec<_>>(),
1617 vec![Some(5)]
1618 );
1619 assert_eq!(
1620 chunk.columns()[2].as_int64().iter().collect::<Vec<_>>(),
1621 vec![Some(100)]
1622 );
1623
1624 drop(tx);
1625 assert!(s.next().await.is_none());
1626 }
1627
1628 #[test]
1629 fn test_reader_startup_polling_selection() {
1630 let recovered_position = OwnedRow::new(vec![Some(ScalarImpl::Int64(1))]);
1631
1632 assert!(!can_poll_upstream_while_creating_reader(
1633 Some(&recovered_position),
1634 false,
1635 ));
1636 assert!(can_poll_upstream_while_creating_reader(None, false));
1637 assert!(can_poll_upstream_while_creating_reader(
1638 Some(&recovered_position),
1639 true,
1640 ));
1641 }
1642
1643 #[tokio::test]
1644 async fn test_snapshot_read_error_rebuilds_reader() {
1645 let external_table = create_mock_external_table().with_mock_snapshot_errors([1, 0]);
1646 let reader_create_probe = external_table.clone();
1647 let (tx, executor) = create_mock_cdc_backfill(
1648 external_table,
1649 create_cdc_state_table(MemoryStateStore::new()).await,
1650 CdcScanOptions {
1651 snapshot_barrier_interval: 1,
1652 ..Default::default()
1653 },
1654 );
1655 let executor = executor.execute_inner();
1656 pin_mut!(executor);
1657
1658 tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
1659 assert!(matches!(
1660 executor.next().await.unwrap().unwrap(),
1661 Message::Barrier(_)
1662 ));
1663
1664 tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
1666 assert!(matches!(
1667 executor.next().await.unwrap().unwrap(),
1668 Message::Barrier(_)
1669 ));
1670
1671 assert!(
1672 tokio::time::timeout(Duration::from_millis(20), executor.next())
1673 .await
1674 .is_err()
1675 );
1676 assert_eq!(reader_create_probe.mock_reader_create_count(), 1);
1677
1678 tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
1680 assert!(matches!(
1681 executor.next().await.unwrap().unwrap(),
1682 Message::Barrier(_)
1683 ));
1684
1685 let Message::Chunk(chunk) = executor.next().await.unwrap().unwrap() else {
1687 panic!("replacement reader should produce a snapshot chunk");
1688 };
1689 assert_eq!(chunk.cardinality(), 6);
1690 assert_eq!(reader_create_probe.mock_reader_create_count(), 2);
1691
1692 tx.send_barrier(Barrier::new_test_barrier(test_epoch(4)));
1693 assert!(matches!(
1694 executor.next().await.unwrap().unwrap(),
1695 Message::Barrier(_)
1696 ));
1697 }
1698
1699 #[tokio::test]
1700 async fn test_snapshot_read_error_before_first_chunk_survives_recovery() {
1701 let scan_options = || CdcScanOptions {
1702 snapshot_barrier_interval: 1,
1703 ..Default::default()
1704 };
1705
1706 let memory_state_store = MemoryStateStore::new();
1707
1708 {
1711 let (tx, executor) = create_mock_cdc_backfill(
1712 create_mock_external_table().with_mock_snapshot_errors([1]),
1713 create_cdc_state_table(memory_state_store.clone()).await,
1714 scan_options(),
1715 );
1716 let executor = executor.execute_inner();
1717
1718 pin_mut!(executor);
1719
1720 for epoch in 1..=3 {
1721 tx.send_barrier(Barrier::new_test_barrier(test_epoch(epoch)));
1722 assert!(matches!(
1723 executor.next().await.unwrap().unwrap(),
1724 Message::Barrier(_)
1725 ));
1726 }
1727 }
1728
1729 let mut restored_state = CdcBackfillState::new(
1730 TableId::new(1234),
1731 create_cdc_state_table(memory_state_store.clone()).await,
1732 5,
1733 );
1734 restored_state
1735 .init_epoch(Barrier::new_test_barrier(test_epoch(4)).epoch)
1736 .await
1737 .unwrap();
1738
1739 let state = restored_state.restore_state().await.unwrap();
1741 assert!(!state.is_finished);
1742 assert_eq!(state.current_pk_pos, None);
1743 assert_eq!(state.last_cdc_offset, None);
1744 assert_eq!(state.row_count, 0);
1745
1746 let (tx, executor) = create_mock_cdc_backfill(
1749 create_mock_external_table(),
1750 create_cdc_state_table(memory_state_store).await,
1751 scan_options(),
1752 );
1753 let executor = executor.execute_inner();
1754
1755 pin_mut!(executor);
1756
1757 for epoch in 4..=5 {
1758 tx.send_barrier(Barrier::new_test_barrier(test_epoch(epoch)));
1759 assert!(matches!(
1760 executor.next().await.unwrap().unwrap(),
1761 Message::Barrier(_)
1762 ));
1763 }
1764 let Message::Chunk(chunk) = tokio::time::timeout(Duration::from_secs(5), executor.next())
1765 .await
1766 .expect("recovered backfill should read the snapshot")
1767 .unwrap()
1768 .unwrap()
1769 else {
1770 panic!("recovered reader should produce a snapshot chunk");
1771 };
1772
1773 assert_eq!(chunk.cardinality(), 6);
1774 }
1775
1776 #[tokio::test]
1777 async fn test_disabled_cdc_backfill_finishes_without_table_reader() {
1778 let actor_context = ActorContext::for_test(1);
1779 let external_storage_table = ExternalStorageTable::new(
1780 TableId::new(1234),
1781 SchemaTableName {
1782 schema_name: "public".to_owned(),
1783 table_name: "orders".to_owned(),
1784 },
1785 "db".to_owned(),
1786 ExternalTableConfig::default(),
1787 ExternalCdcTableType::Unspecified,
1788 Schema::new(vec![Field::with_name(DataType::Int64, "id")]),
1789 vec![OrderType::ascending()],
1790 Some(vec![CdcKeyComparison::Native]),
1791 vec![0],
1792 );
1793 let schema = Schema::new(vec![
1794 Field::unnamed(DataType::Jsonb),
1795 Field::unnamed(DataType::Varchar),
1796 Field::unnamed(DataType::Varchar),
1797 ]);
1798 let stream_key = vec![1];
1799 let (tx, source) = MockSource::channel();
1800 let source = source.into_executor(schema, stream_key);
1801 let output_columns = vec![ColumnDesc::named("id", ColumnId::new(1), DataType::Int64)];
1802 let memory_state_store = MemoryStateStore::new();
1803 let state_table = create_cdc_state_table(memory_state_store.clone()).await;
1804 let cdc = CdcBackfillExecutor::new(
1805 actor_context,
1806 external_storage_table,
1807 source,
1808 vec![0],
1809 output_columns,
1810 None,
1811 StreamingMetrics::unused().into(),
1812 state_table,
1813 None,
1814 CdcScanOptions {
1815 disable_backfill: true,
1816 ..CdcScanOptions::default()
1817 },
1818 BTreeMap::default(),
1819 );
1820 let executor = cdc.execute_inner();
1821 pin_mut!(executor);
1822
1823 tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
1824 assert!(matches!(
1825 executor.next().await.unwrap().unwrap(),
1826 Message::Barrier(_)
1827 ));
1828
1829 tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
1830 assert!(matches!(
1831 executor.next().await.unwrap().unwrap(),
1832 Message::Barrier(_)
1833 ));
1834
1835 let mut restored_state = CdcBackfillState::new(
1836 TableId::new(1234),
1837 create_cdc_state_table(memory_state_store).await,
1838 5,
1839 );
1840 restored_state
1841 .init_epoch(Barrier::new_test_barrier(test_epoch(2)).epoch)
1842 .await
1843 .unwrap();
1844 let state = restored_state.restore_state().await.unwrap();
1845 assert!(state.is_finished);
1846 assert_eq!(state.last_cdc_offset, None);
1847 }
1848
1849 #[tokio::test]
1850 async fn test_unfinished_cdc_backfill_rejects_null_offset_on_restore() {
1851 let memory_state_store = MemoryStateStore::new();
1852 let mut state_writer = CdcBackfillState::new(
1853 TableId::new(1234),
1854 create_cdc_state_table(memory_state_store.clone()).await,
1855 5,
1856 );
1857 state_writer
1858 .init_epoch(Barrier::new_test_barrier(test_epoch(1)).epoch)
1859 .await
1860 .unwrap();
1861 state_writer
1862 .mutate_and_commit_state(
1863 Some(OwnedRow::new(vec![Some(ScalarImpl::Int64(10))])),
1864 None,
1865 10,
1866 false,
1867 Barrier::new_test_barrier(test_epoch(2)).epoch,
1868 )
1869 .await
1870 .unwrap();
1871
1872 let mut restored_state = CdcBackfillState::new(
1873 TableId::new(1234),
1874 create_cdc_state_table(memory_state_store).await,
1875 5,
1876 );
1877 restored_state
1878 .init_epoch(Barrier::new_test_barrier(test_epoch(2)).epoch)
1879 .await
1880 .unwrap();
1881
1882 assert!(restored_state.restore_state().await.is_err());
1883 }
1884
1885 fn create_raw_cdc_chunk(rows: &[(&str, &str)]) -> StreamChunk {
1886 let schema = Schema::new(vec![
1887 Field::unnamed(DataType::Jsonb),
1888 Field::unnamed(DataType::Varchar),
1889 ]);
1890 let mut builders = schema.create_array_builders(rows.len());
1891 for (payload, offset) in rows {
1892 let payload_datum: Datum = Some(JsonbVal::from_str(payload).unwrap().into());
1893 let offset_datum: Datum = Some((*offset).into());
1894 builders[0].append(payload_datum);
1895 builders[1].append(offset_datum);
1896 }
1897 let columns = builders
1898 .into_iter()
1899 .map(|builder| builder.finish().into())
1900 .collect();
1901 StreamChunk::from_parts(
1902 vec![Op::Insert; rows.len()],
1903 DataChunk::new(columns, rows.len()),
1904 )
1905 }
1906
1907 fn create_mock_external_table() -> ExternalStorageTable {
1908 ExternalStorageTable::new(
1909 TableId::new(1234),
1910 SchemaTableName {
1911 schema_name: "public".to_owned(),
1912 table_name: "mock_table".to_owned(),
1913 },
1914 "mydb".to_owned(),
1915 ExternalTableConfig::default(),
1916 ExternalCdcTableType::Mock,
1917 Schema::new(vec![
1918 Field::with_name(DataType::Int64, "id"),
1919 Field::with_name(DataType::Float64, "price"),
1920 ]),
1921 vec![OrderType::ascending()],
1922 Some(vec![CdcKeyComparison::Native]),
1923 vec![0],
1924 )
1925 }
1926
1927 fn create_mock_cdc_backfill(
1930 external_table: ExternalStorageTable,
1931 state_table: StateTable<MemoryStateStore>,
1932 scan_options: CdcScanOptions,
1933 ) -> (MessageSender, CdcBackfillExecutor<MemoryStateStore>) {
1934 let (tx, source) = MockSource::channel();
1935 let source = source.into_executor(
1936 Schema::new(vec![
1937 Field::unnamed(DataType::Jsonb),
1938 Field::unnamed(DataType::Varchar),
1939 ]),
1940 vec![0],
1941 );
1942 let executor = CdcBackfillExecutor::new(
1943 ActorContext::for_test(0x1a),
1944 external_table,
1945 source,
1946 vec![0, 1],
1947 vec![
1948 ColumnDesc::named("id", ColumnId::new(1), DataType::Int64),
1949 ColumnDesc::named("price", ColumnId::new(2), DataType::Float64),
1950 ],
1951 None,
1952 StreamingMetrics::unused().into(),
1953 state_table,
1954 None,
1955 scan_options,
1956 BTreeMap::default(),
1957 );
1958 (tx, executor)
1959 }
1960
1961 async fn create_cdc_state_table(store: MemoryStateStore) -> StateTable<MemoryStateStore> {
1962 let state_schema = Schema::new(vec![
1963 Field::with_name(DataType::Varchar, "split_id"),
1964 Field::with_name(DataType::Int64, "id"),
1965 Field::with_name(DataType::Boolean, "backfill_finished"),
1966 Field::with_name(DataType::Int64, "row_count"),
1967 Field::with_name(DataType::Jsonb, "cdc_offset"),
1968 ]);
1969 let column_descs = vec![
1970 ColumnDesc::unnamed(ColumnId::from(0), state_schema[0].data_type.clone()),
1971 ColumnDesc::unnamed(ColumnId::from(1), state_schema[1].data_type.clone()),
1972 ColumnDesc::unnamed(ColumnId::from(2), state_schema[2].data_type.clone()),
1973 ColumnDesc::unnamed(ColumnId::from(3), state_schema[3].data_type.clone()),
1974 ColumnDesc::unnamed(ColumnId::from(4), state_schema[4].data_type.clone()),
1975 ];
1976
1977 StateTable::from_table_catalog(
1978 &gen_pbtable(
1979 TableId::from(0x42),
1980 column_descs,
1981 vec![OrderType::ascending()],
1982 vec![0],
1983 0,
1984 ),
1985 store,
1986 None,
1987 )
1988 .await
1989 }
1990
1991 async fn create_recovering_cdc_backfill() -> (
1992 MessageSender,
1993 CdcBackfillExecutor<MemoryStateStore>,
1994 MemoryStateStore,
1995 ) {
1996 let memory_state_store = MemoryStateStore::new();
1997 let mut state_writer = CdcBackfillState::new(
1998 TableId::new(1234),
1999 create_cdc_state_table(memory_state_store.clone()).await,
2000 5,
2001 );
2002 state_writer
2003 .init_epoch(Barrier::new_test_barrier(test_epoch(1)).epoch)
2004 .await
2005 .unwrap();
2006 state_writer
2007 .mutate_and_commit_state(
2008 Some(OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
2009 Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2010 5,
2011 false,
2012 Barrier::new_test_barrier(test_epoch(2)).epoch,
2013 )
2014 .await
2015 .unwrap();
2016
2017 let (tx, executor) = create_mock_cdc_backfill(
2018 create_mock_external_table(),
2019 create_cdc_state_table(memory_state_store.clone()).await,
2020 CdcScanOptions::default(),
2021 );
2022 (tx, executor, memory_state_store)
2023 }
2024
2025 async fn assert_recovery_offset(
2026 memory_state_store: MemoryStateStore,
2027 expected_position: u64,
2028 epoch: u64,
2029 ) {
2030 let mut restored_state = CdcBackfillState::new(
2031 TableId::new(1234),
2032 create_cdc_state_table(memory_state_store).await,
2033 5,
2034 );
2035 restored_state
2036 .init_epoch(Barrier::new_test_barrier(epoch).epoch)
2037 .await
2038 .unwrap();
2039 let state = restored_state.restore_state().await.unwrap();
2040 assert_eq!(
2041 state.last_cdc_offset,
2042 Some(CdcOffset::Mysql(MysqlOffset::new(
2043 "1.binlog".to_owned(),
2044 expected_position,
2045 )))
2046 );
2047 }
2048
2049 #[tokio::test]
2050 async fn test_recovery_filters_upstream_while_reader_is_pending() {
2051 let chunk = StreamChunk::from_rows(
2052 &[
2053 (
2054 Op::Insert,
2055 OwnedRow::new(vec![
2056 Some(ScalarImpl::Int64(4)),
2057 Some(ScalarImpl::Float64(44.04.into())),
2058 Some(ScalarImpl::Utf8(
2059 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
2060 .into(),
2061 )),
2062 ]),
2063 ),
2064 (
2065 Op::Insert,
2066 OwnedRow::new(vec![
2067 Some(ScalarImpl::Int64(6)),
2068 Some(ScalarImpl::Float64(66.06.into())),
2069 Some(ScalarImpl::Utf8(
2070 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2071 .into(),
2072 )),
2073 ]),
2074 ),
2075 ],
2076 &[DataType::Int64, DataType::Float64, DataType::Varchar],
2077 );
2078 let mut upstream = stream::iter([
2079 Ok(Message::Chunk(chunk)),
2080 Ok(Message::Barrier(Barrier::new_test_barrier(test_epoch(4)))),
2081 ]);
2082 let mut reader_future = Box::pin(std::future::pending::<ExternalTableReaderImpl>());
2083
2084 let Either::Left(Message::Chunk(chunk)) =
2085 build_reader_and_poll_upstream(&mut upstream, &mut reader_future)
2086 .await
2087 .unwrap()
2088 else {
2089 panic!("expected an upstream chunk while the reader is pending");
2090 };
2091
2092 let parser = ExternalCdcTableType::Mock.get_cdc_offset_parser().unwrap();
2093 let (chunk, consumed_offset) =
2094 CdcBackfillExecutor::<MemoryStateStore>::filter_recovery_chunk(
2095 &parser,
2096 chunk,
2097 &OwnedRow::new(vec![Some(ScalarImpl::Int64(5))]),
2098 PkCompareInfo {
2099 indices: &[0],
2100 order: &[OrderType::ascending()],
2101 needs_unsigned_i64_compare: &[false],
2102 },
2103 &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2104 &[0, 1],
2105 )
2106 .unwrap();
2107 let chunk = chunk.expect("the scanned row should be emitted");
2108 assert_eq!(chunk.cardinality(), 1);
2109 assert_eq!(
2110 chunk.row_at(0).1.to_owned_row()[0],
2111 Some(ScalarImpl::Int64(4))
2112 );
2113 assert_eq!(
2114 consumed_offset,
2115 Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 4)))
2116 );
2117 assert!(matches!(
2118 build_reader_and_poll_upstream(&mut upstream, &mut reader_future)
2119 .await
2120 .unwrap(),
2121 Either::Left(Message::Barrier(_))
2122 ));
2123 }
2124
2125 #[tokio::test]
2126 async fn test_recovery_filters_updates_before_backfill_barrier() {
2127 let (mut tx, executor, memory_state_store) = create_recovering_cdc_backfill().await;
2128 let executor = executor.execute_inner();
2129 pin_mut!(executor);
2130
2131 tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
2132 assert!(matches!(
2133 executor.next().await.unwrap().unwrap(),
2134 Message::Barrier(_)
2135 ));
2136
2137 tx.push_chunk(create_raw_cdc_chunk(&[
2138 (
2139 r#"{ "payload": { "before": { "id": 1, "price": 11.01 }, "after": { "id": 1, "price": 12.01 }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002" }, "op": "u", "ts_ms": 1695277757017, "transaction": null } }"#,
2140 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
2141 ),
2142 (
2143 r#"{ "payload": { "before": { "id": 6, "price": 66.06 }, "after": { "id": 6, "price": 67.06 }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002" }, "op": "u", "ts_ms": 1695277757017, "transaction": null } }"#,
2144 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#,
2145 ),
2146 ]));
2147 tx.send_barrier(Barrier::new_test_barrier(test_epoch(4)));
2148
2149 let Message::Chunk(chunk) = executor.next().await.unwrap().unwrap() else {
2150 panic!("expected the recovered update before the barrier");
2151 };
2152 let rows = chunk
2153 .rows()
2154 .map(|(op, row)| (op, row.to_owned_row()))
2155 .collect::<Vec<_>>();
2156 assert_eq!(rows.len(), 1);
2157 assert_eq!(rows[0].0, Op::Insert);
2158 assert_eq!(rows[0].1[0], Some(ScalarImpl::Int64(1)));
2159 assert!(matches!(
2160 executor.next().await.unwrap().unwrap(),
2161 Message::Barrier(_)
2162 ));
2163 assert_recovery_offset(memory_state_store, 4, test_epoch(4)).await;
2164 }
2165
2166 #[tokio::test]
2167 async fn test_recovery_preserves_scanned_inserts_and_deletes() {
2168 let (mut tx, executor, _) = create_recovering_cdc_backfill().await;
2169 let executor = executor.execute_inner();
2170 pin_mut!(executor);
2171
2172 tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
2173 assert!(matches!(
2174 executor.next().await.unwrap().unwrap(),
2175 Message::Barrier(_)
2176 ));
2177 tx.push_chunk(create_raw_cdc_chunk(&[(
2179 r#"{ "payload": { "before": null, "after": { "id": 3, "price": 30.01 }, "op": "c" } }"#,
2180 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":1},"isHeartbeat":false}"#,
2181 )]));
2182 tx.push_chunk(create_raw_cdc_chunk(&[
2183 (
2184 r#"{ "payload": { "before": null, "after": { "id": 3, "price": 31.01 }, "op": "c" } }"#,
2185 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":1},"isHeartbeat":false}"#,
2186 ),
2187 (
2188 r#"{ "payload": { "before": null, "after": { "id": 4, "price": 44.04 }, "op": "c" } }"#,
2189 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":2},"isHeartbeat":false}"#,
2190 ),
2191 (
2192 r#"{ "payload": { "before": { "id": 5, "price": 55.05 }, "after": null, "op": "d" } }"#,
2193 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
2194 ),
2195 ]));
2196 tx.send_barrier(Barrier::new_test_barrier(test_epoch(4)));
2197
2198 let Message::Chunk(chunk) = executor.next().await.unwrap().unwrap() else {
2199 panic!("expected the scanned-prefix changes before the barrier");
2200 };
2201 let rows = chunk
2202 .rows()
2203 .map(|(op, row)| (op, row.to_owned_row()))
2204 .collect::<Vec<_>>();
2205 assert_eq!(rows.len(), 2);
2206 assert_eq!(rows[0].0, Op::Insert);
2207 assert_eq!(rows[0].1[0], Some(ScalarImpl::Int64(4)));
2208 assert_eq!(rows[1].0, Op::Delete);
2209 assert_eq!(rows[1].1[0], Some(ScalarImpl::Int64(5)));
2210 assert!(matches!(
2211 executor.next().await.unwrap().unwrap(),
2212 Message::Barrier(_)
2213 ));
2214 }
2215
2216 #[tokio::test]
2217 async fn test_recovery_does_not_leave_deleted_unscanned_row() {
2218 let (mut tx, mut executor, memory_state_store) = create_recovering_cdc_backfill().await;
2219 executor.options.snapshot_barrier_interval = 1;
2220 let executor = executor.execute_inner();
2221 pin_mut!(executor);
2222
2223 tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
2224 assert!(matches!(
2225 executor.next().await.unwrap().unwrap(),
2226 Message::Barrier(_)
2227 ));
2228
2229 tx.push_chunk(create_raw_cdc_chunk(&[(
2231 r#"{ "payload": { "before": null, "after": { "id": 100, "price": 100.01 }, "op": "c" } }"#,
2232 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
2233 )]));
2234 tx.send_barrier(Barrier::new_test_barrier(test_epoch(4)));
2235
2236 tx.push_chunk(create_raw_cdc_chunk(&[(
2239 r#"{ "payload": { "before": { "id": 100, "price": 100.01 }, "after": null, "op": "d" } }"#,
2240 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#,
2241 )]));
2242 tx.send_barrier(Barrier::new_test_barrier(test_epoch(5)));
2243 tx.send_barrier(Barrier::new_test_barrier(test_epoch(6)));
2245
2246 let mut materialized = BTreeMap::new();
2247 loop {
2248 match executor.next().await.unwrap().unwrap() {
2249 Message::Chunk(chunk) => {
2250 for (op, row) in chunk.rows() {
2251 let pk = row.datum_at(0).unwrap().into_int64();
2252 match op {
2253 Op::Insert => {
2254 materialized.insert(pk, row.to_owned_row());
2255 }
2256 Op::Delete => {
2257 materialized.remove(&pk);
2258 }
2259 _ => unreachable!("CDC updates are converted to inserts"),
2260 }
2261 }
2262 }
2263 Message::Barrier(barrier) if barrier.epoch.curr == test_epoch(6) => break,
2264 Message::Barrier(_) => {}
2265 Message::Watermark(_) => panic!("unexpected watermark during backfill"),
2266 }
2267 }
2268
2269 assert!(!materialized.contains_key(&100));
2270 let mut restored_state = CdcBackfillState::new(
2271 TableId::new(1234),
2272 create_cdc_state_table(memory_state_store).await,
2273 5,
2274 );
2275 restored_state
2276 .init_epoch(Barrier::new_test_barrier(test_epoch(6)).epoch)
2277 .await
2278 .unwrap();
2279 assert!(restored_state.restore_state().await.unwrap().is_finished);
2280 }
2281
2282 #[test]
2283 fn test_consume_upstream_chunk_buffer_retains_future_rows() {
2284 let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
2285 &[
2286 (
2287 Op::Insert,
2288 OwnedRow::new(vec![
2289 Some(ScalarImpl::Int64(1)),
2290 Some(ScalarImpl::Int64(100)),
2291 Some(ScalarImpl::Utf8(
2292 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
2293 .into(),
2294 )),
2295 ]),
2296 ),
2297 (
2298 Op::Insert,
2299 OwnedRow::new(vec![
2300 Some(ScalarImpl::Int64(6)),
2301 Some(ScalarImpl::Int64(600)),
2302 Some(ScalarImpl::Utf8(
2303 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2304 .into(),
2305 )),
2306 ]),
2307 ),
2308 ],
2309 &[DataType::Int64, DataType::Int64, DataType::Varchar],
2310 )];
2311
2312 let (emitted_chunks, drained_row_count, drained_offset) =
2313 CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
2314 &MockExternalTableReader::get_cdc_offset_parser(),
2315 &mut upstream_chunk_buffer,
2316 Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
2317 PkCompareInfo {
2318 indices: &[0],
2319 order: &[OrderType::ascending()],
2320 needs_unsigned_i64_compare: &[false],
2321 },
2322 &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2323 &[0, 1],
2324 )
2325 .unwrap();
2326
2327 assert_eq!(drained_row_count, 1);
2328 assert_eq!(
2329 drained_offset,
2330 Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 3)))
2331 );
2332 assert_eq!(emitted_chunks.len(), 1);
2333 assert_eq!(emitted_chunks[0].rows().count(), 1);
2334 assert_eq!(
2335 emitted_chunks[0].rows().next().unwrap().1.to_owned_row(),
2336 OwnedRow::new(vec![
2337 Some(ScalarImpl::Int64(1)),
2338 Some(ScalarImpl::Int64(100))
2339 ])
2340 );
2341
2342 assert_eq!(upstream_chunk_buffer.len(), 1);
2343 assert_eq!(upstream_chunk_buffer[0].rows().count(), 1);
2344 assert_eq!(
2345 upstream_chunk_buffer[0]
2346 .rows()
2347 .next()
2348 .unwrap()
2349 .1
2350 .to_owned_row(),
2351 OwnedRow::new(vec![
2352 Some(ScalarImpl::Int64(6)),
2353 Some(ScalarImpl::Int64(600)),
2354 Some(ScalarImpl::Utf8(
2355 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2356 .into(),
2357 )),
2358 ])
2359 );
2360 }
2361
2362 #[test]
2363 fn test_consume_buffer_uses_unsigned_bigint_pk_order() {
2364 let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
2365 &[
2366 (
2367 Op::Insert,
2368 OwnedRow::new(vec![
2369 Some(ScalarImpl::Int64(4)),
2370 Some(ScalarImpl::Int64(400)),
2371 Some(ScalarImpl::Utf8(
2372 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
2373 .into(),
2374 )),
2375 ]),
2376 ),
2377 (
2378 Op::Insert,
2379 OwnedRow::new(vec![
2380 Some(ScalarImpl::Int64(-1)),
2382 Some(ScalarImpl::Int64(900)),
2383 Some(ScalarImpl::Utf8(
2384 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2385 .into(),
2386 )),
2387 ]),
2388 ),
2389 ],
2390 &[DataType::Int64, DataType::Int64, DataType::Varchar],
2391 )];
2392
2393 let (emitted_chunks, drained_row_count, drained_offset) =
2394 CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
2395 &MockExternalTableReader::get_cdc_offset_parser(),
2396 &mut upstream_chunk_buffer,
2397 Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
2398 PkCompareInfo {
2399 indices: &[0],
2400 order: &[OrderType::ascending()],
2401 needs_unsigned_i64_compare: &[true],
2402 },
2403 &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2404 &[0, 1],
2405 )
2406 .unwrap();
2407
2408 assert_eq!(drained_row_count, 1);
2409 assert_eq!(
2410 drained_offset,
2411 Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 3)))
2412 );
2413 assert_eq!(emitted_chunks.len(), 1);
2414 assert_eq!(
2415 emitted_chunks[0].rows().next().unwrap().1.to_owned_row(),
2416 OwnedRow::new(vec![
2417 Some(ScalarImpl::Int64(4)),
2418 Some(ScalarImpl::Int64(400))
2419 ])
2420 );
2421
2422 assert_eq!(upstream_chunk_buffer.len(), 1);
2423 assert_eq!(
2424 upstream_chunk_buffer[0]
2425 .rows()
2426 .next()
2427 .unwrap()
2428 .1
2429 .to_owned_row(),
2430 OwnedRow::new(vec![
2431 Some(ScalarImpl::Int64(-1)),
2432 Some(ScalarImpl::Int64(900)),
2433 Some(ScalarImpl::Utf8(
2434 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2435 .into(),
2436 )),
2437 ])
2438 );
2439 }
2440
2441 #[test]
2442 fn test_consume_buffer_non_monotonic_pk_in_chunk() {
2443 let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
2444 &[
2445 (
2446 Op::Insert,
2447 OwnedRow::new(vec![
2448 Some(ScalarImpl::Int64(1)),
2449 Some(ScalarImpl::Int64(100)),
2450 Some(ScalarImpl::Utf8(
2451 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
2452 .into(),
2453 )),
2454 ]),
2455 ),
2456 (
2457 Op::Insert,
2458 OwnedRow::new(vec![
2459 Some(ScalarImpl::Int64(6)),
2460 Some(ScalarImpl::Int64(600)),
2461 Some(ScalarImpl::Utf8(
2462 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2463 .into(),
2464 )),
2465 ]),
2466 ),
2467 (
2468 Op::Insert,
2469 OwnedRow::new(vec![
2470 Some(ScalarImpl::Int64(2)),
2471 Some(ScalarImpl::Int64(200)),
2472 Some(ScalarImpl::Utf8(
2473 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
2474 .into(),
2475 )),
2476 ]),
2477 ),
2478 ],
2479 &[DataType::Int64, DataType::Int64, DataType::Varchar],
2480 )];
2481
2482 let (emitted_chunks, drained_row_count, drained_offset) =
2483 CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
2484 &MockExternalTableReader::get_cdc_offset_parser(),
2485 &mut upstream_chunk_buffer,
2486 Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
2487 PkCompareInfo {
2488 indices: &[0],
2489 order: &[OrderType::ascending()],
2490 needs_unsigned_i64_compare: &[false],
2491 },
2492 &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2493 &[0, 1],
2494 )
2495 .unwrap();
2496
2497 assert_eq!(drained_row_count, 2);
2498 assert_eq!(
2499 drained_offset,
2500 Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 3)))
2501 );
2502 assert_eq!(emitted_chunks.len(), 1);
2503 assert_eq!(emitted_chunks[0].rows().count(), 2);
2504 assert_eq!(
2505 emitted_chunks[0]
2506 .rows()
2507 .map(|(_, row)| row.to_owned_row())
2508 .collect::<Vec<_>>(),
2509 vec![
2510 OwnedRow::new(vec![
2511 Some(ScalarImpl::Int64(1)),
2512 Some(ScalarImpl::Int64(100))
2513 ]),
2514 OwnedRow::new(vec![
2515 Some(ScalarImpl::Int64(2)),
2516 Some(ScalarImpl::Int64(200))
2517 ]),
2518 ]
2519 );
2520
2521 assert_eq!(upstream_chunk_buffer.len(), 1);
2522 assert_eq!(upstream_chunk_buffer[0].rows().count(), 1);
2523 assert_eq!(
2524 upstream_chunk_buffer[0]
2525 .rows()
2526 .next()
2527 .unwrap()
2528 .1
2529 .to_owned_row(),
2530 OwnedRow::new(vec![
2531 Some(ScalarImpl::Int64(6)),
2532 Some(ScalarImpl::Int64(600)),
2533 Some(ScalarImpl::Utf8(
2534 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2535 .into(),
2536 )),
2537 ])
2538 );
2539 }
2540
2541 #[test]
2542 fn test_consume_buffer_processes_chunks_after_future_row() {
2543 let mut upstream_chunk_buffer = vec![
2544 StreamChunk::from_rows(
2545 &[
2546 (
2547 Op::Insert,
2548 OwnedRow::new(vec![
2549 Some(ScalarImpl::Int64(1)),
2550 Some(ScalarImpl::Int64(100)),
2551 Some(ScalarImpl::Utf8(
2552 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
2553 .into(),
2554 )),
2555 ]),
2556 ),
2557 (
2558 Op::Insert,
2559 OwnedRow::new(vec![
2560 Some(ScalarImpl::Int64(6)),
2561 Some(ScalarImpl::Int64(600)),
2562 Some(ScalarImpl::Utf8(
2563 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2564 .into(),
2565 )),
2566 ]),
2567 ),
2568 ],
2569 &[DataType::Int64, DataType::Int64, DataType::Varchar],
2570 ),
2571 StreamChunk::from_rows(
2572 &[(
2573 Op::Insert,
2574 OwnedRow::new(vec![
2575 Some(ScalarImpl::Int64(2)),
2576 Some(ScalarImpl::Int64(200)),
2577 Some(ScalarImpl::Utf8(
2578 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
2579 .into(),
2580 )),
2581 ]),
2582 )],
2583 &[DataType::Int64, DataType::Int64, DataType::Varchar],
2584 ),
2585 ];
2586
2587 let (emitted_chunks, drained_row_count, drained_offset) =
2588 CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
2589 &MockExternalTableReader::get_cdc_offset_parser(),
2590 &mut upstream_chunk_buffer,
2591 Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
2592 PkCompareInfo {
2593 indices: &[0],
2594 order: &[OrderType::ascending()],
2595 needs_unsigned_i64_compare: &[false],
2596 },
2597 &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2598 &[0, 1],
2599 )
2600 .unwrap();
2601
2602 assert_eq!(drained_row_count, 2);
2603 assert_eq!(
2604 drained_offset,
2605 Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 3)))
2606 );
2607 assert_eq!(emitted_chunks.len(), 2);
2608 assert_eq!(emitted_chunks[0].rows().count(), 1);
2609 assert_eq!(emitted_chunks[1].rows().count(), 1);
2610 assert_eq!(
2611 emitted_chunks[1].rows().next().unwrap().1.to_owned_row(),
2612 OwnedRow::new(vec![
2613 Some(ScalarImpl::Int64(2)),
2614 Some(ScalarImpl::Int64(200))
2615 ])
2616 );
2617
2618 assert_eq!(upstream_chunk_buffer.len(), 1);
2619 assert_eq!(
2620 upstream_chunk_buffer[0]
2621 .rows()
2622 .next()
2623 .unwrap()
2624 .1
2625 .to_owned_row(),
2626 OwnedRow::new(vec![
2627 Some(ScalarImpl::Int64(6)),
2628 Some(ScalarImpl::Int64(600)),
2629 Some(ScalarImpl::Utf8(
2630 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2631 .into(),
2632 )),
2633 ])
2634 );
2635 }
2636
2637 #[test]
2638 fn test_consume_buffer_advances_offset_after_preceding_rows_are_emitted() {
2639 let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
2640 &[
2641 (
2642 Op::Insert,
2643 OwnedRow::new(vec![
2644 Some(ScalarImpl::Int64(6)),
2645 Some(ScalarImpl::Int64(600)),
2646 Some(ScalarImpl::Utf8(
2647 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2648 .into(),
2649 )),
2650 ]),
2651 ),
2652 (
2653 Op::Insert,
2654 OwnedRow::new(vec![
2655 Some(ScalarImpl::Int64(2)),
2656 Some(ScalarImpl::Int64(200)),
2657 Some(ScalarImpl::Utf8(
2658 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
2659 .into(),
2660 )),
2661 ]),
2662 ),
2663 ],
2664 &[DataType::Int64, DataType::Int64, DataType::Varchar],
2665 )];
2666
2667 let (_, drained_row_count, drained_offset) =
2668 CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
2669 &MockExternalTableReader::get_cdc_offset_parser(),
2670 &mut upstream_chunk_buffer,
2671 Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(6))])),
2672 PkCompareInfo {
2673 indices: &[0],
2674 order: &[OrderType::ascending()],
2675 needs_unsigned_i64_compare: &[false],
2676 },
2677 &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 3))),
2678 &[0, 1],
2679 )
2680 .unwrap();
2681
2682 assert_eq!(drained_row_count, 2);
2683 assert_eq!(
2684 drained_offset,
2685 Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 5)))
2686 );
2687 assert!(upstream_chunk_buffer.is_empty());
2688 }
2689
2690 #[tokio::test]
2691 async fn test_cdc_backfill_persists_buffered_offset_on_checkpoint() {
2692 let memory_state_store = MemoryStateStore::new();
2693 let state_table = create_cdc_state_table(memory_state_store.clone()).await;
2694
2695 let (mut tx, executor) = create_mock_cdc_backfill(
2696 create_mock_external_table(),
2697 state_table,
2698 CdcScanOptions {
2699 snapshot_barrier_interval: 10,
2700 ..Default::default()
2701 },
2702 );
2703 let executor = executor.execute_inner();
2704 pin_mut!(executor);
2705
2706 tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
2707 assert!(matches!(
2708 executor.next().await.unwrap().unwrap(),
2709 Message::Barrier(_)
2710 ));
2711
2712 tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
2713 assert!(matches!(
2714 executor.next().await.unwrap().unwrap(),
2715 Message::Barrier(_)
2716 ));
2717
2718 assert!(matches!(
2719 executor.next().await.unwrap().unwrap(),
2720 Message::Chunk(_)
2721 ));
2722
2723 tx.push_chunk(create_raw_cdc_chunk(&[
2724 (
2725 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 } }"#,
2726 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
2727 ),
2728 (
2729 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 } }"#,
2730 r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#,
2731 ),
2732 ]));
2733 tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
2734
2735 assert!(matches!(
2736 executor.next().await.unwrap().unwrap(),
2737 Message::Chunk(_)
2738 ));
2739 assert!(matches!(
2740 executor.next().await.unwrap().unwrap(),
2741 Message::Barrier(_)
2742 ));
2743
2744 let mut restored_state = CdcBackfillState::new(
2745 TableId::new(1234),
2746 create_cdc_state_table(memory_state_store).await,
2747 5,
2748 );
2749 restored_state
2750 .init_epoch(Barrier::new_test_barrier(test_epoch(3)).epoch)
2751 .await
2752 .unwrap();
2753 let state = restored_state.restore_state().await.unwrap();
2754 assert_eq!(
2755 state.last_cdc_offset,
2756 Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 4)))
2757 );
2758 }
2759}