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