1use std::collections::{BTreeMap, BTreeSet};
11use std::time::{Duration, Instant};
12
13use itertools::Itertools;
14use maplit::btreeset;
15use mz_adapter_types::cluster_state::ReconfigurationAudit;
16use mz_catalog::builtin::BUILTINS;
17use mz_catalog::durable::managed_cluster_replica_name;
18use mz_catalog::memory::objects::{
19 ClusterConfig, ClusterReplica, ClusterVariant, ClusterVariantManaged,
20 ManagedReplicaConfigShape, ReconfigurationState, ReconfigurationStatus, ReconfigurationTarget,
21};
22use mz_compute_types::config::ComputeReplicaConfig;
23use mz_controller::clusters::{
24 ClusterStatus, 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::ast::{Ident, QualifiedReplica};
34use mz_sql::catalog::{CatalogCluster, ObjectType};
35use mz_sql::plan::{
36 self, AlterClusterPlanStrategy, AlterClusterRenamePlan, AlterClusterReplicaRenamePlan,
37 AlterClusterSwapPlan, AlterOptionParameter, AlterSetClusterPlan,
38 ComputeReplicaIntrospectionConfig, CreateClusterManagedPlan, CreateClusterPlan,
39 CreateClusterReplicaPlan, CreateClusterUnmanagedPlan, CreateClusterVariant, 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 tracing::{Instrument, Span, debug};
47
48use mz_adapter_types::dyncfgs::{
49 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT, ENABLE_BACKGROUND_ALTER_CLUSTER,
50 ENABLE_CLUSTER_CONTROLLER,
51};
52
53use super::return_if_err;
54use crate::AdapterError::AlterClusterWhilePendingReplicas;
55use crate::catalog::{self, Op, ReplicaCreateDropReason};
56use crate::config::{
57 ClusterEvalContext, ClusterScopeContext, ReplicaEvalContext, ReplicaScopeContext,
58};
59use crate::coord::{
60 AlterCluster, AlterClusterAwaitReconfiguration, AlterClusterFinalize,
61 AlterClusterWaitForHydrated, ClusterReplicaStatuses, ClusterStage, Coordinator, Message,
62 PlanValidity, StageResult, Staged,
63};
64use crate::{AdapterError, ExecuteContext, ExecuteResponse, session::Session};
65
66const PENDING_REPLICA_SUFFIX: &str = "-pending";
67
68impl Staged for ClusterStage {
69 type Ctx = ExecuteContext;
70
71 fn validity(&mut self) -> &mut PlanValidity {
72 match self {
73 Self::Alter(stage) => &mut stage.validity,
74 Self::WaitForHydrated(stage) => &mut stage.validity,
75 Self::Finalize(stage) => &mut stage.validity,
76 Self::AwaitReconfiguration(stage) => &mut stage.validity,
77 }
78 }
79
80 async fn stage(
81 self,
82 coord: &mut Coordinator,
83 ctx: &mut ExecuteContext,
84 ) -> Result<StageResult<Box<Self>>, crate::AdapterError> {
85 match self {
86 Self::Alter(stage) => {
87 coord
88 .sequence_alter_cluster_stage(ctx.session(), stage.plan.clone(), stage.validity)
89 .await
90 }
91 Self::WaitForHydrated(stage) => {
92 let AlterClusterWaitForHydrated {
93 validity,
94 plan,
95 new_config,
96 workload_class,
97 timeout_time,
98 on_timeout,
99 } = stage;
100 coord
101 .check_if_pending_replicas_hydrated_stage(
102 ctx.session(),
103 plan,
104 new_config,
105 workload_class,
106 timeout_time,
107 on_timeout,
108 validity,
109 )
110 .await
111 }
112 Self::Finalize(stage) => {
113 coord
114 .finalize_alter_cluster_stage(
115 ctx.session(),
116 stage.plan.clone(),
117 stage.new_config.clone(),
118 stage.workload_class.clone(),
119 )
120 .await
121 }
122 Self::AwaitReconfiguration(stage) => {
123 coord.await_reconfiguration_stage(stage.validity, stage.cluster_id, stage.target)
124 }
125 }
126 }
127
128 fn message(self, ctx: ExecuteContext, span: tracing::Span) -> Message {
129 Message::ClusterStageReady {
130 ctx,
131 span,
132 stage: self,
133 }
134 }
135
136 fn cancel_enabled(&self) -> bool {
137 true
138 }
139}
140
141impl Coordinator {
142 #[instrument]
143 pub(crate) async fn sequence_alter_cluster_staged(
144 &mut self,
145 ctx: ExecuteContext,
146 plan: plan::AlterClusterPlan,
147 ) {
148 let stage = return_if_err!(self.alter_cluster_validate(ctx.session(), plan).await, ctx);
149 self.sequence_staged(ctx, Span::current(), stage).await;
150 }
151
152 #[instrument]
153 async fn alter_cluster_validate(
154 &self,
155 session: &Session,
156 plan: plan::AlterClusterPlan,
157 ) -> Result<ClusterStage, AdapterError> {
158 let validity = PlanValidity::new(
159 self.catalog(),
160 BTreeSet::new(),
161 Some(plan.id.clone()),
162 None,
163 session.role_metadata().clone(),
164 );
165 Ok(ClusterStage::Alter(AlterCluster { validity, plan }))
166 }
167
168 async fn sequence_alter_cluster_stage(
169 &mut self,
170 session: &Session,
171 plan: plan::AlterClusterPlan,
172 validity: PlanValidity,
173 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
174 let AlterClusterPlan {
175 id: cluster_id,
176 name: _,
177 ref options,
178 ref strategy,
179 } = plan;
180
181 use mz_catalog::memory::objects::ClusterVariant::*;
182 use mz_sql::plan::AlterOptionParameter::*;
183 let cluster = self.catalog.get_cluster(cluster_id);
184 let config = cluster.config.clone();
185 let mut new_config = config.clone();
186
187 match (&new_config.variant, &options.managed) {
188 (Managed(_), Reset) | (Managed(_), Unchanged) | (Managed(_), Set(true)) => {}
189 (Managed(_), Set(false)) => new_config.variant = Unmanaged,
190 (Unmanaged, Unchanged) | (Unmanaged, Set(false)) => {}
191 (Unmanaged, Reset) | (Unmanaged, Set(true)) => {
192 let size = "".to_string();
196 let logging = ReplicaLogging {
197 log_logging: false,
198 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
199 };
200 new_config.variant = Managed(ClusterVariantManaged {
201 size,
202 availability_zones: Default::default(),
203 logging,
204 arrangement_compression: false,
205 replication_factor: 1,
206 optimizer_feature_overrides: Default::default(),
207 schedule: Default::default(),
208 auto_scaling_strategy: None,
209 reconfiguration: None,
210 burst: None,
211 });
212 }
213 }
214
215 match &mut new_config.variant {
216 Managed(ClusterVariantManaged {
217 size,
218 availability_zones,
219 logging,
220 arrangement_compression,
221 replication_factor,
222 optimizer_feature_overrides: _,
223 schedule,
224 auto_scaling_strategy,
225 reconfiguration: _,
226 burst: _,
227 }) => {
228 match &options.size {
229 Set(s) => size.clone_from(s),
230 Reset => coord_bail!("SIZE has no default value"),
231 Unchanged => {}
232 }
233 match &options.availability_zones {
234 Set(az) => availability_zones.clone_from(az),
235 Reset => *availability_zones = Default::default(),
236 Unchanged => {}
237 }
238 match &options.introspection_debugging {
239 Set(id) => logging.log_logging = *id,
240 Reset => logging.log_logging = false,
241 Unchanged => {}
242 }
243 match &options.introspection_interval {
244 Set(ii) => logging.interval = ii.0,
245 Reset => logging.interval = Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
246 Unchanged => {}
247 }
248 match &options.arrangement_compression {
249 Set(ac) => *arrangement_compression = *ac,
250 Reset => *arrangement_compression = false,
251 Unchanged => {}
252 }
253 match &options.replication_factor {
254 Set(rf) => *replication_factor = *rf,
255 Reset => {
256 *replication_factor = self
257 .catalog
258 .system_config()
259 .default_cluster_replication_factor()
260 }
261 Unchanged => {}
262 }
263 match &options.schedule {
264 Set(new_schedule) => {
265 *schedule = new_schedule.clone();
266 }
267 Reset => *schedule = Default::default(),
268 Unchanged => {}
269 }
270 match &options.auto_scaling_strategy {
271 Set(new_strategy) => auto_scaling_strategy.clone_from(new_strategy),
272 Reset => *auto_scaling_strategy = None,
274 Unchanged => {}
275 }
276 if !matches!(options.replicas, Unchanged) {
277 coord_bail!("Cannot change REPLICAS of managed clusters");
278 }
279 }
280 Unmanaged => {
281 if !matches!(options.size, Unchanged) {
282 coord_bail!("Cannot change SIZE of unmanaged clusters");
283 }
284 if !matches!(options.availability_zones, Unchanged) {
285 coord_bail!("Cannot change AVAILABILITY ZONES of unmanaged clusters");
286 }
287 if !matches!(options.introspection_debugging, Unchanged) {
288 coord_bail!("Cannot change INTROSPECTION DEGUBBING of unmanaged clusters");
289 }
290 if !matches!(options.introspection_interval, Unchanged) {
291 coord_bail!("Cannot change INTROSPECTION INTERVAL of unmanaged clusters");
292 }
293 if !matches!(options.arrangement_compression, Unchanged) {
294 coord_bail!(
295 "Cannot change EXPERIMENTAL ARRANGEMENT COMPRESSION of unmanaged clusters"
296 );
297 }
298 if !matches!(options.replication_factor, Unchanged) {
299 coord_bail!("Cannot change REPLICATION FACTOR of unmanaged clusters");
300 }
301 if !matches!(options.auto_scaling_strategy, Unchanged) {
302 coord_bail!("Cannot change AUTO SCALING STRATEGY of unmanaged clusters");
303 }
304 }
305 }
306
307 match &options.workload_class {
308 Set(wc) => new_config.workload_class.clone_from(wc),
309 Reset => new_config.workload_class = None,
310 Unchanged => {}
311 }
312
313 let cluster_controller_owns = ENABLE_CLUSTER_CONTROLLER
320 .get(self.catalog().system_config().dyncfgs())
321 && cluster_id.is_user();
322 let reconfiguration_in_flight = matches!(
323 &config.variant,
324 Managed(managed) if managed
325 .reconfiguration
326 .as_ref()
327 .is_some_and(|record| record.is_in_progress())
328 );
329
330 if cluster_controller_owns
336 && reconfiguration_in_flight
337 && !matches!(options.schedule, Unchanged)
338 {
339 return Err(AdapterError::AlterClusterScheduleWhileReconfiguring);
340 }
341
342 if cluster_controller_owns
349 && reconfiguration_in_flight
350 && !matches!(options.replication_factor, Unchanged)
351 {
352 return Err(AdapterError::AlterClusterReplicationFactorWhileReconfiguring);
353 }
354
355 let cancels_or_retargets =
360 reconfiguration_in_flight && alter_changes_replica_shape(options);
361 if new_config == config && !(cluster_controller_owns && cancels_or_retargets) {
362 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
363 ObjectType::Cluster,
364 )));
365 }
366
367 if cluster_controller_owns {
379 if let (Managed(old_managed), Managed(new_managed)) =
380 (&config.variant, &new_config.variant)
381 {
382 let needs_record = if reconfiguration_in_flight {
383 alter_changes_replica_shape(options)
384 } else {
385 new_managed.replica_config_shape() != old_managed.replica_config_shape()
386 };
387 let scheduled_direct =
401 !matches!(new_managed.schedule, mz_sql::plan::ClusterSchedule::Manual)
402 && !reconfiguration_in_flight;
403 if scheduled_direct && !matches!(strategy, AlterClusterPlanStrategy::None) {
410 return Err(AdapterError::AlterClusterWaitOnScheduledCluster);
411 }
412 if needs_record && !scheduled_direct {
413 return self
414 .reshape_alter_cluster_managed(
415 session,
416 cluster_id,
417 new_config.clone(),
418 options,
419 strategy,
420 validity,
421 )
422 .await;
423 }
424 }
425 }
426
427 match (&config.variant, &new_config.variant) {
428 (Managed(_), Managed(new_config_managed)) => {
429 let alter_followup = self
430 .sequence_alter_cluster_managed_to_managed(
431 Some(session),
432 cluster_id,
433 new_config.clone(),
434 ReplicaCreateDropReason::Manual,
435 strategy.clone(),
436 )
437 .await?;
438 if alter_followup == NeedsFinalization::Yes {
439 self.active_conns
442 .get_mut(session.conn_id())
443 .expect("There must be an active connection")
444 .pending_cluster_alters
445 .insert(cluster_id.clone());
446 let new_config_managed = new_config_managed.clone();
447 return match &strategy {
448 AlterClusterPlanStrategy::None => Err(AdapterError::Internal(
449 "AlterClusterPlanStrategy must not be None if NeedsFinalization is Yes"
450 .into(),
451 )),
452 AlterClusterPlanStrategy::For(duration) => {
453 let span = Span::current();
454 let plan = plan.clone();
455 let duration = duration.clone().to_owned();
456 let workload_class = new_config.workload_class.clone();
457 Ok(StageResult::Handle(mz_ore::task::spawn(
458 || "Finalize Alter Cluster",
459 async move {
460 tokio::time::sleep(duration).await;
461 let stage = ClusterStage::Finalize(AlterClusterFinalize {
462 validity,
463 plan,
464 new_config: new_config_managed,
465 workload_class,
466 });
467 Ok(Box::new(stage))
468 }
469 .instrument(span),
470 )))
471 }
472 AlterClusterPlanStrategy::UntilReady {
473 timeout,
474 on_timeout,
475 } => Ok(StageResult::Immediate(Box::new(
476 ClusterStage::WaitForHydrated(AlterClusterWaitForHydrated {
477 validity,
478 plan: plan.clone(),
479 new_config: new_config_managed.clone(),
480 workload_class: new_config.workload_class.clone(),
481 timeout_time: Instant::now() + timeout.to_owned(),
482 on_timeout: on_timeout.unwrap_or(OnTimeoutAction::Commit),
486 }),
487 ))),
488 };
489 }
490 }
491 (Unmanaged, Managed(new_managed)) => {
492 if !matches!(new_managed.schedule, mz_sql::plan::ClusterSchedule::Manual)
497 && !matches!(strategy, AlterClusterPlanStrategy::None)
498 {
499 return Err(AdapterError::AlterClusterWaitOnScheduledCluster);
500 }
501 self.sequence_alter_cluster_unmanaged_to_managed(
502 session,
503 cluster_id,
504 new_config,
505 options.to_owned(),
506 )
507 .await?;
508 }
509 (Managed(_), Unmanaged) => {
510 self.sequence_alter_cluster_managed_to_unmanaged(session, cluster_id, new_config)
511 .await?;
512 }
513 (Unmanaged, Unmanaged) => {
514 self.sequence_alter_cluster_unmanaged_to_unmanaged(
515 session,
516 cluster_id,
517 new_config,
518 options.replicas.clone(),
519 )
520 .await?;
521 }
522 }
523
524 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
525 ObjectType::Cluster,
526 )))
527 }
528
529 fn validate_reconfiguration_resource_limits(
531 &self,
532 cluster_id: ClusterId,
533 target: &ReconfigurationTarget,
534 ) -> Result<(), AdapterError> {
535 if !cluster_id.is_user() {
538 return Ok(());
539 }
540 let cluster = self.catalog().get_cluster(cluster_id);
541 let ClusterVariant::Managed(realized) = &cluster.config.variant else {
542 return Ok(());
543 };
544
545 if target.matches_realized_config(realized) {
551 return Ok(());
552 }
553
554 self.validate_resource_limit(
569 usize::cast_from(realized.replication_factor),
570 i64::from(target.replication_factor),
571 SystemVars::max_replicas_per_cluster,
572 "cluster replica",
573 MAX_REPLICAS_PER_CLUSTER.name(),
574 )?;
575
576 self.validate_reconfiguration_credit_peak(cluster_id, realized, target)?;
578
579 Ok(())
580 }
581
582 fn validate_reconfiguration_credit_peak(
590 &self,
591 cluster_id: ClusterId,
592 realized: &ClusterVariantManaged,
593 target: &ReconfigurationTarget,
594 ) -> Result<(), AdapterError> {
595 let shape_credit = |size: &str, replication_factor: u32| -> Numeric {
596 let per_replica = self
597 .catalog()
598 .cluster_replica_sizes()
599 .0
600 .get(size)
601 .map(|allocation| allocation.credits_per_hour)
602 .unwrap_or_else(Numeric::zero);
605 per_replica * Numeric::from(replication_factor)
606 };
607 let mut peak_credit = shape_credit(&target.size, target.replication_factor);
608 peak_credit += shape_credit(&realized.size, realized.replication_factor);
609 self.validate_resource_limit_numeric(
610 self.current_credit_consumption_rate(Some(cluster_id)),
611 peak_credit,
612 |system_vars| {
613 self.license_key
614 .max_credit_consumption_rate()
615 .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
616 },
617 "cluster replica",
618 MAX_CREDIT_CONSUMPTION_RATE.name(),
619 )?;
620
621 Ok(())
622 }
623
624 async fn reshape_alter_cluster_managed(
660 &mut self,
661 session: &Session,
662 cluster_id: ClusterId,
663 new_config: ClusterConfig,
664 options: &PlanClusterOption,
665 strategy: &AlterClusterPlanStrategy,
666 validity: PlanValidity,
667 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
668 use mz_sql::plan::AlterOptionParameter::Unchanged;
669
670 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
671 return Err(AdapterError::Internal(
672 "reshape_alter_cluster_managed requires a managed target config".into(),
673 ));
674 };
675
676 let cluster = self.catalog.get_cluster(cluster_id);
682 let in_flight = match &cluster.config.variant {
683 ClusterVariant::Managed(managed) => managed
684 .reconfiguration
685 .as_ref()
686 .filter(|record| record.is_in_progress())
687 .cloned(),
688 ClusterVariant::Unmanaged => None,
689 };
690 let new_target = ReconfigurationTarget {
691 size: new_managed.size.clone(),
692 replication_factor: new_managed.replication_factor,
693 availability_zones: new_managed.availability_zones.clone(),
694 logging: new_managed.logging.clone(),
695 arrangement_compression: new_managed.arrangement_compression,
696 };
697 let unchanged = ReconfigurationDimensionsUnchanged {
698 size: matches!(options.size, Unchanged),
699 replication_factor: matches!(options.replication_factor, Unchanged),
700 availability_zones: matches!(options.availability_zones, Unchanged),
701 log_logging: matches!(options.introspection_debugging, Unchanged),
704 interval: matches!(options.introspection_interval, Unchanged),
705 arrangement_compression: matches!(options.arrangement_compression, Unchanged),
706 };
707 let target = fold_reconfiguration_target(
708 in_flight.as_ref().map(|r| &r.target),
709 new_target,
710 unchanged,
711 );
712
713 let role_id = session.role_metadata().current_role;
716 self.catalog.ensure_valid_replica_size(
717 &self
718 .catalog()
719 .get_role_allowed_cluster_sizes(&Some(role_id)),
720 &target.size,
721 false,
722 )?;
723 self.ensure_valid_azs(target.availability_zones.iter())?;
724 self.validate_reconfiguration_resource_limits(cluster_id, &target)?;
728
729 let now = self.now();
756 let deadline_from = |timeout: Duration| -> Timestamp {
757 now.saturating_add(u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX))
758 .into()
759 };
760 let (deadline, on_timeout) = match strategy {
761 AlterClusterPlanStrategy::None => match &in_flight {
762 Some(record) => (record.deadline, record.on_timeout),
763 None => (
764 deadline_from(
765 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT
766 .get(self.catalog().system_config().dyncfgs()),
767 ),
768 OnTimeoutAction::Rollback,
769 ),
770 },
771 AlterClusterPlanStrategy::For(timeout) => {
772 (deadline_from(*timeout), OnTimeoutAction::Commit)
773 }
774 AlterClusterPlanStrategy::UntilReady {
775 timeout,
776 on_timeout,
777 } => (
778 deadline_from(*timeout),
779 on_timeout.unwrap_or(OnTimeoutAction::Rollback),
780 ),
781 };
782
783 let cluster = self.catalog.get_cluster(cluster_id);
792 let cluster_name = cluster.name().to_string();
793 let ClusterVariant::Managed(realized_now) = &cluster.config.variant else {
794 return Err(AdapterError::Internal(
795 "reshape_alter_cluster_managed requires a managed realized config".into(),
796 ));
797 };
798 let realized_size = realized_now.size.clone();
799 let realized_replication_factor = realized_now.replication_factor;
800 let realized_availability_zones = realized_now.availability_zones.clone();
801 let realized_logging = realized_now.logging.clone();
802 let (status, audit) = if target.matches_realized_config(realized_now) {
806 (
807 ReconfigurationStatus::Cancelled,
808 ReconfigurationAudit::Cancelled,
809 )
810 } else {
811 (
812 ReconfigurationStatus::InProgress,
813 ReconfigurationAudit::Started,
814 )
815 };
816 let record = ReconfigurationState {
817 target: target.clone(),
818 deadline,
819 on_timeout,
820 status,
821 };
822
823 let mut realized = new_config.clone();
824 let ClusterVariant::Managed(realized_managed) = &mut realized.variant else {
825 return Err(AdapterError::Internal(
826 "reshape_alter_cluster_managed requires a managed target config".into(),
827 ));
828 };
829 realized_managed.size = realized_size;
830 realized_managed.replication_factor = realized_replication_factor;
831 realized_managed.availability_zones = realized_availability_zones;
832 realized_managed.logging = realized_logging;
833 realized_managed.reconfiguration = Some(record);
834
835 self.catalog_transact(
836 Some(session),
837 vec![Op::UpdateClusterConfig {
838 id: cluster_id,
839 name: cluster_name,
840 config: realized,
841 reconfiguration_audit: Some(audit),
842 burst_audit: None,
843 }],
844 )
845 .await?;
846
847 let background =
848 ENABLE_BACKGROUND_ALTER_CLUSTER.get(self.catalog().system_config().dyncfgs());
849 if background {
850 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
851 ObjectType::Cluster,
852 )));
853 }
854
855 Ok(StageResult::Immediate(Box::new(
859 ClusterStage::AwaitReconfiguration(AlterClusterAwaitReconfiguration {
860 validity,
861 cluster_id,
862 target,
863 }),
864 )))
865 }
866
867 fn await_reconfiguration_stage(
873 &self,
874 validity: PlanValidity,
875 cluster_id: ClusterId,
876 target: ReconfigurationTarget,
877 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
878 let Some(cluster) = self.catalog().try_get_cluster(cluster_id) else {
879 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
882 ObjectType::Cluster,
883 )));
884 };
885 let record = match &cluster.config.variant {
886 ClusterVariant::Managed(managed) => managed.reconfiguration.clone(),
887 ClusterVariant::Unmanaged => None,
888 };
889
890 let realized_matches_target = match &cluster.config.variant {
891 ClusterVariant::Managed(managed) => target.matches_realized_config(managed),
892 ClusterVariant::Unmanaged => false,
893 };
894
895 match record {
896 None => {
897 if realized_matches_target {
900 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
901 ObjectType::Cluster,
902 )))
903 } else {
904 Err(AdapterError::AlterClusterTimeout)
905 }
906 }
907 Some(record) if !record.is_in_progress() => {
908 if matches!(
909 record.status,
910 ReconfigurationStatus::Finalized | ReconfigurationStatus::Cancelled
911 ) && realized_matches_target
912 {
913 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
914 ObjectType::Cluster,
915 )))
916 } else {
917 Err(AdapterError::AlterClusterTimeout)
918 }
919 }
920 Some(_) => {
921 let poll_duration = self
931 .catalog
932 .system_config()
933 .cluster_alter_check_ready_interval();
934 let span = Span::current();
935 Ok(StageResult::Handle(mz_ore::task::spawn(
936 || "Await Cluster Reconfiguration",
937 async move {
938 tokio::time::sleep(poll_duration).await;
939 Ok(Box::new(ClusterStage::AwaitReconfiguration(
940 AlterClusterAwaitReconfiguration {
941 validity,
942 cluster_id,
943 target,
944 },
945 )))
946 }
947 .instrument(span),
948 )))
949 }
950 }
951 }
952
953 async fn finalize_alter_cluster_stage(
954 &mut self,
955 session: &Session,
956 AlterClusterPlan {
957 id: cluster_id,
958 name: cluster_name,
959 ..
960 }: AlterClusterPlan,
961 new_config: ClusterVariantManaged,
962 workload_class: Option<String>,
963 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
964 let cluster = self.catalog.get_cluster(cluster_id);
965 let mut ops = vec![];
966
967 let remove_replicas = cluster
970 .replicas()
971 .filter_map(|r| {
972 if !r.config.location.pending() && !r.config.location.internal() {
973 Some(catalog::DropObjectInfo::ClusterReplica((
974 cluster_id.clone(),
975 r.replica_id,
976 ReplicaCreateDropReason::Manual,
977 )))
978 } else {
979 None
980 }
981 })
982 .collect();
983 ops.push(catalog::Op::DropObjects(remove_replicas));
984
985 let finalize_replicas: Vec<catalog::Op> = cluster
988 .replicas()
989 .filter_map(|r| {
990 if r.config.location.pending() {
991 let cluster_ident = match Ident::new(cluster.name.clone()) {
992 Ok(id) => id,
993 Err(err) => {
994 return Some(Err(AdapterError::internal(
995 "Unexpected error parsing cluster name",
996 err,
997 )));
998 }
999 };
1000 let replica_ident = match Ident::new(r.name.clone()) {
1001 Ok(id) => id,
1002 Err(err) => {
1003 return Some(Err(AdapterError::internal(
1004 "Unexpected error parsing replica name",
1005 err,
1006 )));
1007 }
1008 };
1009 Some(Ok((cluster_ident, replica_ident, r)))
1010 } else {
1011 None
1012 }
1013 })
1014 .collect::<Result<Vec<(Ident, Ident, &ClusterReplica)>, _>>()?
1017 .into_iter()
1018 .map(|(cluster_ident, replica_ident, replica)| {
1019 let mut new_replica_config = replica.config.clone();
1020 debug!("Promoting replica: {}", replica.name);
1021 match new_replica_config.location {
1022 mz_controller::clusters::ReplicaLocation::Managed(ManagedReplicaLocation {
1023 ref mut pending,
1024 ..
1025 }) => {
1026 *pending = false;
1027 }
1028 mz_controller::clusters::ReplicaLocation::Unmanaged(_) => {}
1029 }
1030
1031 let mut replica_ops = vec![];
1032 let to_name = replica.name.strip_suffix(PENDING_REPLICA_SUFFIX);
1033 if let Some(to_name) = to_name {
1034 replica_ops.push(catalog::Op::RenameClusterReplica {
1035 cluster_id: cluster_id.clone(),
1036 replica_id: replica.replica_id.to_owned(),
1037 name: QualifiedReplica {
1038 cluster: cluster_ident,
1039 replica: replica_ident,
1040 },
1041 to_name: to_name.to_owned(),
1042 });
1043 }
1044 replica_ops.push(catalog::Op::UpdateClusterReplicaConfig {
1045 cluster_id,
1046 replica_id: replica.replica_id.to_owned(),
1047 config: new_replica_config,
1048 });
1049 replica_ops
1050 })
1051 .flatten()
1052 .collect();
1053
1054 ops.extend(finalize_replicas);
1055
1056 let mut final_config = ClusterConfig {
1060 variant: ClusterVariant::Managed(new_config),
1061 workload_class: workload_class.clone(),
1062 };
1063 let reconfiguration_audit = cancel_carried_reconfiguration(&mut final_config);
1064 ops.push(Op::UpdateClusterConfig {
1065 id: cluster_id,
1066 name: cluster_name,
1067 config: final_config,
1068 reconfiguration_audit,
1069 burst_audit: None,
1070 });
1071 self.catalog_transact(Some(session), ops).await?;
1072 self.active_conns
1075 .get_mut(session.conn_id())
1076 .expect("There must be an active connection")
1077 .pending_cluster_alters
1078 .remove(&cluster_id);
1079
1080 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
1081 ObjectType::Cluster,
1082 )))
1083 }
1084
1085 async fn check_if_pending_replicas_hydrated_stage(
1086 &mut self,
1087 session: &Session,
1088 plan: AlterClusterPlan,
1089 new_config: ClusterVariantManaged,
1090 workload_class: Option<String>,
1091 timeout_time: Instant,
1092 on_timeout: OnTimeoutAction,
1093 validity: PlanValidity,
1094 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
1095 let cluster = self.catalog.get_cluster(plan.id);
1097 let pending_replicas = cluster
1098 .replicas()
1099 .filter_map(|r| {
1100 if r.config.location.pending() {
1101 Some(r.replica_id.clone())
1102 } else {
1103 None
1104 }
1105 })
1106 .collect_vec();
1107 if Instant::now() > timeout_time {
1109 match on_timeout {
1111 OnTimeoutAction::Rollback => {
1112 self.active_conns
1113 .get_mut(session.conn_id())
1114 .expect("There must be an active connection")
1115 .pending_cluster_alters
1116 .remove(&cluster.id);
1117 self.drop_reconfiguration_replicas(btreeset!(cluster.id))
1118 .await?;
1119 return Err(AdapterError::AlterClusterTimeout);
1120 }
1121 OnTimeoutAction::Commit => {
1122 let span = Span::current();
1123 let poll_duration = self
1124 .catalog
1125 .system_config()
1126 .cluster_alter_check_ready_interval()
1127 .clone();
1128 return Ok(StageResult::Handle(mz_ore::task::spawn(
1129 || "Finalize Alter Cluster",
1130 async move {
1131 tokio::time::sleep(poll_duration).await;
1132 let stage = ClusterStage::Finalize(AlterClusterFinalize {
1133 validity,
1134 plan,
1135 new_config,
1136 workload_class,
1137 });
1138 Ok(Box::new(stage))
1139 }
1140 .instrument(span),
1141 )));
1142 }
1143 }
1144 }
1145 let compute_hydrated_fut = self
1146 .controller
1147 .compute
1148 .collections_hydrated_for_replicas(cluster.id, pending_replicas.clone(), [].into())
1149 .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1150
1151 let storage_hydrated = self
1152 .controller
1153 .storage
1154 .collections_hydrated_on_replicas(
1155 Some(pending_replicas.clone()),
1156 &cluster.id,
1157 &[].into(),
1158 )
1159 .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1160
1161 let replicas_online = pending_replicas.iter().all(|replica_id| {
1164 let status = self
1165 .cluster_replica_statuses
1166 .try_get_cluster_replica_statuses(cluster.id, *replica_id)
1167 .map(ClusterReplicaStatuses::cluster_replica_status);
1168 matches!(status, Some(ClusterStatus::Online))
1169 });
1170
1171 let span = Span::current();
1172 Ok(StageResult::Handle(mz_ore::task::spawn(
1173 || "Alter Cluster: wait for hydrated",
1174 async move {
1175 let compute_hydrated = compute_hydrated_fut
1176 .await
1177 .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1178
1179 if compute_hydrated && storage_hydrated && replicas_online {
1180 Ok(Box::new(ClusterStage::Finalize(AlterClusterFinalize {
1182 validity,
1183 plan,
1184 new_config: new_config.clone(),
1185 workload_class: workload_class.clone(),
1186 })))
1187 } else {
1188 tokio::time::sleep(Duration::from_secs(1)).await;
1190 let stage = ClusterStage::WaitForHydrated(AlterClusterWaitForHydrated {
1191 validity,
1192 plan,
1193 new_config,
1194 workload_class,
1195 timeout_time,
1196 on_timeout,
1197 });
1198 Ok(Box::new(stage))
1199 }
1200 }
1201 .instrument(span),
1202 )))
1203 }
1204
1205 #[mz_ore::instrument(level = "debug")]
1206 pub(crate) async fn sequence_create_cluster(
1207 &mut self,
1208 session: &Session,
1209 CreateClusterPlan {
1210 name,
1211 variant,
1212 workload_class,
1213 }: CreateClusterPlan,
1214 ) -> Result<ExecuteResponse, AdapterError> {
1215 tracing::debug!("sequence_create_cluster");
1216
1217 let id_ts = self.get_catalog_write_ts().await;
1218 let id = self.catalog().allocate_user_cluster_id(id_ts).await?;
1219 let introspection_sources = BUILTINS::logs().collect();
1224 let cluster_variant = match &variant {
1225 CreateClusterVariant::Managed(plan) => {
1226 let logging = if let Some(config) = plan.compute.introspection {
1227 ReplicaLogging {
1228 log_logging: config.debugging,
1229 interval: Some(config.interval),
1230 }
1231 } else {
1232 ReplicaLogging::default()
1233 };
1234 ClusterVariant::Managed(ClusterVariantManaged {
1235 size: plan.size.clone(),
1236 availability_zones: plan.availability_zones.clone(),
1237 logging,
1238 arrangement_compression: plan.compute.arrangement_compression,
1239 replication_factor: plan.replication_factor,
1240 optimizer_feature_overrides: plan.optimizer_feature_overrides.clone(),
1241 schedule: plan.schedule.clone(),
1242 auto_scaling_strategy: plan.auto_scaling_strategy.clone(),
1243 reconfiguration: None,
1244 burst: None,
1245 })
1246 }
1247 CreateClusterVariant::Unmanaged(_) => ClusterVariant::Unmanaged,
1248 };
1249 let config = ClusterConfig {
1250 variant: cluster_variant,
1251 workload_class,
1252 };
1253 let ops = vec![catalog::Op::CreateCluster {
1254 id,
1255 name: name.clone(),
1256 introspection_sources,
1257 owner_id: *session.current_role_id(),
1258 config,
1259 }];
1260
1261 match variant {
1262 CreateClusterVariant::Managed(plan) => {
1263 self.sequence_create_managed_cluster(session, plan, id, name, ops)
1264 .await
1265 }
1266 CreateClusterVariant::Unmanaged(plan) => {
1267 self.sequence_create_unmanaged_cluster(session, plan, id, name, ops)
1268 .await
1269 }
1270 }
1271 }
1272
1273 #[mz_ore::instrument(level = "debug")]
1274 async fn sequence_create_managed_cluster(
1275 &mut self,
1276 session: &Session,
1277 CreateClusterManagedPlan {
1278 availability_zones,
1279 compute,
1280 replication_factor,
1281 size,
1282 optimizer_feature_overrides: _,
1283 schedule: _,
1284 auto_scaling_strategy,
1285 }: CreateClusterManagedPlan,
1286 cluster_id: ClusterId,
1287 cluster_name: String,
1288 mut ops: Vec<catalog::Op>,
1289 ) -> Result<ExecuteResponse, AdapterError> {
1290 tracing::debug!("sequence_create_managed_cluster");
1291
1292 self.ensure_valid_azs(availability_zones.iter())?;
1293
1294 let role_id = session.role_metadata().current_role;
1295 self.catalog.ensure_valid_replica_size(
1296 &self
1297 .catalog()
1298 .get_role_allowed_cluster_sizes(&Some(role_id)),
1299 &size,
1300 false,
1301 )?;
1302 if let Some(on_hydration) = auto_scaling_strategy
1308 .as_ref()
1309 .and_then(|strategy| strategy.on_hydration.as_ref())
1310 {
1311 self.catalog.ensure_valid_replica_size(
1312 &self
1313 .catalog()
1314 .get_role_allowed_cluster_sizes(&Some(role_id)),
1315 &on_hydration.hydration_size,
1316 false,
1317 )?;
1318 }
1319
1320 if cluster_id.is_user() {
1325 self.validate_resource_limit(
1326 0,
1327 i64::from(replication_factor),
1328 SystemVars::max_replicas_per_cluster,
1329 "cluster replica",
1330 MAX_REPLICAS_PER_CLUSTER.name(),
1331 )?;
1332 }
1333
1334 let id_ts = self.get_catalog_write_ts().await;
1340 let replica_ids = self
1341 .catalog()
1342 .allocate_replica_ids(cluster_id, u64::from(replication_factor), id_ts)
1343 .await?;
1344
1345 let cluster_ctx = ClusterScopeContext {
1346 id: cluster_id.to_string(),
1347 name: cluster_name.clone(),
1348 is_builtin: cluster_id.is_system(),
1349 };
1350
1351 let mut replica_ctxs = Vec::new();
1352 for (replica_id, replica_name) in replica_ids
1353 .into_iter()
1354 .zip_eq((0..replication_factor).map(managed_cluster_replica_name))
1355 {
1356 let size_family = self.create_managed_cluster_replica_op(
1357 cluster_id,
1358 replica_id,
1359 replica_name.clone(),
1360 &compute,
1361 &size,
1362 &mut ops,
1363 if availability_zones.is_empty() {
1364 None
1365 } else {
1366 Some(availability_zones.as_ref())
1367 },
1368 false,
1369 *session.current_role_id(),
1370 ReplicaCreateDropReason::Manual,
1371 )?;
1372 replica_ctxs.push(ReplicaEvalContext {
1373 cluster_id,
1374 replica_id,
1375 cluster: cluster_ctx.clone(),
1376 replica: ReplicaScopeContext {
1377 id: replica_id.to_string(),
1378 name: replica_name,
1379 is_builtin: cluster_id.is_system(),
1380 size: size.clone(),
1381 size_family,
1382 cluster_id: cluster_id.to_string(),
1383 cluster_name: cluster_name.clone(),
1384 },
1385 });
1386 }
1387
1388 let cluster_eval = ClusterEvalContext {
1395 cluster_id,
1396 cluster: cluster_ctx,
1397 };
1398 if let Some(scoped_op) = self.scoped_overrides_create_op(&[cluster_eval], &replica_ctxs) {
1399 ops.push(scoped_op);
1400 }
1401
1402 self.catalog_transact(Some(session), ops).await?;
1403
1404 Ok(ExecuteResponse::CreatedCluster)
1405 }
1406
1407 fn create_managed_cluster_replica_op(
1408 &self,
1409 cluster_id: ClusterId,
1410 replica_id: ReplicaId,
1411 name: String,
1412 compute: &mz_sql::plan::ComputeReplicaConfig,
1413 size: &String,
1414 ops: &mut Vec<Op>,
1415 azs: Option<&[String]>,
1416 pending: bool,
1417 owner_id: RoleId,
1418 reason: ReplicaCreateDropReason,
1419 ) -> Result<String, AdapterError> {
1420 let location = mz_catalog::durable::ReplicaLocation::Managed {
1421 availability_zones: Vec::new(),
1424 billed_as: None,
1425 internal: false,
1426 size: size.clone(),
1427 pending,
1428 };
1429
1430 let logging = if let Some(config) = compute.introspection {
1431 ReplicaLogging {
1432 log_logging: config.debugging,
1433 interval: Some(config.interval),
1434 }
1435 } else {
1436 ReplicaLogging::default()
1437 };
1438
1439 let config = ReplicaConfig {
1440 location: self.catalog().concretize_replica_location(
1441 location,
1442 &self
1443 .catalog()
1444 .get_role_allowed_cluster_sizes(&Some(owner_id)),
1445 azs,
1446 false,
1447 )?,
1448 compute: ComputeReplicaConfig {
1449 logging,
1450 arrangement_compression: compute.arrangement_compression,
1451 },
1452 };
1453
1454 let size_family = match &config.location {
1460 ReplicaLocation::Managed(location) => location.allocation.family().to_string(),
1461 ReplicaLocation::Unmanaged(_) => {
1463 unreachable!("managed cluster replica has a managed location")
1464 }
1465 };
1466
1467 ops.push(catalog::Op::CreateClusterReplica {
1468 cluster_id,
1469 replica_id,
1470 name,
1471 config,
1472 owner_id,
1473 reason,
1474 });
1475 Ok(size_family)
1476 }
1477
1478 fn ensure_valid_azs<'a, I: IntoIterator<Item = &'a String>>(
1479 &self,
1480 azs: I,
1481 ) -> Result<(), AdapterError> {
1482 let cat_azs = self.catalog().state().availability_zones();
1483 for az in azs.into_iter() {
1484 if !cat_azs.contains(az) {
1485 return Err(AdapterError::InvalidClusterReplicaAz {
1486 az: az.to_string(),
1487 expected: cat_azs.to_vec(),
1488 });
1489 }
1490 }
1491 Ok(())
1492 }
1493
1494 #[mz_ore::instrument(level = "debug")]
1495 async fn sequence_create_unmanaged_cluster(
1496 &mut self,
1497 session: &Session,
1498 CreateClusterUnmanagedPlan { replicas }: CreateClusterUnmanagedPlan,
1499 id: ClusterId,
1500 cluster_name: String,
1501 mut ops: Vec<catalog::Op>,
1502 ) -> Result<ExecuteResponse, AdapterError> {
1503 tracing::debug!("sequence_create_unmanaged_cluster");
1504
1505 self.ensure_valid_azs(replicas.iter().filter_map(|(_, r)| {
1506 if let mz_sql::plan::ReplicaConfig::Orchestrated {
1507 availability_zone: Some(az),
1508 ..
1509 } = &r
1510 {
1511 Some(az)
1512 } else {
1513 None
1514 }
1515 }))?;
1516
1517 if id.is_user() {
1522 self.validate_resource_limit(
1523 0,
1524 i64::try_from(replicas.len()).unwrap_or(i64::MAX),
1525 SystemVars::max_replicas_per_cluster,
1526 "cluster replica",
1527 MAX_REPLICAS_PER_CLUSTER.name(),
1528 )?;
1529 }
1530
1531 let id_ts = self.get_catalog_write_ts().await;
1537 let replica_ids = self
1538 .catalog()
1539 .allocate_replica_ids(id, u64::cast_from(replicas.len()), id_ts)
1540 .await?;
1541
1542 let cluster_ctx = ClusterScopeContext {
1543 id: id.to_string(),
1544 name: cluster_name.clone(),
1545 is_builtin: id.is_system(),
1546 };
1547 let mut replica_ctxs = Vec::new();
1548
1549 for (replica_id, (replica_name, replica_config)) in replica_ids.into_iter().zip_eq(replicas)
1550 {
1551 let (compute, location) = match replica_config {
1554 mz_sql::plan::ReplicaConfig::Unorchestrated {
1555 storagectl_addrs,
1556 computectl_addrs,
1557 compute,
1558 } => {
1559 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1560 storagectl_addrs,
1561 computectl_addrs,
1562 };
1563 (compute, location)
1564 }
1565 mz_sql::plan::ReplicaConfig::Orchestrated {
1566 availability_zone,
1567 billed_as,
1568 compute,
1569 internal,
1570 size,
1571 } => {
1572 if !session.user().is_internal() && (internal || billed_as.is_some()) {
1574 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1575 }
1576 if billed_as.is_some() && !internal {
1578 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1579 }
1580
1581 let location = mz_catalog::durable::ReplicaLocation::Managed {
1582 availability_zones: availability_zone.into_iter().collect(),
1585 billed_as,
1586 internal,
1587 size: size.clone(),
1588 pending: false,
1589 };
1590 (compute, location)
1591 }
1592 };
1593
1594 let logging = if let Some(config) = compute.introspection {
1595 ReplicaLogging {
1596 log_logging: config.debugging,
1597 interval: Some(config.interval),
1598 }
1599 } else {
1600 ReplicaLogging::default()
1601 };
1602
1603 let role_id = session.role_metadata().current_role;
1604 let config = ReplicaConfig {
1605 location: self.catalog().concretize_replica_location(
1606 location,
1607 &self
1608 .catalog()
1609 .get_role_allowed_cluster_sizes(&Some(role_id)),
1610 None,
1611 false,
1612 )?,
1613 compute: ComputeReplicaConfig {
1614 logging,
1615 arrangement_compression: compute.arrangement_compression,
1616 },
1617 };
1618
1619 if let ReplicaLocation::Managed(location) = &config.location {
1622 replica_ctxs.push(ReplicaEvalContext {
1623 cluster_id: id,
1624 replica_id,
1625 cluster: cluster_ctx.clone(),
1626 replica: ReplicaScopeContext {
1627 id: replica_id.to_string(),
1628 name: replica_name.clone(),
1629 is_builtin: id.is_system(),
1630 size: location.size.clone(),
1631 size_family: location.allocation.family().to_string(),
1632 cluster_id: id.to_string(),
1633 cluster_name: cluster_name.clone(),
1634 },
1635 });
1636 }
1637
1638 ops.push(catalog::Op::CreateClusterReplica {
1639 cluster_id: id,
1640 replica_id,
1641 name: replica_name.clone(),
1642 config,
1643 owner_id: *session.current_role_id(),
1644 reason: ReplicaCreateDropReason::Manual,
1645 });
1646 }
1647
1648 let cluster_eval = ClusterEvalContext {
1651 cluster_id: id,
1652 cluster: cluster_ctx,
1653 };
1654 if let Some(scoped_op) = self.scoped_overrides_create_op(&[cluster_eval], &replica_ctxs) {
1655 ops.push(scoped_op);
1656 }
1657
1658 self.catalog_transact(Some(session), ops).await?;
1659
1660 Ok(ExecuteResponse::CreatedCluster)
1661 }
1662
1663 #[mz_ore::instrument(level = "debug")]
1664 pub(crate) async fn sequence_create_cluster_replica(
1665 &mut self,
1666 session: &Session,
1667 CreateClusterReplicaPlan {
1668 name,
1669 cluster_id,
1670 config,
1671 }: CreateClusterReplicaPlan,
1672 ) -> Result<ExecuteResponse, AdapterError> {
1673 let (compute, location) = match config {
1675 mz_sql::plan::ReplicaConfig::Unorchestrated {
1676 storagectl_addrs,
1677 computectl_addrs,
1678 compute,
1679 } => {
1680 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1681 storagectl_addrs,
1682 computectl_addrs,
1683 };
1684 (compute, location)
1685 }
1686 mz_sql::plan::ReplicaConfig::Orchestrated {
1687 availability_zone,
1688 billed_as,
1689 compute,
1690 internal,
1691 size,
1692 } => {
1693 let availability_zone = match availability_zone {
1694 Some(az) => {
1695 self.ensure_valid_azs([&az])?;
1696 Some(az)
1697 }
1698 None => None,
1699 };
1700 let location = mz_catalog::durable::ReplicaLocation::Managed {
1701 availability_zones: availability_zone.into_iter().collect(),
1704 billed_as,
1705 internal,
1706 size,
1707 pending: false,
1708 };
1709 (compute, location)
1710 }
1711 };
1712
1713 let logging = if let Some(config) = compute.introspection {
1714 ReplicaLogging {
1715 log_logging: config.debugging,
1716 interval: Some(config.interval),
1717 }
1718 } else {
1719 ReplicaLogging::default()
1720 };
1721
1722 let role_id = session.role_metadata().current_role;
1723 let config = ReplicaConfig {
1724 location: self.catalog().concretize_replica_location(
1725 location,
1726 &self
1727 .catalog()
1728 .get_role_allowed_cluster_sizes(&Some(role_id)),
1729 None,
1732 false,
1733 )?,
1734 compute: ComputeReplicaConfig {
1735 logging,
1736 arrangement_compression: compute.arrangement_compression,
1737 },
1738 };
1739
1740 let cluster = self.catalog().get_cluster(cluster_id);
1741
1742 if let ReplicaLocation::Managed(ManagedReplicaLocation {
1743 internal,
1744 billed_as,
1745 ..
1746 }) = &config.location
1747 {
1748 if !session.user().is_internal() && (*internal || billed_as.is_some()) {
1750 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1751 }
1752 if cluster.is_managed() && !*internal {
1754 coord_bail!("must specify INTERNAL when creating a replica in a managed cluster");
1755 }
1756 if billed_as.is_some() && !*internal {
1758 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1759 }
1760 }
1761
1762 let owner_id = cluster.owner_id();
1765
1766 let cluster_name = cluster.name.clone();
1767 let is_builtin = cluster_id.is_system();
1768
1769 let id_ts = self.get_catalog_write_ts().await;
1777 let replica_id = self
1778 .catalog()
1779 .allocate_replica_ids(cluster_id, 1, id_ts)
1780 .await?
1781 .into_element();
1782
1783 let replica_ctx = match &config.location {
1786 ReplicaLocation::Managed(location) => Some(ReplicaEvalContext {
1787 cluster_id,
1788 replica_id,
1789 cluster: ClusterScopeContext {
1790 id: cluster_id.to_string(),
1791 name: cluster_name.clone(),
1792 is_builtin,
1793 },
1794 replica: ReplicaScopeContext {
1795 id: replica_id.to_string(),
1796 name: name.to_string(),
1797 is_builtin,
1798 size: location.size.clone(),
1799 size_family: location.allocation.family().to_string(),
1800 cluster_id: cluster_id.to_string(),
1801 cluster_name,
1802 },
1803 }),
1804 ReplicaLocation::Unmanaged(_) => None,
1805 };
1806
1807 let mut ops = vec![catalog::Op::CreateClusterReplica {
1808 cluster_id,
1809 replica_id,
1810 name: name.clone(),
1811 config,
1812 owner_id,
1813 reason: ReplicaCreateDropReason::Manual,
1814 }];
1815
1816 if let Some(replica_ctx) = replica_ctx {
1820 if let Some(scoped_op) = self.scoped_overrides_create_op(&[], &[replica_ctx]) {
1821 ops.push(scoped_op);
1822 }
1823 }
1824
1825 self.catalog_transact(Some(session), ops).await?;
1826
1827 Ok(ExecuteResponse::CreatedClusterReplica)
1828 }
1829
1830 pub(crate) async fn sequence_alter_cluster_managed_to_managed(
1839 &mut self,
1840 session: Option<&Session>,
1841 cluster_id: ClusterId,
1842 new_config: ClusterConfig,
1843 reason: ReplicaCreateDropReason,
1844 strategy: AlterClusterPlanStrategy,
1845 ) -> Result<NeedsFinalization, AdapterError> {
1846 let cluster = self.catalog.get_cluster(cluster_id);
1847 let name = cluster.name().to_string();
1848 let owner_id = cluster.owner_id();
1849
1850 let mut ops = vec![];
1851 let mut finalization_needed = NeedsFinalization::No;
1852
1853 let ClusterVariant::Managed(ClusterVariantManaged {
1854 size,
1855 availability_zones,
1856 logging,
1857 arrangement_compression,
1858 replication_factor,
1859 optimizer_feature_overrides: _,
1860 schedule: _,
1861 auto_scaling_strategy,
1862 reconfiguration,
1863 burst: _,
1864 }) = &cluster.config.variant
1865 else {
1866 panic!("expected existing managed cluster config");
1867 };
1868 let size = size.clone();
1872 let availability_zones = availability_zones.clone();
1873 let logging = logging.clone();
1874 let arrangement_compression = *arrangement_compression;
1875 let replication_factor = *replication_factor;
1876 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
1877 panic!("expected new managed cluster config");
1878 };
1879 let ClusterVariantManaged {
1880 size: new_size,
1881 replication_factor: new_replication_factor,
1882 availability_zones: new_availability_zones,
1883 logging: new_logging,
1884 arrangement_compression: new_arrangement_compression,
1885 optimizer_feature_overrides: _,
1886 schedule: _,
1887 auto_scaling_strategy: new_auto_scaling_strategy,
1888 reconfiguration: _,
1889 burst: _,
1890 } = new_managed;
1891
1892 let role_id = session.map(|s| s.role_metadata().current_role);
1893 self.catalog.ensure_valid_replica_size(
1894 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
1895 new_size,
1896 false,
1897 )?;
1898 if new_auto_scaling_strategy != auto_scaling_strategy {
1904 if let Some(on_hydration) = new_auto_scaling_strategy
1905 .as_ref()
1906 .and_then(|strategy| strategy.on_hydration.as_ref())
1907 {
1908 self.catalog.ensure_valid_replica_size(
1909 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
1910 &on_hydration.hydration_size,
1911 false,
1912 )?;
1913 if reconfiguration.as_ref().is_some_and(|record| {
1920 record.is_in_progress() && record.target.size == on_hydration.hydration_size
1921 }) {
1922 coord_bail!(
1923 "HYDRATION SIZE must differ from the target SIZE \
1924 ('{}') of the in-progress cluster resize",
1925 on_hydration.hydration_size
1926 );
1927 }
1928 }
1929 }
1930
1931 if cluster.replicas().any(|r| r.config.location.pending()) {
1933 return Err(AlterClusterWhilePendingReplicas);
1934 }
1935
1936 let replica_id_by_name: BTreeMap<String, ReplicaId> = cluster
1939 .replicas()
1940 .map(|r| (r.name.clone(), r.replica_id))
1941 .collect();
1942 let owned_replica_ids: Vec<ReplicaId> = cluster
1947 .replicas()
1948 .filter(|r| {
1949 !r.config.location.internal()
1950 && r.config.location.billed_as().is_none()
1951 && !r.config.location.pending()
1952 })
1953 .map(|r| r.replica_id)
1954 .collect();
1955
1956 let compute = mz_sql::plan::ComputeReplicaConfig {
1957 introspection: new_logging
1958 .interval
1959 .map(|interval| ComputeReplicaIntrospectionConfig {
1960 debugging: new_logging.log_logging,
1961 interval,
1962 }),
1963 arrangement_compression: *new_arrangement_compression,
1964 };
1965
1966 if *new_replication_factor > replication_factor {
1971 if cluster_id.is_user() {
1972 self.validate_resource_limit(
1973 usize::cast_from(replication_factor),
1974 i64::from(*new_replication_factor) - i64::from(replication_factor),
1975 SystemVars::max_replicas_per_cluster,
1976 "cluster replica",
1977 MAX_REPLICAS_PER_CLUSTER.name(),
1978 )?;
1979 }
1980 }
1981
1982 let controller_owns = ENABLE_CLUSTER_CONTROLLER
1995 .get(self.catalog().system_config().dyncfgs())
1996 && cluster_id.is_user();
1997
1998 let config_changed = new_managed.replica_config_shape()
2005 != ManagedReplicaConfigShape::new(
2006 &size,
2007 &availability_zones,
2008 &logging,
2009 arrangement_compression,
2010 );
2011 let needed_replica_ids = if controller_owns {
2012 0
2013 } else if config_changed {
2014 *new_replication_factor
2015 } else if *new_replication_factor > replication_factor {
2016 *new_replication_factor - replication_factor
2017 } else {
2018 0
2019 };
2020 let mut new_replica_ids = if needed_replica_ids > 0 {
2030 let id_ts = self.get_catalog_write_ts().await;
2031 let ids = self
2032 .catalog()
2033 .allocate_replica_ids(cluster_id, u64::from(needed_replica_ids), id_ts)
2034 .await?;
2035 ids.into_iter()
2036 } else {
2037 Vec::<ReplicaId>::new().into_iter()
2038 };
2039
2040 let cluster_ctx = ClusterScopeContext {
2048 id: cluster_id.to_string(),
2049 name: name.clone(),
2050 is_builtin: cluster_id.is_system(),
2051 };
2052 let mut replica_ctxs = Vec::new();
2053
2054 if controller_owns {
2055 if config_changed {
2061 self.ensure_valid_azs(new_availability_zones.iter())?;
2062 }
2063 } else if config_changed {
2064 self.ensure_valid_azs(new_availability_zones.iter())?;
2065 match strategy {
2069 AlterClusterPlanStrategy::None => {
2070 let replica_ids_and_reasons = owned_replica_ids
2078 .iter()
2079 .map(|replica_id| {
2080 catalog::DropObjectInfo::ClusterReplica((
2081 cluster_id,
2082 *replica_id,
2083 reason.clone(),
2084 ))
2085 })
2086 .collect();
2087 ops.push(catalog::Op::DropObjects(replica_ids_and_reasons));
2088 for replica_name in
2089 (0..*new_replication_factor).map(managed_cluster_replica_name)
2090 {
2091 let replica_id = new_replica_ids
2094 .next()
2095 .expect("pre-allocated enough replica ids");
2096 let size_family = self.create_managed_cluster_replica_op(
2097 cluster_id,
2098 replica_id,
2099 replica_name.clone(),
2100 &compute,
2101 new_size,
2102 &mut ops,
2103 Some(new_availability_zones.as_ref()),
2104 false,
2105 owner_id,
2106 reason.clone(),
2107 )?;
2108 replica_ctxs.push(ReplicaEvalContext {
2109 cluster_id,
2110 replica_id,
2111 cluster: cluster_ctx.clone(),
2112 replica: ReplicaScopeContext {
2113 id: replica_id.to_string(),
2114 name: replica_name,
2115 is_builtin: cluster_id.is_system(),
2116 size: new_size.clone(),
2117 size_family,
2118 cluster_id: cluster_id.to_string(),
2119 cluster_name: cluster_ctx.name.clone(),
2120 },
2121 });
2122 }
2123 }
2124 AlterClusterPlanStrategy::For(_) | AlterClusterPlanStrategy::UntilReady { .. } => {
2125 for replica_name in
2126 (0..*new_replication_factor).map(managed_cluster_replica_name)
2127 {
2128 let replica_name = format!("{replica_name}{PENDING_REPLICA_SUFFIX}");
2129 let replica_id = new_replica_ids
2130 .next()
2131 .expect("pre-allocated enough replica ids");
2132 let size_family = self.create_managed_cluster_replica_op(
2133 cluster_id,
2134 replica_id,
2135 replica_name.clone(),
2136 &compute,
2137 new_size,
2138 &mut ops,
2139 Some(new_availability_zones.as_ref()),
2140 true,
2141 owner_id,
2142 reason.clone(),
2143 )?;
2144 replica_ctxs.push(ReplicaEvalContext {
2145 cluster_id,
2146 replica_id,
2147 cluster: cluster_ctx.clone(),
2148 replica: ReplicaScopeContext {
2149 id: replica_id.to_string(),
2150 name: replica_name,
2151 is_builtin: cluster_id.is_system(),
2152 size: new_size.clone(),
2153 size_family,
2154 cluster_id: cluster_id.to_string(),
2155 cluster_name: cluster_ctx.name.clone(),
2156 },
2157 });
2158 }
2159 finalization_needed = NeedsFinalization::Yes;
2160 }
2161 }
2162 } else if *new_replication_factor < replication_factor {
2163 let replica_ids = (*new_replication_factor..replication_factor)
2165 .map(managed_cluster_replica_name)
2166 .filter_map(|name| replica_id_by_name.get(&name).copied())
2167 .map(|replica_id| {
2168 catalog::DropObjectInfo::ClusterReplica((
2169 cluster_id,
2170 replica_id,
2171 reason.clone(),
2172 ))
2173 })
2174 .collect();
2175 ops.push(catalog::Op::DropObjects(replica_ids));
2176 } else if *new_replication_factor > replication_factor {
2177 for replica_name in
2179 (replication_factor..*new_replication_factor).map(managed_cluster_replica_name)
2180 {
2181 let replica_id = new_replica_ids
2182 .next()
2183 .expect("pre-allocated enough replica ids");
2184 let size_family = self.create_managed_cluster_replica_op(
2185 cluster_id,
2186 replica_id,
2187 replica_name.clone(),
2188 &compute,
2189 new_size,
2190 &mut ops,
2191 Some(new_availability_zones.as_ref()),
2194 false,
2195 owner_id,
2196 reason.clone(),
2197 )?;
2198 replica_ctxs.push(ReplicaEvalContext {
2199 cluster_id,
2200 replica_id,
2201 cluster: cluster_ctx.clone(),
2202 replica: ReplicaScopeContext {
2203 id: replica_id.to_string(),
2204 name: replica_name,
2205 is_builtin: cluster_id.is_system(),
2206 size: new_size.clone(),
2207 size_family,
2208 cluster_id: cluster_id.to_string(),
2209 cluster_name: cluster_ctx.name.clone(),
2210 },
2211 });
2212 }
2213 }
2214
2215 match finalization_needed {
2229 NeedsFinalization::No => {
2230 let mut new_config = new_config;
2231 let reconfiguration_audit = if controller_owns {
2232 None
2233 } else {
2234 cancel_carried_reconfiguration(&mut new_config)
2235 };
2236 ops.push(catalog::Op::UpdateClusterConfig {
2237 id: cluster_id,
2238 name: name.clone(),
2239 config: new_config,
2240 reconfiguration_audit,
2241 burst_audit: None,
2242 });
2243 }
2244 NeedsFinalization::Yes => {}
2245 }
2246
2247 if let Some(scoped_op) = self.scoped_overrides_create_op(&[], &replica_ctxs) {
2254 ops.push(scoped_op);
2255 }
2256
2257 self.catalog_transact(session, ops).await?;
2258 Ok(finalization_needed)
2259 }
2260
2261 async fn sequence_alter_cluster_unmanaged_to_managed(
2265 &mut self,
2266 session: &Session,
2267 cluster_id: ClusterId,
2268 mut new_config: ClusterConfig,
2269 options: PlanClusterOption,
2270 ) -> Result<(), AdapterError> {
2271 let cluster = self.catalog.get_cluster(cluster_id);
2272 let cluster_name = cluster.name().to_string();
2273
2274 let ClusterVariant::Managed(ClusterVariantManaged {
2275 size: new_size,
2276 replication_factor: new_replication_factor,
2277 availability_zones: new_availability_zones,
2278 logging: _,
2279 arrangement_compression: _,
2280 optimizer_feature_overrides: _,
2281 schedule: _,
2282 auto_scaling_strategy: _,
2283 reconfiguration: _,
2284 burst: _,
2285 }) = &mut new_config.variant
2286 else {
2287 panic!("expected new managed cluster config");
2288 };
2289
2290 let user_replica_count = cluster
2292 .user_replicas()
2293 .count()
2294 .try_into()
2295 .expect("must_fit");
2296 match options.replication_factor {
2297 AlterOptionParameter::Set(_) => {
2298 if user_replica_count != *new_replication_factor {
2300 coord_bail!(
2301 "REPLICATION FACTOR {new_replication_factor} does not match number of replicas ({user_replica_count})"
2302 );
2303 }
2304 }
2305 _ => {
2306 *new_replication_factor = user_replica_count;
2307 }
2308 }
2309
2310 let mut names = BTreeSet::new();
2311 let mut sizes = BTreeSet::new();
2312
2313 self.ensure_valid_azs(new_availability_zones.iter())?;
2314
2315 for replica in cluster.user_replicas() {
2317 names.insert(replica.name.clone());
2318 match &replica.config.location {
2319 ReplicaLocation::Unmanaged(_) => coord_bail!(
2320 "Cannot convert unmanaged cluster with unmanaged replicas to managed cluster"
2321 ),
2322 ReplicaLocation::Managed(location) => {
2323 sizes.insert(location.size.clone());
2324
2325 for az in &location.availability_zones {
2329 if !new_availability_zones.contains(az) {
2330 coord_bail!(
2331 "unmanaged replica has availability zone {az} which is not \
2332 in managed {new_availability_zones:?}"
2333 )
2334 }
2335 }
2336 }
2337 }
2338 }
2339
2340 if sizes.is_empty() {
2341 assert!(
2342 cluster.user_replicas().next().is_none(),
2343 "Cluster should not have replicas"
2344 );
2345 match &options.size {
2347 AlterOptionParameter::Reset | AlterOptionParameter::Unchanged => {
2348 coord_bail!("Missing SIZE for empty cluster")
2349 }
2350 AlterOptionParameter::Set(_) => {} }
2352 } else if sizes.len() == 1 {
2353 let size = sizes.into_iter().next().expect("must exist");
2354 match &options.size {
2355 AlterOptionParameter::Set(sz) if *sz != size => {
2356 coord_bail!("Cluster replicas of size {size} do not match expected SIZE {sz}");
2357 }
2358 _ => *new_size = size,
2359 }
2360 } else {
2361 let formatted = sizes
2362 .iter()
2363 .map(String::as_str)
2364 .collect::<Vec<_>>()
2365 .join(", ");
2366 coord_bail!(
2367 "Cannot convert unmanaged cluster to managed, non-unique replica sizes: {formatted}"
2368 );
2369 }
2370
2371 for i in 0..*new_replication_factor {
2372 let name = managed_cluster_replica_name(i);
2373 names.remove(&name);
2374 }
2375 if !names.is_empty() {
2376 let formatted = names
2377 .iter()
2378 .map(String::as_str)
2379 .collect::<Vec<_>>()
2380 .join(", ");
2381 coord_bail!(
2382 "Cannot convert unmanaged cluster to managed, invalid replica names: {formatted}"
2383 );
2384 }
2385
2386 let ops = vec![catalog::Op::UpdateClusterConfig {
2387 id: cluster_id,
2388 name: cluster_name,
2389 config: new_config,
2390 reconfiguration_audit: None,
2391 burst_audit: None,
2392 }];
2393
2394 self.catalog_transact(Some(session), ops).await?;
2395 Ok(())
2396 }
2397
2398 async fn sequence_alter_cluster_managed_to_unmanaged(
2399 &mut self,
2400 session: &Session,
2401 cluster_id: ClusterId,
2402 new_config: ClusterConfig,
2403 ) -> Result<(), AdapterError> {
2404 let cluster = self.catalog().get_cluster(cluster_id);
2405
2406 if let ClusterVariant::Managed(managed) = &cluster.config.variant {
2412 if managed
2413 .reconfiguration
2414 .as_ref()
2415 .is_some_and(|record| record.is_in_progress())
2416 {
2417 return Err(AdapterError::AlterClusterUnmanagedWhileReconfiguring);
2418 }
2419 if managed.burst.is_some() {
2426 return Err(AdapterError::AlterClusterUnmanagedWhileBursting);
2427 }
2428 }
2429
2430 let ops = vec![catalog::Op::UpdateClusterConfig {
2431 id: cluster_id,
2432 name: cluster.name().to_string(),
2433 config: new_config,
2434 reconfiguration_audit: None,
2435 burst_audit: None,
2436 }];
2437
2438 self.catalog_transact(Some(session), ops).await?;
2439 Ok(())
2440 }
2441
2442 async fn sequence_alter_cluster_unmanaged_to_unmanaged(
2443 &mut self,
2444 session: &Session,
2445 cluster_id: ClusterId,
2446 new_config: ClusterConfig,
2447 replicas: AlterOptionParameter<Vec<(String, mz_sql::plan::ReplicaConfig)>>,
2448 ) -> Result<(), AdapterError> {
2449 if !matches!(replicas, AlterOptionParameter::Unchanged) {
2450 coord_bail!("Cannot alter replicas in unmanaged cluster");
2451 }
2452
2453 let cluster = self.catalog().get_cluster(cluster_id);
2454
2455 let ops = vec![catalog::Op::UpdateClusterConfig {
2456 id: cluster_id,
2457 name: cluster.name().to_string(),
2458 config: new_config,
2459 reconfiguration_audit: None,
2460 burst_audit: None,
2461 }];
2462
2463 self.catalog_transact(Some(session), ops).await?;
2464 Ok(())
2465 }
2466
2467 pub(crate) async fn sequence_alter_cluster_rename(
2468 &mut self,
2469 ctx: &mut ExecuteContext,
2470 AlterClusterRenamePlan { id, name, to_name }: AlterClusterRenamePlan,
2471 ) -> Result<ExecuteResponse, AdapterError> {
2472 let op = Op::RenameCluster {
2473 id,
2474 name,
2475 to_name,
2476 check_reserved_names: true,
2477 };
2478 match self
2479 .catalog_transact_with_ddl_transaction(ctx, vec![op], |_, _| Box::pin(async {}))
2480 .await
2481 {
2482 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
2483 Err(err) => Err(err),
2484 }
2485 }
2486
2487 pub(crate) async fn sequence_alter_cluster_swap(
2488 &mut self,
2489 ctx: &mut ExecuteContext,
2490 AlterClusterSwapPlan {
2491 id_a,
2492 id_b,
2493 name_a,
2494 name_b,
2495 name_temp,
2496 }: AlterClusterSwapPlan,
2497 ) -> Result<ExecuteResponse, AdapterError> {
2498 let op_a = Op::RenameCluster {
2499 id: id_a,
2500 name: name_a.clone(),
2501 to_name: name_temp.clone(),
2502 check_reserved_names: false,
2503 };
2504 let op_b = Op::RenameCluster {
2505 id: id_b,
2506 name: name_b.clone(),
2507 to_name: name_a,
2508 check_reserved_names: false,
2509 };
2510 let op_temp = Op::RenameCluster {
2511 id: id_a,
2512 name: name_temp,
2513 to_name: name_b,
2514 check_reserved_names: false,
2515 };
2516
2517 match self
2518 .catalog_transact_with_ddl_transaction(ctx, vec![op_a, op_b, op_temp], |_, _| {
2519 Box::pin(async {})
2520 })
2521 .await
2522 {
2523 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
2524 Err(err) => Err(err),
2525 }
2526 }
2527
2528 pub(crate) async fn sequence_alter_cluster_replica_rename(
2529 &mut self,
2530 session: &Session,
2531 AlterClusterReplicaRenamePlan {
2532 cluster_id,
2533 replica_id,
2534 name,
2535 to_name,
2536 }: AlterClusterReplicaRenamePlan,
2537 ) -> Result<ExecuteResponse, AdapterError> {
2538 let op = catalog::Op::RenameClusterReplica {
2539 cluster_id,
2540 replica_id,
2541 name,
2542 to_name,
2543 };
2544 match self.catalog_transact(Some(session), vec![op]).await {
2545 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::ClusterReplica)),
2546 Err(err) => Err(err),
2547 }
2548 }
2549
2550 pub(crate) async fn sequence_alter_set_cluster(
2552 &self,
2553 _session: &Session,
2554 AlterSetClusterPlan { id, set_cluster: _ }: AlterSetClusterPlan,
2555 ) -> Result<ExecuteResponse, AdapterError> {
2556 async {}.await;
2560 let entry = self.catalog().get_entry(&id);
2561 match entry.item().typ() {
2562 _ => {
2563 Err(AdapterError::Unsupported("ALTER SET CLUSTER"))
2565 }
2566 }
2567 }
2568}
2569
2570struct ReconfigurationDimensionsUnchanged {
2575 size: bool,
2576 replication_factor: bool,
2577 availability_zones: bool,
2578 log_logging: bool,
2579 interval: bool,
2580 arrangement_compression: bool,
2581}
2582
2583pub(crate) fn cancel_carried_reconfiguration(
2595 config: &mut ClusterConfig,
2596) -> Option<ReconfigurationAudit> {
2597 let ClusterVariant::Managed(managed) = &mut config.variant else {
2598 return None;
2599 };
2600 let record = managed.reconfiguration.as_mut()?;
2601 if !record.is_in_progress() {
2602 return None;
2603 }
2604 record.status = ReconfigurationStatus::Cancelled;
2605 Some(ReconfigurationAudit::Cancelled)
2606}
2607
2608fn alter_changes_replica_shape(options: &PlanClusterOption) -> bool {
2618 use mz_sql::plan::AlterOptionParameter::Unchanged;
2619 let PlanClusterOption {
2620 availability_zones,
2621 introspection_debugging,
2622 introspection_interval,
2623 arrangement_compression,
2624 managed: _,
2625 replicas: _,
2626 replication_factor: _,
2627 size,
2628 schedule: _,
2629 workload_class: _,
2630 auto_scaling_strategy: _,
2631 } = options;
2632 !matches!(size, Unchanged)
2633 || !matches!(availability_zones, Unchanged)
2634 || !matches!(introspection_debugging, Unchanged)
2635 || !matches!(introspection_interval, Unchanged)
2636 || !matches!(arrangement_compression, Unchanged)
2637}
2638
2639fn fold_reconfiguration_target(
2657 in_flight: Option<&ReconfigurationTarget>,
2658 new_target: ReconfigurationTarget,
2659 unchanged: ReconfigurationDimensionsUnchanged,
2660) -> ReconfigurationTarget {
2661 let Some(prev) = in_flight else {
2662 return new_target;
2663 };
2664 ReconfigurationTarget {
2665 size: if unchanged.size {
2666 prev.size.clone()
2667 } else {
2668 new_target.size
2669 },
2670 replication_factor: if unchanged.replication_factor {
2671 prev.replication_factor
2672 } else {
2673 new_target.replication_factor
2674 },
2675 availability_zones: if unchanged.availability_zones {
2676 prev.availability_zones.clone()
2677 } else {
2678 new_target.availability_zones
2679 },
2680 logging: ReplicaLogging {
2681 log_logging: if unchanged.log_logging {
2682 prev.logging.log_logging
2683 } else {
2684 new_target.logging.log_logging
2685 },
2686 interval: if unchanged.interval {
2687 prev.logging.interval
2688 } else {
2689 new_target.logging.interval
2690 },
2691 },
2692 arrangement_compression: if unchanged.arrangement_compression {
2693 prev.arrangement_compression
2694 } else {
2695 new_target.arrangement_compression
2696 },
2697 }
2698}
2699
2700#[derive(PartialEq)]
2703pub(crate) enum NeedsFinalization {
2704 Yes,
2706 No,
2707}
2708
2709#[cfg(test)]
2710mod tests {
2711 use mz_controller::clusters::ReplicaLogging;
2712 use mz_controller_types::DEFAULT_REPLICA_LOGGING_INTERVAL;
2713
2714 use super::*;
2715
2716 fn target(size: &str, rf: u32, azs: &[&str], log_logging: bool) -> ReconfigurationTarget {
2717 ReconfigurationTarget {
2718 size: size.to_string(),
2719 replication_factor: rf,
2720 availability_zones: azs.iter().map(|s| s.to_string()).collect(),
2721 logging: ReplicaLogging {
2722 log_logging,
2723 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
2724 },
2725 arrangement_compression: false,
2726 }
2727 }
2728
2729 fn all_changed() -> ReconfigurationDimensionsUnchanged {
2730 ReconfigurationDimensionsUnchanged {
2731 size: false,
2732 replication_factor: false,
2733 availability_zones: false,
2734 log_logging: false,
2735 interval: false,
2736 arrangement_compression: false,
2737 }
2738 }
2739
2740 fn all_unchanged() -> ReconfigurationDimensionsUnchanged {
2741 ReconfigurationDimensionsUnchanged {
2742 size: true,
2743 replication_factor: true,
2744 availability_zones: true,
2745 log_logging: true,
2746 interval: true,
2747 arrangement_compression: true,
2748 }
2749 }
2750
2751 #[mz_ore::test]
2752 fn fold_with_no_record_takes_new_target() {
2753 let new = target("200cc", 3, &["az1"], true);
2755 let folded = fold_reconfiguration_target(None, new.clone(), all_changed());
2756 assert_eq!(folded, new);
2757 }
2758
2759 #[mz_ore::test]
2760 fn fold_rf_only_keeps_in_flight_shape() {
2761 let in_flight = target("200cc", 1, &["az2"], true);
2765 let new = target("100cc", 5, &["az1"], false);
2768 let unchanged = ReconfigurationDimensionsUnchanged {
2769 size: true,
2770 replication_factor: false,
2771 availability_zones: true,
2772 log_logging: true,
2773 interval: true,
2774 arrangement_compression: true,
2775 };
2776 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2777 assert_eq!(folded, target("200cc", 5, &["az2"], true));
2779 }
2780
2781 #[mz_ore::test]
2782 fn fold_with_all_set_overwrites_every_dimension() {
2783 let in_flight = target("200cc", 1, &["az2"], true);
2785 let new = target("400cc", 9, &["az9"], false);
2786 let folded = fold_reconfiguration_target(Some(&in_flight), new.clone(), all_changed());
2787 assert_eq!(folded, new);
2788 }
2789
2790 #[mz_ore::test]
2791 fn fold_all_unchanged_is_alter_back_to_in_flight() {
2792 let in_flight = target("200cc", 2, &["az2"], true);
2797 let realized_shaped = target("100cc", 1, &["az1"], false);
2798 let folded =
2799 fold_reconfiguration_target(Some(&in_flight), realized_shaped, all_unchanged());
2800 assert_eq!(folded, in_flight);
2801 }
2802
2803 #[mz_ore::test]
2804 fn fold_logging_subdimensions_fold_independently() {
2805 let mut in_flight = target("100cc", 1, &["az1"], false);
2810 in_flight.logging.interval = Some(Duration::from_secs(5));
2811 let new = target("100cc", 1, &["az1"], true);
2812 let unchanged = ReconfigurationDimensionsUnchanged {
2813 size: true,
2814 replication_factor: true,
2815 availability_zones: true,
2816 log_logging: false,
2817 interval: true,
2818 arrangement_compression: true,
2819 };
2820 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2821 assert_eq!(
2822 folded.logging,
2823 ReplicaLogging {
2824 log_logging: true,
2825 interval: Some(Duration::from_secs(5)),
2826 }
2827 );
2828 }
2829}