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;
49use crate::barrier::context::CreateSnapshotBackfillJobCommandInfo;
50use crate::barrier::edge_builder::{EdgeBuilderFragmentInfo, FragmentEdgeBuilder};
51use crate::barrier::info::BarrierInfo;
52use crate::barrier::notifier::Notifier;
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<Notifier> },
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 notifiers: Vec<Notifier>,
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 notifiers,
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 notifiers: Vec<Notifier>,
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 node_actors,
708 state_table_ids.iter().copied(),
709 if is_stop {
710 itertools::Either::Left(std::iter::empty())
712 } else {
713 itertools::Either::Right(node_actors.keys().copied())
714 },
715 new_actors,
716 PartialGraphBarrierInfo::new(
717 first_create_info.map_or_else(
718 PostCollectCommand::barrier,
719 CreateSnapshotBackfillJobCommandInfo::into_post_collect,
720 ),
721 barrier_info,
722 notifiers,
723 state_table_ids.clone(),
724 ),
725 )?;
726 Ok(())
727 }
728}
729
730impl BatchRefreshJobCheckpointControl {
733 pub(crate) fn on_new_upstream_barrier(
734 &mut self,
735 partial_graph_manager: &mut PartialGraphManager,
736 barrier_info: &BarrierInfo,
737 mutation: Option<(Mutation, Vec<Notifier>)>,
738 ) -> MetaResult<()> {
739 if !matches!(self.status, BatchRefreshJobStatus::ConsumingSnapshot { .. }) {
740 return Ok(());
743 }
744 let (mut mutation, mut notifiers) = match mutation {
745 Some((mutation, notifiers)) => (Some(mutation), notifiers),
746 None => (None, vec![]),
747 };
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 mutation.take();
759
760 let old_status = replace(
763 &mut self.status,
764 BatchRefreshJobStatus::Idle {
765 last_committed_epoch: 0,
766 },
767 );
768 let BatchRefreshJobStatus::ConsumingSnapshot {
769 prev_epoch_fake_physical_time,
770 mut pending_non_checkpoint_barriers,
771 snapshot_epoch,
772 fragment_infos,
773 create_mview_tracker,
774 node_actors,
775 state_table_ids,
776 ..
777 } = old_status
778 else {
779 unreachable!()
780 };
781
782 let tracking_job = create_mview_tracker.into_tracking_job();
783
784 pending_non_checkpoint_barriers.push(snapshot_epoch);
786 let prev_epoch = Epoch::from_physical_time(prev_epoch_fake_physical_time);
787 let final_checkpoint = BarrierInfo {
788 curr_epoch: TracedEpoch::new(Epoch(snapshot_epoch)),
789 prev_epoch: TracedEpoch::new(prev_epoch),
790 kind: BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_barriers)),
791 };
792
793 let stop_barrier = BarrierInfo {
795 prev_epoch: TracedEpoch::new(Epoch(snapshot_epoch)),
796 curr_epoch: TracedEpoch::new(Epoch(u64::MAX)),
797 kind: BarrierKind::Checkpoint(vec![snapshot_epoch]),
798 };
799
800 let stop_actors: Vec<ActorId> = fragment_infos
801 .values()
802 .flat_map(|f| f.actors.keys().copied())
803 .collect();
804
805 Self::inject_barrier(
806 self.partial_graph_id,
807 partial_graph_manager,
808 &node_actors,
809 &state_table_ids,
810 final_checkpoint,
811 None,
812 None,
813 take(&mut notifiers),
814 None,
815 false,
816 )?;
817 Self::inject_barrier(
818 self.partial_graph_id,
819 partial_graph_manager,
820 &node_actors,
821 &state_table_ids,
822 stop_barrier,
823 None,
824 Some(Mutation::Stop(StopMutation {
825 actors: stop_actors,
826 dropped_sink_fragments: vec![],
827 })),
828 vec![],
829 None,
830 true,
831 )?;
832
833 self.status = BatchRefreshJobStatus::FinishingSnapshot {
834 tracking_job: Some(tracking_job),
835 fragment_infos,
836 };
837 } else {
838 let BatchRefreshJobStatus::ConsumingSnapshot {
840 prev_epoch_fake_physical_time,
841 pending_non_checkpoint_barriers,
842 create_mview_tracker,
843 node_actors,
844 state_table_ids,
845 ..
846 } = &mut self.status
847 else {
848 unreachable!("is_finished was false, status must be ConsumingSnapshot")
849 };
850
851 let mutation = mutation.take().or_else(|| {
853 let pending_backfill_nodes = create_mview_tracker
854 .take_pending_backfill_nodes()
855 .collect_vec();
856 if pending_backfill_nodes.is_empty() {
857 None
858 } else {
859 Some(Mutation::StartFragmentBackfill(
860 StartFragmentBackfillMutation {
861 fragment_ids: pending_backfill_nodes,
862 },
863 ))
864 }
865 });
866 let barrier_to_inject = super::new_fake_barrier(
867 prev_epoch_fake_physical_time,
868 pending_non_checkpoint_barriers,
869 match barrier_info.kind {
870 BarrierKind::Barrier => PbBarrierKind::Barrier,
871 BarrierKind::Checkpoint(_) => PbBarrierKind::Checkpoint,
872 BarrierKind::Initial => {
873 unreachable!("upstream new epoch should not be initial")
874 }
875 },
876 );
877 Self::inject_barrier(
878 self.partial_graph_id,
879 partial_graph_manager,
880 node_actors,
881 state_table_ids,
882 barrier_to_inject,
883 None,
884 mutation,
885 take(&mut notifiers),
886 None,
887 false,
888 )?;
889 }
890 assert!(mutation.is_none(), "must have consumed mutation");
891 assert!(notifiers.is_empty(), "must consumed notifiers");
892 Ok(())
893 }
894
895 pub(crate) fn collect(&mut self, collected_barrier: CollectedBarrier<'_>) -> bool {
896 match &mut self.status {
897 BatchRefreshJobStatus::ConsumingSnapshot {
898 create_mview_tracker,
899 version_stats,
900 ..
901 } => {
902 for progress in collected_barrier
903 .resps
904 .values()
905 .flat_map(|resp| &resp.create_mview_progress)
906 {
907 create_mview_tracker.apply_progress(progress, version_stats);
908 }
909 create_mview_tracker.is_finished()
910 }
911 BatchRefreshJobStatus::InitializingBatchRefresh { .. }
912 | BatchRefreshJobStatus::ConsumingLogStore { .. } => {
913 false
915 }
916 _ => false,
917 }
918 }
919}
920
921impl BatchRefreshJobCheckpointControl {
924 #[expect(clippy::type_complexity)]
925 pub(crate) fn start_completing(
926 &mut self,
927 partial_graph_manager: &mut PartialGraphManager,
928 ) -> Option<(
929 u64,
930 HashMap<WorkerId, BarrierCompleteResponse>,
931 PartialGraphBarrierInfo,
932 Option<TrackingJob>,
933 )> {
934 match &self.status {
935 BatchRefreshJobStatus::ConsumingSnapshot { .. }
936 | BatchRefreshJobStatus::FinishingSnapshot { .. }
937 | BatchRefreshJobStatus::ConsumingLogStore { .. } => {}
938 BatchRefreshJobStatus::Idle { .. }
939 | BatchRefreshJobStatus::InitializingBatchRefresh { .. }
940 | BatchRefreshJobStatus::Resetting { .. } => {
941 return None;
942 }
943 };
944
945 partial_graph_manager
946 .start_completing(
947 self.partial_graph_id,
948 std::ops::Bound::Unbounded,
949 |_non_checkpoint_epoch, _resps, _| {
950 },
952 )
953 .map(|(epoch, resps, info)| {
954 let tracking_job = match &mut self.status {
960 BatchRefreshJobStatus::FinishingSnapshot { tracking_job, .. }
961 if epoch == self.snapshot_epoch =>
962 {
963 Some(
964 tracking_job
965 .take()
966 .expect("tracking job should not have been taken yet"),
967 )
968 }
969 _ => None,
970 };
971 (epoch, resps, info, tracking_job)
972 })
973 }
974
975 pub(super) fn ack_completed(
976 &mut self,
977 partial_graph_manager: &mut PartialGraphManager,
978 completed_epoch: u64,
979 ) {
980 match &self.status {
981 BatchRefreshJobStatus::ConsumingSnapshot { .. } => {
982 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
983 }
984 BatchRefreshJobStatus::FinishingSnapshot { tracking_job, .. }
985 if completed_epoch == self.snapshot_epoch =>
986 {
987 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
988 assert!(
989 tracking_job.is_none(),
990 "tracking job should have been taken at start_completing"
991 );
992 info!(
993 job_id = %self.job_id,
994 completed_epoch,
995 "batch refresh job: snapshot done, transitioned to idle, removing partial graph"
996 );
997 partial_graph_manager.remove_partial_graphs(vec![self.partial_graph_id]);
998 self.status = BatchRefreshJobStatus::Idle {
999 last_committed_epoch: completed_epoch,
1000 };
1001 }
1002 BatchRefreshJobStatus::FinishingSnapshot { .. } => {
1003 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
1004 }
1005 BatchRefreshJobStatus::ConsumingLogStore {
1006 target_upstream_epoch,
1007 ..
1008 } if completed_epoch == *target_upstream_epoch => {
1009 let target = *target_upstream_epoch;
1010 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
1011 info!(
1012 job_id = %self.job_id,
1013 completed_epoch,
1014 target_upstream_epoch = target,
1015 "batch refresh job: logstore done, transitioned to idle, removing partial graph"
1016 );
1017 partial_graph_manager.remove_partial_graphs(vec![self.partial_graph_id]);
1018 self.status = BatchRefreshJobStatus::Idle {
1019 last_committed_epoch: target,
1020 };
1021 }
1022 BatchRefreshJobStatus::ConsumingLogStore { .. } => {
1023 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
1024 }
1025 BatchRefreshJobStatus::Resetting { .. } => {
1026 }
1029 BatchRefreshJobStatus::Idle { .. }
1030 | BatchRefreshJobStatus::InitializingBatchRefresh { .. } => {
1031 unreachable!("batch refresh job should not be completing in this state")
1032 }
1033 }
1034 }
1035
1036 pub(super) fn on_partial_graph_reset(mut self) {
1038 match &mut self.status {
1039 BatchRefreshJobStatus::Resetting { notifiers } => {
1040 for notifier in notifiers.drain(..) {
1041 notifier.notify_collected();
1042 }
1043 }
1044 _ => {
1045 panic!(
1046 "batch refresh job {}: on_partial_graph_reset in unexpected state {:?}",
1047 self.job_id, self.status
1048 );
1049 }
1050 }
1051 }
1052}
1053
1054impl BatchRefreshJobCheckpointControl {
1057 pub(crate) fn gen_backfill_progress(&self) -> Option<BackfillProgress> {
1058 match &self.status {
1059 BatchRefreshJobStatus::ConsumingSnapshot {
1060 create_mview_tracker,
1061 ..
1062 } => {
1063 let progress = if create_mview_tracker.is_finished() {
1064 "Snapshot finished".to_owned()
1065 } else {
1066 let progress = create_mview_tracker.gen_backfill_progress();
1067 format!("BatchRefresh Snapshot [{}]", progress)
1068 };
1069 Some(BackfillProgress {
1070 progress,
1071 backfill_type: PbBackfillType::SnapshotBackfill,
1072 })
1073 }
1074 BatchRefreshJobStatus::FinishingSnapshot { .. } => Some(BackfillProgress {
1075 progress: "BatchRefresh Stopping".to_owned(),
1076 backfill_type: PbBackfillType::SnapshotBackfill,
1077 }),
1078 BatchRefreshJobStatus::InitializingBatchRefresh { .. }
1079 | BatchRefreshJobStatus::ConsumingLogStore { .. } => Some(BackfillProgress {
1080 progress: "BatchRefresh LogStore".to_owned(),
1081 backfill_type: PbBackfillType::SnapshotBackfill,
1082 }),
1083 BatchRefreshJobStatus::Idle { .. } | BatchRefreshJobStatus::Resetting { .. } => None,
1084 }
1085 }
1086
1087 pub(super) fn gen_fragment_backfill_progress(&self) -> Vec<FragmentBackfillProgress> {
1088 match &self.status {
1089 BatchRefreshJobStatus::ConsumingSnapshot {
1090 create_mview_tracker,
1091 fragment_infos,
1092 ..
1093 } => create_mview_tracker.collect_fragment_progress(fragment_infos, true),
1094 BatchRefreshJobStatus::FinishingSnapshot { fragment_infos, .. } => {
1095 collect_done_fragments(self.job_id, fragment_infos)
1096 }
1097 _ => vec![],
1098 }
1099 }
1100
1101 pub(super) fn pinned_upstream_log_epoch(&self) -> (u64, HashSet<TableId>) {
1103 match &self.status {
1104 BatchRefreshJobStatus::ConsumingSnapshot { .. }
1105 | BatchRefreshJobStatus::FinishingSnapshot { .. } => (
1106 self.snapshot_epoch,
1107 self.snapshot_backfill_upstream_tables.clone(),
1108 ),
1109 BatchRefreshJobStatus::ConsumingLogStore {
1110 logstore_start_epoch,
1111 ..
1112 }
1113 | BatchRefreshJobStatus::InitializingBatchRefresh {
1114 logstore_start_epoch,
1115 ..
1116 } => (
1117 *logstore_start_epoch,
1118 self.snapshot_backfill_upstream_tables.clone(),
1119 ),
1120 BatchRefreshJobStatus::Idle {
1121 last_committed_epoch,
1122 } => (
1123 *last_committed_epoch,
1124 self.snapshot_backfill_upstream_tables.clone(),
1125 ),
1126 BatchRefreshJobStatus::Resetting { .. } => (0, HashSet::new()),
1127 }
1128 }
1129
1130 pub(crate) fn fragment_infos(&self) -> Option<&HashMap<FragmentId, InflightFragmentInfo>> {
1131 match &self.status {
1132 BatchRefreshJobStatus::ConsumingSnapshot { fragment_infos, .. } => Some(fragment_infos),
1133 BatchRefreshJobStatus::InitializingBatchRefresh { fragment_infos, .. } => {
1134 Some(fragment_infos)
1135 }
1136 BatchRefreshJobStatus::ConsumingLogStore { fragment_infos, .. } => Some(fragment_infos),
1137 BatchRefreshJobStatus::FinishingSnapshot { .. }
1138 | BatchRefreshJobStatus::Idle { .. }
1139 | BatchRefreshJobStatus::Resetting { .. } => None,
1140 }
1141 }
1142
1143 pub(crate) fn should_start_refresh(&self, upstream_committed_epoch: u64) -> bool {
1148 if let BatchRefreshJobStatus::Idle {
1149 last_committed_epoch,
1150 } = &self.status
1151 {
1152 let job_physical_ms = Epoch(*last_committed_epoch).physical_time();
1153 let upstream_physical_ms = Epoch(upstream_committed_epoch).physical_time();
1154 let threshold_ms = self.batch_refresh_seconds * 1000;
1155 upstream_physical_ms.saturating_sub(job_physical_ms) >= threshold_ms
1156 } else {
1157 false
1158 }
1159 }
1160
1161 pub(crate) fn last_committed_epoch(&self) -> Option<u64> {
1163 if let BatchRefreshJobStatus::Idle {
1164 last_committed_epoch,
1165 } = &self.status
1166 {
1167 Some(*last_committed_epoch)
1168 } else {
1169 None
1170 }
1171 }
1172}
1173
1174impl BatchRefreshJobCheckpointControl {
1177 pub(crate) fn start_refresh_run(
1189 &mut self,
1190 context: &BatchRefreshJobTriggerContext,
1191 worker_nodes: &HashMap<WorkerId, WorkerNode>,
1192 actor_id_counter: &AtomicU32,
1193 partial_graph_manager: &mut PartialGraphManager,
1194 ) -> MetaResult<bool> {
1195 let last_committed_epoch = match &self.status {
1196 BatchRefreshJobStatus::Idle {
1197 last_committed_epoch,
1198 } => *last_committed_epoch,
1199 _ => panic!(
1200 "batch refresh job {}: start_refresh_run called in non-Idle state {:?}",
1201 self.job_id, self.status
1202 ),
1203 };
1204
1205 let target_upstream_epoch = context.target_upstream_epoch;
1207 let Some((first_epoch, pending_log_barriers)) = Self::resolve_log_epoch_barriers(
1208 &self.snapshot_backfill_upstream_tables,
1209 &context.upstream_table_log_epochs,
1210 last_committed_epoch,
1211 )?
1212 else {
1213 info!(
1214 job_id = %self.job_id,
1215 last_committed_epoch,
1216 target_upstream_epoch,
1217 "batch refresh job: no log epochs to consume, staying idle"
1218 );
1219 return Ok(false);
1220 };
1221
1222 let log_target_epoch = pending_log_barriers.last().expect("non-empty").prev_epoch();
1223 if target_upstream_epoch != log_target_epoch {
1224 info!(
1225 job_id = %self.job_id,
1226 last_committed_epoch,
1227 target_upstream_epoch,
1228 log_target_epoch,
1229 "batch refresh job: upstream target has no resolved changelog yet, staying idle"
1230 );
1231 return Ok(false);
1232 }
1233
1234 let logical = BatchRefreshLogicalFragments::from_context(context);
1236
1237 let render_result = Self::render_actors_and_build_job_info(
1239 &logical.fragments,
1240 &logical.downstreams,
1241 &context.definition,
1242 actor_id_counter,
1243 worker_nodes,
1244 &context.database_resource_group,
1245 &context.streaming_job_model,
1246 self.partial_graph_id,
1247 )?;
1248
1249 let added_actors: Vec<ActorId> = render_result
1251 .fragment_infos
1252 .values()
1253 .flat_map(|fragment| fragment.actors.keys().copied())
1254 .collect();
1255
1256 let initial_mutation = Mutation::Add(AddMutation {
1257 actor_dispatchers: Default::default(),
1258 added_actors,
1259 actor_splits: Default::default(),
1260 pause: false,
1261 subscriptions_to_add: Default::default(),
1262 backfill_nodes_to_pause: Default::default(),
1263 actor_cdc_table_snapshot_splits: None,
1264 new_upstream_sinks: Default::default(),
1265 dropped_actors: Default::default(),
1266 sink_log_store_flush: Default::default(),
1267 });
1268
1269 let node_actors = &render_result.node_actors;
1270 let state_table_ids = &render_result.state_table_ids;
1271 let initial_barrier = BarrierInfo {
1272 prev_epoch: TracedEpoch::new(Epoch(last_committed_epoch)),
1273 curr_epoch: TracedEpoch::new(Epoch(first_epoch)),
1274 kind: BarrierKind::Initial,
1275 };
1276 let mut partial_graph_recoverer = partial_graph_manager.start_recover();
1277 let recover_result = partial_graph_recoverer.recover_graph(
1278 self.partial_graph_id,
1279 initial_mutation,
1280 &initial_barrier,
1281 node_actors,
1282 state_table_ids.iter().copied(),
1283 render_result.actors_to_create,
1284 BatchRefreshBarrierStats::new(self.job_id, self.snapshot_epoch),
1285 );
1286 match recover_result {
1287 Ok(()) => {
1288 let initializing_partial_graphs = partial_graph_recoverer.all_initializing();
1289 debug_assert_eq!(initializing_partial_graphs.len(), 1);
1290 debug_assert!(initializing_partial_graphs.contains(&self.partial_graph_id));
1291 }
1292 Err(e) => {
1293 partial_graph_recoverer.failed();
1294 return Err(e);
1295 }
1296 }
1297
1298 let logstore_start_epoch = last_committed_epoch;
1299
1300 info!(
1301 job_id = %self.job_id,
1302 last_committed_epoch,
1303 target_upstream_epoch,
1304 num_log_barriers = pending_log_barriers.len(),
1305 "batch refresh job: initialized logstore consumption partial graph"
1306 );
1307
1308 self.status = BatchRefreshJobStatus::InitializingBatchRefresh {
1309 fragment_infos: render_result.fragment_infos,
1310 node_actors: render_result.node_actors,
1311 state_table_ids: render_result.state_table_ids,
1312 pending_log_barriers,
1313 logstore_start_epoch,
1314 target_upstream_epoch,
1315 };
1316
1317 Ok(true)
1318 }
1319
1320 pub(crate) fn on_log_store_initialized(
1321 &mut self,
1322 partial_graph_manager: &mut PartialGraphManager,
1323 ) -> MetaResult<()> {
1324 let old_status = replace(
1325 &mut self.status,
1326 BatchRefreshJobStatus::Idle {
1327 last_committed_epoch: 0,
1328 },
1329 );
1330 let BatchRefreshJobStatus::InitializingBatchRefresh {
1331 fragment_infos,
1332 node_actors,
1333 state_table_ids,
1334 pending_log_barriers,
1335 logstore_start_epoch,
1336 target_upstream_epoch,
1337 } = old_status
1338 else {
1339 panic!(
1340 "batch refresh job {}: logstore initialized in unexpected status {:?}",
1341 self.job_id, old_status
1342 );
1343 };
1344
1345 let final_barrier_idx = pending_log_barriers.len() - 1;
1346 let mut stop_mutation = Some(Mutation::Stop(StopMutation {
1347 actors: fragment_infos
1348 .values()
1349 .flat_map(|fragment| fragment.actors.keys().copied())
1350 .collect(),
1351 dropped_sink_fragments: vec![],
1352 }));
1353 for (idx, barrier) in pending_log_barriers.into_iter().enumerate() {
1354 let is_stop_barrier = idx == final_barrier_idx;
1355 let mutation = is_stop_barrier.then(|| stop_mutation.take().expect("unused"));
1356 Self::inject_barrier(
1357 self.partial_graph_id,
1358 partial_graph_manager,
1359 &node_actors,
1360 &state_table_ids,
1361 barrier,
1362 None,
1363 mutation,
1364 vec![],
1365 None,
1366 is_stop_barrier,
1367 )?;
1368 }
1369
1370 self.status = BatchRefreshJobStatus::ConsumingLogStore {
1371 fragment_infos,
1372 logstore_start_epoch,
1373 target_upstream_epoch,
1374 };
1375 Ok(())
1376 }
1377
1378 fn resolve_log_epoch_barriers(
1384 snapshot_backfill_upstream_tables: &HashSet<TableId>,
1385 upstream_table_log_epochs: &HashMap<TableId, Vec<(Vec<u64>, u64)>>,
1386 exclusive_start_log_epoch: u64,
1387 ) -> MetaResult<Option<(u64, Vec<BarrierInfo>)>> {
1388 let table_id = snapshot_backfill_upstream_tables
1389 .iter()
1390 .next()
1391 .expect("snapshot backfill job should have upstream");
1392 let Some(epochs) = upstream_table_log_epochs.get(table_id) else {
1393 return Ok(None);
1394 };
1395
1396 let mut epochs_iter = epochs.iter().peekable();
1398 loop {
1399 match epochs_iter.peek() {
1400 Some((_, checkpoint_epoch)) if *checkpoint_epoch <= exclusive_start_log_epoch => {
1401 epochs_iter.next();
1402 }
1403 _ => break,
1404 }
1405 }
1406
1407 let mut epoch_infos = vec![];
1408 for (non_checkpoint_epochs, checkpoint_epoch) in epochs_iter {
1409 epoch_infos.extend(
1410 non_checkpoint_epochs
1411 .iter()
1412 .copied()
1413 .map(|epoch| (epoch, false)),
1414 );
1415 epoch_infos.push((*checkpoint_epoch, true));
1416 }
1417 if epoch_infos.is_empty() {
1418 return Ok(None);
1419 }
1420
1421 let first_epoch = epoch_infos[0].0;
1422 let mut pending_non_checkpoint_epochs = vec![];
1423 let mut replay_barriers = vec![];
1424 for window in epoch_infos.windows(2) {
1425 let (prev_epoch, is_checkpoint) = window[0];
1426 let curr_epoch = window[1].0;
1427 assert!(prev_epoch > exclusive_start_log_epoch);
1428 assert!(curr_epoch > prev_epoch);
1429 pending_non_checkpoint_epochs.push(prev_epoch);
1430 let kind = if is_checkpoint {
1431 BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_epochs))
1432 } else {
1433 BarrierKind::Barrier
1434 };
1435 replay_barriers.push(BarrierInfo {
1436 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
1437 curr_epoch: TracedEpoch::new(Epoch(curr_epoch)),
1438 kind,
1439 });
1440 }
1441
1442 let (last_epoch, _) = *epoch_infos.last().expect("non-empty");
1443 assert!(last_epoch > exclusive_start_log_epoch);
1444 pending_non_checkpoint_epochs.push(last_epoch);
1445 replay_barriers.push(BarrierInfo {
1446 prev_epoch: TracedEpoch::new(Epoch(last_epoch)),
1447 curr_epoch: TracedEpoch::new(Epoch(u64::MAX)),
1448 kind: BarrierKind::Checkpoint(pending_non_checkpoint_epochs),
1449 });
1450
1451 Ok(Some((first_epoch, replay_barriers)))
1452 }
1453}
1454
1455impl BatchRefreshLogicalFragments {
1456 pub(crate) fn from_context(ctx: &BatchRefreshJobTriggerContext) -> Self {
1458 Self {
1459 fragments: ctx.fragments.clone(),
1460 downstreams: ctx.downstreams.clone(),
1461 }
1462 }
1463}
1464
1465impl BatchRefreshJobCheckpointControl {
1468 pub(super) fn drop(
1470 &mut self,
1471 notifiers: &mut Vec<Notifier>,
1472 partial_graph_manager: &mut PartialGraphManager,
1473 ) -> bool {
1474 match &mut self.status {
1475 BatchRefreshJobStatus::Resetting {
1476 notifiers: existing_notifiers,
1477 ..
1478 } => {
1479 for notifier in &mut *notifiers {
1480 notifier.notify_started();
1481 }
1482 existing_notifiers.append(notifiers);
1483 true
1484 }
1485 BatchRefreshJobStatus::ConsumingSnapshot { .. }
1486 | BatchRefreshJobStatus::FinishingSnapshot { .. }
1487 | BatchRefreshJobStatus::InitializingBatchRefresh { .. }
1488 | BatchRefreshJobStatus::ConsumingLogStore { .. } => {
1489 for notifier in &mut *notifiers {
1490 notifier.notify_started();
1491 }
1492 partial_graph_manager.reset_partial_graphs([self.partial_graph_id]);
1493 self.status = BatchRefreshJobStatus::Resetting {
1494 notifiers: take(notifiers),
1495 };
1496 true
1497 }
1498 BatchRefreshJobStatus::Idle { .. } => {
1499 for notifier in &mut *notifiers {
1502 notifier.notify_started();
1503 }
1504 partial_graph_manager.reset_partial_graphs([self.partial_graph_id]);
1505 self.status = BatchRefreshJobStatus::Resetting {
1506 notifiers: take(notifiers),
1507 };
1508 true
1509 }
1510 }
1511 }
1512
1513 pub(crate) fn reset(self) -> bool {
1518 match self.status {
1519 BatchRefreshJobStatus::ConsumingSnapshot { .. }
1520 | BatchRefreshJobStatus::FinishingSnapshot { .. }
1521 | BatchRefreshJobStatus::InitializingBatchRefresh { .. }
1522 | BatchRefreshJobStatus::ConsumingLogStore { .. }
1523 | BatchRefreshJobStatus::Idle { .. } => false,
1524 BatchRefreshJobStatus::Resetting { notifiers, .. } => {
1525 for notifier in notifiers {
1526 notifier.notify_collected();
1527 }
1528 true
1529 }
1530 }
1531 }
1532}
1533
1534struct BatchRefreshBarrierStats {
1537 barrier_latency: LabelGuardedHistogram,
1538 inflight_barrier_num: LabelGuardedIntGauge,
1539}
1540
1541impl BatchRefreshBarrierStats {
1542 fn new(job_id: JobId, _snapshot_epoch: u64) -> Self {
1543 let table_id_str = format!("{}", job_id);
1544 Self {
1545 barrier_latency: GLOBAL_META_METRICS
1546 .snapshot_backfill_barrier_latency
1547 .with_guarded_label_values(&[table_id_str.as_str(), "batch_refresh_snapshot"]),
1548 inflight_barrier_num: GLOBAL_META_METRICS
1549 .snapshot_backfill_inflight_barrier_num
1550 .with_guarded_label_values(&[&table_id_str]),
1551 }
1552 }
1553}
1554
1555impl PartialGraphStat for BatchRefreshBarrierStats {
1556 fn observe_barrier_latency(&self, _epoch: EpochPair, barrier_latency_secs: f64) {
1557 self.barrier_latency.observe(barrier_latency_secs);
1558 }
1559
1560 fn observe_barrier_num(&self, inflight_barrier_num: usize, _collected_barrier_num: usize) {
1561 self.inflight_barrier_num.set(inflight_barrier_num as _);
1562 }
1563}