Repository navigation
console: set a cluster's autoscaling policy from the Alter cluster form - #40043
Conversation
50b125f to
4ea2ff9
Compare
QA LLM Review1. MEDIUM -- Editing the auto-scaling policy cancels an in-flight cluster resize
The new Auto-Scaling panel Edit action submits Details
While a reconfiguration is in flight, any SET that names SIZE or REPLICATION FACTOR goes through The statements this PR removes from the panel were strategy-only ( Fix: emit SIZE and REPLICATION FACTOR only when they differ from the cluster's current values. Skip the SET entirely when it would be empty. |
4ea2ff9 to
72d7ea8
Compare
72d7ea8 to
f386c33
Compare
QA LLM Review1. MEDIUM -- Reopening Alter cluster shows the old policy, and the next submit reverts the change
If a user enables auto-scaling from the Alter cluster menu item and reopens the form within React Query's 5-minute cache window, the form shows auto-scaling as off. Any later submit, such as a replica change, then sends Details
On reopen, The reverse also happens. If a user removes a policy and reopens the form, it shows the policy as on, and a rename-only submit sets the policy again. I reproduced this with a vitest probe. It mounts the modal behind a Fix direction: snapshot the policy the form was built from and diff against that snapshot. Also gate the form on a read made after mount (for example |
f386c33 to
1c67755
Compare
a1edf46 to
ecb0059
Compare
ecb0059 to
4deee1f
Compare
| </InlineLabeledInput> | ||
| </FormControl> | ||
| <FormControl isInvalid={!!formState.errors.size}> | ||
| <InlineLabeledInput label="Size"> |
There was a problem hiding this comment.
Maybe we can show the in-progress resize next to Size, like "Resizing to 1600cc"? The form already reads the target, and right now the field shows the old size with no hint that a resize is running.
There was a problem hiding this comment.
Done. While a resize is in flight, Size now reads "Resizing to 1600cc." under the field.
| )} | ||
| <VStack alignItems="stretch" spacing="2" width="100%"> | ||
| <Divider /> | ||
| <Text textStyle="heading-xs">Equivalent code</Text> |
There was a problem hiding this comment.
nit: "Equivalent Code", to match "Auto-Scaling Policy" above it. Headers use Title Case.
4deee1f to
d3bac49
Compare
d3bac49 to
819a47e
Compare
a58b54c to
6904613
Compare
There was a problem hiding this comment.
Devin Review found 2 potential issues.
3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| clusterSize === opened.size && | ||
| value === opened.autoScaling?.resizeTargetSize | ||
| ) { | ||
| return "Burst size must differ from the size the cluster is resizing to."; |
There was a problem hiding this comment.
🟡 Retargeted resize rejects selected burst
When a resize targets 800cc, hydrationSize accepts 800cc if the form also changes Size to 1600cc. The server still checks the existing resize target and rejects the combined change.
Learn more
An in-progress resize has a target stored separately from the cluster's current size. The form skips the target-size check when the user selects a different Size, assuming the resize will be retargeted. The server's strategy validation still compares a newly set hydration size against the existing reconfiguration target before applying the new target; see strategy validation. The combined statement fails before either change applies.
Example: A cluster runs at 400cc and is resizing to 800cc. Select Size 1600cc and Burst size 800cc. The form accepts both, but the server rejects the 800cc burst against the still-in-progress 800cc target.
Recommended fix: Keep the existing target equality check for any newly submitted policy, including a simultaneous resize, or change the server's validation to use the new target if that combined operation is intended to work. Add a SQL test of the combined operation.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
I don't think the server rejects this one. A SET that names SIZE during a resize goes through reshape_alter_cluster_managed, and the old-target check only runs on the strategy-only path (sequence_alter_cluster_managed_to_managed). I added a SQL test that retargets an in-flight resize and sets the old target as the burst size in one statement.
| if (autoScalingPolicy === null && options.length === 0) { | ||
| // NOTE: Unlike a SET naming the strategy, RESET works with autoscaling's | ||
| // feature flag off, so a policy set while it was on can still be removed. | ||
| return sql`ALTER CLUSTER ${sql.id(clusterName)} RESET (AUTO SCALING STRATEGY)`; | ||
| } | ||
| if (autoScalingPolicy !== undefined) { | ||
| options.push(autoScalingStrategyOption(autoScalingPolicy)); |
There was a problem hiding this comment.
🟡 Combined edits cannot remove disabled policy
With the autoscaling feature flag off, autoScalingPolicy removal alongside a size or replica edit produces a gated SET. The server rejects the whole edit, though a standalone RESET can remove the policy.
Learn more
The server gates the SET form of AUTO SCALING STRATEGY behind enable_auto_scaling_strategy, including the empty () value; see alter planning. A strategy configured before the flag was turned off remains in the catalog. The standalone removal uses RESET and succeeds, but a size or replication-factor change makes the builder use a combined SET and fail before changing either setting.
Example: Disable the flag after setting an autoscaling policy. Open the form, uncheck auto-scaling and change Size. Submission fails even though removing the policy alone succeeds.
Recommended fix: For disabled-flag environments, separate RESET from the other settings operation or explicitly handle the lack of atomic removal when a simultaneous resize needs to reuse the old burst size. Preserve the combined SET when the flag is enabled and atomicity matters.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Confirmed: with enable_auto_scaling_strategy off, this fails with the private-preview error. I'd leave it, since the flag is the break-glass switch and defaults on, nothing changes when it fails, and unchecking on its own still removes the policy with a RESET.
6904613 to
d4ff693
Compare
d4ff693 to
b8a3f9b
Compare
b8a3f9b to
5abc1b2
Compare
5abc1b2 to
bea8596
Compare
bea8596 to
59020e9
Compare
a43b06e to
ed86ac5
Compare
|
Both QA findings are fixed:
|
2d643b6 to
18a9421
Compare
Adds an optional auto-scaling policy section to the Alter cluster form for managed clusters on environments with autoscaling: an Enable checkbox, a burst size, and a linger duration such as 15s or 2m. It starts from the cluster's current policy, validates the burst size against the cluster's size and the linger's format, and shows the SQL it will run as equivalent code. Turning the policy off runs a RESET before the SET. The configuration drawer hands the policy it already read to the form, and its auto-scaling panel gets an Edit action that opens the form on that section in place of the copyable statements.
While a resize is in flight, the Size field reads "Resizing to <size>." under it. The Equivalent Code heading moves to title case, like the Auto-Scaling Policy heading above it. A new SQL test pins that one SET retargeting an in-flight resize can set the old target as the burst size, which the form allows once Size changes.
Self-managed has no LaunchDarkly, so a flagged UI preview shipped there outright, and the cluster page redesign was held back by a stubbed-off flag. A preview can now ask for the opt-in in self-managed too, where the profile menu and the page pill offer it, off until the user opts in. The cluster page redesign does, and its stubbed flag is now on. The cluster list preview still ships outright.
18a9421 to
b0acb94
Compare
…0044) ### Motivation Ninth PR in the cluster details redesign stack (#39509), on top of #40043. The redesign's prototype has a Hydration report tab. This PR adds the data a Hydration tab needs, built on the hydration history the database now records. The next PR in the stack, #40074, adds the tab. Part of [CNS-173](https://linear.app/materializeinc/issue/CNS-173). ### Description - **History**, read with react-query since the history tables have no index: - a cluster's hydration episodes from `mz_replica_hydration_history`, one row per episode with the highest peaks across its processes. Gated on 26.43, which records a row per process, and leaves out older rows. Episodes with no objects stay in: on a replica with nothing to hydrate, such as one running only sources, they're the record that a boot finished; - the cluster's replicas from `mz_cluster_replica_history`, so a dropped replica (a resize, a burst replica) keeps its name and size. Fetched again when the cluster's current replicas change, and not kept once unused, so a set of replicas that comes back, as after a burst replica is dropped, fetches again. The same cluster's answer stays up meanwhile, filtered to the window as it moves on; - an episode's objects from `mz_object_hydration_history`, with catalog ids through `mz_object_global_ids`. Fetched each minute until they reach the episode's object count, since the collector can record an episode a visit before some of its objects; - the highest CPU sampled during an episode, from `mz_cluster_replica_metrics_history`. - **Live state**, in SUBSCRIBE-fed TanStack DB collections: - whether each user object on each of the cluster's replicas is hydrated. A lookup on `mz_compute_hydration_times_ind`, on the fast path; - each object's state on one replica (waiting for a hydration slot, working, hydrated), from that replica's `mz_compute_hydration_times_per_worker` in a session pinned to the replica. Gated on 26.39. It runs on the replica, not on `mz_catalog_server`, so the tab mounts it only for a replica that is hydrating. - **Model** (`hydrationModel.ts`): - an episode is a full hydration when it starts within two minutes of its replica coming online or being created, or covers every object the cluster had when it finished. The second rule catches a zero-downtime upgrade's rehydration, which runs while the environment is read-only and so records no boot. Objects created during an episode raise its threshold as much as its count, so a burst of new objects isn't full. Any other episode is additive: it covers objects added to an already-hydrated replica. The database records no cause, so this is inferred from status history and the objects' creation times; - a replica that goes offline before it finishes hydrating, such as an out-of-memory restart loop, is an unfinished hydration. The history only records episodes that finish, so these come from status history. An attempt runs from the replica's previous restart or its creation, not its last online row, since a zero-downtime upgrade writes that row after the replica hydrated. An attempt that began before the window is skipped unless an object was created in the window during it, since the episodes before the window aren't read. End events within a minute of each other are one restart, unless the replica came back online between them. An offline row counts as a restart only after an online one, since a new replica's pod on Kubernetes records an offline row before it first comes online. An object created during an attempt starts a hydration of its own, so the attempt finished only if a hydration finished after the last such object. The creation times come from the cluster's current objects and, while a restart in the window reads as finished with those, also from its indexes and materialized views created in the window and dropped since, read from `mz_object_history`, so dropping the index behind an out-of-memory restart doesn't hide it. A dropped object counts only for the restarts while it existed, since dropping one unfinished records no finished hydration; - an episode's objects are split into time waiting for one of the hydration slots and time working. - `useObjectCreationTimes` keeps its earlier answer up while the objects overlap, as after one is created, so a new index doesn't blank what depends on it. - `useClusterHydrationData` and `useHydrationDetail` load and derive what the tab shows. The detail's object timings and CPU sample load and fail on their own, and the timings don't wait on a catalog that failed. - **Fix:** `SubscribeManager` starts a clean snapshot when it resends a subscribe the server ended, as when the subscribe's target replica restarts. The old run's rows and error used to stay, so a per-replica subscribe read as failed for as long as it stayed mounted. The held state stays until the new run updates it, so a statement that fails every time keeps its error without flickering. Resends of a statement that keeps failing without a snapshot back off from 1s to 30s, so a replica-pinned subscribe the user lacks USAGE for isn't resent as fast as the round trip. The first resend stays immediate. The delay is the reconnect backoff, now shared as `backoffDelayMs`. - **Fix:** collection subscribes now keep their session variables across connection attempts. Each attempt sent the defaults instead, so a cluster passed to a collection subscribe never reached the server. No existing caller passed one. Only variables that are set are passed, so unpinned subscribes keep running on `mz_catalog_server`. - `types/materialize.d.ts` is regenerated with `yarn gen:types`. Most of its diff is the generator catching up with the catalog: builtin materialized views are no longer `Generated<>`, and relations added since the last run (including the two hydration histories and `mz_object_global_ids`) appear. `yarn gen:types` doesn't run against Materialize as is: kysely 0.28's Postgres introspector calls `pg_get_serial_sequence`, which Materialize lacks, and lists only tables and views, which leaves out the catalog's builtin materialized views. The types here were generated with both patched locally in `node_modules`. Fixing the script is a follow-up. Cost on `mz_catalog_server`: the episode query and the object query read storage, scoped to one cluster or replica and a time bound, as the redesign's status timeline already does. Episodes poll at the history's 60s collection interval. The replica history scans its index, fetched once per change to the replicas and per mount of the tab. An episode's objects poll each minute only while the open detail is missing some. The dropped objects read scans `mz_object_history`'s index, which holds every object the environment has created: 8.5 ms at 32,000 objects on v26.47, plus about 0.23 ms per 1,000. It runs only while a restart reads as finished, and again only when those restarts change. Looking the window's ids up instead costs about 2 ms plus 60 µs an id and takes several round trips to find them, so it beats the scan only past roughly 70,000 objects. The Metrics tab's "Hydrating · N objects" badge counts introspection dataflows as well as user objects, so for a moment after a replica boots it can disagree with this PR's per-replica progress. ### Tests - `hydrationHistory.test.sql.ts`: each builder against shadowed relations (grouping and peaks per episode, the 26.43 filter, episodes with no objects, dropped replicas in the window, user objects only, per-worker states combined into one per object, catalog ids for dropped objects), and against the real catalog, including a new index's hydration streamed from a session pinned to the quickstart replica. - `hydrationModel.test.ts`: full and additive episodes, a zero-downtime upgrade's rehydration counted as full by the objects it covered, a burst of new objects that outnumbers the existing ones, out-of-memory restarts that never finished, a restart merged across processes, restarts after a finished hydration and on drop, a restart while hydrating an object created after the replica finished, a restart after the replica came back online kept apart from the one before it, a crash loop a new object started on a replica up since before the window, the restarts an object missing from the creation times could explain, an object dropped since counted only for the restarts while it existed, an attempt that began before the window, an upgrade's late online row, a new pod's offline row, a boot with nothing to hydrate, the latest full hydration by finish, dropped replicas outside the window, heap limits, and object spans. - `useLiveHydration.test.ts`: progress counted per replica from the stream on `mz_catalog_server`, progress complete at once for a cluster with no replicas, per-object states read in a session pinned to the replica, following the replica's new process after its subscribe fails over a restart, and no subscribe before 26.39. - `SubscribeManager.test.ts`: a subscribe the server ends and runs again drops its error and old rows, one that fails every time keeps reporting its error, and one that fails every time is resent with growing delays. - `queries.test.ts`: the replica history fetched again when a set of replicas comes back, an episode's objects fetched until all are recorded, including on reopening the episode, and the dropped objects read only once a restart needs them. - `objectCreationTimes.test.sql.ts`: against the real catalog, the dropped-objects query returns when the cluster's indexes and materialized views created since the start and dropped since were created and dropped, and leaves out ones still there, created before the start, or of another type. - `replicaProcessMetrics.test.sql.ts` reads its snapshot through the new shared `readSubscribeSnapshot` helper. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/materializeinc/materialize/pull/40044" target="_blank"><picture><source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-devin-review-dark.svg?v=4"><img src="https://static.devin.ai/assets/gh-devin-review-light.svg?v=4" alt="Devin Review"></picture></a> <!-- devin-review-badge-end -->
Motivation
Part of the cluster details redesign stack, on top of #39992. The configuration drawer shows a cluster's autoscaling policy, but changing it meant copying an
ALTER CLUSTERstatement into the SQL shell. Review feedback asked for a form. This adds the policy to the Alter cluster form.Description
The form. Alter cluster gets an optional Auto-Scaling Policy section for managed clusters on environments with autoscaling (v26.34.0 and later): an Enable checkbox, a burst size, and a linger duration such as
15s,2m,1m 30sor1500ms, empty for the environment'sdefault_hydration_burst_linger. The section is a labelled group, and the checkbox is described by its explanation. An Equivalent Code panel shows the statements a submit will run.SIZEorREPLICATION FACTORwhile a reconfiguration is in flight goes throughreshape_alter_cluster_managed, where restoring the realized values cancels it (cluster.rs~325-335). With nothing to change, the preview says so and a submit closes the form without a request.HYDRATION SIZE must differ from the target SIZE ... of the in-progress cluster resize,cluster.rs~1503). A submit that also changes Size retargets the resize, and that SET takes the reshape path, which accepts the old target as the burst size.The modal is shared, so the classic page's Alter cluster item gets the section and these fixes too. It used to send the realized
SIZEandREPLICATION FACTORon every submit, which cancels an in-flight resize. The flag behind the SQL,enable_auto_scaling_strategy, defaults on and has no LaunchDarkly override.The SQL. Each statement commits on its own, in order:
ALTER CLUSTER ... SET (...)naming only the changedSIZE,REPLICATION FACTORandAUTO SCALING STRATEGY. A policy isAUTO SCALING STRATEGY = (ON HYDRATION (HYDRATION SIZE = '...'[, LINGER DURATION = '...'])).AUTO SCALING STRATEGY = (), so a SET the server rejects (a size that isn't allowed, a replica or credit limit) leaves the policy in place. The planner checks the new size against the effective config, so resizing to the old burst size in the same statement passes. A removal on its own isALTER CLUSTER ... RESET (AUTO SCALING STRATEGY), which also works with the feature flag off. Neither cancels an in-flight resize.ALTER CLUSTER ... RENAME TO ...only for a new name; a same-name rename writes an audit event.mz_internal.mz_cluster_auto_scaling_strategiesand the in-progress target frommz_internal.mz_cluster_reconfigurations(status = 'in-progress',target->>'size') in one request. Both are fast-path index lookups onmz_catalog_server, and both shipped in v26.34.0. The request reads at strict serializable: the form makes it right after an ALTER, and a serializable read can come back from before the write.Self-managed. Self-managed has no LaunchDarkly, so a flagged UI preview ships there outright. A preview can now set
selfManagedOptIn, which offers the same per-browser opt-in there: the profile menu's UI previews and the page pill. The cluster page redesign sets it, andcluster-details-page-cns-173leavesdisabledFlexibleDeploymentFlags, so self-managed users get the classic page until they opt in. The cluster list preview still ships outright.The drawer. The Auto-Scaling panel's copyable SET and RESET statements become an Edit action that opens the form at the auto-scaling section. The form does its own post-mount read, since it also needs the resize target, so Edit waits only for the cluster sizes.
Verification
alterCluster.test.sql.ts, against a real Materialize:clusterConfiguration.test.sql.ts: policies with and without a linger, parsed as the drawer parses them.alterCluster.test.ts: statements for size-only, replicas-only, strategy-only, removal with and without other changes, rename-only and nothing changed.autoScalingForm.test.ts: linger parsing and display, size families, the default burst size with and without an in-progress resize, and when a submit sets, removes or keeps the policy.AlterClusterModal.test.tsx:NaNwhile Replicas is empty;useUiPreview.test.tsx: the cluster page redesign is offered in self-managed, off until the user opts in.ClusterConfigurationDrawer.test.tsx: the Auto-Scaling panel's Edit action, and Edit waiting only for the sizes.🤖 Generated with Claude Code