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