diff --git a/console/src/api/materialize/cluster/__snapshots__/availableClusterSizes.test.ts.snap b/console/src/api/materialize/cluster/__snapshots__/availableClusterSizes.test.ts.snap index a004d67e53322..a0064ac6b4b6e 100644 --- a/console/src/api/materialize/cluster/__snapshots__/availableClusterSizes.test.ts.snap +++ b/console/src/api/materialize/cluster/__snapshots__/availableClusterSizes.test.ts.snap @@ -12,3 +12,17 @@ exports[`queries > buildAvailableClusterReplicaSizesQuery produces the expected END, "credits_per_hour"", } `; + +exports[`queries > filters out disabled sizes on environments with the disabled column 1`] = ` +{ + "parameters": [ + "mz_%", + false, + ], + "sql": "select "size" from "mz_cluster_replica_sizes" where "size" not like $1 and "disabled" = $2 order by CASE + WHEN size ~ '^[0-9]+([a-zA-Z]*cc[xX]?|C)$' THEN '0_legacy' + WHEN size LIKE '%-%' THEN '1_' || split_part(size, '-', 1) + ELSE '2_tshirt' + END, "credits_per_hour"", +} +`; diff --git a/console/src/api/materialize/cluster/availableClusterSizes.test.ts b/console/src/api/materialize/cluster/availableClusterSizes.test.ts index 3a497832d4355..3fc19ad4ee684 100644 --- a/console/src/api/materialize/cluster/availableClusterSizes.test.ts +++ b/console/src/api/materialize/cluster/availableClusterSizes.test.ts @@ -7,7 +7,10 @@ // the Business Source License, use of this software will be governed // by the Apache License, Version 2.0. -import { buildAvailableClusterReplicaSizesQuery } from "./availableClusterSizes"; +import { + buildAvailableClusterReplicaSizesQuery, + DISABLED_REPLICA_SIZES_MIN_VERSION, +} from "./availableClusterSizes"; describe("queries", () => { it("buildAvailableClusterReplicaSizesQuery produces the expected query", () => { @@ -15,4 +18,11 @@ describe("queries", () => { buildAvailableClusterReplicaSizesQuery().compile(); expect({ sql, parameters }).toMatchSnapshot(); }); + + it("filters out disabled sizes on environments with the disabled column", () => { + const { sql, parameters } = buildAvailableClusterReplicaSizesQuery( + DISABLED_REPLICA_SIZES_MIN_VERSION, + ).compile(); + expect({ sql, parameters }).toMatchSnapshot(); + }); }); diff --git a/console/src/api/materialize/cluster/availableClusterSizes.ts b/console/src/api/materialize/cluster/availableClusterSizes.ts index 76b221dc2d2aa..4ee9202d15ff6 100644 --- a/console/src/api/materialize/cluster/availableClusterSizes.ts +++ b/console/src/api/materialize/cluster/availableClusterSizes.ts @@ -10,7 +10,13 @@ import { QueryKey } from "@tanstack/react-query"; import { sql } from "kysely"; +import { extractEnvironmentVersion } from "~/api/buildQueryKeySchema"; import { executeSqlV2, queryBuilder } from "~/api/materialize"; +import { semverParseGte } from "~/api/materialize/versioning"; + +// TODO: set to the first release that includes the `disabled` column, and +// drop the gate once every environment is on it. +export const DISABLED_REPLICA_SIZES_MIN_VERSION = "26.50.0-dev"; export const buildAllowedClusterReplicaSizesQuery = () => { return sql<{ @@ -18,13 +24,19 @@ export const buildAllowedClusterReplicaSizesQuery = () => { }>`SHOW allowed_cluster_replica_sizes`; }; -export const buildAvailableClusterReplicaSizesQuery = () => { +export const buildAvailableClusterReplicaSizesQuery = ( + environmentVersion?: string, +) => { + const hasDisabledColumn = + environmentVersion !== undefined && + semverParseGte(environmentVersion, DISABLED_REPLICA_SIZES_MIN_VERSION); return ( queryBuilder .selectFrom("mz_cluster_replica_sizes") .select("size") // We don't want to show the mz_probe size in the UI .where("size", "not like", "mz_%") + .$if(hasDisabledColumn, (qb) => qb.where("disabled", "=", false)) // Group sizes by family (legacy cc, t-shirt, M.1, D.1, ...) so related // sizes stay contiguous, then sort by cost within each family. .orderBy( @@ -49,7 +61,9 @@ export default async function fetchAvailableClusterSizes({ }) { const compiledQueries = [ buildAllowedClusterReplicaSizesQuery().compile(queryBuilder), - buildAvailableClusterReplicaSizesQuery().compile(), + buildAvailableClusterReplicaSizesQuery( + extractEnvironmentVersion(queryKey), + ).compile(), ] as const; let sizes: string[] | null = null; diff --git a/console/types/materialize.d.ts b/console/types/materialize.d.ts index 4213dbdf665a1..0cd0ca1c450da 100644 --- a/console/types/materialize.d.ts +++ b/console/types/materialize.d.ts @@ -541,6 +541,10 @@ export interface MzClusterReplicaSizes { * The number of compute credits consumed per hour. */ credits_per_hour: Generated; + /** + * Whether the size is disabled. Existing replicas keep running on a disabled size, but new clusters and replicas cannot choose it. + */ + disabled: Generated; /** * The disk allocation per process. */ diff --git a/doc/user/content/sql/system-catalog/mz_catalog.md b/doc/user/content/sql/system-catalog/mz_catalog.md index 80ed34853d351..22c4f161efc27 100644 --- a/doc/user/content/sql/system-catalog/mz_catalog.md +++ b/doc/user/content/sql/system-catalog/mz_catalog.md @@ -95,6 +95,8 @@ At this time, we do not make any guarantees about the freshness of these numbers The `mz_cluster_replica_sizes` table contains a mapping of logical sizes (e.g. `100cc`) to physical sizes (number of processes, and CPU and memory allocations per process). +It includes disabled sizes, which existing replicas may still use; to list the +sizes available for new clusters and replicas, filter on `NOT disabled`. This table was previously in the `mz_internal` schema. All queries previously referencing `mz_internal.mz_cluster_replica_sizes` should now reference `mz_catalog.mz_cluster_replica_sizes`. @@ -114,6 +116,7 @@ any kind of capacity planning. | `memory_bytes` | [`uint8`] | The RAM allocation per process, in bytes. | | `disk_bytes` | [`uint8`] | The disk allocation per process. | | `credits_per_hour` | [`numeric`] | The number of compute credits consumed per hour. | +| `disabled` | [`boolean`] | Whether the size is disabled. Existing replicas keep running on a disabled size, but new clusters and replicas cannot choose it. | ### `mz_cluster_replicas` diff --git a/src/adapter/src/catalog/builtin_table_updates.rs b/src/adapter/src/catalog/builtin_table_updates.rs index eb5091b4d84d8..93c8242383070 100644 --- a/src/adapter/src/catalog/builtin_table_updates.rs +++ b/src/adapter/src/catalog/builtin_table_updates.rs @@ -483,12 +483,7 @@ impl CatalogState { // The disk column of mz_clusters / mz_cluster_replicas MVs needs // `swap_enabled` and `disk_bytes`; expose them through a parallel - // internal table. Unlike the public sizes table below, we write - // here unconditionally — `cluster_replica_size_has_disk` previously - // indexed the in-memory map without checking `disabled`, so a - // managed cluster pinned to a disabled size still resolved its - // `disk` column from the real allocation. Writing disabled rows - // here preserves that behavior. + // internal table. let internal_row = Row::pack_slice(&[ size.as_str().into(), Datum::from(alloc.swap_enabled), @@ -500,10 +495,6 @@ impl CatalogState { Diff::ONE, )); - if alloc.disabled { - continue; - } - let cpu_limit = alloc.cpu_limit.unwrap_or(CpuLimit::MAX); let MemoryLimit(ByteSize(memory_bytes)) = (alloc.memory_limit).unwrap_or(MemoryLimit::MAX); @@ -516,6 +507,7 @@ impl CatalogState { memory_bytes.into(), disk_bytes.into(), (alloc.credits_per_hour).into(), + alloc.disabled.into(), ]); updates.push(BuiltinTableUpdate::row( diff --git a/src/adapter/src/catalog/open/builtin_schema_migration.rs b/src/adapter/src/catalog/open/builtin_schema_migration.rs index db1c32a97daec..8af4994f1dab5 100644 --- a/src/adapter/src/catalog/open/builtin_schema_migration.rs +++ b/src/adapter/src/catalog/open/builtin_schema_migration.rs @@ -570,6 +570,14 @@ static MIGRATIONS: LazyLock> = LazyLock::new(|| { MZ_CATALOG_SCHEMA, "mz_index_columns", ), + // Adding the `disabled` column to mz_cluster_replica_sizes. The table + // is rewritten at every startup, so replacing the shard loses nothing. + MigrationStep::replacement( + "26.50.0-dev.0", + CatalogItemType::Table, + MZ_CATALOG_SCHEMA, + "mz_cluster_replica_sizes", + ), ] }); diff --git a/src/catalog/src/builtin/mz_catalog.rs b/src/catalog/src/builtin/mz_catalog.rs index dee649871914b..d039e925396bd 100644 --- a/src/catalog/src/builtin/mz_catalog.rs +++ b/src/catalog/src/builtin/mz_catalog.rs @@ -2832,11 +2832,7 @@ pub static MZ_CLUSTERS: LazyLock = LazyLock::new(|| { // `disk` is computed as in // `CatalogState::cluster_replica_size_has_disk`: // `NOT swap_enabled AND disk_bytes != 0`. Both flags are sourced from - // `mz_internal.mz_cluster_replica_size_internal`, which the catalog - // populates for every size — including ones flagged `disabled`, which - // the public `mz_cluster_replica_sizes` table omits. Using the internal - // table directly preserves the prior behavior of indexing the in-memory - // size map regardless of `disabled`. + // `mz_internal.mz_cluster_replica_size_internal`. // // `availability_zones` is normalised to NULL when empty, matching // `pack_cluster_update`'s `None` for an empty `Vec`. `list_agg` over @@ -3145,6 +3141,7 @@ pub static MZ_CLUSTER_REPLICA_SIZES: LazyLock = LazyLock::new(|| B "credits_per_hour", SqlScalarType::Numeric { max_scale: None }.nullable(false), ) + .with_column("disabled", SqlScalarType::Bool.nullable(false)) .finish(), column_comments: BTreeMap::from_iter([ ("size", "The human-readable replica size."), @@ -3163,12 +3160,16 @@ pub static MZ_CLUSTER_REPLICA_SIZES: LazyLock = LazyLock::new(|| B "credits_per_hour", "The number of compute credits consumed per hour.", ), + ( + "disabled", + "Whether the size is disabled. Existing replicas keep running on a disabled size, but new clusters and replicas cannot choose it.", + ), ]), is_retained_metrics_object: true, access: vec![PUBLIC_SELECT], ontology: Some(Ontology { entity_name: "replica_size", - description: "Available cluster replica sizes with CPU, memory, and credit cost", + description: "Cluster replica sizes with CPU, memory, and credit cost", links: &const { [] }, column_semantic_types: &const { [ diff --git a/src/catalog/src/builtin/mz_internal.rs b/src/catalog/src/builtin/mz_internal.rs index 51b8c5718ce65..352123ff0e958 100644 --- a/src/catalog/src/builtin/mz_internal.rs +++ b/src/catalog/src/builtin/mz_internal.rs @@ -1127,13 +1127,6 @@ WHERE /// so this table is locked down with `access: vec![]` (same pattern as /// `mz_catalog_raw`): builtin MVs read it at bootstrap, but direct user /// `SELECT` is denied. -/// -/// Unlike `mz_cluster_replica_sizes`, this table includes rows for sizes -/// flagged `disabled` — `CatalogState::cluster_replica_size_has_disk` -/// indexed the in-memory map without checking `disabled`, so a managed -/// cluster pinned to a disabled size still resolved its `disk` column from -/// the size's real `swap_enabled` / `disk_limit`. Including disabled sizes -/// here preserves that behavior. pub static MZ_CLUSTER_REPLICA_SIZE_INTERNAL: LazyLock = LazyLock::new(|| { BuiltinTable { name: "mz_cluster_replica_size_internal", diff --git a/test/cluster/mzcompose.py b/test/cluster/mzcompose.py index 3c7ba8f4038ef..cbecfe64f8a0b 100644 --- a/test/cluster/mzcompose.py +++ b/test/cluster/mzcompose.py @@ -7652,8 +7652,8 @@ def workflow_test_sql_cluster_disk(c: Composition) -> None: # writes every size to `mz_cluster_replica_size_internal`, including # disabled ones, so `disk` for the existing `noswap_nonzero` cluster and # its replica should still resolve to `true`. The public - # `mz_cluster_replica_sizes` table drops the disabled row, which the - # final assertion checks. + # `mz_cluster_replica_sizes` table lists the disabled row as disabled, + # which the final assertion checks. c.kill("materialized") sizes_with_disabled = dict(sizes) sizes_with_disabled["noswap,nonzero"] = { @@ -7685,8 +7685,8 @@ def workflow_test_sql_cluster_disk(c: Composition) -> None: noswap_zero false noswap_nonzero true - > SELECT 'noswap,nonzero' IN (SELECT size FROM mz_cluster_replica_sizes) - false + > SELECT disabled FROM mz_cluster_replica_sizes WHERE size = 'noswap,nonzero' + true """)) diff --git a/test/restart/mzcompose.py b/test/restart/mzcompose.py index b374a2000b1a2..b3a70f8c79d45 100644 --- a/test/restart/mzcompose.py +++ b/test/restart/mzcompose.py @@ -401,6 +401,10 @@ def workflow_disabled_cluster_replica_size_survives_restart(c: Composition) -> N > SHOW CLUSTER REPLICAS WHERE cluster = 'test' test r1 {size} true "" + # The disabled size stays listed, so its replicas still join to it. + > SELECT r.name, s.disabled FROM mz_cluster_replicas r JOIN mz_cluster_replica_sizes s USING (size) JOIN mz_clusters c ON r.cluster_id = c.id WHERE c.name = 'test' + r1 true + # Creating a new replica of the disabled size is rejected. ! CREATE CLUSTER REPLICA test.r2 SIZE '{size}' contains:unknown cluster replica size {size} diff --git a/test/sqllogictest/autogenerated/mz_catalog.slt b/test/sqllogictest/autogenerated/mz_catalog.slt index 5c1d90748dfa0..45f5c646ff3bc 100644 --- a/test/sqllogictest/autogenerated/mz_catalog.slt +++ b/test/sqllogictest/autogenerated/mz_catalog.slt @@ -79,6 +79,7 @@ cpu_nano_cores uint8 The␠CPU␠allocation␠per␠process,␠in␠billionths memory_bytes uint8 The␠RAM␠allocation␠per␠process,␠in␠bytes. disk_bytes uint8 The␠disk␠allocation␠per␠process. credits_per_hour numeric The␠number␠of␠compute␠credits␠consumed␠per␠hour. +disabled boolean Whether␠the␠size␠is␠disabled.␠Existing␠replicas␠keep␠running␠on␠a␠disabled␠size,␠but␠new␠clusters␠and␠replicas␠cannot␠choose␠it. query TTT SELECT name, type, comment FROM objects WHERE schema = 'mz_catalog' AND object = 'mz_cluster_replicas' ORDER BY position diff --git a/test/sqllogictest/catalog_server_explain.slt b/test/sqllogictest/catalog_server_explain.slt index 9a50baffe0401..66b899bd9058b 100644 --- a/test/sqllogictest/catalog_server_explain.slt +++ b/test/sqllogictest/catalog_server_explain.slt @@ -649,7 +649,7 @@ mz_internal.mz_cluster_replica_history: →Arrange (#0) →Union →Fused with Child Map/Filter/Project - Project: #0, #6, #7 + Project: #0, #6, #8 Map: true →Arranged mz_catalog.mz_cluster_replica_sizes Key: (#0{size}) @@ -4930,7 +4930,7 @@ mz_catalog.mz_columns: →Read l1 →Differential Join %1:mz_catalog_raw[#1{schema_name}, #2{name}] » %0[#0{schema_name}, #1{relation_name}] →Arrange (#0{schema_name}, #1{relation_name}) - →Constant (1937 rows) + →Constant (1938 rows) →Arrange (#1{schema_name}, #2{name}) →Fused with Child Map/Filter/Project Project: #6, #4, #3 @@ -8796,7 +8796,7 @@ query T multiline EXPLAIN SELECT * FROM "mz_internal"."mz_builtin_columns"; ---- Explained Query (fast path): - →Constant (1937 rows) + →Constant (1938 rows) Target cluster: mz_catalog_server diff --git a/test/sqllogictest/mz_catalog_server_index_accounting.slt b/test/sqllogictest/mz_catalog_server_index_accounting.slt index 7493afec8e68d..d00e326aafd18 100644 --- a/test/sqllogictest/mz_catalog_server_index_accounting.slt +++ b/test/sqllogictest/mz_catalog_server_index_accounting.slt @@ -335,6 +335,7 @@ mz_cluster_replica_size_internal size mz_cluster_replica_size_internal swap_enabled mz_cluster_replica_sizes cpu_nano_cores mz_cluster_replica_sizes credits_per_hour +mz_cluster_replica_sizes disabled mz_cluster_replica_sizes disk_bytes mz_cluster_replica_sizes memory_bytes mz_cluster_replica_sizes processes diff --git a/test/sqllogictest/mz_columns.slt b/test/sqllogictest/mz_columns.slt index f20be47d02e61..ac1754dd300c6 100644 --- a/test/sqllogictest/mz_columns.slt +++ b/test/sqllogictest/mz_columns.slt @@ -95,6 +95,7 @@ ORDER BY columns.position 5 memory_bytes uint8 false NULL uint8 -1 6 disk_bytes uint8 true NULL uint8 -1 7 credits_per_hour numeric false NULL numeric -1 +8 disabled boolean false NULL bool -1 # A log, a source, a materialized view, a view and the generated views that # report the builtins, including the one that reports these columns. diff --git a/test/sqllogictest/system-cluster.slt b/test/sqllogictest/system-cluster.slt index 8b6f37efd2f51..3300f4fe971b1 100644 --- a/test/sqllogictest/system-cluster.slt +++ b/test/sqllogictest/system-cluster.slt @@ -410,9 +410,9 @@ ORDER BY r.id; ---- Explained Query: Finish order_by=[#0{id} asc nulls_last] output=[#0..=#5] - Project (#11{id}, #12{name}, #0{id}, #14{size}, #1{name}, #30) - Map (((uint8_to_double(#25{memory_bytes}) / uint8_to_double(case when (0 = uint8_to_numeric(#19{memory_bytes})) then null else #19{memory_bytes} end)) * 100)) - Join on=(#0{id} = #13{cluster_id} AND #11{id} = #22{replica_id} AND #14{size} = #15{size}) type=delta + Project (#11{id}, #12{name}, #0{id}, #14{size}, #1{name}, #31) + Map (((uint8_to_double(#26{memory_bytes}) / uint8_to_double(case when (0 = uint8_to_numeric(#19{memory_bytes})) then null else #19{memory_bytes} end)) * 100)) + Join on=(#0{id} = #13{cluster_id} AND #11{id} = #23{replica_id} AND #14{size} = #15{size}) type=delta ArrangeBy keys=[[#0{id}]] ReadIndex on=mz_clusters mz_clusters_ind=[delta join 1st input (full scan)] ArrangeBy keys=[[#0{id}], [#2{cluster_id}], [#3{size}]]