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