1use std::collections::BTreeSet;
11use std::time::Duration;
12
13use itertools::Itertools;
14use mz_adapter_types::cluster_state::ReconfigurationAudit;
15use mz_catalog::builtin::BUILTINS;
16use mz_catalog::durable::managed_cluster_replica_name;
17use mz_catalog::memory::error::ErrorKind;
18use mz_catalog::memory::objects::{
19 Cluster, ClusterConfig, ClusterVariant, ClusterVariantManaged, DataSourceDesc,
20 ManagedReplicaConfigShape, ReconfigurationState, ReconfigurationStatus, ReconfigurationTarget,
21};
22use mz_compute_types::config::ComputeReplicaConfig;
23use mz_controller::clusters::{
24 ManagedReplicaLocation, ReplicaConfig, ReplicaLocation, ReplicaLogging,
25};
26use mz_controller_types::{ClusterId, DEFAULT_REPLICA_LOGGING_INTERVAL, ReplicaId};
27use mz_ore::cast::CastFrom;
28use mz_ore::collections::CollectionExt;
29use mz_ore::instrument;
30use mz_repr::Timestamp;
31use mz_repr::adt::numeric::Numeric;
32use mz_repr::role_id::RoleId;
33use mz_sql::catalog::{CatalogCluster, CatalogError, ObjectType};
34use mz_sql::names::QualifiedItemName;
35use mz_sql::plan::{
36 self, AlterClusterPlanStrategy, AlterClusterRenamePlan, AlterClusterReplicaRenamePlan,
37 AlterClusterSwapPlan, AlterOptionParameter, AlterSetClusterPlan, CreateClusterManagedPlan,
38 CreateClusterPlan, CreateClusterReplicaPlan, CreateClusterUnmanagedPlan, CreateClusterVariant,
39 PlanClusterOption,
40};
41use mz_sql::plan::{AlterClusterPlan, OnTimeoutAction};
42use mz_sql::session::metadata::SessionMetadata;
43use mz_sql::session::vars::{
44 MAX_CREDIT_CONSUMPTION_RATE, MAX_REPLICAS_PER_CLUSTER, SystemVars, Var,
45};
46use mz_storage_types::sources::SourceConnection;
47use tracing::{Instrument, Span};
48
49use mz_adapter_types::dyncfgs::{
50 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT, ENABLE_BACKGROUND_ALTER_CLUSTER,
51};
52
53use super::return_if_err;
54use crate::catalog::{self, Op, ReplicaCreateDropReason};
55use crate::coord::{
56 AlterCluster, AlterClusterAwaitReconfiguration, ClusterStage, Coordinator, Message,
57 PlanValidity, StageResult, Staged,
58};
59use crate::{AdapterError, AdapterNotice, ExecuteContext, ExecuteResponse, session::Session};
60
61impl Staged for ClusterStage {
62 type Ctx = ExecuteContext;
63
64 fn validity(&mut self) -> &mut PlanValidity {
65 match self {
66 Self::Alter(stage) => &mut stage.validity,
67 Self::AwaitReconfiguration(stage) => &mut stage.validity,
68 }
69 }
70
71 async fn stage(
72 self,
73 coord: &mut Coordinator,
74 ctx: &mut ExecuteContext,
75 ) -> Result<StageResult<Box<Self>>, crate::AdapterError> {
76 match self {
77 Self::Alter(stage) => {
78 coord
79 .sequence_alter_cluster_stage(ctx.session(), stage.plan.clone(), stage.validity)
80 .await
81 }
82 Self::AwaitReconfiguration(stage) => {
83 coord.await_reconfiguration_stage(stage.validity, stage.cluster_id, stage.target)
84 }
85 }
86 }
87
88 fn message(self, ctx: ExecuteContext, span: tracing::Span) -> Message {
89 Message::ClusterStageReady {
90 ctx,
91 span,
92 stage: self,
93 }
94 }
95
96 fn cancel_enabled(&self) -> bool {
97 true
98 }
99}
100
101impl Coordinator {
102 #[instrument]
103 pub(crate) async fn sequence_alter_cluster_staged(
104 &mut self,
105 ctx: ExecuteContext,
106 plan: plan::AlterClusterPlan,
107 ) {
108 let stage = return_if_err!(self.alter_cluster_validate(ctx.session(), plan).await, ctx);
109 self.sequence_staged(ctx, Span::current(), stage).await;
110 }
111
112 #[instrument]
113 async fn alter_cluster_validate(
114 &self,
115 session: &Session,
116 plan: plan::AlterClusterPlan,
117 ) -> Result<ClusterStage, AdapterError> {
118 let validity = PlanValidity::new(
119 self.catalog(),
120 BTreeSet::new(),
121 Some(plan.id.clone()),
122 None,
123 session.role_metadata().clone(),
124 );
125 Ok(ClusterStage::Alter(AlterCluster { validity, plan }))
126 }
127
128 async fn sequence_alter_cluster_stage(
129 &mut self,
130 session: &Session,
131 plan: plan::AlterClusterPlan,
132 validity: PlanValidity,
133 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
134 let AlterClusterPlan {
135 id: cluster_id,
136 name: _,
137 ref options,
138 ref strategy,
139 } = plan;
140
141 use mz_catalog::memory::objects::ClusterVariant::*;
142 use mz_sql::plan::AlterOptionParameter::*;
143 let cluster = self.catalog.get_cluster(cluster_id);
144 let config = cluster.config.clone();
145 let mut new_config = config.clone();
146
147 match (&new_config.variant, &options.managed) {
148 (Managed(_), Reset) | (Managed(_), Unchanged) | (Managed(_), Set(true)) => {}
149 (Managed(_), Set(false)) => new_config.variant = Unmanaged,
150 (Unmanaged, Unchanged) | (Unmanaged, Set(false)) => {}
151 (Unmanaged, Reset) | (Unmanaged, Set(true)) => {
152 let size = "".to_string();
156 let logging = ReplicaLogging {
157 log_logging: false,
158 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
159 };
160 new_config.variant = Managed(ClusterVariantManaged {
161 size,
162 availability_zones: Default::default(),
163 logging,
164 arrangement_compression: false,
165 replication_factor: 1,
166 optimizer_feature_overrides: Default::default(),
167 schedule: Default::default(),
168 auto_scaling_strategy: None,
169 reconfiguration: None,
170 burst: None,
171 });
172 }
173 }
174
175 match &mut new_config.variant {
176 Managed(ClusterVariantManaged {
177 size,
178 availability_zones,
179 logging,
180 arrangement_compression,
181 replication_factor,
182 optimizer_feature_overrides: _,
183 schedule,
184 auto_scaling_strategy,
185 reconfiguration: _,
186 burst: _,
187 }) => {
188 match &options.size {
189 Set(s) => size.clone_from(s),
190 Reset => coord_bail!("SIZE has no default value"),
191 Unchanged => {}
192 }
193 match &options.availability_zones {
194 Set(az) => availability_zones.clone_from(az),
195 Reset => *availability_zones = Default::default(),
196 Unchanged => {}
197 }
198 match &options.introspection_debugging {
199 Set(id) => logging.log_logging = *id,
200 Reset => logging.log_logging = false,
201 Unchanged => {}
202 }
203 match &options.introspection_interval {
204 Set(ii) => logging.interval = ii.0,
205 Reset => logging.interval = Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
206 Unchanged => {}
207 }
208 match &options.arrangement_compression {
209 Set(ac) => *arrangement_compression = *ac,
210 Reset => *arrangement_compression = false,
211 Unchanged => {}
212 }
213 match &options.replication_factor {
214 Set(rf) => *replication_factor = *rf,
215 Reset => {
216 *replication_factor = self
217 .catalog
218 .system_config()
219 .default_cluster_replication_factor()
220 }
221 Unchanged => {}
222 }
223 match &options.schedule {
224 Set(new_schedule) => {
225 *schedule = new_schedule.clone();
226 }
227 Reset => *schedule = Default::default(),
228 Unchanged => {}
229 }
230 match &options.auto_scaling_strategy {
231 Set(new_strategy) => auto_scaling_strategy.clone_from(new_strategy),
232 Reset => *auto_scaling_strategy = None,
234 Unchanged => {}
235 }
236 if !matches!(options.replicas, Unchanged) {
237 coord_bail!("Cannot change REPLICAS of managed clusters");
238 }
239 }
240 Unmanaged => {
241 if !matches!(options.size, Unchanged) {
242 coord_bail!("Cannot change SIZE of unmanaged clusters");
243 }
244 if !matches!(options.availability_zones, Unchanged) {
245 coord_bail!("Cannot change AVAILABILITY ZONES of unmanaged clusters");
246 }
247 if !matches!(options.introspection_debugging, Unchanged) {
248 coord_bail!("Cannot change INTROSPECTION DEGUBBING of unmanaged clusters");
249 }
250 if !matches!(options.introspection_interval, Unchanged) {
251 coord_bail!("Cannot change INTROSPECTION INTERVAL of unmanaged clusters");
252 }
253 if !matches!(options.arrangement_compression, Unchanged) {
254 coord_bail!(
255 "Cannot change EXPERIMENTAL ARRANGEMENT COMPRESSION of unmanaged clusters"
256 );
257 }
258 if !matches!(options.replication_factor, Unchanged) {
259 coord_bail!("Cannot change REPLICATION FACTOR of unmanaged clusters");
260 }
261 if !matches!(options.auto_scaling_strategy, Unchanged) {
262 coord_bail!("Cannot change AUTO SCALING STRATEGY of unmanaged clusters");
263 }
264 }
265 }
266
267 match &options.workload_class {
268 Set(wc) => new_config.workload_class.clone_from(wc),
269 Reset => new_config.workload_class = None,
270 Unchanged => {}
271 }
272
273 let reconfiguration_in_flight = matches!(
274 &config.variant,
275 Managed(managed) if managed
276 .reconfiguration
277 .as_ref()
278 .is_some_and(|record| record.is_in_progress())
279 );
280
281 if reconfiguration_in_flight && !matches!(options.schedule, Unchanged) {
287 return Err(AdapterError::AlterClusterScheduleWhileReconfiguring);
288 }
289
290 if reconfiguration_in_flight && !matches!(options.replication_factor, Unchanged) {
297 return Err(AdapterError::AlterClusterReplicationFactorWhileReconfiguring);
298 }
299
300 let cancels_or_retargets =
305 reconfiguration_in_flight && alter_changes_replica_shape(options);
306 if new_config == config && !cancels_or_retargets {
307 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
308 ObjectType::Cluster,
309 )));
310 }
311
312 let single_replica_sources_notice = match (&config.variant, &new_config.variant) {
319 (Managed(old_managed), Managed(new_managed))
320 if new_managed.replication_factor > old_managed.replication_factor
321 && new_managed.replication_factor > 1 =>
322 {
323 let sources = self.single_replica_source_names(cluster);
324 (!sources.is_empty()).then(|| {
325 AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster {
326 cluster: cluster.name.clone(),
327 sources,
328 }
329 })
330 }
331 _ => None,
332 };
333
334 if let (Managed(old_managed), Managed(new_managed)) = (&config.variant, &new_config.variant)
346 {
347 let needs_record = if reconfiguration_in_flight {
348 alter_changes_replica_shape(options)
349 } else {
350 new_managed.replica_config_shape() != old_managed.replica_config_shape()
351 };
352 let scheduled_direct =
365 !matches!(new_managed.schedule, mz_sql::plan::ClusterSchedule::Manual)
366 && !reconfiguration_in_flight;
367 if scheduled_direct && !matches!(strategy, AlterClusterPlanStrategy::None) {
374 return Err(AdapterError::AlterClusterWaitOnScheduledCluster);
375 }
376 if needs_record && !scheduled_direct {
377 let result = self
378 .reshape_alter_cluster_managed(
379 session,
380 cluster_id,
381 new_config.clone(),
382 options,
383 strategy,
384 validity,
385 )
386 .await;
387 if result.is_ok() {
388 if let Some(notice) = single_replica_sources_notice {
389 session.add_notice(notice);
390 }
391 }
392 return result;
393 }
394 }
395
396 match (&config.variant, &new_config.variant) {
397 (Managed(_), Managed(_)) => {
398 self.sequence_alter_cluster_managed_to_managed(
399 session,
400 cluster_id,
401 new_config.clone(),
402 )
403 .await?;
404 if let Some(notice) = single_replica_sources_notice {
405 session.add_notice(notice);
406 }
407 }
408 (Unmanaged, Managed(new_managed)) => {
409 if !matches!(new_managed.schedule, mz_sql::plan::ClusterSchedule::Manual)
414 && !matches!(strategy, AlterClusterPlanStrategy::None)
415 {
416 return Err(AdapterError::AlterClusterWaitOnScheduledCluster);
417 }
418 self.sequence_alter_cluster_unmanaged_to_managed(
419 session,
420 cluster_id,
421 new_config,
422 options.to_owned(),
423 )
424 .await?;
425 }
426 (Managed(_), Unmanaged) => {
427 self.sequence_alter_cluster_managed_to_unmanaged(session, cluster_id, new_config)
428 .await?;
429 }
430 (Unmanaged, Unmanaged) => {
431 self.sequence_alter_cluster_unmanaged_to_unmanaged(
432 session,
433 cluster_id,
434 new_config,
435 options.replicas.clone(),
436 )
437 .await?;
438 }
439 }
440
441 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
442 ObjectType::Cluster,
443 )))
444 }
445
446 async fn reshape_alter_cluster_managed(
482 &mut self,
483 session: &Session,
484 cluster_id: ClusterId,
485 new_config: ClusterConfig,
486 options: &PlanClusterOption,
487 strategy: &AlterClusterPlanStrategy,
488 validity: PlanValidity,
489 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
490 use mz_sql::plan::AlterOptionParameter::Unchanged;
491
492 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
493 return Err(AdapterError::Internal(
494 "reshape_alter_cluster_managed requires a managed target config".into(),
495 ));
496 };
497
498 let cluster = self.catalog.get_cluster(cluster_id);
504 let in_flight = match &cluster.config.variant {
505 ClusterVariant::Managed(managed) => managed
506 .reconfiguration
507 .as_ref()
508 .filter(|record| record.is_in_progress())
509 .cloned(),
510 ClusterVariant::Unmanaged => None,
511 };
512 let new_target = ReconfigurationTarget {
513 size: new_managed.size.clone(),
514 replication_factor: new_managed.replication_factor,
515 availability_zones: new_managed.availability_zones.clone(),
516 logging: new_managed.logging.clone(),
517 arrangement_compression: new_managed.arrangement_compression,
518 };
519 let unchanged = ReconfigurationDimensionsUnchanged {
520 size: matches!(options.size, Unchanged),
521 replication_factor: matches!(options.replication_factor, Unchanged),
522 availability_zones: matches!(options.availability_zones, Unchanged),
523 log_logging: matches!(options.introspection_debugging, Unchanged),
526 interval: matches!(options.introspection_interval, Unchanged),
527 arrangement_compression: matches!(options.arrangement_compression, Unchanged),
528 };
529 let target = fold_reconfiguration_target(
530 in_flight.as_ref().map(|r| &r.target),
531 new_target,
532 unchanged,
533 );
534
535 let role_id = session.role_metadata().current_role;
538 self.catalog.ensure_valid_replica_size(
539 &self
540 .catalog()
541 .get_role_allowed_cluster_sizes(&Some(role_id)),
542 &target.size,
543 false,
544 )?;
545 self.ensure_valid_azs(target.availability_zones.iter())?;
546
547 let cancels = match &cluster.config.variant {
548 ClusterVariant::Managed(managed) => target.matches_realized_config(managed),
549 ClusterVariant::Unmanaged => false,
550 };
551 if cluster_id.is_user() && !cancels {
557 self.validate_resource_limit(
558 0,
559 i64::from(target.replication_factor),
560 SystemVars::max_replicas_per_cluster,
561 "cluster replica",
562 MAX_REPLICAS_PER_CLUSTER.name(),
563 )?;
564 }
565
566 let now = self.now();
592 let deadline_from = |timeout: Duration| -> Timestamp {
593 now.saturating_add(u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX))
594 .into()
595 };
596 let (deadline, on_timeout) = match strategy {
597 AlterClusterPlanStrategy::None => match &in_flight {
598 Some(record) => (record.deadline, record.on_timeout),
599 None => (
600 deadline_from(
601 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT
602 .get(self.catalog().system_config().dyncfgs()),
603 ),
604 OnTimeoutAction::Rollback,
605 ),
606 },
607 AlterClusterPlanStrategy::For(timeout) => {
608 (deadline_from(*timeout), OnTimeoutAction::Rollback)
609 }
610 AlterClusterPlanStrategy::UntilReady {
611 timeout,
612 on_timeout,
613 } => (
614 deadline_from(*timeout),
615 on_timeout.unwrap_or(OnTimeoutAction::Rollback),
616 ),
617 };
618
619 let cluster = self.catalog.get_cluster(cluster_id);
628 let cluster_name = cluster.name().to_string();
629 let ClusterVariant::Managed(realized_now) = &cluster.config.variant else {
630 return Err(AdapterError::Internal(
631 "reshape_alter_cluster_managed requires a managed realized config".into(),
632 ));
633 };
634 let realized_target = realized_now.realized_reconfiguration_target();
635 let (status, audit) = if cancels {
639 (
640 ReconfigurationStatus::Cancelled,
641 ReconfigurationAudit::Cancelled,
642 )
643 } else {
644 (
645 ReconfigurationStatus::InProgress,
646 ReconfigurationAudit::Started,
647 )
648 };
649 let record = ReconfigurationState {
650 target: target.clone(),
651 deadline,
652 on_timeout,
653 status,
654 };
655
656 let mut realized = new_config.clone();
657 let ClusterVariant::Managed(realized_managed) = &mut realized.variant else {
658 return Err(AdapterError::Internal(
659 "reshape_alter_cluster_managed requires a managed target config".into(),
660 ));
661 };
662 realized_managed.apply_reconfiguration_target(realized_target);
663 realized_managed.reconfiguration = Some(record);
664
665 self.catalog_transact(
666 Some(session),
667 vec![Op::UpdateClusterConfig {
668 id: cluster_id,
669 name: cluster_name,
670 config: realized,
671 reconfiguration_audit: Some(audit),
672 burst_audit: None,
673 }],
674 )
675 .await?;
676
677 let background =
678 ENABLE_BACKGROUND_ALTER_CLUSTER.get(self.catalog().system_config().dyncfgs());
679 if background {
680 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
681 ObjectType::Cluster,
682 )));
683 }
684
685 Ok(StageResult::Immediate(Box::new(
689 ClusterStage::AwaitReconfiguration(AlterClusterAwaitReconfiguration {
690 validity,
691 cluster_id,
692 target,
693 }),
694 )))
695 }
696
697 fn await_reconfiguration_stage(
703 &self,
704 validity: PlanValidity,
705 cluster_id: ClusterId,
706 target: ReconfigurationTarget,
707 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
708 let Some(cluster) = self.catalog().try_get_cluster(cluster_id) else {
709 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
712 ObjectType::Cluster,
713 )));
714 };
715 let record = match &cluster.config.variant {
716 ClusterVariant::Managed(managed) => managed.reconfiguration.clone(),
717 ClusterVariant::Unmanaged => None,
718 };
719
720 let realized_matches_target = match &cluster.config.variant {
721 ClusterVariant::Managed(managed) => target.matches_realized_config(managed),
722 ClusterVariant::Unmanaged => false,
723 };
724
725 match reconfiguration_wait_result(record.as_ref(), &target, realized_matches_target) {
726 Some(result) => {
727 result?;
728 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
729 ObjectType::Cluster,
730 )))
731 }
732 None => {
733 let poll_duration = self
743 .catalog
744 .system_config()
745 .cluster_alter_check_ready_interval();
746 let span = Span::current();
747 Ok(StageResult::Handle(mz_ore::task::spawn(
748 || "Await Cluster Reconfiguration",
749 async move {
750 tokio::time::sleep(poll_duration).await;
751 Ok(Box::new(ClusterStage::AwaitReconfiguration(
752 AlterClusterAwaitReconfiguration {
753 validity,
754 cluster_id,
755 target,
756 },
757 )))
758 }
759 .instrument(span),
760 )))
761 }
762 }
763 }
764
765 pub(crate) async fn sequence_create_cluster(
766 &mut self,
767 session: &Session,
768 CreateClusterPlan {
769 name,
770 variant,
771 workload_class,
772 if_not_exists,
773 }: CreateClusterPlan,
774 ) -> Result<ExecuteResponse, AdapterError> {
775 tracing::debug!("sequence_create_cluster");
776
777 let id_ts = self.get_catalog_write_ts().await;
778 let id = self.catalog().allocate_user_cluster_id(id_ts).await?;
779 let introspection_sources = BUILTINS::logs().collect();
784 let cluster_variant = match &variant {
785 CreateClusterVariant::Managed(plan) => {
786 let logging = if let Some(config) = plan.compute.introspection {
787 ReplicaLogging {
788 log_logging: config.debugging,
789 interval: Some(config.interval),
790 }
791 } else {
792 ReplicaLogging::default()
793 };
794 ClusterVariant::Managed(ClusterVariantManaged {
795 size: plan.size.clone(),
796 availability_zones: plan.availability_zones.clone(),
797 logging,
798 arrangement_compression: plan.compute.arrangement_compression,
799 replication_factor: plan.replication_factor,
800 optimizer_feature_overrides: plan.optimizer_feature_overrides.clone(),
801 schedule: plan.schedule.clone(),
802 auto_scaling_strategy: plan.auto_scaling_strategy.clone(),
803 reconfiguration: None,
804 burst: None,
805 })
806 }
807 CreateClusterVariant::Unmanaged(_) => ClusterVariant::Unmanaged,
808 };
809 let config = ClusterConfig {
810 variant: cluster_variant,
811 workload_class,
812 };
813 let ops = vec![catalog::Op::CreateCluster {
814 id,
815 name: name.clone(),
816 introspection_sources,
817 owner_id: *session.current_role_id(),
818 config,
819 }];
820
821 match variant {
822 CreateClusterVariant::Managed(plan) => {
823 self.sequence_create_managed_cluster(session, plan, id, ops)
824 .await
825 }
826 CreateClusterVariant::Unmanaged(plan) => {
827 self.sequence_create_unmanaged_cluster(session, plan, id, ops)
828 .await
829 }
830 }
831 .or_else(|err| match err {
832 AdapterError::Catalog(mz_catalog::memory::error::Error {
833 kind: ErrorKind::Sql(CatalogError::ClusterAlreadyExists(_)),
834 }) if if_not_exists => {
835 session.add_notice(AdapterNotice::ObjectAlreadyExists {
836 name,
837 ty: "cluster",
838 });
839 Ok(ExecuteResponse::CreatedCluster)
840 }
841 err => Err(err),
842 })
843 }
844
845 #[mz_ore::instrument(level = "debug")]
846 async fn sequence_create_managed_cluster(
847 &mut self,
848 session: &Session,
849 CreateClusterManagedPlan {
850 availability_zones,
851 compute,
852 replication_factor,
853 size,
854 optimizer_feature_overrides: _,
855 schedule: _,
856 auto_scaling_strategy,
857 }: CreateClusterManagedPlan,
858 cluster_id: ClusterId,
859 mut ops: Vec<catalog::Op>,
860 ) -> Result<ExecuteResponse, AdapterError> {
861 tracing::debug!("sequence_create_managed_cluster");
862
863 self.ensure_valid_azs(availability_zones.iter())?;
864
865 let role_id = session.role_metadata().current_role;
866 self.catalog.ensure_valid_replica_size(
867 &self
868 .catalog()
869 .get_role_allowed_cluster_sizes(&Some(role_id)),
870 &size,
871 false,
872 )?;
873 if let Some(on_hydration) = auto_scaling_strategy
879 .as_ref()
880 .and_then(|strategy| strategy.on_hydration.as_ref())
881 {
882 self.catalog.ensure_valid_replica_size(
883 &self
884 .catalog()
885 .get_role_allowed_cluster_sizes(&Some(role_id)),
886 &on_hydration.hydration_size,
887 false,
888 )?;
889 }
890
891 if cluster_id.is_user() {
896 self.validate_resource_limit(
897 0,
898 i64::from(replication_factor),
899 SystemVars::max_replicas_per_cluster,
900 "cluster replica",
901 MAX_REPLICAS_PER_CLUSTER.name(),
902 )?;
903 }
904
905 let id_ts = self.get_catalog_write_ts().await;
909 let replica_ids = self
910 .catalog()
911 .allocate_replica_ids(cluster_id, u64::from(replication_factor), id_ts)
912 .await?;
913
914 for (replica_id, replica_name) in replica_ids
915 .into_iter()
916 .zip_eq((0..replication_factor).map(managed_cluster_replica_name))
917 {
918 self.create_managed_cluster_replica_op(
919 cluster_id,
920 replica_id,
921 replica_name.clone(),
922 &compute,
923 &size,
924 &mut ops,
925 if availability_zones.is_empty() {
926 None
927 } else {
928 Some(availability_zones.as_ref())
929 },
930 false,
931 *session.current_role_id(),
932 ReplicaCreateDropReason::Manual,
933 )?;
934 }
935
936 self.catalog_transact(Some(session), ops).await?;
937
938 Ok(ExecuteResponse::CreatedCluster)
939 }
940
941 fn create_managed_cluster_replica_op(
942 &self,
943 cluster_id: ClusterId,
944 replica_id: ReplicaId,
945 name: String,
946 compute: &mz_sql::plan::ComputeReplicaConfig,
947 size: &String,
948 ops: &mut Vec<Op>,
949 azs: Option<&[String]>,
950 pending: bool,
951 owner_id: RoleId,
952 reason: ReplicaCreateDropReason,
953 ) -> Result<(), AdapterError> {
954 let location = mz_catalog::durable::ReplicaLocation::Managed {
955 availability_zones: Vec::new(),
958 billed_as: None,
959 internal: false,
960 size: size.clone(),
961 pending,
962 };
963
964 let logging = if let Some(config) = compute.introspection {
965 ReplicaLogging {
966 log_logging: config.debugging,
967 interval: Some(config.interval),
968 }
969 } else {
970 ReplicaLogging::default()
971 };
972
973 let config = ReplicaConfig {
974 location: self.catalog().concretize_replica_location(
975 location,
976 &self
977 .catalog()
978 .get_role_allowed_cluster_sizes(&Some(owner_id)),
979 azs,
980 false,
981 )?,
982 compute: ComputeReplicaConfig {
983 logging,
984 arrangement_compression: compute.arrangement_compression,
985 },
986 };
987
988 ops.push(catalog::Op::CreateClusterReplica {
991 cluster_id,
992 replica_id,
993 name,
994 config,
995 owner_id,
996 reason,
997 });
998 Ok(())
999 }
1000
1001 fn ensure_valid_azs<'a, I: IntoIterator<Item = &'a String>>(
1002 &self,
1003 azs: I,
1004 ) -> Result<(), AdapterError> {
1005 let cat_azs = self.catalog().state().availability_zones();
1006 for az in azs.into_iter() {
1007 if !cat_azs.contains(az) {
1008 return Err(AdapterError::InvalidClusterReplicaAz {
1009 az: az.to_string(),
1010 expected: cat_azs.to_vec(),
1011 });
1012 }
1013 }
1014 Ok(())
1015 }
1016
1017 #[mz_ore::instrument(level = "debug")]
1018 async fn sequence_create_unmanaged_cluster(
1019 &mut self,
1020 session: &Session,
1021 CreateClusterUnmanagedPlan { replicas }: CreateClusterUnmanagedPlan,
1022 id: ClusterId,
1023 mut ops: Vec<catalog::Op>,
1024 ) -> Result<ExecuteResponse, AdapterError> {
1025 tracing::debug!("sequence_create_unmanaged_cluster");
1026
1027 self.ensure_valid_azs(replicas.iter().filter_map(|(_, r)| {
1028 if let mz_sql::plan::ReplicaConfig::Orchestrated {
1029 availability_zone: Some(az),
1030 ..
1031 } = &r
1032 {
1033 Some(az)
1034 } else {
1035 None
1036 }
1037 }))?;
1038
1039 if id.is_user() {
1044 self.validate_resource_limit(
1045 0,
1046 i64::try_from(replicas.len()).unwrap_or(i64::MAX),
1047 SystemVars::max_replicas_per_cluster,
1048 "cluster replica",
1049 MAX_REPLICAS_PER_CLUSTER.name(),
1050 )?;
1051 }
1052
1053 let id_ts = self.get_catalog_write_ts().await;
1057 let replica_ids = self
1058 .catalog()
1059 .allocate_replica_ids(id, u64::cast_from(replicas.len()), id_ts)
1060 .await?;
1061
1062 for (replica_id, (replica_name, replica_config)) in replica_ids.into_iter().zip_eq(replicas)
1063 {
1064 let (compute, location) = match replica_config {
1067 mz_sql::plan::ReplicaConfig::Unorchestrated {
1068 storagectl_addrs,
1069 computectl_addrs,
1070 compute,
1071 } => {
1072 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1073 storagectl_addrs,
1074 computectl_addrs,
1075 };
1076 (compute, location)
1077 }
1078 mz_sql::plan::ReplicaConfig::Orchestrated {
1079 availability_zone,
1080 billed_as,
1081 compute,
1082 internal,
1083 size,
1084 } => {
1085 if !session.user().is_internal() && (internal || billed_as.is_some()) {
1087 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1088 }
1089 if billed_as.is_some() && !internal {
1091 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1092 }
1093 if let Some(billed_as) = &billed_as {
1096 self.ensure_valid_billed_as_size(billed_as)?;
1097 }
1098
1099 let location = mz_catalog::durable::ReplicaLocation::Managed {
1100 availability_zones: availability_zone.into_iter().collect(),
1103 billed_as,
1104 internal,
1105 size: size.clone(),
1106 pending: false,
1107 };
1108 (compute, location)
1109 }
1110 };
1111
1112 let logging = if let Some(config) = compute.introspection {
1113 ReplicaLogging {
1114 log_logging: config.debugging,
1115 interval: Some(config.interval),
1116 }
1117 } else {
1118 ReplicaLogging::default()
1119 };
1120
1121 let role_id = session.role_metadata().current_role;
1122 let config = ReplicaConfig {
1123 location: self.catalog().concretize_replica_location(
1124 location,
1125 &self
1126 .catalog()
1127 .get_role_allowed_cluster_sizes(&Some(role_id)),
1128 None,
1129 false,
1130 )?,
1131 compute: ComputeReplicaConfig {
1132 logging,
1133 arrangement_compression: compute.arrangement_compression,
1134 },
1135 };
1136
1137 ops.push(catalog::Op::CreateClusterReplica {
1138 cluster_id: id,
1139 replica_id,
1140 name: replica_name.clone(),
1141 config,
1142 owner_id: *session.current_role_id(),
1143 reason: ReplicaCreateDropReason::Manual,
1144 });
1145 }
1146
1147 self.catalog_transact(Some(session), ops).await?;
1148
1149 Ok(ExecuteResponse::CreatedCluster)
1150 }
1151
1152 fn single_replica_source_names(&self, cluster: &Cluster) -> Vec<String> {
1156 cluster
1157 .bound_objects
1158 .iter()
1159 .filter_map(|id| {
1160 let entry = self.catalog().get_entry(id);
1161 let single_replica =
1162 entry
1163 .source()
1164 .is_some_and(|source| match &source.data_source {
1165 DataSourceDesc::Ingestion { desc, .. }
1166 | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
1167 desc.connection.prefers_single_replica()
1168 }
1169 _ => false,
1170 });
1171 single_replica.then(|| {
1172 self.catalog()
1173 .resolve_full_name(entry.name(), None)
1174 .to_string()
1175 })
1176 })
1177 .collect()
1178 }
1179
1180 fn notice_relevant_replica_count(&self, cluster: &Cluster) -> usize {
1192 match &cluster.config.variant {
1193 ClusterVariant::Managed(managed) => {
1194 let replication_factor = managed
1195 .reconfiguration
1196 .as_ref()
1197 .filter(|record| record.is_in_progress())
1198 .map_or(managed.replication_factor, |record| {
1199 record.target.replication_factor
1200 });
1201 let manual_replicas = cluster
1202 .replicas()
1203 .filter(|r| {
1204 r.config.location.internal() || r.config.location.billed_as().is_some()
1205 })
1206 .count();
1207 usize::cast_from(replication_factor) + manual_replicas
1208 }
1209 ClusterVariant::Unmanaged => cluster.replicas().count(),
1210 }
1211 }
1212
1213 pub(crate) fn notify_single_replica_sources(
1222 &self,
1223 session: &Session,
1224 cluster: &Cluster,
1225 creating_source: Option<&QualifiedItemName>,
1226 ) {
1227 if self.notice_relevant_replica_count(cluster) <= 1 {
1228 return;
1229 }
1230 let mut sources = self.single_replica_source_names(cluster);
1231 if let Some(name) = creating_source {
1232 let full_name = self.catalog().resolve_full_name(name, None).to_string();
1233 if !sources.contains(&full_name) {
1234 sources.push(full_name);
1235 }
1236 }
1237 if !sources.is_empty() {
1238 session.add_notice(AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster {
1239 cluster: cluster.name.clone(),
1240 sources,
1241 });
1242 }
1243 }
1244
1245 fn ensure_valid_billed_as_size(&self, size: &str) -> Result<(), AdapterError> {
1252 if self.catalog().cluster_replica_sizes().0.contains_key(size) {
1253 Ok(())
1254 } else {
1255 coord_bail!("unknown cluster replica size {size} in BILLED AS")
1256 }
1257 }
1258
1259 #[mz_ore::instrument(level = "debug")]
1260 pub(crate) async fn sequence_create_cluster_replica(
1261 &mut self,
1262 session: &Session,
1263 CreateClusterReplicaPlan {
1264 name,
1265 cluster_id,
1266 config,
1267 if_not_exists,
1268 }: CreateClusterReplicaPlan,
1269 ) -> Result<ExecuteResponse, AdapterError> {
1270 let (compute, location) = match config {
1272 mz_sql::plan::ReplicaConfig::Unorchestrated {
1273 storagectl_addrs,
1274 computectl_addrs,
1275 compute,
1276 } => {
1277 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1278 storagectl_addrs,
1279 computectl_addrs,
1280 };
1281 (compute, location)
1282 }
1283 mz_sql::plan::ReplicaConfig::Orchestrated {
1284 availability_zone,
1285 billed_as,
1286 compute,
1287 internal,
1288 size,
1289 } => {
1290 let availability_zone = match availability_zone {
1291 Some(az) => {
1292 self.ensure_valid_azs([&az])?;
1293 Some(az)
1294 }
1295 None => None,
1296 };
1297 let location = mz_catalog::durable::ReplicaLocation::Managed {
1298 availability_zones: availability_zone.into_iter().collect(),
1301 billed_as,
1302 internal,
1303 size,
1304 pending: false,
1305 };
1306 (compute, location)
1307 }
1308 };
1309
1310 let logging = if let Some(config) = compute.introspection {
1311 ReplicaLogging {
1312 log_logging: config.debugging,
1313 interval: Some(config.interval),
1314 }
1315 } else {
1316 ReplicaLogging::default()
1317 };
1318
1319 let role_id = session.role_metadata().current_role;
1320 let config = ReplicaConfig {
1321 location: self.catalog().concretize_replica_location(
1322 location,
1323 &self
1324 .catalog()
1325 .get_role_allowed_cluster_sizes(&Some(role_id)),
1326 None,
1329 false,
1330 )?,
1331 compute: ComputeReplicaConfig {
1332 logging,
1333 arrangement_compression: compute.arrangement_compression,
1334 },
1335 };
1336
1337 let cluster = self.catalog().get_cluster(cluster_id);
1338
1339 if let ReplicaLocation::Managed(ManagedReplicaLocation {
1340 internal,
1341 billed_as,
1342 ..
1343 }) = &config.location
1344 {
1345 if !session.user().is_internal() && (*internal || billed_as.is_some()) {
1347 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1348 }
1349 if cluster.is_managed() && !*internal {
1351 coord_bail!("must specify INTERNAL when creating a replica in a managed cluster");
1352 }
1353 if billed_as.is_some() && !*internal {
1355 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1356 }
1357 if let Some(billed_as) = billed_as {
1360 self.ensure_valid_billed_as_size(billed_as)?;
1361 }
1362 }
1363
1364 let owner_id = cluster.owner_id();
1367
1368 let cluster_name = cluster.name.clone();
1369 let qualified_name = format!("{cluster_name}.{name}");
1372
1373 let id_ts = self.get_catalog_write_ts().await;
1378 let replica_id = self
1379 .catalog()
1380 .allocate_replica_ids(cluster_id, 1, id_ts)
1381 .await?
1382 .into_element();
1383
1384 let ops = vec![catalog::Op::CreateClusterReplica {
1385 cluster_id,
1386 replica_id,
1387 name: name.clone(),
1388 config,
1389 owner_id,
1390 reason: ReplicaCreateDropReason::Manual,
1391 }];
1392
1393 match self.catalog_transact(Some(session), ops).await {
1394 Ok(()) => {
1395 self.notify_single_replica_sources(
1398 session,
1399 self.catalog().get_cluster(cluster_id),
1400 None,
1401 );
1402 Ok(ExecuteResponse::CreatedClusterReplica)
1403 }
1404 Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
1405 kind: ErrorKind::Sql(CatalogError::DuplicateReplica(_, _)),
1406 })) if if_not_exists => {
1407 session.add_notice(AdapterNotice::ObjectAlreadyExists {
1408 name: qualified_name,
1409 ty: "cluster replica",
1410 });
1411 Ok(ExecuteResponse::CreatedClusterReplica)
1412 }
1413 Err(err) => Err(err),
1414 }
1415 }
1416
1417 pub(crate) async fn sequence_alter_cluster_managed_to_managed(
1431 &mut self,
1432 session: &Session,
1433 cluster_id: ClusterId,
1434 new_config: ClusterConfig,
1435 ) -> Result<(), AdapterError> {
1436 let cluster = self.catalog.get_cluster(cluster_id);
1437 let name = cluster.name().to_string();
1438
1439 let ClusterVariant::Managed(ClusterVariantManaged {
1440 size,
1441 availability_zones,
1442 logging,
1443 arrangement_compression,
1444 replication_factor,
1445 optimizer_feature_overrides: _,
1446 schedule: _,
1447 auto_scaling_strategy,
1448 reconfiguration,
1449 burst: _,
1450 }) = &cluster.config.variant
1451 else {
1452 panic!("expected existing managed cluster config");
1453 };
1454 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
1455 panic!("expected new managed cluster config");
1456 };
1457 let ClusterVariantManaged {
1458 size: new_size,
1459 replication_factor: new_replication_factor,
1460 availability_zones: new_availability_zones,
1461 logging: _,
1462 arrangement_compression: _,
1463 optimizer_feature_overrides: _,
1464 schedule: _,
1465 auto_scaling_strategy: new_auto_scaling_strategy,
1466 reconfiguration: _,
1467 burst: _,
1468 } = new_managed;
1469
1470 let role_id = Some(session.role_metadata().current_role);
1471 self.catalog.ensure_valid_replica_size(
1472 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
1473 new_size,
1474 false,
1475 )?;
1476 if new_auto_scaling_strategy != auto_scaling_strategy {
1482 if let Some(on_hydration) = new_auto_scaling_strategy
1483 .as_ref()
1484 .and_then(|strategy| strategy.on_hydration.as_ref())
1485 {
1486 self.catalog.ensure_valid_replica_size(
1487 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
1488 &on_hydration.hydration_size,
1489 false,
1490 )?;
1491 if reconfiguration.as_ref().is_some_and(|record| {
1498 record.is_in_progress() && record.target.size == on_hydration.hydration_size
1499 }) {
1500 coord_bail!(
1501 "HYDRATION SIZE must differ from the target SIZE \
1502 ('{}') of the in-progress cluster resize",
1503 on_hydration.hydration_size
1504 );
1505 }
1506 }
1507 }
1508
1509 if new_replication_factor > replication_factor && cluster_id.is_user() {
1518 self.validate_resource_limit(
1519 usize::cast_from(*replication_factor),
1520 i64::from(*new_replication_factor) - i64::from(*replication_factor),
1521 SystemVars::max_replicas_per_cluster,
1522 "cluster replica",
1523 MAX_REPLICAS_PER_CLUSTER.name(),
1524 )?;
1525
1526 let credits_per_replica = self
1527 .catalog()
1528 .cluster_replica_sizes()
1529 .0
1530 .get(new_size)
1531 .expect("new replica size was validated")
1532 .credits_per_hour;
1533 let baseline_credits = credits_per_replica * Numeric::from(*new_replication_factor);
1534 self.validate_resource_limit_numeric(
1535 self.current_credit_consumption_rate(Some(cluster_id)),
1536 baseline_credits,
1537 |system_vars| {
1538 self.license_key
1539 .max_credit_consumption_rate()
1540 .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
1541 },
1542 "cluster replica",
1543 MAX_CREDIT_CONSUMPTION_RATE.name(),
1544 )?;
1545 }
1546
1547 let config_changed = new_managed.replica_config_shape()
1548 != ManagedReplicaConfigShape::new(
1549 size,
1550 availability_zones,
1551 logging,
1552 *arrangement_compression,
1553 );
1554 if config_changed {
1558 self.ensure_valid_azs(new_availability_zones.iter())?;
1559 }
1560
1561 let ops = vec![catalog::Op::UpdateClusterConfig {
1564 id: cluster_id,
1565 name,
1566 config: new_config,
1567 reconfiguration_audit: None,
1568 burst_audit: None,
1569 }];
1570
1571 self.catalog_transact(Some(session), ops).await?;
1572 Ok(())
1573 }
1574
1575 async fn sequence_alter_cluster_unmanaged_to_managed(
1579 &mut self,
1580 session: &Session,
1581 cluster_id: ClusterId,
1582 mut new_config: ClusterConfig,
1583 options: PlanClusterOption,
1584 ) -> Result<(), AdapterError> {
1585 let cluster = self.catalog.get_cluster(cluster_id);
1586 let cluster_name = cluster.name().to_string();
1587
1588 let ClusterVariant::Managed(ClusterVariantManaged {
1589 size: new_size,
1590 replication_factor: new_replication_factor,
1591 availability_zones: new_availability_zones,
1592 logging: _,
1593 arrangement_compression: _,
1594 optimizer_feature_overrides: _,
1595 schedule: _,
1596 auto_scaling_strategy: _,
1597 reconfiguration: _,
1598 burst: _,
1599 }) = &mut new_config.variant
1600 else {
1601 panic!("expected new managed cluster config");
1602 };
1603
1604 let user_replica_count = cluster
1606 .user_replicas()
1607 .count()
1608 .try_into()
1609 .expect("must_fit");
1610 match options.replication_factor {
1611 AlterOptionParameter::Set(_) => {
1612 if user_replica_count != *new_replication_factor {
1614 coord_bail!(
1615 "REPLICATION FACTOR {new_replication_factor} does not match number of replicas ({user_replica_count})"
1616 );
1617 }
1618 }
1619 _ => {
1620 *new_replication_factor = user_replica_count;
1621 }
1622 }
1623
1624 let mut names = BTreeSet::new();
1625 let mut sizes = BTreeSet::new();
1626
1627 self.ensure_valid_azs(new_availability_zones.iter())?;
1628
1629 for replica in cluster.user_replicas() {
1631 names.insert(replica.name.clone());
1632 match &replica.config.location {
1633 ReplicaLocation::Unmanaged(_) => coord_bail!(
1634 "Cannot convert unmanaged cluster with unmanaged replicas to managed cluster"
1635 ),
1636 ReplicaLocation::Managed(location) => {
1637 sizes.insert(location.size.clone());
1638
1639 for az in &location.availability_zones {
1643 if !new_availability_zones.contains(az) {
1644 coord_bail!(
1645 "unmanaged replica has availability zone {az} which is not \
1646 in managed {new_availability_zones:?}"
1647 )
1648 }
1649 }
1650 }
1651 }
1652 }
1653
1654 if sizes.is_empty() {
1655 assert!(
1656 cluster.user_replicas().next().is_none(),
1657 "Cluster should not have replicas"
1658 );
1659 match &options.size {
1661 AlterOptionParameter::Reset | AlterOptionParameter::Unchanged => {
1662 coord_bail!("Missing SIZE for empty cluster")
1663 }
1664 AlterOptionParameter::Set(_) => {} }
1666 } else if sizes.len() == 1 {
1667 let size = sizes.into_iter().next().expect("must exist");
1668 match &options.size {
1669 AlterOptionParameter::Set(sz) if *sz != size => {
1670 coord_bail!("Cluster replicas of size {size} do not match expected SIZE {sz}");
1671 }
1672 _ => *new_size = size,
1673 }
1674 } else {
1675 let formatted = sizes
1676 .iter()
1677 .map(String::as_str)
1678 .collect::<Vec<_>>()
1679 .join(", ");
1680 coord_bail!(
1681 "Cannot convert unmanaged cluster to managed, non-unique replica sizes: {formatted}"
1682 );
1683 }
1684
1685 for i in 0..*new_replication_factor {
1686 let name = managed_cluster_replica_name(i);
1687 names.remove(&name);
1688 }
1689 if !names.is_empty() {
1690 let formatted = names
1691 .iter()
1692 .map(String::as_str)
1693 .collect::<Vec<_>>()
1694 .join(", ");
1695 coord_bail!(
1696 "Cannot convert unmanaged cluster to managed, invalid replica names: {formatted}"
1697 );
1698 }
1699
1700 let ops = vec![catalog::Op::UpdateClusterConfig {
1701 id: cluster_id,
1702 name: cluster_name,
1703 config: new_config,
1704 reconfiguration_audit: None,
1705 burst_audit: None,
1706 }];
1707
1708 self.catalog_transact(Some(session), ops).await?;
1709 Ok(())
1710 }
1711
1712 async fn sequence_alter_cluster_managed_to_unmanaged(
1713 &mut self,
1714 session: &Session,
1715 cluster_id: ClusterId,
1716 new_config: ClusterConfig,
1717 ) -> Result<(), AdapterError> {
1718 let cluster = self.catalog().get_cluster(cluster_id);
1719
1720 if let ClusterVariant::Managed(managed) = &cluster.config.variant {
1726 if managed
1727 .reconfiguration
1728 .as_ref()
1729 .is_some_and(|record| record.is_in_progress())
1730 {
1731 return Err(AdapterError::AlterClusterUnmanagedWhileReconfiguring);
1732 }
1733 if managed.burst.is_some() {
1740 return Err(AdapterError::AlterClusterUnmanagedWhileBursting);
1741 }
1742 }
1743
1744 let ops = vec![catalog::Op::UpdateClusterConfig {
1745 id: cluster_id,
1746 name: cluster.name().to_string(),
1747 config: new_config,
1748 reconfiguration_audit: None,
1749 burst_audit: None,
1750 }];
1751
1752 self.catalog_transact(Some(session), ops).await?;
1753 Ok(())
1754 }
1755
1756 async fn sequence_alter_cluster_unmanaged_to_unmanaged(
1757 &mut self,
1758 session: &Session,
1759 cluster_id: ClusterId,
1760 new_config: ClusterConfig,
1761 replicas: AlterOptionParameter<Vec<(String, mz_sql::plan::ReplicaConfig)>>,
1762 ) -> Result<(), AdapterError> {
1763 if !matches!(replicas, AlterOptionParameter::Unchanged) {
1764 coord_bail!("Cannot alter replicas in unmanaged cluster");
1765 }
1766
1767 let cluster = self.catalog().get_cluster(cluster_id);
1768
1769 let ops = vec![catalog::Op::UpdateClusterConfig {
1770 id: cluster_id,
1771 name: cluster.name().to_string(),
1772 config: new_config,
1773 reconfiguration_audit: None,
1774 burst_audit: None,
1775 }];
1776
1777 self.catalog_transact(Some(session), ops).await?;
1778 Ok(())
1779 }
1780
1781 pub(crate) async fn sequence_alter_cluster_rename(
1782 &mut self,
1783 ctx: &mut ExecuteContext,
1784 AlterClusterRenamePlan { id, name, to_name }: AlterClusterRenamePlan,
1785 ) -> Result<ExecuteResponse, AdapterError> {
1786 let op = Op::RenameCluster {
1787 id,
1788 name,
1789 to_name,
1790 check_reserved_names: true,
1791 };
1792 match self
1793 .catalog_transact_with_ddl_transaction(ctx, vec![op], |_, _| Box::pin(async {}))
1794 .await
1795 {
1796 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
1797 Err(err) => Err(err),
1798 }
1799 }
1800
1801 pub(crate) async fn sequence_alter_cluster_swap(
1802 &mut self,
1803 ctx: &mut ExecuteContext,
1804 AlterClusterSwapPlan {
1805 id_a,
1806 id_b,
1807 name_a,
1808 name_b,
1809 name_temp,
1810 }: AlterClusterSwapPlan,
1811 ) -> Result<ExecuteResponse, AdapterError> {
1812 let op_a = Op::RenameCluster {
1813 id: id_a,
1814 name: name_a.clone(),
1815 to_name: name_temp.clone(),
1816 check_reserved_names: false,
1817 };
1818 let op_b = Op::RenameCluster {
1819 id: id_b,
1820 name: name_b.clone(),
1821 to_name: name_a,
1822 check_reserved_names: false,
1823 };
1824 let op_temp = Op::RenameCluster {
1825 id: id_a,
1826 name: name_temp,
1827 to_name: name_b,
1828 check_reserved_names: false,
1829 };
1830
1831 match self
1832 .catalog_transact_with_ddl_transaction(ctx, vec![op_a, op_b, op_temp], |_, _| {
1833 Box::pin(async {})
1834 })
1835 .await
1836 {
1837 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
1838 Err(err) => Err(err),
1839 }
1840 }
1841
1842 pub(crate) async fn sequence_alter_cluster_replica_rename(
1843 &mut self,
1844 session: &Session,
1845 AlterClusterReplicaRenamePlan {
1846 cluster_id,
1847 replica_id,
1848 name,
1849 to_name,
1850 }: AlterClusterReplicaRenamePlan,
1851 ) -> Result<ExecuteResponse, AdapterError> {
1852 let op = catalog::Op::RenameClusterReplica {
1853 cluster_id,
1854 replica_id,
1855 name,
1856 to_name,
1857 };
1858 match self.catalog_transact(Some(session), vec![op]).await {
1859 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::ClusterReplica)),
1860 Err(err) => Err(err),
1861 }
1862 }
1863
1864 pub(crate) async fn sequence_alter_set_cluster(
1866 &self,
1867 _session: &Session,
1868 AlterSetClusterPlan { id, set_cluster: _ }: AlterSetClusterPlan,
1869 ) -> Result<ExecuteResponse, AdapterError> {
1870 async {}.await;
1874 let entry = self.catalog().get_entry(&id);
1875 match entry.item().typ() {
1876 _ => {
1877 Err(AdapterError::Unsupported("ALTER SET CLUSTER"))
1879 }
1880 }
1881 }
1882}
1883
1884struct ReconfigurationDimensionsUnchanged {
1889 size: bool,
1890 replication_factor: bool,
1891 availability_zones: bool,
1892 log_logging: bool,
1893 interval: bool,
1894 arrangement_compression: bool,
1895}
1896
1897fn reconfiguration_wait_result(
1903 record: Option<&ReconfigurationState>,
1904 awaited_target: &ReconfigurationTarget,
1905 realized_matches_target: bool,
1906) -> Option<Result<(), AdapterError>> {
1907 if realized_matches_target {
1908 return Some(Ok(()));
1909 }
1910 let Some(record) = record.filter(|record| record.target == *awaited_target) else {
1911 return Some(Err(AdapterError::AlterClusterSuperseded));
1912 };
1913 match record.status {
1914 ReconfigurationStatus::InProgress => None,
1915 ReconfigurationStatus::ResourceExhausted => {
1916 Some(Err(AdapterError::AlterClusterResourceExhausted))
1917 }
1918 ReconfigurationStatus::TimedOut => Some(Err(AdapterError::AlterClusterTimeout)),
1919 ReconfigurationStatus::Finalized | ReconfigurationStatus::Cancelled => {
1920 Some(Err(AdapterError::AlterClusterSuperseded))
1921 }
1922 }
1923}
1924
1925fn alter_changes_replica_shape(options: &PlanClusterOption) -> bool {
1937 use mz_sql::plan::AlterOptionParameter::Unchanged;
1938 let PlanClusterOption {
1939 availability_zones,
1940 introspection_debugging,
1941 introspection_interval,
1942 arrangement_compression,
1943 managed: _,
1944 replicas: _,
1945 replication_factor: _,
1946 size,
1947 schedule: _,
1948 workload_class: _,
1949 auto_scaling_strategy: _,
1950 } = options;
1951 !matches!(size, Unchanged)
1952 || !matches!(availability_zones, Unchanged)
1953 || !matches!(introspection_debugging, Unchanged)
1954 || !matches!(introspection_interval, Unchanged)
1955 || !matches!(arrangement_compression, Unchanged)
1956}
1957
1958fn fold_reconfiguration_target(
1976 in_flight: Option<&ReconfigurationTarget>,
1977 new_target: ReconfigurationTarget,
1978 unchanged: ReconfigurationDimensionsUnchanged,
1979) -> ReconfigurationTarget {
1980 let Some(prev) = in_flight else {
1981 return new_target;
1982 };
1983 ReconfigurationTarget {
1984 size: if unchanged.size {
1985 prev.size.clone()
1986 } else {
1987 new_target.size
1988 },
1989 replication_factor: if unchanged.replication_factor {
1990 prev.replication_factor
1991 } else {
1992 new_target.replication_factor
1993 },
1994 availability_zones: if unchanged.availability_zones {
1995 prev.availability_zones.clone()
1996 } else {
1997 new_target.availability_zones
1998 },
1999 logging: ReplicaLogging {
2000 log_logging: if unchanged.log_logging {
2001 prev.logging.log_logging
2002 } else {
2003 new_target.logging.log_logging
2004 },
2005 interval: if unchanged.interval {
2006 prev.logging.interval
2007 } else {
2008 new_target.logging.interval
2009 },
2010 },
2011 arrangement_compression: if unchanged.arrangement_compression {
2012 prev.arrangement_compression
2013 } else {
2014 new_target.arrangement_compression
2015 },
2016 }
2017}
2018
2019#[cfg(test)]
2020mod tests {
2021 use mz_controller::clusters::ReplicaLogging;
2022 use mz_controller_types::DEFAULT_REPLICA_LOGGING_INTERVAL;
2023
2024 use super::*;
2025
2026 fn target(size: &str, rf: u32, azs: &[&str], log_logging: bool) -> ReconfigurationTarget {
2027 ReconfigurationTarget {
2028 size: size.to_string(),
2029 replication_factor: rf,
2030 availability_zones: azs.iter().map(|s| s.to_string()).collect(),
2031 logging: ReplicaLogging {
2032 log_logging,
2033 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
2034 },
2035 arrangement_compression: false,
2036 }
2037 }
2038
2039 fn all_changed() -> ReconfigurationDimensionsUnchanged {
2040 ReconfigurationDimensionsUnchanged {
2041 size: false,
2042 replication_factor: false,
2043 availability_zones: false,
2044 log_logging: false,
2045 interval: false,
2046 arrangement_compression: false,
2047 }
2048 }
2049
2050 fn all_unchanged() -> ReconfigurationDimensionsUnchanged {
2051 ReconfigurationDimensionsUnchanged {
2052 size: true,
2053 replication_factor: true,
2054 availability_zones: true,
2055 log_logging: true,
2056 interval: true,
2057 arrangement_compression: true,
2058 }
2059 }
2060
2061 #[mz_ore::test]
2062 fn foreground_wait_succeeds_when_target_is_realized() {
2063 let awaited = target("200cc", 1, &[], false);
2064 let record = ReconfigurationState {
2065 target: target("300cc", 1, &[], false),
2066 deadline: Timestamp::from(0),
2067 on_timeout: OnTimeoutAction::Rollback,
2068 status: ReconfigurationStatus::InProgress,
2069 };
2070
2071 assert!(matches!(
2072 reconfiguration_wait_result(Some(&record), &awaited, true),
2073 Some(Ok(()))
2074 ));
2075 }
2076
2077 #[mz_ore::test]
2078 fn foreground_wait_follows_matching_target() {
2079 let awaited = target("200cc", 1, &[], false);
2080 let mut record = ReconfigurationState {
2081 target: awaited.clone(),
2082 deadline: Timestamp::from(0),
2083 on_timeout: OnTimeoutAction::Rollback,
2084 status: ReconfigurationStatus::InProgress,
2085 };
2086
2087 assert!(reconfiguration_wait_result(Some(&record), &awaited, false).is_none());
2088
2089 record.status = ReconfigurationStatus::ResourceExhausted;
2090 assert!(matches!(
2091 reconfiguration_wait_result(Some(&record), &awaited, false),
2092 Some(Err(AdapterError::AlterClusterResourceExhausted))
2093 ));
2094
2095 record.status = ReconfigurationStatus::TimedOut;
2096 assert!(matches!(
2097 reconfiguration_wait_result(Some(&record), &awaited, false),
2098 Some(Err(AdapterError::AlterClusterTimeout))
2099 ));
2100 }
2101
2102 #[mz_ore::test]
2103 fn foreground_wait_reports_superseded_target() {
2104 let awaited = target("200cc", 1, &[], false);
2105 let record = ReconfigurationState {
2106 target: target("300cc", 1, &[], false),
2107 deadline: Timestamp::from(0),
2108 on_timeout: OnTimeoutAction::Rollback,
2109 status: ReconfigurationStatus::InProgress,
2110 };
2111
2112 assert!(matches!(
2113 reconfiguration_wait_result(Some(&record), &awaited, false),
2114 Some(Err(AdapterError::AlterClusterSuperseded))
2115 ));
2116 assert!(matches!(
2117 reconfiguration_wait_result(None, &awaited, false),
2118 Some(Err(AdapterError::AlterClusterSuperseded))
2119 ));
2120 }
2121
2122 #[mz_ore::test]
2123 fn fold_with_no_record_takes_new_target() {
2124 let new = target("200cc", 3, &["az1"], true);
2126 let folded = fold_reconfiguration_target(None, new.clone(), all_changed());
2127 assert_eq!(folded, new);
2128 }
2129
2130 #[mz_ore::test]
2131 fn fold_rf_only_keeps_in_flight_shape() {
2132 let in_flight = target("200cc", 1, &["az2"], true);
2136 let new = target("100cc", 5, &["az1"], false);
2139 let unchanged = ReconfigurationDimensionsUnchanged {
2140 size: true,
2141 replication_factor: false,
2142 availability_zones: true,
2143 log_logging: true,
2144 interval: true,
2145 arrangement_compression: true,
2146 };
2147 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2148 assert_eq!(folded, target("200cc", 5, &["az2"], true));
2150 }
2151
2152 #[mz_ore::test]
2153 fn fold_with_all_set_overwrites_every_dimension() {
2154 let in_flight = target("200cc", 1, &["az2"], true);
2156 let new = target("400cc", 9, &["az9"], false);
2157 let folded = fold_reconfiguration_target(Some(&in_flight), new.clone(), all_changed());
2158 assert_eq!(folded, new);
2159 }
2160
2161 #[mz_ore::test]
2162 fn fold_all_unchanged_is_alter_back_to_in_flight() {
2163 let in_flight = target("200cc", 2, &["az2"], true);
2168 let realized_shaped = target("100cc", 1, &["az1"], false);
2169 let folded =
2170 fold_reconfiguration_target(Some(&in_flight), realized_shaped, all_unchanged());
2171 assert_eq!(folded, in_flight);
2172 }
2173
2174 #[mz_ore::test]
2175 fn fold_logging_subdimensions_fold_independently() {
2176 let mut in_flight = target("100cc", 1, &["az1"], false);
2181 in_flight.logging.interval = Some(Duration::from_secs(5));
2182 let new = target("100cc", 1, &["az1"], true);
2183 let unchanged = ReconfigurationDimensionsUnchanged {
2184 size: true,
2185 replication_factor: true,
2186 availability_zones: true,
2187 log_logging: false,
2188 interval: true,
2189 arrangement_compression: true,
2190 };
2191 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2192 assert_eq!(
2193 folded.logging,
2194 ReplicaLogging {
2195 log_logging: true,
2196 interval: Some(Duration::from_secs(5)),
2197 }
2198 );
2199 }
2200}