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