Skip to main content

mz_adapter/coord/sequencer/inner/
cluster.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use 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::memory::objects::{
18    ClusterConfig, ClusterReplica, ClusterVariant, ClusterVariantManaged,
19    ManagedReplicaConfigShape, ReconfigurationState, ReconfigurationStatus, ReconfigurationTarget,
20};
21use mz_compute_types::config::ComputeReplicaConfig;
22use mz_controller::clusters::{
23    ManagedReplicaLocation, ReplicaConfig, ReplicaLocation, ReplicaLogging,
24};
25use mz_controller_types::{ClusterId, DEFAULT_REPLICA_LOGGING_INTERVAL, ReplicaId};
26use mz_ore::cast::CastFrom;
27use mz_ore::collections::CollectionExt;
28use mz_ore::instrument;
29use mz_repr::adt::numeric::Numeric;
30use mz_repr::role_id::RoleId;
31use mz_sql::ast::{Ident, QualifiedReplica};
32use mz_sql::catalog::{CatalogCluster, ObjectType};
33use mz_sql::plan::{
34    self, AlterClusterPlanStrategy, AlterClusterRenamePlan, AlterClusterReplicaRenamePlan,
35    AlterClusterSwapPlan, AlterOptionParameter, AlterSetClusterPlan,
36    ComputeReplicaIntrospectionConfig, CreateClusterManagedPlan, CreateClusterPlan,
37    CreateClusterReplicaPlan, CreateClusterUnmanagedPlan, CreateClusterVariant, PlanClusterOption,
38};
39use mz_sql::plan::{AlterClusterPlan, OnTimeoutAction};
40use mz_sql::session::metadata::SessionMetadata;
41use mz_sql::session::vars::{
42    MAX_CREDIT_CONSUMPTION_RATE, MAX_REPLICAS_PER_CLUSTER, SystemVars, Var,
43};
44use tracing::{Instrument, Span, debug};
45
46use mz_adapter_types::dyncfgs::{
47    DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT, ENABLE_BACKGROUND_ALTER_CLUSTER,
48    ENABLE_CLUSTER_CONTROLLER,
49};
50
51use super::return_if_err;
52use crate::AdapterError::AlterClusterWhilePendingReplicas;
53use crate::catalog::{self, Op, ReplicaCreateDropReason};
54use crate::config::{
55    ClusterEvalContext, ClusterScopeContext, ReplicaEvalContext, ReplicaScopeContext,
56};
57use crate::coord::{
58    AlterCluster, AlterClusterAwaitReconfiguration, AlterClusterFinalize,
59    AlterClusterWaitForHydrated, ClusterStage, Coordinator, Message, PlanValidity, StageResult,
60    Staged,
61};
62use crate::{AdapterError, ExecuteContext, ExecuteResponse, session::Session};
63
64const PENDING_REPLICA_SUFFIX: &str = "-pending";
65
66impl Staged for ClusterStage {
67    type Ctx = ExecuteContext;
68
69    fn validity(&mut self) -> &mut PlanValidity {
70        match self {
71            Self::Alter(stage) => &mut stage.validity,
72            Self::WaitForHydrated(stage) => &mut stage.validity,
73            Self::Finalize(stage) => &mut stage.validity,
74            Self::AwaitReconfiguration(stage) => &mut stage.validity,
75        }
76    }
77
78    async fn stage(
79        self,
80        coord: &mut Coordinator,
81        ctx: &mut ExecuteContext,
82    ) -> Result<StageResult<Box<Self>>, crate::AdapterError> {
83        match self {
84            Self::Alter(stage) => {
85                coord
86                    .sequence_alter_cluster_stage(ctx.session(), stage.plan.clone(), stage.validity)
87                    .await
88            }
89            Self::WaitForHydrated(stage) => {
90                let AlterClusterWaitForHydrated {
91                    validity,
92                    plan,
93                    new_config,
94                    workload_class,
95                    timeout_time,
96                    on_timeout,
97                } = stage;
98                coord
99                    .check_if_pending_replicas_hydrated_stage(
100                        ctx.session(),
101                        plan,
102                        new_config,
103                        workload_class,
104                        timeout_time,
105                        on_timeout,
106                        validity,
107                    )
108                    .await
109            }
110            Self::Finalize(stage) => {
111                coord
112                    .finalize_alter_cluster_stage(
113                        ctx.session(),
114                        stage.plan.clone(),
115                        stage.new_config.clone(),
116                        stage.workload_class.clone(),
117                    )
118                    .await
119            }
120            Self::AwaitReconfiguration(stage) => {
121                coord.await_reconfiguration_stage(stage.validity, stage.cluster_id, stage.target)
122            }
123        }
124    }
125
126    fn message(self, ctx: ExecuteContext, span: tracing::Span) -> Message {
127        Message::ClusterStageReady {
128            ctx,
129            span,
130            stage: self,
131        }
132    }
133
134    fn cancel_enabled(&self) -> bool {
135        true
136    }
137}
138
139impl Coordinator {
140    #[instrument]
141    pub(crate) async fn sequence_alter_cluster_staged(
142        &mut self,
143        ctx: ExecuteContext,
144        plan: plan::AlterClusterPlan,
145    ) {
146        let stage = return_if_err!(self.alter_cluster_validate(ctx.session(), plan).await, ctx);
147        self.sequence_staged(ctx, Span::current(), stage).await;
148    }
149
150    #[instrument]
151    async fn alter_cluster_validate(
152        &self,
153        session: &Session,
154        plan: plan::AlterClusterPlan,
155    ) -> Result<ClusterStage, AdapterError> {
156        let validity = PlanValidity::new(
157            self.catalog(),
158            BTreeSet::new(),
159            Some(plan.id.clone()),
160            None,
161            session.role_metadata().clone(),
162        );
163        Ok(ClusterStage::Alter(AlterCluster { validity, plan }))
164    }
165
166    async fn sequence_alter_cluster_stage(
167        &mut self,
168        session: &Session,
169        plan: plan::AlterClusterPlan,
170        validity: PlanValidity,
171    ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
172        let AlterClusterPlan {
173            id: cluster_id,
174            name: _,
175            ref options,
176            ref strategy,
177        } = plan;
178
179        use mz_catalog::memory::objects::ClusterVariant::*;
180        use mz_sql::plan::AlterOptionParameter::*;
181        let cluster = self.catalog.get_cluster(cluster_id);
182        let config = cluster.config.clone();
183        let mut new_config = config.clone();
184
185        match (&new_config.variant, &options.managed) {
186            (Managed(_), Reset) | (Managed(_), Unchanged) | (Managed(_), Set(true)) => {}
187            (Managed(_), Set(false)) => new_config.variant = Unmanaged,
188            (Unmanaged, Unchanged) | (Unmanaged, Set(false)) => {}
189            (Unmanaged, Reset) | (Unmanaged, Set(true)) => {
190                // Generate a minimal correct configuration
191
192                // Size adjusted later when sequencing the actual configuration change.
193                let size = "".to_string();
194                let logging = ReplicaLogging {
195                    log_logging: false,
196                    interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
197                };
198                new_config.variant = Managed(ClusterVariantManaged {
199                    size,
200                    availability_zones: Default::default(),
201                    logging,
202                    replication_factor: 1,
203                    optimizer_feature_overrides: Default::default(),
204                    schedule: Default::default(),
205                    auto_scaling_strategy: None,
206                    reconfiguration: None,
207                    burst: None,
208                });
209            }
210        }
211
212        match &mut new_config.variant {
213            Managed(ClusterVariantManaged {
214                size,
215                availability_zones,
216                logging,
217                replication_factor,
218                optimizer_feature_overrides: _,
219                schedule,
220                auto_scaling_strategy: _,
221                reconfiguration: _,
222                burst: _,
223            }) => {
224                match &options.size {
225                    Set(s) => size.clone_from(s),
226                    Reset => coord_bail!("SIZE has no default value"),
227                    Unchanged => {}
228                }
229                match &options.availability_zones {
230                    Set(az) => availability_zones.clone_from(az),
231                    Reset => *availability_zones = Default::default(),
232                    Unchanged => {}
233                }
234                match &options.introspection_debugging {
235                    Set(id) => logging.log_logging = *id,
236                    Reset => logging.log_logging = false,
237                    Unchanged => {}
238                }
239                match &options.introspection_interval {
240                    Set(ii) => logging.interval = ii.0,
241                    Reset => logging.interval = Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
242                    Unchanged => {}
243                }
244                match &options.replication_factor {
245                    Set(rf) => *replication_factor = *rf,
246                    Reset => {
247                        *replication_factor = self
248                            .catalog
249                            .system_config()
250                            .default_cluster_replication_factor()
251                    }
252                    Unchanged => {}
253                }
254                match &options.schedule {
255                    Set(new_schedule) => {
256                        *schedule = new_schedule.clone();
257                    }
258                    Reset => *schedule = Default::default(),
259                    Unchanged => {}
260                }
261                if !matches!(options.replicas, Unchanged) {
262                    coord_bail!("Cannot change REPLICAS of managed clusters");
263                }
264            }
265            Unmanaged => {
266                if !matches!(options.size, Unchanged) {
267                    coord_bail!("Cannot change SIZE of unmanaged clusters");
268                }
269                if !matches!(options.availability_zones, Unchanged) {
270                    coord_bail!("Cannot change AVAILABILITY ZONES of unmanaged clusters");
271                }
272                if !matches!(options.introspection_debugging, Unchanged) {
273                    coord_bail!("Cannot change INTROSPECTION DEGUBBING of unmanaged clusters");
274                }
275                if !matches!(options.introspection_interval, Unchanged) {
276                    coord_bail!("Cannot change INTROSPECTION INTERVAL of unmanaged clusters");
277                }
278                if !matches!(options.replication_factor, Unchanged) {
279                    coord_bail!("Cannot change REPLICATION FACTOR of unmanaged clusters");
280                }
281            }
282        }
283
284        match &options.workload_class {
285            Set(wc) => new_config.workload_class.clone_from(wc),
286            Reset => new_config.workload_class = None,
287            Unchanged => {}
288        }
289
290        // The controller owns only *user* managed clusters (see `ManagedClusterIds`
291        // in cluster_controller.rs and `controller_owns` in the managed-to-managed
292        // path below). A system/builtin cluster is never converged by the
293        // controller, so it must not be reshaped into a durable reconfiguration
294        // record nobody would cut over. It takes the direct realized-config path
295        // below, exactly as it does with the controller off.
296        let cluster_controller_owns = ENABLE_CLUSTER_CONTROLLER
297            .get(self.catalog().system_config().dyncfgs())
298            && cluster_id.is_user();
299        let reconfiguration_in_flight = matches!(
300            &config.variant,
301            Managed(managed) if managed
302                .reconfiguration
303                .as_ref()
304                .is_some_and(|record| record.is_in_progress())
305        );
306
307        // Replication factor is one of the four dimensions the cut-over sets
308        // atomically from the record's target (`fold_reconfiguration_target`),
309        // so a change applied independently while a reconfiguration is in
310        // flight would be silently clobbered at cut-over. Refused even when the
311        // same statement also re-targets the shape, so a record's target
312        // replication factor is always the one it started with.
313        if cluster_controller_owns
314            && reconfiguration_in_flight
315            && !matches!(options.replication_factor, Unchanged)
316        {
317            return Err(AdapterError::AlterClusterReplicationFactorWhileReconfiguring);
318        }
319
320        // A no-op `ALTER` short-circuits, except that an `ALTER` back to the
321        // realized shape while a reconfiguration is in flight produces a
322        // byte-identical `new_config` and is still meaningful: it must reach
323        // the reshape path below to cancel the record.
324        let cancels_or_retargets =
325            reconfiguration_in_flight && alter_changes_replica_shape(options);
326        if new_config == config && !(cluster_controller_owns && cancels_or_retargets) {
327            return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
328                ObjectType::Cluster,
329            )));
330        }
331
332        // When the controller owns the replica set, a shape-changing `ALTER`
333        // reshapes into a durable `reconfiguration` record (starting,
334        // retargeting, or cancelling one) instead of going through the legacy
335        // 3-stage machine. Everything else falls through to the realized-config
336        // update below without touching the record, in flight or not.
337        //
338        // With a record in flight the statement decides: an `ALTER` back to the
339        // realized shape is value-identical yet must reach the reshape path to
340        // cancel. With nothing in flight the values decide: a shape option set
341        // to its current value reconfigures nothing, and reshaping it anyway
342        // would write a spurious pre-cancelled record.
343        if cluster_controller_owns {
344            if let (Managed(old_managed), Managed(new_managed)) =
345                (&config.variant, &new_config.variant)
346            {
347                let needs_record = if reconfiguration_in_flight {
348                    alter_changes_replica_shape(options)
349                } else {
350                    new_managed.replica_config_shape() != old_managed.replica_config_shape()
351                };
352                if needs_record {
353                    return self
354                        .reshape_alter_cluster_managed(
355                            session,
356                            cluster_id,
357                            new_config.clone(),
358                            options,
359                            strategy,
360                            validity,
361                        )
362                        .await;
363                }
364            }
365        }
366
367        match (&config.variant, &new_config.variant) {
368            (Managed(_), Managed(new_config_managed)) => {
369                let alter_followup = self
370                    .sequence_alter_cluster_managed_to_managed(
371                        Some(session),
372                        cluster_id,
373                        new_config.clone(),
374                        ReplicaCreateDropReason::Manual,
375                        strategy.clone(),
376                    )
377                    .await?;
378                if alter_followup == NeedsFinalization::Yes {
379                    // For non backgrounded zero-downtime alters, store the
380                    // cluster_id in the ConnMeta to allow for cancellation.
381                    self.active_conns
382                        .get_mut(session.conn_id())
383                        .expect("There must be an active connection")
384                        .pending_cluster_alters
385                        .insert(cluster_id.clone());
386                    let new_config_managed = new_config_managed.clone();
387                    return match &strategy {
388                        AlterClusterPlanStrategy::None => Err(AdapterError::Internal(
389                            "AlterClusterPlanStrategy must not be None if NeedsFinalization is Yes"
390                                .into(),
391                        )),
392                        AlterClusterPlanStrategy::For(duration) => {
393                            let span = Span::current();
394                            let plan = plan.clone();
395                            let duration = duration.clone().to_owned();
396                            let workload_class = new_config.workload_class.clone();
397                            Ok(StageResult::Handle(mz_ore::task::spawn(
398                                || "Finalize Alter Cluster",
399                                async move {
400                                    tokio::time::sleep(duration).await;
401                                    let stage = ClusterStage::Finalize(AlterClusterFinalize {
402                                        validity,
403                                        plan,
404                                        new_config: new_config_managed,
405                                        workload_class,
406                                    });
407                                    Ok(Box::new(stage))
408                                }
409                                .instrument(span),
410                            )))
411                        }
412                        AlterClusterPlanStrategy::UntilReady {
413                            timeout,
414                            on_timeout,
415                        } => Ok(StageResult::Immediate(Box::new(
416                            ClusterStage::WaitForHydrated(AlterClusterWaitForHydrated {
417                                validity,
418                                plan: plan.clone(),
419                                new_config: new_config_managed.clone(),
420                                workload_class: new_config.workload_class.clone(),
421                                timeout_time: Instant::now() + timeout.to_owned(),
422                                // The legacy foreground wait uses COMMIT as
423                                // its implicit default. The controller-owned
424                                // paths default to ROLLBACK.
425                                on_timeout: on_timeout.unwrap_or(OnTimeoutAction::Commit),
426                            }),
427                        ))),
428                    };
429                }
430            }
431            (Unmanaged, Managed(_)) => {
432                self.sequence_alter_cluster_unmanaged_to_managed(
433                    session,
434                    cluster_id,
435                    new_config,
436                    options.to_owned(),
437                )
438                .await?;
439            }
440            (Managed(_), Unmanaged) => {
441                self.sequence_alter_cluster_managed_to_unmanaged(session, cluster_id, new_config)
442                    .await?;
443            }
444            (Unmanaged, Unmanaged) => {
445                self.sequence_alter_cluster_unmanaged_to_unmanaged(
446                    session,
447                    cluster_id,
448                    new_config,
449                    options.replicas.clone(),
450                )
451                .await?;
452            }
453        }
454
455        Ok(StageResult::Response(ExecuteResponse::AlteredObject(
456            ObjectType::Cluster,
457        )))
458    }
459
460    /// Validates that a reconfiguration to `target` fits the resource budget.
461    fn validate_reconfiguration_resource_limits(
462        &self,
463        cluster_id: ClusterId,
464        target: &ReconfigurationTarget,
465    ) -> Result<(), AdapterError> {
466        // Only user clusters are converged by the controller and counted against
467        // these limits. A system cluster never reshapes into a record.
468        if !cluster_id.is_user() {
469            return Ok(());
470        }
471        let cluster = self.catalog().get_cluster(cluster_id);
472        let ClusterVariant::Managed(realized) = &cluster.config.variant else {
473            return Ok(());
474        };
475
476        // An `ALTER` back to the realized shape cancels the reconfiguration and
477        // materializes nothing new, so there is nothing to validate. The peak
478        // model below would double count the realized set and spuriously reject
479        // the cancel, exactly when the environment is at its limits and the
480        // escape hatch matters most.
481        if target.matches_realized_config(realized) {
482            return Ok(());
483        }
484
485        // Both checks below model the transient peak: the controller runs the
486        // realized and target sets side by side until cut-over, so this cluster's
487        // peak contribution is both shapes at once, computed from config as
488        // realized plus target. That slightly over-counts a same-shape overlap,
489        // where existing replicas double as target replicas, but it matches the
490        // legacy wait path, which creates the full target set as pending replicas
491        // at `ALTER` time and therefore enforces both limits on the overlap.
492        // Rejecting here is also strictly better than the asynchronous abort the
493        // controller falls back to when a limit shrinks or the environment grows
494        // after the record is written.
495
496        // Per-cluster replica count: the peak is `realized_rf + target_rf`,
497        // deterministic from the cluster's own config. `validate_resource_limit`
498        // returns early on an rf-0 target.
499        self.validate_resource_limit(
500            usize::cast_from(realized.replication_factor),
501            i64::from(target.replication_factor),
502            SystemVars::max_replicas_per_cluster,
503            "cluster replica",
504            MAX_REPLICAS_PER_CLUSTER.name(),
505        )?;
506
507        // Global credit rate: the peak is `credit(realized) + credit(target)`.
508        self.validate_reconfiguration_credit_peak(cluster_id, realized, target)?;
509
510        Ok(())
511    }
512
513    /// Validates that the transient credit-rate peak of a reconfiguration, the
514    /// realized plus the target shape, fits the environment-wide budget.
515    ///
516    /// The base is the live consumption of every other cluster. It excludes
517    /// this cluster's own replicas so a re-target of an in-flight record does
518    /// not additionally count an already-materialized overlap on top of the
519    /// modeled peak.
520    fn validate_reconfiguration_credit_peak(
521        &self,
522        cluster_id: ClusterId,
523        realized: &ClusterVariantManaged,
524        target: &ReconfigurationTarget,
525    ) -> Result<(), AdapterError> {
526        let shape_credit = |size: &str, replication_factor: u32| -> Numeric {
527            let per_replica = self
528                .catalog()
529                .cluster_replica_sizes()
530                .0
531                .get(size)
532                .map(|allocation| allocation.credits_per_hour)
533                // Sizes are validated by `ensure_valid_replica_size` before we get
534                // here, so an unknown size contributes nothing rather than panics.
535                .unwrap_or_else(Numeric::zero);
536            per_replica * Numeric::from(replication_factor)
537        };
538        let mut peak_credit = shape_credit(&target.size, target.replication_factor);
539        peak_credit += shape_credit(&realized.size, realized.replication_factor);
540        self.validate_resource_limit_numeric(
541            self.current_credit_consumption_rate(Some(cluster_id)),
542            peak_credit,
543            |system_vars| {
544                self.license_key
545                    .max_credit_consumption_rate()
546                    .map_or_else(|| system_vars.max_credit_consumption_rate(), Numeric::from)
547            },
548            "cluster replica",
549            MAX_CREDIT_CONSUMPTION_RATE.name(),
550        )?;
551
552        Ok(())
553    }
554
555    /// Reshape a managed→managed `ALTER` into a durable `reconfiguration` record.
556    ///
557    /// Writes (or folds into) the `reconfiguration` record carrying the full target
558    /// config shape and a deadline, while leaving the realized *shape* in place.
559    /// Non-shape fields the `ALTER` changed (`workload_class`, `schedule`,
560    /// `auto_scaling_strategy`, ...) need no hydrate-overlap, so they are applied
561    /// to the realized config immediately. The controller converges the replica
562    /// set onto the target and cuts the realized shape over at hydration.
563    ///
564    /// **Fold semantics.** When a record is already in flight, the target is an
565    /// overlay on the *in-flight target*, not the realized config: a dimension the
566    /// `ALTER` set (`options.*` is `Set`/`Reset`) takes the new value, a dimension
567    /// left `Unchanged` keeps the in-flight target's value. `new_config` was built
568    /// against the realized config, which still holds the pre-reconfiguration shape
569    /// (the realized config is advanced only at cut-over), so seeding `Unchanged`
570    /// dimensions from it would silently revert the in-flight transition along any
571    /// dimension this `ALTER` did not mention. With no record in flight there is
572    /// nothing to fold and the target is exactly `new_config`'s shape.
573    ///
574    /// **Timeout action.** The record carries an `on_timeout` action (resolved
575    /// from `WITH (WAIT ...)`, defaulting to `ROLLBACK`), which the controller
576    /// applies at the deadline only if the target has not hydrated: `ROLLBACK`
577    /// marks the record timed out and drops the in-flight target set, leaving the
578    /// realized config untouched, so the cluster reverts to its
579    /// pre-reconfiguration shape and the strategy disengages. `COMMIT` cuts the
580    /// realized config over to the not-fully-hydrated target and marks the record
581    /// finalized. Success always takes precedence. A target that hydrates before the deadline cuts over regardless
582    /// of the action.
583    ///
584    /// With `enable_background_alter_cluster` on, the statement returns
585    /// immediately. With it off, the session blocks on a wait-shim
586    /// ([`ClusterStage::AwaitReconfiguration`]) that polls until the controller
587    /// resolves the record, reporting success only if the realized config
588    /// reached the target, preserving today's foreground UX over the same
589    /// durable mechanism.
590    async fn reshape_alter_cluster_managed(
591        &mut self,
592        session: &Session,
593        cluster_id: ClusterId,
594        new_config: ClusterConfig,
595        options: &PlanClusterOption,
596        strategy: &AlterClusterPlanStrategy,
597        validity: PlanValidity,
598    ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
599        use mz_sql::plan::AlterOptionParameter::Unchanged;
600
601        let ClusterVariant::Managed(new_managed) = &new_config.variant else {
602            return Err(AdapterError::Internal(
603                "reshape_alter_cluster_managed requires a managed target config".into(),
604            ));
605        };
606
607        // Fold onto the in-flight target when one exists: `new_config` carries the
608        // realized value for any dimension the `ALTER` left `Unchanged`, but the
609        // realized config is the pre-reconfiguration shape, so we instead carry the
610        // in-flight target's value for those dimensions. Only dimensions the `ALTER`
611        // explicitly set diverge from the in-flight target.
612        let cluster = self.catalog.get_cluster(cluster_id);
613        let in_flight = match &cluster.config.variant {
614            ClusterVariant::Managed(managed) => managed
615                .reconfiguration
616                .as_ref()
617                .filter(|record| record.is_in_progress())
618                .cloned(),
619            ClusterVariant::Unmanaged => None,
620        };
621        let new_target = ReconfigurationTarget {
622            size: new_managed.size.clone(),
623            replication_factor: new_managed.replication_factor,
624            availability_zones: new_managed.availability_zones.clone(),
625            logging: new_managed.logging.clone(),
626        };
627        let unchanged = ReconfigurationDimensionsUnchanged {
628            size: matches!(options.size, Unchanged),
629            replication_factor: matches!(options.replication_factor, Unchanged),
630            availability_zones: matches!(options.availability_zones, Unchanged),
631            // The two logging options fold independently, so a debugging-only
632            // `ALTER` cannot revert an in-flight interval change (or vice versa).
633            log_logging: matches!(options.introspection_debugging, Unchanged),
634            interval: matches!(options.introspection_interval, Unchanged),
635        };
636        let target = fold_reconfiguration_target(
637            in_flight.as_ref().map(|r| &r.target),
638            new_target,
639            unchanged,
640        );
641
642        // Validate the target up front, so a bad reshape errors at `ALTER` time
643        // rather than silently parking an unconvergeable record.
644        let role_id = session.role_metadata().current_role;
645        self.catalog.ensure_valid_replica_size(
646            &self
647                .catalog()
648                .get_role_allowed_cluster_sizes(&Some(role_id)),
649            &target.size,
650            false,
651        )?;
652        self.ensure_valid_azs(target.availability_zones.iter())?;
653        // Validate the reconfiguration's resource footprint up front, so a
654        // reshape that cannot fit errors at `ALTER` time rather than writing a
655        // record the controller aborts asynchronously.
656        self.validate_reconfiguration_resource_limits(cluster_id, &target)?;
657
658        // Resolve the deadline and the on-timeout action from the existing
659        // `WITH (WAIT ...)` surface. Both are written relative to the current time
660        // so they survive session disconnect and restart. Unlike the target, which
661        // folds per-dimension onto the in-flight one, the deadline and `on_timeout`
662        // are replaced wholesale by the latest `ALTER`'s `WAIT` clause (they are
663        // resolved fresh here, not merged), so re-issuing an `ALTER` with a
664        // different `ON TIMEOUT` overwrites the prior action.
665        //   - no `WAIT`         -> the system-default timeout and the implicit
666        //                          `on_timeout` default (`ROLLBACK`).
667        //   - `WAIT FOR`        -> sugar for `ON TIMEOUT COMMIT` (cut over at the
668        //                          deadline regardless of hydration).
669        //   - `WAIT UNTIL READY -> the explicit `TIMEOUT` / `ON TIMEOUT`, with
670        //                          `ON TIMEOUT` defaulting to `ROLLBACK` when
671        //                          omitted. The safe default for the controller
672        //                          path reverts an un-hydrated reconfiguration to
673        //                          its pre-reconfiguration shape rather than
674        //                          cutting over to a not-yet-hydrated target
675        //                          (which could induce downtime). The legacy
676        //                          foreground path uses implicit `COMMIT`.
677        let (timeout, on_timeout) = match strategy {
678            AlterClusterPlanStrategy::None => (
679                DEFAULT_CLUSTER_RECONFIGURATION_TIMEOUT
680                    .get(self.catalog().system_config().dyncfgs()),
681                OnTimeoutAction::Rollback,
682            ),
683            AlterClusterPlanStrategy::For(timeout) => (*timeout, OnTimeoutAction::Commit),
684            AlterClusterPlanStrategy::UntilReady {
685                timeout,
686                on_timeout,
687            } => (*timeout, on_timeout.unwrap_or(OnTimeoutAction::Rollback)),
688        };
689        let deadline = self
690            .now()
691            .saturating_add(u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX));
692
693        // Build the durable write from `new_config`, which carries every field the
694        // `ALTER` changed, then reset the config *shape* (size, replication factor,
695        // availability zones, logging) back to the realized values: that transition
696        // is deferred to the `reconfiguration` record and applied at cut-over. This
697        // applies non-shape changes (`workload_class`, `schedule`,
698        // `auto_scaling_strategy`, ...) immediately, matching the legacy path,
699        // rather than silently dropping them. Any existing record is folded over by
700        // the `record` we just built.
701        let cluster = self.catalog.get_cluster(cluster_id);
702        let cluster_name = cluster.name().to_string();
703        let ClusterVariant::Managed(realized_now) = &cluster.config.variant else {
704            return Err(AdapterError::Internal(
705                "reshape_alter_cluster_managed requires a managed realized config".into(),
706            ));
707        };
708        let realized_size = realized_now.size.clone();
709        let realized_replication_factor = realized_now.replication_factor;
710        let realized_availability_zones = realized_now.availability_zones.clone();
711        let realized_logging = realized_now.logging.clone();
712        // The status and the audit intent are two views of the same decision,
713        // made together here: an ALTER back to the realized shape is a cancel,
714        // anything else starts (or re-targets) a reconfiguration.
715        let (status, audit) = if target.matches_realized_config(realized_now) {
716            (
717                ReconfigurationStatus::Cancelled,
718                ReconfigurationAudit::Cancelled,
719            )
720        } else {
721            (
722                ReconfigurationStatus::InProgress,
723                ReconfigurationAudit::Started,
724            )
725        };
726        let record = ReconfigurationState {
727            target: target.clone(),
728            deadline: deadline.into(),
729            on_timeout,
730            status,
731        };
732
733        let mut realized = new_config.clone();
734        let ClusterVariant::Managed(realized_managed) = &mut realized.variant else {
735            return Err(AdapterError::Internal(
736                "reshape_alter_cluster_managed requires a managed target config".into(),
737            ));
738        };
739        realized_managed.size = realized_size;
740        realized_managed.replication_factor = realized_replication_factor;
741        realized_managed.availability_zones = realized_availability_zones;
742        realized_managed.logging = realized_logging;
743        realized_managed.reconfiguration = Some(record);
744
745        self.catalog_transact(
746            Some(session),
747            vec![Op::UpdateClusterConfig {
748                id: cluster_id,
749                name: cluster_name,
750                config: realized,
751                reconfiguration_audit: Some(audit),
752                burst_audit: None,
753            }],
754        )
755        .await?;
756
757        let background =
758            ENABLE_BACKGROUND_ALTER_CLUSTER.get(self.catalog().system_config().dyncfgs());
759        if background {
760            return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
761                ObjectType::Cluster,
762            )));
763        }
764
765        // Foreground wait-shim: poll the durable record until it resolves. The
766        // reconfiguration continues in the background regardless of the session. A
767        // disconnect during the wait only stops waiting.
768        Ok(StageResult::Immediate(Box::new(
769            ClusterStage::AwaitReconfiguration(AlterClusterAwaitReconfiguration {
770                validity,
771                cluster_id,
772                target,
773            }),
774        )))
775    }
776
777    /// Polls the durable `reconfiguration` record for the foreground wait-shim.
778    ///
779    /// The controller owns deadline handling. This stage reports success only once
780    /// the realized config reaches `target`, and otherwise keeps polling while
781    /// the record is in progress.
782    fn await_reconfiguration_stage(
783        &self,
784        validity: PlanValidity,
785        cluster_id: ClusterId,
786        target: ReconfigurationTarget,
787    ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
788        let Some(cluster) = self.catalog().try_get_cluster(cluster_id) else {
789            // The cluster was dropped out from under the reconfiguration.
790            // There is nothing to wait on.
791            return Ok(StageResult::Response(ExecuteResponse::AlteredObject(
792                ObjectType::Cluster,
793            )));
794        };
795        let record = match &cluster.config.variant {
796            ClusterVariant::Managed(managed) => managed.reconfiguration.clone(),
797            ClusterVariant::Unmanaged => None,
798        };
799
800        let realized_matches_target = match &cluster.config.variant {
801            ClusterVariant::Managed(managed) => target.matches_realized_config(managed),
802            ClusterVariant::Unmanaged => false,
803        };
804
805        match record {
806            None => {
807                // Defensive fallback for old or manually-edited catalogs. New
808                // controller writes retain a terminal record.
809                if realized_matches_target {
810                    Ok(StageResult::Response(ExecuteResponse::AlteredObject(
811                        ObjectType::Cluster,
812                    )))
813                } else {
814                    Err(AdapterError::AlterClusterTimeout)
815                }
816            }
817            Some(record) if !record.is_in_progress() => {
818                if matches!(
819                    record.status,
820                    ReconfigurationStatus::Finalized | ReconfigurationStatus::Cancelled
821                ) && realized_matches_target
822                {
823                    Ok(StageResult::Response(ExecuteResponse::AlteredObject(
824                        ObjectType::Cluster,
825                    )))
826                } else {
827                    Err(AdapterError::AlterClusterTimeout)
828                }
829            }
830            Some(_) => {
831                // Still in progress. Re-poll after the configured interval and
832                // wait for the controller to resolve the record. We deliberately
833                // do not consult the deadline here: erroring while the record is
834                // in progress can race the controller and misreport an `ON
835                // TIMEOUT COMMIT` cut-over as a timeout.
836                //
837                // NOTE: If the controller stops resolving a record while it is
838                // in progress, the shim waits indefinitely. Cancelling the session
839                // only stops waiting. It does not abort the durable reconfiguration.
840                let poll_duration = self
841                    .catalog
842                    .system_config()
843                    .cluster_alter_check_ready_interval();
844                let span = Span::current();
845                Ok(StageResult::Handle(mz_ore::task::spawn(
846                    || "Await Cluster Reconfiguration",
847                    async move {
848                        tokio::time::sleep(poll_duration).await;
849                        Ok(Box::new(ClusterStage::AwaitReconfiguration(
850                            AlterClusterAwaitReconfiguration {
851                                validity,
852                                cluster_id,
853                                target,
854                            },
855                        )))
856                    }
857                    .instrument(span),
858                )))
859            }
860        }
861    }
862
863    async fn finalize_alter_cluster_stage(
864        &mut self,
865        session: &Session,
866        AlterClusterPlan {
867            id: cluster_id,
868            name: cluster_name,
869            ..
870        }: AlterClusterPlan,
871        new_config: ClusterVariantManaged,
872        workload_class: Option<String>,
873    ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
874        let cluster = self.catalog.get_cluster(cluster_id);
875        let mut ops = vec![];
876
877        // Gather the ops to remove the non pending replicas
878        // Also skip any billed_as free replicas
879        let remove_replicas = cluster
880            .replicas()
881            .filter_map(|r| {
882                if !r.config.location.pending() && !r.config.location.internal() {
883                    Some(catalog::DropObjectInfo::ClusterReplica((
884                        cluster_id.clone(),
885                        r.replica_id,
886                        ReplicaCreateDropReason::Manual,
887                    )))
888                } else {
889                    None
890                }
891            })
892            .collect();
893        ops.push(catalog::Op::DropObjects(remove_replicas));
894
895        // Gather the Ops to remove the "-pending" suffix from the name and set
896        // pending to false
897        let finalize_replicas: Vec<catalog::Op> = cluster
898            .replicas()
899            .filter_map(|r| {
900                if r.config.location.pending() {
901                    let cluster_ident = match Ident::new(cluster.name.clone()) {
902                        Ok(id) => id,
903                        Err(err) => {
904                            return Some(Err(AdapterError::internal(
905                                "Unexpected error parsing cluster name",
906                                err,
907                            )));
908                        }
909                    };
910                    let replica_ident = match Ident::new(r.name.clone()) {
911                        Ok(id) => id,
912                        Err(err) => {
913                            return Some(Err(AdapterError::internal(
914                                "Unexpected error parsing replica name",
915                                err,
916                            )));
917                        }
918                    };
919                    Some(Ok((cluster_ident, replica_ident, r)))
920                } else {
921                    None
922                }
923            })
924            // Early collection is to handle errors from generating of the
925            // Idents
926            .collect::<Result<Vec<(Ident, Ident, &ClusterReplica)>, _>>()?
927            .into_iter()
928            .map(|(cluster_ident, replica_ident, replica)| {
929                let mut new_replica_config = replica.config.clone();
930                debug!("Promoting replica: {}", replica.name);
931                match new_replica_config.location {
932                    mz_controller::clusters::ReplicaLocation::Managed(ManagedReplicaLocation {
933                        ref mut pending,
934                        ..
935                    }) => {
936                        *pending = false;
937                    }
938                    mz_controller::clusters::ReplicaLocation::Unmanaged(_) => {}
939                }
940
941                let mut replica_ops = vec![];
942                let to_name = replica.name.strip_suffix(PENDING_REPLICA_SUFFIX);
943                if let Some(to_name) = to_name {
944                    replica_ops.push(catalog::Op::RenameClusterReplica {
945                        cluster_id: cluster_id.clone(),
946                        replica_id: replica.replica_id.to_owned(),
947                        name: QualifiedReplica {
948                            cluster: cluster_ident,
949                            replica: replica_ident,
950                        },
951                        to_name: to_name.to_owned(),
952                    });
953                }
954                replica_ops.push(catalog::Op::UpdateClusterReplicaConfig {
955                    cluster_id,
956                    replica_id: replica.replica_id.to_owned(),
957                    config: new_replica_config,
958                });
959                replica_ops
960            })
961            .flatten()
962            .collect();
963
964        ops.extend(finalize_replicas);
965
966        // Add the Op to update the cluster state. A stale in-progress
967        // reconfiguration record carried by this legacy write is retained as
968        // cancelled, with the matching audit intent declared.
969        let mut final_config = ClusterConfig {
970            variant: ClusterVariant::Managed(new_config),
971            workload_class: workload_class.clone(),
972        };
973        let reconfiguration_audit = cancel_carried_reconfiguration(&mut final_config);
974        ops.push(Op::UpdateClusterConfig {
975            id: cluster_id,
976            name: cluster_name,
977            config: final_config,
978            reconfiguration_audit,
979            burst_audit: None,
980        });
981        self.catalog_transact(Some(session), ops).await?;
982        // Remove the cluster being altered from the ConnMeta
983        // pending_cluster_alters BTreeSet
984        self.active_conns
985            .get_mut(session.conn_id())
986            .expect("There must be an active connection")
987            .pending_cluster_alters
988            .remove(&cluster_id);
989
990        Ok(StageResult::Response(ExecuteResponse::AlteredObject(
991            ObjectType::Cluster,
992        )))
993    }
994
995    async fn check_if_pending_replicas_hydrated_stage(
996        &mut self,
997        session: &Session,
998        plan: AlterClusterPlan,
999        new_config: ClusterVariantManaged,
1000        workload_class: Option<String>,
1001        timeout_time: Instant,
1002        on_timeout: OnTimeoutAction,
1003        validity: PlanValidity,
1004    ) -> Result<StageResult<Box<ClusterStage>>, AdapterError> {
1005        // wait and re-signal wait for hydrated if not hydrated
1006        let cluster = self.catalog.get_cluster(plan.id);
1007        let pending_replicas = cluster
1008            .replicas()
1009            .filter_map(|r| {
1010                if r.config.location.pending() {
1011                    Some(r.replica_id.clone())
1012                } else {
1013                    None
1014                }
1015            })
1016            .collect_vec();
1017        // Check For timeout
1018        if Instant::now() > timeout_time {
1019            // Timed out handle timeout action
1020            match on_timeout {
1021                OnTimeoutAction::Rollback => {
1022                    self.active_conns
1023                        .get_mut(session.conn_id())
1024                        .expect("There must be an active connection")
1025                        .pending_cluster_alters
1026                        .remove(&cluster.id);
1027                    self.drop_reconfiguration_replicas(btreeset!(cluster.id))
1028                        .await?;
1029                    return Err(AdapterError::AlterClusterTimeout);
1030                }
1031                OnTimeoutAction::Commit => {
1032                    let span = Span::current();
1033                    let poll_duration = self
1034                        .catalog
1035                        .system_config()
1036                        .cluster_alter_check_ready_interval()
1037                        .clone();
1038                    return Ok(StageResult::Handle(mz_ore::task::spawn(
1039                        || "Finalize Alter Cluster",
1040                        async move {
1041                            tokio::time::sleep(poll_duration).await;
1042                            let stage = ClusterStage::Finalize(AlterClusterFinalize {
1043                                validity,
1044                                plan,
1045                                new_config,
1046                                workload_class,
1047                            });
1048                            Ok(Box::new(stage))
1049                        }
1050                        .instrument(span),
1051                    )));
1052                }
1053            }
1054        }
1055        let compute_hydrated_fut = self
1056            .controller
1057            .compute
1058            .collections_hydrated_for_replicas(cluster.id, pending_replicas.clone(), [].into())
1059            .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1060
1061        let storage_hydrated = self
1062            .controller
1063            .storage
1064            .collections_hydrated_on_replicas(Some(pending_replicas), &cluster.id, &[].into())
1065            .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1066
1067        let span = Span::current();
1068        Ok(StageResult::Handle(mz_ore::task::spawn(
1069            || "Alter Cluster: wait for hydrated",
1070            async move {
1071                let compute_hydrated = compute_hydrated_fut
1072                    .await
1073                    .map_err(|e| AdapterError::internal("Failed to check hydration", e))?;
1074
1075                if compute_hydrated && storage_hydrated {
1076                    // We're done
1077                    Ok(Box::new(ClusterStage::Finalize(AlterClusterFinalize {
1078                        validity,
1079                        plan,
1080                        new_config: new_config.clone(),
1081                        workload_class: workload_class.clone(),
1082                    })))
1083                } else {
1084                    // Check later
1085                    tokio::time::sleep(Duration::from_secs(1)).await;
1086                    let stage = ClusterStage::WaitForHydrated(AlterClusterWaitForHydrated {
1087                        validity,
1088                        plan,
1089                        new_config,
1090                        workload_class,
1091                        timeout_time,
1092                        on_timeout,
1093                    });
1094                    Ok(Box::new(stage))
1095                }
1096            }
1097            .instrument(span),
1098        )))
1099    }
1100
1101    #[mz_ore::instrument(level = "debug")]
1102    pub(crate) async fn sequence_create_cluster(
1103        &mut self,
1104        session: &Session,
1105        CreateClusterPlan {
1106            name,
1107            variant,
1108            workload_class,
1109        }: CreateClusterPlan,
1110    ) -> Result<ExecuteResponse, AdapterError> {
1111        tracing::debug!("sequence_create_cluster");
1112
1113        let id_ts = self.get_catalog_write_ts().await;
1114        let id = self.catalog().allocate_user_cluster_id(id_ts).await?;
1115        // The catalog items for the introspection sources are shared between all replicas
1116        // of a compute instance, so we create them unconditionally during instance creation.
1117        // Whether a replica actually maintains introspection arrangements is determined by the
1118        // per-replica introspection configuration.
1119        let introspection_sources = BUILTINS::logs().collect();
1120        let cluster_variant = match &variant {
1121            CreateClusterVariant::Managed(plan) => {
1122                let logging = if let Some(config) = plan.compute.introspection {
1123                    ReplicaLogging {
1124                        log_logging: config.debugging,
1125                        interval: Some(config.interval),
1126                    }
1127                } else {
1128                    ReplicaLogging::default()
1129                };
1130                ClusterVariant::Managed(ClusterVariantManaged {
1131                    size: plan.size.clone(),
1132                    availability_zones: plan.availability_zones.clone(),
1133                    logging,
1134                    replication_factor: plan.replication_factor,
1135                    optimizer_feature_overrides: plan.optimizer_feature_overrides.clone(),
1136                    schedule: plan.schedule.clone(),
1137                    auto_scaling_strategy: None,
1138                    reconfiguration: None,
1139                    burst: None,
1140                })
1141            }
1142            CreateClusterVariant::Unmanaged(_) => ClusterVariant::Unmanaged,
1143        };
1144        let config = ClusterConfig {
1145            variant: cluster_variant,
1146            workload_class,
1147        };
1148        let ops = vec![catalog::Op::CreateCluster {
1149            id,
1150            name: name.clone(),
1151            introspection_sources,
1152            owner_id: *session.current_role_id(),
1153            config,
1154        }];
1155
1156        match variant {
1157            CreateClusterVariant::Managed(plan) => {
1158                self.sequence_create_managed_cluster(session, plan, id, name, ops)
1159                    .await
1160            }
1161            CreateClusterVariant::Unmanaged(plan) => {
1162                self.sequence_create_unmanaged_cluster(session, plan, id, name, ops)
1163                    .await
1164            }
1165        }
1166    }
1167
1168    #[mz_ore::instrument(level = "debug")]
1169    async fn sequence_create_managed_cluster(
1170        &mut self,
1171        session: &Session,
1172        CreateClusterManagedPlan {
1173            availability_zones,
1174            compute,
1175            replication_factor,
1176            size,
1177            optimizer_feature_overrides: _,
1178            schedule: _,
1179        }: CreateClusterManagedPlan,
1180        cluster_id: ClusterId,
1181        cluster_name: String,
1182        mut ops: Vec<catalog::Op>,
1183    ) -> Result<ExecuteResponse, AdapterError> {
1184        tracing::debug!("sequence_create_managed_cluster");
1185
1186        self.ensure_valid_azs(availability_zones.iter())?;
1187
1188        let role_id = session.role_metadata().current_role;
1189        self.catalog.ensure_valid_replica_size(
1190            &self
1191                .catalog()
1192                .get_role_allowed_cluster_sizes(&Some(role_id)),
1193            &size,
1194            false,
1195        )?;
1196
1197        // Eagerly validate the `max_replicas_per_cluster` limit.
1198        // `catalog_transact` will do this validation too, but allocating
1199        // replica IDs is expensive enough that we need to do this validation
1200        // before allocating replica IDs. See database-issues#6046.
1201        if cluster_id.is_user() {
1202            self.validate_resource_limit(
1203                0,
1204                i64::from(replication_factor),
1205                SystemVars::max_replicas_per_cluster,
1206                "cluster replica",
1207                MAX_REPLICAS_PER_CLUSTER.name(),
1208            )?;
1209        }
1210
1211        // Pre-allocate replica ids out-of-band via the durable allocator,
1212        // picking the id type from the owning cluster, so each replica's scoped
1213        // overrides can be folded into the create transaction below (the
1214        // overrides are keyed by the replica id). This mirrors how cluster and
1215        // item ids are allocated, so nothing allocates a replica id in-apply.
1216        let id_ts = self.get_catalog_write_ts().await;
1217        let replica_ids = self
1218            .catalog()
1219            .allocate_replica_ids(cluster_id, u64::from(replication_factor), id_ts)
1220            .await?;
1221
1222        let cluster_ctx = ClusterScopeContext {
1223            id: cluster_id.to_string(),
1224            name: cluster_name.clone(),
1225            is_builtin: cluster_id.is_system(),
1226        };
1227
1228        let mut replica_ctxs = Vec::new();
1229        for (replica_id, replica_name) in replica_ids
1230            .into_iter()
1231            .zip_eq((0..replication_factor).map(managed_cluster_replica_name))
1232        {
1233            let size_family = self.create_managed_cluster_replica_op(
1234                cluster_id,
1235                replica_id,
1236                replica_name.clone(),
1237                &compute,
1238                &size,
1239                &mut ops,
1240                if availability_zones.is_empty() {
1241                    None
1242                } else {
1243                    Some(availability_zones.as_ref())
1244                },
1245                false,
1246                *session.current_role_id(),
1247                ReplicaCreateDropReason::Manual,
1248            )?;
1249            replica_ctxs.push(ReplicaEvalContext {
1250                cluster_id,
1251                replica_id,
1252                cluster: cluster_ctx.clone(),
1253                replica: ReplicaScopeContext {
1254                    id: replica_id.to_string(),
1255                    name: replica_name,
1256                    is_builtin: cluster_id.is_system(),
1257                    size: size.clone(),
1258                    size_family,
1259                    cluster_id: cluster_id.to_string(),
1260                    cluster_name: cluster_name.clone(),
1261                },
1262            });
1263        }
1264
1265        // Fold the new cluster's cluster-coherent and the replicas' replica-local
1266        // scoped overrides into the create transaction. Folding (rather than a
1267        // post-transact resolve) makes the committed diff drive the
1268        // replica-scoped controller push before create_replica, which
1269        // render-frozen flags require, and gives the new cluster its optimizer
1270        // overrides for its first plan.
1271        let cluster_eval = ClusterEvalContext {
1272            cluster_id,
1273            cluster: cluster_ctx,
1274        };
1275        if let Some(scoped_op) = self.scoped_overrides_create_op(&[cluster_eval], &replica_ctxs) {
1276            ops.push(scoped_op);
1277        }
1278
1279        self.catalog_transact(Some(session), ops).await?;
1280
1281        Ok(ExecuteResponse::CreatedCluster)
1282    }
1283
1284    fn create_managed_cluster_replica_op(
1285        &self,
1286        cluster_id: ClusterId,
1287        replica_id: ReplicaId,
1288        name: String,
1289        compute: &mz_sql::plan::ComputeReplicaConfig,
1290        size: &String,
1291        ops: &mut Vec<Op>,
1292        azs: Option<&[String]>,
1293        pending: bool,
1294        owner_id: RoleId,
1295        reason: ReplicaCreateDropReason,
1296    ) -> Result<String, AdapterError> {
1297        let location = mz_catalog::durable::ReplicaLocation::Managed {
1298            // Concretized below from the cluster config; this intermediate value
1299            // is discarded, so the list is left empty here.
1300            availability_zones: Vec::new(),
1301            billed_as: None,
1302            internal: false,
1303            size: size.clone(),
1304            pending,
1305        };
1306
1307        let logging = if let Some(config) = compute.introspection {
1308            ReplicaLogging {
1309                log_logging: config.debugging,
1310                interval: Some(config.interval),
1311            }
1312        } else {
1313            ReplicaLogging::default()
1314        };
1315
1316        let config = ReplicaConfig {
1317            location: self.catalog().concretize_replica_location(
1318                location,
1319                &self
1320                    .catalog()
1321                    .get_role_allowed_cluster_sizes(&Some(owner_id)),
1322                azs,
1323                false,
1324            )?,
1325            compute: ComputeReplicaConfig { logging },
1326        };
1327
1328        // The caller pre-allocates `replica_id` out-of-band via the durable
1329        // allocator, so nothing allocates a replica id in-apply.
1330        //
1331        // Extract the size family before `config` moves into the op, for the
1332        // replica's scoped eval context.
1333        let size_family = match &config.location {
1334            ReplicaLocation::Managed(location) => location.allocation.family().to_string(),
1335            // A managed replica always concretizes to a managed location.
1336            ReplicaLocation::Unmanaged(_) => {
1337                unreachable!("managed cluster replica has a managed location")
1338            }
1339        };
1340
1341        ops.push(catalog::Op::CreateClusterReplica {
1342            cluster_id,
1343            replica_id,
1344            name,
1345            config,
1346            owner_id,
1347            reason,
1348        });
1349        Ok(size_family)
1350    }
1351
1352    fn ensure_valid_azs<'a, I: IntoIterator<Item = &'a String>>(
1353        &self,
1354        azs: I,
1355    ) -> Result<(), AdapterError> {
1356        let cat_azs = self.catalog().state().availability_zones();
1357        for az in azs.into_iter() {
1358            if !cat_azs.contains(az) {
1359                return Err(AdapterError::InvalidClusterReplicaAz {
1360                    az: az.to_string(),
1361                    expected: cat_azs.to_vec(),
1362                });
1363            }
1364        }
1365        Ok(())
1366    }
1367
1368    #[mz_ore::instrument(level = "debug")]
1369    async fn sequence_create_unmanaged_cluster(
1370        &mut self,
1371        session: &Session,
1372        CreateClusterUnmanagedPlan { replicas }: CreateClusterUnmanagedPlan,
1373        id: ClusterId,
1374        cluster_name: String,
1375        mut ops: Vec<catalog::Op>,
1376    ) -> Result<ExecuteResponse, AdapterError> {
1377        tracing::debug!("sequence_create_unmanaged_cluster");
1378
1379        self.ensure_valid_azs(replicas.iter().filter_map(|(_, r)| {
1380            if let mz_sql::plan::ReplicaConfig::Orchestrated {
1381                availability_zone: Some(az),
1382                ..
1383            } = &r
1384            {
1385                Some(az)
1386            } else {
1387                None
1388            }
1389        }))?;
1390
1391        // Eagerly validate the `max_replicas_per_cluster` limit.
1392        // `catalog_transact` will do this validation too, but allocating
1393        // replica IDs is expensive enough that we need to do this validation
1394        // before allocating replica IDs. See database-issues#6046.
1395        if id.is_user() {
1396            self.validate_resource_limit(
1397                0,
1398                i64::try_from(replicas.len()).unwrap_or(i64::MAX),
1399                SystemVars::max_replicas_per_cluster,
1400                "cluster replica",
1401                MAX_REPLICAS_PER_CLUSTER.name(),
1402            )?;
1403        }
1404
1405        // Pre-allocate replica ids out-of-band via the durable allocator,
1406        // picking the id type from the owning cluster, so each replica's scoped
1407        // overrides can be folded into the create transaction below. This
1408        // mirrors how cluster and item ids are allocated, so nothing allocates
1409        // a replica id in-apply.
1410        let id_ts = self.get_catalog_write_ts().await;
1411        let replica_ids = self
1412            .catalog()
1413            .allocate_replica_ids(id, u64::cast_from(replicas.len()), id_ts)
1414            .await?;
1415
1416        let cluster_ctx = ClusterScopeContext {
1417            id: id.to_string(),
1418            name: cluster_name.clone(),
1419            is_builtin: id.is_system(),
1420        };
1421        let mut replica_ctxs = Vec::new();
1422
1423        for (replica_id, (replica_name, replica_config)) in replica_ids.into_iter().zip_eq(replicas)
1424        {
1425            // If the AZ was not specified, choose one, round-robin, from the ones with
1426            // the lowest number of configured replicas for this cluster.
1427            let (compute, location) = match replica_config {
1428                mz_sql::plan::ReplicaConfig::Unorchestrated {
1429                    storagectl_addrs,
1430                    computectl_addrs,
1431                    compute,
1432                } => {
1433                    let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1434                        storagectl_addrs,
1435                        computectl_addrs,
1436                    };
1437                    (compute, location)
1438                }
1439                mz_sql::plan::ReplicaConfig::Orchestrated {
1440                    availability_zone,
1441                    billed_as,
1442                    compute,
1443                    internal,
1444                    size,
1445                } => {
1446                    // Only internal users have access to INTERNAL and BILLED AS
1447                    if !session.user().is_internal() && (internal || billed_as.is_some()) {
1448                        coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1449                    }
1450                    // BILLED AS implies the INTERNAL flag.
1451                    if billed_as.is_some() && !internal {
1452                        coord_bail!("must specify INTERNAL when specifying BILLED AS");
1453                    }
1454
1455                    let location = mz_catalog::durable::ReplicaLocation::Managed {
1456                        // The user-pinned `AVAILABILITY ZONE`, if any, as a zero-
1457                        // or one-element list.
1458                        availability_zones: availability_zone.into_iter().collect(),
1459                        billed_as,
1460                        internal,
1461                        size: size.clone(),
1462                        pending: false,
1463                    };
1464                    (compute, location)
1465                }
1466            };
1467
1468            let logging = if let Some(config) = compute.introspection {
1469                ReplicaLogging {
1470                    log_logging: config.debugging,
1471                    interval: Some(config.interval),
1472                }
1473            } else {
1474                ReplicaLogging::default()
1475            };
1476
1477            let role_id = session.role_metadata().current_role;
1478            let config = ReplicaConfig {
1479                location: self.catalog().concretize_replica_location(
1480                    location,
1481                    &self
1482                        .catalog()
1483                        .get_role_allowed_cluster_sizes(&Some(role_id)),
1484                    None,
1485                    false,
1486                )?,
1487                compute: ComputeReplicaConfig { logging },
1488            };
1489
1490            // Only orchestrated (managed-location) replicas have a size and size
1491            // family, so only they carry replica-local overrides.
1492            if let ReplicaLocation::Managed(location) = &config.location {
1493                replica_ctxs.push(ReplicaEvalContext {
1494                    cluster_id: id,
1495                    replica_id,
1496                    cluster: cluster_ctx.clone(),
1497                    replica: ReplicaScopeContext {
1498                        id: replica_id.to_string(),
1499                        name: replica_name.clone(),
1500                        is_builtin: id.is_system(),
1501                        size: location.size.clone(),
1502                        size_family: location.allocation.family().to_string(),
1503                        cluster_id: id.to_string(),
1504                        cluster_name: cluster_name.clone(),
1505                    },
1506                });
1507            }
1508
1509            ops.push(catalog::Op::CreateClusterReplica {
1510                cluster_id: id,
1511                replica_id,
1512                name: replica_name.clone(),
1513                config,
1514                owner_id: *session.current_role_id(),
1515                reason: ReplicaCreateDropReason::Manual,
1516            });
1517        }
1518
1519        // Fold the new cluster's and replicas' scoped overrides into the create
1520        // transaction (see the managed path for rationale).
1521        let cluster_eval = ClusterEvalContext {
1522            cluster_id: id,
1523            cluster: cluster_ctx,
1524        };
1525        if let Some(scoped_op) = self.scoped_overrides_create_op(&[cluster_eval], &replica_ctxs) {
1526            ops.push(scoped_op);
1527        }
1528
1529        self.catalog_transact(Some(session), ops).await?;
1530
1531        Ok(ExecuteResponse::CreatedCluster)
1532    }
1533
1534    #[mz_ore::instrument(level = "debug")]
1535    pub(crate) async fn sequence_create_cluster_replica(
1536        &mut self,
1537        session: &Session,
1538        CreateClusterReplicaPlan {
1539            name,
1540            cluster_id,
1541            config,
1542        }: CreateClusterReplicaPlan,
1543    ) -> Result<ExecuteResponse, AdapterError> {
1544        // Choose default AZ if necessary
1545        let (compute, location) = match config {
1546            mz_sql::plan::ReplicaConfig::Unorchestrated {
1547                storagectl_addrs,
1548                computectl_addrs,
1549                compute,
1550            } => {
1551                let location = mz_catalog::durable::ReplicaLocation::Unmanaged {
1552                    storagectl_addrs,
1553                    computectl_addrs,
1554                };
1555                (compute, location)
1556            }
1557            mz_sql::plan::ReplicaConfig::Orchestrated {
1558                availability_zone,
1559                billed_as,
1560                compute,
1561                internal,
1562                size,
1563            } => {
1564                let availability_zone = match availability_zone {
1565                    Some(az) => {
1566                        self.ensure_valid_azs([&az])?;
1567                        Some(az)
1568                    }
1569                    None => None,
1570                };
1571                let location = mz_catalog::durable::ReplicaLocation::Managed {
1572                    // The user-pinned `AVAILABILITY ZONE`, if any, as a zero- or
1573                    // one-element list.
1574                    availability_zones: availability_zone.into_iter().collect(),
1575                    billed_as,
1576                    internal,
1577                    size,
1578                    pending: false,
1579                };
1580                (compute, location)
1581            }
1582        };
1583
1584        let logging = if let Some(config) = compute.introspection {
1585            ReplicaLogging {
1586                log_logging: config.debugging,
1587                interval: Some(config.interval),
1588            }
1589        } else {
1590            ReplicaLogging::default()
1591        };
1592
1593        let role_id = session.role_metadata().current_role;
1594        let config = ReplicaConfig {
1595            location: self.catalog().concretize_replica_location(
1596                location,
1597                &self
1598                    .catalog()
1599                    .get_role_allowed_cluster_sizes(&Some(role_id)),
1600                // Planning ensures all replicas in this codepath
1601                // are unmanaged.
1602                None,
1603                false,
1604            )?,
1605            compute: ComputeReplicaConfig { logging },
1606        };
1607
1608        let cluster = self.catalog().get_cluster(cluster_id);
1609
1610        if let ReplicaLocation::Managed(ManagedReplicaLocation {
1611            internal,
1612            billed_as,
1613            ..
1614        }) = &config.location
1615        {
1616            // Only internal users have access to INTERNAL and BILLED AS
1617            if !session.user().is_internal() && (*internal || billed_as.is_some()) {
1618                coord_bail!("cannot specify INTERNAL or BILLED AS as non-internal user")
1619            }
1620            // Managed clusters require the INTERNAL flag.
1621            if cluster.is_managed() && !*internal {
1622                coord_bail!("must specify INTERNAL when creating a replica in a managed cluster");
1623            }
1624            // BILLED AS implies the INTERNAL flag.
1625            if billed_as.is_some() && !*internal {
1626                coord_bail!("must specify INTERNAL when specifying BILLED AS");
1627            }
1628        }
1629
1630        // Replicas have the same owner as their cluster. Extract the owned
1631        // cluster info we need before the borrow is dropped for the awaits below.
1632        let owner_id = cluster.owner_id();
1633
1634        let cluster_name = cluster.name.clone();
1635        let is_builtin = cluster_id.is_system();
1636
1637        // Pre-allocate the replica id out-of-band via the durable allocator,
1638        // picking the id type from the target cluster, which may be a system
1639        // cluster, so the replica's scoped overrides can be folded into the same
1640        // transaction. The overrides are keyed by replica id, and the
1641        // replica-scoped controller push must run before `create_replica`. This
1642        // mirrors how cluster and item ids are allocated, so nothing allocates a
1643        // replica id in-apply.
1644        let id_ts = self.get_catalog_write_ts().await;
1645        let replica_id = self
1646            .catalog()
1647            .allocate_replica_ids(cluster_id, 1, id_ts)
1648            .await?
1649            .into_element();
1650
1651        // Build the replica's eval context from the plan before `config` moves
1652        // into the op. Only managed replicas have a size (and size family).
1653        let replica_ctx = match &config.location {
1654            ReplicaLocation::Managed(location) => Some(ReplicaEvalContext {
1655                cluster_id,
1656                replica_id,
1657                cluster: ClusterScopeContext {
1658                    id: cluster_id.to_string(),
1659                    name: cluster_name.clone(),
1660                    is_builtin,
1661                },
1662                replica: ReplicaScopeContext {
1663                    id: replica_id.to_string(),
1664                    name: name.to_string(),
1665                    is_builtin,
1666                    size: location.size.clone(),
1667                    size_family: location.allocation.family().to_string(),
1668                    cluster_id: cluster_id.to_string(),
1669                    cluster_name,
1670                },
1671            }),
1672            ReplicaLocation::Unmanaged(_) => None,
1673        };
1674
1675        let mut ops = vec![catalog::Op::CreateClusterReplica {
1676            cluster_id,
1677            replica_id,
1678            name: name.clone(),
1679            config,
1680            owner_id,
1681            reason: ReplicaCreateDropReason::Manual,
1682        }];
1683
1684        // The cluster already exists, so only this replica's local overrides
1685        // need resolving. Fold them into the create transaction so the
1686        // replica-scoped push runs before `create_replica`.
1687        if let Some(replica_ctx) = replica_ctx {
1688            if let Some(scoped_op) = self.scoped_overrides_create_op(&[], &[replica_ctx]) {
1689                ops.push(scoped_op);
1690            }
1691        }
1692
1693        self.catalog_transact(Some(session), ops).await?;
1694
1695        Ok(ExecuteResponse::CreatedClusterReplica)
1696    }
1697
1698    /// When this is called by the automated cluster scheduling, `scheduling_decision_reason` should
1699    /// contain information on why is a cluster being turned On/Off. It will be forwarded to the
1700    /// `details` field of the audit log event that records creating or dropping replicas.
1701    ///
1702    /// # Panics
1703    ///
1704    /// Panics if the identified cluster is not a managed cluster.
1705    /// Panics if `new_config` is not a configuration for a managed cluster.
1706    pub(crate) async fn sequence_alter_cluster_managed_to_managed(
1707        &mut self,
1708        session: Option<&Session>,
1709        cluster_id: ClusterId,
1710        new_config: ClusterConfig,
1711        reason: ReplicaCreateDropReason,
1712        strategy: AlterClusterPlanStrategy,
1713    ) -> Result<NeedsFinalization, AdapterError> {
1714        let cluster = self.catalog.get_cluster(cluster_id);
1715        let name = cluster.name().to_string();
1716        let owner_id = cluster.owner_id();
1717
1718        let mut ops = vec![];
1719        let mut finalization_needed = NeedsFinalization::No;
1720
1721        let ClusterVariant::Managed(ClusterVariantManaged {
1722            size,
1723            availability_zones,
1724            logging,
1725            replication_factor,
1726            optimizer_feature_overrides: _,
1727            schedule: _,
1728            auto_scaling_strategy: _,
1729            reconfiguration: _,
1730            burst: _,
1731        }) = &cluster.config.variant
1732        else {
1733            panic!("expected existing managed cluster config");
1734        };
1735        // Clone the existing managed config out of the cluster so the immutable
1736        // catalog borrow can be released before the out-of-band replica id
1737        // allocation below, which needs mutable access to self.
1738        let size = size.clone();
1739        let availability_zones = availability_zones.clone();
1740        let logging = logging.clone();
1741        let replication_factor = *replication_factor;
1742        let ClusterVariant::Managed(new_managed) = &new_config.variant else {
1743            panic!("expected new managed cluster config");
1744        };
1745        let ClusterVariantManaged {
1746            size: new_size,
1747            replication_factor: new_replication_factor,
1748            availability_zones: new_availability_zones,
1749            logging: new_logging,
1750            optimizer_feature_overrides: _,
1751            schedule: _,
1752            auto_scaling_strategy: _,
1753            reconfiguration: _,
1754            burst: _,
1755        } = new_managed;
1756
1757        let role_id = session.map(|s| s.role_metadata().current_role);
1758        self.catalog.ensure_valid_replica_size(
1759            &self.catalog().get_role_allowed_cluster_sizes(&role_id),
1760            new_size,
1761            false,
1762        )?;
1763
1764        // check for active updates
1765        if cluster.replicas().any(|r| r.config.location.pending()) {
1766            return Err(AlterClusterWhilePendingReplicas);
1767        }
1768
1769        // Resolve existing replica ids by name before releasing the catalog
1770        // borrow, so the drop branches below can build their ops without it.
1771        let replica_id_by_name: BTreeMap<String, ReplicaId> = cluster
1772            .replicas()
1773            .map(|r| (r.name.clone(), r.replica_id))
1774            .collect();
1775
1776        let compute = mz_sql::plan::ComputeReplicaConfig {
1777            introspection: new_logging
1778                .interval
1779                .map(|interval| ComputeReplicaIntrospectionConfig {
1780                    debugging: new_logging.log_logging,
1781                    interval,
1782                }),
1783        };
1784
1785        // Eagerly validate the `max_replicas_per_cluster` limit.
1786        // `catalog_transact` will do this validation too, but allocating
1787        // replica IDs is expensive enough that we need to do this validation
1788        // before allocating replica IDs. See database-issues#6046.
1789        if *new_replication_factor > replication_factor {
1790            if cluster_id.is_user() {
1791                self.validate_resource_limit(
1792                    usize::cast_from(replication_factor),
1793                    i64::from(*new_replication_factor) - i64::from(replication_factor),
1794                    SystemVars::max_replicas_per_cluster,
1795                    "cluster replica",
1796                    MAX_REPLICAS_PER_CLUSTER.name(),
1797                )?;
1798            }
1799        }
1800
1801        // When the controller owns the managed replica set (master gate on, user
1802        // cluster), a non-record change reaching this path is replication-factor
1803        // only. Config-shape changes (size/logging/AZ) are reshaped into a durable
1804        // reconfiguration record before they get here. The controller reconciles
1805        // the replica set to the realized config's new count on its next tick, so
1806        // we update only the realized config and emit no create/drop here. Doing
1807        // both fights the controller. It derives replica names from the observed
1808        // set, so an adapter create by canonical `rN` can collide with a
1809        // controller-chosen name, and an adapter drop by canonical `rN` can miss a
1810        // churned one. With the gate off (or a system cluster, which the
1811        // controller never owns) the legacy path below still does the create/drop
1812        // directly.
1813        let controller_owns = ENABLE_CLUSTER_CONTROLLER
1814            .get(self.catalog().system_config().dyncfgs())
1815            && cluster_id.is_user();
1816
1817        // Count exactly as many replica ids as the branches below consume. The
1818        // config-changed branches recreate all replicas. A pure scale-up creates
1819        // only the delta. Scale-down and no-op create none. A controller-owned
1820        // alter emits no create/drop at all, so it must not allocate. Allocating
1821        // there burns those ids durably and throws them away. The controller
1822        // allocates its own when it materializes the change.
1823        let config_changed = new_managed.replica_config_shape()
1824            != ManagedReplicaConfigShape::new(&size, &availability_zones, &logging);
1825        let needed_replica_ids = if controller_owns {
1826            0
1827        } else if config_changed {
1828            *new_replication_factor
1829        } else if *new_replication_factor > replication_factor {
1830            *new_replication_factor - replication_factor
1831        } else {
1832            0
1833        };
1834        // Allocate the replica ids out-of-band via the durable allocator, only
1835        // after the eager limit validation above so a rejected alter allocates
1836        // nothing. Pick the id type from the target cluster, which may be a
1837        // system cluster. This mirrors how cluster and item ids are allocated,
1838        // so nothing allocates a replica id in-apply. Fetch the catalog write
1839        // timestamp lazily here, since it needs mutable access to self (the
1840        // cluster borrow above is already released) and scale-down, no-op, and
1841        // automated scheduling turn-off alters must not pay an oracle
1842        // round-trip just to allocate nothing.
1843        let mut new_replica_ids = if needed_replica_ids > 0 {
1844            let id_ts = self.get_catalog_write_ts().await;
1845            self.catalog()
1846                .allocate_replica_ids(cluster_id, u64::from(needed_replica_ids), id_ts)
1847                .await?
1848                .into_iter()
1849        } else {
1850            Vec::<ReplicaId>::new().into_iter()
1851        };
1852
1853        // Collect an eval context for each replica recreated below, so the alter
1854        // transaction folds the replicas' replica-scoped overrides the same way
1855        // the create paths do. ALTER CLUSTER SET (SIZE ...) to a different size
1856        // family flips size-family-keyed render-frozen flags, so the override
1857        // must reach the controller before the recreated replica renders. Only
1858        // the replica scope is folded. The cluster already exists and its
1859        // cluster-scoped overrides are unaffected by this alter.
1860        let cluster_ctx = ClusterScopeContext {
1861            id: cluster_id.to_string(),
1862            name: name.clone(),
1863            is_builtin: cluster_id.is_system(),
1864        };
1865        let mut replica_ctxs = Vec::new();
1866
1867        if controller_owns {
1868            // Defer all replica create/drop to the controller. Only the realized
1869            // config update below is applied here.
1870        } else if config_changed {
1871            self.ensure_valid_azs(new_availability_zones.iter())?;
1872            // If we're not doing a zero-downtime reconfig tear down all
1873            // replicas, create new ones else create the pending replicas and
1874            // return early asking for finalization
1875            match strategy {
1876                AlterClusterPlanStrategy::None => {
1877                    let replica_ids_and_reasons = (0..replication_factor)
1878                        .map(managed_cluster_replica_name)
1879                        .filter_map(|name| replica_id_by_name.get(&name).copied())
1880                        .map(|replica_id| {
1881                            catalog::DropObjectInfo::ClusterReplica((
1882                                cluster_id,
1883                                replica_id,
1884                                reason.clone(),
1885                            ))
1886                        })
1887                        .collect();
1888                    ops.push(catalog::Op::DropObjects(replica_ids_and_reasons));
1889                    for replica_name in
1890                        (0..*new_replication_factor).map(managed_cluster_replica_name)
1891                    {
1892                        // The replica id is pre-allocated above like the create
1893                        // paths so its scoped overrides can be folded below.
1894                        let replica_id = new_replica_ids
1895                            .next()
1896                            .expect("pre-allocated enough replica ids");
1897                        let size_family = self.create_managed_cluster_replica_op(
1898                            cluster_id,
1899                            replica_id,
1900                            replica_name.clone(),
1901                            &compute,
1902                            new_size,
1903                            &mut ops,
1904                            Some(new_availability_zones.as_ref()),
1905                            false,
1906                            owner_id,
1907                            reason.clone(),
1908                        )?;
1909                        replica_ctxs.push(ReplicaEvalContext {
1910                            cluster_id,
1911                            replica_id,
1912                            cluster: cluster_ctx.clone(),
1913                            replica: ReplicaScopeContext {
1914                                id: replica_id.to_string(),
1915                                name: replica_name,
1916                                is_builtin: cluster_id.is_system(),
1917                                size: new_size.clone(),
1918                                size_family,
1919                                cluster_id: cluster_id.to_string(),
1920                                cluster_name: cluster_ctx.name.clone(),
1921                            },
1922                        });
1923                    }
1924                }
1925                AlterClusterPlanStrategy::For(_) | AlterClusterPlanStrategy::UntilReady { .. } => {
1926                    for replica_name in
1927                        (0..*new_replication_factor).map(managed_cluster_replica_name)
1928                    {
1929                        let replica_name = format!("{replica_name}{PENDING_REPLICA_SUFFIX}");
1930                        let replica_id = new_replica_ids
1931                            .next()
1932                            .expect("pre-allocated enough replica ids");
1933                        let size_family = self.create_managed_cluster_replica_op(
1934                            cluster_id,
1935                            replica_id,
1936                            replica_name.clone(),
1937                            &compute,
1938                            new_size,
1939                            &mut ops,
1940                            Some(new_availability_zones.as_ref()),
1941                            true,
1942                            owner_id,
1943                            reason.clone(),
1944                        )?;
1945                        replica_ctxs.push(ReplicaEvalContext {
1946                            cluster_id,
1947                            replica_id,
1948                            cluster: cluster_ctx.clone(),
1949                            replica: ReplicaScopeContext {
1950                                id: replica_id.to_string(),
1951                                name: replica_name,
1952                                is_builtin: cluster_id.is_system(),
1953                                size: new_size.clone(),
1954                                size_family,
1955                                cluster_id: cluster_id.to_string(),
1956                                cluster_name: cluster_ctx.name.clone(),
1957                            },
1958                        });
1959                    }
1960                    finalization_needed = NeedsFinalization::Yes;
1961                }
1962            }
1963        } else if *new_replication_factor < replication_factor {
1964            // Adjust replica count down
1965            let replica_ids = (*new_replication_factor..replication_factor)
1966                .map(managed_cluster_replica_name)
1967                .filter_map(|name| replica_id_by_name.get(&name).copied())
1968                .map(|replica_id| {
1969                    catalog::DropObjectInfo::ClusterReplica((
1970                        cluster_id,
1971                        replica_id,
1972                        reason.clone(),
1973                    ))
1974                })
1975                .collect();
1976            ops.push(catalog::Op::DropObjects(replica_ids));
1977        } else if *new_replication_factor > replication_factor {
1978            // Adjust replica count up
1979            for replica_name in
1980                (replication_factor..*new_replication_factor).map(managed_cluster_replica_name)
1981            {
1982                let replica_id = new_replica_ids
1983                    .next()
1984                    .expect("pre-allocated enough replica ids");
1985                let size_family = self.create_managed_cluster_replica_op(
1986                    cluster_id,
1987                    replica_id,
1988                    replica_name.clone(),
1989                    &compute,
1990                    new_size,
1991                    &mut ops,
1992                    // AVAILABILITY ZONES hasn't changed, so existing replicas don't need to be
1993                    // rescheduled.
1994                    Some(new_availability_zones.as_ref()),
1995                    false,
1996                    owner_id,
1997                    reason.clone(),
1998                )?;
1999                replica_ctxs.push(ReplicaEvalContext {
2000                    cluster_id,
2001                    replica_id,
2002                    cluster: cluster_ctx.clone(),
2003                    replica: ReplicaScopeContext {
2004                        id: replica_id.to_string(),
2005                        name: replica_name,
2006                        is_builtin: cluster_id.is_system(),
2007                        size: new_size.clone(),
2008                        size_family,
2009                        cluster_id: cluster_id.to_string(),
2010                        cluster_name: cluster_ctx.name.clone(),
2011                    },
2012                });
2013            }
2014        }
2015
2016        // If finalization is needed, finalization should update the cluster
2017        // config. Otherwise the config write happens here. With the controller
2018        // owning the cluster, a record still in progress belongs to a live,
2019        // converging reconfiguration this write didn't touch: carry it through
2020        // untouched. Without (gate off, or a system cluster), such a record is
2021        // orphaned, so retain it as cancelled with the matching audit intent
2022        // rather than risk a bogus revival if the gate comes back on.
2023        //
2024        // NOTE: `handle_scheduling_decisions` also calls this function and
2025        // bypasses the sequencer's replication-factor guard. It defers its
2026        // flips itself while a reconfiguration is in progress.
2027        match finalization_needed {
2028            NeedsFinalization::No => {
2029                let mut new_config = new_config;
2030                let reconfiguration_audit = if controller_owns {
2031                    None
2032                } else {
2033                    cancel_carried_reconfiguration(&mut new_config)
2034                };
2035                ops.push(catalog::Op::UpdateClusterConfig {
2036                    id: cluster_id,
2037                    name: name.clone(),
2038                    config: new_config,
2039                    reconfiguration_audit,
2040                    burst_audit: None,
2041                });
2042            }
2043            NeedsFinalization::Yes => {}
2044        }
2045
2046        // Fold the recreated replicas' replica-scoped overrides into the same
2047        // transaction, so the committed diff drives the replica-scoped controller
2048        // push before create_replica. Render-frozen flags (chosen at
2049        // arrangement-build time) require the override to land before the replica
2050        // renders. Scale-down and no-op alters recreate no replicas, so this is
2051        // empty and folds nothing.
2052        if let Some(scoped_op) = self.scoped_overrides_create_op(&[], &replica_ctxs) {
2053            ops.push(scoped_op);
2054        }
2055
2056        self.catalog_transact(session, ops).await?;
2057        Ok(finalization_needed)
2058    }
2059
2060    /// # Panics
2061    ///
2062    /// Panics if `new_config` is not a configuration for a managed cluster.
2063    async fn sequence_alter_cluster_unmanaged_to_managed(
2064        &mut self,
2065        session: &Session,
2066        cluster_id: ClusterId,
2067        mut new_config: ClusterConfig,
2068        options: PlanClusterOption,
2069    ) -> Result<(), AdapterError> {
2070        let cluster = self.catalog.get_cluster(cluster_id);
2071        let cluster_name = cluster.name().to_string();
2072
2073        let ClusterVariant::Managed(ClusterVariantManaged {
2074            size: new_size,
2075            replication_factor: new_replication_factor,
2076            availability_zones: new_availability_zones,
2077            logging: _,
2078            optimizer_feature_overrides: _,
2079            schedule: _,
2080            auto_scaling_strategy: _,
2081            reconfiguration: _,
2082            burst: _,
2083        }) = &mut new_config.variant
2084        else {
2085            panic!("expected new managed cluster config");
2086        };
2087
2088        // Validate replication factor parameter
2089        let user_replica_count = cluster
2090            .user_replicas()
2091            .count()
2092            .try_into()
2093            .expect("must_fit");
2094        match options.replication_factor {
2095            AlterOptionParameter::Set(_) => {
2096                // Validate that the replication factor matches the current length only if specified.
2097                if user_replica_count != *new_replication_factor {
2098                    coord_bail!(
2099                        "REPLICATION FACTOR {new_replication_factor} does not match number of replicas ({user_replica_count})"
2100                    );
2101                }
2102            }
2103            _ => {
2104                *new_replication_factor = user_replica_count;
2105            }
2106        }
2107
2108        let mut names = BTreeSet::new();
2109        let mut sizes = BTreeSet::new();
2110
2111        self.ensure_valid_azs(new_availability_zones.iter())?;
2112
2113        // Validate per-replica configuration
2114        for replica in cluster.user_replicas() {
2115            names.insert(replica.name.clone());
2116            match &replica.config.location {
2117                ReplicaLocation::Unmanaged(_) => coord_bail!(
2118                    "Cannot convert unmanaged cluster with unmanaged replicas to managed cluster"
2119                ),
2120                ReplicaLocation::Managed(location) => {
2121                    sizes.insert(location.size.clone());
2122
2123                    // An unmanaged cluster's replica carries its single
2124                    // user-pinned AZ (if any) as the sole entry; every pin must
2125                    // fall within the managed cluster's `AVAILABILITY ZONES`.
2126                    for az in &location.availability_zones {
2127                        if !new_availability_zones.contains(az) {
2128                            coord_bail!(
2129                                "unmanaged replica has availability zone {az} which is not \
2130                                in managed {new_availability_zones:?}"
2131                            )
2132                        }
2133                    }
2134                }
2135            }
2136        }
2137
2138        if sizes.is_empty() {
2139            assert!(
2140                cluster.user_replicas().next().is_none(),
2141                "Cluster should not have replicas"
2142            );
2143            // We didn't collect any size, so the user has to name it.
2144            match &options.size {
2145                AlterOptionParameter::Reset | AlterOptionParameter::Unchanged => {
2146                    coord_bail!("Missing SIZE for empty cluster")
2147                }
2148                AlterOptionParameter::Set(_) => {} // Was set within the calling function.
2149            }
2150        } else if sizes.len() == 1 {
2151            let size = sizes.into_iter().next().expect("must exist");
2152            match &options.size {
2153                AlterOptionParameter::Set(sz) if *sz != size => {
2154                    coord_bail!("Cluster replicas of size {size} do not match expected SIZE {sz}");
2155                }
2156                _ => *new_size = size,
2157            }
2158        } else {
2159            let formatted = sizes
2160                .iter()
2161                .map(String::as_str)
2162                .collect::<Vec<_>>()
2163                .join(", ");
2164            coord_bail!(
2165                "Cannot convert unmanaged cluster to managed, non-unique replica sizes: {formatted}"
2166            );
2167        }
2168
2169        for i in 0..*new_replication_factor {
2170            let name = managed_cluster_replica_name(i);
2171            names.remove(&name);
2172        }
2173        if !names.is_empty() {
2174            let formatted = names
2175                .iter()
2176                .map(String::as_str)
2177                .collect::<Vec<_>>()
2178                .join(", ");
2179            coord_bail!(
2180                "Cannot convert unmanaged cluster to managed, invalid replica names: {formatted}"
2181            );
2182        }
2183
2184        let ops = vec![catalog::Op::UpdateClusterConfig {
2185            id: cluster_id,
2186            name: cluster_name,
2187            config: new_config,
2188            reconfiguration_audit: None,
2189            burst_audit: None,
2190        }];
2191
2192        self.catalog_transact(Some(session), ops).await?;
2193        Ok(())
2194    }
2195
2196    async fn sequence_alter_cluster_managed_to_unmanaged(
2197        &mut self,
2198        session: &Session,
2199        cluster_id: ClusterId,
2200        new_config: ClusterConfig,
2201    ) -> Result<(), AdapterError> {
2202        let cluster = self.catalog().get_cluster(cluster_id);
2203
2204        // The unmanaged variant has no reconfiguration field, so converting
2205        // would silently drop an in-progress record with no terminal status
2206        // and no audit event, and strand any overlap replicas the controller
2207        // already created. Refuse instead: the user can cancel (ALTER back to
2208        // the realized size) or wait for the record to settle first.
2209        if let ClusterVariant::Managed(managed) = &cluster.config.variant {
2210            if managed
2211                .reconfiguration
2212                .as_ref()
2213                .is_some_and(|record| record.is_in_progress())
2214            {
2215                return Err(AdapterError::AlterClusterUnmanagedWhileReconfiguring);
2216            }
2217        }
2218
2219        let ops = vec![catalog::Op::UpdateClusterConfig {
2220            id: cluster_id,
2221            name: cluster.name().to_string(),
2222            config: new_config,
2223            reconfiguration_audit: None,
2224            burst_audit: None,
2225        }];
2226
2227        self.catalog_transact(Some(session), ops).await?;
2228        Ok(())
2229    }
2230
2231    async fn sequence_alter_cluster_unmanaged_to_unmanaged(
2232        &mut self,
2233        session: &Session,
2234        cluster_id: ClusterId,
2235        new_config: ClusterConfig,
2236        replicas: AlterOptionParameter<Vec<(String, mz_sql::plan::ReplicaConfig)>>,
2237    ) -> Result<(), AdapterError> {
2238        if !matches!(replicas, AlterOptionParameter::Unchanged) {
2239            coord_bail!("Cannot alter replicas in unmanaged cluster");
2240        }
2241
2242        let cluster = self.catalog().get_cluster(cluster_id);
2243
2244        let ops = vec![catalog::Op::UpdateClusterConfig {
2245            id: cluster_id,
2246            name: cluster.name().to_string(),
2247            config: new_config,
2248            reconfiguration_audit: None,
2249            burst_audit: None,
2250        }];
2251
2252        self.catalog_transact(Some(session), ops).await?;
2253        Ok(())
2254    }
2255
2256    pub(crate) async fn sequence_alter_cluster_rename(
2257        &mut self,
2258        ctx: &mut ExecuteContext,
2259        AlterClusterRenamePlan { id, name, to_name }: AlterClusterRenamePlan,
2260    ) -> Result<ExecuteResponse, AdapterError> {
2261        let op = Op::RenameCluster {
2262            id,
2263            name,
2264            to_name,
2265            check_reserved_names: true,
2266        };
2267        match self
2268            .catalog_transact_with_ddl_transaction(ctx, vec![op], |_, _| Box::pin(async {}))
2269            .await
2270        {
2271            Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
2272            Err(err) => Err(err),
2273        }
2274    }
2275
2276    pub(crate) async fn sequence_alter_cluster_swap(
2277        &mut self,
2278        ctx: &mut ExecuteContext,
2279        AlterClusterSwapPlan {
2280            id_a,
2281            id_b,
2282            name_a,
2283            name_b,
2284            name_temp,
2285        }: AlterClusterSwapPlan,
2286    ) -> Result<ExecuteResponse, AdapterError> {
2287        let op_a = Op::RenameCluster {
2288            id: id_a,
2289            name: name_a.clone(),
2290            to_name: name_temp.clone(),
2291            check_reserved_names: false,
2292        };
2293        let op_b = Op::RenameCluster {
2294            id: id_b,
2295            name: name_b.clone(),
2296            to_name: name_a,
2297            check_reserved_names: false,
2298        };
2299        let op_temp = Op::RenameCluster {
2300            id: id_a,
2301            name: name_temp,
2302            to_name: name_b,
2303            check_reserved_names: false,
2304        };
2305
2306        match self
2307            .catalog_transact_with_ddl_transaction(ctx, vec![op_a, op_b, op_temp], |_, _| {
2308                Box::pin(async {})
2309            })
2310            .await
2311        {
2312            Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::Cluster)),
2313            Err(err) => Err(err),
2314        }
2315    }
2316
2317    pub(crate) async fn sequence_alter_cluster_replica_rename(
2318        &mut self,
2319        session: &Session,
2320        AlterClusterReplicaRenamePlan {
2321            cluster_id,
2322            replica_id,
2323            name,
2324            to_name,
2325        }: AlterClusterReplicaRenamePlan,
2326    ) -> Result<ExecuteResponse, AdapterError> {
2327        let op = catalog::Op::RenameClusterReplica {
2328            cluster_id,
2329            replica_id,
2330            name,
2331            to_name,
2332        };
2333        match self.catalog_transact(Some(session), vec![op]).await {
2334            Ok(()) => Ok(ExecuteResponse::AlteredObject(ObjectType::ClusterReplica)),
2335            Err(err) => Err(err),
2336        }
2337    }
2338
2339    /// Convert a [`AlterSetClusterPlan`] to a sequence of catalog operators and adjust state.
2340    pub(crate) async fn sequence_alter_set_cluster(
2341        &self,
2342        _session: &Session,
2343        AlterSetClusterPlan { id, set_cluster: _ }: AlterSetClusterPlan,
2344    ) -> Result<ExecuteResponse, AdapterError> {
2345        // TODO: This function needs to be implemented.
2346
2347        // Satisfy Clippy that this is an async func.
2348        async {}.await;
2349        let entry = self.catalog().get_entry(&id);
2350        match entry.item().typ() {
2351            _ => {
2352                // Unexpected; planner permitted unsupported plan.
2353                Err(AdapterError::Unsupported("ALTER SET CLUSTER"))
2354            }
2355        }
2356    }
2357}
2358
2359fn managed_cluster_replica_name(index: u32) -> String {
2360    format!("r{}", index + 1)
2361}
2362
2363/// Which reconfiguration-target dimensions an `ALTER` left unset (`Unchanged`).
2364/// Drives [`fold_reconfiguration_target`]. Logging is two sub-dimensions
2365/// because `INTROSPECTION DEBUGGING` and `INTROSPECTION INTERVAL` are
2366/// independently alterable.
2367struct ReconfigurationDimensionsUnchanged {
2368    size: bool,
2369    replication_factor: bool,
2370    availability_zones: bool,
2371    log_logging: bool,
2372    interval: bool,
2373}
2374
2375/// Retains a stale in-progress reconfiguration record carried by a legacy-path
2376/// config write as cancelled, returning the audit intent to declare with the
2377/// write.
2378///
2379/// The legacy ALTER paths (controller gate off) change the realized config
2380/// directly and know nothing about reconfiguration records. Nothing on those
2381/// paths ever settles a record, and carrying an in-progress one forward invites
2382/// a bogus revival, up to a forced cut-over to an obsolete target, if the gate
2383/// is turned back on later. A record can only be in progress here if it was
2384/// written while the gate was on.
2385fn cancel_carried_reconfiguration(config: &mut ClusterConfig) -> Option<ReconfigurationAudit> {
2386    let ClusterVariant::Managed(managed) = &mut config.variant else {
2387        return None;
2388    };
2389    let record = managed.reconfiguration.as_mut()?;
2390    if !record.is_in_progress() {
2391        return None;
2392    }
2393    record.status = ReconfigurationStatus::Cancelled;
2394    Some(ReconfigurationAudit::Cancelled)
2395}
2396
2397/// Whether an `ALTER` statement sets a replica config shape dimension (`SIZE`,
2398/// `AVAILABILITY ZONES`, or either `INTROSPECTION` option), the changes that
2399/// need a durable `reconfiguration` record and a hydrate-overlap.
2400///
2401/// A statement-level check, used while a reconfiguration is in flight: an
2402/// `ALTER` back to the realized shape sets a shape option without changing its
2403/// value, yet must reach the reshape path to cancel the record. With nothing
2404/// in flight the routing compares values instead (see
2405/// `sequence_alter_cluster_stage`).
2406fn alter_changes_replica_shape(options: &PlanClusterOption) -> bool {
2407    use mz_sql::plan::AlterOptionParameter::Unchanged;
2408    let PlanClusterOption {
2409        availability_zones,
2410        introspection_debugging,
2411        introspection_interval,
2412        managed: _,
2413        replicas: _,
2414        replication_factor: _,
2415        size,
2416        schedule: _,
2417        workload_class: _,
2418    } = options;
2419    !matches!(size, Unchanged)
2420        || !matches!(availability_zones, Unchanged)
2421        || !matches!(introspection_debugging, Unchanged)
2422        || !matches!(introspection_interval, Unchanged)
2423}
2424
2425/// Fold a new `ALTER` onto an in-flight reconfiguration target.
2426///
2427/// `new_target` was built against the *realized* config, so any dimension the
2428/// `ALTER` left `Unchanged` carries the realized (pre-reconfiguration) value. When
2429/// a reconfiguration is in flight (`in_flight` is `Some`), the realized config is
2430/// the pre-reconfiguration shape, so for each `Unchanged` dimension we instead
2431/// keep the in-flight target's value. Only dimensions the `ALTER` explicitly set
2432/// re-target. With nothing in flight (`in_flight` is `None`) the target is exactly
2433/// `new_target`. This is what keeps an `ALTER` that touches one dimension (e.g.
2434/// AZ-only) from silently reverting the in-flight transition along every dimension
2435/// it did not mention.
2436///
2437/// Replication factor folds the same way, but only matters for the
2438/// nothing-in-flight case: a change to it while a reconfiguration is in
2439/// flight is refused before an `ALTER` reaches here, so
2440/// `unchanged.replication_factor` is always `true` when `in_flight` is
2441/// `Some`.
2442fn fold_reconfiguration_target(
2443    in_flight: Option<&ReconfigurationTarget>,
2444    new_target: ReconfigurationTarget,
2445    unchanged: ReconfigurationDimensionsUnchanged,
2446) -> ReconfigurationTarget {
2447    let Some(prev) = in_flight else {
2448        return new_target;
2449    };
2450    ReconfigurationTarget {
2451        size: if unchanged.size {
2452            prev.size.clone()
2453        } else {
2454            new_target.size
2455        },
2456        replication_factor: if unchanged.replication_factor {
2457            prev.replication_factor
2458        } else {
2459            new_target.replication_factor
2460        },
2461        availability_zones: if unchanged.availability_zones {
2462            prev.availability_zones.clone()
2463        } else {
2464            new_target.availability_zones
2465        },
2466        logging: ReplicaLogging {
2467            log_logging: if unchanged.log_logging {
2468                prev.logging.log_logging
2469            } else {
2470                new_target.logging.log_logging
2471            },
2472            interval: if unchanged.interval {
2473                prev.logging.interval
2474            } else {
2475                new_target.logging.interval
2476            },
2477        },
2478    }
2479}
2480
2481/// The type of finalization needed after an
2482/// operation such as alter_cluster_managed_to_managed.
2483#[derive(PartialEq)]
2484pub(crate) enum NeedsFinalization {
2485    /// Wait for the provided duration before finalizing
2486    Yes,
2487    No,
2488}
2489
2490#[cfg(test)]
2491mod tests {
2492    use mz_controller::clusters::ReplicaLogging;
2493    use mz_controller_types::DEFAULT_REPLICA_LOGGING_INTERVAL;
2494
2495    use super::*;
2496
2497    fn target(size: &str, rf: u32, azs: &[&str], log_logging: bool) -> ReconfigurationTarget {
2498        ReconfigurationTarget {
2499            size: size.to_string(),
2500            replication_factor: rf,
2501            availability_zones: azs.iter().map(|s| s.to_string()).collect(),
2502            logging: ReplicaLogging {
2503                log_logging,
2504                interval: Some(DEFAULT_REPLICA_LOGGING_INTERVAL),
2505            },
2506        }
2507    }
2508
2509    fn all_changed() -> ReconfigurationDimensionsUnchanged {
2510        ReconfigurationDimensionsUnchanged {
2511            size: false,
2512            replication_factor: false,
2513            availability_zones: false,
2514            log_logging: false,
2515            interval: false,
2516        }
2517    }
2518
2519    fn all_unchanged() -> ReconfigurationDimensionsUnchanged {
2520        ReconfigurationDimensionsUnchanged {
2521            size: true,
2522            replication_factor: true,
2523            availability_zones: true,
2524            log_logging: true,
2525            interval: true,
2526        }
2527    }
2528
2529    #[mz_ore::test]
2530    fn fold_with_no_record_takes_new_target() {
2531        // No reconfiguration in flight: the target is exactly the new one.
2532        let new = target("200cc", 3, &["az1"], true);
2533        let folded = fold_reconfiguration_target(None, new.clone(), all_changed());
2534        assert_eq!(folded, new);
2535    }
2536
2537    #[mz_ore::test]
2538    fn fold_rf_only_keeps_in_flight_shape() {
2539        // A 200cc size change is in flight. A later rf-only ALTER must NOT revert
2540        // the in-flight size/AZ/logging back to the realized (100cc) values that
2541        // `new_target` carries for the dimensions the ALTER left unchanged.
2542        let in_flight = target("200cc", 1, &["az2"], true);
2543        // new_target reflects realized 100cc/az1 for every dimension but rf, which
2544        // the ALTER set to 5.
2545        let new = target("100cc", 5, &["az1"], false);
2546        let unchanged = ReconfigurationDimensionsUnchanged {
2547            size: true,
2548            replication_factor: false,
2549            availability_zones: true,
2550            log_logging: true,
2551            interval: true,
2552        };
2553        let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2554        // The in-flight size/AZ/logging survive. Only rf is re-targeted.
2555        assert_eq!(folded, target("200cc", 5, &["az2"], true));
2556    }
2557
2558    #[mz_ore::test]
2559    fn fold_with_all_set_overwrites_every_dimension() {
2560        // Every dimension explicitly set: the fold takes all of new_target.
2561        let in_flight = target("200cc", 1, &["az2"], true);
2562        let new = target("400cc", 9, &["az9"], false);
2563        let folded = fold_reconfiguration_target(Some(&in_flight), new.clone(), all_changed());
2564        assert_eq!(folded, new);
2565    }
2566
2567    #[mz_ore::test]
2568    fn fold_all_unchanged_is_alter_back_to_in_flight() {
2569        // An all-unchanged fold keeps the in-flight target intact rather than
2570        // reverting it to the realized shape. Unreachable from the `ALTER`
2571        // path (non-shape statements no longer reach the fold), pinned as a
2572        // property of the pure function.
2573        let in_flight = target("200cc", 2, &["az2"], true);
2574        let realized_shaped = target("100cc", 1, &["az1"], false);
2575        let folded =
2576            fold_reconfiguration_target(Some(&in_flight), realized_shaped, all_unchanged());
2577        assert_eq!(folded, in_flight);
2578    }
2579
2580    #[mz_ore::test]
2581    fn fold_logging_subdimensions_fold_independently() {
2582        // An interval change is in flight. A later ALTER that sets only
2583        // INTROSPECTION DEBUGGING must not revert the in-flight interval to the
2584        // realized value that `new_target` carries for options the ALTER left
2585        // unset.
2586        let mut in_flight = target("100cc", 1, &["az1"], false);
2587        in_flight.logging.interval = Some(Duration::from_secs(5));
2588        let new = target("100cc", 1, &["az1"], true);
2589        let unchanged = ReconfigurationDimensionsUnchanged {
2590            size: true,
2591            replication_factor: true,
2592            availability_zones: true,
2593            log_logging: false,
2594            interval: true,
2595        };
2596        let folded = fold_reconfiguration_target(Some(&in_flight), new, unchanged);
2597        assert_eq!(
2598            folded.logging,
2599            ReplicaLogging {
2600                log_logging: true,
2601                interval: Some(Duration::from_secs(5)),
2602            }
2603        );
2604    }
2605}