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 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 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 return Ok(());
399 }
400 };
401 let Some(database) = database.running_state_mut() else {
402 return Ok(());
404 };
405 if partial_graph_manager.pending_barrier_num(database.partial_graph_id)
406 >= self.in_flight_barrier_nums
407 {
408 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.extend(
431 database_checkpoint_control
432 .database_info
433 .gen_backfill_progress(),
434 );
435 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.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 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 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 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 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
786pub(in crate::barrier) struct DatabaseCheckpointControl {
788 pub(super) database_id: DatabaseId,
789 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 completing_barrier: Option<EpochPair>,
798
799 committed_epoch: Option<u64>,
800
801 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 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 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 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 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 }
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 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 self.last_committed_barrier_time = None;
1383 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 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 self.enqueue_command(epoch, jobs_to_wait);
1438
1439 Ok(())
1440 }
1441
1442 pub(crate) fn get_batch_refresh_trigger_info(&self, job_id: JobId) -> u64 {
1446 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 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 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 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}