Skip to main content

risingwave_meta/barrier/
command.rs

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