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