Skip to content

Commit e3fe017

Browse files
authored
adapter: fix graceful cluster reconfig with arrangement compresssion (#38241)
## The bug Arrangement compression is a replica config shape dimension. It is part of `ReconfigurationTarget`, the reconcile kernel distinguishes replicas by it, and the controller transitions it through the hydrate-overlap exactly like `SIZE`. Three hand-maintained dimension lists forgot it. **1. The realized-config leak (the one that actually hurts).** The reshape path writes the reconfiguration record and resets the shape carried by `new_config` back to the realized values, so the transition is deferred to the record's cut-over. That reset enumerated size, replication factor, availability zones and logging, and missed compression. So a compression-carrying `ALTER` flipped the realized config *immediately*. The baseline strategy then saw the new shape, decided the existing baseline replicas no longer matched, and bounced them in the middle of the reconfiguration. That defeats the graceful overlap the record exists to provide, on a statement the user expected to be graceful. The fix stops hand-listing dimensions: the reset goes through `realized_reconfiguration_target` and a new inverse `apply_reconfiguration_target`. Both destructure `ReconfigurationTarget` exhaustively, so a future dimension fails to compile until it is handled here. **2. The planner's `WAIT` gate** rejected `WITH (WAIT ...)` on a compression-only `ALTER` as having nothing to wait on, even though it transitions through a hydrate-overlap like any other shape change. **3. Observability.** `mz_cluster_reconfigurations.changes` diffed only the other four dimensions, so a compression-only record reported an empty diff while in progress, and the `SHOW CLUSTERS` activity summary showed such a cluster as steady. ## Testing The realized-config behaviour is asserted in `cluster-controller.td`, where testdrive's retries make it deterministic. The `cc_preserve` section now checks that a folding compression `ALTER` lands in the record's target and `changes` while the realized config stays untouched and only the overlap replica is bounced. A new section runs a compression-only `ALTER` through the record path end to end. The `managed_cluster.slt` compression `ALTER`s previously depended on the leak: their `SHOW CREATE` readback only saw the new value because the reshape wrote it early. With the leak gone the value moves at cut-over, and the suite pins `enable_background_alter_cluster` on, so the statement returns before that. Those `ALTER`s are exercised for acceptance instead, one with a `WAIT` clause to cover the planner gate. ## Migration Changing the MV definition changes its builtin fingerprint, so the builtin schema migration registers a replacement step for `mz_cluster_reconfigurations`. The catalog-docs golden and the `mz_show_clusters` plan golden are rewritten.
1 parent 7b35b45 commit e3fe017

11 files changed

Lines changed: 160 additions & 44 deletions

File tree

doc/user/content/reference/system-catalog/mz_internal.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ shape is in [`mz_clusters`](../mz_catalog/#mz_clusters).
181181
| `status` | [`text`] | The lifecycle status of the reconfiguration: `in-progress` while the controller converges on the target, then a terminal `finalized`, `timed-out`, `cancelled`, or `resource-exhausted`. The record is retained after it settles, so the latest outcome stays inspectable until a later reconfiguration overwrites it. |
182182
| `deadline` | [`mz_timestamp`] | The deadline by which the reconfiguration must complete. After it passes, the `on_timeout` action applies. |
183183
| `on_timeout` | [`text`] | The action applied if `deadline` passes before the target hydrates: `commit` (cut over to the not-yet-hydrated target) or `rollback` (revert to the pre-reconfiguration shape). |
184-
| `target` | [`jsonb`] | The config shape the cluster is reconfiguring to, as JSON: `size`, `replication_factor`, `availability_zones`, and `logging`. The realized (current) shape is in `mz_clusters`. |
184+
| `target` | [`jsonb`] | The config shape the cluster is reconfiguring to, as JSON: `size`, `replication_factor`, `availability_zones`, `logging`, and `arrangement_compression`. The realized (current) shape is in `mz_clusters`. |
185185
| `changes` | [`jsonb`] | The dimensions in which `target` differs from the cluster's realized configuration, as a JSON object holding the target value per changed dimension. Empty (`{}`) once a record settles with its target applied. A rolled-back record keeps the abandoned diff. |
186186

187187
## `mz_cluster_auto_scaling_strategies`

src/adapter/src/catalog/open/builtin_schema_migration.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,16 @@ static MIGRATIONS: LazyLock<Vec<MigrationStep>> = LazyLock::new(|| {
372372
MZ_CATALOG_SCHEMA,
373373
"mz_iceberg_sinks",
374374
),
375+
// The mz_cluster_reconfigurations MV definition changed (the `changes`
376+
// diff now includes the `arrangement_compression` dimension). See the
377+
// NOTE above: this version must stay at the workspace's current dev
378+
// version until the change ships.
379+
MigrationStep::replacement(
380+
"26.39.0-dev.0",
381+
CatalogItemType::MaterializedView,
382+
MZ_INTERNAL_SCHEMA,
383+
"mz_cluster_reconfigurations",
384+
),
375385
]
376386
});
377387

