Skip to main content

risingwave_meta/barrier/checkpoint/
control.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::hash_map::Entry;
16use std::collections::{HashMap, HashSet};
17use std::future::{Future, poll_fn};
18use std::ops::Bound::{Excluded, Unbounded};
19use std::sync::atomic::AtomicU32;
20use std::task::Poll;
21
22use anyhow::anyhow;
23use fail::fail_point;
24use itertools::Itertools;
25use risingwave_common::catalog::{DatabaseId, TableId};
26use risingwave_common::id::JobId;
27use risingwave_common::metrics::{LabelGuardedHistogram, LabelGuardedIntGauge};
28use risingwave_common::util::epoch::{Epoch, EpochPair};
29use risingwave_common::util::stream_graph_visitor::visit_stream_node_cont;
30use risingwave_meta_model::WorkerId;
31use risingwave_pb::common::WorkerNode;
32use risingwave_pb::hummock::HummockVersionStats;
33use risingwave_pb::id::{FragmentId, PartialGraphId};
34use risingwave_pb::stream_plan::DispatcherType as PbDispatcherType;
35use risingwave_pb::stream_plan::stream_node::NodeBody;
36use risingwave_pb::stream_service::BarrierCompleteResponse;
37use risingwave_pb::stream_service::streaming_control_stream_response::ResetPartialGraphResponse;
38use tracing::{debug, warn};
39
40use crate::barrier::cdc_progress::CdcProgress;
41use crate::barrier::checkpoint::independent_job::{
42    BatchRefreshJobTriggerContext, IndependentCheckpointJobControl,
43};
44use crate::barrier::checkpoint::recovery::{
45    DatabaseRecoveringState, DatabaseStatusAction, EnterInitializing, EnterRunning,
46    RecoveringStateAction,
47};
48use crate::barrier::checkpoint::state::{ApplyCommandInfo, BarrierWorkerState};
49use crate::barrier::complete_task::{BarrierCompleteOutput, CompleteBarrierTask};
50use crate::barrier::info::{InflightDatabaseInfo, SharedActorInfos};
51use crate::barrier::notifier::Notifier;
52use crate::barrier::partial_graph::{CollectedBarrier, PartialGraphManager, PartialGraphStat};
53use crate::barrier::progress::TrackingJob;
54use crate::barrier::rpc::{from_partial_graph_id, to_partial_graph_id};
55use crate::barrier::schedule::{NewBarrier, PeriodicBarriers};
56use crate::barrier::utils::{BarrierItemCollector, collect_independent_job_commit_epoch_info};
57use crate::barrier::{
58    BackfillProgress, Command, CreateStreamingJobType, FragmentBackfillProgress, Reschedule,
59};
60use crate::controller::fragment::InflightFragmentInfo;
61use crate::controller::scale::{build_no_shuffle_fragment_graph_edges, find_no_shuffle_graphs};
62use crate::manager::MetaSrvEnv;
63
64fn fragment_has_online_unreschedulable_scan(fragment: &InflightFragmentInfo) -> bool {
65    let mut has_unreschedulable_scan = false;
66    visit_stream_node_cont(&fragment.nodes, |node| {
67        if let Some(NodeBody::StreamScan(stream_scan)) = node.node_body.as_ref() {
68            let scan_type = stream_scan.stream_scan_type();
69            if !scan_type.is_reschedulable(true) {
70                has_unreschedulable_scan = true;
71                return false;
72            }
73        }
74        true
75    });
76    has_unreschedulable_scan
77}
78
79fn collect_fragment_upstream_fragment_ids(
80    fragment: &InflightFragmentInfo,
81    upstream_fragment_ids: &mut HashSet<FragmentId>,
82) {
83    visit_stream_node_cont(&fragment.nodes, |node| {
84        if let Some(NodeBody::Merge(merge)) = node.node_body.as_ref() {
85            upstream_fragment_ids.insert(merge.upstream_fragment_id);
86        }
87        true
88    });
89}
90
91use crate::model::ActorId;
92use crate::rpc::metrics::GLOBAL_META_METRICS;
93use crate::{MetaError, MetaResult};
94
95pub(crate) struct CheckpointControl {
96    pub(crate) env: MetaSrvEnv,
97    pub(super) databases: HashMap<DatabaseId, DatabaseCheckpointControlStatus>,
98    pub(super) hummock_version_stats: HummockVersionStats,
99    /// The max barrier nums in flight
100    pub(crate) in_flight_barrier_nums: usize,
101}
102
103impl CheckpointControl {
104    pub fn new(env: MetaSrvEnv) -> Self {
105        Self {
106            in_flight_barrier_nums: env.opts.in_flight_barrier_nums,
107            env,
108            databases: Default::default(),
109            hummock_version_stats: Default::default(),
110        }
111    }
112
113    pub(crate) fn recover(
114        databases: HashMap<DatabaseId, DatabaseCheckpointControl>,
115        failed_databases: HashMap<DatabaseId, HashSet<PartialGraphId>>, /* `database_id` -> set of resetting partial graph ids */
116        hummock_version_stats: HummockVersionStats,
117        env: MetaSrvEnv,
118    ) -> Self {
119        env.shared_actor_infos()
120            .retain_databases(databases.keys().chain(failed_databases.keys()).cloned());
121        Self {
122            in_flight_barrier_nums: env.opts.in_flight_barrier_nums,
123            env,
124            databases: databases
125                .into_iter()
126                .map(|(database_id, control)| {
127                    (
128                        database_id,
129                        DatabaseCheckpointControlStatus::Running(control),
130                    )
131                })
132                .chain(failed_databases.into_iter().map(
133                    |(database_id, resetting_partial_graphs)| {
134                        (
135                            database_id,
136                            DatabaseCheckpointControlStatus::Recovering(
137                                DatabaseRecoveringState::new_resetting(
138                                    database_id,
139                                    resetting_partial_graphs,
140                                ),
141                            ),
142                        )
143                    },
144                ))
145                .collect(),
146            hummock_version_stats,
147        }
148    }
149
150    pub(crate) fn ack_completed(
151        &mut self,
152        partial_graph_manager: &mut PartialGraphManager,
153        output: BarrierCompleteOutput,
154    ) {
155        self.hummock_version_stats = output.hummock_version_stats;
156        for (database_id, (command_prev_epoch, independent_job_epochs)) in output.epochs_to_ack {
157            self.databases
158                .get_mut(&database_id)
159                .expect("should exist")
160                .expect_running("should have wait for completing command before enter recovery")
161                .ack_completed(
162                    partial_graph_manager,
163                    command_prev_epoch,
164                    independent_job_epochs,
165                );
166        }
167    }
168
169    pub(crate) fn next_complete_barrier_task(
170        &mut self,
171        periodic_barriers: &mut PeriodicBarriers,
172        partial_graph_manager: &mut PartialGraphManager,
173    ) -> Option<CompleteBarrierTask> {
174        let mut task = None;
175        for database in self.databases.values_mut() {
176            let Some(database) = database.running_state_mut() else {
177                continue;
178            };
179            database.next_complete_barrier_task(
180                periodic_barriers,
181                partial_graph_manager,
182                &mut task,
183                &self.hummock_version_stats,
184            );
185        }
186        task
187    }
188
189    pub(crate) fn barrier_collected(
190        &mut self,
191        partial_graph_id: PartialGraphId,
192        collected_barrier: CollectedBarrier<'_>,
193        periodic_barriers: &mut PeriodicBarriers,
194    ) -> MetaResult<()> {
195        let (database_id, _) = from_partial_graph_id(partial_graph_id);
196        let database_status = self.databases.get_mut(&database_id).expect("should exist");
197        match database_status {
198            DatabaseCheckpointControlStatus::Running(database) => {
199                database.barrier_collected(partial_graph_id, collected_barrier, periodic_barriers)
200            }
201            DatabaseCheckpointControlStatus::Recovering(_) => {
202                if cfg!(debug_assertions) {
203                    panic!(
204                        "receive collected barrier {:?} on recovering database {} from partial graph {}",
205                        collected_barrier, database_id, partial_graph_id
206                    );
207                } else {
208                    warn!(?collected_barrier, %partial_graph_id, "ignore collected barrier on recovering database");
209                }
210                Ok(())
211            }
212        }
213    }
214
215    pub(crate) fn recovering_databases(&self) -> impl Iterator<Item = DatabaseId> + '_ {
216        self.databases.iter().filter_map(|(database_id, database)| {
217            database.running_state().is_none().then_some(*database_id)
218        })
219    }
220
221    pub(crate) fn running_databases(&self) -> impl Iterator<Item = DatabaseId> + '_ {
222        self.databases.iter().filter_map(|(database_id, database)| {
223            database.running_state().is_some().then_some(*database_id)
224        })
225    }
226
227    pub(crate) fn database_info(&self, database_id: DatabaseId) -> Option<&InflightDatabaseInfo> {
228        self.databases
229            .get(&database_id)
230            .and_then(|database| database.running_state())
231            .map(|database| &database.database_info)
232    }
233
234    /// return Some(failed `database_id` -> `err`)
235    pub(crate) fn handle_new_barrier(
236        &mut self,
237        new_barrier: NewBarrier,
238        partial_graph_manager: &mut PartialGraphManager,
239        worker_nodes: &HashMap<WorkerId, WorkerNode>,
240    ) -> MetaResult<()> {
241        let NewBarrier {
242            database_id,
243            command,
244            span,
245            checkpoint,
246        } = new_barrier;
247
248        if let Some((mut command, notifiers)) = command {
249            if let &mut Command::CreateStreamingJob {
250                ref mut cross_db_snapshot_backfill_info,
251                ref info,
252                ..
253            } = &mut command
254            {
255                for (table_id, snapshot_epoch) in
256                    &mut cross_db_snapshot_backfill_info.upstream_mv_table_id_to_backfill_epoch
257                {
258                    for database in self.databases.values() {
259                        if let Some(database) = database.running_state()
260                            && database.database_info.contains_job(table_id.as_job_id())
261                        {
262                            if let Some(committed_epoch) = database.committed_epoch {
263                                *snapshot_epoch = Some(committed_epoch);
264                            }
265                            break;
266                        }
267                    }
268                    if snapshot_epoch.is_none() {
269                        let table_id = *table_id;
270                        warn!(
271                            ?cross_db_snapshot_backfill_info,
272                            ?table_id,
273                            ?info,
274                            "database of cross db upstream table not found"
275                        );
276                        let err: MetaError =
277                            anyhow!("database of cross db upstream table {} not found", table_id)
278                                .into();
279                        for notifier in notifiers {
280                            notifier.notify_start_failed(err.clone());
281                        }
282
283                        return Ok(());
284                    }
285                }
286            }
287
288            let database = match self.databases.entry(database_id) {
289                Entry::Occupied(entry) => entry
290                    .into_mut()
291                    .expect_running("should not have command when not running"),
292                Entry::Vacant(entry) => match &command {
293                    Command::CreateStreamingJob { info, job_type, .. } => {
294                        let CreateStreamingJobType::Normal = job_type else {
295                            if cfg!(debug_assertions) {
296                                panic!(
297                                    "unexpected first job of type {job_type:?} with info {info:?}"
298                                );
299                            } else {
300                                for notifier in notifiers {
301                                    notifier.notify_start_failed(anyhow!("unexpected job_type {job_type:?} for first job {} in database {database_id}", info.streaming_job.id()).into());
302                                }
303                                return Ok(());
304                            }
305                        };
306                        let new_database = DatabaseCheckpointControl::new(
307                            database_id,
308                            self.env.shared_actor_infos().clone(),
309                        );
310                        let adder = partial_graph_manager.add_partial_graph(
311                            to_partial_graph_id(database_id, None),
312                            DatabaseCheckpointControlMetrics::new(database_id),
313                        );
314                        adder.added();
315                        entry
316                            .insert(DatabaseCheckpointControlStatus::Running(new_database))
317                            .expect_running("just initialized as running")
318                    }
319                    Command::Flush
320                    | Command::Pause
321                    | Command::Resume
322                    | Command::DropStreamingJobs { .. }
323                    | Command::DropSubscription { .. } => {
324                        for mut notifier in notifiers {
325                            notifier.notify_started();
326                            notifier.notify_collected();
327                        }
328                        warn!(?command, "skip command for empty database");
329                        return Ok(());
330                    }
331                    Command::RescheduleIntent { .. }
332                    | Command::ReplaceStreamJob(_)
333                    | Command::SourceChangeSplit(_)
334                    | Command::Throttle { .. }
335                    | Command::CreateSubscription { .. }
336                    | Command::AlterSubscriptionRetention { .. }
337                    | Command::ConnectorPropsChange(_)
338                    | Command::Refresh { .. }
339                    | Command::ListFinish { .. }
340                    | Command::LoadFinish { .. }
341                    | Command::ResetSource { .. }
342                    | Command::ResumeBackfill { .. }
343                    | Command::InjectSourceOffsets { .. } => {
344                        if cfg!(debug_assertions) {
345                            panic!(
346                                "new database graph info can only be created for normal creating streaming job, but get command: {} {:?}",
347                                database_id, command
348                            )
349                        } else {
350                            warn!(%database_id, ?command, "database not exist when handling command");
351                            for notifier in notifiers {
352                                notifier.notify_start_failed(anyhow!("database {database_id} not exist when handling command {command:?}").into());
353                            }
354                            return Ok(());
355                        }
356                    }
357                },
358            };
359
360            database.handle_new_barrier(
361                Some((command, notifiers)),
362                checkpoint,
363                span,
364                partial_graph_manager,
365                &self.hummock_version_stats,
366                worker_nodes,
367            )
368        } else {
369            let database = match self.databases.entry(database_id) {
370                Entry::Occupied(entry) => entry.into_mut(),
371                Entry::Vacant(_) => {
372                    // If it does not exist in the HashMap yet, it means that the first streaming
373                    // job has not been created, and we do not need to send a barrier.
374                    return Ok(());
375                }
376            };
377            let Some(database) = database.running_state_mut() else {
378                // Skip new barrier for database which is not running.
379                return Ok(());
380            };
381            if partial_graph_manager.inflight_barrier_num(database.partial_graph_id)
382                >= self.in_flight_barrier_nums
383            {
384                // Skip new barrier with no explicit command when the database should pause inject additional barrier
385                return Ok(());
386            }
387            database.handle_new_barrier(
388                None,
389                checkpoint,
390                span,
391                partial_graph_manager,
392                &self.hummock_version_stats,
393                worker_nodes,
394            )
395        }
396    }
397
398    pub(crate) fn gen_backfill_progress(&self) -> HashMap<JobId, BackfillProgress> {
399        let mut progress = HashMap::new();
400        for status in self.databases.values() {
401            let Some(database_checkpoint_control) = status.running_state() else {
402                continue;
403            };
404            // Progress of normal backfill
405            progress.extend(
406                database_checkpoint_control
407                    .database_info
408                    .gen_backfill_progress(),
409            );
410            // Progress of independent checkpoint jobs
411            for (job_id, job) in &database_checkpoint_control.independent_checkpoint_job_controls {
412                if let Some(p) = job.gen_backfill_progress() {
413                    progress.insert(*job_id, p);
414                }
415            }
416        }
417        progress
418    }
419
420    pub(crate) fn gen_fragment_backfill_progress(&self) -> Vec<FragmentBackfillProgress> {
421        let mut progress = Vec::new();
422        for status in self.databases.values() {
423            let Some(database_checkpoint_control) = status.running_state() else {
424                continue;
425            };
426            progress.extend(
427                database_checkpoint_control
428                    .database_info
429                    .gen_fragment_backfill_progress(),
430            );
431            for job in database_checkpoint_control
432                .independent_checkpoint_job_controls
433                .values()
434            {
435                progress.extend(job.gen_fragment_backfill_progress());
436            }
437        }
438        progress
439    }
440
441    pub(crate) fn gen_cdc_progress(&self) -> HashMap<JobId, CdcProgress> {
442        let mut progress = HashMap::new();
443        for status in self.databases.values() {
444            let Some(database_checkpoint_control) = status.running_state() else {
445                continue;
446            };
447            // Progress of normal backfill
448            progress.extend(database_checkpoint_control.database_info.gen_cdc_progress());
449        }
450        progress
451    }
452
453    pub(crate) fn databases_failed_at_worker_err(
454        &mut self,
455        worker_id: WorkerId,
456    ) -> impl Iterator<Item = DatabaseId> + '_ {
457        self.databases
458            .iter_mut()
459            .filter_map(
460                move |(database_id, database_status)| match database_status {
461                    DatabaseCheckpointControlStatus::Running(control) => {
462                        if !control.is_valid_after_worker_err(worker_id) {
463                            Some(*database_id)
464                        } else {
465                            None
466                        }
467                    }
468                    DatabaseCheckpointControlStatus::Recovering(state) => {
469                        if !state.is_valid_after_worker_err(worker_id) {
470                            Some(*database_id)
471                        } else {
472                            None
473                        }
474                    }
475                },
476            )
477    }
478
479    // ── Batch refresh trigger helpers (delegating to DatabaseCheckpointControl) ──
480
481    pub(crate) fn get_batch_refresh_trigger_info(
482        &self,
483        database_id: DatabaseId,
484        job_id: JobId,
485    ) -> u64 {
486        let database = self
487            .databases
488            .get(&database_id)
489            .and_then(|s| s.running_state())
490            .expect("database should be running for batch refresh trigger");
491        database.get_batch_refresh_trigger_info(job_id)
492    }
493
494    pub(crate) fn start_batch_refresh_run(
495        &mut self,
496        database_id: DatabaseId,
497        job_id: JobId,
498        context: &BatchRefreshJobTriggerContext,
499        worker_nodes: &HashMap<WorkerId, WorkerNode>,
500        actor_id_counter: &AtomicU32,
501        partial_graph_manager: &mut PartialGraphManager,
502    ) -> MetaResult<bool> {
503        let database = self
504            .databases
505            .get_mut(&database_id)
506            .and_then(|s| s.running_state_mut())
507            .expect("database should be running");
508        database.start_batch_refresh_run(
509            job_id,
510            context,
511            worker_nodes,
512            actor_id_counter,
513            partial_graph_manager,
514        )
515    }
516
517    pub(crate) fn apply_batch_refresh_fragment_infos(
518        &mut self,
519        database_id: DatabaseId,
520        job_id: JobId,
521    ) {
522        let database = self
523            .databases
524            .get_mut(&database_id)
525            .and_then(|s| s.running_state_mut())
526            .expect("database should be running");
527        let br_job = match database
528            .independent_checkpoint_job_controls
529            .get(&job_id)
530            .expect("job should exist")
531        {
532            IndependentCheckpointJobControl::BatchRefresh(job) => job,
533            _ => panic!("expected batch refresh job"),
534        };
535        if let Some(fragment_infos) = br_job.fragment_infos() {
536            database
537                .database_info
538                .shared_actor_infos
539                .upsert(database_id, fragment_infos.values().map(|f| (f, job_id)));
540        }
541    }
542}
543
544pub(crate) enum CheckpointControlEvent<'a> {
545    EnteringInitializing(DatabaseStatusAction<'a, EnterInitializing>),
546    EnteringRunning(DatabaseStatusAction<'a, EnterRunning>),
547    /// A batch refresh job is idle and its upstream has advanced past the refresh interval.
548    /// Carries owned values so the async handler can call into context without borrowing self.
549    BatchRefreshTrigger {
550        database_id: DatabaseId,
551        job_id: JobId,
552    },
553}
554
555impl CheckpointControl {
556    pub(crate) fn on_partial_graph_reset(
557        &mut self,
558        partial_graph_id: PartialGraphId,
559        reset_resps: HashMap<WorkerId, ResetPartialGraphResponse>,
560    ) {
561        let (database_id, independent_job_id) = from_partial_graph_id(partial_graph_id);
562        match self.databases.get_mut(&database_id).expect("should exist") {
563            DatabaseCheckpointControlStatus::Running(database) => {
564                if let Some(independent_job_id) = independent_job_id {
565                    match database
566                        .independent_checkpoint_job_controls
567                        .remove(&independent_job_id)
568                    {
569                        Some(independent_job) => {
570                            independent_job.on_partial_graph_reset();
571                        }
572                        None => {
573                            if cfg!(debug_assertions) {
574                                panic!(
575                                    "receive reset partial graph resp on non-existing independent job {independent_job_id} in database {database_id}"
576                                )
577                            }
578                            warn!(
579                                %database_id,
580                                %independent_job_id,
581                                "ignore reset partial graph resp on non-existing independent job on running database"
582                            );
583                        }
584                    }
585                } else {
586                    unreachable!("should not receive reset database resp when database running")
587                }
588            }
589            DatabaseCheckpointControlStatus::Recovering(state) => {
590                state.on_partial_graph_reset(partial_graph_id, reset_resps);
591            }
592        }
593    }
594
595    pub(crate) fn on_partial_graph_initialized(
596        &mut self,
597        partial_graph_id: PartialGraphId,
598        partial_graph_manager: &mut PartialGraphManager,
599    ) -> MetaResult<()> {
600        let (database_id, independent_job_id) = from_partial_graph_id(partial_graph_id);
601        match self.databases.get_mut(&database_id).expect("should exist") {
602            DatabaseCheckpointControlStatus::Running(database) => {
603                let Some(independent_job_id) = independent_job_id else {
604                    unreachable!("database partial graph should not initialize when running")
605                };
606                let job = database
607                    .independent_checkpoint_job_controls
608                    .get_mut(&independent_job_id)
609                    .expect("independent job should exist");
610                match job {
611                    IndependentCheckpointJobControl::BatchRefresh(job) => {
612                        job.on_log_store_initialized(partial_graph_manager)
613                    }
614                    IndependentCheckpointJobControl::CreatingStreamingJob(_) => {
615                        unreachable!("creating streaming job should not initialize when running")
616                    }
617                }
618            }
619            DatabaseCheckpointControlStatus::Recovering(state) => {
620                state.partial_graph_initialized(partial_graph_id);
621                Ok(())
622            }
623        }
624    }
625
626    pub(crate) fn next_event(
627        &mut self,
628    ) -> impl Future<Output = CheckpointControlEvent<'_>> + Send + '_ {
629        let mut this = Some(self);
630        poll_fn(move |cx| {
631            let Some(this_mut) = this.as_mut() else {
632                unreachable!("should not be polled after poll ready")
633            };
634            for (&database_id, database_status) in &mut this_mut.databases {
635                match database_status {
636                    DatabaseCheckpointControlStatus::Running(database) => {
637                        // Check if any idle batch refresh job should start a refresh run.
638                        if let Some(committed_epoch) = database.committed_epoch {
639                            for (job_id, job) in &database.independent_checkpoint_job_controls {
640                                if let IndependentCheckpointJobControl::BatchRefresh(br_job) = job
641                                    && br_job.should_start_refresh(committed_epoch)
642                                {
643                                    let job_id = *job_id;
644                                    let _ = this.take().expect("checked Some");
645                                    return Poll::Ready(
646                                        CheckpointControlEvent::BatchRefreshTrigger {
647                                            database_id,
648                                            job_id,
649                                        },
650                                    );
651                                }
652                            }
653                        }
654                    }
655                    DatabaseCheckpointControlStatus::Recovering(state) => {
656                        let poll_result = state.poll_next_event(cx);
657                        if let Poll::Ready(action) = poll_result {
658                            let this = this.take().expect("checked Some");
659                            return Poll::Ready(match action {
660                                RecoveringStateAction::EnterInitializing(reset_workers) => {
661                                    CheckpointControlEvent::EnteringInitializing(
662                                        this.new_database_status_action(
663                                            database_id,
664                                            EnterInitializing(reset_workers),
665                                        ),
666                                    )
667                                }
668                                RecoveringStateAction::EnterRunning => {
669                                    CheckpointControlEvent::EnteringRunning(
670                                        this.new_database_status_action(database_id, EnterRunning),
671                                    )
672                                }
673                            });
674                        }
675                    }
676                }
677            }
678            Poll::Pending
679        })
680    }
681}
682
683pub(crate) enum DatabaseCheckpointControlStatus {
684    Running(DatabaseCheckpointControl),
685    Recovering(DatabaseRecoveringState),
686}
687
688impl DatabaseCheckpointControlStatus {
689    fn running_state(&self) -> Option<&DatabaseCheckpointControl> {
690        match self {
691            DatabaseCheckpointControlStatus::Running(state) => Some(state),
692            DatabaseCheckpointControlStatus::Recovering(_) => None,
693        }
694    }
695
696    fn running_state_mut(&mut self) -> Option<&mut DatabaseCheckpointControl> {
697        match self {
698            DatabaseCheckpointControlStatus::Running(state) => Some(state),
699            DatabaseCheckpointControlStatus::Recovering(_) => None,
700        }
701    }
702
703    fn expect_running(&mut self, reason: &'static str) -> &mut DatabaseCheckpointControl {
704        match self {
705            DatabaseCheckpointControlStatus::Running(state) => state,
706            DatabaseCheckpointControlStatus::Recovering(_) => {
707                panic!("should be at running: {}", reason)
708            }
709        }
710    }
711}
712
713pub(in crate::barrier) struct DatabaseCheckpointControlMetrics {
714    barrier_latency: LabelGuardedHistogram,
715    in_flight_barrier_nums: LabelGuardedIntGauge,
716    all_barrier_nums: LabelGuardedIntGauge,
717}
718
719impl DatabaseCheckpointControlMetrics {
720    pub(in crate::barrier) fn new(database_id: DatabaseId) -> Self {
721        let database_id_str = database_id.to_string();
722        let barrier_latency = GLOBAL_META_METRICS
723            .barrier_latency
724            .with_guarded_label_values(&[&database_id_str]);
725        let in_flight_barrier_nums = GLOBAL_META_METRICS
726            .in_flight_barrier_nums
727            .with_guarded_label_values(&[&database_id_str]);
728        let all_barrier_nums = GLOBAL_META_METRICS
729            .all_barrier_nums
730            .with_guarded_label_values(&[&database_id_str]);
731        Self {
732            barrier_latency,
733            in_flight_barrier_nums,
734            all_barrier_nums,
735        }
736    }
737}
738
739impl PartialGraphStat for DatabaseCheckpointControlMetrics {
740    fn observe_barrier_latency(&self, _epoch: EpochPair, barrier_latency_secs: f64) {
741        self.barrier_latency.observe(barrier_latency_secs);
742    }
743
744    fn observe_barrier_num(&self, inflight_barrier_num: usize, collected_barrier_num: usize) {
745        self.in_flight_barrier_nums.set(inflight_barrier_num as _);
746        self.all_barrier_nums
747            .set((inflight_barrier_num + collected_barrier_num) as _);
748    }
749}
750
751/// Controls the concurrent execution of commands.
752pub(in crate::barrier) struct DatabaseCheckpointControl {
753    pub(super) database_id: DatabaseId,
754    partial_graph_id: PartialGraphId,
755    pub(super) state: BarrierWorkerState,
756
757    finishing_jobs_collector:
758        BarrierItemCollector<JobId, (Vec<BarrierCompleteResponse>, TrackingJob), ()>,
759    /// The barrier that are completing.
760    completing_barrier: Option<EpochPair>,
761
762    committed_epoch: Option<u64>,
763
764    /// `None` while the database has no streaming job, so that a frozen timestamp does not
765    /// render as an ever-growing barrier pending time.
766    last_committed_barrier_time: Option<LabelGuardedIntGauge>,
767
768    pub(super) database_info: InflightDatabaseInfo,
769    pub independent_checkpoint_job_controls: HashMap<JobId, IndependentCheckpointJobControl>,
770}
771
772impl DatabaseCheckpointControl {
773    fn new(database_id: DatabaseId, shared_actor_infos: SharedActorInfos) -> Self {
774        Self {
775            database_id,
776            partial_graph_id: to_partial_graph_id(database_id, None),
777            state: BarrierWorkerState::new(),
778            finishing_jobs_collector: BarrierItemCollector::new(false),
779            completing_barrier: None,
780            committed_epoch: None,
781            last_committed_barrier_time: None,
782            database_info: InflightDatabaseInfo::empty(database_id, shared_actor_infos),
783            independent_checkpoint_job_controls: Default::default(),
784        }
785    }
786
787    pub(crate) fn recovery(
788        database_id: DatabaseId,
789        state: BarrierWorkerState,
790        committed_epoch: u64,
791        database_info: InflightDatabaseInfo,
792        independent_checkpoint_job_controls: HashMap<JobId, IndependentCheckpointJobControl>,
793    ) -> Self {
794        Self {
795            database_id,
796            partial_graph_id: to_partial_graph_id(database_id, None),
797            state,
798            finishing_jobs_collector: BarrierItemCollector::new(false),
799            completing_barrier: None,
800            committed_epoch: Some(committed_epoch),
801            last_committed_barrier_time: None,
802            database_info,
803            independent_checkpoint_job_controls,
804        }
805    }
806
807    pub(crate) fn is_valid_after_worker_err(&self, worker_id: WorkerId) -> bool {
808        !self.database_info.contains_worker(worker_id as _)
809            && self
810                .independent_checkpoint_job_controls
811                .values()
812                .all(|job| {
813                    job.fragment_infos()
814                        .map(|fragment_infos| {
815                            !InflightFragmentInfo::contains_worker(
816                                fragment_infos.values(),
817                                worker_id,
818                            )
819                        })
820                        .unwrap_or(true)
821                })
822    }
823
824    /// Enqueue a barrier command
825    fn enqueue_command(&mut self, epoch: EpochPair, independent_jobs_to_wait: HashSet<JobId>) {
826        let prev_epoch = epoch.prev;
827        tracing::trace!(prev_epoch, ?independent_jobs_to_wait, "enqueue command");
828        if !independent_jobs_to_wait.is_empty() {
829            self.finishing_jobs_collector
830                .enqueue(epoch, independent_jobs_to_wait, ());
831        }
832    }
833
834    /// Change the state of this `prev_epoch` to `Completed`. Return continuous nodes
835    /// with `Completed` starting from first node [`Completed`..`InFlight`) and remove them.
836    fn barrier_collected(
837        &mut self,
838        partial_graph_id: PartialGraphId,
839        collected_barrier: CollectedBarrier<'_>,
840        periodic_barriers: &mut PeriodicBarriers,
841    ) -> MetaResult<()> {
842        let prev_epoch = collected_barrier.epoch.prev;
843        tracing::trace!(
844            prev_epoch,
845            partial_graph_id = %partial_graph_id,
846            "barrier collected"
847        );
848        let (database_id, independent_job_id) = from_partial_graph_id(partial_graph_id);
849        assert_eq!(self.database_id, database_id);
850        if let Some(independent_job_id) = independent_job_id {
851            let job = self
852                .independent_checkpoint_job_controls
853                .get_mut(&independent_job_id)
854                .expect("should exist");
855            let should_force_checkpoint = job.collect(collected_barrier);
856            if should_force_checkpoint {
857                periodic_barriers.force_checkpoint_in_next_barrier(self.database_id);
858            }
859        }
860        Ok(())
861    }
862}
863
864impl DatabaseCheckpointControl {
865    /// return creating job table fragment id -> (backfill progress epoch , {`upstream_mv_table_id`})
866    fn collect_backfill_pinned_upstream_log_epoch(
867        &self,
868    ) -> HashMap<JobId, (u64, HashSet<TableId>)> {
869        self.independent_checkpoint_job_controls
870            .iter()
871            .map(|(job_id, job)| (*job_id, job.pinned_upstream_log_epoch()))
872            .collect()
873    }
874
875    fn collect_no_shuffle_fragment_relations_for_reschedule_check(
876        &self,
877    ) -> Vec<(FragmentId, FragmentId)> {
878        let mut no_shuffle_relations = Vec::new();
879        for fragment in self.database_info.fragment_infos() {
880            let downstream_fragment_id = fragment.fragment_id;
881            visit_stream_node_cont(&fragment.nodes, |node| {
882                if let Some(NodeBody::Merge(merge)) = node.node_body.as_ref()
883                    && merge.upstream_dispatcher_type == PbDispatcherType::NoShuffle as i32
884                {
885                    no_shuffle_relations.push((merge.upstream_fragment_id, downstream_fragment_id));
886                }
887                true
888            });
889        }
890
891        for job in self.independent_checkpoint_job_controls.values() {
892            if let Some(fragment_infos) = job.fragment_infos() {
893                for fragment_info in fragment_infos.values() {
894                    let downstream_fragment_id = fragment_info.fragment_id;
895                    visit_stream_node_cont(&fragment_info.nodes, |node| {
896                        if let Some(NodeBody::Merge(merge)) = node.node_body.as_ref()
897                            && merge.upstream_dispatcher_type == PbDispatcherType::NoShuffle as i32
898                        {
899                            no_shuffle_relations
900                                .push((merge.upstream_fragment_id, downstream_fragment_id));
901                        }
902                        true
903                    });
904                }
905            }
906        }
907        no_shuffle_relations
908    }
909
910    fn collect_reschedule_blocked_jobs_for_independent_jobs_inflight(
911        &self,
912    ) -> MetaResult<HashSet<JobId>> {
913        let mut initial_blocked_fragment_ids = HashSet::new();
914        for job in self.independent_checkpoint_job_controls.values() {
915            if let Some(fragment_infos) = job.fragment_infos() {
916                for fragment_info in fragment_infos.values() {
917                    if fragment_has_online_unreschedulable_scan(fragment_info) {
918                        initial_blocked_fragment_ids.insert(fragment_info.fragment_id);
919                        collect_fragment_upstream_fragment_ids(
920                            fragment_info,
921                            &mut initial_blocked_fragment_ids,
922                        );
923                    }
924                }
925            }
926        }
927
928        let mut blocked_fragment_ids = initial_blocked_fragment_ids.clone();
929        if !initial_blocked_fragment_ids.is_empty() {
930            let no_shuffle_relations =
931                self.collect_no_shuffle_fragment_relations_for_reschedule_check();
932            let (forward_edges, backward_edges) =
933                build_no_shuffle_fragment_graph_edges(no_shuffle_relations);
934            let initial_blocked_fragment_ids: Vec<_> =
935                initial_blocked_fragment_ids.iter().copied().collect();
936            for ensemble in find_no_shuffle_graphs(
937                &initial_blocked_fragment_ids,
938                &forward_edges,
939                &backward_edges,
940            )? {
941                blocked_fragment_ids.extend(ensemble.fragments());
942            }
943        }
944
945        let mut blocked_job_ids = HashSet::new();
946        blocked_job_ids.extend(
947            blocked_fragment_ids
948                .into_iter()
949                .filter_map(|fragment_id| self.database_info.job_id_by_fragment(fragment_id)),
950        );
951        Ok(blocked_job_ids)
952    }
953
954    fn collect_reschedule_blocked_job_ids(
955        &self,
956        reschedules: &HashMap<FragmentId, Reschedule>,
957        fragment_actors: &HashMap<FragmentId, HashSet<ActorId>>,
958        blocked_job_ids: &HashSet<JobId>,
959    ) -> HashSet<JobId> {
960        let mut affected_fragment_ids: HashSet<FragmentId> = reschedules.keys().copied().collect();
961        affected_fragment_ids.extend(fragment_actors.keys().copied());
962        for reschedule in reschedules.values() {
963            affected_fragment_ids.extend(reschedule.downstream_fragment_ids.iter().copied());
964            affected_fragment_ids.extend(
965                reschedule
966                    .upstream_fragment_dispatcher_ids
967                    .iter()
968                    .map(|(fragment_id, _)| *fragment_id),
969            );
970        }
971
972        affected_fragment_ids
973            .into_iter()
974            .filter_map(|fragment_id| self.database_info.job_id_by_fragment(fragment_id))
975            .filter(|job_id| blocked_job_ids.contains(job_id))
976            .collect()
977    }
978
979    fn next_complete_barrier_task(
980        &mut self,
981        periodic_barriers: &mut PeriodicBarriers,
982        partial_graph_manager: &mut PartialGraphManager,
983        task: &mut Option<CompleteBarrierTask>,
984        hummock_version_stats: &HummockVersionStats,
985    ) {
986        // `Vec::new` is a const fn, and do not have memory allocation, and therefore is lightweight enough
987        let mut independent_jobs_task = vec![];
988        if let Some(committed_epoch) = self.committed_epoch {
989            // `Vec::new` is a const fn, and do not have memory allocation, and therefore is lightweight enough
990            let mut finished_jobs = Vec::new();
991            let min_upstream_inflight_barrier = partial_graph_manager
992                .first_inflight_barrier(self.partial_graph_id)
993                .map(|epoch| epoch.prev);
994            for (job_id, job) in &mut self.independent_checkpoint_job_controls {
995                match job {
996                    IndependentCheckpointJobControl::CreatingStreamingJob(creating_job) => {
997                        if let Some((epoch, resps, info, is_finish_epoch)) = creating_job
998                            .start_completing(
999                                partial_graph_manager,
1000                                min_upstream_inflight_barrier,
1001                                committed_epoch,
1002                            )
1003                        {
1004                            let resps = resps.into_values().collect_vec();
1005                            if is_finish_epoch {
1006                                assert!(info.notifiers.is_empty());
1007                                finished_jobs.push((*job_id, epoch, resps));
1008                                continue;
1009                            };
1010                            independent_jobs_task.push((*job_id, epoch, resps, info));
1011                        }
1012                    }
1013                    IndependentCheckpointJobControl::BatchRefresh(batch_refresh_job) => {
1014                        if let Some((epoch, resps, info, tracking_job)) =
1015                            batch_refresh_job.start_completing(partial_graph_manager)
1016                        {
1017                            let resps = resps.into_values().collect_vec();
1018                            if let Some(tracking_job) = tracking_job {
1019                                let task = task.get_or_insert_default();
1020                                task.finished_jobs.push(tracking_job);
1021                            }
1022                            independent_jobs_task.push((*job_id, epoch, resps, info));
1023                        }
1024                    }
1025                }
1026            }
1027            if !finished_jobs.is_empty() {
1028                partial_graph_manager.remove_partial_graphs(
1029                    finished_jobs
1030                        .iter()
1031                        .map(|(job_id, ..)| to_partial_graph_id(self.database_id, Some(*job_id)))
1032                        .collect(),
1033                );
1034            }
1035            for (job_id, epoch, resps) in finished_jobs {
1036                debug!(epoch, %job_id, "finish creating job");
1037                // It's safe to remove the creating job, because on CompleteJobType::Finished,
1038                // all previous barriers have been collected and completed.
1039                let Some(IndependentCheckpointJobControl::CreatingStreamingJob(
1040                    creating_streaming_job,
1041                )) = self.independent_checkpoint_job_controls.remove(&job_id)
1042                else {
1043                    panic!("finished job {job_id} should be a creating streaming job");
1044                };
1045                let tracking_job = creating_streaming_job.into_tracking_job();
1046                self.finishing_jobs_collector
1047                    .collect(epoch, job_id, (resps, tracking_job));
1048            }
1049        }
1050        let mut observed_non_checkpoint = false;
1051        self.finishing_jobs_collector.advance_collected();
1052        let epoch_end_bound = self
1053            .finishing_jobs_collector
1054            .first_inflight_epoch()
1055            .map_or(Unbounded, |epoch| Excluded(epoch.prev));
1056        if let Some((epoch, resps, info)) = partial_graph_manager.start_completing(
1057            self.partial_graph_id,
1058            epoch_end_bound,
1059            |_, resps, post_collect_command| {
1060                observed_non_checkpoint = true;
1061                self.handle_refresh_table_info(task, &resps);
1062                self.database_info.apply_collected_command(
1063                    &post_collect_command,
1064                    &resps,
1065                    hummock_version_stats,
1066                );
1067            },
1068        ) {
1069            self.handle_refresh_table_info(task, &resps);
1070            self.database_info.apply_collected_command(
1071                &info.post_collect_command,
1072                &resps,
1073                hummock_version_stats,
1074            );
1075            let mut resps_to_commit = resps.into_values().collect_vec();
1076            let mut staging_commit_info = self.database_info.take_staging_commit_info();
1077            if let Some((_, finished_jobs, _)) =
1078                self.finishing_jobs_collector
1079                    .take_collected_if(|collected_epoch| {
1080                        assert!(epoch <= collected_epoch.prev);
1081                        epoch == collected_epoch.prev
1082                    })
1083            {
1084                finished_jobs
1085                    .into_iter()
1086                    .for_each(|(_, (resps, tracking_job))| {
1087                        resps_to_commit.extend(resps);
1088                        staging_commit_info.finished_jobs.push(tracking_job);
1089                    });
1090            }
1091            {
1092                let task = task.get_or_insert_default();
1093                Command::collect_commit_epoch_info(
1094                    &self.database_info,
1095                    &info,
1096                    task,
1097                    resps_to_commit,
1098                    self.collect_backfill_pinned_upstream_log_epoch(),
1099                );
1100                self.completing_barrier = Some(info.barrier_info.epoch());
1101                task.finished_jobs.extend(staging_commit_info.finished_jobs);
1102                task.finished_cdc_table_backfill
1103                    .extend(staging_commit_info.finished_cdc_table_backfill);
1104                task.epoch_infos
1105                    .try_insert(self.partial_graph_id, info)
1106                    .expect("non duplicate");
1107                task.commit_info
1108                    .truncate_tables
1109                    .extend(staging_commit_info.table_ids_to_truncate);
1110            }
1111        } else if observed_non_checkpoint
1112            && self.database_info.has_pending_finished_jobs()
1113            && !partial_graph_manager.has_pending_checkpoint_barrier(self.partial_graph_id)
1114        {
1115            periodic_barriers.force_checkpoint_in_next_barrier(self.database_id);
1116        }
1117        if !independent_jobs_task.is_empty() {
1118            let task = task.get_or_insert_default();
1119            for (job_id, epoch, resps, info) in independent_jobs_task {
1120                collect_independent_job_commit_epoch_info(task, epoch, resps, &info);
1121                task.epoch_infos
1122                    .try_insert(to_partial_graph_id(self.database_id, Some(job_id)), info)
1123                    .expect("non duplicate");
1124            }
1125        }
1126    }
1127
1128    fn ack_completed(
1129        &mut self,
1130        partial_graph_manager: &mut PartialGraphManager,
1131        command_prev_epoch: Option<u64>,
1132        independent_job_epochs: Vec<(JobId, u64)>,
1133    ) {
1134        {
1135            if let Some(epoch) = self.completing_barrier.take() {
1136                assert_eq!(command_prev_epoch, Some(epoch.prev));
1137                self.committed_epoch = Some(epoch.prev);
1138                partial_graph_manager.ack_completed(self.partial_graph_id, epoch.prev);
1139                self.last_committed_barrier_time
1140                    .get_or_insert_with(|| {
1141                        GLOBAL_META_METRICS
1142                            .last_committed_barrier_time
1143                            .with_guarded_label_values(&[&self.database_id.to_string()])
1144                    })
1145                    .set(Epoch(epoch.curr).as_unix_secs() as i64);
1146            } else {
1147                assert_eq!(command_prev_epoch, None);
1148            };
1149            for (job_id, epoch) in independent_job_epochs {
1150                if let Some(job) = self.independent_checkpoint_job_controls.get_mut(&job_id) {
1151                    job.ack_completed(partial_graph_manager, epoch);
1152                }
1153                // If the job is not found, it was dropped and already removed
1154                // by `on_partial_graph_reset` while the completing task was running.
1155            }
1156        }
1157    }
1158
1159    fn handle_refresh_table_info(
1160        &self,
1161        task: &mut Option<CompleteBarrierTask>,
1162        resps: &HashMap<WorkerId, BarrierCompleteResponse>,
1163    ) {
1164        let list_finished_info = resps
1165            .values()
1166            .flat_map(|resp| resp.list_finished_sources.clone())
1167            .collect::<Vec<_>>();
1168        if !list_finished_info.is_empty() {
1169            let task = task.get_or_insert_default();
1170            task.list_finished_source_ids.extend(list_finished_info);
1171        }
1172
1173        let load_finished_info = resps
1174            .values()
1175            .flat_map(|resp| resp.load_finished_sources.clone())
1176            .collect::<Vec<_>>();
1177        if !load_finished_info.is_empty() {
1178            let task = task.get_or_insert_default();
1179            task.load_finished_source_ids.extend(load_finished_info);
1180        }
1181
1182        let refresh_finished_table_ids: Vec<JobId> = resps
1183            .values()
1184            .flat_map(|resp| {
1185                resp.refresh_finished_tables
1186                    .iter()
1187                    .map(|table_id| table_id.as_job_id())
1188            })
1189            .collect::<Vec<_>>();
1190        if !refresh_finished_table_ids.is_empty() {
1191            let task = task.get_or_insert_default();
1192            task.refresh_finished_table_job_ids
1193                .extend(refresh_finished_table_ids);
1194        }
1195    }
1196}
1197
1198impl DatabaseCheckpointControl {
1199    /// Handle the new barrier from the scheduled queue and inject it.
1200    fn handle_new_barrier(
1201        &mut self,
1202        command: Option<(Command, Vec<Notifier>)>,
1203        checkpoint: bool,
1204        span: tracing::Span,
1205        partial_graph_manager: &mut PartialGraphManager,
1206        hummock_version_stats: &HummockVersionStats,
1207        worker_nodes: &HashMap<WorkerId, WorkerNode>,
1208    ) -> MetaResult<()> {
1209        let curr_epoch = self.state.in_flight_prev_epoch().next();
1210
1211        let (command, mut notifiers) = if let Some((command, notifiers)) = command {
1212            (Some(command), notifiers)
1213        } else {
1214            (None, vec![])
1215        };
1216
1217        debug_assert!(
1218            !matches!(
1219                &command,
1220                Some(Command::RescheduleIntent {
1221                    reschedule_plan: None,
1222                    ..
1223                })
1224            ),
1225            "reschedule intent should be resolved before injection"
1226        );
1227
1228        if let Some(Command::DropStreamingJobs {
1229            streaming_job_ids, ..
1230        }) = &command
1231        {
1232            if streaming_job_ids.len() > 1 {
1233                for job_to_cancel in streaming_job_ids {
1234                    if self
1235                        .independent_checkpoint_job_controls
1236                        .contains_key(job_to_cancel)
1237                    {
1238                        warn!(
1239                            job_id = %job_to_cancel,
1240                            "ignore multi-job cancel command on creating snapshot backfill streaming job"
1241                        );
1242                        for notifier in notifiers {
1243                            notifier
1244                                .notify_start_failed(anyhow!("cannot cancel creating snapshot backfill streaming job with other jobs, \
1245                                the job will continue creating until created or recovery. Please cancel the snapshot backfilling job in a single DDL ").into());
1246                        }
1247                        return Ok(());
1248                    }
1249                }
1250            } else if let Some(job_to_drop) = streaming_job_ids.iter().next()
1251                && let Some(job) = self
1252                    .independent_checkpoint_job_controls
1253                    .get_mut(job_to_drop)
1254            {
1255                let dropped = job.drop(&mut notifiers, partial_graph_manager);
1256                if dropped {
1257                    return Ok(());
1258                }
1259            }
1260        }
1261
1262        if let Some(Command::Throttle { jobs, .. }) = &command
1263            && jobs.len() > 1
1264            && let Some(independent_job_id) = jobs
1265                .iter()
1266                .find(|job| self.independent_checkpoint_job_controls.contains_key(*job))
1267        {
1268            warn!(
1269                job_id = %independent_job_id,
1270                "ignore multi-job throttle command on independent checkpoint job"
1271            );
1272            for notifier in notifiers {
1273                notifier.notify_start_failed(
1274                    anyhow!(
1275                        "cannot alter rate limit for independent checkpoint job with other jobs, \
1276                                the original rate limit will be kept during recovery."
1277                    )
1278                    .into(),
1279                );
1280            }
1281            return Ok(());
1282        };
1283
1284        if let Some(Command::RescheduleIntent {
1285            reschedule_plan: Some(reschedule_plan),
1286            ..
1287        }) = &command
1288            && !self.independent_checkpoint_job_controls.is_empty()
1289        {
1290            let blocked_job_ids =
1291                self.collect_reschedule_blocked_jobs_for_independent_jobs_inflight()?;
1292            let blocked_reschedule_job_ids = self.collect_reschedule_blocked_job_ids(
1293                &reschedule_plan.reschedules,
1294                &reschedule_plan.fragment_actors,
1295                &blocked_job_ids,
1296            );
1297            if !blocked_reschedule_job_ids.is_empty() {
1298                warn!(
1299                    blocked_reschedule_job_ids = ?blocked_reschedule_job_ids,
1300                    "reject reschedule fragments related to creating unreschedulable backfill jobs"
1301                );
1302                for notifier in notifiers {
1303                    notifier.notify_start_failed(
1304                        anyhow!(
1305                            "cannot reschedule jobs {:?} when creating jobs with unreschedulable backfill fragments",
1306                            blocked_reschedule_job_ids
1307                        )
1308                            .into(),
1309                    );
1310                }
1311                return Ok(());
1312            }
1313        }
1314
1315        if !matches!(&command, Some(Command::CreateStreamingJob { .. }))
1316            && self.database_info.is_empty()
1317        {
1318            assert!(
1319                self.independent_checkpoint_job_controls.is_empty(),
1320                "should not have snapshot backfill job when there is no normal job in database"
1321            );
1322            // Drop the guard to remove the metric series of this database.
1323            self.last_committed_barrier_time = None;
1324            // skip the command when there is nothing to do with the barrier
1325            for mut notifier in notifiers {
1326                notifier.notify_started();
1327                notifier.notify_collected();
1328            }
1329            return Ok(());
1330        };
1331
1332        if let Some(Command::CreateStreamingJob {
1333            job_type:
1334                CreateStreamingJobType::SnapshotBackfill { .. }
1335                | CreateStreamingJobType::BatchRefresh(_),
1336            ..
1337        }) = &command
1338            && self.state.is_paused()
1339        {
1340            warn!("cannot create streaming job with snapshot backfill when paused");
1341            for notifier in notifiers {
1342                notifier.notify_start_failed(
1343                    anyhow!("cannot create streaming job with snapshot backfill when paused",)
1344                        .into(),
1345                );
1346            }
1347            return Ok(());
1348        }
1349
1350        let barrier_info = self.state.next_barrier_info(checkpoint, curr_epoch);
1351        // Tracing related stuff
1352        barrier_info.prev_epoch.span().in_scope(|| {
1353            tracing::info!(target: "rw_tracing", epoch = barrier_info.curr_epoch(), "new barrier enqueued");
1354        });
1355        span.record("epoch", barrier_info.curr_epoch());
1356
1357        let epoch = barrier_info.epoch();
1358        let ApplyCommandInfo { jobs_to_wait } = match self.apply_command(
1359            command,
1360            &mut notifiers,
1361            barrier_info,
1362            partial_graph_manager,
1363            hummock_version_stats,
1364            worker_nodes,
1365        ) {
1366            Ok(info) => {
1367                assert!(notifiers.is_empty());
1368                info
1369            }
1370            Err(err) => {
1371                for notifier in notifiers {
1372                    notifier.notify_start_failed(err.clone());
1373                }
1374                fail_point!("inject_barrier_err_success");
1375                return Err(err);
1376            }
1377        };
1378
1379        // Record the in-flight barrier.
1380        self.enqueue_command(epoch, jobs_to_wait);
1381
1382        Ok(())
1383    }
1384
1385    // ── Batch refresh trigger helpers ────────────────────────────────────────
1386
1387    /// Get the last committed epoch for a batch refresh job.
1388    pub(crate) fn get_batch_refresh_trigger_info(&self, job_id: JobId) -> u64 {
1389        let job = self
1390            .independent_checkpoint_job_controls
1391            .get(&job_id)
1392            .expect("batch refresh job should exist");
1393        match job {
1394            IndependentCheckpointJobControl::BatchRefresh(br_job) => br_job
1395                .last_committed_epoch()
1396                .expect("idle job must have a last_committed_epoch"),
1397            _ => panic!("job {} should be a batch refresh job", job_id),
1398        }
1399    }
1400
1401    /// Whether the batch refresh job already has its cached context populated.
1402    /// Start a batch refresh logstore consumption run.
1403    /// Returns true if a run was started, false if no log epochs to consume.
1404    pub(crate) fn start_batch_refresh_run(
1405        &mut self,
1406        job_id: JobId,
1407        context: &BatchRefreshJobTriggerContext,
1408        worker_nodes: &HashMap<WorkerId, WorkerNode>,
1409        actor_id_counter: &AtomicU32,
1410        partial_graph_manager: &mut PartialGraphManager,
1411    ) -> MetaResult<bool> {
1412        let job = self
1413            .independent_checkpoint_job_controls
1414            .get_mut(&job_id)
1415            .expect("batch refresh job should exist");
1416        match job {
1417            IndependentCheckpointJobControl::BatchRefresh(br_job) => br_job.start_refresh_run(
1418                context,
1419                worker_nodes,
1420                actor_id_counter,
1421                partial_graph_manager,
1422            ),
1423            _ => panic!("job {} should be a batch refresh job", job_id),
1424        }
1425    }
1426}