Skip to main content

risingwave_meta/barrier/
progress.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// Represents the backfill nodes that need to be scheduled or cleaned up.
53#[derive(Debug, Default)]
54pub(super) struct PendingBackfillFragments {
55    /// Fragment IDs that should start backfilling in the next checkpoint
56    pub next_backfill_nodes: Vec<FragmentId>,
57    /// State tables of locality provider fragments that should be truncated
58    pub truncate_locality_provider_state_tables: Vec<TableId>,
59}
60
61/// Progress of all actors containing backfill executors while creating mview.
62#[derive(Debug)]
63pub(super) struct Progress {
64    job_id: JobId,
65    // `states` and `done_count` decides whether the progress is done. See `is_done`.
66    states: HashMap<ActorId, BackfillState>,
67    backfill_order_state: BackfillOrderState,
68    done_count: usize,
69
70    /// Tells whether the backfill is from source or mv.
71    backfill_upstream_types: HashMap<ActorId, BackfillUpstreamType>,
72
73    // The following row counts are used to calculate the progress. See `calculate_progress`.
74    /// Upstream mv count.
75    /// Keep track of how many times each upstream MV
76    /// appears in this stream job.
77    upstream_mv_count: HashMap<TableId, usize>,
78    /// Total key count of all the upstream materialized views
79    upstream_mvs_total_key_count: u64,
80    mv_backfill_consumed_rows: u64,
81    source_backfill_consumed_rows: u64,
82    /// Buffered rows (for locality backfill) that are yet to be consumed
83    /// This is used to calculate precise progress: consumed / (`upstream_total` + buffered)
84    mv_backfill_buffered_rows: u64,
85}
86
87impl Progress {
88    /// Create a [`Progress`] for some creating mview, with all `actors` containing the backfill executors.
89    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    /// Update the progress of `actor`.
119    /// Returns the backfill fragments that need to be scheduled or cleaned up.
120    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                // last_backfill_nodes = before_backfill_nodes - after_backfill_nodes
169                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                // do not consider progress for values
205            }
206            BackfillUpstreamType::LocalityProvider => {
207                // Track LocalityProvider progress similar to MView
208                // Update buffered rows for precise progress calculation
209                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    /// Returns whether all backfill executors are done.
235    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    /// `progress` = `consumed_rows` / `upstream_total_key_count`
246    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, /* Count LocalityProvider as an MView for progress */
258            }
259        }
260
261        let mv_progress = (mv_count > 0).then_some({
262            // Include buffered rows in total for precise progress calculation
263            // Progress = consumed / (upstream_total + buffered)
264            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
300/// There are two kinds of `TrackingJobs`:
301/// 1. if `is_recovered` is false, it is a "New" tracking job.
302///    It is instantiated and managed by the stream manager.
303///    On recovery, the stream manager will stop managing the job.
304/// 2. if `is_recovered` is true, it is a "Recovered" tracking job.
305///    On recovery, the barrier manager will recover and start managing the job.
306pub 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    /// Create a new tracking job.
325    pub(crate) fn new(stream_job_fragments: &StreamJobFragments) -> Self {
326        let finished_backfill_fragments = stream_job_fragments.source_backfill_fragments();
327        // `None` when empty, consistent with the recovered-job constructor.
328        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    /// Create a recovered tracking job.
343    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    /// Notify the metadata manager that the job is finished.
380    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/// Information collected during barrier completion that needs to be committed.
407#[derive(Debug, Default)]
408pub(super) struct StagingCommitInfo {
409    /// Finished jobs that should be committed
410    pub finished_jobs: Vec<TrackingJob>,
411    /// Table IDs whose locality provider state tables need to be truncated
412    pub table_ids_to_truncate: Vec<TableId>,
413    pub finished_cdc_table_backfill: Vec<JobId>,
414}
415
416pub(super) enum UpdateProgressResult {
417    None,
418    /// The finished job, along with its pending backfill fragments for cleanup.
419    Finished {
420        truncate_locality_provider_state_tables: Vec<TableId>,
421    },
422    /// Backfill nodes have finished and new ones need to be scheduled.
423    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 of the create-mview DDL.
436        progress: Progress,
437
438        /// Stash of pending backfill nodes. They will start backfilling on checkpoint.
439        pending_backfill_nodes: Vec<FragmentId>,
440
441        /// Table IDs whose locality provider state tables need to be truncated
442        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    /// ## How recovery works
498    ///
499    /// The progress (number of rows consumed) is persisted in state tables.
500    /// During recovery, the backfill executor will restore the number of rows consumed,
501    /// and then it will just report progress like newly created executors.
502    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, // Fill only after first barrier pass
518            upstream_mv_count,
519            upstream_mvs_total_key_count,
520            mv_backfill_consumed_rows: 0, // Fill only after first barrier pass
521            source_backfill_consumed_rows: 0, // Fill only after first barrier pass
522            mv_backfill_buffered_rows: 0, // Fill only after first barrier pass
523        }
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    /// Update the progress of tracked jobs, and add a new job to track if `info` is `Some`.
544    /// Return the table ids whose locality provider state tables need to be truncated.
545    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            // Update the progress of all commands.
563            {
564                // Those with actors complete can be finished immediately.
565                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    /// Refresh tracker state after reschedule so new actors can report progress correctly.
595    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    /// Mark CDC source as finished when offset is updated.
702    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    /// Add a new create-mview DDL command to track.
737    ///
738    /// If the actors to track are empty, return the given command as it can be finished immediately.
739    /// For CDC sources, mark as `CdcSourceInit` instead of Finished.
740    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            // NOTE: This CDC source detection uses hardcoded property checks and should be replaced
758            // with a more reliable identification method in the future.
759            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                // Mark CDC source as CdcSourceInit, will be finished when offset is updated
770                return Self {
771                    tracking_job,
772                    status: CreateMviewStatus::CdcSourceInit,
773                };
774            }
775            // The command can be finished immediately.
776            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    /// Update the progress of `actor` according to the Pb struct.
816    ///
817    /// If all actors in this MV have finished, return the command.
818    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    // CDC sources should be initialized as CdcSourceInit
1085    #[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        // Create a CDC source with cdc_source_job = true
1097        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        // Create empty fragments (no actors to track)
1110        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        // CDC source should be in CdcSourceInit state
1157        assert!(matches!(tracker.status, CreateMviewStatus::CdcSourceInit));
1158        assert!(!tracker.is_finished());
1159    }
1160
1161    // CDC source should transition from CdcSourceInit to Finished when offset is updated
1162    #[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        // Initially in CdcSourceInit state
1174        assert!(matches!(tracker.status, CreateMviewStatus::CdcSourceInit));
1175        assert!(!tracker.is_finished());
1176
1177        // Mark as finished when offset is updated
1178        tracker.mark_cdc_source_finished();
1179
1180        // Should now be in Finished state
1181        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}