src/adapter/src/coord/sequencer/inner/cluster.rs

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ impl Coordinator {
329329
return Err(AdapterError::AlterClusterScheduleWhileReconfiguring);
330330
}
331331

332-
// Replication factor is one of the four dimensions the cut-over sets
332+
// Replication factor is one of the dimensions the cut-over sets
333333
// atomically from the record's target (`fold_reconfiguration_target`),
334334
// so a change applied independently while a reconfiguration is in
335335
// flight would be silently clobbered at cut-over. Refused even when the
@@ -797,10 +797,10 @@ impl Coordinator {
797797
};
798798

799799
// Build the durable write from `new_config`, which carries every field the
800-
// `ALTER` changed, then reset the config *shape* (size, replication factor,
801-
// availability zones, logging) back to the realized values: that transition
802-
// is deferred to the `reconfiguration` record and applied at cut-over. This
803-
// applies non-shape changes (`workload_class`, `schedule`,
800+
// `ALTER` changed, then reset the config *shape* (every
801+
// `ReconfigurationTarget` dimension) back to the realized values: that
802+
// transition is deferred to the `reconfiguration` record and applied at
803+
// cut-over. This applies non-shape changes (`workload_class`, `schedule`,
804804
// `auto_scaling_strategy`, ...) immediately, matching the legacy path,
805805
// rather than silently dropping them. Any existing record is folded over by
806806
// the `record` we just built.
@@ -811,10 +811,7 @@ impl Coordinator {
811811
"reshape_alter_cluster_managed requires a managed realized config".into(),
812812
));
813813
};
814-
let realized_size = realized_now.size.clone();
815-
let realized_replication_factor = realized_now.replication_factor;
816-
let realized_availability_zones = realized_now.availability_zones.clone();
817-
let realized_logging = realized_now.logging.clone();
814+
let realized_target = realized_now.realized_reconfiguration_target();
818815
// The status and the audit intent are two views of the same decision,
819816
// made together here: an ALTER back to the realized shape is a cancel,
820817
// anything else starts (or re-targets) a reconfiguration.
@@ -842,10 +839,7 @@ impl Coordinator {
842839
"reshape_alter_cluster_managed requires a managed target config".into(),
843840
));
844841
};
845-
realized_managed.size = realized_size;
846-
realized_managed.replication_factor = realized_replication_factor;
847-
realized_managed.availability_zones = realized_availability_zones;
848-
realized_managed.logging = realized_logging;
842+
realized_managed.apply_reconfiguration_target(realized_target);
849843
realized_managed.reconfiguration = Some(record);
850844

851845
self.catalog_transact(
@@ -2737,8 +2731,9 @@ fn cancel_carried_reconfiguration(config: &mut ClusterConfig) -> Option<Reconfig
27372731
}
27382732

27392733
/// Whether an `ALTER` statement sets a replica config shape dimension (`SIZE`,
2740-
/// `AVAILABILITY ZONES`, or either `INTROSPECTION` option), the changes that
2741-
/// need a durable `reconfiguration` record and a hydrate-overlap.
2734+
/// `AVAILABILITY ZONES`, either `INTROSPECTION` option, or `EXPERIMENTAL
2735+
/// ARRANGEMENT COMPRESSION`), the changes that need a durable
2736+
/// `reconfiguration` record and a hydrate-overlap.
27422737
///
27432738
/// A statement-level check, used while a reconfiguration is in flight: an
27442739
/// `ALTER` back to the realized shape sets a shape option without changing its

