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