1use std::collections::HashMap;
16use std::mem::take;
17
18use risingwave_common::catalog::{FragmentTypeFlag, TableId};
19use risingwave_common::id::JobId;
20use risingwave_common::util::epoch::Epoch;
21use risingwave_pb::hummock::HummockVersionStats;
22use risingwave_pb::stream_plan::StreamNode;
23use risingwave_pb::stream_service::barrier_complete_response::CreateMviewProgress;
24
25use crate::MetaResult;
26use crate::barrier::backfill_order_control::BackfillOrderState;
27use crate::barrier::info::InflightStreamingJobInfo;
28use crate::barrier::{CreateStreamingJobCommandInfo, FragmentBackfillProgress};
29use crate::controller::fragment::InflightFragmentInfo;
30use crate::manager::MetadataManager;
31use crate::model::{ActorId, BackfillUpstreamType, FragmentId, StreamJobFragments};
32use crate::stream::{SourceChange, SourceManagerRef};
33
34type ConsumedRows = u64;
35type BufferedRows = u64;
36
37#[derive(Debug, Clone, Copy)]
38pub(crate) struct ActorBackfillProgress {
39 pub(crate) actor_id: ActorId,
40 pub(crate) upstream_type: BackfillUpstreamType,
41 pub(crate) consumed_rows: u64,
42 pub(crate) done: bool,
43}
44
45#[derive(Clone, Copy, Debug)]
46enum BackfillState {
47 Init,
48 ConsumingUpstream(#[expect(dead_code)] Epoch, ConsumedRows, BufferedRows),
49 Done(ConsumedRows, BufferedRows),
50}
51
52#[derive(Debug, Default)]
54pub(super) struct PendingBackfillFragments {
55 pub next_backfill_nodes: Vec<FragmentId>,
57 pub truncate_locality_provider_state_tables: Vec<TableId>,
59}
60
61#[derive(Debug)]
63pub(super) struct Progress {
64 job_id: JobId,
65 states: HashMap<ActorId, BackfillState>,
67 backfill_order_state: BackfillOrderState,
68 done_count: usize,
69
70 backfill_upstream_types: HashMap<ActorId, BackfillUpstreamType>,
72
73 upstream_mv_count: HashMap<TableId, usize>,
78 upstream_mvs_total_key_count: u64,
80 mv_backfill_consumed_rows: u64,
81 source_backfill_consumed_rows: u64,
82 mv_backfill_buffered_rows: u64,
85}
86
87impl Progress {
88 fn new(
90 job_id: JobId,
91 actors: impl IntoIterator<Item = (ActorId, BackfillUpstreamType)>,
92 upstream_mv_count: HashMap<TableId, usize>,
93 upstream_total_key_count: u64,
94 backfill_order_state: BackfillOrderState,
95 ) -> Self {
96 let mut states = HashMap::new();
97 let mut backfill_upstream_types = HashMap::new();
98 for (actor, backfill_upstream_type) in actors {
99 states.insert(actor, BackfillState::Init);
100 backfill_upstream_types.insert(actor, backfill_upstream_type);
101 }
102 assert!(!states.is_empty());
103
104 Self {
105 job_id,
106 states,
107 backfill_upstream_types,
108 done_count: 0,
109 upstream_mv_count,
110 upstream_mvs_total_key_count: upstream_total_key_count,
111 mv_backfill_consumed_rows: 0,
112 source_backfill_consumed_rows: 0,
113 mv_backfill_buffered_rows: 0,
114 backfill_order_state,
115 }
116 }
117
118 fn update(
121 &mut self,
122 actor: ActorId,
123 new_state: BackfillState,
124 upstream_total_key_count: u64,
125 ) -> PendingBackfillFragments {
126 let mut result = PendingBackfillFragments::default();
127 self.upstream_mvs_total_key_count = upstream_total_key_count;
128 let total_actors = self.states.len();
129 let Some(backfill_upstream_type) = self.backfill_upstream_types.get(&actor) else {
130 tracing::warn!(%actor, "receive progress from unknown actor, likely removed after reschedule");
131 return result;
132 };
133
134 let mut old_consumed_row = 0;
135 let mut new_consumed_row = 0;
136 let mut old_buffered_row = 0;
137 let mut new_buffered_row = 0;
138 let Some(prev_state) = self.states.remove(&actor) else {
139 tracing::warn!(%actor, "receive progress for actor not in state map");
140 return result;
141 };
142 match prev_state {
143 BackfillState::Init => {}
144 BackfillState::ConsumingUpstream(_, consumed_rows, buffered_rows) => {
145 old_consumed_row = consumed_rows;
146 old_buffered_row = buffered_rows;
147 }
148 BackfillState::Done(_, _) => panic!("should not report done multiple times"),
149 };
150 match &new_state {
151 BackfillState::Init => {}
152 BackfillState::ConsumingUpstream(_, consumed_rows, buffered_rows) => {
153 new_consumed_row = *consumed_rows;
154 new_buffered_row = *buffered_rows;
155 }
156 BackfillState::Done(consumed_rows, buffered_rows) => {
157 tracing::debug!("actor {} done", actor);
158 new_consumed_row = *consumed_rows;
159 new_buffered_row = *buffered_rows;
160 self.done_count += 1;
161 let before_backfill_nodes = self
162 .backfill_order_state
163 .current_backfill_node_fragment_ids();
164 result.next_backfill_nodes = self.backfill_order_state.finish_actor(actor);
165 let after_backfill_nodes = self
166 .backfill_order_state
167 .current_backfill_node_fragment_ids();
168 let last_backfill_nodes_iter = before_backfill_nodes
170 .into_iter()
171 .filter(|x| !after_backfill_nodes.contains(x));
172 result.truncate_locality_provider_state_tables = last_backfill_nodes_iter
173 .filter_map(|fragment_id| {
174 self.backfill_order_state
175 .get_locality_fragment_state_table_mapping()
176 .get(&fragment_id)
177 })
178 .flatten()
179 .copied()
180 .collect();
181 tracing::debug!(
182 "{} actors out of {} complete",
183 self.done_count,
184 total_actors,
185 );
186 }
187 };
188 debug_assert!(
189 new_consumed_row >= old_consumed_row,
190 "backfill progress should not go backward"
191 );
192 debug_assert!(
193 new_buffered_row >= old_buffered_row,
194 "backfill progress should not go backward"
195 );
196 match backfill_upstream_type {
197 BackfillUpstreamType::MView => {
198 self.mv_backfill_consumed_rows += new_consumed_row - old_consumed_row;
199 }
200 BackfillUpstreamType::Source => {
201 self.source_backfill_consumed_rows += new_consumed_row - old_consumed_row;
202 }
203 BackfillUpstreamType::Values => {
204 }
206 BackfillUpstreamType::LocalityProvider => {
207 self.mv_backfill_consumed_rows += new_consumed_row - old_consumed_row;
210 self.mv_backfill_buffered_rows += new_buffered_row - old_buffered_row;
211 }
212 }
213 self.states.insert(actor, new_state);
214 result
215 }
216
217 fn iter_actor_progress(&self) -> impl Iterator<Item = ActorBackfillProgress> + '_ {
218 self.states.iter().filter_map(|(actor_id, state)| {
219 let upstream_type = *self.backfill_upstream_types.get(actor_id)?;
220 let (consumed_rows, done) = match *state {
221 BackfillState::Init => (0, false),
222 BackfillState::ConsumingUpstream(_, consumed_rows, _) => (consumed_rows, false),
223 BackfillState::Done(consumed_rows, _) => (consumed_rows, true),
224 };
225 Some(ActorBackfillProgress {
226 actor_id: *actor_id,
227 upstream_type,
228 consumed_rows,
229 done,
230 })
231 })
232 }
233
234 fn is_done(&self) -> bool {
236 tracing::trace!(
237 "Progress::is_done? {}, {}, {:?}",
238 self.done_count,
239 self.states.len(),
240 self.states
241 );
242 self.done_count == self.states.len()
243 }
244
245 fn calculate_progress(&self) -> String {
247 if self.is_done() || self.states.is_empty() {
248 return "100%".to_owned();
249 }
250 let mut mv_count = 0;
251 let mut source_count = 0;
252 for backfill_upstream_type in self.backfill_upstream_types.values() {
253 match backfill_upstream_type {
254 BackfillUpstreamType::MView => mv_count += 1,
255 BackfillUpstreamType::Source => source_count += 1,
256 BackfillUpstreamType::Values => (),
257 BackfillUpstreamType::LocalityProvider => mv_count += 1, }
259 }
260
261 let mv_progress = (mv_count > 0).then_some({
262 let total_rows_to_consume =
265 self.upstream_mvs_total_key_count + self.mv_backfill_buffered_rows;
266 if total_rows_to_consume == 0 {
267 "99.99%".to_owned()
268 } else {
269 let mut progress =
270 self.mv_backfill_consumed_rows as f64 / (total_rows_to_consume as f64);
271 if progress > 1.0 {
272 progress = 0.9999;
273 }
274 format!(
275 "{:.2}% ({}/{})",
276 progress * 100.0,
277 self.mv_backfill_consumed_rows,
278 total_rows_to_consume
279 )
280 }
281 });
282 let source_progress = (source_count > 0).then_some(format!(
283 "{} rows consumed",
284 self.source_backfill_consumed_rows
285 ));
286 match (mv_progress, source_progress) {
287 (Some(mv_progress), Some(source_progress)) => {
288 format!(
289 "MView Backfill: {}, Source Backfill: {}",
290 mv_progress, source_progress
291 )
292 }
293 (Some(mv_progress), None) => mv_progress,
294 (None, Some(source_progress)) => source_progress,
295 (None, None) => "Unknown".to_owned(),
296 }
297 }
298}
299
300pub struct TrackingJob {
307 job_id: JobId,
308 is_recovered: bool,
309 source_change: Option<SourceChange>,
310}
311
312impl std::fmt::Display for TrackingJob {
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 write!(
315 f,
316 "{}{}",
317 self.job_id,
318 if self.is_recovered { "<recovered>" } else { "" }
319 )
320 }
321}
322
323impl TrackingJob {
324 pub(crate) fn new(stream_job_fragments: &StreamJobFragments) -> Self {
326 let finished_backfill_fragments = stream_job_fragments.source_backfill_fragments();
327 let source_change = if finished_backfill_fragments.is_empty() {
329 None
330 } else {
331 Some(SourceChange::CreateJobFinished {
332 finished_backfill_fragments,
333 })
334 };
335 Self {
336 job_id: stream_job_fragments.stream_job_id,
337 is_recovered: false,
338 source_change,
339 }
340 }
341
342 pub(crate) fn recovered(
344 job_id: JobId,
345 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
346 ) -> Self {
347 Self::recovered_from_fragment_nodes(
348 job_id,
349 fragment_infos
350 .iter()
351 .map(|(fragment_id, fragment)| (*fragment_id, &fragment.nodes)),
352 )
353 }
354
355 pub(crate) fn recovered_from_fragment_nodes<'a>(
356 job_id: JobId,
357 fragment_nodes: impl Iterator<Item = (FragmentId, &'a StreamNode)>,
358 ) -> Self {
359 let source_backfill_fragments =
360 StreamJobFragments::source_backfill_fragments_impl(fragment_nodes);
361 let source_change = if source_backfill_fragments.is_empty() {
362 None
363 } else {
364 Some(SourceChange::CreateJobFinished {
365 finished_backfill_fragments: source_backfill_fragments,
366 })
367 };
368 Self {
369 job_id,
370 is_recovered: true,
371 source_change,
372 }
373 }
374
375 pub(crate) fn job_id(&self) -> JobId {
376 self.job_id
377 }
378
379 pub(crate) async fn finish(
381 self,
382 metadata_manager: &MetadataManager,
383 source_manager: &SourceManagerRef,
384 ) -> MetaResult<()> {
385 metadata_manager
386 .catalog_controller
387 .finish_streaming_job(self.job_id)
388 .await?;
389 if let Some(source_change) = self.source_change {
390 source_manager.apply_source_change(source_change).await;
391 }
392 Ok(())
393 }
394}
395
396impl std::fmt::Debug for TrackingJob {
397 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398 if !self.is_recovered {
399 write!(f, "TrackingJob::New({})", self.job_id)
400 } else {
401 write!(f, "TrackingJob::Recovered({})", self.job_id)
402 }
403 }
404}
405
406#[derive(Debug, Default)]
408pub(super) struct StagingCommitInfo {
409 pub finished_jobs: Vec<TrackingJob>,
411 pub table_ids_to_truncate: Vec<TableId>,
413 pub finished_cdc_table_backfill: Vec<JobId>,
414}
415
416pub(super) enum UpdateProgressResult {
417 None,
418 Finished {
420 truncate_locality_provider_state_tables: Vec<TableId>,
421 },
422 BackfillNodeFinished(PendingBackfillFragments),
424}
425
426#[derive(Debug)]
427pub(super) struct CreateMviewProgressTracker {
428 tracking_job: TrackingJob,
429 status: CreateMviewStatus,
430}
431
432#[derive(Debug)]
433enum CreateMviewStatus {
434 Backfilling {
435 progress: Progress,
437
438 pending_backfill_nodes: Vec<FragmentId>,
440
441 table_ids_to_truncate: Vec<TableId>,
443 },
444 CdcSourceInit,
445 Finished {
446 table_ids_to_truncate: Vec<TableId>,
447 },
448}
449
450impl CreateMviewProgressTracker {
451 pub fn recover(
452 creating_job_id: JobId,
453 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
454 backfill_order_state: BackfillOrderState,
455 version_stats: &HummockVersionStats,
456 ) -> Self {
457 let tracking_job = TrackingJob::recovered(creating_job_id, fragment_infos);
458 let actors = InflightStreamingJobInfo::tracking_progress_actor_ids(fragment_infos);
459 let status = if actors.is_empty() {
460 CreateMviewStatus::Finished {
461 table_ids_to_truncate: vec![],
462 }
463 } else {
464 let mut states = HashMap::new();
465 let mut backfill_upstream_types = HashMap::new();
466
467 for (actor, backfill_upstream_type) in actors {
468 states.insert(actor, BackfillState::ConsumingUpstream(Epoch(0), 0, 0));
469 backfill_upstream_types.insert(actor, backfill_upstream_type);
470 }
471
472 let progress = Self::recover_progress(
473 creating_job_id,
474 states,
475 backfill_upstream_types,
476 StreamJobFragments::upstream_table_counts_impl(
477 fragment_infos.values().map(|fragment| &fragment.nodes),
478 ),
479 version_stats,
480 backfill_order_state,
481 );
482 let pending_backfill_nodes = progress
483 .backfill_order_state
484 .current_backfill_node_fragment_ids();
485 CreateMviewStatus::Backfilling {
486 progress,
487 pending_backfill_nodes,
488 table_ids_to_truncate: vec![],
489 }
490 };
491 Self {
492 tracking_job,
493 status,
494 }
495 }
496
497 fn recover_progress(
503 job_id: JobId,
504 states: HashMap<ActorId, BackfillState>,
505 backfill_upstream_types: HashMap<ActorId, BackfillUpstreamType>,
506 upstream_mv_count: HashMap<TableId, usize>,
507 version_stats: &HummockVersionStats,
508 backfill_order_state: BackfillOrderState,
509 ) -> Progress {
510 let upstream_mvs_total_key_count =
511 calculate_total_key_count(&upstream_mv_count, version_stats);
512 Progress {
513 job_id,
514 states,
515 backfill_order_state,
516 backfill_upstream_types,
517 done_count: 0, upstream_mv_count,
519 upstream_mvs_total_key_count,
520 mv_backfill_consumed_rows: 0, source_backfill_consumed_rows: 0, mv_backfill_buffered_rows: 0, }
524 }
525
526 pub fn gen_backfill_progress(&self) -> String {
527 match &self.status {
528 CreateMviewStatus::Backfilling { progress, .. } => progress.calculate_progress(),
529 CreateMviewStatus::CdcSourceInit => "Initializing CDC source...".to_owned(),
530 CreateMviewStatus::Finished { .. } => "100%".to_owned(),
531 }
532 }
533
534 pub(crate) fn actor_progresses(&self) -> Vec<ActorBackfillProgress> {
535 match &self.status {
536 CreateMviewStatus::Backfilling { progress, .. } => {
537 progress.iter_actor_progress().collect()
538 }
539 CreateMviewStatus::CdcSourceInit | CreateMviewStatus::Finished { .. } => vec![],
540 }
541 }
542
543 pub(super) fn apply_progress(
546 &mut self,
547 create_mview_progress: &CreateMviewProgress,
548 version_stats: &HummockVersionStats,
549 ) {
550 let CreateMviewStatus::Backfilling {
551 progress,
552 pending_backfill_nodes,
553 table_ids_to_truncate,
554 } = &mut self.status
555 else {
556 tracing::warn!(
557 "update the progress of an backfill finished streaming job: {create_mview_progress:?}"
558 );
559 return;
560 };
561 {
562 {
564 match progress.apply(create_mview_progress, version_stats) {
566 UpdateProgressResult::None => {
567 tracing::trace!(?progress, "update progress");
568 }
569 UpdateProgressResult::Finished {
570 truncate_locality_provider_state_tables,
571 } => {
572 let mut table_ids_to_truncate = take(table_ids_to_truncate);
573 table_ids_to_truncate.extend(truncate_locality_provider_state_tables);
574 tracing::trace!(?progress, "finish progress");
575 self.status = CreateMviewStatus::Finished {
576 table_ids_to_truncate,
577 };
578 }
579 UpdateProgressResult::BackfillNodeFinished(pending) => {
580 table_ids_to_truncate
581 .extend(pending.truncate_locality_provider_state_tables.clone());
582 tracing::trace!(
583 ?progress,
584 next_backfill_nodes = ?pending.next_backfill_nodes,
585 "start next backfill node"
586 );
587 pending_backfill_nodes.extend(pending.next_backfill_nodes);
588 }
589 }
590 }
591 }
592 }
593
594 pub fn refresh_after_reschedule(
596 &mut self,
597 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
598 version_stats: &HummockVersionStats,
599 ) {
600 let CreateMviewStatus::Backfilling {
601 progress,
602 pending_backfill_nodes,
603 ..
604 } = &mut self.status
605 else {
606 return;
607 };
608
609 let new_tracking_actors = StreamJobFragments::tracking_progress_actor_ids_impl(
610 fragment_infos
611 .values()
612 .map(|fragment| (fragment.fragment_type_mask, fragment.actors.keys().copied())),
613 );
614
615 #[cfg(debug_assertions)]
616 {
617 use std::collections::HashSet;
618 let old_actor_ids: HashSet<_> = progress.states.keys().copied().collect();
619 let new_actor_ids: HashSet<_> = new_tracking_actors
620 .iter()
621 .map(|(actor_id, _)| *actor_id)
622 .collect();
623 debug_assert!(
624 old_actor_ids.is_disjoint(&new_actor_ids),
625 "reschedule should rebuild backfill actors; old={old_actor_ids:?}, new={new_actor_ids:?}"
626 );
627 }
628
629 let mut new_states = HashMap::new();
630 let mut new_backfill_types = HashMap::new();
631 for (actor_id, upstream_type) in new_tracking_actors {
632 new_states.insert(actor_id, BackfillState::Init);
633 new_backfill_types.insert(actor_id, upstream_type);
634 }
635
636 let fragment_actors: HashMap<_, _> = fragment_infos
637 .iter()
638 .map(|(fragment_id, info)| (*fragment_id, info.actors.keys().copied().collect()))
639 .collect();
640
641 let newly_scheduled = progress
642 .backfill_order_state
643 .refresh_actors(&fragment_actors);
644
645 progress.backfill_upstream_types = new_backfill_types;
646 progress.states = new_states;
647 progress.done_count = 0;
648
649 progress.upstream_mv_count = StreamJobFragments::upstream_table_counts_impl(
650 fragment_infos.values().map(|fragment| &fragment.nodes),
651 );
652 progress.upstream_mvs_total_key_count =
653 calculate_total_key_count(&progress.upstream_mv_count, version_stats);
654
655 progress.mv_backfill_consumed_rows = 0;
656 progress.source_backfill_consumed_rows = 0;
657 progress.mv_backfill_buffered_rows = 0;
658
659 let mut pending = progress
660 .backfill_order_state
661 .current_backfill_node_fragment_ids();
662 pending.extend(newly_scheduled);
663 pending.sort_unstable();
664 pending.dedup();
665 *pending_backfill_nodes = pending;
666 }
667
668 pub(super) fn take_pending_backfill_nodes(&mut self) -> impl Iterator<Item = FragmentId> + '_ {
669 match &mut self.status {
670 CreateMviewStatus::Backfilling {
671 pending_backfill_nodes,
672 ..
673 } => Some(pending_backfill_nodes.drain(..)),
674 CreateMviewStatus::CdcSourceInit => None,
675 CreateMviewStatus::Finished { .. } => None,
676 }
677 .into_iter()
678 .flatten()
679 }
680
681 pub(super) fn collect_staging_commit_info(
682 &mut self,
683 ) -> (bool, Box<dyn Iterator<Item = TableId> + '_>) {
684 match &mut self.status {
685 CreateMviewStatus::Backfilling {
686 table_ids_to_truncate,
687 ..
688 } => (false, Box::new(table_ids_to_truncate.drain(..))),
689 CreateMviewStatus::CdcSourceInit => (false, Box::new(std::iter::empty())),
690 CreateMviewStatus::Finished {
691 table_ids_to_truncate,
692 ..
693 } => (true, Box::new(table_ids_to_truncate.drain(..))),
694 }
695 }
696
697 pub(super) fn is_finished(&self) -> bool {
698 matches!(self.status, CreateMviewStatus::Finished { .. })
699 }
700
701 pub(super) fn mark_cdc_source_finished(&mut self) {
703 if matches!(self.status, CreateMviewStatus::CdcSourceInit) {
704 self.status = CreateMviewStatus::Finished {
705 table_ids_to_truncate: vec![],
706 };
707 }
708 }
709
710 pub(super) fn into_tracking_job(self) -> TrackingJob {
711 let CreateMviewStatus::Finished { .. } = self.status else {
712 panic!("should be called when finished");
713 };
714 self.tracking_job
715 }
716
717 pub(crate) fn job_id(&self) -> JobId {
718 self.tracking_job.job_id
719 }
720
721 pub(crate) fn collect_fragment_progress(
722 &self,
723 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
724 mark_done_when_empty: bool,
725 ) -> Vec<FragmentBackfillProgress> {
726 let actor_progresses = self.actor_progresses();
727 if actor_progresses.is_empty() {
728 if mark_done_when_empty && self.is_finished() {
729 return collect_done_fragments(self.job_id(), fragment_infos);
730 }
731 return vec![];
732 }
733 collect_fragment_progress_from_actors(self.job_id(), fragment_infos, &actor_progresses)
734 }
735
736 pub fn new(
741 info: &CreateStreamingJobCommandInfo,
742 version_stats: &HummockVersionStats,
743 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
744 ) -> Self {
745 tracing::trace!(?info, "add job to track");
746 let CreateStreamingJobCommandInfo {
747 stream_job_fragments,
748 fragment_backfill_ordering,
749 locality_fragment_state_table_mapping,
750 streaming_job,
751 ..
752 } = info;
753 let job_id = stream_job_fragments.stream_job_id();
754 let actors = InflightStreamingJobInfo::tracking_progress_actor_ids(fragment_infos);
755 let tracking_job = TrackingJob::new(&info.stream_job_fragments);
756 if actors.is_empty() {
757 let is_cdc_source = matches!(
760 streaming_job,
761 crate::manager::StreamingJob::Source(source)
762 if source.info.as_ref().map(|info| info.is_shared()).unwrap_or(false) && source
763 .get_with_properties()
764 .get("connector")
765 .map(|connector| connector.to_lowercase().contains("-cdc"))
766 .unwrap_or(false)
767 );
768 if is_cdc_source {
769 return Self {
771 tracking_job,
772 status: CreateMviewStatus::CdcSourceInit,
773 };
774 }
775 return Self {
777 tracking_job,
778 status: CreateMviewStatus::Finished {
779 table_ids_to_truncate: vec![],
780 },
781 };
782 }
783
784 let upstream_mv_count = stream_job_fragments.upstream_table_counts();
785 let upstream_total_key_count: u64 =
786 calculate_total_key_count(&upstream_mv_count, version_stats);
787
788 let backfill_order_state = BackfillOrderState::new(
789 fragment_backfill_ordering,
790 fragment_infos,
791 locality_fragment_state_table_mapping.clone(),
792 );
793 let progress = Progress::new(
794 job_id,
795 actors,
796 upstream_mv_count,
797 upstream_total_key_count,
798 backfill_order_state,
799 );
800 let pending_backfill_nodes = progress
801 .backfill_order_state
802 .current_backfill_node_fragment_ids();
803 Self {
804 tracking_job,
805 status: CreateMviewStatus::Backfilling {
806 progress,
807 pending_backfill_nodes,
808 table_ids_to_truncate: vec![],
809 },
810 }
811 }
812}
813
814impl Progress {
815 fn apply(
819 &mut self,
820 progress: &CreateMviewProgress,
821 version_stats: &HummockVersionStats,
822 ) -> UpdateProgressResult {
823 tracing::trace!(?progress, "update progress");
824 let actor = progress.backfill_actor_id;
825 let job_id = self.job_id;
826
827 let new_state = if progress.done {
828 BackfillState::Done(progress.consumed_rows, progress.buffered_rows)
829 } else {
830 BackfillState::ConsumingUpstream(
831 progress.consumed_epoch.into(),
832 progress.consumed_rows,
833 progress.buffered_rows,
834 )
835 };
836
837 {
838 {
839 let progress_state = self;
840
841 let upstream_total_key_count: u64 =
842 calculate_total_key_count(&progress_state.upstream_mv_count, version_stats);
843
844 tracing::trace!(%job_id, "updating progress for table");
845 let pending = progress_state.update(actor, new_state, upstream_total_key_count);
846
847 if progress_state.is_done() {
848 tracing::debug!(
849 %job_id,
850 "all actors done for creating mview!",
851 );
852
853 let PendingBackfillFragments {
854 next_backfill_nodes,
855 truncate_locality_provider_state_tables,
856 } = pending;
857
858 assert!(next_backfill_nodes.is_empty());
859 UpdateProgressResult::Finished {
860 truncate_locality_provider_state_tables,
861 }
862 } else if !pending.next_backfill_nodes.is_empty()
863 || !pending.truncate_locality_provider_state_tables.is_empty()
864 {
865 UpdateProgressResult::BackfillNodeFinished(pending)
866 } else {
867 UpdateProgressResult::None
868 }
869 }
870 }
871 }
872}
873
874fn calculate_total_key_count(
875 table_count: &HashMap<TableId, usize>,
876 version_stats: &HummockVersionStats,
877) -> u64 {
878 table_count
879 .iter()
880 .map(|(table_id, count)| {
881 assert_ne!(*count, 0);
882 *count as u64
883 * version_stats
884 .table_stats
885 .get(table_id)
886 .map_or(0, |stat| stat.total_key_count as u64)
887 })
888 .sum()
889}
890
891pub(crate) fn collect_fragment_progress_from_actors(
892 job_id: JobId,
893 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
894 actor_progresses: &[ActorBackfillProgress],
895) -> Vec<FragmentBackfillProgress> {
896 let mut actor_to_fragment = HashMap::new();
897 for (fragment_id, info) in fragment_infos {
898 for actor_id in info.actors.keys() {
899 actor_to_fragment.insert(*actor_id, *fragment_id);
900 }
901 }
902
903 let mut per_fragment: HashMap<FragmentId, (u64, usize, usize, BackfillUpstreamType)> =
904 HashMap::new();
905 for progress in actor_progresses {
906 let Some(fragment_id) = actor_to_fragment.get(&progress.actor_id) else {
907 continue;
908 };
909 let entry = per_fragment
910 .entry(*fragment_id)
911 .or_insert((0, 0, 0, progress.upstream_type));
912 entry.0 = entry.0.saturating_add(progress.consumed_rows);
913 entry.1 += progress.done as usize;
914 entry.2 += 1;
915 }
916
917 per_fragment
918 .into_iter()
919 .map(
920 |(fragment_id, (consumed_rows, done_cnt, total_cnt, upstream_type))| {
921 FragmentBackfillProgress {
922 job_id,
923 fragment_id,
924 consumed_rows,
925 done: total_cnt > 0 && done_cnt == total_cnt,
926 upstream_type,
927 }
928 },
929 )
930 .collect()
931}
932
933pub(crate) fn collect_done_fragments(
934 job_id: JobId,
935 fragment_infos: &HashMap<FragmentId, InflightFragmentInfo>,
936) -> Vec<FragmentBackfillProgress> {
937 fragment_infos
938 .iter()
939 .filter(|(_, fragment)| {
940 fragment.fragment_type_mask.contains_any([
941 FragmentTypeFlag::StreamScan,
942 FragmentTypeFlag::SourceScan,
943 FragmentTypeFlag::LocalityProvider,
944 ])
945 })
946 .map(|(fragment_id, fragment)| FragmentBackfillProgress {
947 job_id,
948 fragment_id: *fragment_id,
949 consumed_rows: 0,
950 done: true,
951 upstream_type: BackfillUpstreamType::from_fragment_type_mask(
952 fragment.fragment_type_mask,
953 ),
954 })
955 .collect()
956}
957
958#[cfg(test)]
959mod tests {
960 use std::collections::HashSet;
961
962 use risingwave_common::catalog::{FragmentTypeFlag, FragmentTypeMask};
963 use risingwave_common::id::WorkerId;
964 use risingwave_meta_model::fragment::DistributionType;
965 use risingwave_pb::stream_plan::StreamNode as PbStreamNode;
966
967 use super::*;
968 use crate::controller::fragment::InflightActorInfo;
969
970 fn sample_inflight_fragment(
971 fragment_id: FragmentId,
972 actor_ids: &[ActorId],
973 flag: FragmentTypeFlag,
974 ) -> InflightFragmentInfo {
975 let mut fragment_type_mask = FragmentTypeMask::empty();
976 fragment_type_mask.add(flag);
977 InflightFragmentInfo {
978 fragment_id,
979 distribution_type: DistributionType::Single,
980 fragment_type_mask,
981 vnode_count: 0,
982 nodes: PbStreamNode::default(),
983 actors: actor_ids
984 .iter()
985 .map(|actor_id| {
986 (
987 *actor_id,
988 InflightActorInfo {
989 worker_id: WorkerId::new(1),
990 vnode_bitmap: None,
991 splits: vec![],
992 },
993 )
994 })
995 .collect(),
996 state_table_ids: HashSet::new(),
997 }
998 }
999
1000 fn sample_progress(actor_id: ActorId) -> Progress {
1001 Progress {
1002 job_id: JobId::new(1),
1003 states: HashMap::from([(actor_id, BackfillState::Init)]),
1004 backfill_order_state: BackfillOrderState::default(),
1005 done_count: 0,
1006 backfill_upstream_types: HashMap::from([(actor_id, BackfillUpstreamType::MView)]),
1007 upstream_mv_count: HashMap::new(),
1008 upstream_mvs_total_key_count: 0,
1009 mv_backfill_consumed_rows: 0,
1010 source_backfill_consumed_rows: 0,
1011 mv_backfill_buffered_rows: 0,
1012 }
1013 }
1014
1015 #[test]
1016 fn update_ignores_unknown_actor() {
1017 let actor_known = ActorId::new(1);
1018 let actor_unknown = ActorId::new(2);
1019 let mut progress = sample_progress(actor_known);
1020
1021 let pending = progress.update(
1022 actor_unknown,
1023 BackfillState::Done(0, 0),
1024 progress.upstream_mvs_total_key_count,
1025 );
1026
1027 assert!(pending.next_backfill_nodes.is_empty());
1028 assert_eq!(progress.states.len(), 1);
1029 assert!(progress.states.contains_key(&actor_known));
1030 }
1031
1032 #[test]
1033 fn refresh_rebuilds_tracking_after_reschedule() {
1034 let actor_old = ActorId::new(1);
1035 let actor_new = ActorId::new(2);
1036
1037 let progress = Progress {
1038 job_id: JobId::new(1),
1039 states: HashMap::from([(actor_old, BackfillState::Done(5, 0))]),
1040 backfill_order_state: BackfillOrderState::default(),
1041 done_count: 1,
1042 backfill_upstream_types: HashMap::from([(actor_old, BackfillUpstreamType::MView)]),
1043 upstream_mv_count: HashMap::new(),
1044 upstream_mvs_total_key_count: 0,
1045 mv_backfill_consumed_rows: 5,
1046 source_backfill_consumed_rows: 0,
1047 mv_backfill_buffered_rows: 0,
1048 };
1049
1050 let mut tracker = CreateMviewProgressTracker {
1051 tracking_job: TrackingJob {
1052 job_id: JobId::new(1),
1053 is_recovered: false,
1054 source_change: None,
1055 },
1056 status: CreateMviewStatus::Backfilling {
1057 progress,
1058 pending_backfill_nodes: vec![],
1059 table_ids_to_truncate: vec![],
1060 },
1061 };
1062
1063 let fragment_infos = HashMap::from([(
1064 FragmentId::new(10),
1065 sample_inflight_fragment(
1066 FragmentId::new(10),
1067 &[actor_new],
1068 FragmentTypeFlag::StreamScan,
1069 ),
1070 )]);
1071
1072 tracker.refresh_after_reschedule(&fragment_infos, &HummockVersionStats::default());
1073
1074 let CreateMviewStatus::Backfilling { progress, .. } = tracker.status else {
1075 panic!("expected backfilling status");
1076 };
1077 assert!(progress.states.contains_key(&actor_new));
1078 assert!(!progress.states.contains_key(&actor_old));
1079 assert_eq!(progress.done_count, 0);
1080 assert_eq!(progress.mv_backfill_consumed_rows, 0);
1081 assert_eq!(progress.source_backfill_consumed_rows, 0);
1082 }
1083
1084 #[test]
1086 fn test_cdc_source_initialized_as_cdc_source_init() {
1087 use std::collections::BTreeMap;
1088
1089 use risingwave_meta_model::streaming_job;
1090 use risingwave_pb::catalog::{CreateType, PbSource, StreamSourceInfo};
1091
1092 use crate::barrier::command::CreateStreamingJobCommandInfo;
1093 use crate::manager::{StreamingJob, StreamingJobType};
1094 use crate::model::StreamJobFragmentsToCreate;
1095
1096 let source_info = StreamSourceInfo {
1098 cdc_source_job: true,
1099 ..Default::default()
1100 };
1101
1102 let source = PbSource {
1103 id: risingwave_common::id::SourceId::new(100),
1104 info: Some(source_info),
1105 with_properties: BTreeMap::from([("connector".to_owned(), "fake-cdc".to_owned())]),
1106 ..Default::default()
1107 };
1108
1109 let fragments = StreamJobFragments::for_test(JobId::new(100), BTreeMap::new());
1111 let stream_job_fragments = StreamJobFragmentsToCreate {
1112 inner: fragments,
1113 downstreams: Default::default(),
1114 };
1115
1116 let info = CreateStreamingJobCommandInfo {
1117 stream_job_fragments,
1118 upstream_fragment_downstreams: Default::default(),
1119 init_split_assignment: Default::default(),
1120 definition: "CREATE SOURCE ...".to_owned(),
1121 job_type: StreamingJobType::Source,
1122 create_type: CreateType::Foreground,
1123 streaming_job: StreamingJob::Source(source),
1124 database_resource_group: risingwave_common::util::worker_util::DEFAULT_RESOURCE_GROUP
1125 .to_owned(),
1126 fragment_backfill_ordering: Default::default(),
1127 cdc_table_snapshot_splits: None,
1128 locality_fragment_state_table_mapping: Default::default(),
1129 is_serverless: false,
1130 replace_sink: None,
1131 refresh_interval_sec: None,
1132 streaming_job_model: streaming_job::Model {
1133 job_id: JobId::new(100),
1134 job_status: risingwave_meta_model::JobStatus::Creating,
1135 create_type: risingwave_meta_model::CreateType::Foreground,
1136 timezone: None,
1137 config_override: None,
1138 adaptive_parallelism_strategy: None,
1139 parallelism: risingwave_meta_model::StreamingParallelism::Adaptive,
1140 backfill_parallelism: None,
1141 backfill_adaptive_parallelism_strategy: None,
1142 backfill_orders: None,
1143 max_parallelism: 256,
1144 specific_resource_group: None,
1145 is_serverless_backfill: false,
1146 refresh_interval_sec: None,
1147 },
1148 };
1149
1150 let tracker = CreateMviewProgressTracker::new(
1151 &info,
1152 &HummockVersionStats::default(),
1153 &HashMap::new(),
1154 );
1155
1156 assert!(matches!(tracker.status, CreateMviewStatus::CdcSourceInit));
1158 assert!(!tracker.is_finished());
1159 }
1160
1161 #[test]
1163 fn test_cdc_source_transitions_to_finished_on_offset_update() {
1164 let mut tracker = CreateMviewProgressTracker {
1165 tracking_job: TrackingJob {
1166 job_id: JobId::new(300),
1167 is_recovered: false,
1168 source_change: None,
1169 },
1170 status: CreateMviewStatus::CdcSourceInit,
1171 };
1172
1173 assert!(matches!(tracker.status, CreateMviewStatus::CdcSourceInit));
1175 assert!(!tracker.is_finished());
1176
1177 tracker.mark_cdc_source_finished();
1179
1180 assert!(matches!(tracker.status, CreateMviewStatus::Finished { .. }));
1182 assert!(tracker.is_finished());
1183 }
1184
1185 #[test]
1186 fn tracking_job_new_without_source_backfill_has_no_source_change() {
1187 use std::collections::BTreeMap;
1188
1189 let fragments = StreamJobFragments::for_test(JobId::new(1), BTreeMap::new());
1190 let job = TrackingJob::new(&fragments);
1191 assert!(job.source_change.is_none());
1192 }
1193
1194 #[test]
1195 fn tracking_job_new_with_source_backfill_tracks_finished_fragments() {
1196 use std::collections::{BTreeMap, BTreeSet};
1197
1198 use risingwave_common::id::SourceId;
1199 use risingwave_pb::stream_plan::stream_node::NodeBody;
1200 use risingwave_pb::stream_plan::{MergeNode, SourceBackfillNode};
1201
1202 use crate::model::Fragment;
1203
1204 let source_id = SourceId::new(42);
1205 let backfill_fragment_id = FragmentId::new(2);
1206 let upstream_source_fragment_id = FragmentId::new(1);
1207
1208 let nodes = PbStreamNode {
1209 node_body: Some(NodeBody::SourceBackfill(Box::new(SourceBackfillNode {
1210 upstream_source_id: source_id,
1211 ..Default::default()
1212 }))),
1213 input: vec![PbStreamNode {
1214 node_body: Some(NodeBody::Merge(Box::new(MergeNode {
1215 upstream_fragment_id: upstream_source_fragment_id,
1216 ..Default::default()
1217 }))),
1218 ..Default::default()
1219 }],
1220 ..Default::default()
1221 };
1222 let fragment = Fragment {
1223 fragment_id: backfill_fragment_id,
1224 nodes,
1225 ..Default::default()
1226 };
1227 let fragments = StreamJobFragments::for_test(
1228 JobId::new(1),
1229 BTreeMap::from([(backfill_fragment_id, fragment)]),
1230 );
1231
1232 let job = TrackingJob::new(&fragments);
1233 let Some(SourceChange::CreateJobFinished {
1234 finished_backfill_fragments,
1235 }) = job.source_change
1236 else {
1237 panic!("expected CreateJobFinished");
1238 };
1239 assert_eq!(
1240 finished_backfill_fragments,
1241 HashMap::from([(
1242 source_id,
1243 BTreeSet::from([(backfill_fragment_id, upstream_source_fragment_id)]),
1244 )])
1245 );
1246 }
1247}