Skip to content

Commit 846bf30

Browse files
committed
console: note an in-progress resize on the configuration drawer's overview
While a resize runs, mz_clusters, and so the overview, keeps the cluster's current size and replication factor. The drawer's request now also reads the in-progress reconfiguration's target from mz_cluster_reconfigurations, behind the same v26.34 gate as autoscaling, and the overview names the target size and replication factor in an info alert.
1 parent 3274d31 commit 846bf30

7 files changed

Lines changed: 190 additions & 13 deletions

File tree

‎console/src/api/materialize/cluster/clusterConfiguration.test.sql.ts‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import {
1414
} from "~/test/sql/materializeSqlClient";
1515
import { testdrive } from "~/test/sql/mzcompose";
1616

17-
import { buildClusterAutoScalingQuery } from "./clusterConfiguration";
17+
import {
18+
buildClusterAutoScalingQuery,
19+
buildClusterResizeTargetQuery,
20+
} from "./clusterConfiguration";
1821
import { buildObjectCreationTimesQuery } from "./objectCreationTimes";
1922

2023
async function clusterIdByName(name: string) {
@@ -82,4 +85,24 @@ describe("cluster configuration queries", () => {
8285
});
8386
expect(await policy("steady")).toBeNull();
8487
});
88+
89+
it("reads no resize target once a resize settles, or for a cluster never resized", async () => {
90+
await testdrive(`
91+
> CREATE CLUSTER settled (SIZE 'scale=1,workers=1');
92+
> CREATE CLUSTER unresized (SIZE 'scale=1,workers=1');
93+
> ALTER CLUSTER settled SET (SIZE 'scale=1,workers=2');
94+
`);
95+
await testdrive(
96+
`> SELECT r.status FROM mz_internal.mz_cluster_reconfigurations r JOIN mz_clusters c ON c.id = r.cluster_id WHERE c.name = 'settled'
97+
finalized`,
98+
{ noReset: true, timeoutSeconds: 30 },
99+
);
100+
101+
for (const name of ["settled", "unresized"]) {
102+
const { rows } = await executeSqlHttp(
103+
buildClusterResizeTargetQuery(await clusterIdByName(name)).compile(),
104+
);
105+
expect(rows).toEqual([]);
106+
}
107+
});
85108
});

‎console/src/api/materialize/cluster/clusterConfiguration.ts‎

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@
88
// by the Apache License, Version 2.0.
99

1010
import { QueryKey } from "@tanstack/react-query";
11+
import { sql } from "kysely";
1112

1213
import { executeSqlV2, queryBuilder } from "~/api/materialize";
1314

1415
import { buildObjectCreationTimesQuery } from "./objectCreationTimes";
1516

16-
/** First version with `mz_internal.mz_cluster_auto_scaling_strategies`. */
17+
/**
18+
* First version with `mz_internal.mz_cluster_auto_scaling_strategies` and
19+
* `mz_internal.mz_cluster_reconfigurations`.
20+
*/
1721
export const AUTO_SCALING_MIN_VERSION = "26.34.0";
1822

1923
/** A cluster's autoscaling strategy and any in-flight burst, as JSON. */
@@ -24,6 +28,23 @@ export function buildClusterAutoScalingQuery(clusterId: string) {
2428
.where("cluster_id", "=", clusterId);
2529
}
2630

