1use std::collections::{HashMap, HashSet};
26use std::mem::{replace, take};
27use std::sync::atomic::AtomicU32;
28
29use anyhow::anyhow;
30use itertools::Itertools;
31use risingwave_common::catalog::{DatabaseId, TableId};
32use risingwave_common::id::JobId;
33use risingwave_common::metrics::{LabelGuardedHistogram, LabelGuardedIntGauge};
34use risingwave_common::util::epoch::{Epoch, EpochPair};
35use risingwave_meta_model::{DispatcherType, WorkerId, streaming_job};
36use risingwave_pb::common::WorkerNode;
37use risingwave_pb::ddl_service::PbBackfillType;
38use risingwave_pb::hummock::HummockVersionStats;
39use risingwave_pb::id::{ActorId, FragmentId, PartialGraphId};
40use risingwave_pb::stream_plan::barrier::PbBarrierKind;
41use risingwave_pb::stream_plan::barrier_mutation::Mutation;
42use risingwave_pb::stream_plan::{AddMutation, StartFragmentBackfillMutation, StopMutation};
43use risingwave_pb::stream_service::BarrierCompleteResponse;
44use tracing::{debug, info};
45
46use crate::MetaResult;
47use crate::barrier::backfill_order_control::get_nodes_with_backfill_dependencies;
48use crate::barrier::command::{PostCollectCommand, ThrottleConfigMap, extract_throttle_config};
49use crate::barrier::context::CreateSnapshotBackfillJobCommandInfo;
50use crate::barrier::edge_builder::{EdgeBuilderFragmentInfo, FragmentEdgeBuilder};
51use crate::barrier::info::BarrierInfo;
52use crate::barrier::notifier::{CollectionNotifier, NotifierStarter};
53use crate::barrier::partial_graph::{
54 CollectedBarrier, PartialGraphBarrierInfo, PartialGraphManager, PartialGraphStat,
55};
56use crate::barrier::progress::{CreateMviewProgressTracker, TrackingJob, collect_done_fragments};
57use crate::barrier::rpc::to_partial_graph_id;
58use crate::barrier::{
59 BackfillOrderState, BackfillProgress, BarrierKind, FragmentBackfillProgress, TracedEpoch,
60};
61use crate::controller::fragment::InflightFragmentInfo;
62use crate::controller::scale::{
63 ComponentFragmentAligner, EnsembleActorTemplate, LoadedFragment, NoShuffleEnsemble,
64 build_no_shuffle_fragment_graph_edges, find_no_shuffle_graphs,
65};
66use crate::model::{
67 FragmentDownstreamRelation, StreamActor, StreamJobActorsToCreate, StreamingJobModelContextExt,
68};
69use crate::rpc::metrics::GLOBAL_META_METRICS;
70use crate::stream::ExtendedFragmentBackfillOrder;
71
72#[derive(Debug)]
83pub(crate) struct BatchRefreshLogicalFragments {
84 pub fragments: HashMap<FragmentId, LoadedFragment>,
86 pub downstreams: FragmentDownstreamRelation,
88}
89
90#[derive(Debug)]
95pub(crate) struct BatchRefreshRenderResult {
96 pub fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
97 pub node_actors: HashMap<WorkerId, HashSet<ActorId>>,
98 pub state_table_ids: HashSet<TableId>,
99 pub actors_to_create: StreamJobActorsToCreate,
100}
101
102#[derive(Debug)]
110pub(crate) struct BatchRefreshJobTriggerContext {
111 pub fragments: HashMap<FragmentId, LoadedFragment>,
112 pub downstreams: FragmentDownstreamRelation,
113 pub streaming_job_model: streaming_job::Model,
114 pub definition: String,
115 pub database_resource_group: String,
116 pub upstream_table_log_epochs: HashMap<TableId, Vec<(Vec<u64>, u64)>>,
118 pub target_upstream_epoch: u64,
120}
121
122#[derive(Debug)]
128enum BatchRefreshJobStatus {
129 ConsumingSnapshot {
134 prev_epoch_fake_physical_time: u64,
135 version_stats: HummockVersionStats,
136 create_mview_tracker: CreateMviewProgressTracker,
137 snapshot_epoch: u64,
138 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
139 pending_non_checkpoint_barriers: Vec<u64>,
140 node_actors: HashMap<WorkerId, HashSet<ActorId>>,
141 state_table_ids: HashSet<TableId>,
142 },
143 FinishingSnapshot {
150 tracking_job: Option<TrackingJob>,
151 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
152 },
153 Idle { last_committed_epoch: u64 },
155 InitializingBatchRefresh {
158 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
159 node_actors: HashMap<WorkerId, HashSet<ActorId>>,
160 state_table_ids: HashSet<TableId>,
161 pending_log_barriers: Vec<BarrierInfo>,
164 logstore_start_epoch: u64,
165 target_upstream_epoch: u64,
166 },
167 ConsumingLogStore {
173 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
174 logstore_start_epoch: u64,
176 target_upstream_epoch: u64,
178 },
179 Resetting { notifiers: Vec<CollectionNotifier> },
181}
182
183#[derive(Debug)]
194pub(crate) struct BatchRefreshJobCheckpointControl {
195 job_id: JobId,
196 partial_graph_id: PartialGraphId,
197 snapshot_backfill_upstream_tables: HashSet<TableId>,
198 snapshot_epoch: u64,
199 batch_refresh_seconds: u64,
201
202 status: BatchRefreshJobStatus,
203}
204
205impl BatchRefreshJobCheckpointControl {
208 pub(crate) fn render_actors_and_build_job_info(
219 fragments: &HashMap<FragmentId, LoadedFragment>,
220 downstreams: &FragmentDownstreamRelation,
221 definition: &str,
222 actor_id_generator: &AtomicU32,
224 worker_nodes: &HashMap<WorkerId, WorkerNode>,
225 database_resource_group: &str,
226 streaming_job_model: &streaming_job::Model,
227 partial_graph_id: PartialGraphId,
229 ) -> MetaResult<BatchRefreshRenderResult> {
230 let ensembles = Self::resolve_ensembles(fragments, downstreams)?;
232
233 let mut actor_assignments: HashMap<
235 FragmentId,
236 HashMap<ActorId, (WorkerId, Option<risingwave_common::bitmap::Bitmap>)>,
237 > = HashMap::new();
238
239 for ensemble in &ensembles {
240 let first_component = ensemble
242 .component_fragments()
243 .next()
244 .expect("ensemble must have at least one component");
245 let fragment = &fragments[&first_component];
246 let distribution_type = fragment.distribution_type;
247 let vnode_count = fragment.vnode_count;
248
249 for fid in ensemble.component_fragments() {
251 let f = &fragments[&fid];
252 assert_eq!(
253 vnode_count, f.vnode_count,
254 "fragments {} and {} in same ensemble have different vnode counts",
255 first_component, fid,
256 );
257 }
258
259 let entry_fragment_parallelism = Itertools::exactly_one(
260 ensemble
261 .entry_fragments()
262 .map(|fid| fragments[&fid].parallelism.clone())
263 .dedup(),
264 )
265 .map_err(|_| {
266 anyhow!(
267 "entry fragments have inconsistent parallelism settings in batch refresh job"
268 )
269 })?;
270
271 let actor_template = EnsembleActorTemplate::render_new(
272 streaming_job_model,
273 worker_nodes,
274 entry_fragment_parallelism,
275 database_resource_group.to_owned(),
276 distribution_type,
277 vnode_count,
278 )?;
279
280 for fid in ensemble.component_fragments() {
281 let f = &fragments[&fid];
282 let aligner =
283 ComponentFragmentAligner::new_persistent(&actor_template, actor_id_generator);
284 let assignments = aligner.align_component_actor(f.distribution_type);
285 actor_assignments.insert(fid, assignments);
286 }
287 }
288
289 let mut stream_actors: HashMap<FragmentId, Vec<StreamActor>> = HashMap::new();
291 let mut actor_location: HashMap<ActorId, WorkerId> = HashMap::new();
292
293 for (fragment_id, assignments) in &actor_assignments {
294 let mut actors = Vec::with_capacity(assignments.len());
295 for (&actor_id, (worker_id, vnode_bitmap)) in assignments {
296 actor_location.insert(actor_id, *worker_id);
297 let stream_context = streaming_job_model.stream_context();
298 actors.push(StreamActor {
299 actor_id,
300 fragment_id: *fragment_id,
301 vnode_bitmap: vnode_bitmap.clone(),
302 mview_definition: definition.to_owned(),
303 expr_context: Some(stream_context.to_expr_context()),
304 config_override: stream_context.config_override.clone(),
305 });
306 }
307 stream_actors.insert(*fragment_id, actors);
308 }
309
310 let fragment_infos: HashMap<FragmentId, InflightFragmentInfo> = fragments
312 .iter()
313 .map(|(fragment_id, loaded)| {
314 let actors = stream_actors
315 .get(fragment_id)
316 .into_iter()
317 .flatten()
318 .map(|actor| {
319 (
320 actor.actor_id,
321 crate::controller::fragment::InflightActorInfo {
322 worker_id: actor_location[&actor.actor_id],
323 vnode_bitmap: actor.vnode_bitmap.clone(),
324 splits: vec![], },
326 )
327 })
328 .collect();
329 (
330 *fragment_id,
331 InflightFragmentInfo {
332 fragment_id: *fragment_id,
333 distribution_type: loaded.distribution_type,
334 fragment_type_mask: loaded.fragment_type_mask,
335 vnode_count: loaded.vnode_count,
336 nodes: loaded.nodes.clone(),
337 actors,
338 state_table_ids: loaded.state_table_ids.clone(),
339 },
340 )
341 })
342 .collect();
343
344 let mut builder = FragmentEdgeBuilder::new(fragment_infos.values().map(|f| {
346 (
347 f.fragment_id,
348 EdgeBuilderFragmentInfo::from_inflight_with_worker_nodes(
349 f,
350 partial_graph_id,
351 worker_nodes,
352 ),
353 )
354 }));
355 builder.add_relations(downstreams);
356 let mut edges = builder.build();
357
358 let actors_to_create = edges.collect_actors_to_create(fragment_infos.values().map(|f| {
359 (
360 f.fragment_id,
361 &f.nodes,
362 f.actors.iter().map(|(actor_id, actor)| {
363 let sa = stream_actors[&f.fragment_id]
364 .iter()
365 .find(|a| a.actor_id == *actor_id)
366 .expect("should exist");
367 (sa, actor.worker_id)
368 }),
369 vec![], )
371 }));
372
373 let node_actors = InflightFragmentInfo::actor_ids_to_collect(fragment_infos.values());
375 let state_table_ids =
376 InflightFragmentInfo::existing_table_ids(fragment_infos.values()).collect();
377
378 Ok(BatchRefreshRenderResult {
379 fragment_infos,
380 node_actors,
381 state_table_ids,
382 actors_to_create,
383 })
384 }
385
386 pub(crate) fn build_initial_partial_graph_mutation(
391 render_result: &BatchRefreshRenderResult,
392 backfill_ordering: &ExtendedFragmentBackfillOrder,
393 ) -> Mutation {
394 let added_actors: Vec<ActorId> = render_result
395 .fragment_infos
396 .values()
397 .flat_map(|f| f.actors.keys().copied())
398 .collect();
399 let backfill_nodes_to_pause = get_nodes_with_backfill_dependencies(backfill_ordering)
400 .into_iter()
401 .collect();
402 Mutation::Add(AddMutation {
403 actor_dispatchers: Default::default(),
404 added_actors,
405 actor_splits: Default::default(),
406 pause: false,
407 subscriptions_to_add: Default::default(),
408 backfill_nodes_to_pause,
409 actor_cdc_table_snapshot_splits: None,
410 new_upstream_sinks: Default::default(),
411 dropped_actors: Default::default(),
412 sink_log_store_flush: Default::default(),
413 })
414 }
415
416 fn resolve_ensembles(
418 fragments: &HashMap<FragmentId, LoadedFragment>,
419 downstreams: &FragmentDownstreamRelation,
420 ) -> MetaResult<Vec<NoShuffleEnsemble>> {
421 let mut new_no_shuffle: HashMap<_, HashSet<_>> = HashMap::new();
422 for (upstream_fid, relations) in downstreams {
423 for rel in relations {
424 if rel.dispatcher_type == DispatcherType::NoShuffle {
425 new_no_shuffle
426 .entry(*upstream_fid)
427 .or_default()
428 .insert(rel.downstream_fragment_id);
429 }
430 }
431 }
432
433 let mut ensembles = if new_no_shuffle.is_empty() {
434 Vec::new()
435 } else {
436 let no_shuffle_edges: Vec<(FragmentId, FragmentId)> = new_no_shuffle
437 .iter()
438 .flat_map(|(u, ds)| ds.iter().map(move |d| (*u, *d)))
439 .collect();
440 let all_fragment_ids: Vec<FragmentId> = no_shuffle_edges
441 .iter()
442 .flat_map(|(u, d)| [*u, *d])
443 .collect::<HashSet<_>>()
444 .into_iter()
445 .collect();
446 let (fwd, bwd) = build_no_shuffle_fragment_graph_edges(no_shuffle_edges);
447 find_no_shuffle_graphs(&all_fragment_ids, &fwd, &bwd)?
448 };
449
450 let covered: HashSet<FragmentId> = ensembles
452 .iter()
453 .flat_map(|e| e.component_fragments())
454 .collect();
455 for fragment_id in fragments.keys() {
456 if !covered.contains(fragment_id) {
457 ensembles.push(NoShuffleEnsemble::singleton(*fragment_id));
458 }
459 }
460
461 Ok(ensembles)
462 }
463}
464
465impl BatchRefreshJobCheckpointControl {
468 #[expect(clippy::too_many_arguments)]
473 pub(crate) fn new(
474 database_id: DatabaseId,
475 job_id: JobId,
476 create_info: CreateSnapshotBackfillJobCommandInfo,
477 notifier: Option<&mut NotifierStarter>,
478 snapshot_backfill_upstream_tables: HashSet<TableId>,
479 snapshot_epoch: u64,
480 version_stat: &HummockVersionStats,
481 partial_graph_manager: &mut PartialGraphManager,
482 logical: &BatchRefreshLogicalFragments,
483 worker_nodes: &HashMap<WorkerId, WorkerNode>,
484 batch_refresh_seconds: u64,
485 ) -> MetaResult<Self> {
486 debug!(
487 %job_id,
488 "new batch refresh job"
489 );
490
491 let partial_graph_id = to_partial_graph_id(database_id, Some(job_id));
492 let backfill_ordering = &create_info.info.fragment_backfill_ordering;
493 let actor_id_generator = partial_graph_manager
494 .control_stream_manager()
495 .env
496 .actor_id_generator();
497
498 let render_result = Self::render_actors_and_build_job_info(
499 &logical.fragments,
500 &logical.downstreams,
501 &create_info.info.definition,
502 actor_id_generator,
503 worker_nodes,
504 &create_info.info.database_resource_group,
505 &create_info.info.streaming_job_model,
506 partial_graph_id,
507 )?;
508 let initial_partial_graph_mutation =
509 Self::build_initial_partial_graph_mutation(&render_result, backfill_ordering);
510
511 let backfill_order_state = BackfillOrderState::new(
512 backfill_ordering,
513 &render_result.fragment_infos,
514 create_info
515 .info
516 .locality_fragment_state_table_mapping
517 .clone(),
518 );
519 let create_mview_tracker = CreateMviewProgressTracker::recover(
520 job_id,
521 &render_result.fragment_infos,
522 backfill_order_state,
523 version_stat,
524 );
525
526 let mut prev_epoch_fake_physical_time = 0;
527 let mut pending_non_checkpoint_barriers = vec![];
528
529 let initial_barrier_info = super::new_fake_barrier(
530 &mut prev_epoch_fake_physical_time,
531 &mut pending_non_checkpoint_barriers,
532 PbBarrierKind::Checkpoint,
533 );
534
535 let mut graph_adder = partial_graph_manager.add_partial_graph(
536 partial_graph_id,
537 BatchRefreshBarrierStats::new(job_id, snapshot_epoch),
538 );
539
540 if let Err(e) = Self::inject_barrier(
541 partial_graph_id,
542 graph_adder.manager(),
543 &render_result.node_actors,
544 &render_result.state_table_ids,
545 initial_barrier_info,
546 Some(render_result.actors_to_create),
547 Some(initial_partial_graph_mutation),
548 notifier,
549 Some(create_info),
550 false,
551 ) {
552 graph_adder.failed();
553 return Err(e);
554 }
555
556 graph_adder.added();
557 assert!(pending_non_checkpoint_barriers.is_empty());
558 let this = Self {
559 partial_graph_id,
560 job_id,
561 snapshot_backfill_upstream_tables,
562 snapshot_epoch,
563 batch_refresh_seconds,
564
565 status: BatchRefreshJobStatus::ConsumingSnapshot {
566 prev_epoch_fake_physical_time,
567 version_stats: version_stat.clone(),
568 create_mview_tracker,
569 snapshot_epoch,
570 fragment_infos: render_result.fragment_infos,
571 pending_non_checkpoint_barriers,
572 node_actors: render_result.node_actors,
573 state_table_ids: render_result.state_table_ids,
574 },
575 };
576 Ok(this)
577 }
578
579 #[expect(clippy::too_many_arguments)]
584 pub(crate) fn recover(
585 database_id: DatabaseId,
586 job_id: JobId,
587 snapshot_backfill_upstream_tables: HashSet<TableId>,
588 snapshot_epoch: u64,
589 committed_epoch: u64,
590 backfill_order: ExtendedFragmentBackfillOrder,
591 version_stat: &HummockVersionStats,
592 initial_mutation: Mutation,
593 render_result: BatchRefreshRenderResult,
594 partial_graph_recoverer: &mut crate::barrier::partial_graph::PartialGraphRecoverer<'_>,
595 batch_refresh_seconds: u64,
596 ) -> MetaResult<Self> {
597 let partial_graph_id = to_partial_graph_id(database_id, Some(job_id));
598
599 if committed_epoch >= snapshot_epoch {
600 info!(
602 %job_id,
603 committed_epoch,
604 snapshot_epoch,
605 "recovered idle batch refresh job (no partial graph)"
606 );
607 return Ok(Self {
608 job_id,
609 partial_graph_id,
610 snapshot_backfill_upstream_tables,
611 snapshot_epoch,
612 batch_refresh_seconds,
613
614 status: BatchRefreshJobStatus::Idle {
615 last_committed_epoch: committed_epoch,
616 },
617 });
618 }
619
620 info!(
622 %job_id,
623 committed_epoch,
624 snapshot_epoch,
625 "recovered batch refresh job to consuming snapshot"
626 );
627
628 let mut prev_epoch_fake_physical_time = Epoch(committed_epoch).physical_time();
629 let mut pending_non_checkpoint_barriers = vec![];
630
631 let locality_fragment_state_table_mapping =
632 crate::barrier::rpc::build_locality_fragment_state_table_mapping(
633 &render_result.fragment_infos,
634 );
635 let backfill_order_state = BackfillOrderState::recover_from_fragment_infos(
636 &backfill_order,
637 &render_result.fragment_infos,
638 locality_fragment_state_table_mapping,
639 );
640
641 let create_mview_tracker = CreateMviewProgressTracker::recover(
642 job_id,
643 &render_result.fragment_infos,
644 backfill_order_state,
645 version_stat,
646 );
647 let first_barrier_info = super::new_fake_barrier(
648 &mut prev_epoch_fake_physical_time,
649 &mut pending_non_checkpoint_barriers,
650 PbBarrierKind::Initial,
651 );
652
653 partial_graph_recoverer.recover_graph(
654 partial_graph_id,
655 initial_mutation,
656 &first_barrier_info,
657 &render_result.node_actors,
658 render_result.state_table_ids.iter().copied(),
659 render_result.actors_to_create,
660 BatchRefreshBarrierStats::new(job_id, snapshot_epoch),
661 )?;
662
663 Ok(Self {
664 job_id,
665 partial_graph_id,
666 snapshot_backfill_upstream_tables,
667 snapshot_epoch,
668 batch_refresh_seconds,
669 status: BatchRefreshJobStatus::ConsumingSnapshot {
670 prev_epoch_fake_physical_time,
671 version_stats: version_stat.clone(),
672 create_mview_tracker,
673 fragment_infos: render_result.fragment_infos,
674 snapshot_epoch,
675 pending_non_checkpoint_barriers,
676 node_actors: render_result.node_actors,
677 state_table_ids: render_result.state_table_ids,
678 },
679 })
680 }
681}
682
683impl BatchRefreshJobCheckpointControl {
686 fn inject_barrier(
687 partial_graph_id: PartialGraphId,
688 partial_graph_manager: &mut PartialGraphManager,
689 node_actors: &HashMap<WorkerId, HashSet<ActorId>>,
690 state_table_ids: &HashSet<TableId>,
691 barrier_info: BarrierInfo,
692 new_actors: Option<StreamJobActorsToCreate>,
693 mutation: Option<Mutation>,
694 notifier: Option<&mut NotifierStarter>,
695 first_create_info: Option<CreateSnapshotBackfillJobCommandInfo>,
696 is_stop: bool,
697 ) -> MetaResult<()> {
698 if is_stop {
699 assert!(
700 matches!(&mutation, Some(Mutation::Stop(_))),
701 "stop barrier must carry a Stop mutation"
702 );
703 }
704 partial_graph_manager.inject_barrier(
705 partial_graph_id,
706 mutation,
707 None,
708 node_actors,
709 state_table_ids.iter().copied(),
710 if is_stop {
711 itertools::Either::Left(std::iter::empty())
713 } else {
714 itertools::Either::Right(node_actors.keys().copied())
715 },
716 new_actors,
717 PartialGraphBarrierInfo::new(
718 first_create_info.map_or_else(
719 PostCollectCommand::barrier,
720 CreateSnapshotBackfillJobCommandInfo::into_post_collect,
721 ),
722 barrier_info,
723 notifier,
724 state_table_ids.clone(),
725 ),
726 )?;
727 Ok(())
728 }
729}
730
731impl BatchRefreshJobCheckpointControl {
734 pub(crate) fn on_new_upstream_barrier(
735 &mut self,
736 partial_graph_manager: &mut PartialGraphManager,
737 barrier_info: &BarrierInfo,
738 mutation: Option<(Mutation, Option<&mut NotifierStarter>)>,
739 ) -> MetaResult<()> {
740 if !matches!(self.status, BatchRefreshJobStatus::ConsumingSnapshot { .. }) {
741 return Ok(());
744 }
745 let (mutation, notifier) = match mutation {
746 Some((mutation, notifier)) => (Some(mutation), notifier),
747 None => (None, None),
748 };
749 let is_finished = matches!(
751 &self.status,
752 BatchRefreshJobStatus::ConsumingSnapshot { create_mview_tracker, .. }
753 if create_mview_tracker.is_finished()
754 );
755
756 if is_finished {
757 let old_status = replace(
760 &mut self.status,
761 BatchRefreshJobStatus::Idle {
762 last_committed_epoch: 0,
763 },
764 );
765 let BatchRefreshJobStatus::ConsumingSnapshot {
766 prev_epoch_fake_physical_time,
767 mut pending_non_checkpoint_barriers,
768 snapshot_epoch,
769 fragment_infos,
770 create_mview_tracker,
771 node_actors,
772 state_table_ids,
773 ..
774 } = old_status
775 else {
776 unreachable!()
777 };
778
779 let tracking_job = create_mview_tracker.into_tracking_job();
780
781 pending_non_checkpoint_barriers.push(snapshot_epoch);
783 let prev_epoch = Epoch::from_physical_time(prev_epoch_fake_physical_time);
784 let final_checkpoint = BarrierInfo {
785 curr_epoch: TracedEpoch::new(Epoch(snapshot_epoch)),
786 prev_epoch: TracedEpoch::new(prev_epoch),
787 kind: BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_barriers)),
788 };
789
790 let stop_barrier = BarrierInfo {
792 prev_epoch: TracedEpoch::new(Epoch(snapshot_epoch)),
793 curr_epoch: TracedEpoch::new(Epoch(u64::MAX)),
794 kind: BarrierKind::Checkpoint(vec![snapshot_epoch]),
795 };
796
797 let stop_actors: Vec<ActorId> = fragment_infos
798 .values()
799 .flat_map(|f| f.actors.keys().copied())
800 .collect();
801
802 Self::inject_barrier(
803 self.partial_graph_id,
804 partial_graph_manager,
805 &node_actors,
806 &state_table_ids,
807 final_checkpoint,
808 None,
809 None,
810 notifier,
811 None,
812 false,
813 )?;
814 Self::inject_barrier(
815 self.partial_graph_id,
816 partial_graph_manager,
817 &node_actors,
818 &state_table_ids,
819 stop_barrier,
820 None,
821 Some(Mutation::Stop(StopMutation {
822 actors: stop_actors,
823 dropped_sink_fragments: vec![],
824 })),
825 None,
826 None,
827 true,
828 )?;
829
830 self.status = BatchRefreshJobStatus::FinishingSnapshot {
831 tracking_job: Some(tracking_job),
832 fragment_infos,
833 };
834 } else {
835 let BatchRefreshJobStatus::ConsumingSnapshot {
837 prev_epoch_fake_physical_time,
838 pending_non_checkpoint_barriers,
839 create_mview_tracker,
840 node_actors,
841 state_table_ids,
842 ..
843 } = &mut self.status
844 else {
845 unreachable!("is_finished was false, status must be ConsumingSnapshot")
846 };
847
848 let mutation = mutation.or_else(|| {
850 let pending_backfill_nodes = create_mview_tracker
851 .take_pending_backfill_nodes()
852 .collect_vec();
853 if pending_backfill_nodes.is_empty() {
854 None
855 } else {
856 Some(Mutation::StartFragmentBackfill(
857 StartFragmentBackfillMutation {
858 fragment_ids: pending_backfill_nodes,
859 },
860 ))
861 }
862 });
863 let barrier_to_inject = super::new_fake_barrier(
864 prev_epoch_fake_physical_time,
865 pending_non_checkpoint_barriers,
866 match barrier_info.kind {
867 BarrierKind::Barrier => PbBarrierKind::Barrier,
868 BarrierKind::Checkpoint(_) => PbBarrierKind::Checkpoint,
869 BarrierKind::Initial => {
870 unreachable!("upstream new epoch should not be initial")
871 }
872 },
873 );
874 Self::inject_barrier(
875 self.partial_graph_id,
876 partial_graph_manager,
877 node_actors,
878 state_table_ids,
879 barrier_to_inject,
880 None,
881 mutation,
882 notifier,
883 None,
884 false,
885 )?;
886 }
887 Ok(())
888 }
889
890 pub(crate) fn collect(&mut self, collected_barrier: CollectedBarrier<'_>) -> bool {
891 match &mut self.status {
892 BatchRefreshJobStatus::ConsumingSnapshot {
893 create_mview_tracker,
894 version_stats,
895 ..
896 } => {
897 for progress in collected_barrier
898 .resps
899 .values()
900 .flat_map(|resp| &resp.create_mview_progress)
901 {
902 create_mview_tracker.apply_progress(progress, version_stats);
903 }
904 create_mview_tracker.is_finished()
905 }
906 BatchRefreshJobStatus::InitializingBatchRefresh { .. }
907 | BatchRefreshJobStatus::ConsumingLogStore { .. } => {
908 false
910 }
911 _ => false,
912 }
913 }
914}
915
916impl BatchRefreshJobCheckpointControl {
919 #[expect(clippy::type_complexity)]
920 pub(crate) fn start_completing(
921 &mut self,
922 partial_graph_manager: &mut PartialGraphManager,
923 upstream_committed_epoch: u64,
924 ) -> Option<(
925 u64,
926 HashMap<WorkerId, BarrierCompleteResponse>,
927 PartialGraphBarrierInfo,
928 Option<TrackingJob>,
929 )> {
930 if upstream_committed_epoch < self.snapshot_epoch {
933 return None;
934 }
935 match &self.status {
936 BatchRefreshJobStatus::ConsumingSnapshot { .. }
937 | BatchRefreshJobStatus::FinishingSnapshot { .. }
938 | BatchRefreshJobStatus::ConsumingLogStore { .. } => {}
939 BatchRefreshJobStatus::Idle { .. }
940 | BatchRefreshJobStatus::InitializingBatchRefresh { .. }
941 | BatchRefreshJobStatus::Resetting { .. } => {
942 return None;
943 }
944 };
945
946 partial_graph_manager
947 .start_completing(
948 self.partial_graph_id,
949 std::ops::Bound::Unbounded,
950 |_non_checkpoint_epoch, _resps, _| {
951 },
953 )
954 .map(|(epoch, resps, info)| {
955 let tracking_job = match &mut self.status {
961 BatchRefreshJobStatus::FinishingSnapshot { tracking_job, .. }
962 if epoch == self.snapshot_epoch =>
963 {
964 Some(
965 tracking_job
966 .take()
967 .expect("tracking job should not have been taken yet"),
968 )
969 }
970 _ => None,
971 };
972 (epoch, resps, info, tracking_job)
973 })
974 }
975
976 pub(super) fn ack_completed(
977 &mut self,
978 partial_graph_manager: &mut PartialGraphManager,
979 completed_epoch: u64,
980 ) {
981 match &self.status {
982 BatchRefreshJobStatus::ConsumingSnapshot { .. } => {
983 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
984 }
985 BatchRefreshJobStatus::FinishingSnapshot { tracking_job, .. }
986 if completed_epoch == self.snapshot_epoch =>
987 {
988 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
989 assert!(
990 tracking_job.is_none(),
991 "tracking job should have been taken at start_completing"
992 );
993 info!(
994 job_id = %self.job_id,
995 completed_epoch,
996 "batch refresh job: snapshot done, transitioned to idle, removing partial graph"
997 );
998 partial_graph_manager.remove_partial_graphs(vec![self.partial_graph_id]);
999 self.status = BatchRefreshJobStatus::Idle {
1000 last_committed_epoch: completed_epoch,
1001 };
1002 }
1003 BatchRefreshJobStatus::FinishingSnapshot { .. } => {
1004 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
1005 }
1006 BatchRefreshJobStatus::ConsumingLogStore {
1007 target_upstream_epoch,
1008 ..
1009 } if completed_epoch == *target_upstream_epoch => {
1010 let target = *target_upstream_epoch;
1011 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
1012 info!(
1013 job_id = %self.job_id,
1014 completed_epoch,
1015 target_upstream_epoch = target,
1016 "batch refresh job: logstore done, transitioned to idle, removing partial graph"
1017 );
1018 partial_graph_manager.remove_partial_graphs(vec![self.partial_graph_id]);
1019 self.status = BatchRefreshJobStatus::Idle {
1020 last_committed_epoch: target,
1021 };
1022 }
1023 BatchRefreshJobStatus::ConsumingLogStore { .. } => {
1024 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
1025 }
1026 BatchRefreshJobStatus::Resetting { .. } => {
1027 }
1030 BatchRefreshJobStatus::Idle { .. }
1031 | BatchRefreshJobStatus::InitializingBatchRefresh { .. } => {
1032 unreachable!("batch refresh job should not be completing in this state")
1033 }
1034 }
1035 }
1036
1037 pub(super) fn on_partial_graph_reset(mut self) {
1039 match &mut self.status {
1040 BatchRefreshJobStatus::Resetting { notifiers } => {
1041 for notifier in notifiers.drain(..) {
1042 notifier.notify_collected();
1043 }
1044 }
1045 _ => {
1046 panic!(
1047 "batch refresh job {}: on_partial_graph_reset in unexpected state {:?}",
1048 self.job_id, self.status
1049 );
1050 }
1051 }
1052 }
1053}
1054
1055impl BatchRefreshJobCheckpointControl {
1058 pub(crate) fn gen_backfill_progress(&self) -> Option<BackfillProgress> {
1059 match &self.status {
1060 BatchRefreshJobStatus::ConsumingSnapshot {
1061 create_mview_tracker,
1062 ..
1063 } => {
1064 let progress = if create_mview_tracker.is_finished() {
1065 "Snapshot finished".to_owned()
1066 } else {
1067 let progress = create_mview_tracker.gen_backfill_progress();
1068 format!("BatchRefresh Snapshot [{}]", progress)
1069 };
1070 Some(BackfillProgress {
1071 progress,
1072 backfill_type: PbBackfillType::SnapshotBackfill,
1073 })
1074 }
1075 BatchRefreshJobStatus::FinishingSnapshot { .. } => Some(BackfillProgress {
1076 progress: "BatchRefresh Stopping".to_owned(),
1077 backfill_type: PbBackfillType::SnapshotBackfill,
1078 }),
1079 BatchRefreshJobStatus::InitializingBatchRefresh { .. }
1080 | BatchRefreshJobStatus::ConsumingLogStore { .. } => Some(BackfillProgress {
1081 progress: "BatchRefresh LogStore".to_owned(),
1082 backfill_type: PbBackfillType::SnapshotBackfill,
1083 }),
1084 BatchRefreshJobStatus::Idle { .. } | BatchRefreshJobStatus::Resetting { .. } => None,
1085 }
1086 }
1087
1088 pub(super) fn gen_fragment_backfill_progress(&self) -> Vec<FragmentBackfillProgress> {
1089 match &self.status {
1090 BatchRefreshJobStatus::ConsumingSnapshot {
1091 create_mview_tracker,
1092 fragment_infos,
1093 ..
1094 } => create_mview_tracker.collect_fragment_progress(fragment_infos, true),
1095 BatchRefreshJobStatus::FinishingSnapshot { fragment_infos, .. } => {
1096 collect_done_fragments(self.job_id, fragment_infos)
1097 }
1098 _ => vec![],
1099 }
1100 }
1101
1102 pub(super) fn pinned_upstream_log_epoch(&self) -> (u64, HashSet<TableId>) {
1104 match &self.status {
1105 BatchRefreshJobStatus::ConsumingSnapshot { .. }
1106 | BatchRefreshJobStatus::FinishingSnapshot { .. } => (
1107 self.snapshot_epoch,
1108 self.snapshot_backfill_upstream_tables.clone(),
1109 ),
1110 BatchRefreshJobStatus::ConsumingLogStore {
1111 logstore_start_epoch,
1112 ..
1113 }
1114 | BatchRefreshJobStatus::InitializingBatchRefresh {
1115 logstore_start_epoch,
1116 ..
1117 } => (
1118 *logstore_start_epoch,
1119 self.snapshot_backfill_upstream_tables.clone(),
1120 ),
1121 BatchRefreshJobStatus::Idle {
1122 last_committed_epoch,
1123 } => (
1124 *last_committed_epoch,
1125 self.snapshot_backfill_upstream_tables.clone(),
1126 ),
1127 BatchRefreshJobStatus::Resetting { .. } => (0, HashSet::new()),
1128 }
1129 }
1130
1131 pub(crate) fn fragment_infos(&self) -> Option<&HashMap<FragmentId, InflightFragmentInfo>> {
1132 match &self.status {
1133 BatchRefreshJobStatus::ConsumingSnapshot { fragment_infos, .. } => Some(fragment_infos),
1134 BatchRefreshJobStatus::InitializingBatchRefresh { fragment_infos, .. } => {
1135 Some(fragment_infos)
1136 }
1137 BatchRefreshJobStatus::ConsumingLogStore { fragment_infos, .. } => Some(fragment_infos),
1138 BatchRefreshJobStatus::FinishingSnapshot { .. }
1139 | BatchRefreshJobStatus::Idle { .. }
1140 | BatchRefreshJobStatus::Resetting { .. } => None,
1141 }
1142 }
1143
1144 pub(crate) fn pre_apply_throttle(
1145 &mut self,
1146 config: &mut ThrottleConfigMap,
1147 ) -> Option<Mutation> {
1148 let BatchRefreshJobStatus::ConsumingSnapshot {
1149 fragment_infos,
1150 create_mview_tracker,
1151 ..
1152 } = &mut self.status
1153 else {
1154 return None;
1155 };
1156 if create_mview_tracker.is_finished() {
1157 return None;
1158 }
1159
1160 extract_throttle_config(config, |fragment_id, stream_node| {
1161 if let Some(fragment_info) = fragment_infos.get_mut(&fragment_id) {
1162 fragment_info.nodes = stream_node.clone();
1163 true
1164 } else {
1165 false
1166 }
1167 })
1168 }
1169
1170 pub(crate) fn should_start_refresh(&self, upstream_committed_epoch: u64) -> bool {
1175 if let BatchRefreshJobStatus::Idle {
1176 last_committed_epoch,
1177 } = &self.status
1178 {
1179 let job_physical_ms = Epoch(*last_committed_epoch).physical_time();
1180 let upstream_physical_ms = Epoch(upstream_committed_epoch).physical_time();
1181 let threshold_ms = self.batch_refresh_seconds * 1000;
1182 upstream_physical_ms.saturating_sub(job_physical_ms) >= threshold_ms
1183 } else {
1184 false
1185 }
1186 }
1187
1188 pub(crate) fn last_committed_epoch(&self) -> Option<u64> {
1190 if let BatchRefreshJobStatus::Idle {
1191 last_committed_epoch,
1192 } = &self.status
1193 {
1194 Some(*last_committed_epoch)
1195 } else {
1196 None
1197 }
1198 }
1199}
1200
1201impl BatchRefreshJobCheckpointControl {
1204 pub(crate) fn start_refresh_run(
1216 &mut self,
1217 context: &BatchRefreshJobTriggerContext,
1218 worker_nodes: &HashMap<WorkerId, WorkerNode>,
1219 actor_id_counter: &AtomicU32,
1220 partial_graph_manager: &mut PartialGraphManager,
1221 ) -> MetaResult<bool> {
1222 let last_committed_epoch = match &self.status {
1223 BatchRefreshJobStatus::Idle {
1224 last_committed_epoch,
1225 } => *last_committed_epoch,
1226 _ => panic!(
1227 "batch refresh job {}: start_refresh_run called in non-Idle state {:?}",
1228 self.job_id, self.status
1229 ),
1230 };
1231
1232 let target_upstream_epoch = context.target_upstream_epoch;
1234 let Some((first_epoch, pending_log_barriers)) = Self::resolve_log_epoch_barriers(
1235 &self.snapshot_backfill_upstream_tables,
1236 &context.upstream_table_log_epochs,
1237 last_committed_epoch,
1238 )?
1239 else {
1240 info!(
1241 job_id = %self.job_id,
1242 last_committed_epoch,
1243 target_upstream_epoch,
1244 "batch refresh job: no log epochs to consume, staying idle"
1245 );
1246 return Ok(false);
1247 };
1248
1249 let log_target_epoch = pending_log_barriers.last().expect("non-empty").prev_epoch();
1250 if target_upstream_epoch != log_target_epoch {
1251 info!(
1252 job_id = %self.job_id,
1253 last_committed_epoch,
1254 target_upstream_epoch,
1255 log_target_epoch,
1256 "batch refresh job: upstream target has no resolved changelog yet, staying idle"
1257 );
1258 return Ok(false);
1259 }
1260
1261 let logical = BatchRefreshLogicalFragments::from_context(context);
1263
1264 let render_result = Self::render_actors_and_build_job_info(
1266 &logical.fragments,
1267 &logical.downstreams,
1268 &context.definition,
1269 actor_id_counter,
1270 worker_nodes,
1271 &context.database_resource_group,
1272 &context.streaming_job_model,
1273 self.partial_graph_id,
1274 )?;
1275
1276 let added_actors: Vec<ActorId> = render_result
1278 .fragment_infos
1279 .values()
1280 .flat_map(|fragment| fragment.actors.keys().copied())
1281 .collect();
1282
1283 let initial_mutation = Mutation::Add(AddMutation {
1284 actor_dispatchers: Default::default(),
1285 added_actors,
1286 actor_splits: Default::default(),
1287 pause: false,
1288 subscriptions_to_add: Default::default(),
1289 backfill_nodes_to_pause: Default::default(),
1290 actor_cdc_table_snapshot_splits: None,
1291 new_upstream_sinks: Default::default(),
1292 dropped_actors: Default::default(),
1293 sink_log_store_flush: Default::default(),
1294 });
1295
1296 let node_actors = &render_result.node_actors;
1297 let state_table_ids = &render_result.state_table_ids;
1298 let initial_barrier = BarrierInfo {
1299 prev_epoch: TracedEpoch::new(Epoch(last_committed_epoch)),
1300 curr_epoch: TracedEpoch::new(Epoch(first_epoch)),
1301 kind: BarrierKind::Initial,
1302 };
1303 let mut partial_graph_recoverer = partial_graph_manager.start_recover();
1304 let recover_result = partial_graph_recoverer.recover_graph(
1305 self.partial_graph_id,
1306 initial_mutation,
1307 &initial_barrier,
1308 node_actors,
1309 state_table_ids.iter().copied(),
1310 render_result.actors_to_create,
1311 BatchRefreshBarrierStats::new(self.job_id, self.snapshot_epoch),
1312 );
1313 match recover_result {
1314 Ok(()) => {
1315 let initializing_partial_graphs = partial_graph_recoverer.all_initializing();
1316 debug_assert_eq!(initializing_partial_graphs.len(), 1);
1317 debug_assert!(initializing_partial_graphs.contains(&self.partial_graph_id));
1318 }
1319 Err(e) => {
1320 partial_graph_recoverer.failed();
1321 return Err(e);
1322 }
1323 }
1324
1325 let logstore_start_epoch = last_committed_epoch;
1326
1327 info!(
1328 job_id = %self.job_id,
1329 last_committed_epoch,
1330 target_upstream_epoch,
1331 num_log_barriers = pending_log_barriers.len(),
1332 "batch refresh job: initialized logstore consumption partial graph"
1333 );
1334
1335 self.status = BatchRefreshJobStatus::InitializingBatchRefresh {
1336 fragment_infos: render_result.fragment_infos,
1337 node_actors: render_result.node_actors,
1338 state_table_ids: render_result.state_table_ids,
1339 pending_log_barriers,
1340 logstore_start_epoch,
1341 target_upstream_epoch,
1342 };
1343
1344 Ok(true)
1345 }
1346
1347 pub(crate) fn on_log_store_initialized(
1348 &mut self,
1349 partial_graph_manager: &mut PartialGraphManager,
1350 ) -> MetaResult<()> {
1351 let old_status = replace(
1352 &mut self.status,
1353 BatchRefreshJobStatus::Idle {
1354 last_committed_epoch: 0,
1355 },
1356 );
1357 let BatchRefreshJobStatus::InitializingBatchRefresh {
1358 fragment_infos,
1359 node_actors,
1360 state_table_ids,
1361 pending_log_barriers,
1362 logstore_start_epoch,
1363 target_upstream_epoch,
1364 } = old_status
1365 else {
1366 panic!(
1367 "batch refresh job {}: logstore initialized in unexpected status {:?}",
1368 self.job_id, old_status
1369 );
1370 };
1371
1372 let final_barrier_idx = pending_log_barriers.len() - 1;
1373 let mut stop_mutation = Some(Mutation::Stop(StopMutation {
1374 actors: fragment_infos
1375 .values()
1376 .flat_map(|fragment| fragment.actors.keys().copied())
1377 .collect(),
1378 dropped_sink_fragments: vec![],
1379 }));
1380 for (idx, barrier) in pending_log_barriers.into_iter().enumerate() {
1381 let is_stop_barrier = idx == final_barrier_idx;
1382 let mutation = is_stop_barrier.then(|| stop_mutation.take().expect("unused"));
1383 Self::inject_barrier(
1384 self.partial_graph_id,
1385 partial_graph_manager,
1386 &node_actors,
1387 &state_table_ids,
1388 barrier,
1389 None,
1390 mutation,
1391 None,
1392 None,
1393 is_stop_barrier,
1394 )?;
1395 }
1396
1397 self.status = BatchRefreshJobStatus::ConsumingLogStore {
1398 fragment_infos,
1399 logstore_start_epoch,
1400 target_upstream_epoch,
1401 };
1402 Ok(())
1403 }
1404
1405 fn resolve_log_epoch_barriers(
1411 snapshot_backfill_upstream_tables: &HashSet<TableId>,
1412 upstream_table_log_epochs: &HashMap<TableId, Vec<(Vec<u64>, u64)>>,
1413 exclusive_start_log_epoch: u64,
1414 ) -> MetaResult<Option<(u64, Vec<BarrierInfo>)>> {
1415 let table_id = snapshot_backfill_upstream_tables
1416 .iter()
1417 .next()
1418 .expect("snapshot backfill job should have upstream");
1419 let Some(epochs) = upstream_table_log_epochs.get(table_id) else {
1420 return Ok(None);
1421 };
1422
1423 let mut epochs_iter = epochs.iter().peekable();
1425 loop {
1426 match epochs_iter.peek() {
1427 Some((_, checkpoint_epoch)) if *checkpoint_epoch <= exclusive_start_log_epoch => {
1428 epochs_iter.next();
1429 }
1430 _ => break,
1431 }
1432 }
1433
1434 let mut epoch_infos = vec![];
1435 for (non_checkpoint_epochs, checkpoint_epoch) in epochs_iter {
1436 epoch_infos.extend(
1437 non_checkpoint_epochs
1438 .iter()
1439 .copied()
1440 .map(|epoch| (epoch, false)),
1441 );
1442 epoch_infos.push((*checkpoint_epoch, true));
1443 }
1444 if epoch_infos.is_empty() {
1445 return Ok(None);
1446 }
1447
1448 let first_epoch = epoch_infos[0].0;
1449 let mut pending_non_checkpoint_epochs = vec![];
1450 let mut replay_barriers = vec![];
1451 for window in epoch_infos.windows(2) {
1452 let (prev_epoch, is_checkpoint) = window[0];
1453 let curr_epoch = window[1].0;
1454 assert!(prev_epoch > exclusive_start_log_epoch);
1455 assert!(curr_epoch > prev_epoch);
1456 pending_non_checkpoint_epochs.push(prev_epoch);
1457 let kind = if is_checkpoint {
1458 BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_epochs))
1459 } else {
1460 BarrierKind::Barrier
1461 };
1462 replay_barriers.push(BarrierInfo {
1463 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
1464 curr_epoch: TracedEpoch::new(Epoch(curr_epoch)),
1465 kind,
1466 });
1467 }
1468
1469 let (last_epoch, _) = *epoch_infos.last().expect("non-empty");
1470 assert!(last_epoch > exclusive_start_log_epoch);
1471 pending_non_checkpoint_epochs.push(last_epoch);
1472 replay_barriers.push(BarrierInfo {
1473 prev_epoch: TracedEpoch::new(Epoch(last_epoch)),
1474 curr_epoch: TracedEpoch::new(Epoch(u64::MAX)),
1475 kind: BarrierKind::Checkpoint(pending_non_checkpoint_epochs),
1476 });
1477
1478 Ok(Some((first_epoch, replay_barriers)))
1479 }
1480}
1481
1482impl BatchRefreshLogicalFragments {
1483 pub(crate) fn from_context(ctx: &BatchRefreshJobTriggerContext) -> Self {
1485 Self {
1486 fragments: ctx.fragments.clone(),
1487 downstreams: ctx.downstreams.clone(),
1488 }
1489 }
1490}
1491
1492impl BatchRefreshJobCheckpointControl {
1495 pub(super) fn drop(
1497 &mut self,
1498 notifier: Option<&mut NotifierStarter>,
1499 partial_graph_manager: &mut PartialGraphManager,
1500 ) -> bool {
1501 match &mut self.status {
1502 BatchRefreshJobStatus::Resetting {
1503 notifiers: existing_notifiers,
1504 ..
1505 } => {
1506 existing_notifiers.extend(notifier.map(NotifierStarter::add_notify));
1507 true
1508 }
1509 BatchRefreshJobStatus::ConsumingSnapshot { .. }
1510 | BatchRefreshJobStatus::FinishingSnapshot { .. }
1511 | BatchRefreshJobStatus::InitializingBatchRefresh { .. }
1512 | BatchRefreshJobStatus::ConsumingLogStore { .. } => {
1513 partial_graph_manager.reset_partial_graphs([self.partial_graph_id]);
1514 self.status = BatchRefreshJobStatus::Resetting {
1515 notifiers: notifier
1516 .map(NotifierStarter::add_notify)
1517 .into_iter()
1518 .collect(),
1519 };
1520 true
1521 }
1522 BatchRefreshJobStatus::Idle { .. } => {
1523 partial_graph_manager.reset_partial_graphs([self.partial_graph_id]);
1526 self.status = BatchRefreshJobStatus::Resetting {
1527 notifiers: notifier
1528 .map(NotifierStarter::add_notify)
1529 .into_iter()
1530 .collect(),
1531 };
1532 true
1533 }
1534 }
1535 }
1536
1537 pub(crate) fn reset(self) -> bool {
1542 match self.status {
1543 BatchRefreshJobStatus::ConsumingSnapshot { .. }
1544 | BatchRefreshJobStatus::FinishingSnapshot { .. }
1545 | BatchRefreshJobStatus::InitializingBatchRefresh { .. }
1546 | BatchRefreshJobStatus::ConsumingLogStore { .. }
1547 | BatchRefreshJobStatus::Idle { .. } => false,
1548 BatchRefreshJobStatus::Resetting { notifiers, .. } => {
1549 for notifier in notifiers {
1550 notifier.notify_collected();
1551 }
1552 true
1553 }
1554 }
1555 }
1556}
1557
1558struct BatchRefreshBarrierStats {
1561 barrier_latency: LabelGuardedHistogram,
1562 inflight_barrier_num: LabelGuardedIntGauge,
1563}
1564
1565impl BatchRefreshBarrierStats {
1566 fn new(job_id: JobId, _snapshot_epoch: u64) -> Self {
1567 let table_id_str = format!("{}", job_id);
1568 Self {
1569 barrier_latency: GLOBAL_META_METRICS
1570 .snapshot_backfill_barrier_latency
1571 .with_guarded_label_values(&[table_id_str.as_str(), "batch_refresh_snapshot"]),
1572 inflight_barrier_num: GLOBAL_META_METRICS
1573 .snapshot_backfill_inflight_barrier_num
1574 .with_guarded_label_values(&[&table_id_str]),
1575 }
1576 }
1577}
1578
1579impl PartialGraphStat for BatchRefreshBarrierStats {
1580 fn observe_barrier_latency(&self, _epoch: EpochPair, barrier_latency_secs: f64) {
1581 self.barrier_latency.observe(barrier_latency_secs);
1582 }
1583
1584 fn observe_barrier_num(&self, inflight_barrier_num: usize, _collected_barrier_num: usize) {
1585 self.inflight_barrier_num.set(inflight_barrier_num as _);
1586 }
1587}