Skip to content

console: set a cluster's autoscaling policy from the Alter cluster form - #40043

Merged
leedqin merged 3 commits into
mainfrom
cluster-page-autoscaling-form
Nov 3, 2026
Merged

leedqin merged 3 commits into
mainfrom
cluster-page-autoscaling-form

Conversation

@leedqin

@leedqin leedqin commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

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 CLUSTER statement 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 30s or 1500ms, empty for the environment's default_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.

  • The form opens on a policy it reads after opening, so a policy set or removed moments earlier is never shown stale, from the Clusters list or the configuration drawer. It diffs against the policy it was built from, so a refetch landing while it's open doesn't turn an unchanged checkbox into a change. If the policy can't be read, the section is left out rather than shown as off.
  • It names only what changes. Enabling a policy or renaming during a graceful resize never retargets or cancels it: any SET naming SIZE or REPLICATION FACTOR while a reconfiguration is in flight goes through reshape_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.
  • The burst size defaults to the next larger size in the cluster's size family, and to nothing at a family boundary. During an in-progress resize it defaults to the size after the resize's target, and the target itself is rejected while the size is unchanged, as the server does (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.
  • While a resize is in flight, Size reads "Resizing to ." under the field.
  • From v26.35.0 a size change shows that the cluster keeps serving from its current replicas until the new ones catch up; earlier versions keep the downtime warning.

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 SIZE and REPLICATION FACTOR on 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:

  • Settings: one ALTER CLUSTER ... SET (...) naming only the changed SIZE, REPLICATION FACTOR and AUTO SCALING STRATEGY. A policy is AUTO SCALING STRATEGY = (ON HYDRATION (HYDRATION SIZE = '...'[, LINGER DURATION = '...'])).
  • Removing a policy with other changes joins the same SET as 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 is ALTER CLUSTER ... RESET (AUTO SCALING STRATEGY), which also works with the feature flag off. Neither cancels an in-flight resize.
  • Rename: ALTER CLUSTER ... RENAME TO ... only for a new name; a same-name rename writes an audit event.
  • Reads: the form reads mz_internal.mz_cluster_auto_scaling_strategies and the in-progress target from mz_internal.mz_cluster_reconfigurations (status = 'in-progress', target->>'size') in one request. Both are fast-path index lookups on mz_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, and cluster-details-page-cns-173 leaves disabledFlexibleDeploymentFlags, 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:
    • setting, replacing and keeping a policy;
    • a rejected removal-plus-resize leaves the policy in place, and removing a policy while resizing to its burst size succeeds in one statement;
    • during an in-flight resize, the target is read, a burst equal to it is rejected, and setting a policy, removing it and renaming all leave the resize running until it finalizes at the new size;
    • one SET retargets an in-flight resize and sets its old target as the burst size;
    • settled or absent resizes read no target.
  • 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:
    • the reopen sequence that used to revert a just-set or just-removed policy now shows it and sends only the change;
    • props changing under an open form;
    • no request when nothing changed, and the preview without NaN while Replicas is empty;
    • the in-progress resize's target under Size;
    • errors for an invalid linger, a burst equal to the cluster's size, and one equal to an in-progress resize's target;
    • the labelled group, the version-gated resize copy, no section before v26.34.0, for unmanaged clusters, or when the policy can't be read.
  • 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


Devin Review

@def-

def- commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- Editing the auto-scaling policy cancels an in-flight cluster resize

console/src/platform/clusters/AlterClusterModal.tsx:181

The new Auto-Scaling panel Edit action submits SET (SIZE = <cluster.size>, REPLICATION FACTOR = <cluster.replicationFactor>, AUTO SCALING STRATEGY = ...). While a background resize is in progress, those values are the pre-resize realized shape, so the adapter treats the statement as a cancel. Turning on auto-scaling right after a resize silently reverts the resize, and the form still shows "altered successfully".

Details

alterParams always fills size and replicas from the form, and those default to cluster.size and cluster.replicationFactor. Both come from mz_clusters, which reads the realized config (src/catalog/src/builtin/mz_catalog.rs, config->'variant'->'Managed'->>'size'). With enable_background_alter_cluster on (the default), an ALTER CLUSTER ... SET (SIZE = 'L') returns right away. The realized size stays at the old value until the new replicas hydrate, which can take up to the 24h default timeout. The PR's own SQL test has to poll mz_clusters.size after a resize for this reason.

While a reconfiguration is in flight, any SET that names SIZE or REPLICATION FACTOR goes through reshape_alter_cluster_managed. A target equal to the realized shape counts as a cancel, "even when value-identical" (src/adapter/src/coord/sequencer/inner/cluster.rs:330-335, :539). test/testdrive/cluster-controller.td:651-653 pins this behavior.

The statements this PR removes from the panel were strategy-only (SET (AUTO SCALING STRATEGY = ...)). Those take the direct path and keep the in-flight record. The new Edit flow loses that property. Resizing and then enabling a hydration burst is a natural sequence of steps, and it reverts the resize.

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. ALTER CLUSTER must not send SET (), but a strategy-only SET or a RESET is fine. This also stops a rename-only submit from cancelling a resize.

@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from 4ea2ff9 to 72d7ea8 Compare October 21, 2026 15:15
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from 72d7ea8 to f386c33 Compare October 21, 2026 15:21
@def-

def- commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- Reopening Alter cluster shows the old policy, and the next submit reverts the change

console/src/platform/clusters/AlterClusterModal.tsx:189

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 RESET (AUTO SCALING STRATEGY) and removes the policy that was just set. The checkbox comes from the cached policy at mount, but the submit is diffed against whatever policy a background refetch delivers after that.

Details

AlterClusterMenuItem mounts the modal only while it is open. When an alter succeeds, useAlterCluster invalidates clusterQueryKeys.all(), which starts a refetch of the autoScaling query, and then the modal's onSuccess closes the modal. That removes the query's last observer while the fetch is still in flight. The query function reads signal, so React Query cancels the fetch with revert: true (Query.removeObserver in query-core), and the cache keeps the pre-alter null.

On reopen, useQuery returns the cached null right away. Line 108 skips the spinner, and the form mounts with autoScalingEnabled: false (line 170). The refetch then returns the real policy and autoScalingPolicy changes. autoScalingChange(autoScalingPolicy, formValues) turns the unchecked box into null, which means a RESET. The Equivalent code panel does show the RESET, but the checkbox still looks like a correct "off".

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 useDisclosure toggle, as AlterClusterMenuItem does, and adds 150ms of latency to the policy read. The steps were: open, enable, submit, reopen, set replicas to 2, submit. The second request was RESET (AUTO SCALING STRATEGY), then SET (SIZE = '400cc', REPLICATION FACTOR = 2), then the rename.

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 isFetchedAfterMount), not on any cached value.

