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::error::ErrorKind;
19use mz_catalog::memory::objects::{
20 Cluster, ClusterConfig, ClusterReplica, ClusterVariant, ClusterVariantManaged, DataSourceDesc,
21 ManagedReplicaConfigShape, ReconfigurationState, ReconfigurationStatus, ReconfigurationTarget,
22};
23use mz_compute_types::config::ComputeReplicaConfig;
24use mz_controller::clusters::{
25 ClusterStatus, ManagedReplicaLocation, ReplicaConfig, ReplicaLocation, ReplicaLogging,
26};
27use mz_controller_types::{ClusterId, DEFAULT_REPLICA_LOGGING_INTERVAL, ReplicaId};
28use mz_ore::cast::CastFrom;
29use mz_ore::collections::CollectionExt;
30use mz_ore::instrument;
31use mz_repr::Timestamp;
32use mz_repr::adt::numeric::Numeric;
33use mz_repr::role_id::RoleId;
34use mz_sql::ast::{Ident, QualifiedReplica};
35use mz_sql::catalog::{CatalogCluster, CatalogError, ObjectType};
36use mz_sql::names::QualifiedItemName;
37use mz_sql::plan::{
38 self, AlterClusterPlanStrategy, AlterClusterRenamePlan, AlterClusterReplicaRenamePlan,
39 AlterClusterSwapPlan, AlterOptionParameter, AlterSetClusterPlan,
40 ComputeReplicaIntrospectionConfig, CreateClusterManagedPlan, CreateClusterPlan,
41 CreateClusterReplicaPlan, CreateClusterUnmanagedPlan, CreateClusterVariant, PlanClusterOption,
42};
43use mz_sql::plan::{AlterClusterPlan, OnTimeoutAction};
44use mz_sql::session::metadata::SessionMetadata;
45use mz_sql::session::vars::{
46 MAX_CREDIT_CONSUMPTION_RATE, MAX_REPLICAS_PER_CLUSTER, SystemVars, Var,
47};
48use mz_storage_types::sources::SourceConnection;
49use tracing::{Instrument, Span, debug};
50
51use mz_adapter_types::dyncfgs::{
52 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT, ENABLE_BACKGROUND_ALTER_CLUSTER,
53};
54
55use super::return_if_err;
56use crate::AdapterError::AlterClusterWhilePendingReplicas;
57use crate::catalog::{self, Op, ReplicaCreateDropReason};
58use crate::config::{
59 ClusterEvalContext, ClusterScopeContext, ReplicaEvalContext, ReplicaScopeContext,
60};
61use crate::coord::{
62 AlterCluster, AlterClusterAwaitReconfiguration, AlterClusterFinalize,
63 AlterClusterWaitForHydrated, ClusterReplicaStatuses, ClusterStage, Coordinator, Message,
64 PlanValidity, StageResult, Staged,
65};
66use crate::{AdapterError, AdapterNotice, ExecuteContext, ExecuteResponse, session::Session};
67
68const PENDING_REPLICA_SUFFIX: &str = "-pending";
69
70impl Staged for ClusterStage {
71 type Ctx = ExecuteContext;
72
73 fn validity(&mut self) -> &mut PlanValidity {
74 match self {
75 Self::Alter(stage) => &mut stage.validity,
76 Self::WaitForHydrated(stage) => &mut stage.validity,
77 Self::Finalize(stage) => &mut stage.validity,
78 Self::AwaitReconfiguration(stage) => &mut stage.validity,
79 }
80 }
81
82 async fn stage(
83 self,
84 coord: &mut Coordinator,
85 ctx: &mut ExecuteContext,
86 ) -> Result<StageResult<Box<Self>>, crate::AdapterError> {
87 match self {
88 Self::Alter(stage) => {
89 coord
90 .sequence_alter_cluster_stage(ctx.session(), stage.plan.clone(), stage.validity)
91 .await
92 }
93 Self::WaitForHydrated(stage) => {
94 let AlterClusterWaitForHydrated {
95 validity,
96 plan,
97 new_config,
98 workload_class,
99 timeout_time,
100 on_timeout,
101 } = stage;
102 coord
103 .check_if_pending_replicas_hydrated_stage(
104 ctx.session(),
105 plan,
106 new_config,
107 workload_class,
108 timeout_time,
109 on_timeout,
110 validity,
111 )
112 .await
113 }
114 Self::Finalize(stage) => {
115 coord
116 .finalize_alter_cluster_stage(
117 ctx.session(),
118 stage.plan.clone(),
119 stage.new_config.clone(),
120 stage.workload_class.clone(),
121 )
122 .await
123 }
124 Self::AwaitReconfiguration(stage) => {
125 coord.await_reconfiguration_stage(stage.validity, stage.cluster_id, stage.target)
126 }
127 }
128 }
129
130 fn message(self, ctx: ExecuteContext, span: tracing::Span) -> Message {
131 Message::ClusterStageReady {
132 ctx,
133 span,
134 stage: self,
135 }
136 }
137
138 fn cancel_enabled(&self) -> bool {
139 true
140 }
141}
142
143impl Coordinator {
144 #[instrument]
145 pub(crate) async fn sequence_alter_cluster_staged(
146 &mut self,
147 ctx: ExecuteContext,
148 plan: plan::AlterClusterPlan,
149 ) {
150 let stage = return_if_err!(self.alter_cluster_validate(ctx.session(), plan).await, ctx);
151 self.sequence_staged(ctx, Span::current(), stage).await;
152 }
153
154 #[instrument]
155 async fn alter_cluster_validate(
156 &self,
157 session: &Session,
158 plan: plan::AlterClusterPlan,
159 ) -> Result<ClusterStage, AdapterError> {
160 let validity = PlanValidity::new(
161 self.catalog(),
162 BTreeSet::new(),
163 Some(plan.id.clone()),
164 None,
165 session.role_metadata().clone(),
166 );
167 Ok(ClusterStage::Alter(AlterCluster { validity, plan }))
168 }
169
170 async fn sequence_alter_cluster_stage(
171 &mut self,
172 session: &Session,
173 plan: plan::AlterClusterPlan,
174 validity: PlanValidity,
175 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
176 let AlterClusterPlan {
177 id: cluster_id,
178 name: _,
179 ref options,
180 ref strategy,
181 } = plan;
182
183 use mz_catalog::memory::objects::ClusterVariant::*;
184 use mz_sql::plan::AlterOptionParameter::*;
185 let cluster = self.catalog.get_cluster(cluster_id);
186 let config = cluster.config.clone();
187 let mut new_config = config.clone();
188
189 match (&new_config.variant, &options.managed) {
190 (Managed(_), Reset) | (Managed(_), Unchanged) | (Managed(_), Set(true)) => {}
191 (Managed(_), Set(false)) => new_config.variant = Unmanaged,
192 (Unmanaged, Unchanged) | (Unmanaged, Set(false)) => {}
193 (Unmanaged, Reset) | (Unmanaged, Set(true)) => {
194 let size = "".to_string();
198 let logging = ReplicaLogging {
199 log_logging: false,
200 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
201 };
202 new_config.variant = Managed(ClusterVariantManaged {
203 size,
204 availability_zones: Default::default(),
205 logging,
206 arrangement_compression: false,
207 replication_factor: 1,
208 optimizer_feature_overrides: Default::default(),
209 schedule: Default::default(),
210 auto_scaling_strategy: None,
211 reconfiguration: None,
212 burst: None,
213 });
214 }
215 }
216
217 match &mut new_config.variant {
218 Managed(ClusterVariantManaged {
219 size,
220 availability_zones,
221 logging,
222 arrangement_compression,
223 replication_factor,
224 optimizer_feature_overrides: _,
225 schedule,
226 auto_scaling_strategy,
227 reconfiguration: _,
228 burst: _,
229 }) => {
230 match &options.size {
231 Set(s) => size.clone_from(s),
232 Reset => coord_bail!("SIZE has no default value"),
233 Unchanged => {}
234 }
235 match &options.availability_zones {
236 Set(az) => availability_zones.clone_from(az),
237 Reset => *availability_zones = Default::default(),
238 Unchanged => {}
239 }
240 match &options.introspection_debugging {
241 Set(id) => logging.log_logging = *id,
242 Reset => logging.log_logging = false,
243 Unchanged => {}
244 }
245 match &options.introspection_interval {
246 Set(ii) => logging.interval = ii.0,
247 Reset => logging.interval = Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
248 Unchanged => {}
249 }
250 match &options.arrangement_compression {
251 Set(ac) => *arrangement_compression = *ac,
252 Reset => *arrangement_compression = false,
253 Unchanged => {}
254 }
255 match &options.replication_factor {
256 Set(rf) => *replication_factor = *rf,
257 Reset => {
258 *replication_factor = self
259 .catalog
260 .system_config()
261 .default_cluster_replication_factor()
262 }
263 Unchanged => {}
264 }
265 match &options.schedule {
266 Set(new_schedule) => {
267 *schedule = new_schedule.clone();
268 }
269 Reset => *schedule = Default::default(),
270 Unchanged => {}
271 }
272 match &options.auto_scaling_strategy {
273 Set(new_strategy) => auto_scaling_strategy.clone_from(new_strategy),
274 Reset => *auto_scaling_strategy = None,
276 Unchanged => {}
277 }
278 if !matches!(options.replicas, Unchanged) {
279 coord_bail!("Cannot change REPLICAS of managed clusters");
280 }
281 }
282 Unmanaged => {
283 if !matches!(options.size, Unchanged) {
284 coord_bail!("Cannot change SIZE of unmanaged clusters");
285 }
286 if !matches!(options.availability_zones, Unchanged) {
287 coord_bail!("Cannot change AVAILABILITY ZONES of unmanaged clusters");
288 }
289 if !matches!(options.introspection_debugging, Unchanged) {
290 coord_bail!("Cannot change INTROSPECTION DEGUBBING of unmanaged clusters");
291 }
292 if !matches!(options.introspection_interval, Unchanged) {
293 coord_bail!("Cannot change INTROSPECTION INTERVAL of unmanaged clusters");
294 }
295 if !matches!(options.arrangement_compression, Unchanged) {
296 coord_bail!(
297 "Cannot change EXPERIMENTAL ARRANGEMENT COMPRESSION of unmanaged clusters"
298 );
299 }
300 if !matches!(options.replication_factor, Unchanged) {
301 coord_bail!("Cannot change REPLICATION FACTOR of unmanaged clusters");
302 }
303 if !matches!(options.auto_scaling_strategy, Unchanged) {
304 coord_bail!("Cannot change AUTO SCALING STRATEGY of unmanaged clusters");
305 }
306 }
307 }
308
309 match &options.workload_class {
310 Set(wc) => new_config.workload_class.clone_from(wc),
311 Reset => new_config.workload_class = None,
312 Unchanged => {}
313 }
314
315 let reconfiguration_in_flight = matches!(
316 &config.variant,
317 Managed(managed) if managed
318 .reconfiguration
319 .as_ref()
320 .is_some_and(|record| record.is_in_progress())
321 );
322
323 if reconfiguration_in_flight && !matches!(options.schedule, Unchanged) {
329 return Err(AdapterError::AlterClusterScheduleWhileReconfiguring);
330 }
331
332 if reconfiguration_in_flight && !matches!(options.replication_factor, Unchanged) {
339 return Err(AdapterError::AlterClusterReplicationFactorWhileReconfiguring);
340 }
341
342 let cancels_or_retargets =
347 reconfiguration_in_flight && alter_changes_replica_shape(options);
348 if new_config == config && !cancels_or_retargets {
349 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
350 ObjectType::Cluster,
351 )));
352 }
353
354 let single_replica_sources_notice = match (&config.variant, &new_config.variant) {
361 (Managed(old_managed), Managed(new_managed))
362 if new_managed.replication_factor > old_managed.replication_factor
363 && new_managed.replication_factor > 1 =>
364 {
365 let sources = self.single_replica_source_names(cluster);
366 (!sources.is_empty()).then(|| {
367 AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster {
368 cluster: cluster.name.clone(),
369 sources,
370 }
371 })
372 }
373 _ => None,
374 };
375
376 if let (Managed(old_managed), Managed(new_managed)) = (&config.variant, &new_config.variant)
388 {
389 let needs_record = if reconfiguration_in_flight {
390 alter_changes_replica_shape(options)
391 } else {
392 new_managed.replica_config_shape() != old_managed.replica_config_shape()
393 };
394 let scheduled_direct =
407 !matches!(new_managed.schedule, mz_sql::plan::ClusterSchedule::Manual)
408 && !reconfiguration_in_flight;
409 if scheduled_direct && !matches!(strategy, AlterClusterPlanStrategy::None) {
416 return Err(AdapterError::AlterClusterWaitOnScheduledCluster);
417 }
418 if needs_record && !scheduled_direct {
419 let result = self
420 .reshape_alter_cluster_managed(
421 session,
422 cluster_id,
423 new_config.clone(),
424 options,
425 strategy,
426 validity,
427 )
428 .await;
429 if result.is_ok() {
430 if let Some(notice) = single_replica_sources_notice {
431 session.add_notice(notice);
432 }
433 }
434 return result;
435 }
436 }
437
438 match (&config.variant, &new_config.variant) {
439 (Managed(_), Managed(new_config_managed)) => {
440 let alter_followup = self
441 .sequence_alter_cluster_managed_to_managed(
442 Some(session),
443 cluster_id,
444 new_config.clone(),
445 ReplicaCreateDropReason::Manual,
446 strategy.clone(),
447 )
448 .await?;
449 if let Some(notice) = single_replica_sources_notice {
450 session.add_notice(notice);
451 }
452 if alter_followup == NeedsFinalization::Yes {
453 self.active_conns
456 .get_mut(session.conn_id())
457 .expect("There must be an active connection")
458 .pending_cluster_alters
459 .insert(cluster_id.clone());
460 let new_config_managed = new_config_managed.clone();
461 return match &strategy {
462 AlterClusterPlanStrategy::None => Err(AdapterError::Internal(
463 "AlterClusterPlanStrategy must not be None if NeedsFinalization is Yes"
464 .into(),
465 )),
466 AlterClusterPlanStrategy::For(duration) => {
467 let span = Span::current();
468 let plan = plan.clone();
469 let duration = duration.clone().to_owned();
470 let workload_class = new_config.workload_class.clone();
471 Ok(StageResult::Handle(mz_ore::task::spawn(
472 || "Finalize Alter Cluster",
473 async move {
474 tokio::time::sleep(duration).await;
475 let stage = ClusterStage::Finalize(AlterClusterFinalize {
476 validity,
477 plan,
478 new_config: new_config_managed,
479 workload_class,
480 });
481 Ok(Box::new(stage))
482 }
483 .instrument(span),
484 )))
485 }
486 AlterClusterPlanStrategy::UntilReady {
487 timeout,
488 on_timeout,
489 } => Ok(StageResult::Immediate(Box::new(
490 ClusterStage::WaitForHydrated(AlterClusterWaitForHydrated {
491 validity,
492 plan: plan.clone(),
493 new_config: new_config_managed.clone(),
494 workload_class: new_config.workload_class.clone(),
495 timeout_time: Instant::now() + timeout.to_owned(),
496 on_timeout: on_timeout.unwrap_or(OnTimeoutAction::Commit),
500 }),
501 ))),
502 };
503 }
504 }
505 (Unmanaged, Managed(new_managed)) => {
506 if !matches!(new_managed.schedule, mz_sql::plan::ClusterSchedule::Manual)
511 && !matches!(strategy, AlterClusterPlanStrategy::None)
512 {
513 return Err(AdapterError::AlterClusterWaitOnScheduledCluster);
514 }
515 self.sequence_alter_cluster_unmanaged_to_managed(
516 session,
517 cluster_id,
518 new_config,
519 options.to_owned(),
520 )
521 .await?;
522 }
523 (Managed(_), Unmanaged) => {
524 self.sequence_alter_cluster_managed_to_unmanaged(session, cluster_id, new_config)
525 .await?;
526 }
527 (Unmanaged, Unmanaged) => {
528 self.sequence_alter_cluster_unmanaged_to_unmanaged(
529 session,
530 cluster_id,
531 new_config,
532 options.replicas.clone(),
533 )
534 .await?;
535 }
536 }
537
538 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
539 ObjectType::Cluster,
540 )))
541 }
542
543 fn validate_reconfiguration_resource_limits(
545 &self,
546 cluster_id: ClusterId,
547 target: &ReconfigurationTarget,
548 ) -> Result<(), AdapterError> {
549 if !cluster_id.is_user() {
554 return Ok(());
555 }
556 let cluster = self.catalog().get_cluster(cluster_id);
557 let ClusterVariant::Managed(realized) = &cluster.config.variant else {
558 return Ok(());
559 };
560
561 if target.matches_realized_config(realized) {
567 return Ok(());
568 }
569
570 self.validate_resource_limit(
585 usize::cast_from(realized.replication_factor),
586 i64::from(target.replication_factor),
587 SystemVars::max_replicas_per_cluster,
588 "cluster replica",
589 MAX_REPLICAS_PER_CLUSTER.name(),
590 )?;
591
592 self.validate_reconfiguration_credit_peak(cluster_id, realized, target)?;
594
595 Ok(())
596 }
597
598 fn validate_reconfiguration_credit_peak(
606 &self,
607 cluster_id: ClusterId,
608 realized: &ClusterVariantManaged,
609 target: &ReconfigurationTarget,
610 ) -> Result<(), AdapterError> {
611 let shape_credit = |size: &str, replication_factor: u32| -> Numeric {
612 let per_replica = self
613 .catalog()
614 .cluster_replica_sizes()
615 .0
616 .get(size)
617 .map(|allocation| allocation.credits_per_hour)
618 .unwrap_or_else(Numeric::zero);
621 per_replica * Numeric::from(replication_factor)
622 };
623 let mut peak_credit = shape_credit(&target.size, target.replication_factor);
624 peak_credit += shape_credit(&realized.size, realized.replication_factor);
625 self.validate_resource_limit_numeric(
626 self.current_credit_consumption_rate(Some(cluster_id)),
627 peak_credit,
628 |system_vars| {
629 self.license_key
630 .max_credit_consumption_rate()
631 .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
632 },
633 "cluster replica",
634 MAX_CREDIT_CONSUMPTION_RATE.name(),
635 )?;
636
637 Ok(())
638 }
639
640 async fn reshape_alter_cluster_managed(
676 &mut self,
677 session: &Session,
678 cluster_id: ClusterId,
679 new_config: ClusterConfig,
680 options: &PlanClusterOption,
681 strategy: &AlterClusterPlanStrategy,
682 validity: PlanValidity,
683 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
684 use mz_sql::plan::AlterOptionParameter::Unchanged;
685
686 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
687 return Err(AdapterError::Internal(
688 "reshape_alter_cluster_managed requires a managed target config".into(),
689 ));
690 };
691
692 let cluster = self.catalog.get_cluster(cluster_id);
698 let in_flight = match &cluster.config.variant {
699 ClusterVariant::Managed(managed) => managed
700 .reconfiguration
701 .as_ref()
702 .filter(|record| record.is_in_progress())
703 .cloned(),
704 ClusterVariant::Unmanaged => None,
705 };
706 let new_target = ReconfigurationTarget {
707 size: new_managed.size.clone(),
708 replication_factor: new_managed.replication_factor,
709 availability_zones: new_managed.availability_zones.clone(),
710 logging: new_managed.logging.clone(),
711 arrangement_compression: new_managed.arrangement_compression,
712 };
713 let unchanged = ReconfigurationDimensionsUnchanged {
714 size: matches!(options.size, Unchanged),
715 replication_factor: matches!(options.replication_factor, Unchanged),
716 availability_zones: matches!(options.availability_zones, Unchanged),
717 log_logging: matches!(options.introspection_debugging, Unchanged),
720 interval: matches!(options.introspection_interval, Unchanged),
721 arrangement_compression: matches!(options.arrangement_compression, Unchanged),
722 };
723 let target = fold_reconfiguration_target(
724 in_flight.as_ref().map(|r| &r.target),
725 new_target,
726 unchanged,
727 );
728
729 let role_id = session.role_metadata().current_role;
732 self.catalog.ensure_valid_replica_size(
733 &self
734 .catalog()
735 .get_role_allowed_cluster_sizes(&Some(role_id)),
736 &target.size,
737 false,
738 )?;
739 self.ensure_valid_azs(target.availability_zones.iter())?;
740 self.validate_reconfiguration_resource_limits(cluster_id, &target)?;
744
745 let now = self.now();
772 let deadline_from = |timeout: Duration| -> Timestamp {
773 now.saturating_add(u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX))
774 .into()
775 };
776 let (deadline, on_timeout) = match strategy {
777 AlterClusterPlanStrategy::None => match &in_flight {
778 Some(record) => (record.deadline, record.on_timeout),
779 None => (
780 deadline_from(
781 DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT
782 .get(self.catalog().system_config().dyncfgs()),
783 ),
784 OnTimeoutAction::Rollback,
785 ),
786 },
787 AlterClusterPlanStrategy::For(timeout) => {
788 (deadline_from(*timeout), OnTimeoutAction::Commit)
789 }
790 AlterClusterPlanStrategy::UntilReady {
791 timeout,
792 on_timeout,
793 } => (
794 deadline_from(*timeout),
795 on_timeout.unwrap_or(OnTimeoutAction::Rollback),
796 ),
797 };
798
799 let cluster = self.catalog.get_cluster(cluster_id);
808 let cluster_name = cluster.name().to_string();
809 let ClusterVariant::Managed(realized_now) = &cluster.config.variant else {
810 return Err(AdapterError::Internal(
811 "reshape_alter_cluster_managed requires a managed realized config".into(),
812 ));
813 };
814 let realized_target = realized_now.realized_reconfiguration_target();
815 let (status, audit) = if target.matches_realized_config(realized_now) {
819 (
820 ReconfigurationStatus::Cancelled,
821 ReconfigurationAudit::Cancelled,
822 )
823 } else {
824 (
825 ReconfigurationStatus::InProgress,
826 ReconfigurationAudit::Started,
827 )
828 };
829 let record = ReconfigurationState {
830 target: target.clone(),
831 deadline,
832 on_timeout,
833 status,
834 };
835
836 let mut realized = new_config.clone();
837 let ClusterVariant::Managed(realized_managed) = &mut realized.variant else {
838 return Err(AdapterError::Internal(
839 "reshape_alter_cluster_managed requires a managed target config".into(),
840 ));
841 };
842 realized_managed.apply_reconfiguration_target(realized_target);
843 realized_managed.reconfiguration = Some(record);
844
845 self.catalog_transact(
846 Some(session),
847 vec![Op::UpdateClusterConfig {
848 id: cluster_id,
849 name: cluster_name,
850 config: realized,
851 reconfiguration_audit: Some(audit),
852 burst_audit: None,
853 }],
854 )
855 .await?;
856
857 let background =
858 ENABLE_BACKGROUND_ALTER_CLUSTER.get(self.catalog().system_config().dyncfgs());
859 if background {
860 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
861 ObjectType::Cluster,
862 )));
863 }
864
865 Ok(StageResult::Immediate(Box::new(
869 ClusterStage::AwaitReconfiguration(AlterClusterAwaitReconfiguration {
870 validity,
871 cluster_id,
872 target,
873 }),
874 )))
875 }
876
877 fn await_reconfiguration_stage(
883 &self,
884 validity: PlanValidity,
885 cluster_id: ClusterId,
886 target: ReconfigurationTarget,
887 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
888 let Some(cluster) = self.catalog().try_get_cluster(cluster_id) else {
889 return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
892 ObjectType::Cluster,
893 )));
894 };
895 let record = match &cluster.config.variant {
896 ClusterVariant::Managed(managed) => managed.reconfiguration.clone(),
897 ClusterVariant::Unmanaged => None,
898 };
899
900 let realized_matches_target = match &cluster.config.variant {
901 ClusterVariant::Managed(managed) => target.matches_realized_config(managed),
902 ClusterVariant::Unmanaged => false,
903 };
904
905 match record {
906 None => {
907 if realized_matches_target {
910 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
911 ObjectType::Cluster,
912 )))
913 } else {
914 Err(AdapterError::AlterClusterTimeout)
915 }
916 }
917 Some(record) if !record.is_in_progress() => {
918 if matches!(
919 record.status,
920 ReconfigurationStatus::Finalized | ReconfigurationStatus::Cancelled
921 ) && realized_matches_target
922 {
923 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
924 ObjectType::Cluster,
925 )))
926 } else {
927 Err(AdapterError::AlterClusterTimeout)
928 }
929 }
930 Some(_) => {
931 let poll_duration = self
941 .catalog
942 .system_config()
943 .cluster_alter_check_ready_interval();
944 let span = Span::current();
945 Ok(StageResult::Handle(mz_ore::task::spawn(
946 || "Await Cluster Reconfiguration",
947 async move {
948 tokio::time::sleep(poll_duration).await;
949 Ok(Box::new(ClusterStage::AwaitReconfiguration(
950 AlterClusterAwaitReconfiguration {
951 validity,
952 cluster_id,
953 target,
954 },
955 )))
956 }
957 .instrument(span),
958 )))
959 }
960 }
961 }
962
963 async fn finalize_alter_cluster_stage(
964 &mut self,
965 session: &Session,
966 AlterClusterPlan {
967 id: cluster_id,
968 name: cluster_name,
969 ..
970 }: AlterClusterPlan,
971 new_config: ClusterVariantManaged,
972 workload_class: Option<String>,
973 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
974 let cluster = self.catalog.get_cluster(cluster_id);
975 let mut ops = vec![];
976
977 let remove_replicas = cluster
980 .replicas()
981 .filter_map(|r| {
982 if !r.config.location.pending() && !r.config.location.internal() {
983 Some(catalog::DropObjectInfo::ClusterReplica((
984 cluster_id.clone(),
985 r.replica_id,
986 ReplicaCreateDropReason::Manual,
987 )))
988 } else {
989 None
990 }
991 })
992 .collect();
993 ops.push(catalog::Op::DropObjects(remove_replicas));
994
995 let finalize_replicas: Vec<catalog::Op> = cluster
998 .replicas()
999 .filter_map(|r| {
1000 if r.config.location.pending() {
1001 let cluster_ident = match Ident::new(cluster.name.clone()) {
1002 Ok(id) => id,
1003 Err(err) => {
1004 return Some(Err(AdapterError::internal(
1005 "Unexpected error parsing cluster name",
1006 err,
1007 )));
1008 }
1009 };
1010 let replica_ident = match Ident::new(r.name.clone()) {
1011 Ok(id) => id,
1012 Err(err) => {
1013 return Some(Err(AdapterError::internal(
1014 "Unexpected error parsing replica name",
1015 err,
1016 )));
1017 }
1018 };
1019 Some(Ok((cluster_ident, replica_ident, r)))
1020 } else {
1021 None
1022 }
1023 })
1024 .collect::<Result<Vec<(Ident, Ident, &ClusterReplica)>, _>>()?
1027 .into_iter()
1028 .map(|(cluster_ident, replica_ident, replica)| {
1029 let mut new_replica_config = replica.config.clone();
1030 debug!("Promoting replica: {}", replica.name);
1031 match new_replica_config.location {
1032 mz_controller::clusters::ReplicaLocation::Managed(ManagedReplicaLocation {
1033 ref mut pending,
1034 ..
1035 }) => {
1036 *pending = false;
1037 }
1038 mz_controller::clusters::ReplicaLocation::Unmanaged(_) => {}
1039 }
1040
1041 let mut replica_ops = vec![];
1042 let to_name = replica.name.strip_suffix(PENDING_REPLICA_SUFFIX);
1043 if let Some(to_name) = to_name {
1044 replica_ops.push(catalog::Op::RenameClusterReplica {
1045 cluster_id: cluster_id.clone(),
1046 replica_id: replica.replica_id.to_owned(),
1047 name: QualifiedReplica {
1048 cluster: cluster_ident,
1049 replica: replica_ident,
1050 },
1051 to_name: to_name.to_owned(),
1052 });
1053 }
1054 replica_ops.push(catalog::Op::UpdateClusterReplicaConfig {
1055 cluster_id,
1056 replica_id: replica.replica_id.to_owned(),
1057 config: new_replica_config,
1058 });
1059 replica_ops
1060 })
1061 .flatten()
1062 .collect();
1063
1064 ops.extend(finalize_replicas);
1065
1066 let mut final_config = ClusterConfig {
1070 variant: ClusterVariant::Managed(new_config),
1071 workload_class: workload_class.clone(),
1072 };
1073 let reconfiguration_audit = cancel_carried_reconfiguration(&mut final_config);
1074 ops.push(Op::UpdateClusterConfig {
1075 id: cluster_id,
1076 name: cluster_name,
1077 config: final_config,
1078 reconfiguration_audit,
1079 burst_audit: None,
1080 });
1081 self.catalog_transact(Some(session), ops).await?;
1082 self.active_conns
1085 .get_mut(session.conn_id())
1086 .expect("There must be an active connection")
1087 .pending_cluster_alters
1088 .remove(&cluster_id);
1089
1090 Ok(StageResult::Response(ExecuteResponse::AlteredObject(
1091 ObjectType::Cluster,
1092 )))
1093 }
1094
1095 async fn check_if_pending_replicas_hydrated_stage(
1096 &mut self,
1097 session: &Session,
1098 plan: AlterClusterPlan,
1099 new_config: ClusterVariantManaged,
1100 workload_class: Option<String>,
1101 timeout_time: Instant,
1102 on_timeout: OnTimeoutAction,
1103 validity: PlanValidity,
1104 ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
1105 let cluster = self.catalog.get_cluster(plan.id);
1107 let pending_replicas = cluster
1108 .replicas()
1109 .filter_map(|r| {
1110 if r.config.location.pending() {
1111 Some(r.replica_id.clone())
1112 } else {
1113 None
1114 }
1115 })
1116 .collect_vec();
1117 if Instant::now() > timeout_time {
1119 match on_timeout {
1121 OnTimeoutAction::Rollback => {
1122 self.active_conns
1123 .get_mut(session.conn_id())
1124 .expect("There must be an active connection")
1125 .pending_cluster_alters
1126 .remove(&cluster.id);
1127 self.drop_reconfiguration_replicas(btreeset!(cluster.id))
1128 .await?;
1129 return Err(AdapterError::AlterClusterTimeout);
1130 }
1131 OnTimeoutAction::Commit => {
1132 let span = Span::current();
1133 let poll_duration = self
1134 .catalog
1135 .system_config()
1136 .cluster_alter_check_ready_interval()
1137 .clone();
1138 return Ok(StageResult::Handle(mz_ore::task::spawn(
1139 || "Finalize Alter Cluster",
1140 async move {
1141 tokio::time::sleep(poll_duration).await;
1142 let stage = ClusterStage::Finalize(AlterClusterFinalize {
1143 validity,
1144 plan,
1145 new_config,
1146 workload_class,
1147 });
1148 Ok(Box::new(stage))
1149 }
1150 .instrument(span),
1151 )));
1152 }
1153 }
1154 }
1155 let compute_hydrated_fut = self
1156 .controller
1157 .compute
1158 .collections_hydrated_for_replicas(cluster.id, pending_replicas.clone(), [].into())
1159 .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1160
1161 let storage_hydrated = self
1162 .controller
1163 .storage
1164 .collections_hydrated_on_replicas(
1165 Some(pending_replicas.clone()),
1166 &cluster.id,
1167 &[].into(),
1168 )
1169 .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1170
1171 let replicas_online = pending_replicas.iter().all(|replica_id| {
1174 let status = self
1175 .cluster_replica_statuses
1176 .try_get_cluster_replica_statuses(cluster.id, *replica_id)
1177 .map(ClusterReplicaStatuses::cluster_replica_status);
1178 matches!(status, Some(ClusterStatus::Online))
1179 });
1180
1181 let span = Span::current();
1182 Ok(StageResult::Handle(mz_ore::task::spawn(
1183 || "Alter Cluster: wait for hydrated",
1184 async move {
1185 let compute_hydrated = compute_hydrated_fut
1186 .await
1187 .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1188
1189 if compute_hydrated && storage_hydrated && replicas_online {
1190 Ok(Box::new(ClusterStage::Finalize(AlterClusterFinalize {
1192 validity,
1193 plan,
1194 new_config: new_config.clone(),
1195 workload_class: workload_class.clone(),
1196 })))
1197 } else {
1198 tokio::time::sleep(Duration::from_secs(1)).await;
1200 let stage = ClusterStage::WaitForHydrated(AlterClusterWaitForHydrated {
1201 validity,
1202 plan,
1203 new_config,
1204 workload_class,
1205 timeout_time,
1206 on_timeout,
1207 });
1208 Ok(Box::new(stage))
1209 }
1210 }
1211 .instrument(span),
1212 )))
1213 }
1214
1215 #[mz_ore::instrument(level = "debug")]
1216 pub(crate) async fn sequence_create_cluster(
1217 &mut self,
1218 session: &Session,
1219 CreateClusterPlan {
1220 name,
1221 variant,
1222 workload_class,
1223 if_not_exists,
1224 }: CreateClusterPlan,
1225 ) -> Result<ExecuteResponse, AdapterError> {
1226 tracing::debug!("sequence_create_cluster");
1227
1228 let id_ts = self.get_catalog_write_ts().await;
1229 let id = self.catalog().allocate_user_cluster_id(id_ts).await?;
1230 let introspection_sources = BUILTINS::logs().collect();
1235 let cluster_variant = match &variant {
1236 CreateClusterVariant::Managed(plan) => {
1237 let logging = if let Some(config) = plan.compute.introspection {
1238 ReplicaLogging {
1239 log_logging: config.debugging,
1240 interval: Some(config.interval),
1241 }
1242 } else {
1243 ReplicaLogging::default()
1244 };
1245 ClusterVariant::Managed(ClusterVariantManaged {
1246 size: plan.size.clone(),
1247 availability_zones: plan.availability_zones.clone(),
1248 logging,
1249 arrangement_compression: plan.compute.arrangement_compression,
1250 replication_factor: plan.replication_factor,
1251 optimizer_feature_overrides: plan.optimizer_feature_overrides.clone(),
1252 schedule: plan.schedule.clone(),
1253 auto_scaling_strategy: plan.auto_scaling_strategy.clone(),
1254 reconfiguration: None,
1255 burst: None,
1256 })
1257 }
1258 CreateClusterVariant::Unmanaged(_) => ClusterVariant::Unmanaged,
1259 };
1260 let config = ClusterConfig {
1261 variant: cluster_variant,
1262 workload_class,
1263 };
1264 let ops = vec![catalog::Op::CreateCluster {
1265 id,
1266 name: name.clone(),
1267 introspection_sources,
1268 owner_id: *session.current_role_id(),
1269 config,
1270 }];
1271
1272 match variant {
1273 CreateClusterVariant::Managed(plan) => {
1274 self.sequence_create_managed_cluster(session, plan, id, name.clone(), ops)
1275 .await
1276 }
1277 CreateClusterVariant::Unmanaged(plan) => {
1278 self.sequence_create_unmanaged_cluster(session, plan, id, name.clone(), ops)
1279 .await
1280 }
1281 }
1282 .or_else(|err| match err {
1283 AdapterError::Catalog(mz_catalog::memory::error::Error {
1284 kind: ErrorKind::Sql(CatalogError::ClusterAlreadyExists(_)),
1285 }) if if_not_exists => {
1286 session.add_notice(AdapterNotice::ObjectAlreadyExists {
1287 name,
1288 ty: "cluster",
1289 });
1290 Ok(ExecuteResponse::CreatedCluster)
1291 }
1292 err => Err(err),
1293 })
1294 }
1295
1296 #[mz_ore::instrument(level = "debug")]
1297 async fn sequence_create_managed_cluster(
1298 &mut self,
1299 session: &Session,
1300 CreateClusterManagedPlan {
1301 availability_zones,
1302 compute,
1303 replication_factor,
1304 size,
1305 optimizer_feature_overrides: _,
1306 schedule: _,
1307 auto_scaling_strategy,
1308 }: CreateClusterManagedPlan,
1309 cluster_id: ClusterId,
1310 cluster_name: String,
1311 mut ops: Vec<catalog::Op>,
1312 ) -> Result<ExecuteResponse, AdapterError> {
1313 tracing::debug!("sequence_create_managed_cluster");
1314
1315 self.ensure_valid_azs(availability_zones.iter())?;
1316
1317 let role_id = session.role_metadata().current_role;
1318 self.catalog.ensure_valid_replica_size(
1319 &self
1320 .catalog()
1321 .get_role_allowed_cluster_sizes(&Some(role_id)),
1322 &size,
1323 false,
1324 )?;
1325 if let Some(on_hydration) = auto_scaling_strategy
1331 .as_ref()
1332 .and_then(|strategy| strategy.on_hydration.as_ref())
1333 {
1334 self.catalog.ensure_valid_replica_size(
1335 &self
1336 .catalog()
1337 .get_role_allowed_cluster_sizes(&Some(role_id)),
1338 &on_hydration.hydration_size,
1339 false,
1340 )?;
1341 }
1342
1343 if cluster_id.is_user() {
1348 self.validate_resource_limit(
1349 0,
1350 i64::from(replication_factor),
1351 SystemVars::max_replicas_per_cluster,
1352 "cluster replica",
1353 MAX_REPLICAS_PER_CLUSTER.name(),
1354 )?;
1355 }
1356
1357 let id_ts = self.get_catalog_write_ts().await;
1363 let replica_ids = self
1364 .catalog()
1365 .allocate_replica_ids(cluster_id, u64::from(replication_factor), id_ts)
1366 .await?;
1367
1368 let cluster_ctx = ClusterScopeContext {
1369 id: cluster_id.to_string(),
1370 name: cluster_name.clone(),
1371 is_builtin: cluster_id.is_system(),
1372 };
1373
1374 let mut replica_ctxs = Vec::new();
1375 for (replica_id, replica_name) in replica_ids
1376 .into_iter()
1377 .zip_eq((0..replication_factor).map(managed_cluster_replica_name))
1378 {
1379 let size_family = self.create_managed_cluster_replica_op(
1380 cluster_id,
1381 replica_id,
1382 replica_name.clone(),
1383 &compute,
1384 &size,
1385 &mut ops,
1386 if availability_zones.is_empty() {
1387 None
1388 } else {
1389 Some(availability_zones.as_ref())
1390 },
1391 false,
1392 *session.current_role_id(),
1393 ReplicaCreateDropReason::Manual,
1394 )?;
1395 replica_ctxs.push(ReplicaEvalContext {
1396 cluster_id,
1397 replica_id,
1398 cluster: cluster_ctx.clone(),
1399 replica: ReplicaScopeContext {
1400 id: replica_id.to_string(),
1401 name: replica_name,
1402 is_builtin: cluster_id.is_system(),
1403 size: size.clone(),
1404 size_family,
1405 cluster_id: cluster_id.to_string(),
1406 cluster_name: cluster_name.clone(),
1407 },
1408 });
1409 }
1410
1411 let cluster_eval = ClusterEvalContext {
1418 cluster_id,
1419 cluster: cluster_ctx,
1420 };
1421 if let Some(scoped_op) = self.scoped_overrides_create_op(&[cluster_eval], &replica_ctxs) {
1422 ops.push(scoped_op);
1423 }
1424
1425 self.catalog_transact(Some(session), ops).await?;
1426
1427 Ok(ExecuteResponse::CreatedCluster)
1428 }
1429
1430 fn create_managed_cluster_replica_op(
1431 &self,
1432 cluster_id: ClusterId,
1433 replica_id: ReplicaId,
1434 name: String,
1435 compute: &mz_sql::plan::ComputeReplicaConfig,
1436 size: &String,
1437 ops: &mut Vec<Op>,
1438 azs: Option<&[String]>,
1439 pending: bool,
1440 owner_id: RoleId,
1441 reason: ReplicaCreateDropReason,
1442 ) -> Result<String, AdapterError> {
1443 let location = mz_catalog::durable::ReplicaLocation::Managed {
1444 availability_zones: Vec::new(),
1447 billed_as: None,
1448 internal: false,
1449 size: size.clone(),
1450 pending,
1451 };
1452
1453 let logging = if let Some(config) = compute.introspection {
1454 ReplicaLogging {
1455 log_logging: config.debugging,
1456 interval: Some(config.interval),
1457 }
1458 } else {
1459 ReplicaLogging::default()
1460 };
1461
1462 let config = ReplicaConfig {
1463 location: self.catalog().concretize_replica_location(
1464 location,
1465 &self
1466 .catalog()
1467 .get_role_allowed_cluster_sizes(&Some(owner_id)),
1468 azs,
1469 false,
1470 )?,
1471 compute: ComputeReplicaConfig {
1472 logging,
1473 arrangement_compression: compute.arrangement_compression,
1474 },
1475 };
1476
1477 let size_family = match &config.location {
1483 ReplicaLocation::Managed(location) => location.allocation.family().to_string(),
1484 ReplicaLocation::Unmanaged(_) => {
1486 unreachable!("managed cluster replica has a managed location")
1487 }
1488 };
1489
1490 ops.push(catalog::Op::CreateClusterReplica {
1491 cluster_id,
1492 replica_id,
1493 name,
1494 config,
1495 owner_id,
1496 reason,
1497 });
1498 Ok(size_family)
1499 }
1500
1501 fn ensure_valid_azs<'a, I: IntoIterator<Item = &'a String>>(
1502 &self,
1503 azs: I,
1504 ) -> Result<(), AdapterError> {
1505 let cat_azs = self.catalog().state().availability_zones();
1506 for az in azs.into_iter() {
1507 if !cat_azs.contains(az) {
1508 return Err(AdapterError::InvalidClusterReplicaAz {
1509 az: az.to_string(),
1510 expected: cat_azs.to_vec(),
1511 });
1512 }
1513 }
1514 Ok(())
1515 }
1516
1517 #[mz_ore::instrument(level = "debug")]
1518 async fn sequence_create_unmanaged_cluster(
1519 &mut self,
1520 session: &Session,
1521 CreateClusterUnmanagedPlan { replicas }: CreateClusterUnmanagedPlan,
1522 id: ClusterId,
1523 cluster_name: String,
1524 mut ops: Vec<catalog::Op>,
1525 ) -> Result<ExecuteResponse, AdapterError> {
1526 tracing::debug!("sequence_create_unmanaged_cluster");
1527
1528 self.ensure_valid_azs(replicas.iter().filter_map(|(_, r)| {
1529 if let mz_sql::plan::ReplicaConfig::Orchestrated {
1530 availability_zone: Some(az),
1531 ..
1532 } = &r
1533 {
1534 Some(az)
1535 } else {
1536 None
1537 }
1538 }))?;
1539
1540 if id.is_user() {
1545 self.validate_resource_limit(
1546 0,
1547 i64::try_from(replicas.len()).unwrap_or(i64::MAX),
1548 SystemVars::max_replicas_per_cluster,
1549 "cluster replica",
1550 MAX_REPLICAS_PER_CLUSTER.name(),
1551 )?;
1552 }
1553
1554 let id_ts = self.get_catalog_write_ts().await;
1560 let replica_ids = self
1561 .catalog()
1562 .allocate_replica_ids(id, u64::cast_from(replicas.len()), id_ts)
1563 .await?;
1564
1565 let cluster_ctx = ClusterScopeContext {
1566 id: id.to_string(),
1567 name: cluster_name.clone(),
1568 is_builtin: id.is_system(),
1569 };
1570 let mut replica_ctxs = Vec::new();
1571
1572 for (replica_id, (replica_name, replica_config)) in replica_ids.into_iter().zip_eq(replicas)
1573 {
1574 let (compute, location) = match replica_config {
1577 mz_sql::plan::ReplicaConfig::Unorchestrated {
1578 storagectl_addrs,
1579 computectl_addrs,
1580 compute,
1581 } => {
1582 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1583 storagectl_addrs,
1584 computectl_addrs,
1585 };
1586 (compute, location)
1587 }
1588 mz_sql::plan::ReplicaConfig::Orchestrated {
1589 availability_zone,
1590 billed_as,
1591 compute,
1592 internal,
1593 size,
1594 } => {
1595 if !session.user().is_internal() && (internal || billed_as.is_some()) {
1597 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1598 }
1599 if billed_as.is_some() && !internal {
1601 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1602 }
1603
1604 let location = mz_catalog::durable::ReplicaLocation::Managed {
1605 availability_zones: availability_zone.into_iter().collect(),
1608 billed_as,
1609 internal,
1610 size: size.clone(),
1611 pending: false,
1612 };
1613 (compute, location)
1614 }
1615 };
1616
1617 let logging = if let Some(config) = compute.introspection {
1618 ReplicaLogging {
1619 log_logging: config.debugging,
1620 interval: Some(config.interval),
1621 }
1622 } else {
1623 ReplicaLogging::default()
1624 };
1625
1626 let role_id = session.role_metadata().current_role;
1627 let config = ReplicaConfig {
1628 location: self.catalog().concretize_replica_location(
1629 location,
1630 &self
1631 .catalog()
1632 .get_role_allowed_cluster_sizes(&Some(role_id)),
1633 None,
1634 false,
1635 )?,
1636 compute: ComputeReplicaConfig {
1637 logging,
1638 arrangement_compression: compute.arrangement_compression,
1639 },
1640 };
1641
1642 if let ReplicaLocation::Managed(location) = &config.location {
1645 replica_ctxs.push(ReplicaEvalContext {
1646 cluster_id: id,
1647 replica_id,
1648 cluster: cluster_ctx.clone(),
1649 replica: ReplicaScopeContext {
1650 id: replica_id.to_string(),
1651 name: replica_name.clone(),
1652 is_builtin: id.is_system(),
1653 size: location.size.clone(),
1654 size_family: location.allocation.family().to_string(),
1655 cluster_id: id.to_string(),
1656 cluster_name: cluster_name.clone(),
1657 },
1658 });
1659 }
1660
1661 ops.push(catalog::Op::CreateClusterReplica {
1662 cluster_id: id,
1663 replica_id,
1664 name: replica_name.clone(),
1665 config,
1666 owner_id: *session.current_role_id(),
1667 reason: ReplicaCreateDropReason::Manual,
1668 });
1669 }
1670
1671 let cluster_eval = ClusterEvalContext {
1674 cluster_id: id,
1675 cluster: cluster_ctx,
1676 };
1677 if let Some(scoped_op) = self.scoped_overrides_create_op(&[cluster_eval], &replica_ctxs) {
1678 ops.push(scoped_op);
1679 }
1680
1681 self.catalog_transact(Some(session), ops).await?;
1682
1683 Ok(ExecuteResponse::CreatedCluster)
1684 }
1685
1686 fn single_replica_source_names(&self, cluster: &Cluster) -> Vec<String> {
1690 cluster
1691 .bound_objects
1692 .iter()
1693 .filter_map(|id| {
1694 let entry = self.catalog().get_entry(id);
1695 let single_replica =
1696 entry
1697 .source()
1698 .is_some_and(|source| match &source.data_source {
1699 DataSourceDesc::Ingestion { desc, .. }
1700 | DataSourceDesc::OldSyntaxIngestion { desc, .. } => {
1701 desc.connection.prefers_single_replica()
1702 }
1703 _ => false,
1704 });
1705 single_replica.then(|| {
1706 self.catalog()
1707 .resolve_full_name(entry.name(), None)
1708 .to_string()
1709 })
1710 })
1711 .collect()
1712 }
1713
1714 fn notice_relevant_replica_count(&self, cluster: &Cluster) -> usize {
1728 match &cluster.config.variant {
1729 ClusterVariant::Managed(managed) => {
1730 let replication_factor = managed
1731 .reconfiguration
1732 .as_ref()
1733 .filter(|record| record.is_in_progress())
1734 .map_or(managed.replication_factor, |record| {
1735 record.target.replication_factor
1736 });
1737 let manual_replicas = cluster
1738 .replicas()
1739 .filter(|r| {
1740 r.config.location.internal() || r.config.location.billed_as().is_some()
1741 })
1742 .count();
1743 usize::cast_from(replication_factor) + manual_replicas
1744 }
1745 ClusterVariant::Unmanaged => cluster.replicas().count(),
1746 }
1747 }
1748
1749 pub(crate) fn notify_single_replica_sources(
1758 &self,
1759 session: &Session,
1760 cluster: &Cluster,
1761 creating_source: Option<&QualifiedItemName>,
1762 ) {
1763 if self.notice_relevant_replica_count(cluster) <= 1 {
1764 return;
1765 }
1766 let mut sources = self.single_replica_source_names(cluster);
1767 if let Some(name) = creating_source {
1768 let full_name = self.catalog().resolve_full_name(name, None).to_string();
1769 if !sources.contains(&full_name) {
1770 sources.push(full_name);
1771 }
1772 }
1773 if !sources.is_empty() {
1774 session.add_notice(AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster {
1775 cluster: cluster.name.clone(),
1776 sources,
1777 });
1778 }
1779 }
1780
1781 #[mz_ore::instrument(level = "debug")]
1782 pub(crate) async fn sequence_create_cluster_replica(
1783 &mut self,
1784 session: &Session,
1785 CreateClusterReplicaPlan {
1786 name,
1787 cluster_id,
1788 config,
1789 if_not_exists,
1790 }: CreateClusterReplicaPlan,
1791 ) -> Result<ExecuteResponse, AdapterError> {
1792 let (compute, location) = match config {
1794 mz_sql::plan::ReplicaConfig::Unorchestrated {
1795 storagectl_addrs,
1796 computectl_addrs,
1797 compute,
1798 } => {
1799 let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1800 storagectl_addrs,
1801 computectl_addrs,
1802 };
1803 (compute, location)
1804 }
1805 mz_sql::plan::ReplicaConfig::Orchestrated {
1806 availability_zone,
1807 billed_as,
1808 compute,
1809 internal,
1810 size,
1811 } => {
1812 let availability_zone = match availability_zone {
1813 Some(az) => {
1814 self.ensure_valid_azs([&az])?;
1815 Some(az)
1816 }
1817 None => None,
1818 };
1819 let location = mz_catalog::durable::ReplicaLocation::Managed {
1820 availability_zones: availability_zone.into_iter().collect(),
1823 billed_as,
1824 internal,
1825 size,
1826 pending: false,
1827 };
1828 (compute, location)
1829 }
1830 };
1831
1832 let logging = if let Some(config) = compute.introspection {
1833 ReplicaLogging {
1834 log_logging: config.debugging,
1835 interval: Some(config.interval),
1836 }
1837 } else {
1838 ReplicaLogging::default()
1839 };
1840
1841 let role_id = session.role_metadata().current_role;
1842 let config = ReplicaConfig {
1843 location: self.catalog().concretize_replica_location(
1844 location,
1845 &self
1846 .catalog()
1847 .get_role_allowed_cluster_sizes(&Some(role_id)),
1848 None,
1851 false,
1852 )?,
1853 compute: ComputeReplicaConfig {
1854 logging,
1855 arrangement_compression: compute.arrangement_compression,
1856 },
1857 };
1858
1859 let cluster = self.catalog().get_cluster(cluster_id);
1860
1861 if let ReplicaLocation::Managed(ManagedReplicaLocation {
1862 internal,
1863 billed_as,
1864 ..
1865 }) = &config.location
1866 {
1867 if !session.user().is_internal() && (*internal || billed_as.is_some()) {
1869 coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1870 }
1871 if cluster.is_managed() && !*internal {
1873 coord_bail!("must specify INTERNAL when creating a replica in a managed cluster");
1874 }
1875 if billed_as.is_some() && !*internal {
1877 coord_bail!("must specify INTERNAL when specifying BILLED AS");
1878 }
1879 }
1880
1881 let owner_id = cluster.owner_id();
1884
1885 let cluster_name = cluster.name.clone();
1886 let is_builtin = cluster_id.is_system();
1887 let qualified_name = format!("{cluster_name}.{name}");
1890
1891 let id_ts = self.get_catalog_write_ts().await;
1899 let replica_id = self
1900 .catalog()
1901 .allocate_replica_ids(cluster_id, 1, id_ts)
1902 .await?
1903 .into_element();
1904
1905 let replica_ctx = match &config.location {
1908 ReplicaLocation::Managed(location) => Some(ReplicaEvalContext {
1909 cluster_id,
1910 replica_id,
1911 cluster: ClusterScopeContext {
1912 id: cluster_id.to_string(),
1913 name: cluster_name.clone(),
1914 is_builtin,
1915 },
1916 replica: ReplicaScopeContext {
1917 id: replica_id.to_string(),
1918 name: name.to_string(),
1919 is_builtin,
1920 size: location.size.clone(),
1921 size_family: location.allocation.family().to_string(),
1922 cluster_id: cluster_id.to_string(),
1923 cluster_name,
1924 },
1925 }),
1926 ReplicaLocation::Unmanaged(_) => None,
1927 };
1928
1929 let mut ops = vec![catalog::Op::CreateClusterReplica {
1930 cluster_id,
1931 replica_id,
1932 name: name.clone(),
1933 config,
1934 owner_id,
1935 reason: ReplicaCreateDropReason::Manual,
1936 }];
1937
1938 if let Some(replica_ctx) = replica_ctx {
1942 if let Some(scoped_op) = self.scoped_overrides_create_op(&[], &[replica_ctx]) {
1943 ops.push(scoped_op);
1944 }
1945 }
1946
1947 match self.catalog_transact(Some(session), ops).await {
1948 Ok(()) => {
1949 self.notify_single_replica_sources(
1952 session,
1953 self.catalog().get_cluster(cluster_id),
1954 None,
1955 );
1956 Ok(ExecuteResponse::CreatedClusterReplica)
1957 }
1958 Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
1959 kind: ErrorKind::Sql(CatalogError::DuplicateReplica(_, _)),
1960 })) if if_not_exists => {
1961 session.add_notice(AdapterNotice::ObjectAlreadyExists {
1962 name: qualified_name,
1963 ty: "cluster replica",
1964 });
1965 Ok(ExecuteResponse::CreatedClusterReplica)
1966 }
1967 Err(err) => Err(err),
1968 }
1969 }
1970
1971 pub(crate) async fn sequence_alter_cluster_managed_to_managed(
1980 &mut self,
1981 session: Option<&Session>,
1982 cluster_id: ClusterId,
1983 new_config: ClusterConfig,
1984 reason: ReplicaCreateDropReason,
1985 strategy: AlterClusterPlanStrategy,
1986 ) -> Result<NeedsFinalization, AdapterError> {
1987 let cluster = self.catalog.get_cluster(cluster_id);
1988 let name = cluster.name().to_string();
1989 let owner_id = cluster.owner_id();
1990
1991 let mut ops = vec![];
1992 let mut finalization_needed = NeedsFinalization::No;
1993
1994 let ClusterVariant::Managed(ClusterVariantManaged {
1995 size,
1996 availability_zones,
1997 logging,
1998 arrangement_compression,
1999 replication_factor,
2000 optimizer_feature_overrides: _,
2001 schedule: _,
2002 auto_scaling_strategy,
2003 reconfiguration,
2004 burst: _,
2005 }) = &cluster.config.variant
2006 else {
2007 panic!("expected existing managed cluster config");
2008 };
2009 let size = size.clone();
2013 let availability_zones = availability_zones.clone();
2014 let logging = logging.clone();
2015 let arrangement_compression = *arrangement_compression;
2016 let replication_factor = *replication_factor;
2017 let ClusterVariant::Managed(new_managed) = &new_config.variant else {
2018 panic!("expected new managed cluster config");
2019 };
2020 let ClusterVariantManaged {
2021 size: new_size,
2022 replication_factor: new_replication_factor,
2023 availability_zones: new_availability_zones,
2024 logging: new_logging,
2025 arrangement_compression: new_arrangement_compression,
2026 optimizer_feature_overrides: _,
2027 schedule: _,
2028 auto_scaling_strategy: new_auto_scaling_strategy,
2029 reconfiguration: _,
2030 burst: _,
2031 } = new_managed;
2032
2033 let role_id = session.map(|s| s.role_metadata().current_role);
2034 self.catalog.ensure_valid_replica_size(
2035 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
2036 new_size,
2037 false,
2038 )?;
2039 if new_auto_scaling_strategy != auto_scaling_strategy {
2045 if let Some(on_hydration) = new_auto_scaling_strategy
2046 .as_ref()
2047 .and_then(|strategy| strategy.on_hydration.as_ref())
2048 {
2049 self.catalog.ensure_valid_replica_size(
2050 &self.catalog().get_role_allowed_cluster_sizes(&role_id),
2051 &on_hydration.hydration_size,
2052 false,
2053 )?;
2054 if reconfiguration.as_ref().is_some_and(|record| {
2061 record.is_in_progress() && record.target.size == on_hydration.hydration_size
2062 }) {
2063 coord_bail!(
2064 "HYDRATION SIZE must differ from the target SIZE \
2065 ('{}') of the in-progress cluster resize",
2066 on_hydration.hydration_size
2067 );
2068 }
2069 }
2070 }
2071
2072 if cluster.replicas().any(|r| r.config.location.pending()) {
2074 return Err(AlterClusterWhilePendingReplicas);
2075 }
2076
2077 let replica_id_by_name: BTreeMap<String, ReplicaId> = cluster
2080 .replicas()
2081 .map(|r| (r.name.clone(), r.replica_id))
2082 .collect();
2083 let owned_replica_ids: Vec<ReplicaId> = cluster
2088 .replicas()
2089 .filter(|r| {
2090 !r.config.location.internal()
2091 && r.config.location.billed_as().is_none()
2092 && !r.config.location.pending()
2093 })
2094 .map(|r| r.replica_id)
2095 .collect();
2096
2097 let compute = mz_sql::plan::ComputeReplicaConfig {
2098 introspection: new_logging
2099 .interval
2100 .map(|interval| ComputeReplicaIntrospectionConfig {
2101 debugging: new_logging.log_logging,
2102 interval,
2103 }),
2104 arrangement_compression: *new_arrangement_compression,
2105 };
2106
2107 if *new_replication_factor > replication_factor {
2112 if cluster_id.is_user() {
2113 self.validate_resource_limit(
2114 usize::cast_from(replication_factor),
2115 i64::from(*new_replication_factor) - i64::from(replication_factor),
2116 SystemVars::max_replicas_per_cluster,
2117 "cluster replica",
2118 MAX_REPLICAS_PER_CLUSTER.name(),
2119 )?;
2120 }
2121 }
2122
2123 let controller_owns = true;
2137
2138 let config_changed = new_managed.replica_config_shape()
2145 != ManagedReplicaConfigShape::new(
2146 &size,
2147 &availability_zones,
2148 &logging,
2149 arrangement_compression,
2150 );
2151 let needed_replica_ids = if controller_owns {
2152 0
2153 } else if config_changed {
2154 *new_replication_factor
2155 } else if *new_replication_factor > replication_factor {
2156 *new_replication_factor - replication_factor
2157 } else {
2158 0
2159 };
2160 let mut new_replica_ids = if needed_replica_ids > 0 {
2170 let id_ts = self.get_catalog_write_ts().await;
2171 let ids = self
2172 .catalog()
2173 .allocate_replica_ids(cluster_id, u64::from(needed_replica_ids), id_ts)
2174 .await?;
2175 ids.into_iter()
2176 } else {
2177 Vec::<ReplicaId>::new().into_iter()
2178 };
2179
2180 let cluster_ctx = ClusterScopeContext {
2188 id: cluster_id.to_string(),
2189 name: name.clone(),
2190 is_builtin: cluster_id.is_system(),
2191 };
2192 let mut replica_ctxs = Vec::new();
2193
2194 if controller_owns {
2195 if config_changed {
2201 self.ensure_valid_azs(new_availability_zones.iter())?;
2202 }
2203 } else if config_changed {
2204 self.ensure_valid_azs(new_availability_zones.iter())?;
2205 match strategy {
2209 AlterClusterPlanStrategy::None => {
2210 let replica_ids_and_reasons = owned_replica_ids
2218 .iter()
2219 .map(|replica_id| {
2220 catalog::DropObjectInfo::ClusterReplica((
2221 cluster_id,
2222 *replica_id,
2223 reason.clone(),
2224 ))
2225 })
2226 .collect();
2227 ops.push(catalog::Op::DropObjects(replica_ids_and_reasons));
2228 for replica_name in
2229 (0..*new_replication_factor).map(managed_cluster_replica_name)
2230 {
2231 let replica_id = new_replica_ids
2234 .next()
2235 .expect("pre-allocated enough replica ids");
2236 let size_family = self.create_managed_cluster_replica_op(
2237 cluster_id,
2238 replica_id,
2239 replica_name.clone(),
2240 &compute,
2241 new_size,
2242 &mut ops,
2243 Some(new_availability_zones.as_ref()),
2244 false,
2245 owner_id,
2246 reason.clone(),
2247 )?;
2248 replica_ctxs.push(ReplicaEvalContext {
2249 cluster_id,
2250 replica_id,
2251 cluster: cluster_ctx.clone(),
2252 replica: ReplicaScopeContext {
2253 id: replica_id.to_string(),
2254 name: replica_name,
2255 is_builtin: cluster_id.is_system(),
2256 size: new_size.clone(),
2257 size_family,
2258 cluster_id: cluster_id.to_string(),
2259 cluster_name: cluster_ctx.name.clone(),
2260 },
2261 });
2262 }
2263 }
2264 AlterClusterPlanStrategy::For(_) | AlterClusterPlanStrategy::UntilReady { .. } => {
2265 for replica_name in
2266 (0..*new_replication_factor).map(managed_cluster_replica_name)
2267 {
2268 let replica_name = format!("{replica_name}{PENDING_REPLICA_SUFFIX}");
2269 let replica_id = new_replica_ids
2270 .next()
2271 .expect("pre-allocated enough replica ids");
2272 let size_family = self.create_managed_cluster_replica_op(
2273 cluster_id,
2274 replica_id,
2275 replica_name.clone(),
2276 &compute,
2277 new_size,
2278 &mut ops,
2279 Some(new_availability_zones.as_ref()),
2280 true,
2281 owner_id,
2282 reason.clone(),
2283 )?;
2284 replica_ctxs.push(ReplicaEvalContext {
2285 cluster_id,
2286 replica_id,
2287 cluster: cluster_ctx.clone(),
2288 replica: ReplicaScopeContext {
2289 id: replica_id.to_string(),
2290 name: replica_name,
2291 is_builtin: cluster_id.is_system(),
2292 size: new_size.clone(),
2293 size_family,
2294 cluster_id: cluster_id.to_string(),
2295 cluster_name: cluster_ctx.name.clone(),
2296 },
2297 });
2298 }
2299 finalization_needed = NeedsFinalization::Yes;
2300 }
2301 }
2302 } else if *new_replication_factor < replication_factor {
2303 let replica_ids = (*new_replication_factor..replication_factor)
2305 .map(managed_cluster_replica_name)
2306 .filter_map(|name| replica_id_by_name.get(&name).copied())
2307 .map(|replica_id| {
2308 catalog::DropObjectInfo::ClusterReplica((
2309 cluster_id,
2310 replica_id,
2311 reason.clone(),
2312 ))
2313 })
2314 .collect();
2315 ops.push(catalog::Op::DropObjects(replica_ids));
2316 } else if *new_replication_factor > replication_factor {
2317 for replica_name in
2319 (replication_factor..*new_replication_factor).map(managed_cluster_replica_name)
2320 {
2321 let replica_id = new_replica_ids
2322 .next()
2323 .expect("pre-allocated enough replica ids");
2324 let size_family = self.create_managed_cluster_replica_op(
2325 cluster_id,
2326 replica_id,
2327 replica_name.clone(),
2328 &compute,
2329 new_size,
2330 &mut ops,
2331 Some(new_availability_zones.as_ref()),
2334 false,
2335 owner_id,
2336 reason.clone(),
2337 )?;
2338 replica_ctxs.push(ReplicaEvalContext {
2339 cluster_id,
2340 replica_id,
2341 cluster: cluster_ctx.clone(),
2342 replica: ReplicaScopeContext {
2343 id: replica_id.to_string(),
2344 name: replica_name,
2345 is_builtin: cluster_id.is_system(),
2346 size: new_size.clone(),
2347 size_family,
2348 cluster_id: cluster_id.to_string(),
2349 cluster_name: cluster_ctx.name.clone(),
2350 },
2351 });
2352 }
2353 }
2354
2355 match finalization_needed {
2360 NeedsFinalization::No => {
2361 let mut new_config = new_config;
2362 let reconfiguration_audit = if controller_owns {
2363 None
2364 } else {
2365 cancel_carried_reconfiguration(&mut new_config)
2366 };
2367 ops.push(catalog::Op::UpdateClusterConfig {
2368 id: cluster_id,
2369 name: name.clone(),
2370 config: new_config,
2371 reconfiguration_audit,
2372 burst_audit: None,
2373 });
2374 }
2375 NeedsFinalization::Yes => {}
2376 }
2377
2378 if let Some(scoped_op) = self.scoped_overrides_create_op(&[], &replica_ctxs) {
2385 ops.push(scoped_op);
2386 }
2387
2388 self.catalog_transact(session, ops).await?;
2389 Ok(finalization_needed)
2390 }
2391
2392 async fn sequence_alter_cluster_unmanaged_to_managed(
2396 &mut self,
2397 session: &Session,
2398 cluster_id: ClusterId,
2399 mut new_config: ClusterConfig,
2400 options: PlanClusterOption,
2401 ) -> Result<(), AdapterError> {
2402 let cluster = self.catalog.get_cluster(cluster_id);
2403 let cluster_name = cluster.name().to_string();
2404
2405 let ClusterVariant::Managed(ClusterVariantManaged {
2406 size: new_size,
2407 replication_factor: new_replication_factor,
2408 availability_zones: new_availability_zones,
2409 logging: _,
2410 arrangement_compression: _,
2411 optimizer_feature_overrides: _,
2412 schedule: _,
2413 auto_scaling_strategy: _,
2414 reconfiguration: _,
2415 burst: _,
2416 }) = &mut new_config.variant
2417 else {
2418 panic!("expected new managed cluster config");
2419 };
2420
2421 let user_replica_count = cluster
2423 .user_replicas()
2424 .count()
2425 .try_into()
2426 .expect("must_fit");
2427 match options.replication_factor {
2428 AlterOptionParameter::Set(_) => {
2429 if user_replica_count != *new_replication_factor {
2431 coord_bail!(
2432 "REPLICATION FACTOR {new_replication_factor} does not match number of replicas ({user_replica_count})"
2433 );
2434 }
2435 }
2436 _ => {
2437 *new_replication_factor = user_replica_count;
2438 }
2439 }
2440
2441 let mut names = BTreeSet::new();
2442 let mut sizes = BTreeSet::new();
2443
2444 self.ensure_valid_azs(new_availability_zones.iter())?;
2445
2446 for replica in cluster.user_replicas() {
2448 names.insert(replica.name.clone());
2449 match &replica.config.location {
2450 ReplicaLocation::Unmanaged(_) => coord_bail!(
2451 "Cannot convert unmanaged cluster with unmanaged replicas to managed cluster"
2452 ),
2453 ReplicaLocation::Managed(location) => {
2454 sizes.insert(location.size.clone());
2455
2456 for az in &location.availability_zones {
2460 if !new_availability_zones.contains(az) {
2461 coord_bail!(
2462 "unmanaged replica has availability zone {az} which is not \
2463 in managed {new_availability_zones:?}"
2464 )
2465 }
2466 }
2467 }
2468 }
2469 }
2470
2471 if sizes.is_empty() {
2472 assert!(
2473 cluster.user_replicas().next().is_none(),
2474 "Cluster should not have replicas"
2475 );
2476 match &options.size {
2478 AlterOptionParameter::Reset | AlterOptionParameter::Unchanged => {
2479 coord_bail!("Missing SIZE for empty cluster")
2480 }
2481 AlterOptionParameter::Set(_) => {} }
2483 } else if sizes.len() == 1 {
2484 let size = sizes.into_iter().next().expect("must exist");
2485 match &options.size {
2486 AlterOptionParameter::Set(sz) if *sz != size => {
2487 coord_bail!("Cluster replicas of size {size} do not match expected SIZE {sz}");
2488 }
2489 _ => *new_size = size,
2490 }
2491 } else {
2492 let formatted = sizes
2493 .iter()
2494 .map(String::as_str)
2495 .collect::<Vec<_>>()
2496 .join(", ");
2497 coord_bail!(
2498 "Cannot convert unmanaged cluster to managed, non-unique replica sizes: {formatted}"
2499 );
2500 }
2501
2502 for i in 0..*new_replication_factor {
2503 let name = managed_cluster_replica_name(i);
2504 names.remove(&name);
2505 }
2506 if !names.is_empty() {
2507 let formatted = names
2508 .iter()
2509 .map(String::as_str)
2510 .collect::<Vec<_>>()
2511 .join(", ");
2512 coord_bail!(
2513 "Cannot convert unmanaged cluster to managed, invalid replica names: {formatted}"
2514 );
2515 }
2516
2517 let ops = vec![catalog::Op::UpdateClusterConfig {
2518 id: cluster_id,
2519 name: cluster_name,
2520 config: new_config,
2521 reconfiguration_audit: None,
2522 burst_audit: None,
2523 }];
2524
2525 self.catalog_transact(Some(session), ops).await?;
2526 Ok(())
2527 }
2528
2529 async fn sequence_alter_cluster_managed_to_unmanaged(
2530 &mut self,
2531 session: &Session,
2532 cluster_id: ClusterId,
2533 new_config: ClusterConfig,
2534 ) -> Result<(), AdapterError> {
2535 let cluster = self.catalog().get_cluster(cluster_id);
2536
2537 if let ClusterVariant::Managed(managed) = &cluster.config.variant {
2543 if managed
2544 .reconfiguration
2545 .as_ref()
2546 .is_some_and(|record| record.is_in_progress())
2547 {
2548 return Err(AdapterError::AlterClusterUnmanagedWhileReconfiguring);
2549 }
2550 if managed.burst.is_some() {
2557 return Err(AdapterError::AlterClusterUnmanagedWhileBursting);
2558 }
2559 }
2560
2561 let ops = vec![catalog::Op::UpdateClusterConfig {
2562 id: cluster_id,
2563 name: cluster.name().to_string(),
2564 config: new_config,
2565 reconfiguration_audit: None,
2566 burst_audit: None,
2567 }];
2568
2569 self.catalog_transact(Some(session), ops).await?;
2570 Ok(())
2571 }
2572
2573 async fn sequence_alter_cluster_unmanaged_to_unmanaged(
2574 &mut self,
2575 session: &Session,
2576 cluster_id: ClusterId,
2577 new_config: ClusterConfig,
2578 replicas: AlterOptionParameter<Vec<(String, mz_sql::plan::ReplicaConfig)>>,
2579 ) -> Result<(), AdapterError> {
2580 if !matches!(replicas, AlterOptionParameter::Unchanged) {
2581 coord_bail!("Cannot alter replicas in unmanaged cluster");
2582 }
2583
2584 let cluster = self.catalog().get_cluster(cluster_id);
2585
2586 let ops = vec![catalog::Op::UpdateClusterConfig {
2587 id: cluster_id,
2588 name: cluster.name().to_string(),
2589 config: new_config,
2590 reconfiguration_audit: None,
2591 burst_audit: None,
2592 }];
2593
2594 self.catalog_transact(Some(session), ops).await?;
2595 Ok(())
2596 }
2597
2598 pub(crate) async fn sequence_alter_cluster_rename(
2599 &mut self,
2600 ctx: &mut ExecuteContext,
2601 AlterClusterRenamePlan { id, name, to_name }: AlterClusterRenamePlan,
2602 ) -> Result<ExecuteResponse, AdapterError> {
2603 let op = Op::RenameCluster {
2604 id,
2605 name,
2606 to_name,
2607 check_reserved_names: true,
2608 };
2609 match self
2610 .catalog_transact_with_ddl_transaction(ctx, vec![op], |_, _| Box::pin(async {}))
2611 .await
2612 {
2613 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
2614 Err(err) => Err(err),
2615 }
2616 }
2617
2618 pub(crate) async fn sequence_alter_cluster_swap(
2619 &mut self,
2620 ctx: &mut ExecuteContext,
2621 AlterClusterSwapPlan {
2622 id_a,
2623 id_b,
2624 name_a,
2625 name_b,
2626 name_temp,
2627 }: AlterClusterSwapPlan,
2628 ) -> Result<ExecuteResponse, AdapterError> {
2629 let op_a = Op::RenameCluster {
2630 id: id_a,
2631 name: name_a.clone(),
2632 to_name: name_temp.clone(),
2633 check_reserved_names: false,
2634 };
2635 let op_b = Op::RenameCluster {
2636 id: id_b,
2637 name: name_b.clone(),
2638 to_name: name_a,
2639 check_reserved_names: false,
2640 };
2641 let op_temp = Op::RenameCluster {
2642 id: id_a,
2643 name: name_temp,
2644 to_name: name_b,
2645 check_reserved_names: false,
2646 };
2647
2648 match self
2649 .catalog_transact_with_ddl_transaction(ctx, vec![op_a, op_b, op_temp], |_, _| {
2650 Box::pin(async {})
2651 })
2652 .await
2653 {
2654 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
2655 Err(err) => Err(err),
2656 }
2657 }
2658
2659 pub(crate) async fn sequence_alter_cluster_replica_rename(
2660 &mut self,
2661 session: &Session,
2662 AlterClusterReplicaRenamePlan {
2663 cluster_id,
2664 replica_id,
2665 name,
2666 to_name,
2667 }: AlterClusterReplicaRenamePlan,
2668 ) -> Result<ExecuteResponse, AdapterError> {
2669 let op = catalog::Op::RenameClusterReplica {
2670 cluster_id,
2671 replica_id,
2672 name,
2673 to_name,
2674 };
2675 match self.catalog_transact(Some(session), vec![op]).await {
2676 Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::ClusterReplica)),
2677 Err(err) => Err(err),
2678 }
2679 }
2680
2681 pub(crate) async fn sequence_alter_set_cluster(
2683 &self,
2684 _session: &Session,
2685 AlterSetClusterPlan { id, set_cluster: _ }: AlterSetClusterPlan,
2686 ) -> Result<ExecuteResponse, AdapterError> {
2687 async {}.await;
2691 let entry = self.catalog().get_entry(&id);
2692 match entry.item().typ() {
2693 _ => {
2694 Err(AdapterError::Unsupported("ALTER SET CLUSTER"))
2696 }
2697 }
2698 }
2699}
2700
2701struct ReconfigurationDimensionsUnchanged {
2706 size: bool,
2707 replication_factor: bool,
2708 availability_zones: bool,
2709 log_logging: bool,
2710 interval: bool,
2711 arrangement_compression: bool,
2712}
2713
2714fn cancel_carried_reconfiguration(config: &mut ClusterConfig) -> Option<ReconfigurationAudit> {
2722 let ClusterVariant::Managed(managed) = &mut config.variant else {
2723 return None;
2724 };
2725 let record = managed.reconfiguration.as_mut()?;
2726 if !record.is_in_progress() {
2727 return None;
2728 }
2729 record.status = ReconfigurationStatus::Cancelled;
2730 Some(ReconfigurationAudit::Cancelled)
2731}
2732
2733fn alter_changes_replica_shape(options: &PlanClusterOption) -> bool {
2744 use mz_sql::plan::AlterOptionParameter::Unchanged;
2745 let PlanClusterOption {
2746 availability_zones,
2747 introspection_debugging,
2748 introspection_interval,
2749 arrangement_compression,
2750 managed: _,
2751 replicas: _,
2752 replication_factor: _,
2753 size,
2754 schedule: _,
2755 workload_class: _,
2756 auto_scaling_strategy: _,
2757 } = options;
2758 !matches!(size, Unchanged)
2759 || !matches!(availability_zones, Unchanged)
2760 || !matches!(introspection_debugging, Unchanged)
2761 || !matches!(introspection_interval, Unchanged)
2762 || !matches!(arrangement_compression, Unchanged)
2763}
2764
2765fn fold_reconfiguration_target(
2783 in_flight: Option<&ReconfigurationTarget>,
2784 new_target: ReconfigurationTarget,
2785 unchanged: ReconfigurationDimensionsUnchanged,
2786) -> ReconfigurationTarget {
2787 let Some(prev) = in_flight else {
2788 return new_target;
2789 };
2790 ReconfigurationTarget {
2791 size: if unchanged.size {
2792 prev.size.clone()
2793 } else {
2794 new_target.size
2795 },
2796 replication_factor: if unchanged.replication_factor {
2797 prev.replication_factor
2798 } else {
2799 new_target.replication_factor
2800 },
2801 availability_zones: if unchanged.availability_zones {
2802 prev.availability_zones.clone()
2803 } else {
2804 new_target.availability_zones
2805 },
2806 logging: ReplicaLogging {
2807 log_logging: if unchanged.log_logging {
2808 prev.logging.log_logging
2809 } else {
2810 new_target.logging.log_logging
2811 },
2812 interval: if unchanged.interval {
2813 prev.logging.interval
2814 } else {
2815 new_target.logging.interval
2816 },
2817 },
2818 arrangement_compression: if unchanged.arrangement_compression {
2819 prev.arrangement_compression
2820 } else {
2821 new_target.arrangement_compression
2822 },
2823 }
2824}
2825
2826#[derive(PartialEq)]
2829pub(crate) enum NeedsFinalization {
2830 Yes,
2832 No,
2833}
2834
2835#[cfg(test)]
2836mod tests {
2837 use mz_controller::clusters::ReplicaLogging;
2838 use mz_controller_types::DEFAULT_REPLICA_LOGGING_INTERVAL;
2839
2840 use super::*;
2841
2842 fn target(size: &str, rf: u32, azs: &[&str], log_logging: bool) -> ReconfigurationTarget {
2843 ReconfigurationTarget {
2844 size: size.to_string(),
2845 replication_factor: rf,
2846 availability_zones: azs.iter().map(|s| s.to_string()).collect(),
2847 logging: ReplicaLogging {
2848 log_logging,
2849 interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
2850 },
2851 arrangement_compression: false,
2852 }
2853 }
2854
2855 fn all_changed() -> ReconfigurationDimensionsUnchanged {
2856 ReconfigurationDimensionsUnchanged {
2857 size: false,
2858 replication_factor: false,
2859 availability_zones: false,
2860 log_logging: false,
2861 interval: false,
2862 arrangement_compression: false,
2863 }
2864 }
2865
2866 fn all_unchanged() -> ReconfigurationDimensionsUnchanged {
2867 ReconfigurationDimensionsUnchanged {
2868 size: true,
2869 replication_factor: true,
2870 availability_zones: true,
2871 log_logging: true,
2872 interval: true,
2873 arrangement_compression: true,
2874 }
2875 }
2876
2877 #[mz_ore::test]
2878 fn fold_with_no_record_takes_new_target() {
2879 let new = target("200cc", 3, &["az1"], true);
2881 let folded = fold_reconfiguration_target(None, new.clone(), all_changed());
2882 assert_eq!(folded, new);
2883 }
2884
2885 #[mz_ore::test]
2886 fn fold_rf_only_keeps_in_flight_shape() {
2887 let in_flight = target("200cc", 1, &["az2"], true);
2891 let new = target("100cc", 5, &["az1"], false);
2894 let unchanged = ReconfigurationDimensionsUnchanged {
2895 size: true,
2896 replication_factor: false,
2897 availability_zones: true,
2898 log_logging: true,
2899 interval: true,
2900 arrangement_compression: true,
2901 };
2902 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2903 assert_eq!(folded, target("200cc", 5, &["az2"], true));
2905 }
2906
2907 #[mz_ore::test]
2908 fn fold_with_all_set_overwrites_every_dimension() {
2909 let in_flight = target("200cc", 1, &["az2"], true);
2911 let new = target("400cc", 9, &["az9"], false);
2912 let folded = fold_reconfiguration_target(Some(&in_flight), new.clone(), all_changed());
2913 assert_eq!(folded, new);
2914 }
2915
2916 #[mz_ore::test]
2917 fn fold_all_unchanged_is_alter_back_to_in_flight() {
2918 let in_flight = target("200cc", 2, &["az2"], true);
2923 let realized_shaped = target("100cc", 1, &["az1"], false);
2924 let folded =
2925 fold_reconfiguration_target(Some(&in_flight), realized_shaped, all_unchanged());
2926 assert_eq!(folded, in_flight);
2927 }
2928
2929 #[mz_ore::test]
2930 fn fold_logging_subdimensions_fold_independently() {
2931 let mut in_flight = target("100cc", 1, &["az1"], false);
2936 in_flight.logging.interval = Some(Duration::from_secs(5));
2937 let new = target("100cc", 1, &["az1"], true);
2938 let unchanged = ReconfigurationDimensionsUnchanged {
2939 size: true,
2940 replication_factor: true,
2941 availability_zones: true,
2942 log_logging: false,
2943 interval: true,
2944 arrangement_compression: true,
2945 };
2946 let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2947 assert_eq!(
2948 folded.logging,
2949 ReplicaLogging {
2950 log_logging: true,
2951 interval: Some(Duration::from_secs(5)),
2952 }
2953 );
2954 }
2955}