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