1use std::collections::{HashMap, HashSet, hash_map};
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_mutation::Mutation;
41use risingwave_pb::stream_plan::{AddMutation, StopMutation};
42use risingwave_pb::stream_service::BarrierCompleteResponse;
43use tracing::{debug, info};
44
45use crate::MetaResult;
46use crate::barrier::checkpoint::independent_job::{
47 IndependentCheckpointJob, IndependentCheckpointJobControl, IndependentCheckpointJobStatus,
48 IndependentJobControl, IndependentJobInfo, InitialPartialGraphRequest, SnapshotPhaseControl,
49 add_initial_partial_graph, build_initial_add_mutation,
50};
51use crate::barrier::command::{PostCollectCommand, ThrottleConfigMap, extract_throttle_config};
52use crate::barrier::context::CreateIndependentStreamingJobCommandInfo;
53use crate::barrier::edge_builder::FragmentEdgeBuilder;
54use crate::barrier::info::BarrierInfo;
55use crate::barrier::partial_graph::{
56 CollectedBarrier, PartialGraphBarrierInfo, PartialGraphManager, PartialGraphStat,
57};
58use crate::barrier::progress::{TrackingJob, collect_done_fragments};
59use crate::barrier::rpc::{ControlStreamManager, to_partial_graph_id};
60use crate::barrier::{BackfillProgress, BarrierKind, FragmentBackfillProgress, TracedEpoch};
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::notification::NotifierStarter;
70use crate::rpc::metrics::GLOBAL_META_METRICS;
71use crate::stream::ExtendedFragmentBackfillOrder;
72
73#[derive(Debug)]
84pub(crate) struct BatchRefreshLogicalFragments {
85 pub fragments: HashMap<FragmentId, LoadedFragment>,
87 pub downstreams: FragmentDownstreamRelation,
89}
90
91#[derive(Debug)]
96pub(crate) struct BatchRefreshRenderResult {
97 pub fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
98 pub node_actors: HashMap<WorkerId, HashSet<ActorId>>,
99 pub actors_to_create: StreamJobActorsToCreate,
100}
101
102#[derive(Debug)]
108pub(crate) struct RenderedIndependentJobActors {
109 pub fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
110 pub stream_actors: HashMap<FragmentId, Vec<StreamActor>>,
111}
112
113#[derive(Debug)]
121pub(crate) struct BatchRefreshJobTriggerContext {
122 pub fragments: HashMap<FragmentId, LoadedFragment>,
123 pub downstreams: FragmentDownstreamRelation,
124 pub streaming_job_model: streaming_job::Model,
125 pub definition: String,
126 pub database_resource_group: String,
127 pub upstream_table_log_epochs: HashMap<TableId, Vec<(Vec<u64>, u64)>>,
129 pub target_upstream_epoch: u64,
131}
132
133#[derive(Debug)]
136enum BatchRefreshJobStatus {
137 ConsumingSnapshot {
142 snapshot: SnapshotPhaseControl,
143 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
144 node_actors: HashMap<WorkerId, HashSet<ActorId>>,
145 },
146 FinishingSnapshot {
153 tracking_job: Option<TrackingJob>,
154 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
155 },
156 Idle,
158 InitializingBatchRefresh {
161 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
162 node_actors: HashMap<WorkerId, HashSet<ActorId>>,
163 pending_log_barriers: Vec<BarrierInfo>,
166 target_upstream_epoch: u64,
167 },
168 ConsumingLogStore {
174 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
175 target_upstream_epoch: u64,
177 },
178}
179
180#[derive(Debug)]
191pub(crate) struct BatchRefreshJobCheckpointControl {
192 control: IndependentJobControl,
193 batch_refresh_seconds: u64,
195
196 status: BatchRefreshJobStatus,
197}
198
199impl RenderedIndependentJobActors {
208 pub(crate) fn render(
209 fragments: &HashMap<FragmentId, LoadedFragment>,
210 downstreams: &FragmentDownstreamRelation,
211 definition: &str,
212 actor_id_generator: &AtomicU32,
213 worker_nodes: &HashMap<WorkerId, WorkerNode>,
214 database_resource_group: &str,
215 streaming_job_model: &streaming_job::Model,
216 ) -> MetaResult<Self> {
217 let ensembles =
218 BatchRefreshJobCheckpointControl::resolve_ensembles(fragments, downstreams)?;
219
220 let mut actor_assignments: HashMap<
221 FragmentId,
222 HashMap<ActorId, (WorkerId, Option<risingwave_common::bitmap::Bitmap>)>,
223 > = HashMap::new();
224
225 for ensemble in &ensembles {
226 let first_component = ensemble
227 .component_fragments()
228 .next()
229 .expect("ensemble must have at least one component");
230 let fragment = &fragments[&first_component];
231 let distribution_type = fragment.distribution_type;
232 let vnode_count = fragment.vnode_count;
233
234 for fid in ensemble.component_fragments() {
235 let f = &fragments[&fid];
236 assert_eq!(
237 vnode_count, f.vnode_count,
238 "fragments {} and {} in same ensemble have different vnode counts",
239 first_component, fid,
240 );
241 }
242
243 let entry_fragment_parallelism = Itertools::exactly_one(
244 ensemble
245 .entry_fragments()
246 .map(|fid| fragments[&fid].parallelism.clone())
247 .dedup(),
248 )
249 .map_err(|_| {
250 anyhow!("entry fragments have inconsistent parallelism settings in independent job")
251 })?;
252
253 let actor_template = EnsembleActorTemplate::render_new(
254 streaming_job_model,
255 worker_nodes,
256 entry_fragment_parallelism,
257 database_resource_group.to_owned(),
258 distribution_type,
259 vnode_count,
260 )?;
261
262 for fid in ensemble.component_fragments() {
263 let f = &fragments[&fid];
264 let aligner =
265 ComponentFragmentAligner::new_persistent(&actor_template, actor_id_generator);
266 let assignments = aligner.align_component_actor(f.distribution_type);
267 actor_assignments.insert(fid, assignments);
268 }
269 }
270
271 let stream_context = streaming_job_model.stream_context();
272 let mut stream_actors: HashMap<FragmentId, Vec<StreamActor>> = HashMap::new();
273 let mut actor_location: HashMap<ActorId, WorkerId> = HashMap::new();
274
275 for (fragment_id, assignments) in &actor_assignments {
276 let mut actors = Vec::with_capacity(assignments.len());
277 for (&actor_id, (worker_id, vnode_bitmap)) in assignments {
278 actor_location.insert(actor_id, *worker_id);
279 actors.push(StreamActor {
280 actor_id,
281 fragment_id: *fragment_id,
282 vnode_bitmap: vnode_bitmap.clone(),
283 mview_definition: definition.to_owned(),
284 expr_context: Some(stream_context.to_expr_context()),
285 config_override: stream_context.config_override.clone(),
286 });
287 }
288 stream_actors.insert(*fragment_id, actors);
289 }
290
291 let fragment_infos = fragments
292 .iter()
293 .map(|(fragment_id, loaded)| {
294 let actors = stream_actors
295 .get(fragment_id)
296 .into_iter()
297 .flatten()
298 .map(|actor| {
299 (
300 actor.actor_id,
301 crate::controller::fragment::InflightActorInfo {
302 worker_id: actor_location[&actor.actor_id],
303 vnode_bitmap: actor.vnode_bitmap.clone(),
304 splits: vec![],
305 },
306 )
307 })
308 .collect();
309 (
310 *fragment_id,
311 InflightFragmentInfo {
312 fragment_id: *fragment_id,
313 distribution_type: loaded.distribution_type,
314 fragment_type_mask: loaded.fragment_type_mask,
315 vnode_count: loaded.vnode_count,
316 nodes: loaded.nodes.clone(),
317 actors,
318 state_table_ids: loaded.state_table_ids.clone(),
319 },
320 )
321 })
322 .collect();
323
324 Ok(Self {
325 fragment_infos,
326 stream_actors,
327 })
328 }
329
330 pub(crate) fn build_job_info(
335 self,
336 downstreams: &FragmentDownstreamRelation,
337 partial_graph_id: PartialGraphId,
338 control_stream_manager: &ControlStreamManager,
339 ) -> MetaResult<BatchRefreshRenderResult> {
340 let Self {
341 fragment_infos,
342 stream_actors,
343 } = self;
344
345 let (mut edges, _) = FragmentEdgeBuilder::new()
346 .add_new_fragments(
347 fragment_infos.values(),
348 partial_graph_id,
349 control_stream_manager,
350 )
351 .finish_fragments()
352 .add_relations(downstreams)?
353 .build();
354
355 let actors_to_create =
356 edges.collect_actors_to_create(fragment_infos.values().map(|fragment| {
357 (
358 fragment.fragment_id,
359 &fragment.nodes,
360 fragment.actors.iter().map(|(actor_id, actor)| {
361 let stream_actor = stream_actors[&fragment.fragment_id]
362 .iter()
363 .find(|stream_actor| stream_actor.actor_id == *actor_id)
364 .expect("rendered actor should exist");
365 (stream_actor, actor.worker_id)
366 }),
367 vec![], )
369 }));
370
371 let node_actors = InflightFragmentInfo::actor_ids_to_collect(fragment_infos.values());
372 Ok(BatchRefreshRenderResult {
373 fragment_infos,
374 node_actors,
375 actors_to_create,
376 })
377 }
378}
379
380impl BatchRefreshJobCheckpointControl {
381 pub(crate) fn render_actors_and_build_job_info(
392 fragments: &HashMap<FragmentId, LoadedFragment>,
393 downstreams: &FragmentDownstreamRelation,
394 definition: &str,
395 actor_id_generator: &AtomicU32,
397 worker_nodes: &HashMap<WorkerId, WorkerNode>,
398 control_stream_manager: &ControlStreamManager,
399 database_resource_group: &str,
400 streaming_job_model: &streaming_job::Model,
401 partial_graph_id: PartialGraphId,
403 ) -> MetaResult<BatchRefreshRenderResult> {
404 let rendered = RenderedIndependentJobActors::render(
405 fragments,
406 downstreams,
407 definition,
408 actor_id_generator,
409 worker_nodes,
410 database_resource_group,
411 streaming_job_model,
412 )?;
413 rendered.build_job_info(downstreams, partial_graph_id, control_stream_manager)
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<'a>(
474 entry: hash_map::VacantEntry<'a, JobId, IndependentCheckpointJobControl>,
475 database_id: DatabaseId,
476 job_id: JobId,
477 create_info: CreateIndependentStreamingJobCommandInfo,
478 notifier: Option<&mut NotifierStarter>,
479 snapshot_backfill_upstream_tables: HashSet<TableId>,
480 snapshot_epoch: u64,
481 version_stat: &HummockVersionStats,
482 term_id: &str,
483 partial_graph_manager: &mut PartialGraphManager,
484 logical: &BatchRefreshLogicalFragments,
485 worker_nodes: &HashMap<WorkerId, WorkerNode>,
486 batch_refresh_seconds: u64,
487 ) -> MetaResult<&'a mut Self> {
488 debug!(
489 %job_id,
490 "new batch refresh job"
491 );
492
493 let partial_graph_id = to_partial_graph_id(database_id, Some(job_id));
494 let backfill_ordering = &create_info.info.fragment_backfill_ordering;
495 let actor_id_generator = partial_graph_manager
496 .control_stream_manager()
497 .env
498 .actor_id_generator();
499
500 let render_result = Self::render_actors_and_build_job_info(
501 &logical.fragments,
502 &logical.downstreams,
503 &create_info.info.definition,
504 actor_id_generator,
505 worker_nodes,
506 partial_graph_manager.control_stream_manager(),
507 &create_info.info.database_resource_group,
508 &create_info.info.streaming_job_model,
509 partial_graph_id,
510 )?;
511 let initial_partial_graph_mutation = build_initial_add_mutation(
512 &render_result.fragment_infos,
513 backfill_ordering,
514 Default::default(), );
516
517 let (snapshot, initial_barrier_info) = SnapshotPhaseControl::for_new_job(
518 snapshot_epoch,
519 &render_result.fragment_infos,
520 &create_info.info,
521 version_stat,
522 );
523
524 let BatchRefreshRenderResult {
525 fragment_infos,
526 node_actors,
527 actors_to_create,
528 } = render_result;
529 let control = IndependentJobControl::new(IndependentJobInfo::from_fragment_infos(
530 database_id,
531 job_id,
532 snapshot_epoch,
533 snapshot_backfill_upstream_tables,
534 fragment_infos.values(),
535 ));
536
537 let mut graph_adder = partial_graph_manager.add_partial_graph(
538 partial_graph_id,
539 term_id,
540 BatchRefreshBarrierStats::new(job_id, snapshot_epoch),
541 );
542 if let Err(e) = add_initial_partial_graph(
543 &mut graph_adder,
544 partial_graph_id,
545 InitialPartialGraphRequest {
546 node_actors: &node_actors,
547 state_table_ids: &control.info.state_table_ids,
548 barrier_info: initial_barrier_info,
549 actors_to_create,
550 mutation: initial_partial_graph_mutation,
551 notifier,
552 create_info,
553 },
554 ) {
555 graph_adder.failed();
556 entry.insert(IndependentCheckpointJobControl::resetting(
557 control.info.snapshot_backfill_upstream_tables,
558 ));
559 return Err(e);
560 }
561 graph_adder.added();
562 let this = Self {
563 control,
564 batch_refresh_seconds,
565 status: BatchRefreshJobStatus::ConsumingSnapshot {
566 snapshot,
567 fragment_infos,
568 node_actors,
569 },
570 };
571 let control = entry.insert(IndependentCheckpointJobControl::batch_refresh(
572 job_id,
573 partial_graph_id,
574 IndependentCheckpointJobStatus::Initial { snapshot_epoch },
575 this,
576 ));
577 let Some(IndependentCheckpointJob::BatchRefresh(job)) = control.running_mut() else {
578 unreachable!()
579 };
580 Ok(job)
581 }
582
583 pub(crate) fn recover(
588 control: IndependentJobControl,
589 backfill_order: ExtendedFragmentBackfillOrder,
590 version_stat: &HummockVersionStats,
591 initial_mutation: Mutation,
592 render_result: BatchRefreshRenderResult,
593 term_id: &str,
594 partial_graph_recoverer: &mut crate::barrier::partial_graph::PartialGraphRecoverer<'_>,
595 batch_refresh_seconds: u64,
596 ) -> MetaResult<Self> {
597 let job_id = control.info.job_id;
598 let partial_graph_id = control.info.partial_graph_id;
599 let snapshot_epoch = control.info.snapshot_epoch;
600 let committed_epoch = control
601 .max_committed_epoch
602 .expect("recovered independent job should have committed");
603
604 if committed_epoch >= snapshot_epoch {
605 info!(
607 %job_id,
608 committed_epoch,
609 snapshot_epoch,
610 "recovered idle batch refresh job (no partial graph)"
611 );
612 return Ok(Self {
613 control,
614 batch_refresh_seconds,
615 status: BatchRefreshJobStatus::Idle,
616 });
617 }
618
619 info!(
621 %job_id,
622 committed_epoch,
623 snapshot_epoch,
624 "recovered batch refresh job to consuming snapshot"
625 );
626
627 let (snapshot, first_barrier_info) = SnapshotPhaseControl::for_recovery(
628 job_id,
629 snapshot_epoch,
630 committed_epoch,
631 &render_result.fragment_infos,
632 &backfill_order,
633 version_stat,
634 );
635
636 partial_graph_recoverer.recover_graph(
637 partial_graph_id,
638 term_id,
639 initial_mutation,
640 &first_barrier_info,
641 &render_result.node_actors,
642 control.info.state_table_ids.iter().copied(),
643 render_result.actors_to_create,
644 BatchRefreshBarrierStats::new(job_id, snapshot_epoch),
645 )?;
646
647 Ok(Self {
648 control,
649 batch_refresh_seconds,
650 status: BatchRefreshJobStatus::ConsumingSnapshot {
651 snapshot,
652 fragment_infos: render_result.fragment_infos,
653 node_actors: render_result.node_actors,
654 },
655 })
656 }
657}
658
659impl BatchRefreshJobCheckpointControl {
662 fn inject_barrier(
663 partial_graph_id: PartialGraphId,
664 partial_graph_manager: &mut PartialGraphManager,
665 node_actors: &HashMap<WorkerId, HashSet<ActorId>>,
666 state_table_ids: &HashSet<TableId>,
667 barrier_info: BarrierInfo,
668 new_actors: Option<StreamJobActorsToCreate>,
669 mutation: Option<Mutation>,
670 notifier: Option<&mut NotifierStarter>,
671 first_create_info: Option<CreateIndependentStreamingJobCommandInfo>,
672 is_stop: bool,
673 ) -> MetaResult<()> {
674 if is_stop {
675 assert!(
676 matches!(&mutation, Some(Mutation::Stop(_))),
677 "stop barrier must carry a Stop mutation"
678 );
679 }
680 partial_graph_manager.inject_barrier(
681 partial_graph_id,
682 mutation,
683 node_actors,
684 state_table_ids.iter().copied(),
685 if is_stop {
686 itertools::Either::Left(std::iter::empty())
688 } else {
689 itertools::Either::Right(node_actors.keys().copied())
690 },
691 new_actors,
692 PartialGraphBarrierInfo::new(
693 first_create_info.map_or_else(
694 PostCollectCommand::barrier,
695 CreateIndependentStreamingJobCommandInfo::into_post_collect,
696 ),
697 barrier_info,
698 notifier,
699 state_table_ids.clone(),
700 ),
701 )?;
702 Ok(())
703 }
704}
705
706impl BatchRefreshJobCheckpointControl {
709 pub(crate) fn on_new_upstream_barrier(
710 &mut self,
711 partial_graph_manager: &mut PartialGraphManager,
712 barrier_info: &BarrierInfo,
713 mutation: Option<(Mutation, Option<&mut NotifierStarter>)>,
714 ) -> MetaResult<()> {
715 if !matches!(self.status, BatchRefreshJobStatus::ConsumingSnapshot { .. }) {
716 return Ok(());
719 }
720 let (mutation, notifier) = match mutation {
721 Some((mutation, notifier)) => (Some(mutation), notifier),
722 None => (None, None),
723 };
724 let is_finished = matches!(
726 &self.status,
727 BatchRefreshJobStatus::ConsumingSnapshot { snapshot, .. }
728 if snapshot.create_mview_tracker.is_finished()
729 );
730
731 if is_finished {
732 let old_status = replace(&mut self.status, BatchRefreshJobStatus::Idle);
735 let BatchRefreshJobStatus::ConsumingSnapshot {
736 mut snapshot,
737 fragment_infos,
738 node_actors,
739 } = old_status
740 else {
741 unreachable!()
742 };
743
744 let final_checkpoint = snapshot.finish_snapshot_barrier();
746 let tracking_job = snapshot.create_mview_tracker.into_tracking_job();
747
748 let stop_barrier = BarrierInfo {
750 prev_epoch: TracedEpoch::new(Epoch(snapshot.snapshot_epoch)),
751 curr_epoch: TracedEpoch::new(Epoch(u64::MAX)),
752 kind: BarrierKind::Checkpoint(vec![snapshot.snapshot_epoch]),
753 };
754
755 let stop_actors: Vec<ActorId> = fragment_infos
756 .values()
757 .flat_map(|f| f.actors.keys().copied())
758 .collect();
759
760 Self::inject_barrier(
761 self.control.info.partial_graph_id,
762 partial_graph_manager,
763 &node_actors,
764 &self.control.info.state_table_ids,
765 final_checkpoint,
766 None,
767 None,
768 notifier,
769 None,
770 false,
771 )?;
772 Self::inject_barrier(
773 self.control.info.partial_graph_id,
774 partial_graph_manager,
775 &node_actors,
776 &self.control.info.state_table_ids,
777 stop_barrier,
778 None,
779 Some(Mutation::Stop(StopMutation {
780 actors: stop_actors,
781 dropped_sink_fragments: vec![],
782 })),
783 None,
784 None,
785 true,
786 )?;
787
788 self.status = BatchRefreshJobStatus::FinishingSnapshot {
789 tracking_job: Some(tracking_job),
790 fragment_infos,
791 };
792 } else {
793 let BatchRefreshJobStatus::ConsumingSnapshot {
795 snapshot,
796 node_actors,
797 ..
798 } = &mut self.status
799 else {
800 unreachable!("is_finished was false, status must be ConsumingSnapshot")
801 };
802
803 let mutation = mutation.or_else(|| snapshot.take_start_backfill_mutation());
805 let barrier_to_inject = snapshot.next_fake_barrier(&barrier_info.kind);
806 Self::inject_barrier(
807 self.control.info.partial_graph_id,
808 partial_graph_manager,
809 node_actors,
810 &self.control.info.state_table_ids,
811 barrier_to_inject,
812 None,
813 mutation,
814 notifier,
815 None,
816 false,
817 )?;
818 }
819 Ok(())
820 }
821
822 pub(crate) fn collect(&mut self, collected_barrier: CollectedBarrier<'_>) -> bool {
823 match &mut self.status {
824 BatchRefreshJobStatus::ConsumingSnapshot { snapshot, .. } => snapshot.apply_progress(
825 collected_barrier
826 .resps
827 .values()
828 .flat_map(|resp| &resp.create_mview_progress),
829 ),
830 BatchRefreshJobStatus::InitializingBatchRefresh { .. }
831 | BatchRefreshJobStatus::ConsumingLogStore { .. } => {
832 false
834 }
835 _ => false,
836 }
837 }
838}
839
840impl BatchRefreshJobCheckpointControl {
843 #[expect(clippy::type_complexity)]
844 pub(crate) fn start_completing(
845 &mut self,
846 partial_graph_manager: &mut PartialGraphManager,
847 ) -> Option<(
848 u64,
849 HashMap<WorkerId, BarrierCompleteResponse>,
850 PartialGraphBarrierInfo,
851 Option<TrackingJob>,
852 )> {
853 match &self.status {
854 BatchRefreshJobStatus::ConsumingSnapshot { .. }
855 | BatchRefreshJobStatus::FinishingSnapshot { .. }
856 | BatchRefreshJobStatus::ConsumingLogStore { .. } => {}
857 BatchRefreshJobStatus::Idle
858 | BatchRefreshJobStatus::InitializingBatchRefresh { .. } => {
859 return None;
860 }
861 };
862
863 partial_graph_manager
864 .start_completing(
865 self.control.info.partial_graph_id,
866 std::ops::Bound::Unbounded,
867 |_non_checkpoint_epoch, _resps, _| {
868 },
870 )
871 .map(|(epoch, resps, info)| {
872 let tracking_job = match &mut self.status {
878 BatchRefreshJobStatus::FinishingSnapshot { tracking_job, .. }
879 if epoch == self.control.info.snapshot_epoch =>
880 {
881 Some(
882 tracking_job
883 .take()
884 .expect("tracking job should not have been taken yet"),
885 )
886 }
887 _ => None,
888 };
889 (epoch, resps, info, tracking_job)
890 })
891 }
892
893 pub(super) fn ack_completed(
894 &mut self,
895 partial_graph_manager: &mut PartialGraphManager,
896 completed_epoch: u64,
897 ) {
898 match &self.status {
899 BatchRefreshJobStatus::ConsumingSnapshot { .. } => {
900 partial_graph_manager
901 .ack_completed(self.control.info.partial_graph_id, completed_epoch);
902 self.control.ack_completed(completed_epoch);
903 }
904 BatchRefreshJobStatus::FinishingSnapshot { tracking_job, .. }
905 if completed_epoch == self.control.info.snapshot_epoch =>
906 {
907 partial_graph_manager
908 .ack_completed(self.control.info.partial_graph_id, completed_epoch);
909 self.control.ack_completed(completed_epoch);
910 assert!(
911 tracking_job.is_none(),
912 "tracking job should have been taken at start_completing"
913 );
914 info!(
915 job_id = %self.control.info.job_id,
916 completed_epoch,
917 "batch refresh job: snapshot done, transitioned to idle, removing partial graph"
918 );
919 partial_graph_manager
920 .remove_partial_graphs(vec![self.control.info.partial_graph_id]);
921 self.status = BatchRefreshJobStatus::Idle;
922 }
923 BatchRefreshJobStatus::FinishingSnapshot { .. } => {
924 partial_graph_manager
925 .ack_completed(self.control.info.partial_graph_id, completed_epoch);
926 self.control.ack_completed(completed_epoch);
927 }
928 BatchRefreshJobStatus::ConsumingLogStore {
929 target_upstream_epoch,
930 ..
931 } if completed_epoch == *target_upstream_epoch => {
932 let target = *target_upstream_epoch;
933 partial_graph_manager
934 .ack_completed(self.control.info.partial_graph_id, completed_epoch);
935 self.control.ack_completed(completed_epoch);
936 info!(
937 job_id = %self.control.info.job_id,
938 completed_epoch,
939 target_upstream_epoch = target,
940 "batch refresh job: logstore done, transitioned to idle, removing partial graph"
941 );
942 partial_graph_manager
943 .remove_partial_graphs(vec![self.control.info.partial_graph_id]);
944 self.status = BatchRefreshJobStatus::Idle;
945 }
946 BatchRefreshJobStatus::ConsumingLogStore { .. } => {
947 partial_graph_manager
948 .ack_completed(self.control.info.partial_graph_id, completed_epoch);
949 self.control.ack_completed(completed_epoch);
950 }
951 BatchRefreshJobStatus::Idle
952 | BatchRefreshJobStatus::InitializingBatchRefresh { .. } => {
953 unreachable!("batch refresh job should not be completing in this state")
954 }
955 }
956 }
957}
958
959impl BatchRefreshJobCheckpointControl {
962 pub(crate) fn gen_backfill_progress(&self) -> Option<BackfillProgress> {
963 match &self.status {
964 BatchRefreshJobStatus::ConsumingSnapshot { snapshot, .. } => {
965 let progress = if snapshot.create_mview_tracker.is_finished() {
966 "Snapshot finished".to_owned()
967 } else {
968 let progress = snapshot.create_mview_tracker.gen_backfill_progress();
969 format!("BatchRefresh Snapshot [{}]", progress)
970 };
971 Some(BackfillProgress {
972 progress,
973 backfill_type: PbBackfillType::SnapshotBackfill,
974 })
975 }
976 BatchRefreshJobStatus::FinishingSnapshot { .. } => Some(BackfillProgress {
977 progress: "BatchRefresh Stopping".to_owned(),
978 backfill_type: PbBackfillType::SnapshotBackfill,
979 }),
980 BatchRefreshJobStatus::InitializingBatchRefresh { .. }
981 | BatchRefreshJobStatus::ConsumingLogStore { .. } => Some(BackfillProgress {
982 progress: "BatchRefresh LogStore".to_owned(),
983 backfill_type: PbBackfillType::SnapshotBackfill,
984 }),
985 BatchRefreshJobStatus::Idle => None,
986 }
987 }
988
989 pub(super) fn gen_fragment_backfill_progress(&self) -> Vec<FragmentBackfillProgress> {
990 match &self.status {
991 BatchRefreshJobStatus::ConsumingSnapshot {
992 snapshot,
993 fragment_infos,
994 ..
995 } => snapshot
996 .create_mview_tracker
997 .collect_fragment_progress(fragment_infos, true),
998 BatchRefreshJobStatus::FinishingSnapshot { fragment_infos, .. } => {
999 collect_done_fragments(self.control.info.job_id, fragment_infos)
1000 }
1001 _ => vec![],
1002 }
1003 }
1004
1005 pub(super) fn pinned_upstream_tables(&self) -> &HashSet<TableId> {
1006 match &self.status {
1007 BatchRefreshJobStatus::ConsumingSnapshot { .. }
1008 | BatchRefreshJobStatus::FinishingSnapshot { .. }
1009 | BatchRefreshJobStatus::ConsumingLogStore { .. }
1010 | BatchRefreshJobStatus::InitializingBatchRefresh { .. }
1011 | BatchRefreshJobStatus::Idle => &self.control.info.snapshot_backfill_upstream_tables,
1012 }
1013 }
1014
1015 pub(crate) fn fragment_infos(&self) -> Option<&HashMap<FragmentId, InflightFragmentInfo>> {
1016 match &self.status {
1017 BatchRefreshJobStatus::ConsumingSnapshot { fragment_infos, .. } => Some(fragment_infos),
1018 BatchRefreshJobStatus::InitializingBatchRefresh { fragment_infos, .. } => {
1019 Some(fragment_infos)
1020 }
1021 BatchRefreshJobStatus::ConsumingLogStore { fragment_infos, .. } => Some(fragment_infos),
1022 BatchRefreshJobStatus::FinishingSnapshot { .. } | BatchRefreshJobStatus::Idle => None,
1023 }
1024 }
1025
1026 pub(crate) fn pre_apply_throttle(
1027 &mut self,
1028 config: &mut ThrottleConfigMap,
1029 ) -> Option<Mutation> {
1030 let BatchRefreshJobStatus::ConsumingSnapshot {
1031 fragment_infos,
1032 snapshot,
1033 ..
1034 } = &mut self.status
1035 else {
1036 return None;
1037 };
1038 if snapshot.create_mview_tracker.is_finished() {
1039 return None;
1040 }
1041
1042 extract_throttle_config(config, |fragment_id, stream_node| {
1043 if let Some(fragment_info) = fragment_infos.get_mut(&fragment_id) {
1044 fragment_info.nodes = stream_node.clone();
1045 true
1046 } else {
1047 false
1048 }
1049 })
1050 }
1051
1052 pub(crate) fn should_start_refresh(&self, upstream_committed_epoch: u64) -> bool {
1057 if let BatchRefreshJobStatus::Idle = &self.status {
1058 let last_committed_epoch = self
1059 .control
1060 .max_committed_epoch
1061 .expect("batch refresh job should have committed when entering Idle");
1062 let job_physical_ms = Epoch(last_committed_epoch).physical_time();
1063 let upstream_physical_ms = Epoch(upstream_committed_epoch).physical_time();
1064 let threshold_ms = self.batch_refresh_seconds * 1000;
1065 upstream_physical_ms.saturating_sub(job_physical_ms) >= threshold_ms
1066 } else {
1067 false
1068 }
1069 }
1070
1071 pub(crate) fn last_committed_epoch(&self) -> Option<u64> {
1073 if let BatchRefreshJobStatus::Idle = &self.status {
1074 self.control.max_committed_epoch
1075 } else {
1076 None
1077 }
1078 }
1079}
1080
1081impl BatchRefreshJobCheckpointControl {
1084 pub(crate) fn start_refresh_run(
1096 &mut self,
1097 context: &BatchRefreshJobTriggerContext,
1098 worker_nodes: &HashMap<WorkerId, WorkerNode>,
1099 actor_id_counter: &AtomicU32,
1100 term_id: &str,
1101 partial_graph_manager: &mut PartialGraphManager,
1102 ) -> MetaResult<bool> {
1103 let last_committed_epoch = match &self.status {
1104 BatchRefreshJobStatus::Idle => self
1105 .control
1106 .max_committed_epoch
1107 .expect("batch refresh job should have committed when entering Idle"),
1108 _ => panic!(
1109 "batch refresh job {}: start_refresh_run called in non-Idle state {:?}",
1110 self.control.info.job_id, self.status
1111 ),
1112 };
1113
1114 let target_upstream_epoch = context.target_upstream_epoch;
1116 let Some((first_epoch, pending_log_barriers)) = Self::resolve_log_epoch_barriers(
1117 &self.control.info.snapshot_backfill_upstream_tables,
1118 &context.upstream_table_log_epochs,
1119 last_committed_epoch,
1120 )?
1121 else {
1122 info!(
1123 job_id = %self.control.info.job_id,
1124 last_committed_epoch,
1125 target_upstream_epoch,
1126 "batch refresh job: no log epochs to consume, staying idle"
1127 );
1128 return Ok(false);
1129 };
1130
1131 let log_target_epoch = pending_log_barriers.last().expect("non-empty").prev_epoch();
1132 if target_upstream_epoch != log_target_epoch {
1133 info!(
1134 job_id = %self.control.info.job_id,
1135 last_committed_epoch,
1136 target_upstream_epoch,
1137 log_target_epoch,
1138 "batch refresh job: upstream target has no resolved changelog yet, staying idle"
1139 );
1140 return Ok(false);
1141 }
1142
1143 let logical = BatchRefreshLogicalFragments::from_context(context);
1145
1146 let render_result = Self::render_actors_and_build_job_info(
1148 &logical.fragments,
1149 &logical.downstreams,
1150 &context.definition,
1151 actor_id_counter,
1152 worker_nodes,
1153 partial_graph_manager.control_stream_manager(),
1154 &context.database_resource_group,
1155 &context.streaming_job_model,
1156 self.control.info.partial_graph_id,
1157 )?;
1158
1159 let added_actors: Vec<ActorId> = render_result
1161 .fragment_infos
1162 .values()
1163 .flat_map(|fragment| fragment.actors.keys().copied())
1164 .collect();
1165
1166 let initial_mutation = Mutation::Add(AddMutation {
1167 actor_dispatchers: Default::default(),
1168 added_actors,
1169 actor_splits: Default::default(),
1170 pause: false,
1171 subscriptions_to_add: Default::default(),
1172 backfill_nodes_to_pause: Default::default(),
1173 actor_cdc_table_snapshot_splits: None,
1174 new_upstream_sinks: Default::default(),
1175 dropped_actors: Default::default(),
1176 sink_log_store_flush: Default::default(),
1177 });
1178
1179 let node_actors = &render_result.node_actors;
1180 let initial_barrier = BarrierInfo {
1181 prev_epoch: TracedEpoch::new(Epoch(last_committed_epoch)),
1182 curr_epoch: TracedEpoch::new(Epoch(first_epoch)),
1183 kind: BarrierKind::Initial,
1184 };
1185 let mut partial_graph_recoverer = partial_graph_manager.start_recover();
1186 let recover_result = partial_graph_recoverer.recover_graph(
1187 self.control.info.partial_graph_id,
1188 term_id,
1189 initial_mutation,
1190 &initial_barrier,
1191 node_actors,
1192 self.control.info.state_table_ids.iter().copied(),
1193 render_result.actors_to_create,
1194 BatchRefreshBarrierStats::new(
1195 self.control.info.job_id,
1196 self.control.info.snapshot_epoch,
1197 ),
1198 );
1199 match recover_result {
1200 Ok(()) => {
1201 let initializing_partial_graphs = partial_graph_recoverer.all_initializing();
1202 debug_assert_eq!(initializing_partial_graphs.len(), 1);
1203 debug_assert!(
1204 initializing_partial_graphs.contains(&self.control.info.partial_graph_id)
1205 );
1206 }
1207 Err(e) => {
1208 partial_graph_recoverer.failed();
1209 return Err(e);
1210 }
1211 }
1212
1213 info!(
1214 job_id = %self.control.info.job_id,
1215 last_committed_epoch,
1216 target_upstream_epoch,
1217 num_log_barriers = pending_log_barriers.len(),
1218 "batch refresh job: initialized logstore consumption partial graph"
1219 );
1220
1221 self.status = BatchRefreshJobStatus::InitializingBatchRefresh {
1222 fragment_infos: render_result.fragment_infos,
1223 node_actors: render_result.node_actors,
1224 pending_log_barriers,
1225 target_upstream_epoch,
1226 };
1227
1228 Ok(true)
1229 }
1230
1231 pub(crate) fn on_log_store_initialized(
1232 &mut self,
1233 partial_graph_manager: &mut PartialGraphManager,
1234 ) -> MetaResult<()> {
1235 let old_status = replace(&mut self.status, BatchRefreshJobStatus::Idle);
1236 let BatchRefreshJobStatus::InitializingBatchRefresh {
1237 fragment_infos,
1238 node_actors,
1239 pending_log_barriers,
1240 target_upstream_epoch,
1241 } = old_status
1242 else {
1243 panic!(
1244 "batch refresh job {}: logstore initialized in unexpected status {:?}",
1245 self.control.info.job_id, old_status
1246 );
1247 };
1248
1249 let final_barrier_idx = pending_log_barriers.len() - 1;
1250 let mut stop_mutation = Some(Mutation::Stop(StopMutation {
1251 actors: fragment_infos
1252 .values()
1253 .flat_map(|fragment| fragment.actors.keys().copied())
1254 .collect(),
1255 dropped_sink_fragments: vec![],
1256 }));
1257 for (idx, barrier) in pending_log_barriers.into_iter().enumerate() {
1258 let is_stop_barrier = idx == final_barrier_idx;
1259 let mutation = is_stop_barrier.then(|| stop_mutation.take().expect("unused"));
1260 Self::inject_barrier(
1261 self.control.info.partial_graph_id,
1262 partial_graph_manager,
1263 &node_actors,
1264 &self.control.info.state_table_ids,
1265 barrier,
1266 None,
1267 mutation,
1268 None,
1269 None,
1270 is_stop_barrier,
1271 )?;
1272 }
1273
1274 self.status = BatchRefreshJobStatus::ConsumingLogStore {
1275 fragment_infos,
1276 target_upstream_epoch,
1277 };
1278 Ok(())
1279 }
1280
1281 fn resolve_log_epoch_barriers(
1287 snapshot_backfill_upstream_tables: &HashSet<TableId>,
1288 upstream_table_log_epochs: &HashMap<TableId, Vec<(Vec<u64>, u64)>>,
1289 exclusive_start_log_epoch: u64,
1290 ) -> MetaResult<Option<(u64, Vec<BarrierInfo>)>> {
1291 let table_id = snapshot_backfill_upstream_tables
1292 .iter()
1293 .next()
1294 .expect("snapshot backfill job should have upstream");
1295 let Some(epochs) = upstream_table_log_epochs.get(table_id) else {
1296 return Ok(None);
1297 };
1298
1299 let mut epochs_iter = epochs.iter().peekable();
1301 loop {
1302 match epochs_iter.peek() {
1303 Some((_, checkpoint_epoch)) if *checkpoint_epoch <= exclusive_start_log_epoch => {
1304 epochs_iter.next();
1305 }
1306 _ => break,
1307 }
1308 }
1309
1310 let mut epoch_infos = vec![];
1311 for (non_checkpoint_epochs, checkpoint_epoch) in epochs_iter {
1312 epoch_infos.extend(
1313 non_checkpoint_epochs
1314 .iter()
1315 .copied()
1316 .map(|epoch| (epoch, false)),
1317 );
1318 epoch_infos.push((*checkpoint_epoch, true));
1319 }
1320 if epoch_infos.is_empty() {
1321 return Ok(None);
1322 }
1323
1324 let first_epoch = epoch_infos[0].0;
1325 let mut pending_non_checkpoint_epochs = vec![];
1326 let mut replay_barriers = vec![];
1327 for window in epoch_infos.windows(2) {
1328 let (prev_epoch, is_checkpoint) = window[0];
1329 let curr_epoch = window[1].0;
1330 assert!(prev_epoch > exclusive_start_log_epoch);
1331 assert!(curr_epoch > prev_epoch);
1332 pending_non_checkpoint_epochs.push(prev_epoch);
1333 let kind = if is_checkpoint {
1334 BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_epochs))
1335 } else {
1336 BarrierKind::Barrier
1337 };
1338 replay_barriers.push(BarrierInfo {
1339 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
1340 curr_epoch: TracedEpoch::new(Epoch(curr_epoch)),
1341 kind,
1342 });
1343 }
1344
1345 let (last_epoch, _) = *epoch_infos.last().expect("non-empty");
1346 assert!(last_epoch > exclusive_start_log_epoch);
1347 pending_non_checkpoint_epochs.push(last_epoch);
1348 replay_barriers.push(BarrierInfo {
1349 prev_epoch: TracedEpoch::new(Epoch(last_epoch)),
1350 curr_epoch: TracedEpoch::new(Epoch(u64::MAX)),
1351 kind: BarrierKind::Checkpoint(pending_non_checkpoint_epochs),
1352 });
1353
1354 Ok(Some((first_epoch, replay_barriers)))
1355 }
1356}
1357
1358impl BatchRefreshLogicalFragments {
1359 pub(crate) fn from_context(ctx: &BatchRefreshJobTriggerContext) -> Self {
1361 Self {
1362 fragments: ctx.fragments.clone(),
1363 downstreams: ctx.downstreams.clone(),
1364 }
1365 }
1366}
1367
1368struct BatchRefreshBarrierStats {
1371 barrier_latency: LabelGuardedHistogram,
1372 inflight_barrier_num: LabelGuardedIntGauge,
1373}
1374
1375impl BatchRefreshBarrierStats {
1376 fn new(job_id: JobId, _snapshot_epoch: u64) -> Self {
1377 let table_id_str = format!("{}", job_id);
1378 Self {
1379 barrier_latency: GLOBAL_META_METRICS
1380 .snapshot_backfill_barrier_latency
1381 .with_guarded_label_values(&[table_id_str.as_str(), "batch_refresh_snapshot"]),
1382 inflight_barrier_num: GLOBAL_META_METRICS
1383 .snapshot_backfill_inflight_barrier_num
1384 .with_guarded_label_values(&[&table_id_str]),
1385 }
1386 }
1387}
1388
1389impl PartialGraphStat for BatchRefreshBarrierStats {
1390 fn observe_barrier_latency(&self, _epoch: EpochPair, barrier_latency_secs: f64) {
1391 self.barrier_latency.observe(barrier_latency_secs);
1392 }
1393
1394 fn observe_barrier_num(&self, inflight_barrier_num: usize, _collected_barrier_num: usize) {
1395 self.inflight_barrier_num.set(inflight_barrier_num as _);
1396 }
1397}