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