Skip to main content

risingwave_meta/barrier/checkpoint/
state.rs

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