Skip to main content

risingwave_meta/barrier/checkpoint/
state.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::assert_matches;
16use std::collections::hash_map::Entry;
17use std::collections::{HashMap, HashSet};
18use std::mem::take;
19use std::sync::atomic::AtomicU32;
20
21use risingwave_common::bail;
22use risingwave_common::bitmap::Bitmap;
23use risingwave_common::catalog::TableId;
24use risingwave_common::hash::VnodeCountCompat;
25use risingwave_common::id::JobId;
26use risingwave_common::util::epoch::Epoch;
27use risingwave_meta_model::fragment::DistributionType;
28use risingwave_meta_model::{DispatcherType, WorkerId, streaming_job};
29use risingwave_pb::common::WorkerNode;
30use risingwave_pb::hummock::HummockVersionStats;
31use risingwave_pb::source::{ConnectorSplit, ConnectorSplits};
32use risingwave_pb::stream_plan::barrier_mutation::{Mutation, PbMutation};
33use risingwave_pb::stream_plan::update_mutation::PbDispatcherUpdate;
34use risingwave_pb::stream_plan::{
35    AddMutation, PbStartFragmentBackfillMutation, PbSubscriptionUpstreamInfo, PbUpdateMutation,
36    PbUpstreamSinkInfo,
37};
38use tracing::warn;
39
40use crate::barrier::cdc_progress::CdcTableBackfillTracker;
41use crate::barrier::checkpoint::{
42    BatchRefreshJobCheckpointControl, BatchRefreshLogicalFragments, CreatingStreamingJobControl,
43    DatabaseCheckpointControl, IndependentCheckpointJobControl,
44};
45use crate::barrier::command::{
46    CreateStreamingJobCommandInfo, PostCollectCommand, ReschedulePlan, ThrottleConfigMap,
47};
48use crate::barrier::context::CreateSnapshotBackfillJobCommandInfo;
49use crate::barrier::edge_builder::{EdgeBuilderFragmentInfo, FragmentEdgeBuilder};
50use crate::barrier::info::{
51    BarrierInfo, CreateStreamingJobStatus, InflightDatabaseInfo, InflightStreamingJobInfo,
52    SubscriberType,
53};
54use crate::barrier::notifier::NotifierStarter;
55use crate::barrier::partial_graph::{PartialGraphBarrierInfo, PartialGraphManager};
56use crate::barrier::rpc::to_partial_graph_id;
57use crate::barrier::{BarrierKind, Command, CreateStreamingJobType, TracedEpoch};
58use crate::controller::fragment::{InflightActorInfo, InflightFragmentInfo};
59use crate::controller::scale::{
60    ComponentFragmentAligner, EnsembleActorTemplate, LoadedFragment, NoShuffleEnsemble,
61    build_no_shuffle_fragment_graph_edges, find_no_shuffle_graphs,
62};
63use crate::model::{
64    ActorId, ActorNewNoShuffle, FragmentDownstreamRelation, FragmentId, StreamActor, StreamContext,
65    StreamJobActorsToCreate, StreamJobFragmentsToCreate,
66};
67use crate::stream::cdc::parallel_cdc_table_backfill_fragment;
68use crate::stream::{
69    GlobalActorIdGen, ReplaceJobSplitPlan, SourceManager, SplitAssignment,
70    fill_snapshot_backfill_epoch,
71};
72use crate::{MetaError, MetaResult};
73
74/// The latest state of `GlobalBarrierWorker` after injecting the latest barrier.
75pub(in crate::barrier) struct BarrierWorkerState {
76    /// The last sent `prev_epoch`
77    ///
78    /// There's no need to persist this field. On recovery, we will restore this from the latest
79    /// committed snapshot in `HummockManager`.
80    in_flight_prev_epoch: TracedEpoch,
81
82    /// The `prev_epoch` of pending non checkpoint barriers
83    pending_non_checkpoint_barriers: Vec<u64>,
84
85    /// Whether the cluster is paused.
86    is_paused: bool,
87}
88
89impl BarrierWorkerState {
90    pub(super) fn new() -> Self {
91        Self {
92            in_flight_prev_epoch: TracedEpoch::new(Epoch::now()),
93            pending_non_checkpoint_barriers: vec![],
94            is_paused: false,
95        }
96    }
97
98    pub fn recovery(in_flight_prev_epoch: TracedEpoch, is_paused: bool) -> Self {
99        Self {
100            in_flight_prev_epoch,
101            pending_non_checkpoint_barriers: vec![],
102            is_paused,
103        }
104    }
105
106    pub fn is_paused(&self) -> bool {
107        self.is_paused
108    }
109
110    fn set_is_paused(&mut self, is_paused: bool) {
111        if self.is_paused != is_paused {
112            tracing::info!(
113                currently_paused = self.is_paused,
114                newly_paused = is_paused,
115                "update paused state"
116            );
117            self.is_paused = is_paused;
118        }
119    }
120
121    pub fn in_flight_prev_epoch(&self) -> &TracedEpoch {
122        &self.in_flight_prev_epoch
123    }
124
125    /// Returns the `BarrierInfo` for the next barrier, and updates the state.
126    pub fn next_barrier_info(
127        &mut self,
128        is_checkpoint: bool,
129        curr_epoch: TracedEpoch,
130    ) -> BarrierInfo {
131        assert!(
132            self.in_flight_prev_epoch.value() < curr_epoch.value(),
133            "curr epoch regress. {} > {}",
134            self.in_flight_prev_epoch.value(),
135            curr_epoch.value()
136        );
137        let prev_epoch = self.in_flight_prev_epoch.clone();
138        self.in_flight_prev_epoch = curr_epoch.clone();
139        self.pending_non_checkpoint_barriers
140            .push(prev_epoch.value().0);
141        let kind = if is_checkpoint {
142            let epochs = take(&mut self.pending_non_checkpoint_barriers);
143            BarrierKind::Checkpoint(epochs)
144        } else {
145            BarrierKind::Barrier
146        };
147        BarrierInfo {
148            prev_epoch,
149            curr_epoch,
150            kind,
151        }
152    }
153}
154
155pub(super) struct ApplyCommandInfo {
156    pub jobs_to_wait: HashSet<JobId>,
157}
158
159/// Result tuple of `apply_command`: mutation, table IDs to commit, actors to create,
160/// node actors, and post-collect command.
161type ApplyCommandResult = (
162    Option<Mutation>,
163    HashSet<TableId>,
164    Option<StreamJobActorsToCreate>,
165    HashMap<WorkerId, HashSet<ActorId>>,
166    PostCollectCommand,
167);
168
169/// Result of actor rendering for a create/replace streaming job.
170pub(crate) struct RenderResult {
171    /// Rendered actors grouped by fragment.
172    pub stream_actors: HashMap<FragmentId, Vec<StreamActor>>,
173    /// Worker placement for each actor.
174    pub actor_location: HashMap<ActorId, WorkerId>,
175}
176
177/// Derive `NoShuffle` edges from fragment downstream relations and resolve ensembles.
178///
179/// This scans both the internal downstream relations (`fragments.downstreams`) and
180/// the cross-boundary upstream-to-new-fragment relations (`upstream_fragment_downstreams`)
181/// to find all `NoShuffle` edges. It then runs BFS to find connected components (ensembles)
182/// and categorizes them into:
183/// - Ensembles whose entry fragments include existing (non-new) fragments
184/// - Ensembles whose entry fragments are all newly created
185pub(crate) fn resolve_no_shuffle_ensembles(
186    fragments: &StreamJobFragmentsToCreate,
187    upstream_fragment_downstreams: &FragmentDownstreamRelation,
188) -> MetaResult<Vec<NoShuffleEnsemble>> {
189    // Derive FragmentNewNoShuffle from the two downstream relation maps.
190    let mut new_no_shuffle: HashMap<_, HashSet<_>> = HashMap::new();
191
192    // Internal edges (new → new) and edges from new → existing downstream (replace job).
193    for (upstream_fid, relations) in &fragments.downstreams {
194        for rel in relations {
195            if rel.dispatcher_type == DispatcherType::NoShuffle {
196                new_no_shuffle
197                    .entry(*upstream_fid)
198                    .or_default()
199                    .insert(rel.downstream_fragment_id);
200            }
201        }
202    }
203
204    // Cross-boundary edges: existing upstream → new downstream.
205    for (upstream_fid, relations) in upstream_fragment_downstreams {
206        for rel in relations {
207            if rel.dispatcher_type == DispatcherType::NoShuffle {
208                new_no_shuffle
209                    .entry(*upstream_fid)
210                    .or_default()
211                    .insert(rel.downstream_fragment_id);
212            }
213        }
214    }
215
216    let mut ensembles = if new_no_shuffle.is_empty() {
217        Vec::new()
218    } else {
219        // Flatten into directed edge pairs for BFS.
220        let no_shuffle_edges: Vec<(FragmentId, FragmentId)> = new_no_shuffle
221            .iter()
222            .flat_map(|(upstream_fid, downstream_fids)| {
223                downstream_fids
224                    .iter()
225                    .map(move |downstream_fid| (*upstream_fid, *downstream_fid))
226            })
227            .collect();
228
229        let all_fragment_ids: Vec<FragmentId> = no_shuffle_edges
230            .iter()
231            .flat_map(|(u, d)| [*u, *d])
232            .collect::<HashSet<_>>()
233            .into_iter()
234            .collect();
235
236        let (fwd, bwd) = build_no_shuffle_fragment_graph_edges(no_shuffle_edges);
237        find_no_shuffle_graphs(&all_fragment_ids, &fwd, &bwd)?
238    };
239
240    // Add standalone fragments (not covered by any ensemble) as single-fragment ensembles.
241    let covered: HashSet<FragmentId> = ensembles
242        .iter()
243        .flat_map(|e| e.component_fragments())
244        .collect();
245    for fragment_id in fragments.inner.fragments.keys() {
246        if !covered.contains(fragment_id) {
247            ensembles.push(NoShuffleEnsemble::singleton(*fragment_id));
248        }
249    }
250
251    Ok(ensembles)
252}
253
254/// Render actors for a create or replace streaming job.
255///
256/// This determines the parallelism for each no-shuffle ensemble (either from an existing
257/// inflight upstream or computed fresh), and produces `StreamActor` instances with worker
258/// placements and actor-level no-shuffle mappings.
259///
260/// The process follows three steps:
261/// 1. For each ensemble, resolve `EnsembleActorTemplate` (from existing or fresh).
262/// 2. For each new component fragment, allocate actor IDs and compute worker/vnode assignments.
263/// 3. Expand the simple assignments into full `StreamActor` structures.
264pub(super) fn render_actors(
265    fragments: &StreamJobFragmentsToCreate,
266    database_info: &InflightDatabaseInfo,
267    definition: &str,
268    ctx: &StreamContext,
269    streaming_job_model: &streaming_job::Model,
270    actor_id_counter: &AtomicU32,
271    worker_map: &HashMap<WorkerId, WorkerNode>,
272    ensembles: &[NoShuffleEnsemble],
273    database_resource_group: &str,
274) -> MetaResult<RenderResult> {
275    // Step 2: Render actors for each ensemble.
276    // For each new fragment, produce a simple assignment: actor_id -> (worker_id, vnode_bitmap).
277    let mut actor_assignments: HashMap<FragmentId, HashMap<ActorId, (WorkerId, Option<Bitmap>)>> =
278        HashMap::new();
279
280    for ensemble in ensembles {
281        // Determine the EnsembleActorTemplate for this ensemble.
282        //
283        // Check if any component fragment in the ensemble already exists (i.e. is inflight).
284        // If so, derive the actor assignment from an existing fragment. Otherwise render fresh.
285        let existing_fragment_ids: Vec<FragmentId> = ensemble
286            .component_fragments()
287            .filter(|fragment_id| !fragments.inner.fragments.contains_key(fragment_id))
288            .collect();
289
290        let actor_template = if let Some(&first_existing) = existing_fragment_ids.first() {
291            let template = EnsembleActorTemplate::from_existing_inflight_fragment(
292                database_info.fragment(first_existing),
293            );
294
295            // Sanity check: all existing fragments in the same ensemble must be aligned —
296            // same actor count and same worker placement per vnode.
297            for &other_fragment_id in &existing_fragment_ids[1..] {
298                let other = EnsembleActorTemplate::from_existing_inflight_fragment(
299                    database_info.fragment(other_fragment_id),
300                );
301                template.assert_aligned_with(&other, first_existing, other_fragment_id);
302            }
303
304            template
305        } else {
306            // All fragments are new — render from scratch.
307            let first_component = ensemble
308                .component_fragments()
309                .next()
310                .expect("ensemble must have at least one component");
311            let fragment = &fragments.inner.fragments[&first_component];
312            let distribution_type: DistributionType = fragment.distribution_type.into();
313            let vnode_count = fragment.vnode_count();
314
315            // Assert all component fragments in this ensemble share the same vnode count.
316            for fragment_id in ensemble.component_fragments() {
317                let f = &fragments.inner.fragments[&fragment_id];
318                assert_eq!(
319                    vnode_count,
320                    f.vnode_count(),
321                    "component fragments {} and {} in the same no-shuffle ensemble have \
322                     different vnode counts: {} vs {}",
323                    first_component,
324                    fragment_id,
325                    vnode_count,
326                    f.vnode_count(),
327                );
328            }
329
330            EnsembleActorTemplate::render_new(
331                streaming_job_model,
332                worker_map,
333                None,
334                database_resource_group.to_owned(),
335                distribution_type,
336                vnode_count,
337            )?
338        };
339
340        // Render each new component fragment in this ensemble.
341        for fragment_id in ensemble.component_fragments() {
342            if !fragments.inner.fragments.contains_key(&fragment_id) {
343                continue; // Skip existing fragments.
344            }
345            let fragment = &fragments.inner.fragments[&fragment_id];
346            let distribution_type: DistributionType = fragment.distribution_type.into();
347            let aligner =
348                ComponentFragmentAligner::new_persistent(&actor_template, actor_id_counter);
349            let assignments = aligner.align_component_actor(distribution_type);
350            actor_assignments.insert(fragment_id, assignments);
351        }
352    }
353
354    // Step 3: Expand simple assignments into full StreamActor structures.
355    let mut result_stream_actors: HashMap<FragmentId, Vec<StreamActor>> = HashMap::new();
356    let mut result_actor_location: HashMap<ActorId, WorkerId> = HashMap::new();
357
358    for (fragment_id, assignments) in &actor_assignments {
359        let mut actors = Vec::with_capacity(assignments.len());
360        for (&actor_id, (worker_id, vnode_bitmap)) in assignments {
361            result_actor_location.insert(actor_id, *worker_id);
362            actors.push(StreamActor {
363                actor_id,
364                fragment_id: *fragment_id,
365                vnode_bitmap: vnode_bitmap.clone(),
366                mview_definition: definition.to_owned(),
367                expr_context: Some(ctx.to_expr_context()),
368                config_override: ctx.config_override.clone(),
369            });
370        }
371        result_stream_actors.insert(*fragment_id, actors);
372    }
373
374    Ok(RenderResult {
375        stream_actors: result_stream_actors,
376        actor_location: result_actor_location,
377    })
378}
379impl DatabaseCheckpointControl {
380    /// Collect table IDs to commit and actor IDs to collect from current fragment infos.
381    fn collect_base_info(&self) -> (HashSet<TableId>, HashMap<WorkerId, HashSet<ActorId>>) {
382        let table_ids_to_commit = self.database_info.existing_table_ids().collect();
383        let node_actors =
384            InflightFragmentInfo::actor_ids_to_collect(self.database_info.fragment_infos());
385        (table_ids_to_commit, node_actors)
386    }
387
388    /// Helper for the simplest command variants: those that only need a
389    /// pre-computed mutation and a command name, with no actors to create
390    /// and no additional side effects on `self`.
391    fn apply_simple_command(
392        &self,
393        mutation: Option<Mutation>,
394        command_name: &'static str,
395    ) -> ApplyCommandResult {
396        let (table_ids, node_actors) = self.collect_base_info();
397        (
398            mutation,
399            table_ids,
400            None,
401            node_actors,
402            PostCollectCommand::Command(command_name.to_owned()),
403        )
404    }
405
406    /// Returns the inflight actor infos that have included the newly added actors in the given command. The dropped actors
407    /// will be removed from the state after the info get resolved.
408    pub(super) fn apply_command(
409        &mut self,
410        command: Option<Command>,
411        notifier: &mut Option<NotifierStarter>,
412        barrier_info: BarrierInfo,
413        partial_graph_manager: &mut PartialGraphManager,
414        hummock_version_stats: &HummockVersionStats,
415        worker_nodes: &HashMap<WorkerId, WorkerNode>,
416    ) -> MetaResult<ApplyCommandInfo> {
417        debug_assert!(
418            !matches!(
419                command,
420                Some(Command::RescheduleIntent {
421                    reschedule_plan: None,
422                    ..
423                })
424            ),
425            "reschedule intent must be resolved before apply"
426        );
427        if matches!(
428            command,
429            Some(Command::RescheduleIntent {
430                reschedule_plan: None,
431                ..
432            })
433        ) {
434            bail!("reschedule intent must be resolved before apply");
435        }
436
437        /// Resolve source splits for a create streaming job command.
438        ///
439        /// Combines source fragment split resolution and backfill split alignment
440        /// into one step, looking up existing upstream actor splits from the inflight database info.
441        fn resolve_source_splits(
442            info: &CreateStreamingJobCommandInfo,
443            render_result: &RenderResult,
444            actor_no_shuffle: &ActorNewNoShuffle,
445            database_info: &InflightDatabaseInfo,
446        ) -> MetaResult<SplitAssignment> {
447            let fragment_actor_ids: HashMap<FragmentId, Vec<ActorId>> = render_result
448                .stream_actors
449                .iter()
450                .map(|(fragment_id, actors)| {
451                    (
452                        *fragment_id,
453                        actors.iter().map(|a| a.actor_id).collect::<Vec<_>>(),
454                    )
455                })
456                .collect();
457            let mut resolved = SourceManager::resolve_fragment_to_actor_splits(
458                &info.stream_job_fragments,
459                &info.init_split_assignment,
460                &fragment_actor_ids,
461            )?;
462            resolved.extend(SourceManager::resolve_backfill_splits(
463                &info.stream_job_fragments,
464                actor_no_shuffle,
465                |fragment_id, actor_id| {
466                    database_info
467                        .fragment(fragment_id)
468                        .actors
469                        .get(&actor_id)
470                        .map(|info| info.splits.clone())
471                },
472            )?);
473            Ok(resolved)
474        }
475
476        let mut notify_database_graph = command.is_some();
477        let mut throttle_config: Option<ThrottleConfigMap> = None;
478
479        // Each variant handles its own pre-apply, edge building, mutation generation,
480        // collect base info, and post-apply. The match produces values consumed by the
481        // common snapshot-backfill-merging code that follows.
482        let (
483            mutation,
484            mut table_ids_to_commit,
485            mut actors_to_create,
486            mut node_actors,
487            post_collect_command,
488        ) = match command {
489            None => self.apply_simple_command(None, "barrier"),
490            Some(Command::CreateStreamingJob {
491                mut info,
492                job_type:
493                    CreateStreamingJobType::SnapshotBackfill {
494                        mut snapshot_backfill_info,
495                        since_epoch,
496                    },
497                cross_db_snapshot_backfill_info,
498            }) => {
499                notify_database_graph = false;
500                let ensembles = resolve_no_shuffle_ensembles(
501                    &info.stream_job_fragments,
502                    &info.upstream_fragment_downstreams,
503                )?;
504                let actors = render_actors(
505                    &info.stream_job_fragments,
506                    &self.database_info,
507                    &info.definition,
508                    &info.stream_job_fragments.inner.ctx,
509                    &info.streaming_job_model,
510                    partial_graph_manager
511                        .control_stream_manager()
512                        .env
513                        .actor_id_generator(),
514                    worker_nodes,
515                    &ensembles,
516                    &info.database_resource_group,
517                )?;
518                {
519                    assert!(!self.state.is_paused());
520                    let (snapshot_epoch, since_timestamp_upstream_log_epochs) =
521                        if let Some(since_epoch) = &since_epoch {
522                            let (snapshot_epoch, log_epochs) =
523                                since_epoch.resolved.as_ref().ok_or_else(|| {
524                            MetaError::from(anyhow::anyhow!(
525                                "since_timestamp epoch has not been resolved for snapshot backfill"
526                            ))
527                        })?;
528                            (
529                                *snapshot_epoch,
530                                Some((
531                                    log_epochs,
532                                    to_partial_graph_id(self.database_id, None),
533                                    barrier_info.prev_epoch(),
534                                )),
535                            )
536                        } else {
537                            (barrier_info.prev_epoch(), None)
538                        };
539                    // set snapshot epoch of upstream table for snapshot backfill
540                    for snapshot_backfill_epoch in snapshot_backfill_info
541                        .upstream_mv_table_id_to_backfill_epoch
542                        .values_mut()
543                    {
544                        assert_eq!(
545                            snapshot_backfill_epoch.replace(snapshot_epoch),
546                            None,
547                            "must not set previously"
548                        );
549                    }
550                    for fragment in info.stream_job_fragments.inner.fragments.values_mut() {
551                        fill_snapshot_backfill_epoch(
552                            &mut fragment.nodes,
553                            Some(&snapshot_backfill_info),
554                            &cross_db_snapshot_backfill_info,
555                        )?;
556                    }
557                    let job_id = info.stream_job_fragments.stream_job_id();
558                    let snapshot_backfill_upstream_tables = snapshot_backfill_info
559                        .upstream_mv_table_id_to_backfill_epoch
560                        .keys()
561                        .cloned()
562                        .collect();
563                    // Build edges first (needed for no-shuffle mapping used in split resolution)
564                    let mut edges = self.database_info.build_edge(
565                        Some((&info, true)),
566                        None,
567                        None,
568                        partial_graph_manager.control_stream_manager(),
569                        &actors.stream_actors,
570                        &actors.actor_location,
571                    );
572                    // Phase 2: Resolve source-level DiscoveredSplits to actor-level SplitAssignment
573                    let resolved_split_assignment = resolve_source_splits(
574                        &info,
575                        &actors,
576                        edges.actor_new_no_shuffle(),
577                        &self.database_info,
578                    )?;
579
580                    let Entry::Vacant(entry) =
581                        self.independent_checkpoint_job_controls.entry(job_id)
582                    else {
583                        panic!("duplicated creating snapshot backfill job {job_id}");
584                    };
585
586                    let term_id = self.term_id.as_str();
587                    let job = CreatingStreamingJobControl::new(
588                        entry,
589                        CreateSnapshotBackfillJobCommandInfo {
590                            info: info.clone(),
591                            snapshot_backfill_info: snapshot_backfill_info.clone(),
592                            cross_db_snapshot_backfill_info,
593                            resolved_split_assignment: resolved_split_assignment.clone(),
594                            refresh_interval_sec: None,
595                        },
596                        notifier.as_mut(),
597                        snapshot_backfill_upstream_tables,
598                        snapshot_epoch,
599                        since_timestamp_upstream_log_epochs,
600                        hummock_version_stats,
601                        term_id,
602                        partial_graph_manager,
603                        &mut edges,
604                        &resolved_split_assignment,
605                        &actors,
606                    )?;
607
608                    if let Some(fragment_infos) = job.fragment_infos() {
609                        self.database_info.shared_actor_infos.upsert(
610                            self.database_id,
611                            fragment_infos.values().map(|f| (f, job_id)),
612                        );
613                    }
614
615                    for upstream_mv_table_id in snapshot_backfill_info
616                        .upstream_mv_table_id_to_backfill_epoch
617                        .keys()
618                    {
619                        self.database_info.register_subscriber(
620                            upstream_mv_table_id.as_job_id(),
621                            info.streaming_job.id().as_subscriber_id(),
622                            SubscriberType::SnapshotBackfill,
623                        );
624                    }
625
626                    let mutation = Command::create_streaming_job_to_mutation(
627                        &info,
628                        &CreateStreamingJobType::SnapshotBackfill {
629                            snapshot_backfill_info,
630                            since_epoch,
631                        },
632                        [],
633                        self.state.is_paused(),
634                        &mut edges,
635                        partial_graph_manager.control_stream_manager(),
636                        None,
637                        &resolved_split_assignment,
638                        &actors.stream_actors,
639                        &actors.actor_location,
640                    )?;
641
642                    let (table_ids, node_actors) = self.collect_base_info();
643                    (
644                        Some(mutation),
645                        table_ids,
646                        None,
647                        node_actors,
648                        PostCollectCommand::barrier(),
649                    )
650                }
651            }
652            Some(Command::CreateStreamingJob {
653                mut info,
654                job_type: CreateStreamingJobType::BatchRefresh(mut batch_refresh_info),
655                cross_db_snapshot_backfill_info,
656            }) => {
657                notify_database_graph = false;
658                {
659                    if self.state.is_paused() {
660                        bail!("cannot create batch refresh job while database barrier is paused");
661                    }
662                    let snapshot_epoch = barrier_info.prev_epoch();
663                    let job_id = info.stream_job_fragments.stream_job_id();
664                    let database_id = info.streaming_job.database_id();
665
666                    // 1. Fill snapshot backfill epochs.
667                    let snapshot_backfill_info = &mut batch_refresh_info.snapshot_backfill_info;
668                    for snapshot_backfill_epoch in snapshot_backfill_info
669                        .upstream_mv_table_id_to_backfill_epoch
670                        .values_mut()
671                    {
672                        assert_eq!(
673                            snapshot_backfill_epoch.replace(snapshot_epoch),
674                            None,
675                            "must not set previously"
676                        );
677                    }
678                    for fragment in info.stream_job_fragments.inner.fragments.values_mut() {
679                        fill_snapshot_backfill_epoch(
680                            &mut fragment.nodes,
681                            Some(snapshot_backfill_info),
682                            &cross_db_snapshot_backfill_info,
683                        )?;
684                    }
685                    let snapshot_backfill_upstream_tables: HashSet<TableId> =
686                        snapshot_backfill_info
687                            .upstream_mv_table_id_to_backfill_epoch
688                            .keys()
689                            .cloned()
690                            .collect();
691
692                    // 2. Build BatchRefreshLogicalFragments (after epoch filling).
693                    let logical = BatchRefreshLogicalFragments {
694                        fragments: info
695                            .stream_job_fragments
696                            .inner
697                            .fragments
698                            .iter()
699                            .map(|(&fid, fragment)| {
700                                (
701                                    fid,
702                                    LoadedFragment {
703                                        fragment_id: fid,
704                                        job_id,
705                                        fragment_type_mask: fragment.fragment_type_mask,
706                                        distribution_type: fragment.distribution_type.into(),
707                                        vnode_count: fragment.vnode_count(),
708                                        nodes: fragment.nodes.clone(),
709                                        state_table_ids: fragment
710                                            .state_table_ids
711                                            .iter()
712                                            .cloned()
713                                            .collect(),
714                                        parallelism: None,
715                                    },
716                                )
717                            })
718                            .collect(),
719                        downstreams: info.stream_job_fragments.downstreams.clone(),
720                    };
721
722                    // 3. Create BatchRefreshJobCheckpointControl. `new()` handles actor
723                    //    rendering, the partial-graph initial barrier, and produces the
724                    //    database-graph mutation for the main barrier.
725                    assert!(
726                        !self
727                            .independent_checkpoint_job_controls
728                            .contains_key(&job_id),
729                        "duplicated creating batch refresh job {job_id}"
730                    );
731
732                    let snapshot_backfill_info_clone =
733                        batch_refresh_info.snapshot_backfill_info.clone();
734                    let refresh_interval_sec = batch_refresh_info.refresh_interval_sec;
735
736                    // Database-graph `Add` mutation: batch refresh has no actors in the
737                    // database graph; it only needs to register snapshot-backfill
738                    // subscribers on the upstream MV tables.
739                    let subscriber_id =
740                        info.stream_job_fragments.stream_job_id().as_subscriber_id();
741                    let mutation = Mutation::Add(AddMutation {
742                        actor_dispatchers: Default::default(),
743                        added_actors: Default::default(),
744                        actor_splits: Default::default(),
745                        pause: false,
746                        subscriptions_to_add: snapshot_backfill_info_clone
747                            .upstream_mv_table_id_to_backfill_epoch
748                            .keys()
749                            .map(|table_id| PbSubscriptionUpstreamInfo {
750                                subscriber_id,
751                                upstream_mv_table_id: *table_id,
752                            })
753                            .collect(),
754                        backfill_nodes_to_pause: Default::default(),
755                        actor_cdc_table_snapshot_splits: None,
756                        new_upstream_sinks: Default::default(),
757                        dropped_actors: Default::default(),
758                        sink_log_store_flush: Default::default(),
759                    });
760
761                    let job = BatchRefreshJobCheckpointControl::new(
762                        database_id,
763                        job_id,
764                        CreateSnapshotBackfillJobCommandInfo {
765                            info: info.clone(),
766                            snapshot_backfill_info: snapshot_backfill_info_clone.clone(),
767                            cross_db_snapshot_backfill_info,
768                            resolved_split_assignment: Default::default(),
769                            refresh_interval_sec: Some(refresh_interval_sec),
770                        },
771                        notifier.as_mut(),
772                        snapshot_backfill_upstream_tables,
773                        snapshot_epoch,
774                        hummock_version_stats,
775                        self.term_id(),
776                        partial_graph_manager,
777                        &logical,
778                        worker_nodes,
779                        refresh_interval_sec,
780                    )?;
781
782                    if let Some(fragment_infos) = job.fragment_infos() {
783                        self.database_info.shared_actor_infos.upsert(
784                            self.database_id,
785                            fragment_infos.values().map(|f| (f, job_id)),
786                        );
787                    }
788
789                    self.independent_checkpoint_job_controls
790                        .insert(job_id, IndependentCheckpointJobControl::BatchRefresh(job));
791
792                    // Register permanent subscriber (never unregistered until MV is dropped)
793                    for upstream_mv_table_id in snapshot_backfill_info_clone
794                        .upstream_mv_table_id_to_backfill_epoch
795                        .keys()
796                    {
797                        self.database_info.register_subscriber(
798                            upstream_mv_table_id.as_job_id(),
799                            info.streaming_job.id().as_subscriber_id(),
800                            SubscriberType::SnapshotBackfill,
801                        );
802                    }
803
804                    let (table_ids, node_actors) = self.collect_base_info();
805                    (
806                        Some(mutation),
807                        table_ids,
808                        None,
809                        node_actors,
810                        PostCollectCommand::barrier(),
811                    )
812                }
813            }
814            Some(Command::CreateStreamingJob {
815                mut info,
816                job_type,
817                cross_db_snapshot_backfill_info,
818            }) => {
819                let ensembles = resolve_no_shuffle_ensembles(
820                    &info.stream_job_fragments,
821                    &info.upstream_fragment_downstreams,
822                )?;
823                let actors = render_actors(
824                    &info.stream_job_fragments,
825                    &self.database_info,
826                    &info.definition,
827                    &info.stream_job_fragments.inner.ctx,
828                    &info.streaming_job_model,
829                    partial_graph_manager
830                        .control_stream_manager()
831                        .env
832                        .actor_id_generator(),
833                    worker_nodes,
834                    &ensembles,
835                    &info.database_resource_group,
836                )?;
837                for fragment in info.stream_job_fragments.inner.fragments.values_mut() {
838                    fill_snapshot_backfill_epoch(
839                        &mut fragment.nodes,
840                        None,
841                        &cross_db_snapshot_backfill_info,
842                    )?;
843                }
844
845                // Build edges
846                let new_upstream_sink =
847                    if let CreateStreamingJobType::SinkIntoTable(ref ctx) = job_type {
848                        Some(ctx)
849                    } else {
850                        None
851                    };
852
853                let mut edges = self.database_info.build_edge(
854                    Some((&info, false)),
855                    None,
856                    new_upstream_sink,
857                    partial_graph_manager.control_stream_manager(),
858                    &actors.stream_actors,
859                    &actors.actor_location,
860                );
861                // Phase 2: Resolve source-level DiscoveredSplits to actor-level SplitAssignment
862                let resolved_split_assignment = resolve_source_splits(
863                    &info,
864                    &actors,
865                    edges.actor_new_no_shuffle(),
866                    &self.database_info,
867                )?;
868
869                let old_sink_job_id = info
870                    .replace_sink
871                    .as_ref()
872                    .map(|old_sink_id| old_sink_id.as_job_id());
873                if old_sink_job_id.is_some()
874                    && matches!(
875                        job_type,
876                        CreateStreamingJobType::SnapshotBackfill { .. }
877                            | CreateStreamingJobType::BatchRefresh(_)
878                    )
879                {
880                    bail!("replace sink must not use snapshot backfill");
881                }
882
883                // Pre-apply: add new job and fragments
884                let cdc_tracker = if let Some(splits) = &info.cdc_table_snapshot_splits {
885                    let (fragment, _) =
886                        parallel_cdc_table_backfill_fragment(info.stream_job_fragments.fragments())
887                            .expect("should have parallel cdc fragment");
888                    Some(CdcTableBackfillTracker::new(
889                        fragment.fragment_id,
890                        splits.clone(),
891                    ))
892                } else {
893                    None
894                };
895                self.database_info
896                    .pre_apply_new_job(info.streaming_job.id(), cdc_tracker);
897                self.database_info.pre_apply_new_fragments(
898                    info.stream_job_fragments
899                        .new_fragment_info(
900                            &actors.stream_actors,
901                            &actors.actor_location,
902                            &resolved_split_assignment,
903                        )
904                        .map(|(fragment_id, fragment_infos)| {
905                            (fragment_id, info.streaming_job.id(), fragment_infos)
906                        }),
907                );
908                if let CreateStreamingJobType::SinkIntoTable(ref ctx) = job_type {
909                    let downstream_fragment_id = ctx.new_sink_downstream.downstream_fragment_id;
910                    self.database_info.pre_apply_add_node_upstream(
911                        downstream_fragment_id,
912                        &PbUpstreamSinkInfo {
913                            upstream_fragment_id: ctx.sink_fragment_id,
914                            sink_output_schema: ctx.sink_output_fields.clone(),
915                            project_exprs: ctx.project_exprs.clone(),
916                        },
917                    );
918                }
919
920                let (table_ids, node_actors) = self.collect_base_info();
921                let dropped_actors = if let Some(old_sink_job_id) = old_sink_job_id {
922                    let Some(job) = self.database_info.post_apply_remove_job(old_sink_job_id)
923                    else {
924                        bail!(
925                            "old sink job {} not found in barrier state",
926                            old_sink_job_id
927                        );
928                    };
929                    job.fragment_infos
930                        .values()
931                        .flat_map(|fragment| fragment.actors.keys().copied())
932                        .collect()
933                } else {
934                    vec![]
935                };
936
937                // Actors to create
938                let actors_to_create = Some(Command::create_streaming_job_actors_to_create(
939                    &info,
940                    &mut edges,
941                    &actors.stream_actors,
942                    &actors.actor_location,
943                ));
944
945                // CDC table snapshot splits
946                let actor_cdc_table_snapshot_splits = self
947                    .database_info
948                    .assign_cdc_backfill_splits(info.stream_job_fragments.stream_job_id())?;
949
950                // Mutation
951                let is_currently_paused = self.state.is_paused();
952                let mutation = Command::create_streaming_job_to_mutation(
953                    &info,
954                    &job_type,
955                    dropped_actors,
956                    is_currently_paused,
957                    &mut edges,
958                    partial_graph_manager.control_stream_manager(),
959                    actor_cdc_table_snapshot_splits,
960                    &resolved_split_assignment,
961                    &actors.stream_actors,
962                    &actors.actor_location,
963                )?;
964
965                (
966                    Some(mutation),
967                    table_ids,
968                    actors_to_create,
969                    node_actors,
970                    PostCollectCommand::CreateStreamingJob {
971                        info,
972                        job_type,
973                        cross_db_snapshot_backfill_info,
974                        resolved_split_assignment,
975                    },
976                )
977            }
978
979            Some(Command::Flush) => self.apply_simple_command(None, "Flush"),
980
981            Some(Command::Pause) => {
982                let prev_is_paused = self.state.is_paused();
983                self.state.set_is_paused(true);
984                let mutation = Command::pause_to_mutation(prev_is_paused);
985                let (table_ids, node_actors) = self.collect_base_info();
986                (
987                    mutation,
988                    table_ids,
989                    None,
990                    node_actors,
991                    PostCollectCommand::Command("Pause".to_owned()),
992                )
993            }
994
995            Some(Command::Resume) => {
996                let prev_is_paused = self.state.is_paused();
997                self.state.set_is_paused(false);
998                let mutation = Command::resume_to_mutation(prev_is_paused);
999                let (table_ids, node_actors) = self.collect_base_info();
1000                (
1001                    mutation,
1002                    table_ids,
1003                    None,
1004                    node_actors,
1005                    PostCollectCommand::Command("Resume".to_owned()),
1006                )
1007            }
1008
1009            Some(Command::Throttle { mut config }) => {
1010                let mutation = self.database_info.pre_apply_throttle(&mut config);
1011                notify_database_graph = mutation.is_some();
1012                throttle_config = Some(config);
1013                self.apply_simple_command(mutation, "Throttle")
1014            }
1015
1016            Some(Command::DropStreamingJobs {
1017                streaming_job_ids,
1018                unregistered_state_table_ids: _,
1019                dropped_sink_fragment_by_targets,
1020            }) => {
1021                // pre_apply: drop node upstream for sink targets
1022                for (target_fragment, sink_fragments) in &dropped_sink_fragment_by_targets {
1023                    self.database_info
1024                        .pre_apply_drop_node_upstream(*target_fragment, sink_fragments);
1025                }
1026
1027                let (table_ids, node_actors) = self.collect_base_info();
1028
1029                let mut actors = Vec::new();
1030                for job_id in streaming_job_ids {
1031                    let Some(job) = self.database_info.post_apply_remove_job(job_id) else {
1032                        warn!(
1033                            %job_id,
1034                            "skip drop payload for streaming job that has already been removed from barrier worker"
1035                        );
1036                        continue;
1037                    };
1038
1039                    for fragment in job.fragment_infos.values() {
1040                        actors.extend(fragment.actors.keys().copied());
1041                    }
1042                }
1043
1044                let mutation = Some(Command::drop_streaming_jobs_to_mutation(
1045                    &actors,
1046                    &dropped_sink_fragment_by_targets,
1047                ));
1048                (
1049                    mutation,
1050                    table_ids,
1051                    None,
1052                    node_actors,
1053                    PostCollectCommand::DropStreamingJobs,
1054                )
1055            }
1056
1057            Some(Command::RescheduleIntent {
1058                reschedule_plan, ..
1059            }) => {
1060                let ReschedulePlan {
1061                    reschedules,
1062                    fragment_actors,
1063                } = reschedule_plan
1064                    .as_ref()
1065                    .expect("reschedule intent should be resolved in global barrier worker");
1066
1067                // Pre-apply: reschedule fragments
1068                for (fragment_id, reschedule) in reschedules {
1069                    self.database_info.pre_apply_reschedule(
1070                        *fragment_id,
1071                        reschedule
1072                            .added_actors
1073                            .iter()
1074                            .flat_map(|(node_id, actors): (&WorkerId, &Vec<ActorId>)| {
1075                                actors.iter().map(|actor_id| {
1076                                    (
1077                                        *actor_id,
1078                                        InflightActorInfo {
1079                                            worker_id: *node_id,
1080                                            vnode_bitmap: reschedule
1081                                                .newly_created_actors
1082                                                .get(actor_id)
1083                                                .expect("should exist")
1084                                                .0
1085                                                .0
1086                                                .vnode_bitmap
1087                                                .clone(),
1088                                            splits: reschedule
1089                                                .actor_splits
1090                                                .get(actor_id)
1091                                                .cloned()
1092                                                .unwrap_or_default(),
1093                                        },
1094                                    )
1095                                })
1096                            })
1097                            .collect(),
1098                        reschedule
1099                            .vnode_bitmap_updates
1100                            .iter()
1101                            .filter(|(actor_id, _)| {
1102                                !reschedule.newly_created_actors.contains_key(*actor_id)
1103                            })
1104                            .map(|(actor_id, bitmap)| (*actor_id, bitmap.clone()))
1105                            .collect(),
1106                        reschedule.actor_splits.clone(),
1107                    );
1108                }
1109
1110                let (table_ids, node_actors) = self.collect_base_info();
1111
1112                // Actors to create
1113                let actors_to_create = Some(Command::reschedule_actors_to_create(
1114                    reschedules,
1115                    fragment_actors,
1116                    &self.database_info,
1117                    partial_graph_manager.control_stream_manager(),
1118                ));
1119
1120                // Post-apply: remove old actors
1121                self.database_info
1122                    .post_apply_reschedules(reschedules.iter().map(|(fragment_id, reschedule)| {
1123                        (
1124                            *fragment_id,
1125                            reschedule.removed_actors.iter().cloned().collect(),
1126                        )
1127                    }));
1128
1129                // Mutation
1130                let mutation = Command::reschedule_to_mutation(
1131                    reschedules,
1132                    fragment_actors,
1133                    partial_graph_manager.control_stream_manager(),
1134                    &mut self.database_info,
1135                )?;
1136
1137                let reschedules = reschedule_plan
1138                    .expect("reschedule intent should be resolved in global barrier worker")
1139                    .reschedules;
1140                (
1141                    mutation,
1142                    table_ids,
1143                    actors_to_create,
1144                    node_actors,
1145                    PostCollectCommand::Reschedule { reschedules },
1146                )
1147            }
1148
1149            Some(Command::ReplaceStreamJob(plan)) => {
1150                let ensembles = resolve_no_shuffle_ensembles(
1151                    &plan.new_fragments,
1152                    &plan.upstream_fragment_downstreams,
1153                )?;
1154                let mut render_result = render_actors(
1155                    &plan.new_fragments,
1156                    &self.database_info,
1157                    "", // replace jobs don't need mview definition
1158                    &plan.new_fragments.inner.ctx,
1159                    &plan.streaming_job_model,
1160                    partial_graph_manager
1161                        .control_stream_manager()
1162                        .env
1163                        .actor_id_generator(),
1164                    worker_nodes,
1165                    &ensembles,
1166                    &plan.database_resource_group,
1167                )?;
1168
1169                // Render actors for auto_refresh_schema_sinks.
1170                // Each sink's new_fragment inherits parallelism from its original_fragment.
1171                if let Some(sinks) = &plan.auto_refresh_schema_sinks {
1172                    let actor_id_counter = partial_graph_manager
1173                        .control_stream_manager()
1174                        .env
1175                        .actor_id_generator();
1176                    for sink_ctx in sinks {
1177                        let original_fragment_id = sink_ctx.original_fragment.fragment_id;
1178                        let original_frag_info = self.database_info.fragment(original_fragment_id);
1179                        let actor_template = EnsembleActorTemplate::from_existing_inflight_fragment(
1180                            original_frag_info,
1181                        );
1182                        let new_aligner = ComponentFragmentAligner::new_persistent(
1183                            &actor_template,
1184                            actor_id_counter,
1185                        );
1186                        let distribution_type: DistributionType =
1187                            sink_ctx.new_fragment.distribution_type.into();
1188                        let actor_assignments =
1189                            new_aligner.align_component_actor(distribution_type);
1190                        let new_fragment_id = sink_ctx.new_fragment.fragment_id;
1191                        let mut actors = Vec::with_capacity(actor_assignments.len());
1192                        for (&actor_id, (worker_id, vnode_bitmap)) in &actor_assignments {
1193                            render_result.actor_location.insert(actor_id, *worker_id);
1194                            actors.push(StreamActor {
1195                                actor_id,
1196                                fragment_id: new_fragment_id,
1197                                vnode_bitmap: vnode_bitmap.clone(),
1198                                mview_definition: String::new(),
1199                                expr_context: Some(sink_ctx.ctx.to_expr_context()),
1200                                config_override: sink_ctx.ctx.config_override.clone(),
1201                            });
1202                        }
1203                        render_result.stream_actors.insert(new_fragment_id, actors);
1204                    }
1205                }
1206
1207                // Build edges first (needed for no-shuffle mapping used in split resolution)
1208                let mut edges = self.database_info.build_edge(
1209                    None,
1210                    Some(&plan),
1211                    None,
1212                    partial_graph_manager.control_stream_manager(),
1213                    &render_result.stream_actors,
1214                    &render_result.actor_location,
1215                );
1216
1217                // Phase 2: Resolve splits to actor-level assignment.
1218                let fragment_actor_ids: HashMap<FragmentId, Vec<ActorId>> = render_result
1219                    .stream_actors
1220                    .iter()
1221                    .map(|(fragment_id, actors)| {
1222                        (
1223                            *fragment_id,
1224                            actors.iter().map(|a| a.actor_id).collect::<Vec<_>>(),
1225                        )
1226                    })
1227                    .collect();
1228                let resolved_split_assignment = match &plan.split_plan {
1229                    ReplaceJobSplitPlan::Discovered(discovered) => {
1230                        SourceManager::resolve_fragment_to_actor_splits(
1231                            &plan.new_fragments,
1232                            discovered,
1233                            &fragment_actor_ids,
1234                        )?
1235                    }
1236                    ReplaceJobSplitPlan::AlignFromPrevious => {
1237                        SourceManager::resolve_replace_source_splits(
1238                            &plan.new_fragments,
1239                            &plan.replace_upstream,
1240                            edges.actor_new_no_shuffle(),
1241                            |_fragment_id, actor_id| {
1242                                self.database_info.fragment_infos().find_map(|fragment| {
1243                                    fragment
1244                                        .actors
1245                                        .get(&actor_id)
1246                                        .map(|info| info.splits.clone())
1247                                })
1248                            },
1249                        )?
1250                    }
1251                };
1252
1253                // Pre-apply: add new fragments and replace upstream
1254                self.database_info.pre_apply_new_fragments(
1255                    plan.new_fragments
1256                        .new_fragment_info(
1257                            &render_result.stream_actors,
1258                            &render_result.actor_location,
1259                            &resolved_split_assignment,
1260                        )
1261                        .map(|(fragment_id, new_fragment)| {
1262                            (fragment_id, plan.streaming_job.id(), new_fragment)
1263                        }),
1264                );
1265                for (fragment_id, replace_map) in &plan.replace_upstream {
1266                    self.database_info
1267                        .pre_apply_replace_node_upstream(*fragment_id, replace_map);
1268                }
1269                if let Some(sinks) = &plan.auto_refresh_schema_sinks {
1270                    self.database_info
1271                        .pre_apply_new_fragments(sinks.iter().map(|sink| {
1272                            (
1273                                sink.new_fragment.fragment_id,
1274                                sink.original_sink.id.as_job_id(),
1275                                sink.new_fragment_info(
1276                                    &render_result.stream_actors,
1277                                    &render_result.actor_location,
1278                                ),
1279                            )
1280                        }));
1281                }
1282
1283                let (table_ids, node_actors) = self.collect_base_info();
1284
1285                // Actors to create
1286                let actors_to_create = Some(Command::replace_stream_job_actors_to_create(
1287                    &plan,
1288                    &mut edges,
1289                    &self.database_info,
1290                    &render_result.stream_actors,
1291                    &render_result.actor_location,
1292                ));
1293
1294                // Mutation (must be generated before removing old fragments,
1295                // because it reads actor info from database_info)
1296                let mutation = Command::replace_stream_job_to_mutation(
1297                    &plan,
1298                    &mut edges,
1299                    &mut self.database_info,
1300                    &resolved_split_assignment,
1301                )?;
1302
1303                // Post-apply: remove old fragments
1304                {
1305                    let mut fragment_ids_to_remove: Vec<_> = plan
1306                        .old_fragments
1307                        .fragments
1308                        .values()
1309                        .map(|f| f.fragment_id)
1310                        .collect();
1311                    if let Some(sinks) = &plan.auto_refresh_schema_sinks {
1312                        fragment_ids_to_remove
1313                            .extend(sinks.iter().map(|sink| sink.original_fragment.fragment_id));
1314                    }
1315                    self.database_info
1316                        .post_apply_remove_fragments(fragment_ids_to_remove);
1317                }
1318
1319                (
1320                    mutation,
1321                    table_ids,
1322                    actors_to_create,
1323                    node_actors,
1324                    PostCollectCommand::ReplaceStreamJob {
1325                        plan,
1326                        resolved_split_assignment,
1327                    },
1328                )
1329            }
1330
1331            Some(Command::SourceChangeSplit(split_state)) => {
1332                // Pre-apply: split assignments
1333                self.database_info.pre_apply_split_assignments(
1334                    split_state
1335                        .split_assignment
1336                        .iter()
1337                        .map(|(&fragment_id, splits)| (fragment_id, splits.clone())),
1338                );
1339
1340                let mutation = Some(Command::source_change_split_to_mutation(
1341                    &split_state.split_assignment,
1342                ));
1343                let (table_ids, node_actors) = self.collect_base_info();
1344                (
1345                    mutation,
1346                    table_ids,
1347                    None,
1348                    node_actors,
1349                    PostCollectCommand::SourceChangeSplit {
1350                        split_assignment: split_state.split_assignment,
1351                    },
1352                )
1353            }
1354
1355            Some(Command::CreateSubscription {
1356                subscription_id,
1357                upstream_mv_table_id,
1358                retention_second,
1359            }) => {
1360                self.database_info.register_subscriber(
1361                    upstream_mv_table_id.as_job_id(),
1362                    subscription_id.as_subscriber_id(),
1363                    SubscriberType::Subscription(retention_second),
1364                );
1365                let mutation = Some(Command::create_subscription_to_mutation(
1366                    upstream_mv_table_id,
1367                    subscription_id,
1368                ));
1369                let (table_ids, node_actors) = self.collect_base_info();
1370                (
1371                    mutation,
1372                    table_ids,
1373                    None,
1374                    node_actors,
1375                    PostCollectCommand::CreateSubscription { subscription_id },
1376                )
1377            }
1378
1379            Some(Command::DropSubscription {
1380                subscription_id,
1381                upstream_mv_table_id,
1382            }) => {
1383                if self
1384                    .database_info
1385                    .unregister_subscriber(
1386                        upstream_mv_table_id.as_job_id(),
1387                        subscription_id.as_subscriber_id(),
1388                    )
1389                    .is_none()
1390                {
1391                    warn!(%subscription_id, %upstream_mv_table_id, "no subscription to drop");
1392                }
1393                let mutation = Some(Command::drop_subscription_to_mutation(
1394                    upstream_mv_table_id,
1395                    subscription_id,
1396                ));
1397                let (table_ids, node_actors) = self.collect_base_info();
1398                (
1399                    mutation,
1400                    table_ids,
1401                    None,
1402                    node_actors,
1403                    PostCollectCommand::Command("DropSubscription".to_owned()),
1404                )
1405            }
1406
1407            Some(Command::AlterSubscriptionRetention {
1408                subscription_id,
1409                upstream_mv_table_id,
1410                retention_second,
1411            }) => {
1412                self.database_info.update_subscription_retention(
1413                    upstream_mv_table_id.as_job_id(),
1414                    subscription_id.as_subscriber_id(),
1415                    retention_second,
1416                );
1417                self.apply_simple_command(None, "AlterSubscriptionRetention")
1418            }
1419
1420            Some(Command::ConnectorPropsChange(config)) => {
1421                let mutation = Some(Command::connector_props_change_to_mutation(&config));
1422                let (table_ids, node_actors) = self.collect_base_info();
1423                (
1424                    mutation,
1425                    table_ids,
1426                    None,
1427                    node_actors,
1428                    PostCollectCommand::ConnectorPropsChange(config),
1429                )
1430            }
1431
1432            Some(Command::Refresh {
1433                table_id,
1434                associated_source_id,
1435            }) => {
1436                let mutation = Some(Command::refresh_to_mutation(table_id, associated_source_id));
1437                self.apply_simple_command(mutation, "Refresh")
1438            }
1439
1440            Some(Command::ListFinish {
1441                table_id: _,
1442                associated_source_id,
1443            }) => {
1444                let mutation = Some(Command::list_finish_to_mutation(associated_source_id));
1445                self.apply_simple_command(mutation, "ListFinish")
1446            }
1447
1448            Some(Command::LoadFinish {
1449                table_id: _,
1450                associated_source_id,
1451            }) => {
1452                let mutation = Some(Command::load_finish_to_mutation(associated_source_id));
1453                self.apply_simple_command(mutation, "LoadFinish")
1454            }
1455
1456            Some(Command::ResetSource { source_id }) => {
1457                let mutation = Some(Command::reset_source_to_mutation(source_id));
1458                self.apply_simple_command(mutation, "ResetSource")
1459            }
1460
1461            Some(Command::ResumeBackfill { target }) => {
1462                let mutation = Command::resume_backfill_to_mutation(&target, &self.database_info)?;
1463                let (table_ids, node_actors) = self.collect_base_info();
1464                (
1465                    mutation,
1466                    table_ids,
1467                    None,
1468                    node_actors,
1469                    PostCollectCommand::ResumeBackfill { target },
1470                )
1471            }
1472
1473            Some(Command::InjectSourceOffsets {
1474                source_id,
1475                split_offsets,
1476            }) => {
1477                let mutation = Some(Command::inject_source_offsets_to_mutation(
1478                    source_id,
1479                    &split_offsets,
1480                ));
1481                self.apply_simple_command(mutation, "InjectSourceOffsets")
1482            }
1483        };
1484
1485        let mut finished_snapshot_backfill_jobs = HashSet::new();
1486        let mutation = match mutation {
1487            Some(mutation) => Some(mutation),
1488            None => {
1489                let mut finished_snapshot_backfill_job_info = HashMap::new();
1490                if barrier_info.kind.is_checkpoint() {
1491                    for (&job_id, job) in &mut self.independent_checkpoint_job_controls {
1492                        if let IndependentCheckpointJobControl::CreatingStreamingJob(creating_job) =
1493                            job
1494                            && creating_job.should_merge_to_upstream(partial_graph_manager)
1495                        {
1496                            // The independent actors will stop on this barrier. Apply throttle to
1497                            // the in-memory plan used to create the database-graph actors, and let
1498                            // the database barrier own the collection notification.
1499                            if throttle_config
1500                                .as_mut()
1501                                .and_then(|config| creating_job.pre_apply_throttle(config))
1502                                .is_some()
1503                            {
1504                                notify_database_graph = true;
1505                            }
1506                            let info = creating_job
1507                                .start_consume_upstream(partial_graph_manager, &barrier_info)?;
1508                            finished_snapshot_backfill_job_info
1509                                .try_insert(job_id, info)
1510                                .expect("non-duplicated");
1511                        }
1512                    }
1513                }
1514
1515                if !finished_snapshot_backfill_job_info.is_empty() {
1516                    let actors_to_create = actors_to_create.get_or_insert_default();
1517                    let mut subscriptions_to_drop = vec![];
1518                    let mut dispatcher_update = vec![];
1519                    let mut actor_splits = HashMap::new();
1520                    for (job_id, info) in finished_snapshot_backfill_job_info {
1521                        finished_snapshot_backfill_jobs.insert(job_id);
1522                        subscriptions_to_drop.extend(
1523                            info.snapshot_backfill_upstream_tables.iter().map(
1524                                |upstream_table_id| PbSubscriptionUpstreamInfo {
1525                                    subscriber_id: job_id.as_subscriber_id(),
1526                                    upstream_mv_table_id: *upstream_table_id,
1527                                },
1528                            ),
1529                        );
1530                        for upstream_mv_table_id in &info.snapshot_backfill_upstream_tables {
1531                            assert_matches!(
1532                                self.database_info.unregister_subscriber(
1533                                    upstream_mv_table_id.as_job_id(),
1534                                    job_id.as_subscriber_id()
1535                                ),
1536                                Some(SubscriberType::SnapshotBackfill)
1537                            );
1538                        }
1539
1540                        table_ids_to_commit.extend(
1541                            info.fragment_infos
1542                                .values()
1543                                .flat_map(|fragment| fragment.state_table_ids.iter())
1544                                .copied(),
1545                        );
1546
1547                        let actor_len = info
1548                            .fragment_infos
1549                            .values()
1550                            .map(|fragment| fragment.actors.len() as u64)
1551                            .sum();
1552                        let id_gen = GlobalActorIdGen::new(
1553                            partial_graph_manager
1554                                .control_stream_manager()
1555                                .env
1556                                .actor_id_generator(),
1557                            actor_len,
1558                        );
1559                        let mut next_local_actor_id = 0;
1560                        // mapping from old_actor_id to new_actor_id
1561                        let actor_mapping: HashMap<_, _> = info
1562                            .fragment_infos
1563                            .values()
1564                            .flat_map(|fragment| fragment.actors.keys())
1565                            .map(|old_actor_id| {
1566                                let new_actor_id = id_gen.to_global_id(next_local_actor_id);
1567                                next_local_actor_id += 1;
1568                                (*old_actor_id, new_actor_id.as_global_id())
1569                            })
1570                            .collect();
1571                        let actor_mapping = &actor_mapping;
1572                        let new_stream_actors: HashMap<_, _> = info
1573                            .stream_actors
1574                            .into_iter()
1575                            .map(|(old_actor_id, mut actor)| {
1576                                let new_actor_id = actor_mapping[&old_actor_id];
1577                                actor.actor_id = new_actor_id;
1578                                (new_actor_id, actor)
1579                            })
1580                            .collect();
1581                        let new_fragment_info: HashMap<_, _> = info
1582                            .fragment_infos
1583                            .into_iter()
1584                            .map(|(fragment_id, mut fragment)| {
1585                                let actors = take(&mut fragment.actors);
1586                                fragment.actors = actors
1587                                    .into_iter()
1588                                    .map(|(old_actor_id, actor)| {
1589                                        let new_actor_id = actor_mapping[&old_actor_id];
1590                                        (new_actor_id, actor)
1591                                    })
1592                                    .collect();
1593                                (fragment_id, fragment)
1594                            })
1595                            .collect();
1596                        actor_splits.extend(
1597                            new_fragment_info
1598                                .values()
1599                                .flat_map(|fragment| &fragment.actors)
1600                                .map(|(actor_id, actor)| {
1601                                    (
1602                                        *actor_id,
1603                                        ConnectorSplits {
1604                                            splits: actor
1605                                                .splits
1606                                                .iter()
1607                                                .map(ConnectorSplit::from)
1608                                                .collect(),
1609                                        },
1610                                    )
1611                                }),
1612                        );
1613                        // new actors belong to the database partial graph
1614                        let partial_graph_id = to_partial_graph_id(self.database_id, None);
1615                        let mut edge_builder = FragmentEdgeBuilder::new(
1616                            info.upstream_fragment_downstreams
1617                                .keys()
1618                                .map(|upstream_fragment_id| {
1619                                    self.database_info.fragment(*upstream_fragment_id)
1620                                })
1621                                .chain(new_fragment_info.values())
1622                                .map(|fragment| {
1623                                    (
1624                                        fragment.fragment_id,
1625                                        EdgeBuilderFragmentInfo::from_inflight(
1626                                            fragment,
1627                                            partial_graph_id,
1628                                            partial_graph_manager.control_stream_manager(),
1629                                        ),
1630                                    )
1631                                }),
1632                        );
1633                        edge_builder.add_relations(&info.upstream_fragment_downstreams);
1634                        edge_builder.add_relations(&info.downstreams);
1635                        let mut edges = edge_builder.build();
1636                        let new_actors_to_create = edges.collect_actors_to_create(
1637                            new_fragment_info.values().map(|fragment| {
1638                                (
1639                                    fragment.fragment_id,
1640                                    &fragment.nodes,
1641                                    fragment.actors.iter().map(|(actor_id, actor)| {
1642                                        (&new_stream_actors[actor_id], actor.worker_id)
1643                                    }),
1644                                    [], // no initial subscriber for backfilling job
1645                                )
1646                            }),
1647                        );
1648                        dispatcher_update.extend(
1649                            info.upstream_fragment_downstreams.keys().flat_map(
1650                                |upstream_fragment_id| {
1651                                    let new_actor_dispatchers = edges
1652                                        .dispatchers
1653                                        .remove(upstream_fragment_id)
1654                                        .expect("should exist");
1655                                    new_actor_dispatchers.into_iter().flat_map(
1656                                        |(upstream_actor_id, dispatchers)| {
1657                                            dispatchers.into_iter().map(move |dispatcher| {
1658                                                PbDispatcherUpdate {
1659                                                    actor_id: upstream_actor_id,
1660                                                    dispatcher_id: dispatcher.dispatcher_id,
1661                                                    hash_mapping: dispatcher.hash_mapping,
1662                                                    removed_downstream_actor_id: dispatcher
1663                                                        .downstream_actor_id
1664                                                        .iter()
1665                                                        .map(|new_downstream_actor_id| {
1666                                                            actor_mapping
1667                                                            .iter()
1668                                                            .find_map(
1669                                                                |(old_actor_id, new_actor_id)| {
1670                                                                    (new_downstream_actor_id
1671                                                                        == new_actor_id)
1672                                                                        .then_some(*old_actor_id)
1673                                                                },
1674                                                            )
1675                                                            .expect("should exist")
1676                                                        })
1677                                                        .collect(),
1678                                                    added_downstream_actor_id: dispatcher
1679                                                        .downstream_actor_id,
1680                                                }
1681                                            })
1682                                        },
1683                                    )
1684                                },
1685                            ),
1686                        );
1687                        assert!(edges.is_empty(), "remaining edges: {:?}", edges);
1688                        for (worker_id, worker_actors) in new_actors_to_create {
1689                            node_actors.entry(worker_id).or_default().extend(
1690                                worker_actors.values().flat_map(|(_, actors, _)| {
1691                                    actors.iter().map(|(actor, _, _)| actor.actor_id)
1692                                }),
1693                            );
1694                            actors_to_create
1695                                .entry(worker_id)
1696                                .or_default()
1697                                .extend(worker_actors);
1698                        }
1699                        self.database_info.add_existing(InflightStreamingJobInfo {
1700                            job_id,
1701                            fragment_infos: new_fragment_info,
1702                            subscribers: Default::default(), // no initial subscribers for newly created snapshot backfill
1703                            status: CreateStreamingJobStatus::Created,
1704                            cdc_table_backfill_tracker: None, // no cdc table backfill for snapshot backfill
1705                        });
1706                    }
1707
1708                    Some(PbMutation::Update(PbUpdateMutation {
1709                        dispatcher_update,
1710                        merge_update: vec![], // no upstream update on existing actors
1711                        actor_vnode_bitmap_update: Default::default(), /* no in place update vnode bitmap happened */
1712                        dropped_actors: vec![], /* no actors to drop in the partial graph of database */
1713                        actor_splits,
1714                        actor_new_dispatchers: Default::default(), // no new dispatcher
1715                        actor_cdc_table_snapshot_splits: None, /* no cdc table backfill in snapshot backfill */
1716                        sink_schema_change: Default::default(), /* no sink auto schema change happened here */
1717                        subscriptions_to_drop,
1718                    }))
1719                } else {
1720                    let fragment_ids = self.database_info.take_pending_backfill_nodes();
1721                    if fragment_ids.is_empty() {
1722                        None
1723                    } else {
1724                        Some(PbMutation::StartFragmentBackfill(
1725                            PbStartFragmentBackfillMutation { fragment_ids },
1726                        ))
1727                    }
1728                }
1729            }
1730        };
1731
1732        // Forward barrier to independent job controls
1733        for (job_id, job) in &mut self.independent_checkpoint_job_controls {
1734            match job {
1735                IndependentCheckpointJobControl::CreatingStreamingJob(creating_job) => {
1736                    if finished_snapshot_backfill_jobs.contains(job_id) {
1737                        continue;
1738                    }
1739                    let throttle_mutation = throttle_config.as_mut().and_then(|config| {
1740                        creating_job
1741                            .pre_apply_throttle(config)
1742                            .map(|mutation| (mutation, notifier.as_mut()))
1743                    });
1744                    creating_job.on_new_upstream_barrier(
1745                        partial_graph_manager,
1746                        &barrier_info,
1747                        throttle_mutation,
1748                    )?;
1749                }
1750                IndependentCheckpointJobControl::BatchRefresh(batch_refresh_job) => {
1751                    let throttle_mutation = throttle_config.as_mut().and_then(|config| {
1752                        batch_refresh_job
1753                            .pre_apply_throttle(config)
1754                            .map(|mutation| (mutation, notifier.as_mut()))
1755                    });
1756                    batch_refresh_job.on_new_upstream_barrier(
1757                        partial_graph_manager,
1758                        &barrier_info,
1759                        throttle_mutation,
1760                    )?;
1761                }
1762            }
1763        }
1764
1765        let database_notifier = if notify_database_graph {
1766            notifier.as_mut()
1767        } else {
1768            None
1769        };
1770        partial_graph_manager.inject_barrier(
1771            to_partial_graph_id(self.database_id, None),
1772            mutation,
1773            None,
1774            &node_actors,
1775            InflightFragmentInfo::existing_table_ids(self.database_info.fragment_infos()),
1776            InflightFragmentInfo::workers(self.database_info.fragment_infos()),
1777            actors_to_create,
1778            PartialGraphBarrierInfo::new(
1779                post_collect_command,
1780                barrier_info,
1781                database_notifier,
1782                table_ids_to_commit,
1783            ),
1784        )?;
1785
1786        // Publish the collection receivers only after all parts of a scheduled command have been
1787        // dispatched successfully. Periodic barriers do not have a notifier.
1788        if let Some(notifier) = notifier.take() {
1789            notifier.started();
1790        }
1791
1792        Ok(ApplyCommandInfo {
1793            jobs_to_wait: finished_snapshot_backfill_jobs,
1794        })
1795    }
1796}