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