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::FullItemName;
35use mz_sql::plan::{
36 self, AlterClusterPlanStrategy, AlterOptionParameter, AlterSetClusterPlan,
37 CreateClusterManagedPlan, CreateClusterPlan, CreateClusterReplicaPlan,
38 CreateClusterUnmanagedPlan, CreateClusterVariant, PlanClusterOption,
39};
40use mz_sql::plan::{AlterClusterPlan, OnTimeoutAction};
41use mz_sql::session::metadata::SessionMetadata;
42use mz_sql::session::vars::{
43 MAX_CREDIT_CONSUMPTION_RATE, MAX_REPLICAS_PER_CLUSTER, SystemVars, Var,
44};
45use mz_storage_types::sources::SourceConnection;
46use tracing::{Instrument, Span};
47
48use mz_adapter_types::dyncfgs::{
49 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT, ENABLE_BACKGROUND_ALTER_CLUSTER,
50};
51
52use super::return_if_err;
53use crate::catalog::{self, Op, ReplicaCreateDropReason};
54use crate::coord::{
55 AlterCluster, AlterClusterAwaitReconfiguration, ClusterStage, Coordinator, Message,
56 PlanValidity, StageResult, Staged,
57};
58use crate::{AdapterError, AdapterNotice, ExecuteContext, ExecuteResponse, session::Session};
59
60impl Staged for ClusterStage {
61 type Ctx = ExecuteContext;
62
63 fn validity(&mut self) -> &mut PlanValidity {
64 match self {
65 Self::Alter(stage) => &mut stage.validity,
66 Self::AwaitReconfiguration(stage) => &mut stage.validity,
67 }
68 }
69
70 async fn stage(
71 self,
72 coord: &mut Coordinator,
73 ctx: &mut ExecuteContext,
74 ) -> Result<StageResult<Box<Self>>, crate::AdapterError> {
75 match self {
76 Self::Alter(stage) => {
77 coord
78 .sequence_alter_cluster_stage(ctx.session(), stage.plan.clone(), stage.validity)
79 .await
80 }
81 Self::AwaitReconfiguration(stage) => {
82 coord.await_reconfiguration_stage(stage.validity, stage.cluster_id, stage.target)
83 }
84 }
85 }
86
87 fn message(self, ctx: ExecuteContext, span: tracing::Span) -> Message {
88 Message::ClusterStageReady {
89 ctx,
90 span,
91 stage: self,
92 }
93 }
94
95 fn cancel_enabled(&self) -> bool {
96 true
97 }
98}
99
100impl Coordinator {
101 #[instrument]
102 pub(crate) async fn sequence_alter_cluster_staged(
103 &mut self,
104 ctx: ExecuteContext,
105 plan: plan::AlterClusterPlan,
106 ) {
107 let stage = return_if_err!(self.alter_cluster_validate(ctx.session(), plan).await, ctx);
108 self.sequence_staged(ctx, Span::current(), stage).await;
109 }
110
111 #[instrument]
112 async fn alter_cluster_validate(
113 &self,
114 session: &Session,
115 plan: plan::AlterClusterPlan,
116 ) -> Result<ClusterStage, AdapterError> {
117 let validity = PlanValidity::new(
118 self.catalog(),
119 BTreeSet::new(),
120 Some(plan.id.clone()),
121 None,
122 session.role_metadata().clone(),
123 );
124 Ok(ClusterStage::Alter(AlterCluster { validity, plan }))
125 }
126
127 async fn sequence_alter_cluster_stage(
128 &mut self,
129 session: &Session,
130 plan: plan::AlterClusterPlan,
131 validity: PlanValidity,
132 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
133 let AlterClusterPlan {
134 id: cluster_id,
135 name: _,
136 ref options,
137 ref strategy,
138 } = plan;
139
140 use mz_catalog::memory::objects::ClusterVariant::*;
141 use mz_sql::plan::AlterOptionParameter::*;
142 let cluster = self.catalog.get_cluster(cluster_id);
143 let config = cluster.config.clone();
144 let mut new_config = config.clone();
145
146 match (&new_config.variant, &options.managed) {
147 (Managed(_), Reset) | (Managed(_), Unchanged) | (Managed(_), Set(true)) => {}
148 (Managed(_), Set(false)) => new_config.variant = Unmanaged,
149 (Unmanaged, Unchanged) | (Unmanaged, Set(false)) => {}
150 (Unmanaged, Reset) | (Unmanaged, Set(true)) => {
151 let size = "".to_string();
155 let logging = ReplicaLogging {
156 log_logging: false,
157 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
158 };
159 new_config.variant = Managed(ClusterVariantManaged {
160 size,
161 availability_zones: Default::default(),
162 logging,
163 arrangement_compression: false,
164 replication_factor: 1,
165 optimizer_feature_overrides: Default::default(),
166 schedule: Default::default(),
167 auto_scaling_strategy: None,
168 reconfiguration: None,
169 burst: None,
170 });
171 }
172 }
173
174 match &mut new_config.variant {
175 Managed(ClusterVariantManaged {
176 size,
177 availability_zones,
178 logging,
179 arrangement_compression,
180 replication_factor,
181 optimizer_feature_overrides: _,
182 schedule,
183 auto_scaling_strategy,
184 reconfiguration: _,
185 burst: _,
186 }) => {
187 match &options.size {
188 Set(s) => size.clone_from(s),
189 Reset => coord_bail!("SIZE has no default value"),
190 Unchanged => {}
191 }
192 match &options.availability_zones {
193 Set(az) => availability_zones.clone_from(az),
194 Reset => *availability_zones = Default::default(),
195 Unchanged => {}
196 }
197 match &options.introspection_debugging {
198 Set(id) => logging.log_logging = *id,
199 Reset => logging.log_logging = false,
200 Unchanged => {}
201 }
202 match &options.introspection_interval {
203 Set(ii) => logging.interval = ii.0,
204 Reset => logging.interval = Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
205 Unchanged => {}
206 }
207 match &options.arrangement_compression {
208 Set(ac) => *arrangement_compression = *ac,
209 Reset => *arrangement_compression = false,
210 Unchanged => {}
211 }
212 match &options.replication_factor {
213 Set(rf) => *replication_factor = *rf,
214 Reset => {
215 *replication_factor = self
216 .catalog
217 .system_config()
218 .default_cluster_replication_factor()
219 }
220 Unchanged => {}
221 }
222 match &options.schedule {
223 Set(new_schedule) => {
224 *schedule = new_schedule.clone();
225 }
226 Reset => *schedule = Default::default(),
227 Unchanged => {}
228 }
229 match &options.auto_scaling_strategy {
230 Set(new_strategy) => auto_scaling_strategy.clone_from(new_strategy),
231 Reset => *auto_scaling_strategy = None,
233 Unchanged => {}
234 }
235 if !matches!(options.replicas, Unchanged) {
236 coord_bail!("Cannot change REPLICAS of managed clusters");
237 }
238 }
239 Unmanaged => {
240 if !matches!(options.size, Unchanged) {
241 coord_bail!("Cannot change SIZE of unmanaged clusters");
242 }
243 if !matches!(options.availability_zones, Unchanged) {
244 coord_bail!("Cannot change AVAILABILITY ZONES of unmanaged clusters");
245 }
246 if !matches!(options.introspection_debugging, Unchanged) {
247 coord_bail!("Cannot change INTROSPECTION DEGUBBING of unmanaged clusters");
248 }
249 if !matches!(options.introspection_interval, Unchanged) {
250 coord_bail!("Cannot change INTROSPECTION INTERVAL of unmanaged clusters");
251 }
252 if !matches!(options.arrangement_compression, Unchanged) {
253 coord_bail!(
254 "Cannot change EXPERIMENTAL ARRANGEMENT COMPRESSION of unmanaged clusters"
255 );
256 }
257 if !matches!(options.replication_factor, Unchanged) {
258 coord_bail!("Cannot change REPLICATION FACTOR of unmanaged clusters");
259 }
260 if !matches!(options.auto_scaling_strategy, Unchanged) {
261 coord_bail!("Cannot change AUTO SCALING STRATEGY of unmanaged clusters");
262 }
263 }
264 }
265
266 match &options.workload_class {
267 Set(wc) => new_config.workload_class.clone_from(wc),
268 Reset => new_config.workload_class = None,
269 Unchanged => {}
270 }
271
272 if let Managed(new_managed) = &new_config.variant {
276 if let Some(on_hydration) = new_managed
277 .auto_scaling_strategy
278 .as_ref()
279 .and_then(|strategy| strategy.on_hydration.as_ref())
280 {
281 let old_hydration_size = match &config.variant {
282 Managed(managed) => managed
283 .auto_scaling_strategy
284 .as_ref()
285 .and_then(|strategy| strategy.on_hydration.as_ref())
286 .map(|policy| &policy.hydration_size),
287 Unmanaged => None,
288 };
289 if old_hydration_size != Some(&on_hydration.hydration_size) {
290 let role_id = Some(session.role_metadata().current_role);
291 self.catalog.ensure_valid_replica_size(
292 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
293 &on_hydration.hydration_size,
294 false,
295 )?;
296 }
297 }
298 }
299
300 let reconfiguration_in_flight = matches!(
301 &config.variant,
302 Managed(managed) if managed
303 .reconfiguration
304 .as_ref()
305 .is_some_and(|record| record.is_in_progress())
306 );
307
308 if reconfiguration_in_flight && !matches!(options.schedule, Unchanged) {
314 return Err(AdapterError::AlterClusterScheduleWhileReconfiguring);
315 }
316
317 let cancels_or_retargets =
322 reconfiguration_in_flight && alter_changes_reconfiguration_target(options);
323 if new_config == config && !cancels_or_retargets {
324 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
325 ObjectType::Cluster,
326 )));
327 }
328
329 let single_replica_sources_notice = match (&config.variant, &new_config.variant) {
336 (Managed(old_managed), Managed(new_managed))
337 if new_managed.replication_factor > old_managed.replication_factor
338 && new_managed.replication_factor > 1 =>
339 {
340 let sources = self.single_replica_source_names(cluster);
341 (!sources.is_empty()).then(|| {
342 AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster {
343 cluster: cluster.name.clone(),
344 sources,
345 }
346 })
347 }
348 _ => None,
349 };
350
351 if let (Managed(old_managed), Managed(new_managed)) = (&config.variant, &new_config.variant)
364 {
365 let needs_record = if reconfiguration_in_flight {
366 alter_changes_reconfiguration_target(options)
367 } else {
368 new_managed.replica_config_shape() != old_managed.replica_config_shape()
369 };
370 let scheduled_direct =
383 !matches!(new_managed.schedule, mz_sql::plan::ClusterSchedule::Manual)
384 && !reconfiguration_in_flight;
385 if scheduled_direct && !matches!(strategy, AlterClusterPlanStrategy::None) {
391 return Err(AdapterError::AlterClusterWaitOnScheduledCluster);
392 }
393 if needs_record && !scheduled_direct {
394 let result = self
395 .reshape_alter_cluster_managed(
396 session,
397 cluster_id,
398 new_config.clone(),
399 options,
400 strategy,
401 validity,
402 )
403 .await;
404 if result.is_ok() {
405 if let Some(notice) = single_replica_sources_notice {
406 session.add_notice(notice);
407 }
408 }
409 return result;
410 }
411 }
412
413 match (&config.variant, &new_config.variant) {
414 (Managed(_), Managed(_)) => {
415 self.sequence_alter_cluster_managed_to_managed(
418 session,
419 cluster_id,
420 new_config.clone(),
421 )
422 .await?;
423 if let Some(notice) = single_replica_sources_notice {
424 session.add_notice(notice);
425 }
426 }
427 (Unmanaged, Managed(new_managed)) => {
428 if !matches!(new_managed.schedule, mz_sql::plan::ClusterSchedule::Manual)
433 && !matches!(strategy, AlterClusterPlanStrategy::None)
434 {
435 return Err(AdapterError::AlterClusterWaitOnScheduledCluster);
436 }
437 self.sequence_alter_cluster_unmanaged_to_managed(
438 session,
439 cluster_id,
440 new_config,
441 options.to_owned(),
442 )
443 .await?;
444 }
445 (Managed(_), Unmanaged) => {
446 self.sequence_alter_cluster_managed_to_unmanaged(session, cluster_id, new_config)
447 .await?;
448 }
449 (Unmanaged, Unmanaged) => {
450 self.sequence_alter_cluster_unmanaged_to_unmanaged(
451 session,
452 cluster_id,
453 new_config,
454 options.replicas.clone(),
455 )
456 .await?;
457 }
458 }
459
460 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
461 ObjectType::Cluster,
462 )))
463 }
464
465 async fn reshape_alter_cluster_managed(
501 &mut self,
502 session: &Session,
503 cluster_id: ClusterId,
504 new_config: ClusterConfig,
505 options: &PlanClusterOption,
506 strategy: &AlterClusterPlanStrategy,
507 validity: PlanValidity,
508 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
509 use mz_sql::plan::AlterOptionParameter::Unchanged;
510
511 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
512 return Err(AdapterError::Internal(
513 "reshape_alter_cluster_managed requires a managed target config".into(),
514 ));
515 };
516
517 let cluster = self.catalog.get_cluster(cluster_id);
523 let in_flight = match &cluster.config.variant {
524 ClusterVariant::Managed(managed) => managed
525 .reconfiguration
526 .as_ref()
527 .filter(|record| record.is_in_progress())
528 .cloned(),
529 ClusterVariant::Unmanaged => None,
530 };
531 let new_target = ReconfigurationTarget {
532 size: new_managed.size.clone(),
533 replication_factor: new_managed.replication_factor,
534 availability_zones: new_managed.availability_zones.clone(),
535 logging: new_managed.logging.clone(),
536 arrangement_compression: new_managed.arrangement_compression,
537 };
538 let unchanged = ReconfigurationDimensionsUnchanged {
539 size: matches!(options.size, Unchanged),
540 replication_factor: matches!(options.replication_factor, Unchanged),
541 availability_zones: matches!(options.availability_zones, Unchanged),
542 log_logging: matches!(options.introspection_debugging, Unchanged),
545 interval: matches!(options.introspection_interval, Unchanged),
546 arrangement_compression: matches!(options.arrangement_compression, Unchanged),
547 };
548 let target = fold_reconfiguration_target(
549 in_flight.as_ref().map(|r| &r.target),
550 new_target,
551 unchanged,
552 );
553
554 let size_in_use = matches!(
558 &cluster.config.variant,
559 ClusterVariant::Managed(managed) if managed.size == target.size
560 ) || in_flight
561 .as_ref()
562 .is_some_and(|record| record.target.size == target.size);
563 let role_id = session.role_metadata().current_role;
564 self.catalog.ensure_valid_replica_size(
565 &self
566 .catalog()
567 .get_role_allowed_cluster_sizes(&Some(role_id)),
568 &target.size,
569 size_in_use,
570 )?;
571 self.ensure_valid_azs(target.availability_zones.iter())?;
572
573 let cancels = match &cluster.config.variant {
574 ClusterVariant::Managed(managed) => target.matches_realized_config(managed),
575 ClusterVariant::Unmanaged => false,
576 };
577 if cluster_id.is_user() && !cancels {
583 self.validate_resource_limit(
584 0,
585 i64::from(target.replication_factor),
586 SystemVars::max_replicas_per_cluster,
587 "cluster replica",
588 MAX_REPLICAS_PER_CLUSTER.name(),
589 )?;
590
591 let credits_per_replica = self
592 .catalog()
593 .cluster_replica_sizes()
594 .0
595 .get(&target.size)
596 .expect("target replica size was validated")
597 .credits_per_hour;
598 let baseline_credits = credits_per_replica * Numeric::from(target.replication_factor);
599 self.validate_resource_limit_numeric(
600 self.current_credit_consumption_rate(Some(cluster_id)),
601 baseline_credits,
602 |system_vars| {
603 self.license_key
604 .max_credit_consumption_rate()
605 .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
606 },
607 "cluster replica",
608 MAX_CREDIT_CONSUMPTION_RATE.name(),
609 )?;
610 }
611
612 let now = self.now();
638 let deadline_from = |timeout: Duration| -> Timestamp {
639 now.saturating_add(u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX))
640 .into()
641 };
642 let (deadline, on_timeout) = match strategy {
643 AlterClusterPlanStrategy::None => match &in_flight {
644 Some(record) => (record.deadline, record.on_timeout),
645 None => (
646 deadline_from(
647 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT
648 .get(self.catalog().system_config().dyncfgs()),
649 ),
650 OnTimeoutAction::Rollback,
651 ),
652 },
653 AlterClusterPlanStrategy::For(timeout) => {
654 (deadline_from(*timeout), OnTimeoutAction::Rollback)
655 }
656 AlterClusterPlanStrategy::UntilReady {
657 timeout,
658 on_timeout,
659 } => (
660 deadline_from(*timeout),
661 on_timeout.unwrap_or(OnTimeoutAction::Rollback),
662 ),
663 };
664
665 let cluster = self.catalog.get_cluster(cluster_id);
674 let cluster_name = cluster.name().to_string();
675 let ClusterVariant::Managed(realized_now) = &cluster.config.variant else {
676 return Err(AdapterError::Internal(
677 "reshape_alter_cluster_managed requires a managed realized config".into(),
678 ));
679 };
680 let realized_target = realized_now.realized_reconfiguration_target();
681 let (status, audit) = if cancels {
685 (
686 ReconfigurationStatus::Cancelled,
687 ReconfigurationAudit::Cancelled,
688 )
689 } else {
690 (
691 ReconfigurationStatus::InProgress,
692 ReconfigurationAudit::Started,
693 )
694 };
695 let record = ReconfigurationState {
696 target: target.clone(),
697 deadline,
698 on_timeout,
699 status,
700 };
701
702 let mut realized = new_config.clone();
703 let ClusterVariant::Managed(realized_managed) = &mut realized.variant else {
704 return Err(AdapterError::Internal(
705 "reshape_alter_cluster_managed requires a managed target config".into(),
706 ));
707 };
708 realized_managed.apply_reconfiguration_target(realized_target);
709 realized_managed.reconfiguration = Some(record);
710
711 self.catalog_transact(
712 Some(session),
713 vec![Op::UpdateClusterConfig {
714 id: cluster_id,
715 name: cluster_name,
716 config: realized,
717 reconfiguration_audit: Some(audit),
718 burst_audit: None,
719 }],
720 )
721 .await?;
722
723 let background =
724 ENABLE_BACKGROUND_ALTER_CLUSTER.get(self.catalog().system_config().dyncfgs());
725 if background {
726 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
727 ObjectType::Cluster,
728 )));
729 }
730
731 Ok(StageResult::Immediate(Box::new(
735 ClusterStage::AwaitReconfiguration(AlterClusterAwaitReconfiguration {
736 validity,
737 cluster_id,
738 target,
739 }),
740 )))
741 }
742
743 fn await_reconfiguration_stage(
749 &self,
750 validity: PlanValidity,
751 cluster_id: ClusterId,
752 target: ReconfigurationTarget,
753 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
754 let Some(cluster) = self.catalog().try_get_cluster(cluster_id) else {
755 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
758 ObjectType::Cluster,
759 )));
760 };
761 let record = match &cluster.config.variant {
762 ClusterVariant::Managed(managed) => managed.reconfiguration.clone(),
763 ClusterVariant::Unmanaged => None,
764 };
765
766 let realized_matches_target = match &cluster.config.variant {
767 ClusterVariant::Managed(managed) => target.matches_realized_config(managed),
768 ClusterVariant::Unmanaged => false,
769 };
770
771 match reconfiguration_wait_result(record.as_ref(), &target, realized_matches_target) {
772 Some(result) => {
773 result?;
774 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
775 ObjectType::Cluster,
776 )))
777 }
778 None => {
779 let poll_duration = self
789 .catalog
790 .system_config()
791 .cluster_alter_check_ready_interval();
792 let span = Span::current();
793 Ok(StageResult::Handle(mz_ore::task::spawn(
794 || "Await Cluster Reconfiguration",
795 async move {
796 tokio::time::sleep(poll_duration).await;
797 Ok(Box::new(ClusterStage::AwaitReconfiguration(
798 AlterClusterAwaitReconfiguration {
799 validity,
800 cluster_id,
801 target,
802 },
803 )))
804 }
805 .instrument(span),
806 )))
807 }
808 }
809 }
810
811 pub(crate) async fn sequence_create_cluster(
812 &mut self,
813 session: &Session,
814 CreateClusterPlan {
815 name,
816 variant,
817 workload_class,
818 if_not_exists,
819 }: CreateClusterPlan,
820 ) -> Result<ExecuteResponse, AdapterError> {
821 tracing::debug!("sequence_create_cluster");
822
823 let id_ts = self.get_catalog_write_ts().await;
824 let id = self.catalog().allocate_user_cluster_id(id_ts).await?;
825 let introspection_sources = BUILTINS::logs().collect();
830 let cluster_variant = match &variant {
831 CreateClusterVariant::Managed(plan) => {
832 let logging = if let Some(config) = plan.compute.introspection {
833 ReplicaLogging {
834 log_logging: config.debugging,
835 interval: Some(config.interval),
836 }
837 } else {
838 ReplicaLogging::default()
839 };
840 ClusterVariant::Managed(ClusterVariantManaged {
841 size: plan.size.clone(),
842 availability_zones: plan.availability_zones.clone(),
843 logging,
844 arrangement_compression: plan.compute.arrangement_compression,
845 replication_factor: plan.replication_factor,
846 optimizer_feature_overrides: plan.optimizer_feature_overrides.clone(),
847 schedule: plan.schedule.clone(),
848 auto_scaling_strategy: plan.auto_scaling_strategy.clone(),
849 reconfiguration: None,
850 burst: None,
851 })
852 }
853 CreateClusterVariant::Unmanaged(_) => ClusterVariant::Unmanaged,
854 };
855 let config = ClusterConfig {
856 variant: cluster_variant,
857 workload_class,
858 };
859 let ops = vec![catalog::Op::CreateCluster {
860 id,
861 name: name.clone(),
862 introspection_sources,
863 owner_id: *session.current_role_id(),
864 config,
865 }];
866
867 match variant {
868 CreateClusterVariant::Managed(plan) => {
869 self.sequence_create_managed_cluster(session, plan, id, ops)
870 .await
871 }
872 CreateClusterVariant::Unmanaged(plan) => {
873 self.sequence_create_unmanaged_cluster(session, plan, id, ops)
874 .await
875 }
876 }
877 .or_else(|err| match err {
878 AdapterError::Catalog(mz_catalog::memory::error::Error {
879 kind: ErrorKind::Sql(CatalogError::ClusterAlreadyExists(_)),
880 }) if if_not_exists => {
881 session.add_notice(AdapterNotice::ObjectAlreadyExists {
882 name,
883 ty: "cluster",
884 });
885 Ok(ExecuteResponse::CreatedCluster)
886 }
887 err => Err(err),
888 })
889 }
890
891 #[mz_ore::instrument(level = "debug")]
892 async fn sequence_create_managed_cluster(
893 &mut self,
894 session: &Session,
895 CreateClusterManagedPlan {
896 availability_zones,
897 compute,
898 replication_factor,
899 size,
900 optimizer_feature_overrides: _,
901 schedule: _,
902 auto_scaling_strategy,
903 }: CreateClusterManagedPlan,
904 cluster_id: ClusterId,
905 mut ops: Vec<catalog::Op>,
906 ) -> Result<ExecuteResponse, AdapterError> {
907 tracing::debug!("sequence_create_managed_cluster");
908
909 self.ensure_valid_azs(availability_zones.iter())?;
910
911 let role_id = session.role_metadata().current_role;
912 self.catalog.ensure_valid_replica_size(
913 &self
914 .catalog()
915 .get_role_allowed_cluster_sizes(&Some(role_id)),
916 &size,
917 false,
918 )?;
919 if let Some(on_hydration) = auto_scaling_strategy
925 .as_ref()
926 .and_then(|strategy| strategy.on_hydration.as_ref())
927 {
928 self.catalog.ensure_valid_replica_size(
929 &self
930 .catalog()
931 .get_role_allowed_cluster_sizes(&Some(role_id)),
932 &on_hydration.hydration_size,
933 false,
934 )?;
935 }
936
937 if cluster_id.is_user() {
942 self.validate_resource_limit(
943 0,
944 i64::from(replication_factor),
945 SystemVars::max_replicas_per_cluster,
946 "cluster replica",
947 MAX_REPLICAS_PER_CLUSTER.name(),
948 )?;
949 }
950
951 let id_ts = self.get_catalog_write_ts().await;
955 let replica_ids = self
956 .catalog()
957 .allocate_replica_ids(cluster_id, u64::from(replication_factor), id_ts)
958 .await?;
959
960 for (replica_id, replica_name) in replica_ids
961 .into_iter()
962 .zip_eq((0..replication_factor).map(managed_cluster_replica_name))
963 {
964 self.create_managed_cluster_replica_op(
965 cluster_id,
966 replica_id,
967 replica_name.clone(),
968 &compute,
969 &size,
970 &mut ops,
971 if availability_zones.is_empty() {
972 None
973 } else {
974 Some(availability_zones.as_ref())
975 },
976 false,
977 *session.current_role_id(),
978 ReplicaCreateDropReason::Manual,
979 )?;
980 }
981
982 self.catalog_transact(Some(session), ops).await?;
983
984 Ok(ExecuteResponse::CreatedCluster)
985 }
986
987 fn create_managed_cluster_replica_op(
988 &self,
989 cluster_id: ClusterId,
990 replica_id: ReplicaId,
991 name: String,
992 compute: &mz_sql::plan::ComputeReplicaConfig,
993 size: &String,
994 ops: &mut Vec<Op>,
995 azs: Option<&[String]>,
996 pending: bool,
997 owner_id: RoleId,
998 reason: ReplicaCreateDropReason,
999 ) -> Result<(), AdapterError> {
1000 let location = mz_catalog::durable::ReplicaLocation::Managed {
1001 availability_zones: Vec::new(),
1004 billed_as: None,
1005 internal: false,
1006 size: size.clone(),
1007 pending,
1008 };
1009
1010 let logging = if let Some(config) = compute.introspection {
1011 ReplicaLogging {
1012 log_logging: config.debugging,
1013 interval: Some(config.interval),
1014 }
1015 } else {
1016 ReplicaLogging::default()
1017 };
1018
1019 let config = ReplicaConfig {
1020 location: self.catalog().concretize_replica_location(
1021 location,
1022 &self
1023 .catalog()
1024 .get_role_allowed_cluster_sizes(&Some(owner_id)),
1025 azs,
1026 false,
1027 )?,
1028 compute: ComputeReplicaConfig {
1029 logging,
1030 arrangement_compression: compute.arrangement_compression,
1031 },
1032 };
1033
1034 ops.push(catalog::Op::CreateClusterReplica {
1037 cluster_id,
1038 replica_id,
1039 name,
1040 config,
1041 owner_id,
1042 reason,
1043 });
1044 Ok(())
1045 }
1046
1047 fn ensure_valid_azs<'a, I: IntoIterator<Item = &'a String>>(
1048 &self,
1049 azs: I,
1050 ) -> Result<(), AdapterError> {
1051 let cat_azs = self.catalog().state().availability_zones();
1052 for az in azs.into_iter() {
1053 if !cat_azs.contains(az) {
1054 return Err(AdapterError::InvalidClusterReplicaAz {
1055 az: az.to_string(),
1056 expected: cat_azs.to_vec(),
1057 });
1058 }
1059 }
1060 Ok(())
1061 }
1062
1063 #[mz_ore::instrument(level = "debug")]
1064 async fn sequence_create_unmanaged_cluster(
1065 &mut self,
1066 session: &Session,
1067 CreateClusterUnmanagedPlan { replicas }: CreateClusterUnmanagedPlan,
1068 id: ClusterId,
1069 mut ops: Vec<catalog::Op>,
1070 ) -> Result<ExecuteResponse, AdapterError> {
1071 tracing::debug!("sequence_create_unmanaged_cluster");
1072
1073 self.ensure_valid_azs(replicas.iter().filter_map(|(_, r)| {
1074 if let mz_sql::plan::ReplicaConfig::Orchestrated {
1075 availability_zone: Some(az),
1076 ..
1077 } = &r
1078 {
1079 Some(az)
1080 } else {
1081 None
1082 }
1083 }))?;
1084
1085 if id.is_user() {
1090 self.validate_resource_limit(
1091 0,
1092 i64::try_from(replicas.len()).unwrap_or(i64::MAX),
1093 SystemVars::max_replicas_per_cluster,
1094 "cluster replica",
1095 MAX_REPLICAS_PER_CLUSTER.name(),
1096 )?;
1097 }
1098
1099 let id_ts = self.get_catalog_write_ts().await;
1103 let replica_ids = self
1104 .catalog()
1105 .allocate_replica_ids(id, u64::cast_from(replicas.len()), id_ts)
1106 .await?;
1107
1108 for (replica_id, (replica_name, replica_config)) in replica_ids.into_iter().zip_eq(replicas)
1109 {
1110 let (compute, location) = match replica_config {
1113 mz_sql::plan::ReplicaConfig::Unorchestrated {
1114 storagectl_addrs,
1115 computectl_addrs,
1116 compute,
1117 } => {
1118 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1119 storagectl_addrs,
1120 computectl_addrs,
1121 };
1122 (compute, location)
1123 }
1124 mz_sql::plan::ReplicaConfig::Orchestrated {
1125 availability_zone,
1126 billed_as,
1127 compute,
1128 internal,
1129 size,
1130 } => {
1131 if !session.user().is_internal() && (internal || billed_as.is_some()) {
1133 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1134 }
1135 if billed_as.is_some() && !internal {
1137 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1138 }
1139 if let Some(billed_as) = &billed_as {
1142 self.ensure_valid_billed_as_size(billed_as)?;
1143 }
1144
1145 let location = mz_catalog::durable::ReplicaLocation::Managed {
1146 availability_zones: availability_zone.into_iter().collect(),
1149 billed_as,
1150 internal,
1151 size: size.clone(),
1152 pending: false,
1153 };
1154 (compute, location)
1155 }
1156 };
1157
1158 let logging = if let Some(config) = compute.introspection {
1159 ReplicaLogging {
1160 log_logging: config.debugging,
1161 interval: Some(config.interval),
1162 }
1163 } else {
1164 ReplicaLogging::default()
1165 };
1166
1167 let role_id = session.role_metadata().current_role;
1168 let config = ReplicaConfig {
1169 location: self.catalog().concretize_replica_location(
1170 location,
1171 &self
1172 .catalog()
1173 .get_role_allowed_cluster_sizes(&Some(role_id)),
1174 None,
1175 false,
1176 )?,
1177 compute: ComputeReplicaConfig {
1178 logging,
1179 arrangement_compression: compute.arrangement_compression,
1180 },
1181 };
1182
1183 ops.push(catalog::Op::CreateClusterReplica {
1184 cluster_id: id,
1185 replica_id,
1186 name: replica_name.clone(),
1187 config,
1188 owner_id: *session.current_role_id(),
1189 reason: ReplicaCreateDropReason::Manual,
1190 });
1191 }
1192
1193 self.catalog_transact(Some(session), ops).await?;
1194
1195 Ok(ExecuteResponse::CreatedCluster)
1196 }
1197
1198 fn single_replica_source_names(&self, cluster: &Cluster) -> Vec<String> {
1202 cluster
1203 .bound_objects
1204 .iter()
1205 .filter_map(|id| {
1206 let entry = self.catalog().get_entry(id);
1207 let single_replica =
1208 entry
1209 .source()
1210 .is_some_and(|source| match &source.data_source {
1211 DataSourceDesc::Ingestion { desc, .. }
1212 | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
1213 desc.connection.prefers_single_replica()
1214 }
1215 _ => false,
1216 });
1217 single_replica.then(|| {
1218 self.catalog()
1219 .resolve_full_name(entry.name(), None)
1220 .to_string()
1221 })
1222 })
1223 .collect()
1224 }
1225
1226 fn notice_relevant_replica_count(&self, cluster: &Cluster) -> usize {
1238 match &cluster.config.variant {
1239 ClusterVariant::Managed(managed) => {
1240 let replication_factor = managed
1241 .reconfiguration
1242 .as_ref()
1243 .filter(|record| record.is_in_progress())
1244 .map_or(managed.replication_factor, |record| {
1245 record.target.replication_factor
1246 });
1247 let manual_replicas = cluster
1248 .replicas()
1249 .filter(|r| {
1250 r.config.location.internal() || r.config.location.billed_as().is_some()
1251 })
1252 .count();
1253 usize::cast_from(replication_factor) + manual_replicas
1254 }
1255 ClusterVariant::Unmanaged => cluster.replicas().count(),
1256 }
1257 }
1258
1259 pub(crate) fn notify_single_replica_sources(
1268 &self,
1269 session: &Session,
1270 cluster: &Cluster,
1271 creating_source: Option<&FullItemName>,
1272 ) {
1273 if self.notice_relevant_replica_count(cluster) <= 1 {
1274 return;
1275 }
1276 let mut sources = self.single_replica_source_names(cluster);
1277 if let Some(name) = creating_source {
1278 let full_name = name.to_string();
1279 if !sources.contains(&full_name) {
1280 sources.push(full_name);
1281 }
1282 }
1283 if !sources.is_empty() {
1284 session.add_notice(AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster {
1285 cluster: cluster.name.clone(),
1286 sources,
1287 });
1288 }
1289 }
1290
1291 fn ensure_valid_billed_as_size(&self, size: &str) -> Result<(), AdapterError> {
1298 if self.catalog().cluster_replica_sizes().0.contains_key(size) {
1299 Ok(())
1300 } else {
1301 coord_bail!("unknown cluster replica size {size} in BILLED AS")
1302 }
1303 }
1304
1305 #[mz_ore::instrument(level = "debug")]
1306 pub(crate) async fn sequence_create_cluster_replica(
1307 &mut self,
1308 session: &Session,
1309 CreateClusterReplicaPlan {
1310 name,
1311 cluster_id,
1312 config,
1313 if_not_exists,
1314 }: CreateClusterReplicaPlan,
1315 ) -> Result<ExecuteResponse, AdapterError> {
1316 let (compute, location) = match config {
1318 mz_sql::plan::ReplicaConfig::Unorchestrated {
1319 storagectl_addrs,
1320 computectl_addrs,
1321 compute,
1322 } => {
1323 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1324 storagectl_addrs,
1325 computectl_addrs,
1326 };
1327 (compute, location)
1328 }
1329 mz_sql::plan::ReplicaConfig::Orchestrated {
1330 availability_zone,
1331 billed_as,
1332 compute,
1333 internal,
1334 size,
1335 } => {
1336 let availability_zone = match availability_zone {
1337 Some(az) => {
1338 self.ensure_valid_azs([&az])?;
1339 Some(az)
1340 }
1341 None => None,
1342 };
1343 let location = mz_catalog::durable::ReplicaLocation::Managed {
1344 availability_zones: availability_zone.into_iter().collect(),
1347 billed_as,
1348 internal,
1349 size,
1350 pending: false,
1351 };
1352 (compute, location)
1353 }
1354 };
1355
1356 let logging = if let Some(config) = compute.introspection {
1357 ReplicaLogging {
1358 log_logging: config.debugging,
1359 interval: Some(config.interval),
1360 }
1361 } else {
1362 ReplicaLogging::default()
1363 };
1364
1365 let role_id = session.role_metadata().current_role;
1366 let config = ReplicaConfig {
1367 location: self.catalog().concretize_replica_location(
1368 location,
1369 &self
1370 .catalog()
1371 .get_role_allowed_cluster_sizes(&Some(role_id)),
1372 None,
1375 false,
1376 )?,
1377 compute: ComputeReplicaConfig {
1378 logging,
1379 arrangement_compression: compute.arrangement_compression,
1380 },
1381 };
1382
1383 let cluster = self.catalog().get_cluster(cluster_id);
1384
1385 if let ReplicaLocation::Managed(ManagedReplicaLocation {
1386 internal,
1387 billed_as,
1388 ..
1389 }) = &config.location
1390 {
1391 if !session.user().is_internal() && (*internal || billed_as.is_some()) {
1393 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1394 }
1395 if cluster.is_managed() && !*internal {
1397 coord_bail!("must specify INTERNAL when creating a replica in a managed cluster");
1398 }
1399 if billed_as.is_some() && !*internal {
1401 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1402 }
1403 if let Some(billed_as) = billed_as {
1406 self.ensure_valid_billed_as_size(billed_as)?;
1407 }
1408 }
1409
1410 let owner_id = cluster.owner_id();
1413
1414 let cluster_name = cluster.name.clone();
1415 let qualified_name = format!("{cluster_name}.{name}");
1418
1419 let id_ts = self.get_catalog_write_ts().await;
1424 let replica_id = self
1425 .catalog()
1426 .allocate_replica_ids(cluster_id, 1, id_ts)
1427 .await?
1428 .into_element();
1429
1430 let ops = vec![catalog::Op::CreateClusterReplica {
1431 cluster_id,
1432 replica_id,
1433 name: name.clone(),
1434 config,
1435 owner_id,
1436 reason: ReplicaCreateDropReason::Manual,
1437 }];
1438
1439 match self.catalog_transact(Some(session), ops).await {
1440 Ok(()) => {
1441 self.notify_single_replica_sources(
1444 session,
1445 self.catalog().get_cluster(cluster_id),
1446 None,
1447 );
1448 Ok(ExecuteResponse::CreatedClusterReplica)
1449 }
1450 Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
1451 kind: ErrorKind::Sql(CatalogError::DuplicateReplica(_, _)),
1452 })) if if_not_exists => {
1453 session.add_notice(AdapterNotice::ObjectAlreadyExists {
1454 name: qualified_name,
1455 ty: "cluster replica",
1456 });
1457 Ok(ExecuteResponse::CreatedClusterReplica)
1458 }
1459 Err(err) => Err(err),
1460 }
1461 }
1462
1463 pub(crate) async fn sequence_alter_cluster_managed_to_managed(
1477 &mut self,
1478 session: &Session,
1479 cluster_id: ClusterId,
1480 new_config: ClusterConfig,
1481 ) -> Result<(), AdapterError> {
1482 let cluster = self.catalog.get_cluster(cluster_id);
1483 let name = cluster.name().to_string();
1484
1485 let ClusterVariant::Managed(ClusterVariantManaged {
1486 size,
1487 availability_zones,
1488 logging,
1489 arrangement_compression,
1490 replication_factor,
1491 optimizer_feature_overrides: _,
1492 schedule: _,
1493 auto_scaling_strategy,
1494 reconfiguration,
1495 burst: _,
1496 }) = &cluster.config.variant
1497 else {
1498 panic!("expected existing managed cluster config");
1499 };
1500 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
1501 panic!("expected new managed cluster config");
1502 };
1503 let ClusterVariantManaged {
1504 size: new_size,
1505 replication_factor: new_replication_factor,
1506 availability_zones: new_availability_zones,
1507 logging: _,
1508 arrangement_compression: _,
1509 optimizer_feature_overrides: _,
1510 schedule: _,
1511 auto_scaling_strategy: new_auto_scaling_strategy,
1512 reconfiguration: _,
1513 burst: _,
1514 } = new_managed;
1515
1516 let role_id = Some(session.role_metadata().current_role);
1517 self.catalog.ensure_valid_replica_size(
1519 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
1520 new_size,
1521 new_size == size,
1522 )?;
1523 if new_auto_scaling_strategy != auto_scaling_strategy {
1524 if let Some(on_hydration) = new_auto_scaling_strategy
1525 .as_ref()
1526 .and_then(|strategy| strategy.on_hydration.as_ref())
1527 {
1528 if reconfiguration.as_ref().is_some_and(|record| {
1535 record.is_in_progress() && record.target.size == on_hydration.hydration_size
1536 }) {
1537 coord_bail!(
1538 "HYDRATION SIZE must differ from the target SIZE \
1539 ('{}') of the in-progress cluster resize",
1540 on_hydration.hydration_size
1541 );
1542 }
1543 }
1544 }
1545
1546 if new_replication_factor > replication_factor && cluster_id.is_user() {
1555 self.validate_resource_limit(
1556 usize::cast_from(*replication_factor),
1557 i64::from(*new_replication_factor) - i64::from(*replication_factor),
1558 SystemVars::max_replicas_per_cluster,
1559 "cluster replica",
1560 MAX_REPLICAS_PER_CLUSTER.name(),
1561 )?;
1562
1563 let credits_per_replica = self
1564 .catalog()
1565 .cluster_replica_sizes()
1566 .0
1567 .get(new_size)
1568 .expect("new replica size was validated")
1569 .credits_per_hour;
1570 let baseline_credits = credits_per_replica * Numeric::from(*new_replication_factor);
1571 self.validate_resource_limit_numeric(
1572 self.current_credit_consumption_rate(Some(cluster_id)),
1573 baseline_credits,
1574 |system_vars| {
1575 self.license_key
1576 .max_credit_consumption_rate()
1577 .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
1578 },
1579 "cluster replica",
1580 MAX_CREDIT_CONSUMPTION_RATE.name(),
1581 )?;
1582 }
1583
1584 let config_changed = new_managed.replica_config_shape()
1585 != ManagedReplicaConfigShape::new(
1586 size,
1587 availability_zones,
1588 logging,
1589 *arrangement_compression,
1590 );
1591 if config_changed {
1595 self.ensure_valid_azs(new_availability_zones.iter())?;
1596 }
1597
1598 let ops = vec![catalog::Op::UpdateClusterConfig {
1601 id: cluster_id,
1602 name,
1603 config: new_config,
1604 reconfiguration_audit: None,
1605 burst_audit: None,
1606 }];
1607
1608 self.catalog_transact(Some(session), ops).await?;
1609 Ok(())
1610 }
1611
1612 async fn sequence_alter_cluster_unmanaged_to_managed(
1616 &mut self,
1617 session: &Session,
1618 cluster_id: ClusterId,
1619 mut new_config: ClusterConfig,
1620 options: PlanClusterOption,
1621 ) -> Result<(), AdapterError> {
1622 let cluster = self.catalog.get_cluster(cluster_id);
1623 let cluster_name = cluster.name().to_string();
1624
1625 let ClusterVariant::Managed(ClusterVariantManaged {
1626 size: new_size,
1627 replication_factor: new_replication_factor,
1628 availability_zones: new_availability_zones,
1629 logging: _,
1630 arrangement_compression: _,
1631 optimizer_feature_overrides: _,
1632 schedule: _,
1633 auto_scaling_strategy: _,
1634 reconfiguration: _,
1635 burst: _,
1636 }) = &mut new_config.variant
1637 else {
1638 panic!("expected new managed cluster config");
1639 };
1640
1641 let user_replica_count = cluster
1643 .user_replicas()
1644 .count()
1645 .try_into()
1646 .expect("must_fit");
1647 match options.replication_factor {
1648 AlterOptionParameter::Set(_) => {
1649 if user_replica_count != *new_replication_factor {
1651 coord_bail!(
1652 "REPLICATION FACTOR {new_replication_factor} does not match number of replicas ({user_replica_count})"
1653 );
1654 }
1655 }
1656 _ => {
1657 *new_replication_factor = user_replica_count;
1658 }
1659 }
1660
1661 let mut names = BTreeSet::new();
1662 let mut sizes = BTreeSet::new();
1663
1664 self.ensure_valid_azs(new_availability_zones.iter())?;
1665
1666 for replica in cluster.user_replicas() {
1668 names.insert(replica.name.clone());
1669 match &replica.config.location {
1670 ReplicaLocation::Unmanaged(_) => coord_bail!(
1671 "Cannot convert unmanaged cluster with unmanaged replicas to managed cluster"
1672 ),
1673 ReplicaLocation::Managed(location) => {
1674 sizes.insert(location.size.clone());
1675
1676 for az in &location.availability_zones {
1680 if !new_availability_zones.contains(az) {
1681 coord_bail!(
1682 "unmanaged replica has availability zone {az} which is not \
1683 in managed {new_availability_zones:?}"
1684 )
1685 }
1686 }
1687 }
1688 }
1689 }
1690
1691 if sizes.is_empty() {
1692 assert!(
1693 cluster.user_replicas().next().is_none(),
1694 "Cluster should not have replicas"
1695 );
1696 match &options.size {
1698 AlterOptionParameter::Reset | AlterOptionParameter::Unchanged => {
1699 coord_bail!("Missing SIZE for empty cluster")
1700 }
1701 AlterOptionParameter::Set(size) => {
1702 let role_id = Some(session.role_metadata().current_role);
1703 self.catalog.ensure_valid_replica_size(
1704 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
1705 size,
1706 false,
1707 )?;
1708 }
1709 }
1710 } else if sizes.len() == 1 {
1711 let size = sizes.into_iter().next().expect("must exist");
1712 match &options.size {
1713 AlterOptionParameter::Set(sz) if *sz != size => {
1714 coord_bail!("Cluster replicas of size {size} do not match expected SIZE {sz}");
1715 }
1716 _ => *new_size = size,
1717 }
1718 } else {
1719 let formatted = sizes
1720 .iter()
1721 .map(String::as_str)
1722 .collect::<Vec<_>>()
1723 .join(", ");
1724 coord_bail!(
1725 "Cannot convert unmanaged cluster to managed, non-unique replica sizes: {formatted}"
1726 );
1727 }
1728
1729 for i in 0..*new_replication_factor {
1730 let name = managed_cluster_replica_name(i);
1731 names.remove(&name);
1732 }
1733 if !names.is_empty() {
1734 let formatted = names
1735 .iter()
1736 .map(String::as_str)
1737 .collect::<Vec<_>>()
1738 .join(", ");
1739 coord_bail!(
1740 "Cannot convert unmanaged cluster to managed, invalid replica names: {formatted}"
1741 );
1742 }
1743
1744 let ops = vec![catalog::Op::UpdateClusterConfig {
1745 id: cluster_id,
1746 name: cluster_name,
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_managed_to_unmanaged(
1757 &mut self,
1758 session: &Session,
1759 cluster_id: ClusterId,
1760 new_config: ClusterConfig,
1761 ) -> Result<(), AdapterError> {
1762 let cluster = self.catalog().get_cluster(cluster_id);
1763
1764 if let ClusterVariant::Managed(managed) = &cluster.config.variant {
1770 if managed
1771 .reconfiguration
1772 .as_ref()
1773 .is_some_and(|record| record.is_in_progress())
1774 {
1775 return Err(AdapterError::AlterClusterUnmanagedWhileReconfiguring);
1776 }
1777 if managed.burst.is_some() {
1784 return Err(AdapterError::AlterClusterUnmanagedWhileBursting);
1785 }
1786 }
1787
1788 let ops = vec![catalog::Op::UpdateClusterConfig {
1789 id: cluster_id,
1790 name: cluster.name().to_string(),
1791 config: new_config,
1792 reconfiguration_audit: None,
1793 burst_audit: None,
1794 }];
1795
1796 self.catalog_transact(Some(session), ops).await?;
1797 Ok(())
1798 }
1799
1800 async fn sequence_alter_cluster_unmanaged_to_unmanaged(
1801 &mut self,
1802 session: &Session,
1803 cluster_id: ClusterId,
1804 new_config: ClusterConfig,
1805 replicas: AlterOptionParameter<Vec<(String, mz_sql::plan::ReplicaConfig)>>,
1806 ) -> Result<(), AdapterError> {
1807 if !matches!(replicas, AlterOptionParameter::Unchanged) {
1808 coord_bail!("Cannot alter replicas in unmanaged cluster");
1809 }
1810
1811 let cluster = self.catalog().get_cluster(cluster_id);
1812
1813 let ops = vec![catalog::Op::UpdateClusterConfig {
1814 id: cluster_id,
1815 name: cluster.name().to_string(),
1816 config: new_config,
1817 reconfiguration_audit: None,
1818 burst_audit: None,
1819 }];
1820
1821 self.catalog_transact(Some(session), ops).await?;
1822 Ok(())
1823 }
1824
1825 pub(crate) async fn sequence_alter_set_cluster(
1827 &self,
1828 _session: &Session,
1829 AlterSetClusterPlan { id, set_cluster: _ }: AlterSetClusterPlan,
1830 ) -> Result<ExecuteResponse, AdapterError> {
1831 async {}.await;
1835 let entry = self.catalog().get_entry(&id);
1836 match entry.item().typ() {
1837 _ => {
1838 Err(AdapterError::Unsupported("ALTER SET CLUSTER"))
1840 }
1841 }
1842 }
1843}
1844
1845struct ReconfigurationDimensionsUnchanged {
1850 size: bool,
1851 replication_factor: bool,
1852 availability_zones: bool,
1853 log_logging: bool,
1854 interval: bool,
1855 arrangement_compression: bool,
1856}
1857
1858fn reconfiguration_wait_result(
1864 record: Option<&ReconfigurationState>,
1865 awaited_target: &ReconfigurationTarget,
1866 realized_matches_target: bool,
1867) -> Option<Result<(), AdapterError>> {
1868 if realized_matches_target {
1869 return Some(Ok(()));
1870 }
1871 let Some(record) = record.filter(|record| record.target == *awaited_target) else {
1872 return Some(Err(AdapterError::AlterClusterSuperseded));
1873 };
1874 match record.status {
1875 ReconfigurationStatus::InProgress => None,
1876 ReconfigurationStatus::ResourceExhausted => {
1877 Some(Err(AdapterError::AlterClusterResourceExhausted))
1878 }
1879 ReconfigurationStatus::TimedOut => Some(Err(AdapterError::AlterClusterTimeout)),
1880 ReconfigurationStatus::Finalized | ReconfigurationStatus::Cancelled => {
1881 Some(Err(AdapterError::AlterClusterSuperseded))
1882 }
1883 }
1884}
1885
1886fn alter_changes_reconfiguration_target(options: &PlanClusterOption) -> bool {
1896 use mz_sql::plan::AlterOptionParameter::Unchanged;
1897 let PlanClusterOption {
1898 availability_zones,
1899 introspection_debugging,
1900 introspection_interval,
1901 arrangement_compression,
1902 managed: _,
1903 replicas: _,
1904 replication_factor,
1905 size,
1906 schedule: _,
1907 workload_class: _,
1908 auto_scaling_strategy: _,
1909 } = options;
1910 !matches!(size, Unchanged)
1911 || !matches!(replication_factor, Unchanged)
1912 || !matches!(availability_zones, Unchanged)
1913 || !matches!(introspection_debugging, Unchanged)
1914 || !matches!(introspection_interval, Unchanged)
1915 || !matches!(arrangement_compression, Unchanged)
1916}
1917
1918fn fold_reconfiguration_target(
1930 in_flight: Option<&ReconfigurationTarget>,
1931 new_target: ReconfigurationTarget,
1932 unchanged: ReconfigurationDimensionsUnchanged,
1933) -> ReconfigurationTarget {
1934 let Some(prev) = in_flight else {
1935 return new_target;
1936 };
1937 ReconfigurationTarget {
1938 size: if unchanged.size {
1939 prev.size.clone()
1940 } else {
1941 new_target.size
1942 },
1943 replication_factor: if unchanged.replication_factor {
1944 prev.replication_factor
1945 } else {
1946 new_target.replication_factor
1947 },
1948 availability_zones: if unchanged.availability_zones {
1949 prev.availability_zones.clone()
1950 } else {
1951 new_target.availability_zones
1952 },
1953 logging: ReplicaLogging {
1954 log_logging: if unchanged.log_logging {
1955 prev.logging.log_logging
1956 } else {
1957 new_target.logging.log_logging
1958 },
1959 interval: if unchanged.interval {
1960 prev.logging.interval
1961 } else {
1962 new_target.logging.interval
1963 },
1964 },
1965 arrangement_compression: if unchanged.arrangement_compression {
1966 prev.arrangement_compression
1967 } else {
1968 new_target.arrangement_compression
1969 },
1970 }
1971}
1972
1973#[cfg(test)]
1974mod tests {
1975 use mz_controller::clusters::ReplicaLogging;
1976 use mz_controller_types::DEFAULT_REPLICA_LOGGING_INTERVAL;
1977
1978 use super::*;
1979
1980 fn target(size: &str, rf: u32, azs: &[&str], log_logging: bool) -> ReconfigurationTarget {
1981 ReconfigurationTarget {
1982 size: size.to_string(),
1983 replication_factor: rf,
1984 availability_zones: azs.iter().map(|s| s.to_string()).collect(),
1985 logging: ReplicaLogging {
1986 log_logging,
1987 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
1988 },
1989 arrangement_compression: false,
1990 }
1991 }
1992
1993 fn all_changed() -> ReconfigurationDimensionsUnchanged {
1994 ReconfigurationDimensionsUnchanged {
1995 size: false,
1996 replication_factor: false,
1997 availability_zones: false,
1998 log_logging: false,
1999 interval: false,
2000 arrangement_compression: false,
2001 }
2002 }
2003
2004 fn all_unchanged() -> ReconfigurationDimensionsUnchanged {
2005 ReconfigurationDimensionsUnchanged {
2006 size: true,
2007 replication_factor: true,
2008 availability_zones: true,
2009 log_logging: true,
2010 interval: true,
2011 arrangement_compression: true,
2012 }
2013 }
2014
2015 #[mz_ore::test]
2016 fn foreground_wait_succeeds_when_target_is_realized() {
2017 let awaited = target("200cc", 1, &[], false);
2018 let record = ReconfigurationState {
2019 target: target("300cc", 1, &[], false),
2020 deadline: Timestamp::from(0),
2021 on_timeout: OnTimeoutAction::Rollback,
2022 status: ReconfigurationStatus::InProgress,
2023 };
2024
2025 assert!(matches!(
2026 reconfiguration_wait_result(Some(&record), &awaited, true),
2027 Some(Ok(()))
2028 ));
2029 }
2030
2031 #[mz_ore::test]
2032 fn foreground_wait_follows_matching_target() {
2033 let awaited = target("200cc", 1, &[], false);
2034 let mut record = ReconfigurationState {
2035 target: awaited.clone(),
2036 deadline: Timestamp::from(0),
2037 on_timeout: OnTimeoutAction::Rollback,
2038 status: ReconfigurationStatus::InProgress,
2039 };
2040
2041 assert!(reconfiguration_wait_result(Some(&record), &awaited, false).is_none());
2042
2043 record.status = ReconfigurationStatus::ResourceExhausted;
2044 assert!(matches!(
2045 reconfiguration_wait_result(Some(&record), &awaited, false),
2046 Some(Err(AdapterError::AlterClusterResourceExhausted))
2047 ));
2048
2049 record.status = ReconfigurationStatus::TimedOut;
2050 assert!(matches!(
2051 reconfiguration_wait_result(Some(&record), &awaited, false),
2052 Some(Err(AdapterError::AlterClusterTimeout))
2053 ));
2054 }
2055
2056 #[mz_ore::test]
2057 fn foreground_wait_reports_superseded_target() {
2058 let awaited = target("200cc", 1, &[], false);
2059 let record = ReconfigurationState {
2060 target: target("300cc", 1, &[], false),
2061 deadline: Timestamp::from(0),
2062 on_timeout: OnTimeoutAction::Rollback,
2063 status: ReconfigurationStatus::InProgress,
2064 };
2065
2066 assert!(matches!(
2067 reconfiguration_wait_result(Some(&record), &awaited, false),
2068 Some(Err(AdapterError::AlterClusterSuperseded))
2069 ));
2070 assert!(matches!(
2071 reconfiguration_wait_result(None, &awaited, false),
2072 Some(Err(AdapterError::AlterClusterSuperseded))
2073 ));
2074 }
2075
2076 #[mz_ore::test]
2077 fn fold_with_no_record_takes_new_target() {
2078 let new = target("200cc", 3, &["az1"], true);
2080 let folded = fold_reconfiguration_target(None, new.clone(), all_changed());
2081 assert_eq!(folded, new);
2082 }
2083
2084 #[mz_ore::test]
2085 fn fold_rf_only_keeps_in_flight_shape() {
2086 let in_flight = target("200cc", 1, &["az2"], true);
2090 let new = target("100cc", 5, &["az1"], false);
2093 let unchanged = ReconfigurationDimensionsUnchanged {
2094 size: true,
2095 replication_factor: false,
2096 availability_zones: true,
2097 log_logging: true,
2098 interval: true,
2099 arrangement_compression: true,
2100 };
2101 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2102 assert_eq!(folded, target("200cc", 5, &["az2"], true));
2104 }
2105
2106 #[mz_ore::test]
2107 fn fold_with_all_set_overwrites_every_dimension() {
2108 let in_flight = target("200cc", 1, &["az2"], true);
2110 let new = target("400cc", 9, &["az9"], false);
2111 let folded = fold_reconfiguration_target(Some(&in_flight), new.clone(), all_changed());
2112 assert_eq!(folded, new);
2113 }
2114
2115 #[mz_ore::test]
2116 fn fold_all_unchanged_is_alter_back_to_in_flight() {
2117 let in_flight = target("200cc", 2, &["az2"], true);
2122 let realized_shaped = target("100cc", 1, &["az1"], false);
2123 let folded =
2124 fold_reconfiguration_target(Some(&in_flight), realized_shaped, all_unchanged());
2125 assert_eq!(folded, in_flight);
2126 }
2127
2128 #[mz_ore::test]
2129 fn fold_logging_subdimensions_fold_independently() {
2130 let mut in_flight = target("100cc", 1, &["az1"], false);
2135 in_flight.logging.interval = Some(Duration::from_secs(5));
2136 let new = target("100cc", 1, &["az1"], true);
2137 let unchanged = ReconfigurationDimensionsUnchanged {
2138 size: true,
2139 replication_factor: true,
2140 availability_zones: true,
2141 log_logging: false,
2142 interval: true,
2143 arrangement_compression: true,
2144 };
2145 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2146 assert_eq!(
2147 folded.logging,
2148 ReplicaLogging {
2149 log_logging: true,
2150 interval: Some(Duration::from_secs(5)),
2151 }
2152 );
2153 }
2154}