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