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