Skip to main content

risingwave_meta/barrier/checkpoint/independent_job/creating_job/
status.rs

1// Copyright 2026 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::hash_map::Entry;
16use std::collections::{HashMap, HashSet, VecDeque};
17use std::mem::{replace, take};
18use std::time::Duration;
19
20use itertools::Itertools;
21use risingwave_common::hash::ActorId;
22use risingwave_common::util::epoch::Epoch;
23use risingwave_pb::hummock::HummockVersionStats;
24use risingwave_pb::id::{FragmentId, PartialGraphId};
25use risingwave_pb::stream_plan::StartFragmentBackfillMutation;
26use risingwave_pb::stream_plan::barrier::PbBarrierKind;
27use risingwave_pb::stream_plan::barrier_mutation::Mutation;
28use risingwave_pb::stream_service::barrier_complete_response::{
29    CreateMviewProgress, PbCreateMviewProgress,
30};
31use tracing::warn;
32
33use crate::barrier::checkpoint::independent_job::creating_job::CreatingJobInfo;
34use crate::barrier::command::{ThrottleConfigMap, extract_throttle_config};
35use crate::barrier::notifier::CollectionNotifier;
36use crate::barrier::partial_graph::PartialGraphManager;
37use crate::barrier::progress::{CreateMviewProgressTracker, TrackingJob};
38use crate::barrier::{BarrierInfo, BarrierKind, TracedEpoch};
39use crate::controller::fragment::InflightFragmentInfo;
40
41#[derive(Debug)]
42pub(super) struct CreateMviewLogStoreProgressTracker {
43    /// `actor_id` -> `pending_epoch_lag`
44    ongoing_actors: HashMap<ActorId, u64>,
45    finished_actors: HashSet<ActorId>,
46}
47
48impl CreateMviewLogStoreProgressTracker {
49    pub(super) fn new(actors: impl Iterator<Item = ActorId>, pending_barrier_lag: u64) -> Self {
50        Self {
51            ongoing_actors: HashMap::from_iter(actors.map(|actor| (actor, pending_barrier_lag))),
52            finished_actors: HashSet::new(),
53        }
54    }
55
56    pub(super) fn gen_backfill_progress(&self) -> String {
57        let sum = self.ongoing_actors.values().sum::<u64>() as f64;
58        let count = if self.ongoing_actors.is_empty() {
59            1
60        } else {
61            self.ongoing_actors.len()
62        } as f64;
63        let avg = sum / count;
64        let avg_lag_time = Duration::from_millis(Epoch(avg as _).physical_time());
65        format!(
66            "actor: {}/{}, avg lag {:?}",
67            self.finished_actors.len(),
68            self.ongoing_actors.len() + self.finished_actors.len(),
69            avg_lag_time
70        )
71    }
72
73    fn update(&mut self, progress: impl IntoIterator<Item = &PbCreateMviewProgress>) {
74        for progress in progress {
75            match self.ongoing_actors.entry(progress.backfill_actor_id) {
76                Entry::Occupied(mut entry) => {
77                    if progress.done {
78                        entry.remove_entry();
79                        assert!(
80                            self.finished_actors.insert(progress.backfill_actor_id),
81                            "non-duplicate"
82                        );
83                    } else {
84                        *entry.get_mut() = progress.pending_epoch_lag as _;
85                    }
86                }
87                Entry::Vacant(_) => {
88                    if cfg!(debug_assertions) {
89                        panic!(
90                            "reporting progress on non-inflight actor: {:?} {:?}",
91                            progress, self
92                        );
93                    } else {
94                        warn!(?progress, progress_tracker = ?self, "reporting progress on non-inflight actor");
95                    }
96                }
97            }
98        }
99    }
100
101    pub(super) fn is_finished(&self) -> bool {
102        self.ongoing_actors.is_empty()
103    }
104}
105
106#[derive(Debug)]
107pub(super) enum CreatingStreamingJobStatus {
108    /// The creating job is consuming upstream snapshot.
109    /// Will transit to `ConsumingLogStore` on `update_progress` when
110    /// the snapshot has been fully consumed after `update_progress`.
111    ConsumingSnapshot {
112        prev_epoch_fake_physical_time: u64,
113        pending_upstream_barriers: Vec<BarrierInfo>,
114        version_stats: HummockVersionStats,
115        create_mview_tracker: CreateMviewProgressTracker,
116        snapshot_backfill_actors: HashSet<ActorId>,
117        snapshot_epoch: u64,
118        info: CreatingJobInfo,
119        /// The `prev_epoch` of pending non checkpoint barriers
120        pending_non_checkpoint_barriers: Vec<u64>,
121    },
122    /// The creating job is consuming log store.
123    ///
124    /// Will transit to `Finishing` on `on_new_upstream_epoch` when `start_consume_upstream` is `true`.
125    ConsumingLogStore {
126        tracking_job: TrackingJob,
127        info: CreatingJobInfo,
128        log_store_progress_tracker: CreateMviewLogStoreProgressTracker,
129        pending_barriers: VecDeque<BarrierInfo>,
130    },
131    /// All backfill actors have started consuming upstream, and the job
132    /// will be finished when all previously injected barriers have been collected
133    /// Store the `prev_epoch` that will finish at.
134    Finishing(u64, TrackingJob),
135    Resetting(Vec<CollectionNotifier>),
136    PlaceHolder,
137}
138
139impl CreatingStreamingJobStatus {
140    pub(super) fn update_progress(
141        &mut self,
142        create_mview_progress: impl IntoIterator<Item = &CreateMviewProgress>,
143    ) {
144        match self {
145            &mut Self::ConsumingSnapshot {
146                ref mut create_mview_tracker,
147                ref version_stats,
148                ref mut prev_epoch_fake_physical_time,
149                ref mut pending_upstream_barriers,
150                ref mut pending_non_checkpoint_barriers,
151                ref snapshot_epoch,
152                ..
153            } => {
154                for progress in create_mview_progress {
155                    create_mview_tracker.apply_progress(progress, version_stats);
156                }
157                if create_mview_tracker.is_finished() {
158                    pending_non_checkpoint_barriers.push(*snapshot_epoch);
159
160                    let prev_epoch = Epoch::from_physical_time(*prev_epoch_fake_physical_time);
161                    let pending_barriers: VecDeque<_> = [BarrierInfo {
162                        curr_epoch: TracedEpoch::new(Epoch(*snapshot_epoch)),
163                        prev_epoch: TracedEpoch::new(prev_epoch),
164                        kind: BarrierKind::Checkpoint(take(pending_non_checkpoint_barriers)),
165                    }]
166                    .into_iter()
167                    .chain(pending_upstream_barriers.drain(..))
168                    .collect();
169
170                    let CreatingStreamingJobStatus::ConsumingSnapshot {
171                        create_mview_tracker,
172                        info,
173                        snapshot_epoch,
174                        snapshot_backfill_actors,
175                        ..
176                    } = replace(self, CreatingStreamingJobStatus::PlaceHolder)
177                    else {
178                        unreachable!()
179                    };
180
181                    let tracking_job = create_mview_tracker.into_tracking_job();
182
183                    *self = CreatingStreamingJobStatus::ConsumingLogStore {
184                        tracking_job,
185                        info,
186                        log_store_progress_tracker: CreateMviewLogStoreProgressTracker::new(
187                            snapshot_backfill_actors.iter().cloned(),
188                            pending_barriers
189                                .back()
190                                .map(|barrier_info| {
191                                    barrier_info.prev_epoch().saturating_sub(snapshot_epoch)
192                                })
193                                .unwrap_or(0),
194                        ),
195                        pending_barriers,
196                    };
197                }
198            }
199            CreatingStreamingJobStatus::ConsumingLogStore {
200                log_store_progress_tracker,
201                ..
202            } => {
203                log_store_progress_tracker.update(create_mview_progress);
204            }
205            CreatingStreamingJobStatus::Finishing(..)
206            | CreatingStreamingJobStatus::Resetting(..) => {}
207            CreatingStreamingJobStatus::PlaceHolder => {
208                unreachable!()
209            }
210        }
211    }
212
213    pub(super) fn start_consume_upstream(&mut self, barrier_info: &BarrierInfo) -> CreatingJobInfo {
214        match self {
215            CreatingStreamingJobStatus::ConsumingSnapshot { .. } => {
216                unreachable!(
217                    "should not start consuming upstream for a job that are consuming snapshot"
218                )
219            }
220            CreatingStreamingJobStatus::ConsumingLogStore { .. } => {
221                let prev_epoch = barrier_info.prev_epoch();
222                {
223                    assert!(barrier_info.kind.is_checkpoint());
224                    let CreatingStreamingJobStatus::ConsumingLogStore {
225                        info, tracking_job, ..
226                    } = replace(self, CreatingStreamingJobStatus::PlaceHolder)
227                    else {
228                        unreachable!()
229                    };
230                    *self = CreatingStreamingJobStatus::Finishing(prev_epoch, tracking_job);
231                    info
232                }
233            }
234            CreatingStreamingJobStatus::Finishing { .. } => {
235                unreachable!("should not start consuming upstream for a job again")
236            }
237            CreatingStreamingJobStatus::Resetting(..) => {
238                unreachable!("unlikely to start consume upstream when resetting")
239            }
240            CreatingStreamingJobStatus::PlaceHolder => {
241                unreachable!()
242            }
243        }
244    }
245
246    pub(super) fn on_new_upstream_epoch(
247        &mut self,
248        partial_graph_manager: &PartialGraphManager,
249        partial_graph_id: PartialGraphId,
250        max_pending_barrier_num: usize,
251        barrier_info: &BarrierInfo,
252        mutation: Option<Mutation>, // mutation to be set for the first barrier to inject
253    ) -> Vec<(BarrierInfo, Option<Mutation>)> {
254        let resolve_initial_barrier_num_to_inject = || {
255            max_pending_barrier_num
256                .saturating_sub(partial_graph_manager.pending_barrier_num(partial_graph_id))
257        };
258        match self {
259            CreatingStreamingJobStatus::ConsumingSnapshot {
260                pending_upstream_barriers,
261                prev_epoch_fake_physical_time,
262                pending_non_checkpoint_barriers,
263                create_mview_tracker,
264                ..
265            } => {
266                let mutation = mutation.or_else(|| {
267                    let pending_backfill_nodes = create_mview_tracker
268                        .take_pending_backfill_nodes()
269                        .collect_vec();
270                    if pending_backfill_nodes.is_empty() {
271                        None
272                    } else {
273                        Some(Mutation::StartFragmentBackfill(
274                            StartFragmentBackfillMutation {
275                                fragment_ids: pending_backfill_nodes,
276                            },
277                        ))
278                    }
279                });
280                let barrier_num_to_inject = resolve_initial_barrier_num_to_inject();
281                pending_upstream_barriers.push(barrier_info.clone());
282                // Mutation barriers must be forwarded even when the partial graph has reached the
283                // configured pending-barrier limit.
284                if barrier_num_to_inject == 0 && mutation.is_none() {
285                    return vec![];
286                }
287                vec![(
288                    CreatingStreamingJobStatus::new_fake_barrier(
289                        prev_epoch_fake_physical_time,
290                        pending_non_checkpoint_barriers,
291                        match barrier_info.kind {
292                            BarrierKind::Barrier => PbBarrierKind::Barrier,
293                            BarrierKind::Checkpoint(_) => PbBarrierKind::Checkpoint,
294                            BarrierKind::Initial => {
295                                unreachable!("upstream new epoch should not be initial")
296                            }
297                        },
298                    ),
299                    mutation,
300                )]
301            }
302            CreatingStreamingJobStatus::ConsumingLogStore {
303                pending_barriers, ..
304            } => {
305                // Throttle has no effect on the snapshot executor after it starts consuming the
306                // log store. The updated fragment plan is kept for the actors created on merge,
307                // so the mutation does not need to be forwarded in this phase.
308                drain_pending_barriers(
309                    pending_barriers,
310                    barrier_info.clone(),
311                    resolve_initial_barrier_num_to_inject(),
312                )
313                .into_iter()
314                .map(|barrier_info| (barrier_info, None))
315                .collect()
316            }
317            CreatingStreamingJobStatus::Finishing { .. }
318            | CreatingStreamingJobStatus::Resetting(..) => vec![],
319            CreatingStreamingJobStatus::PlaceHolder => {
320                unreachable!()
321            }
322        }
323    }
324
325    pub(super) fn new_fake_barrier(
326        prev_epoch_fake_physical_time: &mut u64,
327        pending_non_checkpoint_barriers: &mut Vec<u64>,
328        kind: PbBarrierKind,
329    ) -> BarrierInfo {
330        super::super::new_fake_barrier(
331            prev_epoch_fake_physical_time,
332            pending_non_checkpoint_barriers,
333            kind,
334        )
335    }
336
337    pub(super) fn fragment_infos(&self) -> Option<&HashMap<FragmentId, InflightFragmentInfo>> {
338        match self {
339            CreatingStreamingJobStatus::ConsumingSnapshot { info, .. }
340            | CreatingStreamingJobStatus::ConsumingLogStore { info, .. } => {
341                Some(&info.fragment_infos)
342            }
343            CreatingStreamingJobStatus::Finishing(..)
344            | CreatingStreamingJobStatus::Resetting(..) => None,
345            CreatingStreamingJobStatus::PlaceHolder => {
346                unreachable!()
347            }
348        }
349    }
350
351    pub(super) fn pre_apply_throttle(
352        &mut self,
353        config: &mut ThrottleConfigMap,
354    ) -> Option<Mutation> {
355        let fragment_infos = match self {
356            CreatingStreamingJobStatus::ConsumingSnapshot { info, .. }
357            | CreatingStreamingJobStatus::ConsumingLogStore { info, .. } => {
358                &mut info.fragment_infos
359            }
360            CreatingStreamingJobStatus::Finishing(..)
361            | CreatingStreamingJobStatus::Resetting(..) => return None,
362            CreatingStreamingJobStatus::PlaceHolder => {
363                unreachable!()
364            }
365        };
366
367        extract_throttle_config(config, |fragment_id, stream_node| {
368            if let Some(fragment_info) = fragment_infos.get_mut(&fragment_id) {
369                fragment_info.nodes = stream_node.clone();
370                true
371            } else {
372                false
373            }
374        })
375    }
376}
377
378fn drain_pending_barriers(
379    pending_barriers: &mut VecDeque<BarrierInfo>,
380    new_upstream_barrier: BarrierInfo,
381    barrier_num_to_inject: usize,
382) -> Vec<BarrierInfo> {
383    pending_barriers.push_back(new_upstream_barrier);
384    let barrier_count = pending_barriers.len().min(barrier_num_to_inject);
385    pending_barriers.drain(..barrier_count).collect()
386}
387
388#[cfg(test)]
389mod tests {
390    use risingwave_pb::stream_plan::PbStreamNode;
391
392    use super::*;
393
394    fn barrier(prev_epoch: u64, curr_epoch: u64) -> BarrierInfo {
395        BarrierInfo {
396            prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
397            curr_epoch: TracedEpoch::new(Epoch(curr_epoch)),
398            kind: BarrierKind::Barrier,
399        }
400    }
401
402    fn epochs(barriers: &[BarrierInfo]) -> Vec<(u64, u64)> {
403        barriers
404            .iter()
405            .map(|barrier| (barrier.prev_epoch(), barrier.curr_epoch()))
406            .collect()
407    }
408
409    #[test]
410    fn test_drain_pending_barriers_with_available_capacity() {
411        let mut pending_barriers = VecDeque::from([barrier(1, 2), barrier(2, 3), barrier(3, 4)]);
412
413        let injected = drain_pending_barriers(&mut pending_barriers, barrier(4, 5), 0);
414        assert!(injected.is_empty());
415        assert_eq!(
416            epochs(pending_barriers.make_contiguous()),
417            vec![(1, 2), (2, 3), (3, 4), (4, 5)]
418        );
419
420        let injected = drain_pending_barriers(&mut pending_barriers, barrier(5, 6), 2);
421        assert_eq!(epochs(&injected), vec![(1, 2), (2, 3)]);
422        assert_eq!(
423            epochs(pending_barriers.make_contiguous()),
424            vec![(3, 4), (4, 5), (5, 6)]
425        );
426
427        let injected = drain_pending_barriers(&mut pending_barriers, barrier(6, 7), 2);
428        assert_eq!(epochs(&injected), vec![(3, 4), (4, 5)]);
429        assert_eq!(
430            epochs(pending_barriers.make_contiguous()),
431            vec![(5, 6), (6, 7)]
432        );
433
434        let injected = drain_pending_barriers(&mut pending_barriers, barrier(7, 8), 2);
435        assert_eq!(epochs(&injected), vec![(5, 6), (6, 7)]);
436        assert_eq!(epochs(pending_barriers.make_contiguous()), vec![(7, 8)]);
437    }
438
439    #[test]
440    fn test_drain_pending_barriers_without_backlog() {
441        let mut pending_barriers = VecDeque::new();
442
443        let injected = drain_pending_barriers(&mut pending_barriers, barrier(1, 2), 100);
444
445        assert_eq!(epochs(&injected), vec![(1, 2)]);
446        assert!(pending_barriers.is_empty());
447    }
448
449    #[tokio::test]
450    async fn test_resetting_skips_barrier_capacity_lookup() {
451        let mut status = CreatingStreamingJobStatus::Resetting(vec![]);
452        let partial_graph_manager =
453            PartialGraphManager::uninitialized(crate::manager::MetaSrvEnv::for_test().await);
454
455        let injected = status.on_new_upstream_epoch(
456            &partial_graph_manager,
457            PartialGraphId::new(1),
458            10,
459            &barrier(1, 2),
460            None,
461        );
462
463        assert!(injected.is_empty());
464    }
465
466    #[test]
467    fn test_pre_apply_throttle_before_merge() {
468        let job_id = risingwave_common::id::JobId::new(1);
469        let fragment_id = FragmentId::new(1);
470        let old_node = PbStreamNode {
471            identity: "old".to_owned(),
472            ..Default::default()
473        };
474        let new_node = PbStreamNode {
475            identity: "new".to_owned(),
476            ..Default::default()
477        };
478        let fragment_infos = HashMap::from([(
479            fragment_id,
480            InflightFragmentInfo {
481                fragment_id,
482                distribution_type: risingwave_meta_model::fragment::DistributionType::Single,
483                fragment_type_mask: Default::default(),
484                vnode_count: 1,
485                nodes: old_node,
486                actors: Default::default(),
487                state_table_ids: Default::default(),
488            },
489        )]);
490        let mut status = CreatingStreamingJobStatus::ConsumingLogStore {
491            tracking_job: TrackingJob::recovered(job_id, &fragment_infos),
492            info: CreatingJobInfo {
493                fragment_infos,
494                upstream_fragment_downstreams: Default::default(),
495                downstreams: Default::default(),
496                snapshot_backfill_upstream_tables: Default::default(),
497                stream_actors: Default::default(),
498            },
499            log_store_progress_tracker: CreateMviewLogStoreProgressTracker::new(
500                std::iter::empty(),
501                0,
502            ),
503            pending_barriers: Default::default(),
504        };
505        let mut config = HashMap::from([(
506            fragment_id,
507            (
508                risingwave_pb::stream_plan::throttle_mutation::ThrottleConfig {
509                    rate_limit: Some(1_000),
510                    throttle_type: Default::default(),
511                },
512                new_node.clone(),
513            ),
514        )]);
515
516        assert!(status.pre_apply_throttle(&mut config).is_some());
517        assert!(config.is_empty());
518
519        let info = status.start_consume_upstream(&BarrierInfo {
520            prev_epoch: TracedEpoch::new(Epoch(1)),
521            curr_epoch: TracedEpoch::new(Epoch(2)),
522            kind: BarrierKind::Checkpoint(vec![1]),
523        });
524        assert_eq!(info.fragment_infos[&fragment_id].nodes, new_node);
525    }
526}