1use 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 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>>, 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 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 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 return Ok(());
405 }
406 };
407 let Some(database) = database.running_state_mut() else {
408 return Ok(());
410 };
411 if partial_graph_manager.pending_barrier_num(database.partial_graph_id)
412 >= self.in_flight_barrier_nums
413 {
414 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.extend(
434 database_checkpoint_control
435 .database_info
436 .gen_backfill_progress(),
437 );
438 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.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 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 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 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 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
789pub(in crate::barrier) struct DatabaseCheckpointControl {
791 pub(super) database_id: DatabaseId,
792 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 completing_barrier: Option<EpochPair>,
801
802 committed_epoch: Option<u64>,
803
804 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 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 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 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 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 }
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 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 self.last_committed_barrier_time = None;
1384 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 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 self.enqueue_command(epoch, jobs_to_wait);
1443
1444 Ok(())
1445 }
1446
1447 pub(crate) fn get_batch_refresh_trigger_info(&self, job_id: JobId) -> u64 {
1451 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 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 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 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}