31+
/**
32+
* The size and replication factor an in-progress resize of the cluster is
33+
* heading to.
34+
*/
35+
export function buildClusterResizeTargetQuery(clusterId: string) {
36+
return queryBuilder
37+
.selectFrom("mz_cluster_reconfigurations")
38+
.select([
39+
sql<string | null>`target->>'size'`.as("size"),
40+
sql<number | null>`(target->>'replication_factor')::int4`.as(
41+
"replicationFactor",
42+
),
43+
])
44+
.where("cluster_id", "=", clusterId)
45+
.where("status", "=", "in-progress");
46+
}
47+
2748
/** The configuration the cluster list doesn't carry, read in one request. */
2849
export async function fetchClusterConfiguration({
2950
params: { clusterId, replicaIds, includeAutoScaling },
@@ -33,7 +54,10 @@ export async function fetchClusterConfiguration({
3354
params: {
3455
clusterId: string;
3556
replicaIds: string[];
36-
/** The autoscaling relation only exists from `AUTO_SCALING_MIN_VERSION`. */
57+
/**
58+
* The autoscaling and reconfiguration relations only exist from
59+
* `AUTO_SCALING_MIN_VERSION`.
60+
*/
3761
includeAutoScaling: boolean;
3862
};
3963
queryKey: QueryKey;
@@ -52,19 +76,27 @@ export async function fetchClusterConfiguration({
5276
queryKey,
5377
requestOptions,
5478
});
55-
return { creationTimes: creationTimesRes.rows, autoScaling: undefined };
79+
return {
80+
creationTimes: creationTimesRes.rows,
81+
autoScaling: undefined,
82+
resizeTarget: undefined,
83+
};
5684
}
57-
const [creationTimesRes, autoScalingRes] = await executeSqlV2({
58-
queries: [
59-
creationTimes,
60-
buildClusterAutoScalingQuery(clusterId).compile(),
61-
] as const,
62-
queryKey,
63-
requestOptions,
64-
});
85+
const [creationTimesRes, autoScalingRes, resizeTargetRes] =
86+
await executeSqlV2({
87+
queries: [
88+
creationTimes,
89+
buildClusterAutoScalingQuery(clusterId).compile(),
90+
buildClusterResizeTargetQuery(clusterId).compile(),
91+
] as const,
92+
queryKey,
93+
requestOptions,
94+
});
6595
return {
6696
creationTimes: creationTimesRes.rows,
6797
// No row means no strategy and no burst.
6898
autoScaling: autoScalingRes.rows.at(0) ?? null,
99+
// No row means no resize in progress.
100+
resizeTarget: resizeTargetRes.rows.at(0),
69101
};
70102
}

‎console/src/platform/clusters/ClusterPage/Configuration/ClusterConfigurationDrawer.test.tsx‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const environmentAt = (version: string) => ({
5050
});
5151

5252
type AutoScalingRow = { strategy: unknown; state: unknown };
53+
type ResizeTargetRow = { size: string; replicationFactor: number };
5354

5455
const CLUSTER_CREATED_MS = Date.UTC(2026, 6, 23, 22, 26);
5556
const REPLICA_CREATED_MS = Date.UTC(2026, 6, 24, 9, 0);
@@ -88,6 +89,7 @@ const SYSTEM_CLUSTER = buildClusterServerResponse(
8889
const configurationHandler = (
8990
cluster: Cluster,
9091
autoScaling: AutoScalingRow[] | null,
92+
resizeTargets: ResizeTargetRow[],
9193
fails: boolean,
9294
) => {
9395
// Cluster and replica ids come from separate counters, so each id here is
@@ -148,6 +150,16 @@ const configurationHandler = (
148150
buildColumn({ name: "state", type_oid: MzDataType.jsonb }),
149151
],
150152
}),
153+
mapKyselyToTabular({
154+
rows: resizeTargets,
155+
columns: [
156+
buildColumn({ name: "size" }),
157+
buildColumn({
158+
name: "replicationFactor",
159+
type_oid: MzDataType.int4,
160+
}),
161+
],
162+
}),
151163
],
152164
});
153165
};
@@ -216,6 +228,7 @@ const renderDrawer = async ({
216228
isOwner = true,
217229
version = "26.45.0",
218230
autoScaling = [],
231+
resizeTargets = [],
219232
configurationFails = false,
220233
clusterSizesLoading = false,
221234
}: {
@@ -225,13 +238,19 @@ const renderDrawer = async ({
225238
version?: string;
226239
/** Null for an environment without autoscaling. */
227240
autoScaling?: AutoScalingRow[] | null;
241+
resizeTargets?: ResizeTargetRow[];
228242
configurationFails?: boolean;
229243
clusterSizesLoading?: boolean;
230244
}) => {
231245
const environment = environmentAt(version);
232246
await setFakeEnvironment(getStore().set, "aws/us-east-1", environment);
233247
server.use(
234-
configurationHandler(cluster, autoScaling, configurationFails),
248+
configurationHandler(
249+
cluster,
250+
autoScaling,
251+
resizeTargets,
252+
configurationFails,
253+
),
235254
ownersHandler(isOwner),
236255
maxReplicasHandler(),
237256
clusterSizesHandler(clusterSizesLoading),
@@ -289,6 +308,19 @@ describe("ClusterConfigurationDrawer", () => {
289308
).toBeVisible();
290309
});
291310

311+
it("notes an in-progress resize's target size", async () => {
312+
await renderDrawer({
313+
section: "overview",
314+
resizeTargets: [{ size: "800cc", replicationFactor: 1 }],
315+
});
316+
317+
expect(
318+
await screen.findByText(
319+
"A resize to 800cc is in progress. Size shows the current size until it finishes.",
320+
),
321+
).toBeVisible();
322+
});
323+
292324
it("disables Edit while the cluster sizes load", async () => {
293325
await renderDrawer({ section: "overview", clusterSizesLoading: true });
294326

‎console/src/platform/clusters/ClusterPage/Configuration/ClusterConfigurationDrawer.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
parseAutoScaling,
6464
replicaStatus,
6565
resetAutoScalingStatement,
66+
resizeNote,
6667
setAutoScalingStatement,
6768
} from "./clusterConfigurationModel";
6869

@@ -221,6 +222,9 @@ const OverviewPanel = ({
221222
const createdAt = configuration?.creationTimes.find(
222223
({ id, objectType }) => objectType === "cluster" && id === cluster.id,
223224
)?.occurredAt;
225+
const resize =
226+
configuration?.resizeTarget &&
227+
resizeNote(cluster, configuration.resizeTarget);
224228
return (
225229
<VStack spacing={6} alignItems="stretch">
226230
<PanelHeader
@@ -235,6 +239,7 @@ const OverviewPanel = ({
235239
)
236240
}
237241
/>
242+
{resize && <Alert variant="info" message={resize} />}
238243
<Grid templateColumns="repeat(3, minmax(0, 1fr))" gap={5}>
239244
<Detail label="Name">{cluster.name}</Detail>
240245
<Detail label="Size">{cluster.size ?? "-"}</Detail>

‎console/src/platform/clusters/ClusterPage/Configuration/clusterConfigurationModel.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
parseAutoScaling,
1313
replicaStatus,
1414
resetAutoScalingStatement,
15+
resizeNote,
1516
setAutoScalingStatement,
1617
} from "./clusterConfigurationModel";
1718

@@ -133,3 +134,28 @@ describe("formatLinger", () => {
133134
expect(formatLinger(1_500)).toBe("1.5s");
134135
});
135136
});
137+
138+
describe("resizeNote", () => {
139+
const CLUSTER = { size: "100cc", replicationFactor: 1 };
140+
141+
it("names the target size, and the target replication factor when it changes too", () => {
142+
expect(resizeNote(CLUSTER, { size: "800cc", replicationFactor: 1 })).toBe(
143+
"A resize to 800cc is in progress. Size shows the current size until it finishes.",
144+
);
145+
expect(resizeNote(CLUSTER, { size: "800cc", replicationFactor: 2 })).toBe(
146+
"A resize to 800cc with replication factor 2 is in progress. Size and replication factor show the current values until it finishes.",
147+
);
148+
expect(resizeNote(CLUSTER, { size: "100cc", replicationFactor: 2 })).toBe(
149+
"A change to replication factor 2 is in progress. Replication factor shows the current factor until it finishes.",
150+
);
151+
});
152+
153+
it("says nothing when the size and replication factor stay", () => {
154+
expect(
155+
resizeNote(CLUSTER, { size: "100cc", replicationFactor: 1 }),
156+
).toBeUndefined();
157+
expect(
158+
resizeNote(CLUSTER, { size: null, replicationFactor: null }),
159+
).toBeUndefined();
160+
});
161+
});

‎console/src/platform/clusters/ClusterPage/Configuration/clusterConfigurationModel.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,33 @@ export const setAutoScalingStatement = (
132132
/** Removes a cluster's autoscaling strategy. */
133133
export const resetAutoScalingStatement = (clusterName: string) =>
134134
`ALTER CLUSTER ${quoteIdentifier(clusterName)} RESET (AUTO SCALING STRATEGY);`;
135+
136+
/**
137+
* Says what an in-progress resize changes about the size and replication
138+
* factor, or undefined when it changes neither. Until it finishes,
139+
* `mz_clusters` keeps the current values.
140+
*/
141+
export function resizeNote(
142+
cluster: Pick<Cluster, "size" | "replicationFactor">,
143+
target: { size: string | null; replicationFactor: number | null },
144+
) {
145+
const size =
146+
target.size !== null && target.size !== cluster.size
147+
? target.size
148+
: undefined;
149+
const factor =
150+
target.replicationFactor !== null &&
151+
target.replicationFactor !== cluster.replicationFactor
152+
? target.replicationFactor
153+
: undefined;
154+
if (size !== undefined && factor !== undefined) {
155+
return `A resize to ${size} with replication factor ${factor} is in progress. Size and replication factor show the current values until it finishes.`;
156+
}
157+
if (size !== undefined) {
158+
return `A resize to ${size} is in progress. Size shows the current size until it finishes.`;
159+
}
160+
if (factor !== undefined) {
161+
return `A change to replication factor ${factor} is in progress. Replication factor shows the current factor until it finishes.`;
162+
}
163+
return undefined;
164+
}

‎console/types/materialize.d.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,34 @@ export interface MzClusterDeploymentLineage {
382382
current_deployment_cluster_id: string;
383383
}
384384

385+
// TODO: hand-added for mz >= 26.34. Keep through `yarn gen:types` until every environment has it.
386+
export interface MzClusterReconfigurations {
387+
/**
388+
* The ID of the cluster. Corresponds to `mz_clusters.id`.
389+
*/
390+
cluster_id: string;
391+
/**
392+
* 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.
393+
*/
394+
status: string;
395+
/**
396+
* The deadline by which the reconfiguration must complete. After it passes, the `on_timeout` action applies.
397+
*/
398+
deadline: string;
399+
/**
400+
* 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).
401+
*/
402+
on_timeout: string;
403+
/**
404+
* 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`.
405+
*/
406+
target: Json;
407+
/**
408+
* 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.
409+
*/
410+
changes: Json;
411+
}
412+
385413
export interface MzClusterReplicaFrontiers {
386414
/**
387415
* The ID of the source, sink, index, materialized view, or subscription.
@@ -4339,6 +4367,7 @@ export interface DB {
43394367
mz_base_types: MzBaseTypes;
43404368
mz_cluster_auto_scaling_strategies: MzClusterAutoScalingStrategies;
43414369
mz_cluster_deployment_lineage: MzClusterDeploymentLineage;
4370+
mz_cluster_reconfigurations: MzClusterReconfigurations;
43424371
mz_cluster_replica_frontiers: MzClusterReplicaFrontiers;
43434372
mz_cluster_replica_history: MzClusterReplicaHistory;
43444373
mz_cluster_replica_metrics: MzClusterReplicaMetrics;

0 commit comments

Comments
 (0)