Skip to main content

risingwave_meta/barrier/checkpoint/independent_job/creating_job/
mod.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
15mod barrier_control;
16mod status;
17
18use std::cmp::max;
19use std::collections::{HashMap, HashSet, hash_map};
20use std::mem::take;
21use std::ops::Bound::{Excluded, Unbounded};
22use std::time::Duration;
23
24use risingwave_common::catalog::{DatabaseId, TableId};
25use risingwave_common::id::JobId;
26use risingwave_common::metrics::LabelGuardedIntGauge;
27use risingwave_common::util::epoch::Epoch;
28use risingwave_meta_model::WorkerId;
29use risingwave_pb::ddl_service::PbBackfillType;
30use risingwave_pb::hummock::HummockVersionStats;
31use risingwave_pb::id::{ActorId, FragmentId, PartialGraphId};
32use risingwave_pb::stream_plan::barrier::PbBarrierKind;
33use risingwave_pb::stream_plan::barrier_mutation::Mutation;
34use risingwave_pb::stream_plan::{AddMutation, StopMutation};
35use risingwave_pb::stream_service::BarrierCompleteResponse;
36use status::CreatingStreamingJobStatus;
37use tracing::{debug, info};
38
39use super::super::state::RenderResult;
40use super::IndependentCheckpointJobControl;
41use crate::MetaResult;
42use crate::barrier::backfill_order_control::get_nodes_with_backfill_dependencies;
43use crate::barrier::checkpoint::independent_job::creating_job::barrier_control::CreatingStreamingJobBarrierStats;
44use crate::barrier::checkpoint::independent_job::creating_job::status::CreateMviewLogStoreProgressTracker;
45use crate::barrier::command::{PostCollectCommand, TableLogEpochs, UpstreamTableLogEpochs};
46use crate::barrier::context::CreateSnapshotBackfillJobCommandInfo;
47use crate::barrier::edge_builder::FragmentEdgeBuildResult;
48use crate::barrier::info::{BarrierInfo, InflightStreamingJobInfo};
49use crate::barrier::notifier::Notifier;
50use crate::barrier::partial_graph::{
51    CollectedBarrier, PartialGraphBarrierInfo, PartialGraphManager, PartialGraphRecoverer,
52};
53use crate::barrier::progress::{CreateMviewProgressTracker, TrackingJob, collect_done_fragments};
54use crate::barrier::rpc::{build_locality_fragment_state_table_mapping, to_partial_graph_id};
55use crate::barrier::{
56    BackfillOrderState, BackfillProgress, BarrierKind, Command, FragmentBackfillProgress,
57    TracedEpoch,
58};
59use crate::controller::fragment::InflightFragmentInfo;
60use crate::model::{FragmentDownstreamRelation, StreamActor, StreamJobActorsToCreate};
61use crate::rpc::metrics::GLOBAL_META_METRICS;
62use crate::stream::source_manager::SplitAssignment;
63use crate::stream::{ExtendedFragmentBackfillOrder, build_actor_connector_splits};
64
65#[derive(Debug)]
66pub(crate) struct CreatingJobInfo {
67    pub fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
68    pub upstream_fragment_downstreams: FragmentDownstreamRelation,
69    pub downstreams: FragmentDownstreamRelation,
70    pub snapshot_backfill_upstream_tables: HashSet<TableId>,
71    pub stream_actors: HashMap<ActorId, StreamActor>,
72}
73
74#[derive(Debug)]
75pub(crate) struct CreatingStreamingJobControl {
76    job_id: JobId,
77    partial_graph_id: PartialGraphId,
78    snapshot_backfill_upstream_tables: HashSet<TableId>,
79    snapshot_epoch: u64,
80
81    node_actors: HashMap<WorkerId, HashSet<ActorId>>,
82    state_table_ids: HashSet<TableId>,
83
84    max_committed_epoch: Option<u64>,
85    status: CreatingStreamingJobStatus,
86    max_lagged_barrier_num: usize,
87
88    upstream_lag: LabelGuardedIntGauge,
89}
90
91impl CreatingStreamingJobControl {
92    #[expect(clippy::too_many_arguments)]
93    pub(crate) fn new<'a>(
94        entry: hash_map::VacantEntry<'a, JobId, IndependentCheckpointJobControl>,
95        create_info: CreateSnapshotBackfillJobCommandInfo,
96        notifiers: Vec<Notifier>,
97        snapshot_backfill_upstream_tables: HashSet<TableId>,
98        snapshot_epoch: u64,
99        since_timestamp_upstream_log_epochs: Option<(&TableLogEpochs, PartialGraphId, u64)>,
100        version_stat: &HummockVersionStats,
101        partial_graph_manager: &mut PartialGraphManager,
102        edges: &mut FragmentEdgeBuildResult,
103        split_assignment: &SplitAssignment,
104        actors: &RenderResult,
105    ) -> MetaResult<&'a mut Self> {
106        let info = create_info.info.clone();
107        let job_id = info.stream_job_fragments.stream_job_id();
108        let database_id = info.streaming_job.database_id();
109        debug!(
110            %job_id,
111            definition = info.definition,
112            "new creating job"
113        );
114        let fragment_infos = info
115            .stream_job_fragments
116            .new_fragment_info(
117                &actors.stream_actors,
118                &actors.actor_location,
119                split_assignment,
120            )
121            .collect();
122        let snapshot_backfill_actors: HashSet<ActorId> =
123            InflightStreamingJobInfo::snapshot_backfill_actor_ids(&fragment_infos).collect();
124        let backfill_nodes_to_pause =
125            get_nodes_with_backfill_dependencies(&info.fragment_backfill_ordering)
126                .into_iter()
127                .collect();
128        let backfill_order_state = BackfillOrderState::new(
129            &info.fragment_backfill_ordering,
130            &fragment_infos,
131            info.locality_fragment_state_table_mapping.clone(),
132        );
133        let create_mview_tracker = CreateMviewProgressTracker::recover(
134            job_id,
135            &fragment_infos,
136            backfill_order_state,
137            version_stat,
138        );
139
140        let actors_to_create = Command::create_streaming_job_actors_to_create(
141            &info,
142            edges,
143            &actors.stream_actors,
144            &actors.actor_location,
145        );
146
147        let mut prev_epoch_fake_physical_time = 0;
148        let mut pending_non_checkpoint_barriers = vec![];
149
150        let (initial_barrier_info, log_store_barriers_to_inject) = if let Some((
151            upstream_log_epochs,
152            upstream_partial_graph_id,
153            new_upstream_barrier_prev_epoch,
154        )) =
155            since_timestamp_upstream_log_epochs
156        {
157            let (initial_barrier, barriers_to_inject) =
158                Self::resolve_since_timestamp_upstream_log_epochs(
159                    upstream_log_epochs,
160                    partial_graph_manager.pending_barrier_infos(upstream_partial_graph_id),
161                    snapshot_epoch,
162                    new_upstream_barrier_prev_epoch,
163                )?;
164            (initial_barrier, Some(barriers_to_inject))
165        } else {
166            (
167                CreatingStreamingJobStatus::new_fake_barrier(
168                    &mut prev_epoch_fake_physical_time,
169                    &mut pending_non_checkpoint_barriers,
170                    PbBarrierKind::Checkpoint,
171                ),
172                None,
173            )
174        };
175
176        let added_actors: Vec<ActorId> = actors
177            .stream_actors
178            .values()
179            .flatten()
180            .map(|actor| actor.actor_id)
181            .collect();
182        let actor_splits = split_assignment
183            .values()
184            .flat_map(build_actor_connector_splits)
185            .collect();
186
187        assert!(
188            info.cdc_table_snapshot_splits.is_none(),
189            "should not have cdc backfill for snapshot backfill job"
190        );
191
192        let initial_mutation = Mutation::Add(AddMutation {
193            // for mutation of snapshot backfill job, we won't include changes to dispatchers of upstream actors.
194            actor_dispatchers: Default::default(),
195            added_actors,
196            actor_splits,
197            // we assume that when handling snapshot backfill, the cluster must not be paused
198            pause: false,
199            subscriptions_to_add: Default::default(),
200            backfill_nodes_to_pause,
201            actor_cdc_table_snapshot_splits: None,
202            new_upstream_sinks: Default::default(),
203            dropped_actors: Default::default(),
204            sink_log_store_flush: Default::default(),
205        });
206
207        let node_actors = InflightFragmentInfo::actor_ids_to_collect(fragment_infos.values());
208        let state_table_ids =
209            InflightFragmentInfo::existing_table_ids(fragment_infos.values()).collect();
210
211        let partial_graph_id = to_partial_graph_id(database_id, Some(job_id));
212        let max_lagged_barrier_num = partial_graph_manager
213            .control_stream_manager()
214            .env
215            .opts
216            .snapshot_backfill_finish_max_lagged_barriers;
217
218        let IndependentCheckpointJobControl::CreatingStreamingJob(job) = entry.insert(
219            IndependentCheckpointJobControl::CreatingStreamingJob(Self {
220                partial_graph_id,
221                job_id,
222                snapshot_backfill_upstream_tables,
223                max_committed_epoch: None,
224                snapshot_epoch,
225                status: CreatingStreamingJobStatus::PlaceHolder, // filled in later code
226                max_lagged_barrier_num,
227                upstream_lag: GLOBAL_META_METRICS
228                    .snapshot_backfill_lag
229                    .with_guarded_label_values(&[&format!("{}", job_id)]),
230                node_actors,
231                state_table_ids,
232            }),
233        ) else {
234            unreachable!()
235        };
236
237        let mut graph_adder = partial_graph_manager.add_partial_graph(
238            partial_graph_id,
239            CreatingStreamingJobBarrierStats::new(job_id, snapshot_epoch),
240        );
241
242        if let Err(e) = Self::inject_barrier(
243            partial_graph_id,
244            graph_adder.manager(),
245            &job.node_actors,
246            &job.state_table_ids,
247            false,
248            initial_barrier_info,
249            Some(actors_to_create),
250            Some(initial_mutation),
251            notifiers,
252            Some(create_info),
253        ) {
254            graph_adder.failed();
255            job.status = CreatingStreamingJobStatus::Resetting(vec![]);
256            Err(e)
257        } else {
258            graph_adder.added();
259            let job_info = CreatingJobInfo {
260                fragment_infos,
261                upstream_fragment_downstreams: info.upstream_fragment_downstreams.clone(),
262                downstreams: info.stream_job_fragments.downstreams,
263                snapshot_backfill_upstream_tables: job.snapshot_backfill_upstream_tables.clone(),
264                stream_actors: actors
265                    .stream_actors
266                    .values()
267                    .flatten()
268                    .map(|actor| (actor.actor_id, actor.clone()))
269                    .collect(),
270            };
271            if let Some(log_store_barriers_to_inject) = log_store_barriers_to_inject {
272                let upstream_lag = log_store_barriers_to_inject
273                    .last()
274                    .map(|info| info.prev_epoch().saturating_sub(snapshot_epoch))
275                    .unwrap_or(0);
276                job.status = CreatingStreamingJobStatus::ConsumingLogStore {
277                    tracking_job: TrackingJob::recovered(job_id, &job_info.fragment_infos),
278                    info: job_info,
279                    log_store_progress_tracker: CreateMviewLogStoreProgressTracker::new(
280                        snapshot_backfill_actors.iter().cloned(),
281                        upstream_lag,
282                    ),
283                    barriers_to_inject: Some(log_store_barriers_to_inject),
284                };
285            } else {
286                assert!(pending_non_checkpoint_barriers.is_empty());
287                job.status = CreatingStreamingJobStatus::ConsumingSnapshot {
288                    prev_epoch_fake_physical_time,
289                    pending_upstream_barriers: vec![],
290                    version_stats: version_stat.clone(),
291                    create_mview_tracker,
292                    snapshot_backfill_actors,
293                    snapshot_epoch,
294                    info: job_info,
295                    pending_non_checkpoint_barriers,
296                };
297            };
298            Ok(job)
299        }
300    }
301
302    pub(super) fn gen_fragment_backfill_progress(&self) -> Vec<FragmentBackfillProgress> {
303        match &self.status {
304            CreatingStreamingJobStatus::ConsumingSnapshot {
305                create_mview_tracker,
306                info,
307                ..
308            } => create_mview_tracker.collect_fragment_progress(&info.fragment_infos, true),
309            CreatingStreamingJobStatus::ConsumingLogStore { info, .. } => {
310                collect_done_fragments(self.job_id, &info.fragment_infos)
311            }
312            CreatingStreamingJobStatus::Finishing(_, _)
313            | CreatingStreamingJobStatus::Resetting(_)
314            | CreatingStreamingJobStatus::PlaceHolder => vec![],
315        }
316    }
317
318    fn resolve_upstream_log_epochs(
319        snapshot_backfill_upstream_tables: &HashSet<TableId>,
320        upstream_table_log_epochs: &UpstreamTableLogEpochs,
321        exclusive_start_log_epoch: u64,
322        upstream_barrier_info: &BarrierInfo,
323    ) -> MetaResult<Vec<BarrierInfo>> {
324        let table_id = snapshot_backfill_upstream_tables
325            .iter()
326            .next()
327            .expect("snapshot backfill job should have upstream");
328        let epochs_iter = if let Some(epochs) = upstream_table_log_epochs.get(table_id) {
329            let mut epochs_iter = epochs.iter();
330            loop {
331                let (_, checkpoint_epoch) =
332                    epochs_iter.next().expect("not reach committed epoch yet");
333                if *checkpoint_epoch < exclusive_start_log_epoch {
334                    continue;
335                }
336                assert_eq!(*checkpoint_epoch, exclusive_start_log_epoch);
337                break;
338            }
339            epochs_iter
340        } else {
341            // snapshot backfill job has been marked as creating, but upstream table has not committed a new epoch yet, so no table change log
342            assert_eq!(
343                upstream_barrier_info.prev_epoch(),
344                exclusive_start_log_epoch
345            );
346            static EMPTY_VEC: Vec<(Vec<u64>, u64)> = Vec::new();
347            EMPTY_VEC.iter()
348        };
349
350        let mut ret = vec![];
351        let mut prev_epoch = exclusive_start_log_epoch;
352        let mut pending_non_checkpoint_barriers = vec![];
353        for (non_checkpoint_epochs, checkpoint_epoch) in epochs_iter {
354            for (i, epoch) in non_checkpoint_epochs
355                .iter()
356                .chain([checkpoint_epoch])
357                .enumerate()
358            {
359                assert!(*epoch > prev_epoch);
360                pending_non_checkpoint_barriers.push(prev_epoch);
361                ret.push(BarrierInfo {
362                    prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
363                    curr_epoch: TracedEpoch::new(Epoch(*epoch)),
364                    kind: if i == 0 {
365                        BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_barriers))
366                    } else {
367                        BarrierKind::Barrier
368                    },
369                });
370                prev_epoch = *epoch;
371            }
372        }
373        ret.push(BarrierInfo {
374            prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
375            curr_epoch: TracedEpoch::new(Epoch(upstream_barrier_info.curr_epoch())),
376            kind: BarrierKind::Checkpoint(pending_non_checkpoint_barriers),
377        });
378        Ok(ret)
379    }
380
381    /// Resolves the log-store barriers that must be injected before the create barrier.
382    ///
383    /// Example with pending upstream barriers:
384    ///
385    /// ```text
386    /// snapshot epoch: 60
387    /// changelog after snapshot: [61, 62, 63, 64] + 65
388    /// pending upstream barriers: 65 -> 66 checkpoint, 66 -> 67 barrier,
389    ///                            67 -> 68 barrier, 68 -> 69 barrier,
390    ///                            69 -> 70 barrier
391    /// new create barrier: 70 -> 71
392    ///
393    /// injected: 60 -> 61 checkpoint, 61 -> 62 barrier, ..., 64 -> 65 barrier,
394    ///           65 -> 66 checkpoint, 66 -> 67 barrier, ..., 69 -> 70 barrier
395    /// current create barrier later injects: 70 -> 71 checkpoint
396    /// ```
397    ///
398    /// Example without pending upstream barriers:
399    ///
400    /// ```text
401    /// snapshot epoch: 60
402    /// changelog after snapshot: [61, 62, 63, 64] + 65
403    /// new create barrier: 66 -> 67
404    ///
405    /// injected: 60 -> 61 checkpoint, 61 -> 62 barrier, ..., 64 -> 65 barrier,
406    ///           65 -> 66 checkpoint
407    /// current create barrier later injects: 66 -> 67 checkpoint
408    /// ```
409    fn resolve_since_timestamp_upstream_log_epochs(
410        upstream_log_epochs: &TableLogEpochs,
411        pending_upstream_barriers: impl Iterator<Item = &BarrierInfo>,
412        snapshot_epoch: u64,
413        new_upstream_barrier_prev_epoch: u64,
414    ) -> MetaResult<(BarrierInfo, Vec<BarrierInfo>)> {
415        let mut initial_barrier = None;
416        let mut barriers = vec![];
417        fn emit_barrier(
418            initial_barrier: &mut Option<BarrierInfo>,
419            barriers: &mut Vec<BarrierInfo>,
420            barrier: BarrierInfo,
421        ) {
422            if initial_barrier.is_none() {
423                *initial_barrier = Some(barrier);
424            } else {
425                barriers.push(barrier);
426            }
427        }
428
429        let mut prev_epoch = snapshot_epoch;
430        let mut pending_non_checkpoint_barriers = vec![];
431        for (non_checkpoint_epochs, checkpoint_epoch) in upstream_log_epochs {
432            for (i, epoch) in non_checkpoint_epochs
433                .iter()
434                .chain([checkpoint_epoch])
435                .enumerate()
436            {
437                assert!(
438                    *epoch > prev_epoch,
439                    "changelog epochs should be strictly increasing"
440                );
441                pending_non_checkpoint_barriers.push(prev_epoch);
442                emit_barrier(
443                    &mut initial_barrier,
444                    &mut barriers,
445                    BarrierInfo {
446                        prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
447                        curr_epoch: TracedEpoch::new(Epoch(*epoch)),
448                        kind: if i == 0 {
449                            BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_barriers))
450                        } else {
451                            BarrierKind::Barrier
452                        },
453                    },
454                );
455                prev_epoch = *epoch;
456            }
457        }
458
459        let mut pending_upstream_barriers = pending_upstream_barriers.peekable();
460        if pending_upstream_barriers.peek().is_none() {
461            assert!(
462                new_upstream_barrier_prev_epoch > prev_epoch,
463                "new upstream barrier prev epoch should be newer than the latest changelog epoch"
464            );
465            emit_barrier(
466                &mut initial_barrier,
467                &mut barriers,
468                BarrierInfo {
469                    prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
470                    curr_epoch: TracedEpoch::new(Epoch(new_upstream_barrier_prev_epoch)),
471                    kind: BarrierKind::Checkpoint(pending_non_checkpoint_barriers),
472                },
473            );
474        } else {
475            for (index, pending_barrier) in pending_upstream_barriers.enumerate() {
476                assert_eq!(
477                    pending_barrier.prev_epoch(),
478                    prev_epoch,
479                    "pending upstream barriers should continue from resolved changelog epochs"
480                );
481                if index == 0 {
482                    assert!(
483                        pending_barrier.kind.is_checkpoint(),
484                        "first pending upstream barrier should checkpoint from the latest resolved changelog epoch"
485                    );
486                }
487                pending_non_checkpoint_barriers.push(prev_epoch);
488                emit_barrier(
489                    &mut initial_barrier,
490                    &mut barriers,
491                    BarrierInfo {
492                        prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
493                        curr_epoch: TracedEpoch::new(Epoch(pending_barrier.curr_epoch())),
494                        kind: if pending_barrier.kind.is_checkpoint() {
495                            BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_barriers))
496                        } else {
497                            BarrierKind::Barrier
498                        },
499                    },
500                );
501                prev_epoch = pending_barrier.curr_epoch();
502            }
503            assert_eq!(
504                new_upstream_barrier_prev_epoch, prev_epoch,
505                "new upstream barrier prev epoch should match the latest pending log-store epoch"
506            );
507        }
508        let Some(initial_barrier) = initial_barrier else {
509            return Err(anyhow::anyhow!(
510                "missing lagging barriers for direct log-store start from snapshot epoch {}",
511                snapshot_epoch
512            )
513            .into());
514        };
515        assert!(initial_barrier.kind.is_checkpoint());
516        Ok((initial_barrier, barriers))
517    }
518
519    fn recover_consuming_snapshot(
520        job_id: JobId,
521        upstream_table_log_epochs: &UpstreamTableLogEpochs,
522        snapshot_epoch: u64,
523        committed_epoch: u64,
524        upstream_barrier_info: &BarrierInfo,
525        info: CreatingJobInfo,
526        backfill_order_state: BackfillOrderState,
527        version_stat: &HummockVersionStats,
528    ) -> MetaResult<(CreatingStreamingJobStatus, BarrierInfo)> {
529        let mut prev_epoch_fake_physical_time = Epoch(committed_epoch).physical_time();
530        let mut pending_non_checkpoint_barriers = vec![];
531        let create_mview_tracker = CreateMviewProgressTracker::recover(
532            job_id,
533            &info.fragment_infos,
534            backfill_order_state,
535            version_stat,
536        );
537        let barrier_info = CreatingStreamingJobStatus::new_fake_barrier(
538            &mut prev_epoch_fake_physical_time,
539            &mut pending_non_checkpoint_barriers,
540            PbBarrierKind::Initial,
541        );
542
543        Ok((
544            CreatingStreamingJobStatus::ConsumingSnapshot {
545                prev_epoch_fake_physical_time,
546                pending_upstream_barriers: Self::resolve_upstream_log_epochs(
547                    &info.snapshot_backfill_upstream_tables,
548                    upstream_table_log_epochs,
549                    snapshot_epoch,
550                    upstream_barrier_info,
551                )?,
552                version_stats: version_stat.clone(),
553                create_mview_tracker,
554                snapshot_backfill_actors: InflightStreamingJobInfo::snapshot_backfill_actor_ids(
555                    &info.fragment_infos,
556                )
557                .collect(),
558                info,
559                snapshot_epoch,
560                pending_non_checkpoint_barriers,
561            },
562            barrier_info,
563        ))
564    }
565
566    fn recover_consuming_log_store(
567        job_id: JobId,
568        upstream_table_log_epochs: &UpstreamTableLogEpochs,
569        committed_epoch: u64,
570        upstream_barrier_info: &BarrierInfo,
571        info: CreatingJobInfo,
572    ) -> MetaResult<(CreatingStreamingJobStatus, BarrierInfo)> {
573        let mut barriers_to_inject = Self::resolve_upstream_log_epochs(
574            &info.snapshot_backfill_upstream_tables,
575            upstream_table_log_epochs,
576            committed_epoch,
577            upstream_barrier_info,
578        )?;
579        let mut first_barrier = barriers_to_inject.remove(0);
580        assert!(first_barrier.kind.is_checkpoint());
581        first_barrier.kind = BarrierKind::Initial;
582
583        Ok((
584            CreatingStreamingJobStatus::ConsumingLogStore {
585                tracking_job: TrackingJob::recovered(job_id, &info.fragment_infos),
586                log_store_progress_tracker: CreateMviewLogStoreProgressTracker::new(
587                    InflightStreamingJobInfo::snapshot_backfill_actor_ids(&info.fragment_infos),
588                    barriers_to_inject
589                        .last()
590                        .map(|info| info.prev_epoch() - committed_epoch)
591                        .unwrap_or(0),
592                ),
593                barriers_to_inject: Some(barriers_to_inject),
594                info,
595            },
596            first_barrier,
597        ))
598    }
599
600    #[expect(clippy::too_many_arguments)]
601    pub(crate) fn recover(
602        database_id: DatabaseId,
603        job_id: JobId,
604        snapshot_backfill_upstream_tables: HashSet<TableId>,
605        upstream_table_log_epochs: &UpstreamTableLogEpochs,
606        snapshot_epoch: u64,
607        committed_epoch: u64,
608        upstream_barrier_info: &BarrierInfo,
609        fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
610        backfill_order: ExtendedFragmentBackfillOrder,
611        fragment_relations: &FragmentDownstreamRelation,
612        version_stat: &HummockVersionStats,
613        new_actors: StreamJobActorsToCreate,
614        initial_mutation: Mutation,
615        partial_graph_recoverer: &mut PartialGraphRecoverer<'_>,
616    ) -> MetaResult<Self> {
617        info!(
618            %job_id,
619            "recovered creating snapshot backfill job"
620        );
621
622        let node_actors = InflightFragmentInfo::actor_ids_to_collect(fragment_infos.values());
623        let state_table_ids: HashSet<_> =
624            InflightFragmentInfo::existing_table_ids(fragment_infos.values()).collect();
625
626        let mut upstream_fragment_downstreams: FragmentDownstreamRelation = Default::default();
627        for (upstream_fragment_id, downstreams) in fragment_relations {
628            if fragment_infos.contains_key(upstream_fragment_id) {
629                continue;
630            }
631            for downstream in downstreams {
632                if fragment_infos.contains_key(&downstream.downstream_fragment_id) {
633                    upstream_fragment_downstreams
634                        .entry(*upstream_fragment_id)
635                        .or_default()
636                        .push(downstream.clone());
637                }
638            }
639        }
640        let downstreams = fragment_infos
641            .keys()
642            .filter_map(|fragment_id| {
643                fragment_relations
644                    .get(fragment_id)
645                    .map(|relation| (*fragment_id, relation.clone()))
646            })
647            .collect();
648
649        let info = CreatingJobInfo {
650            fragment_infos,
651            upstream_fragment_downstreams,
652            downstreams,
653            snapshot_backfill_upstream_tables: snapshot_backfill_upstream_tables.clone(),
654            stream_actors: new_actors
655                .values()
656                .flat_map(|fragments| {
657                    fragments.values().flat_map(|(_, actors, _)| {
658                        actors
659                            .iter()
660                            .map(|(actor, _, _)| (actor.actor_id, actor.clone()))
661                    })
662                })
663                .collect(),
664        };
665
666        let (status, first_barrier_info) = if committed_epoch < snapshot_epoch {
667            let locality_fragment_state_table_mapping =
668                build_locality_fragment_state_table_mapping(&info.fragment_infos);
669            let backfill_order_state = BackfillOrderState::recover_from_fragment_infos(
670                &backfill_order,
671                &info.fragment_infos,
672                locality_fragment_state_table_mapping,
673            );
674            Self::recover_consuming_snapshot(
675                job_id,
676                upstream_table_log_epochs,
677                snapshot_epoch,
678                committed_epoch,
679                upstream_barrier_info,
680                info,
681                backfill_order_state,
682                version_stat,
683            )?
684        } else {
685            Self::recover_consuming_log_store(
686                job_id,
687                upstream_table_log_epochs,
688                committed_epoch,
689                upstream_barrier_info,
690                info,
691            )?
692        };
693
694        let partial_graph_id = to_partial_graph_id(database_id, Some(job_id));
695        let max_lagged_barrier_num = partial_graph_recoverer
696            .control_stream_manager()
697            .env
698            .opts
699            .snapshot_backfill_finish_max_lagged_barriers;
700
701        partial_graph_recoverer.recover_graph(
702            partial_graph_id,
703            initial_mutation,
704            &first_barrier_info,
705            &node_actors,
706            state_table_ids.iter().copied(),
707            new_actors,
708            CreatingStreamingJobBarrierStats::new(job_id, snapshot_epoch),
709        )?;
710
711        Ok(Self {
712            job_id,
713            partial_graph_id,
714            snapshot_backfill_upstream_tables,
715            snapshot_epoch,
716            node_actors,
717            state_table_ids,
718            max_committed_epoch: Some(committed_epoch),
719            status,
720            max_lagged_barrier_num,
721            upstream_lag: GLOBAL_META_METRICS
722                .snapshot_backfill_lag
723                .with_guarded_label_values(&[&format!("{}", job_id)]),
724        })
725    }
726
727    pub(crate) fn gen_backfill_progress(&self) -> BackfillProgress {
728        let progress = match &self.status {
729            CreatingStreamingJobStatus::ConsumingSnapshot {
730                create_mview_tracker,
731                ..
732            } => {
733                if create_mview_tracker.is_finished() {
734                    "Snapshot finished".to_owned()
735                } else {
736                    let progress = create_mview_tracker.gen_backfill_progress();
737                    format!("Snapshot [{}]", progress)
738                }
739            }
740            CreatingStreamingJobStatus::ConsumingLogStore {
741                log_store_progress_tracker,
742                ..
743            } => {
744                format!(
745                    "LogStore [{}]",
746                    log_store_progress_tracker.gen_backfill_progress()
747                )
748            }
749            CreatingStreamingJobStatus::Finishing(finish_epoch, ..) => {
750                let committed_epoch = self.max_committed_epoch.expect("should have committed");
751                let lag = Duration::from_millis(
752                    Epoch(*finish_epoch).physical_time() - Epoch(committed_epoch).physical_time(),
753                );
754                format!("Finishing [epoch lag: {lag:?}]",)
755            }
756            CreatingStreamingJobStatus::Resetting(_) => "Resetting".to_owned(),
757            CreatingStreamingJobStatus::PlaceHolder => {
758                unreachable!()
759            }
760        };
761        BackfillProgress {
762            progress,
763            backfill_type: PbBackfillType::SnapshotBackfill,
764        }
765    }
766
767    pub(super) fn pinned_upstream_log_epoch(&self) -> (u64, HashSet<TableId>) {
768        (
769            max(self.max_committed_epoch.unwrap_or(0), self.snapshot_epoch),
770            self.snapshot_backfill_upstream_tables.clone(),
771        )
772    }
773
774    fn inject_barrier(
775        partial_graph_id: PartialGraphId,
776        partial_graph_manager: &mut PartialGraphManager,
777        node_actors: &HashMap<WorkerId, HashSet<ActorId>>,
778        state_table_ids: &HashSet<TableId>,
779        is_finishing: bool,
780        barrier_info: BarrierInfo,
781        new_actors: Option<StreamJobActorsToCreate>,
782        mutation: Option<Mutation>,
783        notifiers: Vec<Notifier>,
784        first_create_info: Option<CreateSnapshotBackfillJobCommandInfo>,
785    ) -> MetaResult<()> {
786        let (table_ids_to_sync, nodes_to_sync_table) = if !is_finishing {
787            (Some(state_table_ids), Some(node_actors.keys().copied()))
788        } else {
789            (None, None)
790        };
791        partial_graph_manager.inject_barrier(
792            partial_graph_id,
793            mutation,
794            node_actors,
795            table_ids_to_sync.into_iter().flatten().copied(),
796            nodes_to_sync_table.into_iter().flatten(),
797            new_actors,
798            PartialGraphBarrierInfo::new(
799                first_create_info.map_or_else(
800                    PostCollectCommand::barrier,
801                    CreateSnapshotBackfillJobCommandInfo::into_post_collect,
802                ),
803                barrier_info,
804                notifiers,
805                state_table_ids.clone(),
806            ),
807        )?;
808        Ok(())
809    }
810
811    pub(crate) fn start_consume_upstream(
812        &mut self,
813        partial_graph_manager: &mut PartialGraphManager,
814        barrier_info: &BarrierInfo,
815    ) -> MetaResult<CreatingJobInfo> {
816        info!(
817            job_id = %self.job_id,
818            prev_epoch = barrier_info.prev_epoch(),
819            "start consuming upstream"
820        );
821        let info = self.status.start_consume_upstream(barrier_info);
822        Self::inject_barrier(
823            self.partial_graph_id,
824            partial_graph_manager,
825            &self.node_actors,
826            &self.state_table_ids,
827            true,
828            barrier_info.clone(),
829            None,
830            Some(Mutation::Stop(StopMutation {
831                // stop all actors
832                actors: info
833                    .fragment_infos
834                    .values()
835                    .flat_map(|info| info.actors.keys().copied())
836                    .collect(),
837                dropped_sink_fragments: vec![], // not related to sink-into-table
838            })),
839            vec![], // no notifiers when start consuming upstream
840            None,
841        )?;
842        Ok(info)
843    }
844
845    pub(crate) fn on_new_upstream_barrier(
846        &mut self,
847        partial_graph_manager: &mut PartialGraphManager,
848        barrier_info: &BarrierInfo,
849        mutation: Option<(Mutation, Vec<Notifier>)>,
850    ) -> MetaResult<()> {
851        let progress_epoch = if let Some(max_committed_epoch) = self.max_committed_epoch {
852            max(max_committed_epoch, self.snapshot_epoch)
853        } else {
854            self.snapshot_epoch
855        };
856        self.upstream_lag.set(
857            barrier_info
858                .prev_epoch
859                .value()
860                .0
861                .saturating_sub(progress_epoch) as _,
862        );
863        let (mut mutation, mut notifiers) = match mutation {
864            Some((mutation, notifiers)) => (Some(mutation), notifiers),
865            None => (None, vec![]),
866        };
867        {
868            for (barrier_to_inject, mutation) in self
869                .status
870                .on_new_upstream_epoch(barrier_info, mutation.take())
871            {
872                Self::inject_barrier(
873                    self.partial_graph_id,
874                    partial_graph_manager,
875                    &self.node_actors,
876                    &self.state_table_ids,
877                    false,
878                    barrier_to_inject,
879                    None,
880                    mutation,
881                    take(&mut notifiers),
882                    None,
883                )?;
884            }
885            assert!(mutation.is_none(), "must have consumed mutation");
886            assert!(notifiers.is_empty(), "must consumed notifiers");
887        }
888        Ok(())
889    }
890
891    /// Returns whether the next barrier should be forced to a checkpoint.
892    pub(crate) fn collect(&mut self, collected_barrier: CollectedBarrier<'_>) -> bool {
893        let pending_barrier_num = collected_barrier.pending_barrier_num;
894        self.status.update_progress(
895            collected_barrier
896                .resps
897                .values()
898                .flat_map(|resp| &resp.create_mview_progress),
899        );
900        self.is_ready_to_merge() && pending_barrier_num <= self.max_lagged_barrier_num
901    }
902
903    fn is_ready_to_merge(&self) -> bool {
904        if let CreatingStreamingJobStatus::ConsumingLogStore {
905            log_store_progress_tracker,
906            barriers_to_inject,
907            ..
908        } = &self.status
909            && barriers_to_inject.is_none()
910            && log_store_progress_tracker.is_finished()
911        {
912            true
913        } else {
914            false
915        }
916    }
917
918    pub(crate) fn should_merge_to_upstream(
919        &self,
920        partial_graph_manager: &PartialGraphManager,
921    ) -> bool {
922        if !self.is_ready_to_merge() {
923            return false;
924        }
925
926        // A job that is ready to merge has finished initialization and is not resetting, so its
927        // partial graph must be running.
928        partial_graph_manager.pending_barrier_num(self.partial_graph_id)
929            <= self.max_lagged_barrier_num
930    }
931}
932
933impl CreatingStreamingJobControl {
934    pub(crate) fn start_completing(
935        &mut self,
936        partial_graph_manager: &mut PartialGraphManager,
937        min_upstream_inflight_epoch: Option<u64>,
938        upstream_committed_epoch: u64,
939    ) -> Option<(
940        u64,
941        HashMap<WorkerId, BarrierCompleteResponse>,
942        PartialGraphBarrierInfo,
943        bool,
944    )> {
945        // do not commit snapshot backfill job until upstream has committed the snapshot epoch
946        if upstream_committed_epoch < self.snapshot_epoch {
947            return None;
948        }
949        let (finished_at_epoch, epoch_end_bound) = match &self.status {
950            CreatingStreamingJobStatus::Finishing(finish_at_epoch, _) => {
951                let epoch_end_bound = min_upstream_inflight_epoch
952                    .map(|upstream_epoch| {
953                        if upstream_epoch < *finish_at_epoch {
954                            Excluded(upstream_epoch)
955                        } else {
956                            Unbounded
957                        }
958                    })
959                    .unwrap_or(Unbounded);
960                (Some(*finish_at_epoch), epoch_end_bound)
961            }
962            CreatingStreamingJobStatus::ConsumingSnapshot { .. }
963            | CreatingStreamingJobStatus::ConsumingLogStore { .. } => (
964                None,
965                min_upstream_inflight_epoch
966                    .map(Excluded)
967                    .unwrap_or(Unbounded),
968            ),
969            CreatingStreamingJobStatus::Resetting(..) => {
970                return None;
971            }
972            CreatingStreamingJobStatus::PlaceHolder => {
973                unreachable!()
974            }
975        };
976        partial_graph_manager
977            .start_completing(
978                self.partial_graph_id,
979                epoch_end_bound,
980                |non_checkpoint_epoch, _, _| {
981                    if let Some(finish_at_epoch) = finished_at_epoch {
982                        assert!(non_checkpoint_epoch.prev < finish_at_epoch);
983                    }
984                },
985            )
986            .map(|(epoch, resps, info)| {
987                let is_finish_epoch = if let Some(finish_at_epoch) = finished_at_epoch {
988                    assert!(!info.post_collect_command.should_checkpoint());
989                    if epoch == finish_at_epoch {
990                        // TODO: can early remove partial graph here
991                        self.ack_completed(partial_graph_manager, epoch);
992                        true
993                    } else {
994                        false
995                    }
996                } else {
997                    false
998                };
999                (epoch, resps, info, is_finish_epoch)
1000            })
1001    }
1002
1003    pub(super) fn ack_completed(
1004        &mut self,
1005        partial_graph_manager: &mut PartialGraphManager,
1006        completed_epoch: u64,
1007    ) {
1008        match &self.status {
1009            CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1010            | CreatingStreamingJobStatus::ConsumingLogStore { .. }
1011            | CreatingStreamingJobStatus::Finishing(_, _) => {
1012                partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
1013                if let Some(prev_max_committed_epoch) =
1014                    self.max_committed_epoch.replace(completed_epoch)
1015                {
1016                    assert!(completed_epoch > prev_max_committed_epoch);
1017                }
1018            }
1019            CreatingStreamingJobStatus::Resetting(_) => {
1020                // The job was dropped while the completing task was running in the background.
1021                // The partial graph has already been reset, so skip the ack.
1022            }
1023            CreatingStreamingJobStatus::PlaceHolder => {
1024                unreachable!()
1025            }
1026        }
1027    }
1028
1029    pub(crate) fn fragment_infos(&self) -> Option<&HashMap<FragmentId, InflightFragmentInfo>> {
1030        self.status.fragment_infos()
1031    }
1032
1033    pub fn into_tracking_job(self) -> TrackingJob {
1034        match self.status {
1035            CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1036            | CreatingStreamingJobStatus::ConsumingLogStore { .. }
1037            | CreatingStreamingJobStatus::Resetting(..)
1038            | CreatingStreamingJobStatus::PlaceHolder => {
1039                unreachable!("expect finish")
1040            }
1041            CreatingStreamingJobStatus::Finishing(_, tracking_job) => tracking_job,
1042        }
1043    }
1044
1045    pub(super) fn on_partial_graph_reset(mut self) {
1046        match &mut self.status {
1047            CreatingStreamingJobStatus::Resetting(notifiers) => {
1048                for notifier in notifiers.drain(..) {
1049                    notifier.notify_collected();
1050                }
1051            }
1052            CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1053            | CreatingStreamingJobStatus::ConsumingLogStore { .. }
1054            | CreatingStreamingJobStatus::Finishing(_, _) => {
1055                panic!(
1056                    "should be resetting when receiving reset partial graph resp, but at {:?}",
1057                    self.status
1058                )
1059            }
1060            CreatingStreamingJobStatus::PlaceHolder => {
1061                unreachable!()
1062            }
1063        }
1064    }
1065
1066    /// Drop a creating snapshot backfill job by directly resetting the partial graph
1067    /// Return `false` if the partial graph has been merged to upstream database, and `true` otherwise
1068    /// to mean that the job has been dropped.
1069    pub(super) fn drop(
1070        &mut self,
1071        notifiers: &mut Vec<Notifier>,
1072        partial_graph_manager: &mut PartialGraphManager,
1073    ) -> bool {
1074        match &mut self.status {
1075            CreatingStreamingJobStatus::Resetting(existing_notifiers) => {
1076                for notifier in &mut *notifiers {
1077                    notifier.notify_started();
1078                }
1079                existing_notifiers.append(notifiers);
1080                true
1081            }
1082            CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1083            | CreatingStreamingJobStatus::ConsumingLogStore { .. } => {
1084                for notifier in &mut *notifiers {
1085                    notifier.notify_started();
1086                }
1087                partial_graph_manager.reset_partial_graphs([self.partial_graph_id]);
1088                self.status = CreatingStreamingJobStatus::Resetting(take(notifiers));
1089                true
1090            }
1091            CreatingStreamingJobStatus::Finishing(_, _) => false,
1092            CreatingStreamingJobStatus::PlaceHolder => {
1093                unreachable!()
1094            }
1095        }
1096    }
1097
1098    pub(crate) fn reset(self) -> bool {
1099        match self.status {
1100            CreatingStreamingJobStatus::ConsumingSnapshot { .. }
1101            | CreatingStreamingJobStatus::ConsumingLogStore { .. }
1102            | CreatingStreamingJobStatus::Finishing(_, _) => false,
1103            CreatingStreamingJobStatus::Resetting(notifiers) => {
1104                for notifier in notifiers {
1105                    notifier.notify_collected();
1106                }
1107                true
1108            }
1109            CreatingStreamingJobStatus::PlaceHolder => {
1110                unreachable!()
1111            }
1112        }
1113    }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118    use super::*;
1119
1120    #[test]
1121    fn test_resolve_since_timestamp_upstream_log_epochs() {
1122        let upstream_log_epochs = vec![(vec![45, 50], 55)];
1123
1124        let (initial_barrier, barriers) =
1125            CreatingStreamingJobControl::resolve_since_timestamp_upstream_log_epochs(
1126                &upstream_log_epochs,
1127                [].iter(),
1128                40,
1129                60,
1130            )
1131            .unwrap();
1132
1133        assert_eq!(
1134            (initial_barrier.prev_epoch(), initial_barrier.curr_epoch()),
1135            (40, 45)
1136        );
1137        assert!(initial_barrier.kind.is_checkpoint());
1138        assert_eq!(
1139            barriers
1140                .iter()
1141                .map(|barrier| (barrier.prev_epoch(), barrier.curr_epoch()))
1142                .collect::<Vec<_>>(),
1143            vec![(45, 50), (50, 55), (55, 60)]
1144        );
1145        assert_eq!(
1146            barriers
1147                .iter()
1148                .map(|barrier| barrier.kind.is_checkpoint())
1149                .collect::<Vec<_>>(),
1150            vec![false, false, true]
1151        );
1152    }
1153
1154    #[test]
1155    fn test_resolve_since_timestamp_upstream_log_epochs_with_pending_barriers() {
1156        let upstream_log_epochs = vec![(vec![45, 50], 55)];
1157        let pending_upstream_barriers = [
1158            BarrierInfo {
1159                prev_epoch: TracedEpoch::new(Epoch(55)),
1160                curr_epoch: TracedEpoch::new(Epoch(60)),
1161                kind: BarrierKind::Checkpoint(vec![55]),
1162            },
1163            BarrierInfo {
1164                prev_epoch: TracedEpoch::new(Epoch(60)),
1165                curr_epoch: TracedEpoch::new(Epoch(70)),
1166                kind: BarrierKind::Barrier,
1167            },
1168        ];
1169
1170        let (initial_barrier, barriers) =
1171            CreatingStreamingJobControl::resolve_since_timestamp_upstream_log_epochs(
1172                &upstream_log_epochs,
1173                pending_upstream_barriers.iter(),
1174                40,
1175                70,
1176            )
1177            .unwrap();
1178
1179        assert_eq!(
1180            (initial_barrier.prev_epoch(), initial_barrier.curr_epoch()),
1181            (40, 45)
1182        );
1183        assert!(initial_barrier.kind.is_checkpoint());
1184        assert_eq!(
1185            barriers
1186                .iter()
1187                .map(|barrier| (barrier.prev_epoch(), barrier.curr_epoch()))
1188                .collect::<Vec<_>>(),
1189            vec![(45, 50), (50, 55), (55, 60), (60, 70)]
1190        );
1191        assert_eq!(
1192            barriers
1193                .iter()
1194                .map(|barrier| barrier.kind.is_checkpoint())
1195                .collect::<Vec<_>>(),
1196            vec![false, false, true, false]
1197        );
1198    }
1199
1200    #[test]
1201    fn test_resolve_since_timestamp_upstream_log_epochs_with_gap_before_pending_barriers() {
1202        let upstream_log_epochs = vec![(vec![61, 62, 63, 64], 65)];
1203        let pending_upstream_barriers = [
1204            BarrierInfo {
1205                prev_epoch: TracedEpoch::new(Epoch(65)),
1206                curr_epoch: TracedEpoch::new(Epoch(66)),
1207                kind: BarrierKind::Checkpoint(vec![65]),
1208            },
1209            BarrierInfo {
1210                prev_epoch: TracedEpoch::new(Epoch(66)),
1211                curr_epoch: TracedEpoch::new(Epoch(67)),
1212                kind: BarrierKind::Barrier,
1213            },
1214            BarrierInfo {
1215                prev_epoch: TracedEpoch::new(Epoch(67)),
1216                curr_epoch: TracedEpoch::new(Epoch(68)),
1217                kind: BarrierKind::Barrier,
1218            },
1219            BarrierInfo {
1220                prev_epoch: TracedEpoch::new(Epoch(68)),
1221                curr_epoch: TracedEpoch::new(Epoch(69)),
1222                kind: BarrierKind::Barrier,
1223            },
1224            BarrierInfo {
1225                prev_epoch: TracedEpoch::new(Epoch(69)),
1226                curr_epoch: TracedEpoch::new(Epoch(70)),
1227                kind: BarrierKind::Barrier,
1228            },
1229        ];
1230
1231        let (initial_barrier, barriers) =
1232            CreatingStreamingJobControl::resolve_since_timestamp_upstream_log_epochs(
1233                &upstream_log_epochs,
1234                pending_upstream_barriers.iter(),
1235                60,
1236                70,
1237            )
1238            .unwrap();
1239
1240        assert_eq!(
1241            (initial_barrier.prev_epoch(), initial_barrier.curr_epoch()),
1242            (60, 61)
1243        );
1244        assert!(initial_barrier.kind.is_checkpoint());
1245        assert_eq!(
1246            barriers
1247                .iter()
1248                .map(|barrier| (barrier.prev_epoch(), barrier.curr_epoch()))
1249                .collect::<Vec<_>>(),
1250            vec![
1251                (61, 62),
1252                (62, 63),
1253                (63, 64),
1254                (64, 65),
1255                (65, 66),
1256                (66, 67),
1257                (67, 68),
1258                (68, 69),
1259                (69, 70)
1260            ]
1261        );
1262        assert_eq!(
1263            barriers
1264                .iter()
1265                .map(|barrier| barrier.kind.is_checkpoint())
1266                .collect::<Vec<_>>(),
1267            vec![false, false, false, false, true, false, false, false, false]
1268        );
1269    }
1270
1271    #[test]
1272    fn test_resolve_since_timestamp_upstream_log_epochs_without_pending_barriers() {
1273        let upstream_log_epochs = vec![(vec![61, 62, 63, 64], 65)];
1274
1275        let (initial_barrier, barriers) =
1276            CreatingStreamingJobControl::resolve_since_timestamp_upstream_log_epochs(
1277                &upstream_log_epochs,
1278                [].iter(),
1279                60,
1280                66,
1281            )
1282            .unwrap();
1283
1284        assert_eq!(
1285            (initial_barrier.prev_epoch(), initial_barrier.curr_epoch()),
1286            (60, 61)
1287        );
1288        assert!(initial_barrier.kind.is_checkpoint());
1289        assert_eq!(
1290            barriers
1291                .iter()
1292                .map(|barrier| (barrier.prev_epoch(), barrier.curr_epoch()))
1293                .collect::<Vec<_>>(),
1294            vec![(61, 62), (62, 63), (63, 64), (64, 65), (65, 66)]
1295        );
1296        assert_eq!(
1297            barriers
1298                .iter()
1299                .map(|barrier| barrier.kind.is_checkpoint())
1300                .collect::<Vec<_>>(),
1301            vec![false, false, false, false, true]
1302        );
1303    }
1304}