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