src/catalog/src/builtin/mz_internal.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ pub static MZ_CLUSTER_RECONFIGURATIONS: LazyLock<BuiltinMaterializedView> = Lazy
876876
),
877877
(
878878
"target",
879-
"The config shape the cluster is reconfiguring to, as JSON: `size`, `replication_factor`, `availability_zones`, and `logging`. The realized (current) shape is in `mz_clusters`.",
879+
"The config shape the cluster is reconfiguring to, as JSON: `size`, `replication_factor`, `availability_zones`, `logging`, and `arrangement_compression`. The realized (current) shape is in `mz_clusters`.",
880880
),
881881
(
882882
"changes",
@@ -953,7 +953,9 @@ SELECT
953953
CASE WHEN r.target->'availability_zones' != r.config->'availability_zones'
954954
THEN jsonb_build_object('availability_zones', r.target->'availability_zones') ELSE '{}'::jsonb END ||
955955
CASE WHEN r.target->'logging' != r.config->'logging'
956-
THEN jsonb_build_object('logging', r.target->'logging') ELSE '{}'::jsonb END
956+
THEN jsonb_build_object('logging', r.target->'logging') ELSE '{}'::jsonb END ||
957+
CASE WHEN r.target->'arrangement_compression' != r.config->'arrangement_compression'
958+
THEN jsonb_build_object('arrangement_compression', r.target->'arrangement_compression') ELSE '{}'::jsonb END
957959
AS changes
958960
FROM records r",
959961
is_retained_metrics_object: false,
@@ -5651,7 +5653,8 @@ pub static MZ_SHOW_CLUSTERS: LazyLock<BuiltinView> = LazyLock::new(|| {
56515653
'size to ' || (changes->>'size'),
56525654
'replication factor to ' || (changes->>'replication_factor'),
56535655
CASE WHEN changes->'availability_zones' IS NOT NULL THEN 'availability zones' END,
5654-
CASE WHEN changes->'logging' IS NOT NULL THEN 'introspection settings' END
5656+
CASE WHEN changes->'logging' IS NOT NULL THEN 'introspection settings' END,
5657+
CASE WHEN changes->'arrangement_compression' IS NOT NULL THEN 'arrangement compression' END
56555658
], ', '), '') AS summary
56565659
FROM mz_internal.mz_cluster_reconfigurations
56575660
WHERE status = 'in-progress'

src/catalog/src/memory/objects.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3433,6 +3433,26 @@ impl ClusterVariantManaged {
34333433
}
34343434
}
34353435

3436+
/// Advances this config's realized shape to `target`, the write a
3437+
/// reconfiguration cut-over performs. The inverse of
3438+
/// [`ClusterVariantManaged::realized_reconfiguration_target`].
3439+
pub fn apply_reconfiguration_target(&mut self, target: ReconfigurationTarget) {
3440+
// Destructured so a new target dimension fails to compile until it is
3441+
// applied here too.
3442+
let ReconfigurationTarget {
3443+
size,
3444+
replication_factor,
3445+
availability_zones,
3446+
logging,
3447+
arrangement_compression,
3448+
} = target;
3449+
self.size = size;
3450+
self.replication_factor = replication_factor;
3451+
self.availability_zones = availability_zones;
3452+
self.logging = logging;
3453+
self.arrangement_compression = arrangement_compression;
3454+
}
3455+
34363456
/// Whether the in-flight `burst` record is no longer warranted by this
34373457
/// config: the `ON HYDRATION` policy was removed or re-sized away from the
34383458
/// record's size, or the cluster was turned off (`replication_factor` 0).

