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