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