src/cluster-controller/src/ctx.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,10 @@ pub struct RefreshWindowInputs {
219219
/// This is the input every strategy reads. Unmanaged clusters are not
220220
/// controller-owned and are not represented here.
221221
///
222-
/// The `size`, `replication_factor`, `availability_zones`, and `logging` fields
223-
/// together are the realized config the cluster is currently serving. The
224-
/// implicit baseline desires `replication_factor` replicas at that shape.
222+
/// The `size`, `replication_factor`, `availability_zones`, `logging`, and
223+
/// `arrangement_compression` fields together are the realized config the
224+
/// cluster is currently serving. The implicit baseline desires
225+
/// `replication_factor` replicas at that shape.
225226
#[derive(Clone, Debug)]
226227
pub struct ClusterState {
227228
pub cluster_id: ClusterId,

src/sql/src/plan/statement/ddl.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6550,10 +6550,11 @@ pub fn plan_alter_cluster(
65506550
&& availability_zones.is_none()
65516551
&& introspection_debugging.is_none()
65526552
&& introspection_interval.is_none()
6553+
&& experimental_arrangement_compression.is_none()
65536554
{
65546555
sql_bail!(
65556556
"WAIT can only be used together with a SIZE, AVAILABILITY ZONES, \
6556-
or INTROSPECTION change"
6557+
INTROSPECTION, or EXPERIMENTAL ARRANGEMENT COMPRESSION change"
65576558
);
65586559
}
65596560

test/sqllogictest/autogenerated/mz_internal.slt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ cluster_id text The␠ID␠of␠the␠cluster.␠Corresponds␠to␠`mz_cluste
129129
status text The␠lifecycle␠status␠of␠the␠reconfiguration:␠`in-progress`␠while␠the␠controller␠converges␠on␠the␠target,␠then␠a␠terminal␠`finalized`,␠`timed-out`,␠`cancelled`,␠or␠`resource-exhausted`.␠The␠record␠is␠retained␠after␠it␠settles,␠so␠the␠latest␠outcome␠stays␠inspectable␠until␠a␠later␠reconfiguration␠overwrites␠it.
130130
deadline mz_timestamp The␠deadline␠by␠which␠the␠reconfiguration␠must␠complete.␠After␠it␠passes,␠the␠`on_timeout`␠action␠applies.
131131
on_timeout text The␠action␠applied␠if␠`deadline`␠passes␠before␠the␠target␠hydrates:␠`commit`␠(cut␠over␠to␠the␠not-yet-hydrated␠target)␠or␠`rollback`␠(revert␠to␠the␠pre-reconfiguration␠shape).
132-
target jsonb The␠config␠shape␠the␠cluster␠is␠reconfiguring␠to,␠as␠JSON:␠`size`,␠`replication_factor`,␠`availability_zones`,␠and␠`logging`.␠The␠realized␠(current)␠shape␠is␠in␠`mz_clusters`.
132+
target jsonb The␠config␠shape␠the␠cluster␠is␠reconfiguring␠to,␠as␠JSON:␠`size`,␠`replication_factor`,␠`availability_zones`,␠`logging`,␠and␠`arrangement_compression`.␠The␠realized␠(current)␠shape␠is␠in␠`mz_clusters`.
133133
changes jsonb The␠dimensions␠in␠which␠`target`␠differs␠from␠the␠cluster's␠realized␠configuration,␠as␠a␠JSON␠object␠holding␠the␠target␠value␠per␠changed␠dimension.␠Empty␠(`{}`)␠once␠a␠record␠settles␠with␠its␠target␠applied.␠A␠rolled-back␠record␠keeps␠the␠abandoned␠diff.
134134

135135
query TTT

test/sqllogictest/catalog_server_explain.slt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,7 +2009,7 @@ mz_internal.mz_show_clusters:
20092009
→Fused with Child Map/Filter/Project
20102010
Project: #0, #6, #7
20112011
Filter: (#1{status} = "in-progress")
2012-
Map: ("reconfiguring " || case when ("" = array_to_string(array[("size to " || (#5{changes} ->> "size")), ("replication factor to " || (#5{changes} ->> "replication_factor")), case when ((#5{changes} -> "availability_zones")) IS NOT NULL then "availability zones" else null end, case when ((#5{changes} -> "logging")) IS NOT NULL then "introspection settings" else null end], ", ")) then null else array_to_string(array[("size to " || (#5{changes} ->> "size")), ("replication factor to " || (#5{changes} ->> "replication_factor")), case when ((#5{changes} -> "availability_zones")) IS NOT NULL then "availability zones" else null end, case when ((#5{changes} -> "logging")) IS NOT NULL then "introspection settings" else null end], ", ") end), true
2012+
Map: ("reconfiguring " || case when ("" = array_to_string(array[("size to " || (#5{changes} ->> "size")), ("replication factor to " || (#5{changes} ->> "replication_factor")), case when ((#5{changes} -> "availability_zones")) IS NOT NULL then "availability zones" else null end, case when ((#5{changes} -> "logging")) IS NOT NULL then "introspection settings" else null end, case when ((#5{changes} -> "arrangement_compression")) IS NOT NULL then "arrangement compression" else null end], ", ")) then null else array_to_string(array[("size to " || (#5{changes} ->> "size")), ("replication factor to " || (#5{changes} ->> "replication_factor")), case when ((#5{changes} -> "availability_zones")) IS NOT NULL then "availability zones" else null end, case when ((#5{changes} -> "logging")) IS NOT NULL then "introspection settings" else null end, case when ((#5{changes} -> "arrangement_compression")) IS NOT NULL then "arrangement compression" else null end], ", ") end), true
20132013
→Arranged mz_internal.mz_cluster_reconfigurations
20142014
Key: (#0{cluster_id})
20152015
→Map/Filter/Project
@@ -5371,7 +5371,7 @@ mz_internal.mz_cluster_reconfigurations:
53715371
Source mz_internal.mz_catalog_raw
53725372
project=(#3, #5..=#7, #4, #8)
53735373
filter=(("Cluster" = (#0{data} ->> "kind")) AND (json_null != #2) AND (#2 != json_null))
5374-
map=(((((#0{data} -> "value") -> "config") -> "variant") -> "Managed"), (#1 -> "reconfiguration"), parse_catalog_id(((#0{data} -> "key") -> "id")), (#2 -> "target"), case when ("InProgress" = (#2{reconfiguration} ->> "status")) then "in-progress" else case when ("Finalized" = (#2 ->> "status")) then "finalized" else case when ("TimedOut" = (#2 ->> "status")) then "timed-out" else case when ("Cancelled" = (#2 ->> "status")) then "cancelled" else case when ("ResourceExhausted" = (#2 ->> "status")) then "resource-exhausted" else (#2 ->> "status") end end end end end, text_to_mz_timestamp((#2{reconfiguration} ->> "deadline")), case when ("Commit" = (#2{reconfiguration} ->> "on_timeout")) then "commit" else case when ("Rollback" = (#2 ->> "on_timeout")) then "rollback" else (#2 ->> "on_timeout") end end, (((case when ((#1{config} -> "size") != (#4{target} -> "size")) then jsonb_build_object("size", jsonbable_to_jsonb((#4 -> "size"))) else {} end || case when ((#1{config} -> "replication_factor") != (#4{target} -> "replication_factor")) then jsonb_build_object("replication_factor", jsonbable_to_jsonb((#4 -> "replication_factor"))) else {} end) || case when ((#1{config} -> "availability_zones") != (#4{target} -> "availability_zones")) then jsonb_build_object("availability_zones", jsonbable_to_jsonb((#4 -> "availability_zones"))) else {} end) || case when ((#1{config} -> "logging") != (#4{target} -> "logging")) then jsonb_build_object("logging", jsonbable_to_jsonb((#4 -> "logging"))) else {} end))
5374+
map=(((((#0{data} -> "value") -> "config") -> "variant") -> "Managed"), (#1 -> "reconfiguration"), parse_catalog_id(((#0{data} -> "key") -> "id")), (#2 -> "target"), case when ("InProgress" = (#2{reconfiguration} ->> "status")) then "in-progress" else case when ("Finalized" = (#2 ->> "status")) then "finalized" else case when ("TimedOut" = (#2 ->> "status")) then "timed-out" else case when ("Cancelled" = (#2 ->> "status")) then "cancelled" else case when ("ResourceExhausted" = (#2 ->> "status")) then "resource-exhausted" else (#2 ->> "status") end end end end end, text_to_mz_timestamp((#2{reconfiguration} ->> "deadline")), case when ("Commit" = (#2{reconfiguration} ->> "on_timeout")) then "commit" else case when ("Rollback" = (#2 ->> "on_timeout")) then "rollback" else (#2 ->> "on_timeout") end end, ((((case when ((#1{config} -> "size") != (#4{target} -> "size")) then jsonb_build_object("size", jsonbable_to_jsonb((#4 -> "size"))) else {} end || case when ((#1{config} -> "replication_factor") != (#4{target} -> "replication_factor")) then jsonb_build_object("replication_factor", jsonbable_to_jsonb((#4 -> "replication_factor"))) else {} end) || case when ((#1{config} -> "availability_zones") != (#4{target} -> "availability_zones")) then jsonb_build_object("availability_zones", jsonbable_to_jsonb((#4 -> "availability_zones"))) else {} end) || case when ((#1{config} -> "logging") != (#4{target} -> "logging")) then jsonb_build_object("logging", jsonbable_to_jsonb((#4 -> "logging"))) else {} end) || case when ((#1{config} -> "arrangement_compression") != (#4{target} -> "arrangement_compression")) then jsonb_build_object("arrangement_compression", jsonbable_to_jsonb((#4 -> "arrangement_compression"))) else {} end))
53755375

53765376
Target cluster: mz_catalog_server
53775377

0 commit comments

Comments
 (0)