@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from f386c33 to 1c67755 Compare October 21, 2026 21:16
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch 3 times, most recently from a1edf46 to ecb0059 Compare October 22, 2026 15:23
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from ecb0059 to 4deee1f Compare October 22, 2026 15:44
</InlineLabeledInput>
</FormControl>
<FormControl isInvalid={!!formState.errors.size}>
<InlineLabeledInput label="Size">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "Equivalent Code", to match "Auto-Scaling Policy" above it. Headers use Title Case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from 4deee1f to d3bac49 Compare October 23, 2026 19:16
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from d3bac49 to 819a47e Compare October 23, 2026 20:07
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch 2 times, most recently from a58b54c to 6904613 Compare October 26, 2026 16:06

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +415 to +418
clusterSize === opened.size &&
value === opened.autoScaling?.resizeTargetSize
) {
return "Burst size must differ from the size the cluster is resizing to.";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +76 to +82
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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from 6904613 to d4ff693 Compare October 26, 2026 17:52
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from d4ff693 to b8a3f9b Compare October 26, 2026 18:01
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from b8a3f9b to 5abc1b2 Compare October 28, 2026 20:42
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from 5abc1b2 to bea8596 Compare October 29, 2026 15:59
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from bea8596 to 59020e9 Compare October 29, 2026 16:15
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch 5 times, most recently from a43b06e to ed86ac5 Compare October 29, 2026 20:22
@leedqin

leedqin commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Both QA findings are fixed:

  • Resize cancel (6304766245): the form names SIZE and REPLICATION FACTOR only when they change, diffing against what it opened with, so editing the policy or renaming leaves an in-flight resize running. The SQL test "leaves an in-flight resize running when the policy or name changes" covers it.
  • Stale policy on reopen (6320788838): the policy read refetches on mount, the form waits for it, and submits diff against the snapshot the form opened with. Two tests reopen the form after setting and after removing a policy.

Base automatically changed from cluster-page-configuration to main November 2, 2026 19:29
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch 2 times, most recently from 2d643b6 to 18a9421 Compare November 2, 2026 20:37
@leedqin
leedqin enabled stacked auto-merge (squash) November 2, 2026 20:38
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.
@leedqin
leedqin force-pushed the cluster-page-autoscaling-form branch from 18a9421 to b0acb94 Compare November 3, 2026 00:12
@leedqin
leedqin merged commit 7affdc7 into main Nov 3, 2026
78 checks passed
@leedqin
leedqin deleted the cluster-page-autoscaling-form branch November 3, 2026 00:37
leedqin added a commit that referenced this pull request Nov 4, 2026
…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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants