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::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)]
125enum BatchRefreshJobStatus {
126 ConsumingSnapshot {
131 prev_epoch_fake_physical_time: u64,
132 version_stats: HummockVersionStats,
133 create_mview_tracker: CreateMviewProgressTracker,
134 snapshot_epoch: u64,
135 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
136 pending_non_checkpoint_barriers: Vec<u64>,
137 node_actors: HashMap<WorkerId, HashSet<ActorId>>,
138 state_table_ids: HashSet<TableId>,
139 },
140 FinishingSnapshot {
147 tracking_job: Option<TrackingJob>,
148 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
149 },
150 Idle { last_committed_epoch: u64 },
152 InitializingBatchRefresh {
155 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
156 node_actors: HashMap<WorkerId, HashSet<ActorId>>,
157 state_table_ids: HashSet<TableId>,
158 pending_log_barriers: Vec<BarrierInfo>,
161 target_upstream_epoch: u64,
162 },
163 ConsumingLogStore {
169 fragment_infos: HashMap<FragmentId, InflightFragmentInfo>,
170 target_upstream_epoch: u64,
172 },
173}
174
175#[derive(Debug)]
186pub(crate) struct BatchRefreshJobCheckpointControl {
187 job_id: JobId,
188 partial_graph_id: PartialGraphId,
189 snapshot_backfill_upstream_tables: HashSet<TableId>,
190 snapshot_epoch: u64,
191 batch_refresh_seconds: u64,
193
194 status: BatchRefreshJobStatus,
195}
196
197impl BatchRefreshJobCheckpointControl {
200 pub(crate) fn render_actors_and_build_job_info(
211 fragments: &HashMap<FragmentId, LoadedFragment>,
212 downstreams: &FragmentDownstreamRelation,
213 definition: &str,
214 actor_id_generator: &AtomicU32,
216 worker_nodes: &HashMap<WorkerId, WorkerNode>,
217 database_resource_group: &str,
218 streaming_job_model: &streaming_job::Model,
219 partial_graph_id: PartialGraphId,
221 ) -> MetaResult<BatchRefreshRenderResult> {
222 let ensembles = Self::resolve_ensembles(fragments, downstreams)?;
224
225 let mut actor_assignments: HashMap<
227 FragmentId,
228 HashMap<ActorId, (WorkerId, Option<risingwave_common::bitmap::Bitmap>)>,
229 > = HashMap::new();
230
231 for ensemble in &ensembles {
232 let first_component = ensemble
234 .component_fragments()
235 .next()
236 .expect("ensemble must have at least one component");
237 let fragment = &fragments[&first_component];
238 let distribution_type = fragment.distribution_type;
239 let vnode_count = fragment.vnode_count;
240
241 for fid in ensemble.component_fragments() {
243 let f = &fragments[&fid];
244 assert_eq!(
245 vnode_count, f.vnode_count,
246 "fragments {} and {} in same ensemble have different vnode counts",
247 first_component, fid,
248 );
249 }
250
251 let entry_fragment_parallelism = Itertools::exactly_one(
252 ensemble
253 .entry_fragments()
254 .map(|fid| fragments[&fid].parallelism.clone())
255 .dedup(),
256 )
257 .map_err(|_| {
258 anyhow!(
259 "entry fragments have inconsistent parallelism settings in batch refresh job"
260 )
261 })?;
262
263 let actor_template = EnsembleActorTemplate::render_new(
264 streaming_job_model,
265 worker_nodes,
266 entry_fragment_parallelism,
267 database_resource_group.to_owned(),
268 distribution_type,
269 vnode_count,
270 )?;
271
272 for fid in ensemble.component_fragments() {
273 let f = &fragments[&fid];
274 let aligner =
275 ComponentFragmentAligner::new_persistent(&actor_template, actor_id_generator);
276 let assignments = aligner.align_component_actor(f.distribution_type);
277 actor_assignments.insert(fid, assignments);
278 }
279 }
280
281 let mut stream_actors: HashMap<FragmentId, Vec<StreamActor>> = HashMap::new();
283 let mut actor_location: HashMap<ActorId, WorkerId> = HashMap::new();
284
285 for (fragment_id, assignments) in &actor_assignments {
286 let mut actors = Vec::with_capacity(assignments.len());
287 for (&actor_id, (worker_id, vnode_bitmap)) in assignments {
288 actor_location.insert(actor_id, *worker_id);
289 let stream_context = streaming_job_model.stream_context();
290 actors.push(StreamActor {
291 actor_id,
292 fragment_id: *fragment_id,
293 vnode_bitmap: vnode_bitmap.clone(),
294 mview_definition: definition.to_owned(),
295 expr_context: Some(stream_context.to_expr_context()),
296 config_override: stream_context.config_override.clone(),
297 });
298 }
299 stream_actors.insert(*fragment_id, actors);
300 }
301
302 let fragment_infos: HashMap<FragmentId, InflightFragmentInfo> = fragments
304 .iter()
305 .map(|(fragment_id, loaded)| {
306 let actors = stream_actors
307 .get(fragment_id)
308 .into_iter()
309 .flatten()
310 .map(|actor| {
311 (
312 actor.actor_id,
313 crate::controller::fragment::InflightActorInfo {
314 worker_id: actor_location[&actor.actor_id],
315 vnode_bitmap: actor.vnode_bitmap.clone(),
316 splits: vec![], },
318 )
319 })
320 .collect();
321 (
322 *fragment_id,
323 InflightFragmentInfo {
324 fragment_id: *fragment_id,
325 distribution_type: loaded.distribution_type,
326 fragment_type_mask: loaded.fragment_type_mask,
327 vnode_count: loaded.vnode_count,
328 nodes: loaded.nodes.clone(),
329 actors,
330 state_table_ids: loaded.state_table_ids.clone(),
331 },
332 )
333 })
334 .collect();
335
336 let mut builder = FragmentEdgeBuilder::new(fragment_infos.values().map(|f| {
338 (
339 f.fragment_id,
340 EdgeBuilderFragmentInfo::from_inflight_with_worker_nodes(
341 f,
342 partial_graph_id,
343 worker_nodes,
344 ),
345 )
346 }));
347 builder.add_relations(downstreams);
348 let mut edges = builder.build();
349
350 let actors_to_create = edges.collect_actors_to_create(fragment_infos.values().map(|f| {
351 (
352 f.fragment_id,
353 &f.nodes,
354 f.actors.iter().map(|(actor_id, actor)| {
355 let sa = stream_actors[&f.fragment_id]
356 .iter()
357 .find(|a| a.actor_id == *actor_id)
358 .expect("should exist");
359 (sa, actor.worker_id)
360 }),
361 vec![], )
363 }));
364
365 let node_actors = InflightFragmentInfo::actor_ids_to_collect(fragment_infos.values());
367 let state_table_ids =
368 InflightFragmentInfo::existing_table_ids(fragment_infos.values()).collect();
369
370 Ok(BatchRefreshRenderResult {
371 fragment_infos,
372 node_actors,
373 state_table_ids,
374 actors_to_create,
375 })
376 }
377
378 pub(crate) fn build_initial_partial_graph_mutation(
383 render_result: &BatchRefreshRenderResult,
384 backfill_ordering: &ExtendedFragmentBackfillOrder,
385 ) -> Mutation {
386 let added_actors: Vec<ActorId> = render_result
387 .fragment_infos
388 .values()
389 .flat_map(|f| f.actors.keys().copied())
390 .collect();
391 let backfill_nodes_to_pause = get_nodes_with_backfill_dependencies(backfill_ordering)
392 .into_iter()
393 .collect();
394 Mutation::Add(AddMutation {
395 actor_dispatchers: Default::default(),
396 added_actors,
397 actor_splits: Default::default(),
398 pause: false,
399 subscriptions_to_add: Default::default(),
400 backfill_nodes_to_pause,
401 actor_cdc_table_snapshot_splits: None,
402 new_upstream_sinks: Default::default(),
403 dropped_actors: Default::default(),
404 sink_log_store_flush: Default::default(),
405 })
406 }
407
408 fn resolve_ensembles(
410 fragments: &HashMap<FragmentId, LoadedFragment>,
411 downstreams: &FragmentDownstreamRelation,
412 ) -> MetaResult<Vec<NoShuffleEnsemble>> {
413 let mut new_no_shuffle: HashMap<_, HashSet<_>> = HashMap::new();
414 for (upstream_fid, relations) in downstreams {
415 for rel in relations {
416 if rel.dispatcher_type == DispatcherType::NoShuffle {
417 new_no_shuffle
418 .entry(*upstream_fid)
419 .or_default()
420 .insert(rel.downstream_fragment_id);
421 }
422 }
423 }
424
425 let mut ensembles = if new_no_shuffle.is_empty() {
426 Vec::new()
427 } else {
428 let no_shuffle_edges: Vec<(FragmentId, FragmentId)> = new_no_shuffle
429 .iter()
430 .flat_map(|(u, ds)| ds.iter().map(move |d| (*u, *d)))
431 .collect();
432 let all_fragment_ids: Vec<FragmentId> = no_shuffle_edges
433 .iter()
434 .flat_map(|(u, d)| [*u, *d])
435 .collect::<HashSet<_>>()
436 .into_iter()
437 .collect();
438 let (fwd, bwd) = build_no_shuffle_fragment_graph_edges(no_shuffle_edges);
439 find_no_shuffle_graphs(&all_fragment_ids, &fwd, &bwd)?
440 };
441
442 let covered: HashSet<FragmentId> = ensembles
444 .iter()
445 .flat_map(|e| e.component_fragments())
446 .collect();
447 for fragment_id in fragments.keys() {
448 if !covered.contains(fragment_id) {
449 ensembles.push(NoShuffleEnsemble::singleton(*fragment_id));
450 }
451 }
452
453 Ok(ensembles)
454 }
455}
456
457impl BatchRefreshJobCheckpointControl {
460 #[expect(clippy::too_many_arguments)]
465 pub(crate) fn new(
466 database_id: DatabaseId,
467 job_id: JobId,
468 create_info: CreateSnapshotBackfillJobCommandInfo,
469 notifier: Option<&mut NotifierStarter>,
470 snapshot_backfill_upstream_tables: HashSet<TableId>,
471 snapshot_epoch: u64,
472 version_stat: &HummockVersionStats,
473 term_id: &str,
474 partial_graph_manager: &mut PartialGraphManager,
475 logical: &BatchRefreshLogicalFragments,
476 worker_nodes: &HashMap<WorkerId, WorkerNode>,
477 batch_refresh_seconds: u64,
478 ) -> MetaResult<Self> {
479 debug!(
480 %job_id,
481 "new batch refresh job"
482 );
483
484 let partial_graph_id = to_partial_graph_id(database_id, Some(job_id));
485 let backfill_ordering = &create_info.info.fragment_backfill_ordering;
486 let actor_id_generator = partial_graph_manager
487 .control_stream_manager()
488 .env
489 .actor_id_generator();
490
491 let render_result = Self::render_actors_and_build_job_info(
492 &logical.fragments,
493 &logical.downstreams,
494 &create_info.info.definition,
495 actor_id_generator,
496 worker_nodes,
497 &create_info.info.database_resource_group,
498 &create_info.info.streaming_job_model,
499 partial_graph_id,
500 )?;
501 let initial_partial_graph_mutation =
502 Self::build_initial_partial_graph_mutation(&render_result, backfill_ordering);
503
504 let backfill_order_state = BackfillOrderState::new(
505 backfill_ordering,
506 &render_result.fragment_infos,
507 create_info
508 .info
509 .locality_fragment_state_table_mapping
510 .clone(),
511 );
512 let create_mview_tracker = CreateMviewProgressTracker::recover(
513 job_id,
514 &render_result.fragment_infos,
515 backfill_order_state,
516 version_stat,
517 );
518
519 let mut prev_epoch_fake_physical_time = 0;
520 let mut pending_non_checkpoint_barriers = vec![];
521
522 let initial_barrier_info = super::new_fake_barrier(
523 &mut prev_epoch_fake_physical_time,
524 &mut pending_non_checkpoint_barriers,
525 PbBarrierKind::Checkpoint,
526 );
527
528 let mut graph_adder = partial_graph_manager.add_partial_graph(
529 partial_graph_id,
530 term_id,
531 BatchRefreshBarrierStats::new(job_id, snapshot_epoch),
532 );
533
534 if let Err(e) = Self::inject_barrier(
535 partial_graph_id,
536 graph_adder.manager(),
537 &render_result.node_actors,
538 &render_result.state_table_ids,
539 initial_barrier_info,
540 Some(render_result.actors_to_create),
541 Some(initial_partial_graph_mutation),
542 notifier,
543 Some(create_info),
544 false,
545 ) {
546 graph_adder.failed();
547 return Err(e);
548 }
549
550 graph_adder.added();
551 assert!(pending_non_checkpoint_barriers.is_empty());
552 let this = Self {
553 partial_graph_id,
554 job_id,
555 snapshot_backfill_upstream_tables,
556 snapshot_epoch,
557 batch_refresh_seconds,
558
559 status: BatchRefreshJobStatus::ConsumingSnapshot {
560 prev_epoch_fake_physical_time,
561 version_stats: version_stat.clone(),
562 create_mview_tracker,
563 snapshot_epoch,
564 fragment_infos: render_result.fragment_infos,
565 pending_non_checkpoint_barriers,
566 node_actors: render_result.node_actors,
567 state_table_ids: render_result.state_table_ids,
568 },
569 };
570 Ok(this)
571 }
572
573 #[expect(clippy::too_many_arguments)]
578 pub(crate) fn recover(
579 database_id: DatabaseId,
580 job_id: JobId,
581 snapshot_backfill_upstream_tables: HashSet<TableId>,
582 snapshot_epoch: u64,
583 committed_epoch: u64,
584 backfill_order: ExtendedFragmentBackfillOrder,
585 version_stat: &HummockVersionStats,
586 initial_mutation: Mutation,
587 render_result: BatchRefreshRenderResult,
588 term_id: &str,
589 partial_graph_recoverer: &mut crate::barrier::partial_graph::PartialGraphRecoverer<'_>,
590 batch_refresh_seconds: u64,
591 ) -> MetaResult<Self> {
592 let partial_graph_id = to_partial_graph_id(database_id, Some(job_id));
593
594 if committed_epoch >= snapshot_epoch {
595 info!(
597 %job_id,
598 committed_epoch,
599 snapshot_epoch,
600 "recovered idle batch refresh job (no partial graph)"
601 );
602 return Ok(Self {
603 job_id,
604 partial_graph_id,
605 snapshot_backfill_upstream_tables,
606 snapshot_epoch,
607 batch_refresh_seconds,
608
609 status: BatchRefreshJobStatus::Idle {
610 last_committed_epoch: committed_epoch,
611 },
612 });
613 }
614
615 info!(
617 %job_id,
618 committed_epoch,
619 snapshot_epoch,
620 "recovered batch refresh job to consuming snapshot"
621 );
622
623 let mut prev_epoch_fake_physical_time = Epoch(committed_epoch).physical_time();
624 let mut pending_non_checkpoint_barriers = vec![];
625
626 let locality_fragment_state_table_mapping =
627 crate::barrier::rpc::build_locality_fragment_state_table_mapping(
628 &render_result.fragment_infos,
629 );
630 let backfill_order_state = BackfillOrderState::recover_from_fragment_infos(
631 &backfill_order,
632 &render_result.fragment_infos,
633 locality_fragment_state_table_mapping,
634 );
635
636 let create_mview_tracker = CreateMviewProgressTracker::recover(
637 job_id,
638 &render_result.fragment_infos,
639 backfill_order_state,
640 version_stat,
641 );
642 let first_barrier_info = super::new_fake_barrier(
643 &mut prev_epoch_fake_physical_time,
644 &mut pending_non_checkpoint_barriers,
645 PbBarrierKind::Initial,
646 );
647
648 partial_graph_recoverer.recover_graph(
649 partial_graph_id,
650 term_id,
651 initial_mutation,
652 &first_barrier_info,
653 &render_result.node_actors,
654 render_result.state_table_ids.iter().copied(),
655 render_result.actors_to_create,
656 BatchRefreshBarrierStats::new(job_id, snapshot_epoch),
657 )?;
658
659 Ok(Self {
660 job_id,
661 partial_graph_id,
662 snapshot_backfill_upstream_tables,
663 snapshot_epoch,
664 batch_refresh_seconds,
665 status: BatchRefreshJobStatus::ConsumingSnapshot {
666 prev_epoch_fake_physical_time,
667 version_stats: version_stat.clone(),
668 create_mview_tracker,
669 fragment_infos: render_result.fragment_infos,
670 snapshot_epoch,
671 pending_non_checkpoint_barriers,
672 node_actors: render_result.node_actors,
673 state_table_ids: render_result.state_table_ids,
674 },
675 })
676 }
677}
678
679impl BatchRefreshJobCheckpointControl {
682 fn inject_barrier(
683 partial_graph_id: PartialGraphId,
684 partial_graph_manager: &mut PartialGraphManager,
685 node_actors: &HashMap<WorkerId, HashSet<ActorId>>,
686 state_table_ids: &HashSet<TableId>,
687 barrier_info: BarrierInfo,
688 new_actors: Option<StreamJobActorsToCreate>,
689 mutation: Option<Mutation>,
690 notifier: Option<&mut NotifierStarter>,
691 first_create_info: Option<CreateSnapshotBackfillJobCommandInfo>,
692 is_stop: bool,
693 ) -> MetaResult<()> {
694 if is_stop {
695 assert!(
696 matches!(&mutation, Some(Mutation::Stop(_))),
697 "stop barrier must carry a Stop mutation"
698 );
699 }
700 partial_graph_manager.inject_barrier(
701 partial_graph_id,
702 mutation,
703 node_actors,
704 state_table_ids.iter().copied(),
705 if is_stop {
706 itertools::Either::Left(std::iter::empty())
708 } else {
709 itertools::Either::Right(node_actors.keys().copied())
710 },
711 new_actors,
712 PartialGraphBarrierInfo::new(
713 first_create_info.map_or_else(
714 PostCollectCommand::barrier,
715 CreateSnapshotBackfillJobCommandInfo::into_post_collect,
716 ),
717 barrier_info,
718 notifier,
719 state_table_ids.clone(),
720 ),
721 )?;
722 Ok(())
723 }
724}
725
726impl BatchRefreshJobCheckpointControl {
729 pub(crate) fn on_new_upstream_barrier(
730 &mut self,
731 partial_graph_manager: &mut PartialGraphManager,
732 barrier_info: &BarrierInfo,
733 mutation: Option<(Mutation, Option<&mut NotifierStarter>)>,
734 ) -> MetaResult<()> {
735 if !matches!(self.status, BatchRefreshJobStatus::ConsumingSnapshot { .. }) {
736 return Ok(());
739 }
740 let (mutation, notifier) = match mutation {
741 Some((mutation, notifier)) => (Some(mutation), notifier),
742 None => (None, None),
743 };
744 let is_finished = matches!(
746 &self.status,
747 BatchRefreshJobStatus::ConsumingSnapshot { create_mview_tracker, .. }
748 if create_mview_tracker.is_finished()
749 );
750
751 if is_finished {
752 let old_status = replace(
755 &mut self.status,
756 BatchRefreshJobStatus::Idle {
757 last_committed_epoch: 0,
758 },
759 );
760 let BatchRefreshJobStatus::ConsumingSnapshot {
761 prev_epoch_fake_physical_time,
762 mut pending_non_checkpoint_barriers,
763 snapshot_epoch,
764 fragment_infos,
765 create_mview_tracker,
766 node_actors,
767 state_table_ids,
768 ..
769 } = old_status
770 else {
771 unreachable!()
772 };
773
774 let tracking_job = create_mview_tracker.into_tracking_job();
775
776 pending_non_checkpoint_barriers.push(snapshot_epoch);
778 let prev_epoch = Epoch::from_physical_time(prev_epoch_fake_physical_time);
779 let final_checkpoint = BarrierInfo {
780 curr_epoch: TracedEpoch::new(Epoch(snapshot_epoch)),
781 prev_epoch: TracedEpoch::new(prev_epoch),
782 kind: BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_barriers)),
783 };
784
785 let stop_barrier = BarrierInfo {
787 prev_epoch: TracedEpoch::new(Epoch(snapshot_epoch)),
788 curr_epoch: TracedEpoch::new(Epoch(u64::MAX)),
789 kind: BarrierKind::Checkpoint(vec![snapshot_epoch]),
790 };
791
792 let stop_actors: Vec<ActorId> = fragment_infos
793 .values()
794 .flat_map(|f| f.actors.keys().copied())
795 .collect();
796
797 Self::inject_barrier(
798 self.partial_graph_id,
799 partial_graph_manager,
800 &node_actors,
801 &state_table_ids,
802 final_checkpoint,
803 None,
804 None,
805 notifier,
806 None,
807 false,
808 )?;
809 Self::inject_barrier(
810 self.partial_graph_id,
811 partial_graph_manager,
812 &node_actors,
813 &state_table_ids,
814 stop_barrier,
815 None,
816 Some(Mutation::Stop(StopMutation {
817 actors: stop_actors,
818 dropped_sink_fragments: vec![],
819 })),
820 None,
821 None,
822 true,
823 )?;
824
825 self.status = BatchRefreshJobStatus::FinishingSnapshot {
826 tracking_job: Some(tracking_job),
827 fragment_infos,
828 };
829 } else {
830 let BatchRefreshJobStatus::ConsumingSnapshot {
832 prev_epoch_fake_physical_time,
833 pending_non_checkpoint_barriers,
834 create_mview_tracker,
835 node_actors,
836 state_table_ids,
837 ..
838 } = &mut self.status
839 else {
840 unreachable!("is_finished was false, status must be ConsumingSnapshot")
841 };
842
843 let mutation = mutation.or_else(|| {
845 let pending_backfill_nodes = create_mview_tracker
846 .take_pending_backfill_nodes()
847 .collect_vec();
848 if pending_backfill_nodes.is_empty() {
849 None
850 } else {
851 Some(Mutation::StartFragmentBackfill(
852 StartFragmentBackfillMutation {
853 fragment_ids: pending_backfill_nodes,
854 },
855 ))
856 }
857 });
858 let barrier_to_inject = super::new_fake_barrier(
859 prev_epoch_fake_physical_time,
860 pending_non_checkpoint_barriers,
861 match barrier_info.kind {
862 BarrierKind::Barrier => PbBarrierKind::Barrier,
863 BarrierKind::Checkpoint(_) => PbBarrierKind::Checkpoint,
864 BarrierKind::Initial => {
865 unreachable!("upstream new epoch should not be initial")
866 }
867 },
868 );
869 Self::inject_barrier(
870 self.partial_graph_id,
871 partial_graph_manager,
872 node_actors,
873 state_table_ids,
874 barrier_to_inject,
875 None,
876 mutation,
877 notifier,
878 None,
879 false,
880 )?;
881 }
882 Ok(())
883 }
884
885 pub(crate) fn collect(&mut self, collected_barrier: CollectedBarrier<'_>) -> bool {
886 match &mut self.status {
887 BatchRefreshJobStatus::ConsumingSnapshot {
888 create_mview_tracker,
889 version_stats,
890 ..
891 } => {
892 for progress in collected_barrier
893 .resps
894 .values()
895 .flat_map(|resp| &resp.create_mview_progress)
896 {
897 create_mview_tracker.apply_progress(progress, version_stats);
898 }
899 create_mview_tracker.is_finished()
900 }
901 BatchRefreshJobStatus::InitializingBatchRefresh { .. }
902 | BatchRefreshJobStatus::ConsumingLogStore { .. } => {
903 false
905 }
906 _ => false,
907 }
908 }
909}
910
911impl BatchRefreshJobCheckpointControl {
914 #[expect(clippy::type_complexity)]
915 pub(crate) fn start_completing(
916 &mut self,
917 partial_graph_manager: &mut PartialGraphManager,
918 ) -> Option<(
919 u64,
920 HashMap<WorkerId, BarrierCompleteResponse>,
921 PartialGraphBarrierInfo,
922 Option<TrackingJob>,
923 )> {
924 match &self.status {
925 BatchRefreshJobStatus::ConsumingSnapshot { .. }
926 | BatchRefreshJobStatus::FinishingSnapshot { .. }
927 | BatchRefreshJobStatus::ConsumingLogStore { .. } => {}
928 BatchRefreshJobStatus::Idle { .. }
929 | BatchRefreshJobStatus::InitializingBatchRefresh { .. } => {
930 return None;
931 }
932 };
933
934 partial_graph_manager
935 .start_completing(
936 self.partial_graph_id,
937 std::ops::Bound::Unbounded,
938 |_non_checkpoint_epoch, _resps, _| {
939 },
941 )
942 .map(|(epoch, resps, info)| {
943 let tracking_job = match &mut self.status {
949 BatchRefreshJobStatus::FinishingSnapshot { tracking_job, .. }
950 if epoch == self.snapshot_epoch =>
951 {
952 Some(
953 tracking_job
954 .take()
955 .expect("tracking job should not have been taken yet"),
956 )
957 }
958 _ => None,
959 };
960 (epoch, resps, info, tracking_job)
961 })
962 }
963
964 pub(super) fn ack_completed(
965 &mut self,
966 partial_graph_manager: &mut PartialGraphManager,
967 completed_epoch: u64,
968 ) {
969 match &self.status {
970 BatchRefreshJobStatus::ConsumingSnapshot { .. } => {
971 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
972 }
973 BatchRefreshJobStatus::FinishingSnapshot { tracking_job, .. }
974 if completed_epoch == self.snapshot_epoch =>
975 {
976 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
977 assert!(
978 tracking_job.is_none(),
979 "tracking job should have been taken at start_completing"
980 );
981 info!(
982 job_id = %self.job_id,
983 completed_epoch,
984 "batch refresh job: snapshot done, transitioned to idle, removing partial graph"
985 );
986 partial_graph_manager.remove_partial_graphs(vec![self.partial_graph_id]);
987 self.status = BatchRefreshJobStatus::Idle {
988 last_committed_epoch: completed_epoch,
989 };
990 }
991 BatchRefreshJobStatus::FinishingSnapshot { .. } => {
992 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
993 }
994 BatchRefreshJobStatus::ConsumingLogStore {
995 target_upstream_epoch,
996 ..
997 } if completed_epoch == *target_upstream_epoch => {
998 let target = *target_upstream_epoch;
999 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
1000 info!(
1001 job_id = %self.job_id,
1002 completed_epoch,
1003 target_upstream_epoch = target,
1004 "batch refresh job: logstore done, transitioned to idle, removing partial graph"
1005 );
1006 partial_graph_manager.remove_partial_graphs(vec![self.partial_graph_id]);
1007 self.status = BatchRefreshJobStatus::Idle {
1008 last_committed_epoch: target,
1009 };
1010 }
1011 BatchRefreshJobStatus::ConsumingLogStore { .. } => {
1012 partial_graph_manager.ack_completed(self.partial_graph_id, completed_epoch);
1013 }
1014 BatchRefreshJobStatus::Idle { .. }
1015 | BatchRefreshJobStatus::InitializingBatchRefresh { .. } => {
1016 unreachable!("batch refresh job should not be completing in this state")
1017 }
1018 }
1019 }
1020}
1021
1022impl BatchRefreshJobCheckpointControl {
1025 pub(crate) fn gen_backfill_progress(&self) -> Option<BackfillProgress> {
1026 match &self.status {
1027 BatchRefreshJobStatus::ConsumingSnapshot {
1028 create_mview_tracker,
1029 ..
1030 } => {
1031 let progress = if create_mview_tracker.is_finished() {
1032 "Snapshot finished".to_owned()
1033 } else {
1034 let progress = create_mview_tracker.gen_backfill_progress();
1035 format!("BatchRefresh Snapshot [{}]", progress)
1036 };
1037 Some(BackfillProgress {
1038 progress,
1039 backfill_type: PbBackfillType::SnapshotBackfill,
1040 })
1041 }
1042 BatchRefreshJobStatus::FinishingSnapshot { .. } => Some(BackfillProgress {
1043 progress: "BatchRefresh Stopping".to_owned(),
1044 backfill_type: PbBackfillType::SnapshotBackfill,
1045 }),
1046 BatchRefreshJobStatus::InitializingBatchRefresh { .. }
1047 | BatchRefreshJobStatus::ConsumingLogStore { .. } => Some(BackfillProgress {
1048 progress: "BatchRefresh LogStore".to_owned(),
1049 backfill_type: PbBackfillType::SnapshotBackfill,
1050 }),
1051 BatchRefreshJobStatus::Idle { .. } => None,
1052 }
1053 }
1054
1055 pub(super) fn gen_fragment_backfill_progress(&self) -> Vec<FragmentBackfillProgress> {
1056 match &self.status {
1057 BatchRefreshJobStatus::ConsumingSnapshot {
1058 create_mview_tracker,
1059 fragment_infos,
1060 ..
1061 } => create_mview_tracker.collect_fragment_progress(fragment_infos, true),
1062 BatchRefreshJobStatus::FinishingSnapshot { fragment_infos, .. } => {
1063 collect_done_fragments(self.job_id, fragment_infos)
1064 }
1065 _ => vec![],
1066 }
1067 }
1068
1069 pub(super) fn pinned_upstream_tables(&self) -> &HashSet<TableId> {
1070 match &self.status {
1071 BatchRefreshJobStatus::ConsumingSnapshot { .. }
1072 | BatchRefreshJobStatus::FinishingSnapshot { .. }
1073 | BatchRefreshJobStatus::ConsumingLogStore { .. }
1074 | BatchRefreshJobStatus::InitializingBatchRefresh { .. }
1075 | BatchRefreshJobStatus::Idle { .. } => &self.snapshot_backfill_upstream_tables,
1076 }
1077 }
1078
1079 pub(crate) fn fragment_infos(&self) -> Option<&HashMap<FragmentId, InflightFragmentInfo>> {
1080 match &self.status {
1081 BatchRefreshJobStatus::ConsumingSnapshot { fragment_infos, .. } => Some(fragment_infos),
1082 BatchRefreshJobStatus::InitializingBatchRefresh { fragment_infos, .. } => {
1083 Some(fragment_infos)
1084 }
1085 BatchRefreshJobStatus::ConsumingLogStore { fragment_infos, .. } => Some(fragment_infos),
1086 BatchRefreshJobStatus::FinishingSnapshot { .. }
1087 | BatchRefreshJobStatus::Idle { .. } => None,
1088 }
1089 }
1090
1091 pub(crate) fn pre_apply_throttle(
1092 &mut self,
1093 config: &mut ThrottleConfigMap,
1094 ) -> Option<Mutation> {
1095 let BatchRefreshJobStatus::ConsumingSnapshot {
1096 fragment_infos,
1097 create_mview_tracker,
1098 ..
1099 } = &mut self.status
1100 else {
1101 return None;
1102 };
1103 if create_mview_tracker.is_finished() {
1104 return None;
1105 }
1106
1107 extract_throttle_config(config, |fragment_id, stream_node| {
1108 if let Some(fragment_info) = fragment_infos.get_mut(&fragment_id) {
1109 fragment_info.nodes = stream_node.clone();
1110 true
1111 } else {
1112 false
1113 }
1114 })
1115 }
1116
1117 pub(crate) fn should_start_refresh(&self, upstream_committed_epoch: u64) -> bool {
1122 if let BatchRefreshJobStatus::Idle {
1123 last_committed_epoch,
1124 } = &self.status
1125 {
1126 let job_physical_ms = Epoch(*last_committed_epoch).physical_time();
1127 let upstream_physical_ms = Epoch(upstream_committed_epoch).physical_time();
1128 let threshold_ms = self.batch_refresh_seconds * 1000;
1129 upstream_physical_ms.saturating_sub(job_physical_ms) >= threshold_ms
1130 } else {
1131 false
1132 }
1133 }
1134
1135 pub(crate) fn last_committed_epoch(&self) -> Option<u64> {
1137 if let BatchRefreshJobStatus::Idle {
1138 last_committed_epoch,
1139 } = &self.status
1140 {
1141 Some(*last_committed_epoch)
1142 } else {
1143 None
1144 }
1145 }
1146}
1147
1148impl BatchRefreshJobCheckpointControl {
1151 pub(crate) fn start_refresh_run(
1163 &mut self,
1164 context: &BatchRefreshJobTriggerContext,
1165 worker_nodes: &HashMap<WorkerId, WorkerNode>,
1166 actor_id_counter: &AtomicU32,
1167 term_id: &str,
1168 partial_graph_manager: &mut PartialGraphManager,
1169 ) -> MetaResult<bool> {
1170 let last_committed_epoch = match &self.status {
1171 BatchRefreshJobStatus::Idle {
1172 last_committed_epoch,
1173 } => *last_committed_epoch,
1174 _ => panic!(
1175 "batch refresh job {}: start_refresh_run called in non-Idle state {:?}",
1176 self.job_id, self.status
1177 ),
1178 };
1179
1180 let target_upstream_epoch = context.target_upstream_epoch;
1182 let Some((first_epoch, pending_log_barriers)) = Self::resolve_log_epoch_barriers(
1183 &self.snapshot_backfill_upstream_tables,
1184 &context.upstream_table_log_epochs,
1185 last_committed_epoch,
1186 )?
1187 else {
1188 info!(
1189 job_id = %self.job_id,
1190 last_committed_epoch,
1191 target_upstream_epoch,
1192 "batch refresh job: no log epochs to consume, staying idle"
1193 );
1194 return Ok(false);
1195 };
1196
1197 let log_target_epoch = pending_log_barriers.last().expect("non-empty").prev_epoch();
1198 if target_upstream_epoch != log_target_epoch {
1199 info!(
1200 job_id = %self.job_id,
1201 last_committed_epoch,
1202 target_upstream_epoch,
1203 log_target_epoch,
1204 "batch refresh job: upstream target has no resolved changelog yet, staying idle"
1205 );
1206 return Ok(false);
1207 }
1208
1209 let logical = BatchRefreshLogicalFragments::from_context(context);
1211
1212 let render_result = Self::render_actors_and_build_job_info(
1214 &logical.fragments,
1215 &logical.downstreams,
1216 &context.definition,
1217 actor_id_counter,
1218 worker_nodes,
1219 &context.database_resource_group,
1220 &context.streaming_job_model,
1221 self.partial_graph_id,
1222 )?;
1223
1224 let added_actors: Vec<ActorId> = render_result
1226 .fragment_infos
1227 .values()
1228 .flat_map(|fragment| fragment.actors.keys().copied())
1229 .collect();
1230
1231 let initial_mutation = Mutation::Add(AddMutation {
1232 actor_dispatchers: Default::default(),
1233 added_actors,
1234 actor_splits: Default::default(),
1235 pause: false,
1236 subscriptions_to_add: Default::default(),
1237 backfill_nodes_to_pause: Default::default(),
1238 actor_cdc_table_snapshot_splits: None,
1239 new_upstream_sinks: Default::default(),
1240 dropped_actors: Default::default(),
1241 sink_log_store_flush: Default::default(),
1242 });
1243
1244 let node_actors = &render_result.node_actors;
1245 let state_table_ids = &render_result.state_table_ids;
1246 let initial_barrier = BarrierInfo {
1247 prev_epoch: TracedEpoch::new(Epoch(last_committed_epoch)),
1248 curr_epoch: TracedEpoch::new(Epoch(first_epoch)),
1249 kind: BarrierKind::Initial,
1250 };
1251 let mut partial_graph_recoverer = partial_graph_manager.start_recover();
1252 let recover_result = partial_graph_recoverer.recover_graph(
1253 self.partial_graph_id,
1254 term_id,
1255 initial_mutation,
1256 &initial_barrier,
1257 node_actors,
1258 state_table_ids.iter().copied(),
1259 render_result.actors_to_create,
1260 BatchRefreshBarrierStats::new(self.job_id, self.snapshot_epoch),
1261 );
1262 match recover_result {
1263 Ok(()) => {
1264 let initializing_partial_graphs = partial_graph_recoverer.all_initializing();
1265 debug_assert_eq!(initializing_partial_graphs.len(), 1);
1266 debug_assert!(initializing_partial_graphs.contains(&self.partial_graph_id));
1267 }
1268 Err(e) => {
1269 partial_graph_recoverer.failed();
1270 return Err(e);
1271 }
1272 }
1273
1274 info!(
1275 job_id = %self.job_id,
1276 last_committed_epoch,
1277 target_upstream_epoch,
1278 num_log_barriers = pending_log_barriers.len(),
1279 "batch refresh job: initialized logstore consumption partial graph"
1280 );
1281
1282 self.status = BatchRefreshJobStatus::InitializingBatchRefresh {
1283 fragment_infos: render_result.fragment_infos,
1284 node_actors: render_result.node_actors,
1285 state_table_ids: render_result.state_table_ids,
1286 pending_log_barriers,
1287 target_upstream_epoch,
1288 };
1289
1290 Ok(true)
1291 }
1292
1293 pub(crate) fn on_log_store_initialized(
1294 &mut self,
1295 partial_graph_manager: &mut PartialGraphManager,
1296 ) -> MetaResult<()> {
1297 let old_status = replace(
1298 &mut self.status,
1299 BatchRefreshJobStatus::Idle {
1300 last_committed_epoch: 0,
1301 },
1302 );
1303 let BatchRefreshJobStatus::InitializingBatchRefresh {
1304 fragment_infos,
1305 node_actors,
1306 state_table_ids,
1307 pending_log_barriers,
1308 target_upstream_epoch,
1309 } = old_status
1310 else {
1311 panic!(
1312 "batch refresh job {}: logstore initialized in unexpected status {:?}",
1313 self.job_id, old_status
1314 );
1315 };
1316
1317 let final_barrier_idx = pending_log_barriers.len() - 1;
1318 let mut stop_mutation = Some(Mutation::Stop(StopMutation {
1319 actors: fragment_infos
1320 .values()
1321 .flat_map(|fragment| fragment.actors.keys().copied())
1322 .collect(),
1323 dropped_sink_fragments: vec![],
1324 }));
1325 for (idx, barrier) in pending_log_barriers.into_iter().enumerate() {
1326 let is_stop_barrier = idx == final_barrier_idx;
1327 let mutation = is_stop_barrier.then(|| stop_mutation.take().expect("unused"));
1328 Self::inject_barrier(
1329 self.partial_graph_id,
1330 partial_graph_manager,
1331 &node_actors,
1332 &state_table_ids,
1333 barrier,
1334 None,
1335 mutation,
1336 None,
1337 None,
1338 is_stop_barrier,
1339 )?;
1340 }
1341
1342 self.status = BatchRefreshJobStatus::ConsumingLogStore {
1343 fragment_infos,
1344 target_upstream_epoch,
1345 };
1346 Ok(())
1347 }
1348
1349 fn resolve_log_epoch_barriers(
1355 snapshot_backfill_upstream_tables: &HashSet<TableId>,
1356 upstream_table_log_epochs: &HashMap<TableId, Vec<(Vec<u64>, u64)>>,
1357 exclusive_start_log_epoch: u64,
1358 ) -> MetaResult<Option<(u64, Vec<BarrierInfo>)>> {
1359 let table_id = snapshot_backfill_upstream_tables
1360 .iter()
1361 .next()
1362 .expect("snapshot backfill job should have upstream");
1363 let Some(epochs) = upstream_table_log_epochs.get(table_id) else {
1364 return Ok(None);
1365 };
1366
1367 let mut epochs_iter = epochs.iter().peekable();
1369 loop {
1370 match epochs_iter.peek() {
1371 Some((_, checkpoint_epoch)) if *checkpoint_epoch <= exclusive_start_log_epoch => {
1372 epochs_iter.next();
1373 }
1374 _ => break,
1375 }
1376 }
1377
1378 let mut epoch_infos = vec![];
1379 for (non_checkpoint_epochs, checkpoint_epoch) in epochs_iter {
1380 epoch_infos.extend(
1381 non_checkpoint_epochs
1382 .iter()
1383 .copied()
1384 .map(|epoch| (epoch, false)),
1385 );
1386 epoch_infos.push((*checkpoint_epoch, true));
1387 }
1388 if epoch_infos.is_empty() {
1389 return Ok(None);
1390 }
1391
1392 let first_epoch = epoch_infos[0].0;
1393 let mut pending_non_checkpoint_epochs = vec![];
1394 let mut replay_barriers = vec![];
1395 for window in epoch_infos.windows(2) {
1396 let (prev_epoch, is_checkpoint) = window[0];
1397 let curr_epoch = window[1].0;
1398 assert!(prev_epoch > exclusive_start_log_epoch);
1399 assert!(curr_epoch > prev_epoch);
1400 pending_non_checkpoint_epochs.push(prev_epoch);
1401 let kind = if is_checkpoint {
1402 BarrierKind::Checkpoint(take(&mut pending_non_checkpoint_epochs))
1403 } else {
1404 BarrierKind::Barrier
1405 };
1406 replay_barriers.push(BarrierInfo {
1407 prev_epoch: TracedEpoch::new(Epoch(prev_epoch)),
1408 curr_epoch: TracedEpoch::new(Epoch(curr_epoch)),
1409 kind,
1410 });
1411 }
1412
1413 let (last_epoch, _) = *epoch_infos.last().expect("non-empty");
1414 assert!(last_epoch > exclusive_start_log_epoch);
1415 pending_non_checkpoint_epochs.push(last_epoch);
1416 replay_barriers.push(BarrierInfo {
1417 prev_epoch: TracedEpoch::new(Epoch(last_epoch)),
1418 curr_epoch: TracedEpoch::new(Epoch(u64::MAX)),
1419 kind: BarrierKind::Checkpoint(pending_non_checkpoint_epochs),
1420 });
1421
1422 Ok(Some((first_epoch, replay_barriers)))
1423 }
1424}
1425
1426impl BatchRefreshLogicalFragments {
1427 pub(crate) fn from_context(ctx: &BatchRefreshJobTriggerContext) -> Self {
1429 Self {
1430 fragments: ctx.fragments.clone(),
1431 downstreams: ctx.downstreams.clone(),
1432 }
1433 }
1434}
1435
1436struct BatchRefreshBarrierStats {
1439 barrier_latency: LabelGuardedHistogram,
1440 inflight_barrier_num: LabelGuardedIntGauge,
1441}
1442
1443impl BatchRefreshBarrierStats {
1444 fn new(job_id: JobId, _snapshot_epoch: u64) -> Self {
1445 let table_id_str = format!("{}", job_id);
1446 Self {
1447 barrier_latency: GLOBAL_META_METRICS
1448 .snapshot_backfill_barrier_latency
1449 .with_guarded_label_values(&[table_id_str.as_str(), "batch_refresh_snapshot"]),
1450 inflight_barrier_num: GLOBAL_META_METRICS
1451 .snapshot_backfill_inflight_barrier_num
1452 .with_guarded_label_values(&[&table_id_str]),
1453 }
1454 }
1455}
1456
1457impl PartialGraphStat for BatchRefreshBarrierStats {
1458 fn observe_barrier_latency(&self, _epoch: EpochPair, barrier_latency_secs: f64) {
1459 self.barrier_latency.observe(barrier_latency_secs);
1460 }
1461
1462 fn observe_barrier_num(&self, inflight_barrier_num: usize, _collected_barrier_num: usize) {
1463 self.inflight_barrier_num.set(inflight_barrier_num as _);
1464 }
1465}