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