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 let cancels_or_retargets =
295 reconfiguration_in_flight && alter_changes_reconfiguration_target(options);
296 if new_config == config && !cancels_or_retargets {
297 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
298 ObjectType::Cluster,
299 )));
300 }
301
302 let single_replica_sources_notice = match (&config.variant, &new_config.variant) {
309 (Managed(old_managed), Managed(new_managed))
310 if new_managed.replication_factor > old_managed.replication_factor
311 && new_managed.replication_factor > 1 =>
312 {
313 let sources = self.single_replica_source_names(cluster);
314 (!sources.is_empty()).then(|| {
315 AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster {
316 cluster: cluster.name.clone(),
317 sources,
318 }
319 })
320 }
321 _ => None,
322 };
323
324 if let (Managed(old_managed), Managed(new_managed)) = (&config.variant, &new_config.variant)
337 {
338 let needs_record = if reconfiguration_in_flight {
339 alter_changes_reconfiguration_target(options)
340 } else {
341 new_managed.replica_config_shape() != old_managed.replica_config_shape()
342 };
343 let scheduled_direct =
356 !matches!(new_managed.schedule, mz_sql::plan::ClusterSchedule::Manual)
357 && !reconfiguration_in_flight;
358 if scheduled_direct && !matches!(strategy, AlterClusterPlanStrategy::None) {
364 return Err(AdapterError::AlterClusterWaitOnScheduledCluster);
365 }
366 if needs_record && !scheduled_direct {
367 let result = self
368 .reshape_alter_cluster_managed(
369 session,
370 cluster_id,
371 new_config.clone(),
372 options,
373 strategy,
374 validity,
375 )
376 .await;
377 if result.is_ok() {
378 if let Some(notice) = single_replica_sources_notice {
379 session.add_notice(notice);
380 }
381 }
382 return result;
383 }
384 }
385
386 match (&config.variant, &new_config.variant) {
387 (Managed(_), Managed(_)) => {
388 self.sequence_alter_cluster_managed_to_managed(
391 session,
392 cluster_id,
393 new_config.clone(),
394 )
395 .await?;
396 if let Some(notice) = single_replica_sources_notice {
397 session.add_notice(notice);
398 }
399 }
400 (Unmanaged, Managed(new_managed)) => {
401 if !matches!(new_managed.schedule, mz_sql::plan::ClusterSchedule::Manual)
406 && !matches!(strategy, AlterClusterPlanStrategy::None)
407 {
408 return Err(AdapterError::AlterClusterWaitOnScheduledCluster);
409 }
410 self.sequence_alter_cluster_unmanaged_to_managed(
411 session,
412 cluster_id,
413 new_config,
414 options.to_owned(),
415 )
416 .await?;
417 }
418 (Managed(_), Unmanaged) => {
419 self.sequence_alter_cluster_managed_to_unmanaged(session, cluster_id, new_config)
420 .await?;
421 }
422 (Unmanaged, Unmanaged) => {
423 self.sequence_alter_cluster_unmanaged_to_unmanaged(
424 session,
425 cluster_id,
426 new_config,
427 options.replicas.clone(),
428 )
429 .await?;
430 }
431 }
432
433 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
434 ObjectType::Cluster,
435 )))
436 }
437
438 async fn reshape_alter_cluster_managed(
474 &mut self,
475 session: &Session,
476 cluster_id: ClusterId,
477 new_config: ClusterConfig,
478 options: &PlanClusterOption,
479 strategy: &AlterClusterPlanStrategy,
480 validity: PlanValidity,
481 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
482 use mz_sql::plan::AlterOptionParameter::Unchanged;
483
484 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
485 return Err(AdapterError::Internal(
486 "reshape_alter_cluster_managed requires a managed target config".into(),
487 ));
488 };
489
490 let cluster = self.catalog.get_cluster(cluster_id);
496 let in_flight = match &cluster.config.variant {
497 ClusterVariant::Managed(managed) => managed
498 .reconfiguration
499 .as_ref()
500 .filter(|record| record.is_in_progress())
501 .cloned(),
502 ClusterVariant::Unmanaged => None,
503 };
504 let new_target = ReconfigurationTarget {
505 size: new_managed.size.clone(),
506 replication_factor: new_managed.replication_factor,
507 availability_zones: new_managed.availability_zones.clone(),
508 logging: new_managed.logging.clone(),
509 arrangement_compression: new_managed.arrangement_compression,
510 };
511 let unchanged = ReconfigurationDimensionsUnchanged {
512 size: matches!(options.size, Unchanged),
513 replication_factor: matches!(options.replication_factor, Unchanged),
514 availability_zones: matches!(options.availability_zones, Unchanged),
515 log_logging: matches!(options.introspection_debugging, Unchanged),
518 interval: matches!(options.introspection_interval, Unchanged),
519 arrangement_compression: matches!(options.arrangement_compression, Unchanged),
520 };
521 let target = fold_reconfiguration_target(
522 in_flight.as_ref().map(|r| &r.target),
523 new_target,
524 unchanged,
525 );
526
527 let role_id = session.role_metadata().current_role;
530 self.catalog.ensure_valid_replica_size(
531 &self
532 .catalog()
533 .get_role_allowed_cluster_sizes(&Some(role_id)),
534 &target.size,
535 false,
536 )?;
537 self.ensure_valid_azs(target.availability_zones.iter())?;
538
539 let cancels = match &cluster.config.variant {
540 ClusterVariant::Managed(managed) => target.matches_realized_config(managed),
541 ClusterVariant::Unmanaged => false,
542 };
543 if cluster_id.is_user() && !cancels {
549 self.validate_resource_limit(
550 0,
551 i64::from(target.replication_factor),
552 SystemVars::max_replicas_per_cluster,
553 "cluster replica",
554 MAX_REPLICAS_PER_CLUSTER.name(),
555 )?;
556
557 let credits_per_replica = self
558 .catalog()
559 .cluster_replica_sizes()
560 .0
561 .get(&target.size)
562 .expect("target replica size was validated")
563 .credits_per_hour;
564 let baseline_credits = credits_per_replica * Numeric::from(target.replication_factor);
565 self.validate_resource_limit_numeric(
566 self.current_credit_consumption_rate(Some(cluster_id)),
567 baseline_credits,
568 |system_vars| {
569 self.license_key
570 .max_credit_consumption_rate()
571 .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
572 },
573 "cluster replica",
574 MAX_CREDIT_CONSUMPTION_RATE.name(),
575 )?;
576 }
577
578 let now = self.now();
604 let deadline_from = |timeout: Duration| -> Timestamp {
605 now.saturating_add(u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX))
606 .into()
607 };
608 let (deadline, on_timeout) = match strategy {
609 AlterClusterPlanStrategy::None => match &in_flight {
610 Some(record) => (record.deadline, record.on_timeout),
611 None => (
612 deadline_from(
613 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT
614 .get(self.catalog().system_config().dyncfgs()),
615 ),
616 OnTimeoutAction::Rollback,
617 ),
618 },
619 AlterClusterPlanStrategy::For(timeout) => {
620 (deadline_from(*timeout), OnTimeoutAction::Rollback)
621 }
622 AlterClusterPlanStrategy::UntilReady {
623 timeout,
624 on_timeout,
625 } => (
626 deadline_from(*timeout),
627 on_timeout.unwrap_or(OnTimeoutAction::Rollback),
628 ),
629 };
630
631 let cluster = self.catalog.get_cluster(cluster_id);
640 let cluster_name = cluster.name().to_string();
641 let ClusterVariant::Managed(realized_now) = &cluster.config.variant else {
642 return Err(AdapterError::Internal(
643 "reshape_alter_cluster_managed requires a managed realized config".into(),
644 ));
645 };
646 let realized_target = realized_now.realized_reconfiguration_target();
647 let (status, audit) = if cancels {
651 (
652 ReconfigurationStatus::Cancelled,
653 ReconfigurationAudit::Cancelled,
654 )
655 } else {
656 (
657 ReconfigurationStatus::InProgress,
658 ReconfigurationAudit::Started,
659 )
660 };
661 let record = ReconfigurationState {
662 target: target.clone(),
663 deadline,
664 on_timeout,
665 status,
666 };
667
668 let mut realized = new_config.clone();
669 let ClusterVariant::Managed(realized_managed) = &mut realized.variant else {
670 return Err(AdapterError::Internal(
671 "reshape_alter_cluster_managed requires a managed target config".into(),
672 ));
673 };
674 realized_managed.apply_reconfiguration_target(realized_target);
675 realized_managed.reconfiguration = Some(record);
676
677 self.catalog_transact(
678 Some(session),
679 vec![Op::UpdateClusterConfig {
680 id: cluster_id,
681 name: cluster_name,
682 config: realized,
683 reconfiguration_audit: Some(audit),
684 burst_audit: None,
685 }],
686 )
687 .await?;
688
689 let background =
690 ENABLE_BACKGROUND_ALTER_CLUSTER.get(self.catalog().system_config().dyncfgs());
691 if background {
692 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
693 ObjectType::Cluster,
694 )));
695 }
696
697 Ok(StageResult::Immediate(Box::new(
701 ClusterStage::AwaitReconfiguration(AlterClusterAwaitReconfiguration {
702 validity,
703 cluster_id,
704 target,
705 }),
706 )))
707 }
708
709 fn await_reconfiguration_stage(
715 &self,
716 validity: PlanValidity,
717 cluster_id: ClusterId,
718 target: ReconfigurationTarget,
719 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
720 let Some(cluster) = self.catalog().try_get_cluster(cluster_id) else {
721 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
724 ObjectType::Cluster,
725 )));
726 };
727 let record = match &cluster.config.variant {
728 ClusterVariant::Managed(managed) => managed.reconfiguration.clone(),
729 ClusterVariant::Unmanaged => None,
730 };
731
732 let realized_matches_target = match &cluster.config.variant {
733 ClusterVariant::Managed(managed) => target.matches_realized_config(managed),
734 ClusterVariant::Unmanaged => false,
735 };
736
737 match reconfiguration_wait_result(record.as_ref(), &target, realized_matches_target) {
738 Some(result) => {
739 result?;
740 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
741 ObjectType::Cluster,
742 )))
743 }
744 None => {
745 let poll_duration = self
755 .catalog
756 .system_config()
757 .cluster_alter_check_ready_interval();
758 let span = Span::current();
759 Ok(StageResult::Handle(mz_ore::task::spawn(
760 || "Await Cluster Reconfiguration",
761 async move {
762 tokio::time::sleep(poll_duration).await;
763 Ok(Box::new(ClusterStage::AwaitReconfiguration(
764 AlterClusterAwaitReconfiguration {
765 validity,
766 cluster_id,
767 target,
768 },
769 )))
770 }
771 .instrument(span),
772 )))
773 }
774 }
775 }
776
777 pub(crate) async fn sequence_create_cluster(
778 &mut self,
779 session: &Session,
780 CreateClusterPlan {
781 name,
782 variant,
783 workload_class,
784 if_not_exists,
785 }: CreateClusterPlan,
786 ) -> Result<ExecuteResponse, AdapterError> {
787 tracing::debug!("sequence_create_cluster");
788
789 let id_ts = self.get_catalog_write_ts().await;
790 let id = self.catalog().allocate_user_cluster_id(id_ts).await?;
791 let introspection_sources = BUILTINS::logs().collect();
796 let cluster_variant = match &variant {
797 CreateClusterVariant::Managed(plan) => {
798 let logging = if let Some(config) = plan.compute.introspection {
799 ReplicaLogging {
800 log_logging: config.debugging,
801 interval: Some(config.interval),
802 }
803 } else {
804 ReplicaLogging::default()
805 };
806 ClusterVariant::Managed(ClusterVariantManaged {
807 size: plan.size.clone(),
808 availability_zones: plan.availability_zones.clone(),
809 logging,
810 arrangement_compression: plan.compute.arrangement_compression,
811 replication_factor: plan.replication_factor,
812 optimizer_feature_overrides: plan.optimizer_feature_overrides.clone(),
813 schedule: plan.schedule.clone(),
814 auto_scaling_strategy: plan.auto_scaling_strategy.clone(),
815 reconfiguration: None,
816 burst: None,
817 })
818 }
819 CreateClusterVariant::Unmanaged(_) => ClusterVariant::Unmanaged,
820 };
821 let config = ClusterConfig {
822 variant: cluster_variant,
823 workload_class,
824 };
825 let ops = vec![catalog::Op::CreateCluster {
826 id,
827 name: name.clone(),
828 introspection_sources,
829 owner_id: *session.current_role_id(),
830 config,
831 }];
832
833 match variant {
834 CreateClusterVariant::Managed(plan) => {
835 self.sequence_create_managed_cluster(session, plan, id, ops)
836 .await
837 }
838 CreateClusterVariant::Unmanaged(plan) => {
839 self.sequence_create_unmanaged_cluster(session, plan, id, ops)
840 .await
841 }
842 }
843 .or_else(|err| match err {
844 AdapterError::Catalog(mz_catalog::memory::error::Error {
845 kind: ErrorKind::Sql(CatalogError::ClusterAlreadyExists(_)),
846 }) if if_not_exists => {
847 session.add_notice(AdapterNotice::ObjectAlreadyExists {
848 name,
849 ty: "cluster",
850 });
851 Ok(ExecuteResponse::CreatedCluster)
852 }
853 err => Err(err),
854 })
855 }
856
857 #[mz_ore::instrument(level = "debug")]
858 async fn sequence_create_managed_cluster(
859 &mut self,
860 session: &Session,
861 CreateClusterManagedPlan {
862 availability_zones,
863 compute,
864 replication_factor,
865 size,
866 optimizer_feature_overrides: _,
867 schedule: _,
868 auto_scaling_strategy,
869 }: CreateClusterManagedPlan,
870 cluster_id: ClusterId,
871 mut ops: Vec<catalog::Op>,
872 ) -> Result<ExecuteResponse, AdapterError> {
873 tracing::debug!("sequence_create_managed_cluster");
874
875 self.ensure_valid_azs(availability_zones.iter())?;
876
877 let role_id = session.role_metadata().current_role;
878 self.catalog.ensure_valid_replica_size(
879 &self
880 .catalog()
881 .get_role_allowed_cluster_sizes(&Some(role_id)),
882 &size,
883 false,
884 )?;
885 if let Some(on_hydration) = auto_scaling_strategy
891 .as_ref()
892 .and_then(|strategy| strategy.on_hydration.as_ref())
893 {
894 self.catalog.ensure_valid_replica_size(
895 &self
896 .catalog()
897 .get_role_allowed_cluster_sizes(&Some(role_id)),
898 &on_hydration.hydration_size,
899 false,
900 )?;
901 }
902
903 if cluster_id.is_user() {
908 self.validate_resource_limit(
909 0,
910 i64::from(replication_factor),
911 SystemVars::max_replicas_per_cluster,
912 "cluster replica",
913 MAX_REPLICAS_PER_CLUSTER.name(),
914 )?;
915 }
916
917 let id_ts = self.get_catalog_write_ts().await;
921 let replica_ids = self
922 .catalog()
923 .allocate_replica_ids(cluster_id, u64::from(replication_factor), id_ts)
924 .await?;
925
926 for (replica_id, replica_name) in replica_ids
927 .into_iter()
928 .zip_eq((0..replication_factor).map(managed_cluster_replica_name))
929 {
930 self.create_managed_cluster_replica_op(
931 cluster_id,
932 replica_id,
933 replica_name.clone(),
934 &compute,
935 &size,
936 &mut ops,
937 if availability_zones.is_empty() {
938 None
939 } else {
940 Some(availability_zones.as_ref())
941 },
942 false,
943 *session.current_role_id(),
944 ReplicaCreateDropReason::Manual,
945 )?;
946 }
947
948 self.catalog_transact(Some(session), ops).await?;
949
950 Ok(ExecuteResponse::CreatedCluster)
951 }
952
953 fn create_managed_cluster_replica_op(
954 &self,
955 cluster_id: ClusterId,
956 replica_id: ReplicaId,
957 name: String,
958 compute: &mz_sql::plan::ComputeReplicaConfig,
959 size: &String,
960 ops: &mut Vec<Op>,
961 azs: Option<&[String]>,
962 pending: bool,
963 owner_id: RoleId,
964 reason: ReplicaCreateDropReason,
965 ) -> Result<(), AdapterError> {
966 let location = mz_catalog::durable::ReplicaLocation::Managed {
967 availability_zones: Vec::new(),
970 billed_as: None,
971 internal: false,
972 size: size.clone(),
973 pending,
974 };
975
976 let logging = if let Some(config) = compute.introspection {
977 ReplicaLogging {
978 log_logging: config.debugging,
979 interval: Some(config.interval),
980 }
981 } else {
982 ReplicaLogging::default()
983 };
984
985 let config = ReplicaConfig {
986 location: self.catalog().concretize_replica_location(
987 location,
988 &self
989 .catalog()
990 .get_role_allowed_cluster_sizes(&Some(owner_id)),
991 azs,
992 false,
993 )?,
994 compute: ComputeReplicaConfig {
995 logging,
996 arrangement_compression: compute.arrangement_compression,
997 },
998 };
999
1000 ops.push(catalog::Op::CreateClusterReplica {
1003 cluster_id,
1004 replica_id,
1005 name,
1006 config,
1007 owner_id,
1008 reason,
1009 });
1010 Ok(())
1011 }
1012
1013 fn ensure_valid_azs<'a, I: IntoIterator<Item = &'a String>>(
1014 &self,
1015 azs: I,
1016 ) -> Result<(), AdapterError> {
1017 let cat_azs = self.catalog().state().availability_zones();
1018 for az in azs.into_iter() {
1019 if !cat_azs.contains(az) {
1020 return Err(AdapterError::InvalidClusterReplicaAz {
1021 az: az.to_string(),
1022 expected: cat_azs.to_vec(),
1023 });
1024 }
1025 }
1026 Ok(())
1027 }
1028
1029 #[mz_ore::instrument(level = "debug")]
1030 async fn sequence_create_unmanaged_cluster(
1031 &mut self,
1032 session: &Session,
1033 CreateClusterUnmanagedPlan { replicas }: CreateClusterUnmanagedPlan,
1034 id: ClusterId,
1035 mut ops: Vec<catalog::Op>,
1036 ) -> Result<ExecuteResponse, AdapterError> {
1037 tracing::debug!("sequence_create_unmanaged_cluster");
1038
1039 self.ensure_valid_azs(replicas.iter().filter_map(|(_, r)| {
1040 if let mz_sql::plan::ReplicaConfig::Orchestrated {
1041 availability_zone: Some(az),
1042 ..
1043 } = &r
1044 {
1045 Some(az)
1046 } else {
1047 None
1048 }
1049 }))?;
1050
1051 if id.is_user() {
1056 self.validate_resource_limit(
1057 0,
1058 i64::try_from(replicas.len()).unwrap_or(i64::MAX),
1059 SystemVars::max_replicas_per_cluster,
1060 "cluster replica",
1061 MAX_REPLICAS_PER_CLUSTER.name(),
1062 )?;
1063 }
1064
1065 let id_ts = self.get_catalog_write_ts().await;
1069 let replica_ids = self
1070 .catalog()
1071 .allocate_replica_ids(id, u64::cast_from(replicas.len()), id_ts)
1072 .await?;
1073
1074 for (replica_id, (replica_name, replica_config)) in replica_ids.into_iter().zip_eq(replicas)
1075 {
1076 let (compute, location) = match replica_config {
1079 mz_sql::plan::ReplicaConfig::Unorchestrated {
1080 storagectl_addrs,
1081 computectl_addrs,
1082 compute,
1083 } => {
1084 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1085 storagectl_addrs,
1086 computectl_addrs,
1087 };
1088 (compute, location)
1089 }
1090 mz_sql::plan::ReplicaConfig::Orchestrated {
1091 availability_zone,
1092 billed_as,
1093 compute,
1094 internal,
1095 size,
1096 } => {
1097 if !session.user().is_internal() && (internal || billed_as.is_some()) {
1099 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1100 }
1101 if billed_as.is_some() && !internal {
1103 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1104 }
1105 if let Some(billed_as) = &billed_as {
1108 self.ensure_valid_billed_as_size(billed_as)?;
1109 }
1110
1111 let location = mz_catalog::durable::ReplicaLocation::Managed {
1112 availability_zones: availability_zone.into_iter().collect(),
1115 billed_as,
1116 internal,
1117 size: size.clone(),
1118 pending: false,
1119 };
1120 (compute, location)
1121 }
1122 };
1123
1124 let logging = if let Some(config) = compute.introspection {
1125 ReplicaLogging {
1126 log_logging: config.debugging,
1127 interval: Some(config.interval),
1128 }
1129 } else {
1130 ReplicaLogging::default()
1131 };
1132
1133 let role_id = session.role_metadata().current_role;
1134 let config = ReplicaConfig {
1135 location: self.catalog().concretize_replica_location(
1136 location,
1137 &self
1138 .catalog()
1139 .get_role_allowed_cluster_sizes(&Some(role_id)),
1140 None,
1141 false,
1142 )?,
1143 compute: ComputeReplicaConfig {
1144 logging,
1145 arrangement_compression: compute.arrangement_compression,
1146 },
1147 };
1148
1149 ops.push(catalog::Op::CreateClusterReplica {
1150 cluster_id: id,
1151 replica_id,
1152 name: replica_name.clone(),
1153 config,
1154 owner_id: *session.current_role_id(),
1155 reason: ReplicaCreateDropReason::Manual,
1156 });
1157 }
1158
1159 self.catalog_transact(Some(session), ops).await?;
1160
1161 Ok(ExecuteResponse::CreatedCluster)
1162 }
1163
1164 fn single_replica_source_names(&self, cluster: &Cluster) -> Vec<String> {
1168 cluster
1169 .bound_objects
1170 .iter()
1171 .filter_map(|id| {
1172 let entry = self.catalog().get_entry(id);
1173 let single_replica =
1174 entry
1175 .source()
1176 .is_some_and(|source| match &source.data_source {
1177 DataSourceDesc::Ingestion { desc, .. }
1178 | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
1179 desc.connection.prefers_single_replica()
1180 }
1181 _ => false,
1182 });
1183 single_replica.then(|| {
1184 self.catalog()
1185 .resolve_full_name(entry.name(), None)
1186 .to_string()
1187 })
1188 })
1189 .collect()
1190 }
1191
1192 fn notice_relevant_replica_count(&self, cluster: &Cluster) -> usize {
1204 match &cluster.config.variant {
1205 ClusterVariant::Managed(managed) => {
1206 let replication_factor = managed
1207 .reconfiguration
1208 .as_ref()
1209 .filter(|record| record.is_in_progress())
1210 .map_or(managed.replication_factor, |record| {
1211 record.target.replication_factor
1212 });
1213 let manual_replicas = cluster
1214 .replicas()
1215 .filter(|r| {
1216 r.config.location.internal() || r.config.location.billed_as().is_some()
1217 })
1218 .count();
1219 usize::cast_from(replication_factor) + manual_replicas
1220 }
1221 ClusterVariant::Unmanaged => cluster.replicas().count(),
1222 }
1223 }
1224
1225 pub(crate) fn notify_single_replica_sources(
1234 &self,
1235 session: &Session,
1236 cluster: &Cluster,
1237 creating_source: Option<&QualifiedItemName>,
1238 ) {
1239 if self.notice_relevant_replica_count(cluster) <= 1 {
1240 return;
1241 }
1242 let mut sources = self.single_replica_source_names(cluster);
1243 if let Some(name) = creating_source {
1244 let full_name = self.catalog().resolve_full_name(name, None).to_string();
1245 if !sources.contains(&full_name) {
1246 sources.push(full_name);
1247 }
1248 }
1249 if !sources.is_empty() {
1250 session.add_notice(AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster {
1251 cluster: cluster.name.clone(),
1252 sources,
1253 });
1254 }
1255 }
1256
1257 fn ensure_valid_billed_as_size(&self, size: &str) -> Result<(), AdapterError> {
1264 if self.catalog().cluster_replica_sizes().0.contains_key(size) {
1265 Ok(())
1266 } else {
1267 coord_bail!("unknown cluster replica size {size} in BILLED AS")
1268 }
1269 }
1270
1271 #[mz_ore::instrument(level = "debug")]
1272 pub(crate) async fn sequence_create_cluster_replica(
1273 &mut self,
1274 session: &Session,
1275 CreateClusterReplicaPlan {
1276 name,
1277 cluster_id,
1278 config,
1279 if_not_exists,
1280 }: CreateClusterReplicaPlan,
1281 ) -> Result<ExecuteResponse, AdapterError> {
1282 let (compute, location) = match config {
1284 mz_sql::plan::ReplicaConfig::Unorchestrated {
1285 storagectl_addrs,
1286 computectl_addrs,
1287 compute,
1288 } => {
1289 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1290 storagectl_addrs,
1291 computectl_addrs,
1292 };
1293 (compute, location)
1294 }
1295 mz_sql::plan::ReplicaConfig::Orchestrated {
1296 availability_zone,
1297 billed_as,
1298 compute,
1299 internal,
1300 size,
1301 } => {
1302 let availability_zone = match availability_zone {
1303 Some(az) => {
1304 self.ensure_valid_azs([&az])?;
1305 Some(az)
1306 }
1307 None => None,
1308 };
1309 let location = mz_catalog::durable::ReplicaLocation::Managed {
1310 availability_zones: availability_zone.into_iter().collect(),
1313 billed_as,
1314 internal,
1315 size,
1316 pending: false,
1317 };
1318 (compute, location)
1319 }
1320 };
1321
1322 let logging = if let Some(config) = compute.introspection {
1323 ReplicaLogging {
1324 log_logging: config.debugging,
1325 interval: Some(config.interval),
1326 }
1327 } else {
1328 ReplicaLogging::default()
1329 };
1330
1331 let role_id = session.role_metadata().current_role;
1332 let config = ReplicaConfig {
1333 location: self.catalog().concretize_replica_location(
1334 location,
1335 &self
1336 .catalog()
1337 .get_role_allowed_cluster_sizes(&Some(role_id)),
1338 None,
1341 false,
1342 )?,
1343 compute: ComputeReplicaConfig {
1344 logging,
1345 arrangement_compression: compute.arrangement_compression,
1346 },
1347 };
1348
1349 let cluster = self.catalog().get_cluster(cluster_id);
1350
1351 if let ReplicaLocation::Managed(ManagedReplicaLocation {
1352 internal,
1353 billed_as,
1354 ..
1355 }) = &config.location
1356 {
1357 if !session.user().is_internal() && (*internal || billed_as.is_some()) {
1359 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1360 }
1361 if cluster.is_managed() && !*internal {
1363 coord_bail!("must specify INTERNAL when creating a replica in a managed cluster");
1364 }
1365 if billed_as.is_some() && !*internal {
1367 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1368 }
1369 if let Some(billed_as) = billed_as {
1372 self.ensure_valid_billed_as_size(billed_as)?;
1373 }
1374 }
1375
1376 let owner_id = cluster.owner_id();
1379
1380 let cluster_name = cluster.name.clone();
1381 let qualified_name = format!("{cluster_name}.{name}");
1384
1385 let id_ts = self.get_catalog_write_ts().await;
1390 let replica_id = self
1391 .catalog()
1392 .allocate_replica_ids(cluster_id, 1, id_ts)
1393 .await?
1394 .into_element();
1395
1396 let ops = vec![catalog::Op::CreateClusterReplica {
1397 cluster_id,
1398 replica_id,
1399 name: name.clone(),
1400 config,
1401 owner_id,
1402 reason: ReplicaCreateDropReason::Manual,
1403 }];
1404
1405 match self.catalog_transact(Some(session), ops).await {
1406 Ok(()) => {
1407 self.notify_single_replica_sources(
1410 session,
1411 self.catalog().get_cluster(cluster_id),
1412 None,
1413 );
1414 Ok(ExecuteResponse::CreatedClusterReplica)
1415 }
1416 Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
1417 kind: ErrorKind::Sql(CatalogError::DuplicateReplica(_, _)),
1418 })) if if_not_exists => {
1419 session.add_notice(AdapterNotice::ObjectAlreadyExists {
1420 name: qualified_name,
1421 ty: "cluster replica",
1422 });
1423 Ok(ExecuteResponse::CreatedClusterReplica)
1424 }
1425 Err(err) => Err(err),
1426 }
1427 }
1428
1429 pub(crate) async fn sequence_alter_cluster_managed_to_managed(
1443 &mut self,
1444 session: &Session,
1445 cluster_id: ClusterId,
1446 new_config: ClusterConfig,
1447 ) -> Result<(), AdapterError> {
1448 let cluster = self.catalog.get_cluster(cluster_id);
1449 let name = cluster.name().to_string();
1450
1451 let ClusterVariant::Managed(ClusterVariantManaged {
1452 size,
1453 availability_zones,
1454 logging,
1455 arrangement_compression,
1456 replication_factor,
1457 optimizer_feature_overrides: _,
1458 schedule: _,
1459 auto_scaling_strategy,
1460 reconfiguration,
1461 burst: _,
1462 }) = &cluster.config.variant
1463 else {
1464 panic!("expected existing managed cluster config");
1465 };
1466 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
1467 panic!("expected new managed cluster config");
1468 };
1469 let ClusterVariantManaged {
1470 size: new_size,
1471 replication_factor: new_replication_factor,
1472 availability_zones: new_availability_zones,
1473 logging: _,
1474 arrangement_compression: _,
1475 optimizer_feature_overrides: _,
1476 schedule: _,
1477 auto_scaling_strategy: new_auto_scaling_strategy,
1478 reconfiguration: _,
1479 burst: _,
1480 } = new_managed;
1481
1482 let role_id = Some(session.role_metadata().current_role);
1483 self.catalog.ensure_valid_replica_size(
1484 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
1485 new_size,
1486 false,
1487 )?;
1488 if new_auto_scaling_strategy != auto_scaling_strategy {
1494 if let Some(on_hydration) = new_auto_scaling_strategy
1495 .as_ref()
1496 .and_then(|strategy| strategy.on_hydration.as_ref())
1497 {
1498 self.catalog.ensure_valid_replica_size(
1499 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
1500 &on_hydration.hydration_size,
1501 false,
1502 )?;
1503 if reconfiguration.as_ref().is_some_and(|record| {
1510 record.is_in_progress() && record.target.size == on_hydration.hydration_size
1511 }) {
1512 coord_bail!(
1513 "HYDRATION SIZE must differ from the target SIZE \
1514 ('{}') of the in-progress cluster resize",
1515 on_hydration.hydration_size
1516 );
1517 }
1518 }
1519 }
1520
1521 if new_replication_factor > replication_factor && cluster_id.is_user() {
1530 self.validate_resource_limit(
1531 usize::cast_from(*replication_factor),
1532 i64::from(*new_replication_factor) - i64::from(*replication_factor),
1533 SystemVars::max_replicas_per_cluster,
1534 "cluster replica",
1535 MAX_REPLICAS_PER_CLUSTER.name(),
1536 )?;
1537
1538 let credits_per_replica = self
1539 .catalog()
1540 .cluster_replica_sizes()
1541 .0
1542 .get(new_size)
1543 .expect("new replica size was validated")
1544 .credits_per_hour;
1545 let baseline_credits = credits_per_replica * Numeric::from(*new_replication_factor);
1546 self.validate_resource_limit_numeric(
1547 self.current_credit_consumption_rate(Some(cluster_id)),
1548 baseline_credits,
1549 |system_vars| {
1550 self.license_key
1551 .max_credit_consumption_rate()
1552 .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
1553 },
1554 "cluster replica",
1555 MAX_CREDIT_CONSUMPTION_RATE.name(),
1556 )?;
1557 }
1558
1559 let config_changed = new_managed.replica_config_shape()
1560 != ManagedReplicaConfigShape::new(
1561 size,
1562 availability_zones,
1563 logging,
1564 *arrangement_compression,
1565 );
1566 if config_changed {
1570 self.ensure_valid_azs(new_availability_zones.iter())?;
1571 }
1572
1573 let ops = vec![catalog::Op::UpdateClusterConfig {
1576 id: cluster_id,
1577 name,
1578 config: new_config,
1579 reconfiguration_audit: None,
1580 burst_audit: None,
1581 }];
1582
1583 self.catalog_transact(Some(session), ops).await?;
1584 Ok(())
1585 }
1586
1587 async fn sequence_alter_cluster_unmanaged_to_managed(
1591 &mut self,
1592 session: &Session,
1593 cluster_id: ClusterId,
1594 mut new_config: ClusterConfig,
1595 options: PlanClusterOption,
1596 ) -> Result<(), AdapterError> {
1597 let cluster = self.catalog.get_cluster(cluster_id);
1598 let cluster_name = cluster.name().to_string();
1599
1600 let ClusterVariant::Managed(ClusterVariantManaged {
1601 size: new_size,
1602 replication_factor: new_replication_factor,
1603 availability_zones: new_availability_zones,
1604 logging: _,
1605 arrangement_compression: _,
1606 optimizer_feature_overrides: _,
1607 schedule: _,
1608 auto_scaling_strategy: _,
1609 reconfiguration: _,
1610 burst: _,
1611 }) = &mut new_config.variant
1612 else {
1613 panic!("expected new managed cluster config");
1614 };
1615
1616 let user_replica_count = cluster
1618 .user_replicas()
1619 .count()
1620 .try_into()
1621 .expect("must_fit");
1622 match options.replication_factor {
1623 AlterOptionParameter::Set(_) => {
1624 if user_replica_count != *new_replication_factor {
1626 coord_bail!(
1627 "REPLICATION FACTOR {new_replication_factor} does not match number of replicas ({user_replica_count})"
1628 );
1629 }
1630 }
1631 _ => {
1632 *new_replication_factor = user_replica_count;
1633 }
1634 }
1635
1636 let mut names = BTreeSet::new();
1637 let mut sizes = BTreeSet::new();
1638
1639 self.ensure_valid_azs(new_availability_zones.iter())?;
1640
1641 for replica in cluster.user_replicas() {
1643 names.insert(replica.name.clone());
1644 match &replica.config.location {
1645 ReplicaLocation::Unmanaged(_) => coord_bail!(
1646 "Cannot convert unmanaged cluster with unmanaged replicas to managed cluster"
1647 ),
1648 ReplicaLocation::Managed(location) => {
1649 sizes.insert(location.size.clone());
1650
1651 for az in &location.availability_zones {
1655 if !new_availability_zones.contains(az) {
1656 coord_bail!(
1657 "unmanaged replica has availability zone {az} which is not \
1658 in managed {new_availability_zones:?}"
1659 )
1660 }
1661 }
1662 }
1663 }
1664 }
1665
1666 if sizes.is_empty() {
1667 assert!(
1668 cluster.user_replicas().next().is_none(),
1669 "Cluster should not have replicas"
1670 );
1671 match &options.size {
1673 AlterOptionParameter::Reset | AlterOptionParameter::Unchanged => {
1674 coord_bail!("Missing SIZE for empty cluster")
1675 }
1676 AlterOptionParameter::Set(_) => {} }
1678 } else if sizes.len() == 1 {
1679 let size = sizes.into_iter().next().expect("must exist");
1680 match &options.size {
1681 AlterOptionParameter::Set(sz) if *sz != size => {
1682 coord_bail!("Cluster replicas of size {size} do not match expected SIZE {sz}");
1683 }
1684 _ => *new_size = size,
1685 }
1686 } else {
1687 let formatted = sizes
1688 .iter()
1689 .map(String::as_str)
1690 .collect::<Vec<_>>()
1691 .join(", ");
1692 coord_bail!(
1693 "Cannot convert unmanaged cluster to managed, non-unique replica sizes: {formatted}"
1694 );
1695 }
1696
1697 for i in 0..*new_replication_factor {
1698 let name = managed_cluster_replica_name(i);
1699 names.remove(&name);
1700 }
1701 if !names.is_empty() {
1702 let formatted = names
1703 .iter()
1704 .map(String::as_str)
1705 .collect::<Vec<_>>()
1706 .join(", ");
1707 coord_bail!(
1708 "Cannot convert unmanaged cluster to managed, invalid replica names: {formatted}"
1709 );
1710 }
1711
1712 let ops = vec![catalog::Op::UpdateClusterConfig {
1713 id: cluster_id,
1714 name: cluster_name,
1715 config: new_config,
1716 reconfiguration_audit: None,
1717 burst_audit: None,
1718 }];
1719
1720 self.catalog_transact(Some(session), ops).await?;
1721 Ok(())
1722 }
1723
1724 async fn sequence_alter_cluster_managed_to_unmanaged(
1725 &mut self,
1726 session: &Session,
1727 cluster_id: ClusterId,
1728 new_config: ClusterConfig,
1729 ) -> Result<(), AdapterError> {
1730 let cluster = self.catalog().get_cluster(cluster_id);
1731
1732 if let ClusterVariant::Managed(managed) = &cluster.config.variant {
1738 if managed
1739 .reconfiguration
1740 .as_ref()
1741 .is_some_and(|record| record.is_in_progress())
1742 {
1743 return Err(AdapterError::AlterClusterUnmanagedWhileReconfiguring);
1744 }
1745 if managed.burst.is_some() {
1752 return Err(AdapterError::AlterClusterUnmanagedWhileBursting);
1753 }
1754 }
1755
1756 let ops = vec![catalog::Op::UpdateClusterConfig {
1757 id: cluster_id,
1758 name: cluster.name().to_string(),
1759 config: new_config,
1760 reconfiguration_audit: None,
1761 burst_audit: None,
1762 }];
1763
1764 self.catalog_transact(Some(session), ops).await?;
1765 Ok(())
1766 }
1767
1768 async fn sequence_alter_cluster_unmanaged_to_unmanaged(
1769 &mut self,
1770 session: &Session,
1771 cluster_id: ClusterId,
1772 new_config: ClusterConfig,
1773 replicas: AlterOptionParameter<Vec<(String, mz_sql::plan::ReplicaConfig)>>,
1774 ) -> Result<(), AdapterError> {
1775 if !matches!(replicas, AlterOptionParameter::Unchanged) {
1776 coord_bail!("Cannot alter replicas in unmanaged cluster");
1777 }
1778
1779 let cluster = self.catalog().get_cluster(cluster_id);
1780
1781 let ops = vec![catalog::Op::UpdateClusterConfig {
1782 id: cluster_id,
1783 name: cluster.name().to_string(),
1784 config: new_config,
1785 reconfiguration_audit: None,
1786 burst_audit: None,
1787 }];
1788
1789 self.catalog_transact(Some(session), ops).await?;
1790 Ok(())
1791 }
1792
1793 pub(crate) async fn sequence_alter_cluster_rename(
1794 &mut self,
1795 ctx: &mut ExecuteContext,
1796 AlterClusterRenamePlan { id, name, to_name }: AlterClusterRenamePlan,
1797 ) -> Result<ExecuteResponse, AdapterError> {
1798 let op = Op::RenameCluster {
1799 id,
1800 name,
1801 to_name,
1802 check_reserved_names: true,
1803 };
1804 match self
1805 .catalog_transact_with_ddl_transaction(ctx, vec![op], |_, _| Box::pin(async {}))
1806 .await
1807 {
1808 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
1809 Err(err) => Err(err),
1810 }
1811 }
1812
1813 pub(crate) async fn sequence_alter_cluster_swap(
1814 &mut self,
1815 ctx: &mut ExecuteContext,
1816 AlterClusterSwapPlan {
1817 id_a,
1818 id_b,
1819 name_a,
1820 name_b,
1821 name_temp,
1822 }: AlterClusterSwapPlan,
1823 ) -> Result<ExecuteResponse, AdapterError> {
1824 let op_a = Op::RenameCluster {
1825 id: id_a,
1826 name: name_a.clone(),
1827 to_name: name_temp.clone(),
1828 check_reserved_names: false,
1829 };
1830 let op_b = Op::RenameCluster {
1831 id: id_b,
1832 name: name_b.clone(),
1833 to_name: name_a,
1834 check_reserved_names: false,
1835 };
1836 let op_temp = Op::RenameCluster {
1837 id: id_a,
1838 name: name_temp,
1839 to_name: name_b,
1840 check_reserved_names: false,
1841 };
1842
1843 match self
1844 .catalog_transact_with_ddl_transaction(ctx, vec![op_a, op_b, op_temp], |_, _| {
1845 Box::pin(async {})
1846 })
1847 .await
1848 {
1849 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
1850 Err(err) => Err(err),
1851 }
1852 }
1853
1854 pub(crate) async fn sequence_alter_cluster_replica_rename(
1855 &mut self,
1856 session: &Session,
1857 AlterClusterReplicaRenamePlan {
1858 cluster_id,
1859 replica_id,
1860 name,
1861 to_name,
1862 }: AlterClusterReplicaRenamePlan,
1863 ) -> Result<ExecuteResponse, AdapterError> {
1864 let op = catalog::Op::RenameClusterReplica {
1865 cluster_id,
1866 replica_id,
1867 name,
1868 to_name,
1869 };
1870 match self.catalog_transact(Some(session), vec![op]).await {
1871 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::ClusterReplica)),
1872 Err(err) => Err(err),
1873 }
1874 }
1875
1876 pub(crate) async fn sequence_alter_set_cluster(
1878 &self,
1879 _session: &Session,
1880 AlterSetClusterPlan { id, set_cluster: _ }: AlterSetClusterPlan,
1881 ) -> Result<ExecuteResponse, AdapterError> {
1882 async {}.await;
1886 let entry = self.catalog().get_entry(&id);
1887 match entry.item().typ() {
1888 _ => {
1889 Err(AdapterError::Unsupported("ALTER SET CLUSTER"))
1891 }
1892 }
1893 }
1894}
1895
1896struct ReconfigurationDimensionsUnchanged {
1901 size: bool,
1902 replication_factor: bool,
1903 availability_zones: bool,
1904 log_logging: bool,
1905 interval: bool,
1906 arrangement_compression: bool,
1907}
1908
1909fn reconfiguration_wait_result(
1915 record: Option<&ReconfigurationState>,
1916 awaited_target: &ReconfigurationTarget,
1917 realized_matches_target: bool,
1918) -> Option<Result<(), AdapterError>> {
1919 if realized_matches_target {
1920 return Some(Ok(()));
1921 }
1922 let Some(record) = record.filter(|record| record.target == *awaited_target) else {
1923 return Some(Err(AdapterError::AlterClusterSuperseded));
1924 };
1925 match record.status {
1926 ReconfigurationStatus::InProgress => None,
1927 ReconfigurationStatus::ResourceExhausted => {
1928 Some(Err(AdapterError::AlterClusterResourceExhausted))
1929 }
1930 ReconfigurationStatus::TimedOut => Some(Err(AdapterError::AlterClusterTimeout)),
1931 ReconfigurationStatus::Finalized | ReconfigurationStatus::Cancelled => {
1932 Some(Err(AdapterError::AlterClusterSuperseded))
1933 }
1934 }
1935}
1936
1937fn alter_changes_reconfiguration_target(options: &PlanClusterOption) -> bool {
1947 use mz_sql::plan::AlterOptionParameter::Unchanged;
1948 let PlanClusterOption {
1949 availability_zones,
1950 introspection_debugging,
1951 introspection_interval,
1952 arrangement_compression,
1953 managed: _,
1954 replicas: _,
1955 replication_factor,
1956 size,
1957 schedule: _,
1958 workload_class: _,
1959 auto_scaling_strategy: _,
1960 } = options;
1961 !matches!(size, Unchanged)
1962 || !matches!(replication_factor, Unchanged)
1963 || !matches!(availability_zones, Unchanged)
1964 || !matches!(introspection_debugging, Unchanged)
1965 || !matches!(introspection_interval, Unchanged)
1966 || !matches!(arrangement_compression, Unchanged)
1967}
1968
1969fn fold_reconfiguration_target(
1981 in_flight: Option<&ReconfigurationTarget>,
1982 new_target: ReconfigurationTarget,
1983 unchanged: ReconfigurationDimensionsUnchanged,
1984) -> ReconfigurationTarget {
1985 let Some(prev) = in_flight else {
1986 return new_target;
1987 };
1988 ReconfigurationTarget {
1989 size: if unchanged.size {
1990 prev.size.clone()
1991 } else {
1992 new_target.size
1993 },
1994 replication_factor: if unchanged.replication_factor {
1995 prev.replication_factor
1996 } else {
1997 new_target.replication_factor
1998 },
1999 availability_zones: if unchanged.availability_zones {
2000 prev.availability_zones.clone()
2001 } else {
2002 new_target.availability_zones
2003 },
2004 logging: ReplicaLogging {
2005 log_logging: if unchanged.log_logging {
2006 prev.logging.log_logging
2007 } else {
2008 new_target.logging.log_logging
2009 },
2010 interval: if unchanged.interval {
2011 prev.logging.interval
2012 } else {
2013 new_target.logging.interval
2014 },
2015 },
2016 arrangement_compression: if unchanged.arrangement_compression {
2017 prev.arrangement_compression
2018 } else {
2019 new_target.arrangement_compression
2020 },
2021 }
2022}
2023
2024#[cfg(test)]
2025mod tests {
2026 use mz_controller::clusters::ReplicaLogging;
2027 use mz_controller_types::DEFAULT_REPLICA_LOGGING_INTERVAL;
2028
2029 use super::*;
2030
2031 fn target(size: &str, rf: u32, azs: &[&str], log_logging: bool) -> ReconfigurationTarget {
2032 ReconfigurationTarget {
2033 size: size.to_string(),
2034 replication_factor: rf,
2035 availability_zones: azs.iter().map(|s| s.to_string()).collect(),
2036 logging: ReplicaLogging {
2037 log_logging,
2038 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
2039 },
2040 arrangement_compression: false,
2041 }
2042 }
2043
2044 fn all_changed() -> ReconfigurationDimensionsUnchanged {
2045 ReconfigurationDimensionsUnchanged {
2046 size: false,
2047 replication_factor: false,
2048 availability_zones: false,
2049 log_logging: false,
2050 interval: false,
2051 arrangement_compression: false,
2052 }
2053 }
2054
2055 fn all_unchanged() -> ReconfigurationDimensionsUnchanged {
2056 ReconfigurationDimensionsUnchanged {
2057 size: true,
2058 replication_factor: true,
2059 availability_zones: true,
2060 log_logging: true,
2061 interval: true,
2062 arrangement_compression: true,
2063 }
2064 }
2065
2066 #[mz_ore::test]
2067 fn foreground_wait_succeeds_when_target_is_realized() {
2068 let awaited = target("200cc", 1, &[], false);
2069 let record = ReconfigurationState {
2070 target: target("300cc", 1, &[], false),
2071 deadline: Timestamp::from(0),
2072 on_timeout: OnTimeoutAction::Rollback,
2073 status: ReconfigurationStatus::InProgress,
2074 };
2075
2076 assert!(matches!(
2077 reconfiguration_wait_result(Some(&record), &awaited, true),
2078 Some(Ok(()))
2079 ));
2080 }
2081
2082 #[mz_ore::test]
2083 fn foreground_wait_follows_matching_target() {
2084 let awaited = target("200cc", 1, &[], false);
2085 let mut record = ReconfigurationState {
2086 target: awaited.clone(),
2087 deadline: Timestamp::from(0),
2088 on_timeout: OnTimeoutAction::Rollback,
2089 status: ReconfigurationStatus::InProgress,
2090 };
2091
2092 assert!(reconfiguration_wait_result(Some(&record), &awaited, false).is_none());
2093
2094 record.status = ReconfigurationStatus::ResourceExhausted;
2095 assert!(matches!(
2096 reconfiguration_wait_result(Some(&record), &awaited, false),
2097 Some(Err(AdapterError::AlterClusterResourceExhausted))
2098 ));
2099
2100 record.status = ReconfigurationStatus::TimedOut;
2101 assert!(matches!(
2102 reconfiguration_wait_result(Some(&record), &awaited, false),
2103 Some(Err(AdapterError::AlterClusterTimeout))
2104 ));
2105 }
2106
2107 #[mz_ore::test]
2108 fn foreground_wait_reports_superseded_target() {
2109 let awaited = target("200cc", 1, &[], false);
2110 let record = ReconfigurationState {
2111 target: target("300cc", 1, &[], false),
2112 deadline: Timestamp::from(0),
2113 on_timeout: OnTimeoutAction::Rollback,
2114 status: ReconfigurationStatus::InProgress,
2115 };
2116
2117 assert!(matches!(
2118 reconfiguration_wait_result(Some(&record), &awaited, false),
2119 Some(Err(AdapterError::AlterClusterSuperseded))
2120 ));
2121 assert!(matches!(
2122 reconfiguration_wait_result(None, &awaited, false),
2123 Some(Err(AdapterError::AlterClusterSuperseded))
2124 ));
2125 }
2126
2127 #[mz_ore::test]
2128 fn fold_with_no_record_takes_new_target() {
2129 let new = target("200cc", 3, &["az1"], true);
2131 let folded = fold_reconfiguration_target(None, new.clone(), all_changed());
2132 assert_eq!(folded, new);
2133 }
2134
2135 #[mz_ore::test]
2136 fn fold_rf_only_keeps_in_flight_shape() {
2137 let in_flight = target("200cc", 1, &["az2"], true);
2141 let new = target("100cc", 5, &["az1"], false);
2144 let unchanged = ReconfigurationDimensionsUnchanged {
2145 size: true,
2146 replication_factor: false,
2147 availability_zones: true,
2148 log_logging: true,
2149 interval: true,
2150 arrangement_compression: true,
2151 };
2152 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2153 assert_eq!(folded, target("200cc", 5, &["az2"], true));
2155 }
2156
2157 #[mz_ore::test]
2158 fn fold_with_all_set_overwrites_every_dimension() {
2159 let in_flight = target("200cc", 1, &["az2"], true);
2161 let new = target("400cc", 9, &["az9"], false);
2162 let folded = fold_reconfiguration_target(Some(&in_flight), new.clone(), all_changed());
2163 assert_eq!(folded, new);
2164 }
2165
2166 #[mz_ore::test]
2167 fn fold_all_unchanged_is_alter_back_to_in_flight() {
2168 let in_flight = target("200cc", 2, &["az2"], true);
2173 let realized_shaped = target("100cc", 1, &["az1"], false);
2174 let folded =
2175 fold_reconfiguration_target(Some(&in_flight), realized_shaped, all_unchanged());
2176 assert_eq!(folded, in_flight);
2177 }
2178
2179 #[mz_ore::test]
2180 fn fold_logging_subdimensions_fold_independently() {
2181 let mut in_flight = target("100cc", 1, &["az1"], false);
2186 in_flight.logging.interval = Some(Duration::from_secs(5));
2187 let new = target("100cc", 1, &["az1"], true);
2188 let unchanged = ReconfigurationDimensionsUnchanged {
2189 size: true,
2190 replication_factor: true,
2191 availability_zones: true,
2192 log_logging: false,
2193 interval: true,
2194 arrangement_compression: true,
2195 };
2196 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2197 assert_eq!(
2198 folded.logging,
2199 ReplicaLogging {
2200 log_logging: true,
2201 interval: Some(Duration::from_secs(5)),
2202 }
2203 );
2204 }
2205}