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