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