Skip to main content

risingwave_meta/barrier/
command.rs

1// Copyright 2022 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::collections::{HashMap, HashSet};
16use std::fmt::{Display, Formatter};
17
18use itertools::Itertools;
19use risingwave_common::bitmap::Bitmap;
20use risingwave_common::catalog::{DatabaseId, TableId};
21use risingwave_common::hash::{ActorMapping, VnodeCountCompat};
22use risingwave_common::id::{JobId, SinkId, SourceId};
23use risingwave_common::must_match;
24use risingwave_connector::source::{CdcTableSnapshotSplitRaw, SplitImpl};
25use risingwave_hummock_sdk::change_log::build_table_change_log_delta;
26use risingwave_hummock_sdk::vector_index::VectorIndexDelta;
27use risingwave_meta_model::{DispatcherType, WorkerId, fragment_relation, streaming_job};
28use risingwave_pb::catalog::CreateType;
29use risingwave_pb::common::PbActorInfo;
30use risingwave_pb::hummock::vector_index_delta::PbVectorIndexInit;
31use risingwave_pb::plan_common::{ColumnCatalog as PbColumnCatalog, PbField};
32use risingwave_pb::source::{
33    ConnectorSplit, ConnectorSplits, PbCdcTableSnapshotSplits,
34    PbCdcTableSnapshotSplitsWithGeneration,
35};
36use risingwave_pb::stream_plan::add_mutation::PbNewUpstreamSink;
37use risingwave_pb::stream_plan::barrier::BarrierKind as PbBarrierKind;
38use risingwave_pb::stream_plan::barrier_mutation::Mutation;
39use risingwave_pb::stream_plan::connector_props_change_mutation::ConnectorPropsInfo;
40use risingwave_pb::stream_plan::sink_schema_change::Op as PbSinkSchemaChangeOp;
41use risingwave_pb::stream_plan::throttle_mutation::ThrottleConfig;
42use risingwave_pb::stream_plan::update_mutation::{DispatcherUpdate, MergeUpdate};
43use risingwave_pb::stream_plan::{
44    AddMutation, ConnectorPropsChangeMutation, Dispatcher, Dispatchers, DropSubscriptionsMutation,
45    ListFinishMutation, LoadFinishMutation, PauseMutation, PbSinkAddColumnsOp, PbSinkDropColumnsOp,
46    PbSinkSchemaChange, PbStreamNode, PbUpstreamSinkInfo, ResumeMutation,
47    SourceChangeSplitMutation, StartFragmentBackfillMutation, StopMutation,
48    SubscriptionUpstreamInfo, ThrottleMutation, UpdateMutation,
49};
50use risingwave_pb::stream_service::BarrierCompleteResponse;
51use tracing::warn;
52
53use super::info::InflightDatabaseInfo;
54use crate::barrier::backfill_order_control::get_nodes_with_backfill_dependencies;
55use crate::barrier::complete_task::CompleteBarrierTask;
56use crate::barrier::edge_builder::FragmentEdgeBuildResult;
57use crate::barrier::partial_graph::PartialGraphBarrierInfo;
58use crate::barrier::rpc::{ControlStreamManager, to_partial_graph_id};
59use crate::barrier::utils::{collect_new_vector_index_info, collect_resp_info};
60use crate::controller::fragment::{InflightActorInfo, InflightFragmentInfo};
61use crate::controller::scale::LoadedFragmentContext;
62use crate::controller::utils::StreamingJobExtraInfo;
63use crate::hummock::NewTableFragmentInfo;
64use crate::manager::{StreamingJob, StreamingJobType};
65use crate::model::{
66    ActorId, ActorUpstreams, DispatcherId, FragmentActorDispatchers, FragmentDownstreamRelation,
67    FragmentId, FragmentReplaceUpstream, StreamActor, StreamActorWithDispatchers,
68    StreamJobActorsToCreate, StreamJobFragments, StreamJobFragmentsToCreate, SubscriptionId,
69};
70use crate::stream::{
71    AutoRefreshSchemaSinkContext, ConnectorPropsChange, ExtendedFragmentBackfillOrder,
72    ReplaceJobSplitPlan, SourceSplitAssignment, SplitAssignment, SplitState, UpstreamSinkInfo,
73    build_actor_connector_splits,
74};
75use crate::{MetaError, MetaResult};
76
77pub(crate) type ThrottleConfigMap = HashMap<FragmentId, (ThrottleConfig, PbStreamNode)>;
78
79pub(crate) fn extract_throttle_config(
80    config: &mut ThrottleConfigMap,
81    mut apply: impl FnMut(FragmentId, &PbStreamNode) -> bool,
82) -> Option<Mutation> {
83    let fragment_throttle = config
84        .extract_if(|fragment_id, (_, stream_node)| apply(*fragment_id, stream_node))
85        .map(|(fragment_id, (throttle_config, _))| (fragment_id, throttle_config))
86        .collect::<HashMap<_, _>>();
87    (!fragment_throttle.is_empty())
88        .then_some(Mutation::Throttle(ThrottleMutation { fragment_throttle }))
89}
90
91/// [`Reschedule`] describes per-fragment changes in a resolved reschedule plan,
92/// used for actor scaling or migration.
93#[derive(Debug, Clone)]
94pub struct Reschedule {
95    /// Added actors in this fragment.
96    pub added_actors: HashMap<WorkerId, Vec<ActorId>>,
97
98    /// Removed actors in this fragment.
99    pub removed_actors: HashSet<ActorId>,
100
101    /// Vnode bitmap updates for some actors in this fragment.
102    pub vnode_bitmap_updates: HashMap<ActorId, Bitmap>,
103
104    /// The upstream fragments of this fragment, and the dispatchers that should be updated.
105    pub upstream_fragment_dispatcher_ids: Vec<(FragmentId, DispatcherId)>,
106    /// New hash mapping of the upstream dispatcher to be updated.
107    ///
108    /// This field exists only when there's upstream fragment and the current fragment is
109    /// hash-sharded.
110    pub upstream_dispatcher_mapping: Option<ActorMapping>,
111
112    /// The downstream fragments of this fragment.
113    pub downstream_fragment_ids: Vec<FragmentId>,
114
115    /// Reassigned splits for source actors.
116    /// It becomes the `actor_splits` in [`UpdateMutation`].
117    /// `Source` and `SourceBackfill` are handled together here.
118    pub actor_splits: HashMap<ActorId, Vec<SplitImpl>>,
119
120    pub newly_created_actors: HashMap<ActorId, (StreamActorWithDispatchers, WorkerId)>,
121}
122
123#[derive(Debug, Clone)]
124pub struct ReschedulePlan {
125    pub reschedules: HashMap<FragmentId, Reschedule>,
126    /// Should contain the actor ids in upstream and downstream fragments referenced by
127    /// `reschedules`.
128    pub fragment_actors: HashMap<FragmentId, HashSet<ActorId>>,
129}
130
131/// Preloaded context for rescheduling, built outside the barrier worker.
132#[derive(Debug, Clone)]
133pub struct RescheduleContext {
134    pub loaded: LoadedFragmentContext,
135    pub job_extra_info: HashMap<JobId, StreamingJobExtraInfo>,
136    pub upstream_fragments: HashMap<FragmentId, HashMap<FragmentId, DispatcherType>>,
137    pub downstream_fragments: HashMap<FragmentId, HashMap<FragmentId, DispatcherType>>,
138    pub downstream_relations: HashMap<(FragmentId, FragmentId), fragment_relation::Model>,
139}
140
141impl RescheduleContext {
142    pub fn empty() -> Self {
143        Self {
144            loaded: LoadedFragmentContext::default(),
145            job_extra_info: HashMap::new(),
146            upstream_fragments: HashMap::new(),
147            downstream_fragments: HashMap::new(),
148            downstream_relations: HashMap::new(),
149        }
150    }
151
152    pub fn is_empty(&self) -> bool {
153        self.loaded.is_empty()
154    }
155
156    pub fn for_database(&self, database_id: DatabaseId) -> Option<Self> {
157        let loaded = self.loaded.for_database(database_id)?;
158        let job_ids: HashSet<JobId> = loaded.job_map.keys().copied().collect();
159        // Use the filtered loaded context as the source of truth so every side map is pruned by
160        // the same fragment set.
161        let fragment_ids: HashSet<FragmentId> = loaded
162            .job_fragments
163            .values()
164            .flat_map(|fragments| fragments.keys().copied())
165            .collect();
166
167        let job_extra_info = self
168            .job_extra_info
169            .iter()
170            .filter(|(job_id, _)| job_ids.contains(*job_id))
171            .map(|(job_id, info)| (*job_id, info.clone()))
172            .collect();
173
174        let upstream_fragments = self
175            .upstream_fragments
176            .iter()
177            .filter(|(fragment_id, _)| fragment_ids.contains(*fragment_id))
178            .map(|(fragment_id, upstreams)| (*fragment_id, upstreams.clone()))
179            .collect();
180
181        let downstream_fragments = self
182            .downstream_fragments
183            .iter()
184            .filter(|(fragment_id, _)| fragment_ids.contains(*fragment_id))
185            .map(|(fragment_id, downstreams)| (*fragment_id, downstreams.clone()))
186            .collect();
187
188        let downstream_relations = self
189            .downstream_relations
190            .iter()
191            // Ownership of this map is source-fragment based. We keep all downstream edges for
192            // selected sources because the target side can still be referenced during dispatcher
193            // reconstruction even if that target fragment is not being rescheduled.
194            .filter(|((source_fragment_id, _), _)| fragment_ids.contains(source_fragment_id))
195            .map(|(key, relation)| (*key, relation.clone()))
196            .collect();
197
198        Some(Self {
199            loaded,
200            job_extra_info,
201            upstream_fragments,
202            downstream_fragments,
203            downstream_relations,
204        })
205    }
206
207    /// Split this context into per-database contexts without cloning the large loaded graph
208    /// payloads.
209    pub fn into_database_contexts(self) -> HashMap<DatabaseId, Self> {
210        let Self {
211            loaded,
212            job_extra_info,
213            upstream_fragments,
214            downstream_fragments,
215            downstream_relations,
216        } = self;
217
218        let mut contexts: HashMap<_, _> = loaded
219            .into_database_contexts()
220            .into_iter()
221            .map(|(database_id, loaded)| {
222                (
223                    database_id,
224                    Self {
225                        loaded,
226                        job_extra_info: HashMap::new(),
227                        upstream_fragments: HashMap::new(),
228                        downstream_fragments: HashMap::new(),
229                        downstream_relations: HashMap::new(),
230                    },
231                )
232            })
233            .collect();
234
235        if contexts.is_empty() {
236            return contexts;
237        }
238
239        let mut job_databases = HashMap::new();
240        let mut fragment_databases = HashMap::new();
241        for (&database_id, context) in &contexts {
242            for job_id in context.loaded.job_map.keys().copied() {
243                job_databases.insert(job_id, database_id);
244            }
245            for fragment_id in context
246                .loaded
247                .job_fragments
248                .values()
249                .flat_map(|fragments| fragments.keys().copied())
250            {
251                fragment_databases.insert(fragment_id, database_id);
252            }
253        }
254
255        for (job_id, info) in job_extra_info {
256            if let Some(database_id) = job_databases.get(&job_id).copied() {
257                contexts
258                    .get_mut(&database_id)
259                    .expect("database context should exist for job")
260                    .job_extra_info
261                    .insert(job_id, info);
262            }
263        }
264
265        for (fragment_id, upstreams) in upstream_fragments {
266            if let Some(database_id) = fragment_databases.get(&fragment_id).copied() {
267                contexts
268                    .get_mut(&database_id)
269                    .expect("database context should exist for fragment")
270                    .upstream_fragments
271                    .insert(fragment_id, upstreams);
272            }
273        }
274
275        for (fragment_id, downstreams) in downstream_fragments {
276            if let Some(database_id) = fragment_databases.get(&fragment_id).copied() {
277                contexts
278                    .get_mut(&database_id)
279                    .expect("database context should exist for fragment")
280                    .downstream_fragments
281                    .insert(fragment_id, downstreams);
282            }
283        }
284
285        for ((source_fragment_id, target_fragment_id), relation) in downstream_relations {
286            // Route by source fragment ownership. A target may be outside of current reschedule
287            // set, but this edge still belongs to the source-side command.
288            if let Some(database_id) = fragment_databases.get(&source_fragment_id).copied() {
289                contexts
290                    .get_mut(&database_id)
291                    .expect("database context should exist for relation source")
292                    .downstream_relations
293                    .insert((source_fragment_id, target_fragment_id), relation);
294            }
295        }
296
297        contexts
298    }
299}
300
301/// Replacing an old job with a new one. All actors in the job will be rebuilt.
302///
303/// Current use cases:
304/// - `ALTER SOURCE` (via [`Command::ReplaceStreamJob`]) will replace a source job's plan.
305/// - `ALTER TABLE` (via [`Command::ReplaceStreamJob`]) and `CREATE SINK INTO table` ([`Command::CreateStreamingJob`])
306///   will replace a table job's plan.
307#[derive(Debug, Clone)]
308pub struct ReplaceStreamJobPlan {
309    pub old_fragments: StreamJobFragments,
310    pub new_fragments: StreamJobFragmentsToCreate,
311    /// The resource group of the database this job belongs to.
312    pub database_resource_group: String,
313    /// Downstream jobs of the replaced job need to update their `Merge` node to
314    /// connect to the new fragment.
315    pub replace_upstream: FragmentReplaceUpstream,
316    pub upstream_fragment_downstreams: FragmentDownstreamRelation,
317    /// Split plan for the replace job. Determines how splits are resolved in Phase 2
318    /// inside the barrier worker.
319    pub split_plan: ReplaceJobSplitPlan,
320    /// The `StreamingJob` info of the table to be replaced. Must be `StreamingJob::Table`
321    pub streaming_job: StreamingJob,
322    /// The `streaming_job::Model` for this job, loaded from meta store.
323    pub streaming_job_model: streaming_job::Model,
324    /// The temporary dummy job fragments id of new table fragment
325    pub tmp_id: JobId,
326    /// The state table ids to be dropped.
327    pub to_drop_state_table_ids: Vec<TableId>,
328    pub auto_refresh_schema_sinks: Option<Vec<AutoRefreshSchemaSinkContext>>,
329}
330
331impl ReplaceStreamJobPlan {
332    /// `old_fragment_id` -> `new_fragment_id`
333    pub fn fragment_replacements(&self) -> HashMap<FragmentId, FragmentId> {
334        let mut fragment_replacements = HashMap::new();
335        for (upstream_fragment_id, new_upstream_fragment_id) in
336            self.replace_upstream.values().flatten()
337        {
338            {
339                let r =
340                    fragment_replacements.insert(*upstream_fragment_id, *new_upstream_fragment_id);
341                if let Some(r) = r {
342                    assert_eq!(
343                        *new_upstream_fragment_id, r,
344                        "one fragment is replaced by multiple fragments"
345                    );
346                }
347            }
348        }
349        fragment_replacements
350    }
351}
352
353#[derive(educe::Educe, Clone)]
354#[educe(Debug)]
355pub struct CreateStreamingJobCommandInfo {
356    #[educe(Debug(ignore))]
357    pub stream_job_fragments: StreamJobFragmentsToCreate,
358    pub upstream_fragment_downstreams: FragmentDownstreamRelation,
359    /// The resource group of the database this job belongs to.
360    pub database_resource_group: String,
361    /// Source-level split assignment (Phase 1). Resolved to actor-level in the barrier worker.
362    pub init_split_assignment: SourceSplitAssignment,
363    pub definition: String,
364    pub job_type: StreamingJobType,
365    pub create_type: CreateType,
366    pub streaming_job: StreamingJob,
367    pub fragment_backfill_ordering: ExtendedFragmentBackfillOrder,
368    pub cdc_table_snapshot_splits: Option<Vec<CdcTableSnapshotSplitRaw>>,
369    pub locality_fragment_state_table_mapping: HashMap<FragmentId, Vec<TableId>>,
370    pub is_serverless: bool,
371    /// The `streaming_job::Model` for this job, loaded from meta store.
372    pub streaming_job_model: streaming_job::Model,
373    /// If set, this create command replaces an existing sink while creating the new sink job.
374    pub replace_sink: Option<SinkId>,
375    /// Batch refresh interval in seconds. If set, the MV uses batch refresh semantics.
376    pub refresh_interval_sec: Option<u64>,
377}
378
379impl StreamJobFragments {
380    /// Build fragment-level info for new fragments, populating actor infos from the
381    /// rendered actors and their locations, and applying split assignment to actor splits.
382    pub(super) fn new_fragment_info<'a>(
383        &'a self,
384        stream_actors: &'a HashMap<FragmentId, Vec<StreamActor>>,
385        actor_location: &'a HashMap<ActorId, WorkerId>,
386        assignment: &'a SplitAssignment,
387    ) -> impl Iterator<Item = (FragmentId, InflightFragmentInfo)> + 'a {
388        self.fragments.values().map(|fragment| {
389            (
390                fragment.fragment_id,
391                InflightFragmentInfo {
392                    fragment_id: fragment.fragment_id,
393                    distribution_type: fragment.distribution_type.into(),
394                    fragment_type_mask: fragment.fragment_type_mask,
395                    vnode_count: fragment.vnode_count(),
396                    nodes: fragment.nodes.clone(),
397                    actors: stream_actors
398                        .get(&fragment.fragment_id)
399                        .into_iter()
400                        .flatten()
401                        .map(|actor| {
402                            (
403                                actor.actor_id,
404                                InflightActorInfo {
405                                    worker_id: actor_location[&actor.actor_id],
406                                    vnode_bitmap: actor.vnode_bitmap.clone(),
407                                    splits: assignment
408                                        .get(&fragment.fragment_id)
409                                        .and_then(|s| s.get(&actor.actor_id))
410                                        .cloned()
411                                        .unwrap_or_default(),
412                                },
413                            )
414                        })
415                        .collect(),
416                    state_table_ids: fragment.state_table_ids.iter().copied().collect(),
417                },
418            )
419        })
420    }
421}
422
423pub type TableLogEpochs = Vec<(Vec<u64>, u64)>;
424pub type UpstreamTableLogEpochs = HashMap<TableId, TableLogEpochs>;
425pub type SinceTimestampResolvedEpoch = (u64, TableLogEpochs);
426
427#[derive(Debug, Clone)]
428pub struct SnapshotBackfillInfo {
429    /// `table_id` -> `Some(snapshot_backfill_epoch)`
430    /// The `snapshot_backfill_epoch` should be None at the beginning, and be filled
431    /// by global barrier worker when handling the command.
432    pub upstream_mv_table_id_to_backfill_epoch: HashMap<TableId, Option<u64>>,
433}
434
435#[derive(Debug, Clone)]
436pub struct SinceEpochInfo {
437    pub provided_since_epoch: u64,
438    pub resolved: Option<SinceTimestampResolvedEpoch>,
439}
440
441#[derive(Debug, Clone)]
442pub struct BatchRefreshInfo {
443    pub snapshot_backfill_info: SnapshotBackfillInfo,
444    pub refresh_interval_sec: u64,
445}
446
447#[derive(Debug, Clone)]
448pub enum CreateStreamingJobType {
449    Normal,
450    SinkIntoTable(UpstreamSinkInfo),
451    SnapshotBackfill {
452        snapshot_backfill_info: SnapshotBackfillInfo,
453        since_epoch: Option<SinceEpochInfo>,
454    },
455    BatchRefresh(BatchRefreshInfo),
456}
457
458/// [`Command`] is the input of [`crate::barrier::worker::GlobalBarrierWorker`]. For different commands,
459/// it will build different barriers to send via corresponding `*_to_mutation` helpers,
460/// and may [do different stuffs after the barrier is collected](PostCollectCommand::post_collect).
461// FIXME: this enum is significantly large on stack, box it
462#[derive(Debug)]
463pub enum Command {
464    /// `Flush` command will generate a checkpoint barrier. After the barrier is collected and committed
465    /// all messages before the checkpoint barrier should have been committed.
466    Flush,
467
468    /// `Pause` command generates a `Pause` barrier **only if**
469    /// the cluster is not already paused. Otherwise, a barrier with no mutation will be generated.
470    Pause,
471
472    /// `Resume` command generates a `Resume` barrier **only
473    /// if** the cluster is paused with the same reason. Otherwise, a barrier with no mutation
474    /// will be generated.
475    Resume,
476
477    /// `DropStreamingJobs` command generates a `Stop` barrier to stop the given
478    /// [`Vec<ActorId>`]. The catalog has ensured that these streaming jobs are safe to be
479    /// dropped by reference counts before.
480    ///
481    /// Barriers from the actors to be dropped will STILL be collected.
482    /// After the barrier is collected, it notifies the local stream manager of compute nodes to
483    /// drop actors, and then delete the job fragments info from meta store.
484    DropStreamingJobs {
485        streaming_job_ids: HashSet<JobId>,
486        /// Used by recovery quick path when draining buffered drop/cancel commands.
487        unregistered_state_table_ids: HashSet<TableId>,
488        // target_fragment -> [sink_fragments]
489        dropped_sink_fragment_by_targets: HashMap<FragmentId, Vec<FragmentId>>,
490    },
491
492    /// `CreateStreamingJob` command generates a `Add` barrier by given info.
493    ///
494    /// Barriers from the actors to be created, which is marked as `Inactive` at first, will STILL
495    /// be collected since the barrier should be passthrough.
496    ///
497    /// After the barrier is collected, these newly created actors will be marked as `Running`. And
498    /// it adds the job fragments info to meta store. However, the creating progress will **last
499    /// for a while** until the `finish` channel is signaled, then the state of `TableFragments`
500    /// will be set to `Created`.
501    CreateStreamingJob {
502        info: CreateStreamingJobCommandInfo,
503        job_type: CreateStreamingJobType,
504        cross_db_snapshot_backfill_info: SnapshotBackfillInfo,
505    },
506
507    /// Reschedule context. It must be resolved inside the barrier worker before injection.
508    RescheduleIntent {
509        context: RescheduleContext,
510        /// Filled by the barrier worker after resolving `context` against current worker topology.
511        ///
512        /// We keep unresolved `context` outside of checkpoint state and only materialize this
513        /// execution plan right before injection, then drop `context` to release memory earlier.
514        reschedule_plan: Option<ReschedulePlan>,
515    },
516
517    /// `ReplaceStreamJob` command generates a `Update` barrier with the given `replace_upstream`. This is
518    /// essentially switching the downstream of the old job fragments to the new ones, and
519    /// dropping the old job fragments. Used for schema change.
520    ///
521    /// This can be treated as a special case of reschedule, while the upstream fragment
522    /// of the Merge executors are changed additionally.
523    ReplaceStreamJob(ReplaceStreamJobPlan),
524
525    /// `SourceChangeSplit` generates a `Splits` barrier for pushing initialized splits or
526    /// changed splits.
527    SourceChangeSplit(SplitState),
528
529    /// `Throttle` command generates a `Throttle` barrier with the given throttle config to change
530    /// the `rate_limit` of executors. `throttle_type` specifies which executor kinds should apply it.
531    Throttle {
532        config: ThrottleConfigMap,
533    },
534
535    /// `CreateSubscription` command generates a `CreateSubscriptionMutation` to notify
536    /// materialize executor to start storing old value for subscription.
537    CreateSubscription {
538        subscription_id: SubscriptionId,
539        upstream_mv_table_id: TableId,
540        retention_second: u64,
541    },
542
543    /// `DropSubscription` command generates a `DropSubscriptionsMutation` to notify
544    /// materialize executor to stop storing old value when there is no
545    /// subscription depending on it.
546    DropSubscription {
547        subscription_id: SubscriptionId,
548        upstream_mv_table_id: TableId,
549    },
550
551    /// `AlterSubscriptionRetention` command updates the subscription retention time.
552    AlterSubscriptionRetention {
553        subscription_id: SubscriptionId,
554        upstream_mv_table_id: TableId,
555        retention_second: u64,
556    },
557
558    ConnectorPropsChange(ConnectorPropsChange),
559
560    /// `Refresh` command generates a barrier to refresh a table by truncating state
561    /// and reloading data from source.
562    Refresh {
563        table_id: TableId,
564        associated_source_id: SourceId,
565    },
566    ListFinish {
567        table_id: TableId,
568        associated_source_id: SourceId,
569    },
570    LoadFinish {
571        table_id: TableId,
572        associated_source_id: SourceId,
573    },
574
575    /// `ResetSource` command generates a barrier to reset CDC source offset to latest.
576    /// Used when upstream binlog/oplog has expired.
577    ResetSource {
578        source_id: SourceId,
579    },
580
581    /// `ResumeBackfill` command generates a `StartFragmentBackfill` barrier to force backfill
582    /// to resume for troubleshooting.
583    ResumeBackfill {
584        target: ResumeBackfillTarget,
585    },
586
587    /// `InjectSourceOffsets` command generates a barrier to inject specific offsets
588    /// into source splits (UNSAFE - admin only).
589    /// This can cause data duplication or loss depending on the correctness of the provided offsets.
590    InjectSourceOffsets {
591        source_id: SourceId,
592        /// Split ID -> offset (JSON-encoded based on connector type)
593        split_offsets: HashMap<String, String>,
594    },
595}
596
597#[derive(Debug, Clone, Copy)]
598pub enum ResumeBackfillTarget {
599    Job(JobId),
600    Fragment(FragmentId),
601}
602
603// For debugging and observability purposes. Can add more details later if needed.
604impl std::fmt::Display for Command {
605    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
606        match self {
607            Command::Flush => write!(f, "Flush"),
608            Command::Pause => write!(f, "Pause"),
609            Command::Resume => write!(f, "Resume"),
610            Command::DropStreamingJobs {
611                streaming_job_ids, ..
612            } => {
613                write!(
614                    f,
615                    "DropStreamingJobs: {}",
616                    streaming_job_ids.iter().sorted().join(", ")
617                )
618            }
619            Command::CreateStreamingJob { info, .. } => {
620                write!(f, "CreateStreamingJob: {}", info.streaming_job)
621            }
622            Command::RescheduleIntent {
623                reschedule_plan, ..
624            } => {
625                if reschedule_plan.is_some() {
626                    write!(f, "RescheduleIntent(planned)")
627                } else {
628                    write!(f, "RescheduleIntent")
629                }
630            }
631            Command::ReplaceStreamJob(plan) => {
632                write!(f, "ReplaceStreamJob: {}", plan.streaming_job)
633            }
634            Command::SourceChangeSplit { .. } => write!(f, "SourceChangeSplit"),
635            Command::Throttle { .. } => write!(f, "Throttle"),
636            Command::CreateSubscription {
637                subscription_id, ..
638            } => write!(f, "CreateSubscription: {subscription_id}"),
639            Command::DropSubscription {
640                subscription_id, ..
641            } => write!(f, "DropSubscription: {subscription_id}"),
642            Command::AlterSubscriptionRetention {
643                subscription_id,
644                retention_second,
645                ..
646            } => write!(
647                f,
648                "AlterSubscriptionRetention: {subscription_id} -> {retention_second}"
649            ),
650            Command::ConnectorPropsChange(_) => write!(f, "ConnectorPropsChange"),
651            Command::Refresh {
652                table_id,
653                associated_source_id,
654            } => write!(
655                f,
656                "Refresh: {} (source: {})",
657                table_id, associated_source_id
658            ),
659            Command::ListFinish {
660                table_id,
661                associated_source_id,
662            } => write!(
663                f,
664                "ListFinish: {} (source: {})",
665                table_id, associated_source_id
666            ),
667            Command::LoadFinish {
668                table_id,
669                associated_source_id,
670            } => write!(
671                f,
672                "LoadFinish: {} (source: {})",
673                table_id, associated_source_id
674            ),
675            Command::ResetSource { source_id } => write!(f, "ResetSource: {source_id}"),
676            Command::ResumeBackfill { target } => match target {
677                ResumeBackfillTarget::Job(job_id) => {
678                    write!(f, "ResumeBackfill: job={job_id}")
679                }
680                ResumeBackfillTarget::Fragment(fragment_id) => {
681                    write!(f, "ResumeBackfill: fragment={fragment_id}")
682                }
683            },
684            Command::InjectSourceOffsets {
685                source_id,
686                split_offsets,
687            } => write!(
688                f,
689                "InjectSourceOffsets: {} ({} splits)",
690                source_id,
691                split_offsets.len()
692            ),
693        }
694    }
695}
696
697impl Command {
698    pub fn pause() -> Self {
699        Self::Pause
700    }
701
702    pub fn resume() -> Self {
703        Self::Resume
704    }
705
706    pub fn need_checkpoint(&self) -> bool {
707        // todo! Reviewing the flow of different command to reduce the amount of checkpoint
708        !matches!(self, Command::Resume)
709    }
710}
711
712#[derive(Debug)]
713pub enum PostCollectCommand {
714    Command(String),
715    DropStreamingJobs,
716    CreateStreamingJob {
717        info: CreateStreamingJobCommandInfo,
718        job_type: CreateStreamingJobType,
719        cross_db_snapshot_backfill_info: SnapshotBackfillInfo,
720        resolved_split_assignment: SplitAssignment,
721    },
722    Reschedule {
723        reschedules: HashMap<FragmentId, Reschedule>,
724    },
725    ReplaceStreamJob {
726        plan: ReplaceStreamJobPlan,
727        resolved_split_assignment: SplitAssignment,
728    },
729    SourceChangeSplit {
730        split_assignment: SplitAssignment,
731    },
732    CreateSubscription {
733        subscription_id: SubscriptionId,
734    },
735    ConnectorPropsChange(ConnectorPropsChange),
736    ResumeBackfill {
737        target: ResumeBackfillTarget,
738    },
739}
740
741impl PostCollectCommand {
742    pub fn barrier() -> Self {
743        PostCollectCommand::Command("barrier".to_owned())
744    }
745
746    pub fn should_checkpoint(&self) -> bool {
747        match self {
748            PostCollectCommand::DropStreamingJobs
749            | PostCollectCommand::CreateStreamingJob { .. }
750            | PostCollectCommand::Reschedule { .. }
751            | PostCollectCommand::ReplaceStreamJob { .. }
752            | PostCollectCommand::SourceChangeSplit { .. }
753            | PostCollectCommand::CreateSubscription { .. }
754            | PostCollectCommand::ConnectorPropsChange(_)
755            | PostCollectCommand::ResumeBackfill { .. } => true,
756            PostCollectCommand::Command(_) => false,
757        }
758    }
759
760    pub fn command_name(&self) -> &str {
761        match self {
762            PostCollectCommand::Command(name) => name.as_str(),
763            PostCollectCommand::DropStreamingJobs => "DropStreamingJobs",
764            PostCollectCommand::CreateStreamingJob { .. } => "CreateStreamingJob",
765            PostCollectCommand::Reschedule { .. } => "Reschedule",
766            PostCollectCommand::ReplaceStreamJob { .. } => "ReplaceStreamJob",
767            PostCollectCommand::SourceChangeSplit { .. } => "SourceChangeSplit",
768            PostCollectCommand::CreateSubscription { .. } => "CreateSubscription",
769            PostCollectCommand::ConnectorPropsChange(_) => "ConnectorPropsChange",
770            PostCollectCommand::ResumeBackfill { .. } => "ResumeBackfill",
771        }
772    }
773}
774
775impl Display for PostCollectCommand {
776    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
777        f.write_str(self.command_name())
778    }
779}
780
781#[derive(Debug, Clone, PartialEq, Eq)]
782pub enum BarrierKind {
783    Initial,
784    Barrier,
785    /// Hold a list of previous non-checkpoint prev-epoch + current prev-epoch
786    Checkpoint(Vec<u64>),
787}
788
789impl BarrierKind {
790    pub fn to_protobuf(&self) -> PbBarrierKind {
791        match self {
792            BarrierKind::Initial => PbBarrierKind::Initial,
793            BarrierKind::Barrier => PbBarrierKind::Barrier,
794            BarrierKind::Checkpoint(_) => PbBarrierKind::Checkpoint,
795        }
796    }
797
798    pub fn is_checkpoint(&self) -> bool {
799        matches!(self, BarrierKind::Checkpoint(_))
800    }
801
802    pub fn is_initial(&self) -> bool {
803        matches!(self, BarrierKind::Initial)
804    }
805
806    pub fn as_str_name(&self) -> &'static str {
807        match self {
808            BarrierKind::Initial => "Initial",
809            BarrierKind::Barrier => "Barrier",
810            BarrierKind::Checkpoint(_) => "Checkpoint",
811        }
812    }
813}
814
815fn sink_original_schema_fields(columns: &[PbColumnCatalog]) -> Vec<PbField> {
816    columns
817        .iter()
818        .filter(|col| !col.is_hidden)
819        .map(|col| {
820            let column_desc = col
821                .column_desc
822                .as_ref()
823                .expect("sink column catalog should have a column descriptor");
824            PbField {
825                data_type: Some(
826                    column_desc
827                        .column_type
828                        .as_ref()
829                        .expect("sink column descriptor should have a column type")
830                        .clone(),
831                ),
832                name: column_desc.name.clone(),
833            }
834        })
835        .collect()
836}
837
838impl Command {
839    pub(super) fn collect_commit_epoch_info(
840        database_info: &InflightDatabaseInfo,
841        barrier_info: &PartialGraphBarrierInfo,
842        task: &mut CompleteBarrierTask,
843        resps: Vec<BarrierCompleteResponse>,
844        backfill_pinned_upstream_tables: HashSet<TableId>,
845    ) {
846        let (
847            sst_to_context,
848            synced_ssts,
849            new_table_watermarks,
850            old_value_ssts,
851            vector_index_adds,
852            truncate_tables,
853            iceberg_pk_index_sink_metadata,
854        ) = collect_resp_info(resps);
855
856        let new_table_fragment_infos = match &barrier_info.post_collect_command {
857            PostCollectCommand::CreateStreamingJob { info, job_type, .. } => {
858                assert!(!matches!(
859                    job_type,
860                    CreateStreamingJobType::SnapshotBackfill { .. }
861                        | CreateStreamingJobType::BatchRefresh(_)
862                ));
863                let table_fragments = &info.stream_job_fragments;
864                let mut table_ids: HashSet<_> =
865                    table_fragments.internal_table_ids().into_iter().collect();
866                if let Some(mv_table_id) = table_fragments.mv_table_id() {
867                    table_ids.insert(mv_table_id);
868                }
869
870                vec![NewTableFragmentInfo { table_ids }]
871            }
872            _ => vec![],
873        };
874
875        let mut log_store_table_ids = HashSet::new();
876        // TODO: may collect cross db snapshot backfill
877        log_store_table_ids.extend(database_info.subscribed_tables());
878        log_store_table_ids.extend(backfill_pinned_upstream_tables);
879
880        let table_new_change_log = build_table_change_log_delta(
881            old_value_ssts.into_iter(),
882            synced_ssts.iter().map(|sst| &sst.sst_info),
883            must_match!(&barrier_info.barrier_info.kind, BarrierKind::Checkpoint(epochs) => epochs),
884            log_store_table_ids.into_iter(),
885        );
886
887        let epoch = barrier_info.barrier_info.prev_epoch();
888        let info = &mut task.commit_info;
889        for table_id in &barrier_info.table_ids_to_commit {
890            info.tables_to_commit
891                .try_insert(*table_id, epoch)
892                .expect("non duplicate");
893        }
894
895        info.sstables.extend(synced_ssts);
896        info.new_table_watermarks.extend(new_table_watermarks);
897        info.sst_to_context.extend(sst_to_context);
898        info.new_table_fragment_infos
899            .extend(new_table_fragment_infos);
900        info.change_log_delta.extend(table_new_change_log);
901        for (table_id, vector_index_adds) in vector_index_adds {
902            info.vector_index_delta
903                .try_insert(table_id, VectorIndexDelta::Adds(vector_index_adds))
904                .expect("non-duplicate");
905        }
906        if let PostCollectCommand::CreateStreamingJob { info: job_info, .. } =
907            &barrier_info.post_collect_command
908            && let Some(index_table) = collect_new_vector_index_info(job_info)
909        {
910            info.vector_index_delta
911                .try_insert(
912                    index_table.id,
913                    VectorIndexDelta::Init(PbVectorIndexInit {
914                        info: Some(index_table.vector_index_info.unwrap()),
915                    }),
916                )
917                .expect("non-duplicate");
918        }
919        info.truncate_tables.extend(truncate_tables);
920        task.iceberg_pk_index_pre_commit_metadata
921            .extend(iceberg_pk_index_sink_metadata.into_iter().map(Into::into));
922    }
923}
924
925impl Command {
926    /// Build the `Pause` mutation.
927    pub(super) fn pause_to_mutation(is_currently_paused: bool) -> Option<Mutation> {
928        {
929            {
930                // Only pause when the cluster is not already paused.
931                // XXX: what if pause(r1) - pause(r2) - resume(r1) - resume(r2)??
932                if !is_currently_paused {
933                    Some(Mutation::Pause(PauseMutation {}))
934                } else {
935                    None
936                }
937            }
938        }
939    }
940
941    /// Build the `Resume` mutation.
942    pub(super) fn resume_to_mutation(is_currently_paused: bool) -> Option<Mutation> {
943        {
944            {
945                // Only resume when the cluster is paused with the same reason.
946                if is_currently_paused {
947                    Some(Mutation::Resume(ResumeMutation {}))
948                } else {
949                    None
950                }
951            }
952        }
953    }
954
955    /// Build the `Splits` mutation for `SourceChangeSplit`.
956    pub(super) fn source_change_split_to_mutation(split_assignment: &SplitAssignment) -> Mutation {
957        {
958            {
959                let mut diff = HashMap::new();
960
961                for actor_splits in split_assignment.values() {
962                    diff.extend(actor_splits.clone());
963                }
964
965                Mutation::Splits(SourceChangeSplitMutation {
966                    actor_splits: build_actor_connector_splits(&diff),
967                })
968            }
969        }
970    }
971
972    /// Build the `Stop` mutation for `DropStreamingJobs`.
973    pub(super) fn drop_streaming_jobs_to_mutation(
974        actors: &Vec<ActorId>,
975        dropped_sink_fragment_by_targets: &HashMap<FragmentId, Vec<FragmentId>>,
976    ) -> Mutation {
977        {
978            Mutation::Stop(StopMutation {
979                actors: actors.clone(),
980                dropped_sink_fragments: dropped_sink_fragment_by_targets
981                    .values()
982                    .flatten()
983                    .cloned()
984                    .collect(),
985            })
986        }
987    }
988
989    /// Build the `Add` mutation for `CreateStreamingJob`.
990    pub(super) fn create_streaming_job_to_mutation(
991        info: &CreateStreamingJobCommandInfo,
992        job_type: &CreateStreamingJobType,
993        dropped_actors: impl IntoIterator<Item = ActorId>,
994        is_currently_paused: bool,
995        edges: &mut FragmentEdgeBuildResult,
996        control_stream_manager: &ControlStreamManager,
997        actor_cdc_table_snapshot_splits: Option<HashMap<ActorId, PbCdcTableSnapshotSplits>>,
998        split_assignment: &SplitAssignment,
999        stream_actors: &HashMap<FragmentId, Vec<StreamActor>>,
1000        actor_location: &HashMap<ActorId, WorkerId>,
1001    ) -> MetaResult<Mutation> {
1002        {
1003            {
1004                let CreateStreamingJobCommandInfo {
1005                    stream_job_fragments,
1006                    upstream_fragment_downstreams,
1007                    fragment_backfill_ordering,
1008                    streaming_job,
1009                    ..
1010                } = info;
1011                let database_id = streaming_job.database_id();
1012                let added_actors: Vec<ActorId> = stream_actors
1013                    .values()
1014                    .flatten()
1015                    .map(|actor| actor.actor_id)
1016                    .collect();
1017                let dropped_actors = dropped_actors.into_iter().collect();
1018                let actor_splits = split_assignment
1019                    .values()
1020                    .flat_map(build_actor_connector_splits)
1021                    .collect();
1022                let subscriptions_to_add = {
1023                    if let CreateStreamingJobType::SnapshotBackfill {
1024                        snapshot_backfill_info,
1025                        ..
1026                    }
1027                    | CreateStreamingJobType::BatchRefresh(BatchRefreshInfo {
1028                        snapshot_backfill_info,
1029                        ..
1030                    }) = job_type
1031                    {
1032                        snapshot_backfill_info
1033                            .upstream_mv_table_id_to_backfill_epoch
1034                            .keys()
1035                            .map(|table_id| SubscriptionUpstreamInfo {
1036                                subscriber_id: stream_job_fragments
1037                                    .stream_job_id()
1038                                    .as_subscriber_id(),
1039                                upstream_mv_table_id: *table_id,
1040                            })
1041                            .collect()
1042                    } else {
1043                        Default::default()
1044                    }
1045                };
1046                let backfill_nodes_to_pause: Vec<_> =
1047                    get_nodes_with_backfill_dependencies(fragment_backfill_ordering)
1048                        .into_iter()
1049                        .collect();
1050
1051                let new_upstream_sinks =
1052                    if let CreateStreamingJobType::SinkIntoTable(UpstreamSinkInfo {
1053                        sink_fragment_id,
1054                        sink_output_fields,
1055                        project_exprs,
1056                        new_sink_downstream,
1057                        ..
1058                    }) = job_type
1059                    {
1060                        let new_sink_actors = stream_actors
1061                            .get(sink_fragment_id)
1062                            .unwrap_or_else(|| {
1063                                panic!("upstream sink fragment {sink_fragment_id} does not exist")
1064                            })
1065                            .iter()
1066                            .map(|actor| {
1067                                let worker_id = actor_location[&actor.actor_id];
1068                                PbActorInfo {
1069                                    actor_id: actor.actor_id,
1070                                    host: Some(control_stream_manager.host_addr(worker_id)),
1071                                    partial_graph_id: to_partial_graph_id(database_id, None),
1072                                }
1073                            });
1074                        let new_upstream_sink = PbNewUpstreamSink {
1075                            info: Some(PbUpstreamSinkInfo {
1076                                upstream_fragment_id: *sink_fragment_id,
1077                                sink_output_schema: sink_output_fields.clone(),
1078                                project_exprs: project_exprs.clone(),
1079                            }),
1080                            upstream_actors: new_sink_actors.collect(),
1081                        };
1082                        HashMap::from([(
1083                            new_sink_downstream.downstream_fragment_id,
1084                            new_upstream_sink,
1085                        )])
1086                    } else {
1087                        HashMap::new()
1088                    };
1089
1090                let actor_cdc_table_snapshot_splits = actor_cdc_table_snapshot_splits
1091                    .map(|splits| PbCdcTableSnapshotSplitsWithGeneration { splits });
1092
1093                let add_mutation = AddMutation {
1094                    actor_dispatchers: edges
1095                        .dispatchers
1096                        .extract_if(|fragment_id, _| {
1097                            upstream_fragment_downstreams.contains_key(fragment_id)
1098                        })
1099                        .flat_map(|(_, fragment_dispatchers)| fragment_dispatchers.into_iter())
1100                        .map(|(actor_id, dispatchers)| (actor_id, Dispatchers { dispatchers }))
1101                        .collect(),
1102                    added_actors,
1103                    actor_splits,
1104                    // If the cluster is already paused, the new actors should be paused too.
1105                    pause: is_currently_paused,
1106                    subscriptions_to_add,
1107                    backfill_nodes_to_pause,
1108                    actor_cdc_table_snapshot_splits,
1109                    new_upstream_sinks,
1110                    dropped_actors,
1111                    sink_log_store_flush: info
1112                        .replace_sink
1113                        .map(|old_sink_id| vec![old_sink_id])
1114                        .unwrap_or_default(),
1115                };
1116
1117                Ok(Mutation::Add(add_mutation))
1118            }
1119        }
1120    }
1121
1122    /// Build the `Update` mutation for `ReplaceStreamJob`.
1123    pub(super) fn replace_stream_job_to_mutation(
1124        ReplaceStreamJobPlan {
1125            old_fragments,
1126            replace_upstream,
1127            upstream_fragment_downstreams,
1128            auto_refresh_schema_sinks,
1129            ..
1130        }: &ReplaceStreamJobPlan,
1131        edges: &mut FragmentEdgeBuildResult,
1132        database_info: &mut InflightDatabaseInfo,
1133        split_assignment: &SplitAssignment,
1134    ) -> MetaResult<Option<Mutation>> {
1135        {
1136            {
1137                let merge_updates = edges
1138                    .merge_updates
1139                    .extract_if(|fragment_id, _| replace_upstream.contains_key(fragment_id))
1140                    .collect();
1141                let dispatchers = edges
1142                    .dispatchers
1143                    .extract_if(|fragment_id, _| {
1144                        upstream_fragment_downstreams.contains_key(fragment_id)
1145                    })
1146                    .collect();
1147                let actor_cdc_table_snapshot_splits = database_info
1148                    .assign_cdc_backfill_splits(old_fragments.stream_job_id)?
1149                    .map(|splits| PbCdcTableSnapshotSplitsWithGeneration { splits });
1150                let old_fragments = old_fragments.fragments.keys().copied();
1151                let auto_refresh_sink_fragment_ids = auto_refresh_schema_sinks
1152                    .as_ref()
1153                    .into_iter()
1154                    .flat_map(|sinks| sinks.iter())
1155                    .map(|sink| sink.original_fragment.fragment_id);
1156                Ok(Self::generate_update_mutation_for_replace_table(
1157                    old_fragments
1158                        .chain(auto_refresh_sink_fragment_ids)
1159                        .flat_map(|fragment_id| {
1160                            database_info.fragment(fragment_id).actors.keys().copied()
1161                        }),
1162                    merge_updates,
1163                    dispatchers,
1164                    split_assignment,
1165                    actor_cdc_table_snapshot_splits,
1166                    auto_refresh_schema_sinks.as_ref(),
1167                ))
1168            }
1169        }
1170    }
1171
1172    /// Build the `Update` mutation for `RescheduleIntent`.
1173    pub(super) fn reschedule_to_mutation(
1174        reschedules: &HashMap<FragmentId, Reschedule>,
1175        fragment_actors: &HashMap<FragmentId, HashSet<ActorId>>,
1176        control_stream_manager: &ControlStreamManager,
1177        database_info: &mut InflightDatabaseInfo,
1178    ) -> MetaResult<Option<Mutation>> {
1179        {
1180            {
1181                let database_id = database_info.database_id;
1182                let mut dispatcher_update = HashMap::new();
1183                for reschedule in reschedules.values() {
1184                    for &(upstream_fragment_id, dispatcher_id) in
1185                        &reschedule.upstream_fragment_dispatcher_ids
1186                    {
1187                        // Find the actors of the upstream fragment.
1188                        let upstream_actor_ids = fragment_actors
1189                            .get(&upstream_fragment_id)
1190                            .expect("should contain");
1191
1192                        let upstream_reschedule = reschedules.get(&upstream_fragment_id);
1193
1194                        // Record updates for all actors.
1195                        for &actor_id in upstream_actor_ids {
1196                            let added_downstream_actor_id = if upstream_reschedule
1197                                .map(|reschedule| !reschedule.removed_actors.contains(&actor_id))
1198                                .unwrap_or(true)
1199                            {
1200                                reschedule
1201                                    .added_actors
1202                                    .values()
1203                                    .flatten()
1204                                    .cloned()
1205                                    .collect()
1206                            } else {
1207                                Default::default()
1208                            };
1209                            // Index with the dispatcher id to check duplicates.
1210                            dispatcher_update
1211                                .try_insert(
1212                                    (actor_id, dispatcher_id),
1213                                    DispatcherUpdate {
1214                                        actor_id,
1215                                        dispatcher_id,
1216                                        hash_mapping: reschedule
1217                                            .upstream_dispatcher_mapping
1218                                            .as_ref()
1219                                            .map(|m| m.to_protobuf()),
1220                                        added_downstream_actor_id,
1221                                        removed_downstream_actor_id: reschedule
1222                                            .removed_actors
1223                                            .iter()
1224                                            .cloned()
1225                                            .collect(),
1226                                    },
1227                                )
1228                                .unwrap();
1229                        }
1230                    }
1231                }
1232                let dispatcher_update = dispatcher_update.into_values().collect();
1233
1234                let mut merge_update = HashMap::new();
1235                for (&fragment_id, reschedule) in reschedules {
1236                    for &downstream_fragment_id in &reschedule.downstream_fragment_ids {
1237                        // Find the actors of the downstream fragment.
1238                        let downstream_actor_ids = fragment_actors
1239                            .get(&downstream_fragment_id)
1240                            .expect("should contain");
1241
1242                        // Downstream removed actors should be skipped
1243                        // Newly created actors of the current fragment will not dispatch Update
1244                        // barriers to them
1245                        let downstream_removed_actors: HashSet<_> = reschedules
1246                            .get(&downstream_fragment_id)
1247                            .map(|downstream_reschedule| {
1248                                downstream_reschedule
1249                                    .removed_actors
1250                                    .iter()
1251                                    .copied()
1252                                    .collect()
1253                            })
1254                            .unwrap_or_default();
1255
1256                        // Record updates for all actors.
1257                        for &actor_id in downstream_actor_ids {
1258                            if downstream_removed_actors.contains(&actor_id) {
1259                                continue;
1260                            }
1261
1262                            // Index with the fragment id to check duplicates.
1263                            merge_update
1264                                .try_insert(
1265                                    (actor_id, fragment_id),
1266                                    MergeUpdate {
1267                                        actor_id,
1268                                        upstream_fragment_id: fragment_id,
1269                                        new_upstream_fragment_id: None,
1270                                        added_upstream_actors: reschedule
1271                                            .added_actors
1272                                            .iter()
1273                                            .flat_map(|(worker_id, actors)| {
1274                                                let host =
1275                                                    control_stream_manager.host_addr(*worker_id);
1276                                                actors.iter().map(move |&actor_id| PbActorInfo {
1277                                                    actor_id,
1278                                                    host: Some(host.clone()),
1279                                                    // we assume that we only scale the partial graph of database
1280                                                    partial_graph_id: to_partial_graph_id(
1281                                                        database_id,
1282                                                        None,
1283                                                    ),
1284                                                })
1285                                            })
1286                                            .collect(),
1287                                        removed_upstream_actor_id: reschedule
1288                                            .removed_actors
1289                                            .iter()
1290                                            .cloned()
1291                                            .collect(),
1292                                    },
1293                                )
1294                                .unwrap();
1295                        }
1296                    }
1297                }
1298                let merge_update = merge_update.into_values().collect();
1299
1300                let mut actor_vnode_bitmap_update = HashMap::new();
1301                for reschedule in reschedules.values() {
1302                    // Record updates for all actors in this fragment.
1303                    for (&actor_id, bitmap) in &reschedule.vnode_bitmap_updates {
1304                        let bitmap = bitmap.to_protobuf();
1305                        actor_vnode_bitmap_update
1306                            .try_insert(actor_id, bitmap)
1307                            .unwrap();
1308                    }
1309                }
1310                let dropped_actors = reschedules
1311                    .values()
1312                    .flat_map(|r| r.removed_actors.iter().copied())
1313                    .collect();
1314                let mut actor_splits = HashMap::new();
1315                let mut actor_cdc_table_snapshot_splits = HashMap::new();
1316                for (fragment_id, reschedule) in reschedules {
1317                    for (actor_id, splits) in &reschedule.actor_splits {
1318                        actor_splits.insert(
1319                            *actor_id,
1320                            ConnectorSplits {
1321                                splits: splits.iter().map(ConnectorSplit::from).collect(),
1322                            },
1323                        );
1324                    }
1325
1326                    if let Some(assignment) =
1327                        database_info.may_assign_fragment_cdc_backfill_splits(*fragment_id)?
1328                    {
1329                        actor_cdc_table_snapshot_splits.extend(assignment)
1330                    }
1331                }
1332
1333                // we don't create dispatchers in reschedule scenario
1334                let actor_new_dispatchers = HashMap::new();
1335                let mutation = Mutation::Update(UpdateMutation {
1336                    dispatcher_update,
1337                    merge_update,
1338                    actor_vnode_bitmap_update,
1339                    dropped_actors,
1340                    actor_splits,
1341                    actor_new_dispatchers,
1342                    actor_cdc_table_snapshot_splits: Some(PbCdcTableSnapshotSplitsWithGeneration {
1343                        splits: actor_cdc_table_snapshot_splits,
1344                    }),
1345                    sink_schema_change: Default::default(),
1346                    subscriptions_to_drop: vec![],
1347                    iceberg_pk_index_compaction: None,
1348                });
1349                tracing::debug!("update mutation: {mutation:?}");
1350                Ok(Some(mutation))
1351            }
1352        }
1353    }
1354
1355    /// Build the `Add` mutation for `CreateSubscription`.
1356    pub(super) fn create_subscription_to_mutation(
1357        upstream_mv_table_id: TableId,
1358        subscription_id: SubscriptionId,
1359    ) -> Mutation {
1360        {
1361            Mutation::Add(AddMutation {
1362                actor_dispatchers: Default::default(),
1363                added_actors: vec![],
1364                actor_splits: Default::default(),
1365                pause: false,
1366                subscriptions_to_add: vec![SubscriptionUpstreamInfo {
1367                    upstream_mv_table_id,
1368                    subscriber_id: subscription_id.as_subscriber_id(),
1369                }],
1370                backfill_nodes_to_pause: vec![],
1371                actor_cdc_table_snapshot_splits: None,
1372                new_upstream_sinks: Default::default(),
1373                dropped_actors: Default::default(),
1374                sink_log_store_flush: Default::default(),
1375            })
1376        }
1377    }
1378
1379    /// Build the `DropSubscriptions` mutation for `DropSubscription`.
1380    pub(super) fn drop_subscription_to_mutation(
1381        upstream_mv_table_id: TableId,
1382        subscription_id: SubscriptionId,
1383    ) -> Mutation {
1384        {
1385            Mutation::DropSubscriptions(DropSubscriptionsMutation {
1386                info: vec![SubscriptionUpstreamInfo {
1387                    subscriber_id: subscription_id.as_subscriber_id(),
1388                    upstream_mv_table_id,
1389                }],
1390            })
1391        }
1392    }
1393
1394    /// Build the `ConnectorPropsChange` mutation.
1395    pub(super) fn connector_props_change_to_mutation(config: &ConnectorPropsChange) -> Mutation {
1396        {
1397            {
1398                let mut connector_props_infos = HashMap::default();
1399                for (k, v) in config {
1400                    connector_props_infos.insert(
1401                        k.as_raw_id(),
1402                        ConnectorPropsInfo {
1403                            connector_props_info: v.clone(),
1404                        },
1405                    );
1406                }
1407                Mutation::ConnectorPropsChange(ConnectorPropsChangeMutation {
1408                    connector_props_infos,
1409                })
1410            }
1411        }
1412    }
1413
1414    /// Build the `RefreshStart` mutation.
1415    pub(super) fn refresh_to_mutation(
1416        table_id: TableId,
1417        associated_source_id: SourceId,
1418    ) -> Mutation {
1419        Mutation::RefreshStart(risingwave_pb::stream_plan::RefreshStartMutation {
1420            table_id,
1421            associated_source_id,
1422        })
1423    }
1424
1425    /// Build the `ListFinish` mutation.
1426    pub(super) fn list_finish_to_mutation(associated_source_id: SourceId) -> Mutation {
1427        Mutation::ListFinish(ListFinishMutation {
1428            associated_source_id,
1429        })
1430    }
1431
1432    /// Build the `LoadFinish` mutation.
1433    pub(super) fn load_finish_to_mutation(associated_source_id: SourceId) -> Mutation {
1434        Mutation::LoadFinish(LoadFinishMutation {
1435            associated_source_id,
1436        })
1437    }
1438
1439    /// Build the `ResetSource` mutation.
1440    pub(super) fn reset_source_to_mutation(source_id: SourceId) -> Mutation {
1441        Mutation::ResetSource(risingwave_pb::stream_plan::ResetSourceMutation {
1442            source_id: source_id.as_raw_id(),
1443        })
1444    }
1445
1446    /// Build the `StartFragmentBackfill` mutation for `ResumeBackfill`.
1447    pub(super) fn resume_backfill_to_mutation(
1448        target: &ResumeBackfillTarget,
1449        database_info: &InflightDatabaseInfo,
1450    ) -> MetaResult<Option<Mutation>> {
1451        {
1452            {
1453                let fragment_ids: HashSet<_> = match target {
1454                    ResumeBackfillTarget::Job(job_id) => {
1455                        database_info.backfill_fragment_ids_for_job(*job_id)?
1456                    }
1457                    ResumeBackfillTarget::Fragment(fragment_id) => {
1458                        if !database_info.is_backfill_fragment(*fragment_id)? {
1459                            return Err(MetaError::invalid_parameter(format!(
1460                                "fragment {} is not a backfill node",
1461                                fragment_id
1462                            )));
1463                        }
1464                        HashSet::from([*fragment_id])
1465                    }
1466                };
1467                if fragment_ids.is_empty() {
1468                    warn!(
1469                        ?target,
1470                        "resume backfill command ignored because no backfill fragments found"
1471                    );
1472                    Ok(None)
1473                } else {
1474                    Ok(Some(Mutation::StartFragmentBackfill(
1475                        StartFragmentBackfillMutation {
1476                            fragment_ids: fragment_ids.into_iter().collect(),
1477                        },
1478                    )))
1479                }
1480            }
1481        }
1482    }
1483
1484    /// Build the `InjectSourceOffsets` mutation.
1485    pub(super) fn inject_source_offsets_to_mutation(
1486        source_id: SourceId,
1487        split_offsets: &HashMap<String, String>,
1488    ) -> Mutation {
1489        Mutation::InjectSourceOffsets(risingwave_pb::stream_plan::InjectSourceOffsetsMutation {
1490            source_id: source_id.as_raw_id(),
1491            split_offsets: split_offsets.clone(),
1492        })
1493    }
1494
1495    /// Collect actors to create for `CreateStreamingJob` (non-snapshot-backfill).
1496    pub(super) fn create_streaming_job_actors_to_create(
1497        info: &CreateStreamingJobCommandInfo,
1498        edges: &mut FragmentEdgeBuildResult,
1499        stream_actors: &HashMap<FragmentId, Vec<StreamActor>>,
1500        actor_location: &HashMap<ActorId, WorkerId>,
1501    ) -> StreamJobActorsToCreate {
1502        {
1503            {
1504                edges.collect_actors_to_create(info.stream_job_fragments.fragments.values().map(
1505                    |fragment| {
1506                        let actors = stream_actors
1507                            .get(&fragment.fragment_id)
1508                            .into_iter()
1509                            .flatten()
1510                            .map(|actor| (actor, actor_location[&actor.actor_id]));
1511                        (
1512                            fragment.fragment_id,
1513                            &fragment.nodes,
1514                            actors,
1515                            [], // no subscriber for new job to create
1516                        )
1517                    },
1518                ))
1519            }
1520        }
1521    }
1522
1523    /// Collect actors to create for `RescheduleIntent`.
1524    pub(super) fn reschedule_actors_to_create(
1525        reschedules: &HashMap<FragmentId, Reschedule>,
1526        fragment_actors: &HashMap<FragmentId, HashSet<ActorId>>,
1527        database_info: &InflightDatabaseInfo,
1528        control_stream_manager: &ControlStreamManager,
1529    ) -> StreamJobActorsToCreate {
1530        {
1531            {
1532                let mut actor_upstreams = Self::collect_database_partial_graph_actor_upstreams(
1533                    reschedules.iter().map(|(fragment_id, reschedule)| {
1534                        (
1535                            *fragment_id,
1536                            reschedule.newly_created_actors.values().map(
1537                                |((actor, dispatchers), _)| {
1538                                    (actor.actor_id, dispatchers.as_slice())
1539                                },
1540                            ),
1541                        )
1542                    }),
1543                    Some((reschedules, fragment_actors)),
1544                    database_info,
1545                    control_stream_manager,
1546                );
1547                let mut map: HashMap<WorkerId, HashMap<_, (_, Vec<_>, _)>> = HashMap::new();
1548                for (fragment_id, (actor, dispatchers), worker_id) in
1549                    reschedules.iter().flat_map(|(fragment_id, reschedule)| {
1550                        reschedule
1551                            .newly_created_actors
1552                            .values()
1553                            .map(|(actors, status)| (*fragment_id, actors, status))
1554                    })
1555                {
1556                    let upstreams = actor_upstreams.remove(&actor.actor_id).unwrap_or_default();
1557                    map.entry(*worker_id)
1558                        .or_default()
1559                        .entry(fragment_id)
1560                        .or_insert_with(|| {
1561                            let node = database_info.fragment(fragment_id).nodes.clone();
1562                            let subscribers =
1563                                database_info.fragment_subscribers(fragment_id).collect();
1564                            (node, vec![], subscribers)
1565                        })
1566                        .1
1567                        .push((actor.clone(), upstreams, dispatchers.clone()));
1568                }
1569                map
1570            }
1571        }
1572    }
1573
1574    /// Collect actors to create for `ReplaceStreamJob`.
1575    pub(super) fn replace_stream_job_actors_to_create(
1576        replace_table: &ReplaceStreamJobPlan,
1577        edges: &mut FragmentEdgeBuildResult,
1578        database_info: &InflightDatabaseInfo,
1579        stream_actors: &HashMap<FragmentId, Vec<StreamActor>>,
1580        actor_location: &HashMap<ActorId, WorkerId>,
1581    ) -> StreamJobActorsToCreate {
1582        {
1583            {
1584                let mut actors = edges.collect_actors_to_create(
1585                    replace_table
1586                        .new_fragments
1587                        .fragments
1588                        .values()
1589                        .map(|fragment| {
1590                            let actors = stream_actors
1591                                .get(&fragment.fragment_id)
1592                                .into_iter()
1593                                .flatten()
1594                                .map(|actor| (actor, actor_location[&actor.actor_id]));
1595                            (
1596                                fragment.fragment_id,
1597                                &fragment.nodes,
1598                                actors,
1599                                database_info
1600                                    .job_subscribers(replace_table.old_fragments.stream_job_id),
1601                            )
1602                        }),
1603                );
1604
1605                // Handle auto-refresh schema sinks
1606                if let Some(sinks) = &replace_table.auto_refresh_schema_sinks {
1607                    let sink_actors = edges.collect_actors_to_create(sinks.iter().map(|sink| {
1608                        (
1609                            sink.new_fragment.fragment_id,
1610                            &sink.new_fragment.nodes,
1611                            stream_actors
1612                                .get(&sink.new_fragment.fragment_id)
1613                                .into_iter()
1614                                .flatten()
1615                                .map(|actor| (actor, actor_location[&actor.actor_id])),
1616                            database_info.job_subscribers(sink.original_sink.id.as_job_id()),
1617                        )
1618                    }));
1619                    for (worker_id, fragment_actors) in sink_actors {
1620                        actors.entry(worker_id).or_default().extend(fragment_actors);
1621                    }
1622                }
1623                actors
1624            }
1625        }
1626    }
1627
1628    fn generate_update_mutation_for_replace_table(
1629        dropped_actors: impl IntoIterator<Item = ActorId>,
1630        merge_updates: HashMap<FragmentId, Vec<MergeUpdate>>,
1631        dispatchers: FragmentActorDispatchers,
1632        split_assignment: &SplitAssignment,
1633        cdc_table_snapshot_split_assignment: Option<PbCdcTableSnapshotSplitsWithGeneration>,
1634        auto_refresh_schema_sinks: Option<&Vec<AutoRefreshSchemaSinkContext>>,
1635    ) -> Option<Mutation> {
1636        let dropped_actors = dropped_actors.into_iter().collect();
1637
1638        let actor_new_dispatchers = dispatchers
1639            .into_values()
1640            .flatten()
1641            .map(|(actor_id, dispatchers)| (actor_id, Dispatchers { dispatchers }))
1642            .collect();
1643
1644        let actor_splits = split_assignment
1645            .values()
1646            .flat_map(build_actor_connector_splits)
1647            .collect();
1648        Some(Mutation::Update(UpdateMutation {
1649            actor_new_dispatchers,
1650            merge_update: merge_updates.into_values().flatten().collect(),
1651            dropped_actors,
1652            actor_splits,
1653            actor_cdc_table_snapshot_splits: cdc_table_snapshot_split_assignment,
1654            sink_schema_change: auto_refresh_schema_sinks
1655                .as_ref()
1656                .into_iter()
1657                .flat_map(|sinks| {
1658                    sinks.iter().map(|sink| {
1659                        let op = if !sink.removed_column_names.is_empty() {
1660                            PbSinkSchemaChangeOp::DropColumns(PbSinkDropColumnsOp {
1661                                column_names: sink.removed_column_names.clone(),
1662                            })
1663                        } else {
1664                            PbSinkSchemaChangeOp::AddColumns(PbSinkAddColumnsOp {
1665                                fields: sink
1666                                    .newly_add_fields
1667                                    .iter()
1668                                    .map(|field| field.to_prost())
1669                                    .collect(),
1670                            })
1671                        };
1672                        (
1673                            sink.original_sink.id.as_raw_id(),
1674                            PbSinkSchemaChange {
1675                                original_schema: sink_original_schema_fields(
1676                                    &sink.original_sink.columns,
1677                                ),
1678                                op: Some(op),
1679                            },
1680                        )
1681                    })
1682                })
1683                .collect(),
1684            ..Default::default()
1685        }))
1686    }
1687}
1688
1689impl Command {
1690    #[expect(clippy::type_complexity)]
1691    pub(super) fn collect_database_partial_graph_actor_upstreams(
1692        actor_dispatchers: impl Iterator<
1693            Item = (FragmentId, impl Iterator<Item = (ActorId, &[Dispatcher])>),
1694        >,
1695        reschedule_dispatcher_update: Option<(
1696            &HashMap<FragmentId, Reschedule>,
1697            &HashMap<FragmentId, HashSet<ActorId>>,
1698        )>,
1699        database_info: &InflightDatabaseInfo,
1700        control_stream_manager: &ControlStreamManager,
1701    ) -> HashMap<ActorId, ActorUpstreams> {
1702        let mut actor_upstreams: HashMap<ActorId, ActorUpstreams> = HashMap::new();
1703        for (upstream_fragment_id, upstream_actors) in actor_dispatchers {
1704            let upstream_fragment = database_info.fragment(upstream_fragment_id);
1705            for (upstream_actor_id, dispatchers) in upstream_actors {
1706                let upstream_actor_location =
1707                    upstream_fragment.actors[&upstream_actor_id].worker_id;
1708                let upstream_actor_host = control_stream_manager.host_addr(upstream_actor_location);
1709                for downstream_actor_id in dispatchers
1710                    .iter()
1711                    .flat_map(|dispatcher| dispatcher.downstream_actor_id.iter())
1712                {
1713                    actor_upstreams
1714                        .entry(*downstream_actor_id)
1715                        .or_default()
1716                        .entry(upstream_fragment_id)
1717                        .or_default()
1718                        .insert(
1719                            upstream_actor_id,
1720                            PbActorInfo {
1721                                actor_id: upstream_actor_id,
1722                                host: Some(upstream_actor_host.clone()),
1723                                partial_graph_id: to_partial_graph_id(
1724                                    database_info.database_id,
1725                                    None,
1726                                ),
1727                            },
1728                        );
1729                }
1730            }
1731        }
1732        if let Some((reschedules, fragment_actors)) = reschedule_dispatcher_update {
1733            for reschedule in reschedules.values() {
1734                for (upstream_fragment_id, _) in &reschedule.upstream_fragment_dispatcher_ids {
1735                    let upstream_fragment = database_info.fragment(*upstream_fragment_id);
1736                    let upstream_reschedule = reschedules.get(upstream_fragment_id);
1737                    for upstream_actor_id in fragment_actors
1738                        .get(upstream_fragment_id)
1739                        .expect("should exist")
1740                    {
1741                        let upstream_actor_location =
1742                            upstream_fragment.actors[upstream_actor_id].worker_id;
1743                        let upstream_actor_host =
1744                            control_stream_manager.host_addr(upstream_actor_location);
1745                        if let Some(upstream_reschedule) = upstream_reschedule
1746                            && upstream_reschedule
1747                                .removed_actors
1748                                .contains(upstream_actor_id)
1749                        {
1750                            continue;
1751                        }
1752                        for (_, downstream_actor_id) in
1753                            reschedule
1754                                .added_actors
1755                                .iter()
1756                                .flat_map(|(worker_id, actors)| {
1757                                    actors.iter().map(|actor| (*worker_id, *actor))
1758                                })
1759                        {
1760                            actor_upstreams
1761                                .entry(downstream_actor_id)
1762                                .or_default()
1763                                .entry(*upstream_fragment_id)
1764                                .or_default()
1765                                .insert(
1766                                    *upstream_actor_id,
1767                                    PbActorInfo {
1768                                        actor_id: *upstream_actor_id,
1769                                        host: Some(upstream_actor_host.clone()),
1770                                        partial_graph_id: to_partial_graph_id(
1771                                            database_info.database_id,
1772                                            None,
1773                                        ),
1774                                    },
1775                                );
1776                        }
1777                    }
1778                }
1779            }
1780        }
1781        actor_upstreams
1782    }
1783}
1784
1785#[cfg(test)]
1786mod tests {
1787    use risingwave_pb::data::PbDataType;
1788    use risingwave_pb::data::data_type::PbTypeName;
1789    use risingwave_pb::plan_common::{ColumnCatalog as PbColumnCatalog, ColumnDesc};
1790
1791    use super::sink_original_schema_fields;
1792
1793    fn column(name: &str, type_name: PbTypeName, is_hidden: bool) -> PbColumnCatalog {
1794        PbColumnCatalog {
1795            column_desc: Some(ColumnDesc {
1796                column_type: Some(PbDataType {
1797                    type_name: type_name as i32,
1798                    ..Default::default()
1799                }),
1800                name: name.to_owned(),
1801                ..Default::default()
1802            }),
1803            is_hidden,
1804        }
1805    }
1806
1807    #[test]
1808    fn test_sink_original_schema_fields_skips_hidden_columns() {
1809        let columns = vec![
1810            column("k", PbTypeName::Int32, false),
1811            column("v", PbTypeName::Varchar, false),
1812            column("_row_id", PbTypeName::Serial, true),
1813        ];
1814
1815        let fields = sink_original_schema_fields(&columns);
1816        let field_names = fields
1817            .iter()
1818            .map(|field| field.name.as_str())
1819            .collect::<Vec<_>>();
1820
1821        assert_eq!(field_names, ["k", "v"]);
1822    }
1823}