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