From fae682e8a8d198dce7953c7599964da41843298f Mon Sep 17 00:00:00 2001 From: Aljoscha Krettek Date: Thu, 5 Nov 2026 09:45:39 +0000 Subject: [PATCH 1/2] adapter: Realize accepted cluster sizes after allow-list changes The cluster controller realizes configurations accepted by DDL. Checking the owner's current size allow-list again can strand refresh schedules and hydration bursts, and block allowed resizes in the same decision batch. Concretize controller replicas without a size allow-list. Keep allocation lookup, disabled-size checks, availability-zone configuration, and transactional resource limits unchanged. DDL remains responsible for authorizing requested sizes. Extend cluster-controller testdrive coverage for refresh wake-up and hydration bursts after the allow-list shrinks, including DDL rejection and an allowed resize while the retained burst is active. Fixes SQL-564. Amp-Thread-ID: https://ampcode.com/threads/T-01a1ab00-89fa-7525-ad7f-a802e235a04b Co-authored-by: Amp --- src/adapter/src/coord/cluster_controller.rs | 13 ++++---- test/testdrive/cluster-controller.td | 33 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/adapter/src/coord/cluster_controller.rs b/src/adapter/src/coord/cluster_controller.rs index a51d6b3a1f399..780910f5aed68 100644 --- a/src/adapter/src/coord/cluster_controller.rs +++ b/src/adapter/src/coord/cluster_controller.rs @@ -985,14 +985,11 @@ impl Coordinator { } else { Some(&shape.availability_zones.0) }; - let location = self.catalog().concretize_replica_location( - location, - &self - .catalog() - .get_role_allowed_cluster_sizes(&Some(owner_id)), - azs, - false, - )?; + // DDL authorizes sizes before committing the configuration. Reconcile + // must still realize it if the owner's size allow-list later shrinks. + let location = self + .catalog() + .concretize_replica_location(location, &vec![], azs, false)?; let config = mz_controller::clusters::ReplicaConfig { location, diff --git a/test/testdrive/cluster-controller.td b/test/testdrive/cluster-controller.td index 8b07b1618f3ac..2b5ee57634c20 100644 --- a/test/testdrive/cluster-controller.td +++ b/test/testdrive/cluster-controller.td @@ -1052,6 +1052,16 @@ ALTER SYSTEM SET unsafe_enable_unstable_dependencies = false > SELECT count(*) FROM mz_cluster_replicas r JOIN mz_clusters c ON r.cluster_id = c.id WHERE c.name = 'cc_sched' 0 +# SQL-564: scheduling an accepted configuration must not reauthorize its size. +$ postgres-execute connection=mz_system +ALTER SYSTEM SET allowed_cluster_replica_sizes = 'scale=1,workers=2' + +! CREATE CLUSTER cc_disallowed SIZE 'scale=1,workers=1' +contains:unknown cluster replica size scale=1,workers=1 + +! ALTER CLUSTER cc_sched SET (SIZE = 'scale=2,workers=2') +contains:unknown cluster replica size scale=2,workers=2 + # Bind a REFRESH EVERY materialized view: the cluster is now inside a refresh window # (it must rehydrate ahead of each refresh), so the controller brings up exactly one # replica at the cluster size. The replication factor still reads 0 (it is the @@ -1092,6 +1102,9 @@ drop retired true > DROP TABLE sched_t > DROP CLUSTER cc_sched +$ postgres-execute connection=mz_system +ALTER SYSTEM RESET allowed_cluster_replica_sizes + # A config-shape ALTER on a scheduled cluster takes the direct path: no # reconfiguration record is written (so no lifecycle transition is audited), # the realized size advances immediately, and the controller reconciles the @@ -1382,6 +1395,14 @@ ALTER SYSTEM SET unsafe_enable_unstable_dependencies = true > CREATE CLUSTER cc_burst_e2e (SIZE 'scale=1,workers=1', REPLICATION FACTOR 1, AUTO SCALING STRATEGY = (ON HYDRATION (HYDRATION SIZE = 'scale=2,workers=2', LINGER DURATION = '600s'))) +# The retained hydration size remains realizable after the allow-list shrinks, +# but a new policy using it must still be rejected by DDL authorization. +$ postgres-execute connection=mz_system +ALTER SYSTEM SET allowed_cluster_replica_sizes = 'scale=1,workers=1', 'scale=1,workers=2' + +! CREATE CLUSTER cc_burst_disallowed (SIZE 'scale=1,workers=1', AUTO SCALING STRATEGY = (ON HYDRATION (HYDRATION SIZE = 'scale=2,workers=2'))) +contains:unknown cluster replica size scale=2,workers=2 + > CREATE TABLE cc_burst_e2e_t (id int) > INSERT INTO cc_burst_e2e_t VALUES (1) @@ -1416,6 +1437,15 @@ started scale=2,workers=2 ! ALTER CLUSTER cc_burst_e2e SET (MANAGED = false) contains:cannot convert cluster to unmanaged while a hydration burst is in progress +# An allowed resize must progress without waiting for the retained burst to +# retire. The sleeping view keeps all three replicas unhydrated. +> ALTER CLUSTER cc_burst_e2e SET (SIZE = 'scale=1,workers=2') + +> SELECT r.size FROM mz_cluster_replicas r JOIN mz_clusters c ON r.cluster_id = c.id WHERE c.name = 'cc_burst_e2e' +scale=1,workers=1 +scale=1,workers=2 +scale=2,workers=2 + # RESET tears the burst down: the record clears with a finished event, the # burst replica is retired, and the relation row disappears with the policy. > ALTER CLUSTER cc_burst_e2e RESET (AUTO SCALING STRATEGY) @@ -1433,6 +1463,9 @@ no-longer-warranted > DROP TABLE cc_burst_e2e_t +$ postgres-execute connection=mz_system +ALTER SYSTEM RESET allowed_cluster_replica_sizes + # NOTE: The next section covers a singleton source riding a hydration burst, # which requires the two-cluster replica topology: on a unified cluster the # blocking mz_sleep gate starves co-hosted sources and their command From 1609f3c0f7014ed631800761a5b896d3e0b6c186 Mon Sep 17 00:00:00 2001 From: Aljoscha Krettek Date: Thu, 5 Nov 2026 11:45:28 +0000 Subject: [PATCH 2/2] adapter: Validate sizes when converting empty clusters An empty unmanaged cluster has no accepted replica size to inherit. Validate its explicit SIZE against the invoking role's allow-list and the size map before committing the managed configuration. Keep sizes adopted from existing replicas valid after the allow-list shrinks. Extend cluster-controller coverage for disallowed, unknown, and disabled sizes, successful empty-cluster conversion, and retention of an existing size. Amp-Thread-ID: https://ampcode.com/threads/T-01a1ab00-89fa-7525-ad7f-a802e235a04b Co-authored-by: Amp --- .../src/coord/sequencer/inner/cluster.rs | 9 ++++- test/testdrive/cluster-controller.td | 39 ++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/adapter/src/coord/sequencer/inner/cluster.rs b/src/adapter/src/coord/sequencer/inner/cluster.rs index 5936260072185..93a2cb3718b56 100644 --- a/src/adapter/src/coord/sequencer/inner/cluster.rs +++ b/src/adapter/src/coord/sequencer/inner/cluster.rs @@ -1690,7 +1690,14 @@ impl Coordinator { AlterOptionParameter::Reset | AlterOptionParameter::Unchanged => { coord_bail!("Missing SIZE for empty cluster") } - AlterOptionParameter::Set(_) => {} // Was set within the calling function. + AlterOptionParameter::Set(size) => { + let role_id = Some(session.role_metadata().current_role); + self.catalog.ensure_valid_replica_size( + &self.catalog().get_role_allowed_cluster_sizes(&role_id), + size, + false, + )?; + } } } else if sizes.len() == 1 { let size = sizes.into_iter().next().expect("must exist"); diff --git a/test/testdrive/cluster-controller.td b/test/testdrive/cluster-controller.td index 2b5ee57634c20..8dcabec520b61 100644 --- a/test/testdrive/cluster-controller.td +++ b/test/testdrive/cluster-controller.td @@ -1620,15 +1620,44 @@ ALTER SYSTEM RESET enable_unified_cluster # ----- Direct-path invariants on scheduled clusters ----- -# Converting a nonempty unmanaged cluster to ON REFRESH adopts its replica -# rather than failing replication-factor validation, and the controller then -# normalizes the factor to 0. +# An empty cluster introduces a size when converted, even if ON REFRESH leaves +# it without replicas until a refresh window opens. +> CREATE CLUSTER cc_empty_conv REPLICAS () > CREATE CLUSTER cc_conv (REPLICAS (r1 (SIZE 'scale=1,workers=1'))) + +! ALTER CLUSTER cc_empty_conv SET (MANAGED, SIZE = 'notasize') +contains:unknown cluster replica size notasize + +! ALTER CLUSTER cc_empty_conv SET (MANAGED, SIZE = 'free') +contains:unknown cluster replica size free + +$ postgres-execute connection=mz_system +ALTER SYSTEM SET allowed_cluster_replica_sizes = 'scale=1,workers=2' + +! ALTER CLUSTER cc_empty_conv SET (MANAGED, SIZE = 'scale=1,workers=1', SCHEDULE = ON REFRESH) +contains:unknown cluster replica size scale=1,workers=1 + +> SELECT managed FROM mz_clusters WHERE name = 'cc_empty_conv' +false + +> ALTER CLUSTER cc_empty_conv SET (MANAGED, SIZE = 'scale=1,workers=2', SCHEDULE = ON REFRESH) + +> SELECT managed, size FROM mz_clusters WHERE name = 'cc_empty_conv' +true scale=1,workers=2 + +> DROP CLUSTER cc_empty_conv + +# Converting a nonempty unmanaged cluster to ON REFRESH adopts its replica +# and its accepted size, even after the allow-list shrinks. The controller +# normalizes the replication factor to 0. > ALTER CLUSTER cc_conv SET (MANAGED = true, SIZE = 'scale=1,workers=1', SCHEDULE = ON REFRESH (HYDRATION TIME ESTIMATE = '60 seconds')) -> SELECT replication_factor FROM mz_catalog.mz_clusters WHERE name = 'cc_conv' -0 +> SELECT size, replication_factor FROM mz_catalog.mz_clusters WHERE name = 'cc_conv' +scale=1,workers=1 0 > DROP CLUSTER cc_conv +$ postgres-execute connection=mz_system +ALTER SYSTEM RESET allowed_cluster_replica_sizes + # An availability-zone change on a scheduled cluster is validated like on every # other path, not written unchecked into the realized config. > CREATE CLUSTER cc_az_sched (SIZE 'scale=1,workers=1', SCHEDULE = ON REFRESH (HYDRATION TIME ESTIMATE = '60 seconds'))