Skip to content

console: add a hydration tab to the redesigned cluster page - #40074

Open
leedqin wants to merge 1 commit into
cluster-page-hydration-datafrom
cluster-page-hydration
Open

leedqin wants to merge 1 commit into
cluster-page-hydration-datafrom
cluster-page-hydration

Conversation

@leedqin

@leedqin leedqin commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Tenth PR in the cluster details redesign stack (#39509), on top of #40044, which adds the data. The redesign's prototype has a Hydration report tab. This adds a Hydration tab built on what the database records about hydration, and says which parts the Console infers. Part of CNS-173 and CNS-209. There's no Figma design; the layout follows the prototype and the PM's feedback on it.

Description

  • Tab: Hydration, after Objects. Its link carries the ?replica= choice like the Metrics and Workers tabs, and the classic page redirects a Hydration link to its overview.
  • Controls: the replica select shared with the other tabs, and a time range of 24 hours, 7 days (the default) or 30 days. Restarts come from the replica status history, which is kept 30 days.
  • Alert: only when something needs attention. A replica that keeps restarting before it finishes, with when it began (date and time), how many times, and a link to the docs on hydration failures. A replica hydrating, with how many objects it has done and how long it has been going, dated from its boot or, when every object it hasn't hydrated is newer than the boot, from the first one's creation. A replica that's offline, whatever its last progress said, or one that hasn't reported yet on a cluster with objects. A cluster without indexes or materialized views says it has nothing to hydrate.
  • Hydration in Progress: shown while a replica isn't hydrated, with elapsed times that tick every second. A replica that hasn't reported reads as starting only for two minutes after it boots. How many objects it has hydrated so far, and for a replica that is hydrating, the objects it is working on and how many wait for a hydration slot, read from the replica itself and mounted only while it hydrates.
  • History chart: a dot per hydration by replica, filled for a full hydration and hollow for an additive one, with restarts that never finished in a lane below. Restarts close together are one mark with a count, so a restart loop stays readable. The y axis is hydration time, ticked at round durations. Each mark is a button that selects its hydration.
  • Before the next full hydration: how long the last full hydration took, how many objects it covered against how many the cluster has now, and its memory as a share of the heap limit. Facts, not a forecast.
  • Detail: the last successful hydration by default, or the one selected. Its duration, peak memory, disk and heap, each as a share of its limit with the bytes (heap turns red near the limit), and the highest CPU sample during it. Below, a waterfall of each object's time waiting for a slot and working, in start order or slowest first, 30 objects a page. For a restart, why there is nothing to break down.
  • Table: every hydration, filterable by full, additive or restarted, timed by when it began or, for a restart, when the replica restarted; each row's time selects it.
  • Replica select: a retired replica, such as a burst replica, stays selectable while its history is in the window.
  • Labels: tooltips say what each figure is. The Console's inferences (full or additive, restarts before finishing) say "Inferred".
  • Loading: each section renders as soon as its own data arrives and shows its own error. The history doesn't wait for the live progress, and the alert waits for both, since telling a restart before finishing from a crash after hydrating needs the history. The history waits for the cluster's objects and their creation times, which tell a full hydration from an additive one, and falls back to replica starts alone when the catalog can't be read. A new replica or index keeps the history up while its data loads, and the detail holds its height while another hydration's object timings load.

Shared pieces: the stack's ReplicaSelect, which can now mark a replica retired, a HYDRATION_TAB_PATH in tabLinks.ts, percentOf exported from processReadingsModel.ts, replicaStatus from the configuration drawer, each replica's size allocation from useClusterHydrationData, and PaginationControls, split out of TablePagination so the waterfall pages the same way tables do.

This is larger than the stack's other PRs, about 2,400 lines of components and 1,200 of tests, because it adds the whole tab at once.

Verification

Not yet checked in a real browser. Worth a look: dark mode, chart spacing with many hydrations and a restart loop, the waterfall's pages on a cluster with many objects, the red heap stat, and a replica that is hydrating, for the live object list.

ClusterHydration.test.tsx renders the tab against MSW handlers and the subscribe socket mock and covers:

  • the last successful hydration shown by default with no alert, and selecting another from the table;
  • a hydration's objects ordered slowest first and paged;
  • object timings kept when the CPU sample fails to load;
  • a replica restarting before it finishes, with the restart count, the docs link, and its restarts merged into one mark;
  • a hydrating replica's working and queued objects;
  • an offline replica called out rather than its last progress;
  • a cluster without indexes or materialized views;
  • a retired replica offered while its history is in the window, with no alert about the current replicas;
  • the history kept up while a new replica's or object's data loads;
  • elapsed times that move on, and restarts that went offline labelled as such;
  • a tooltip opening on keyboard focus;
  • the history showing without waiting for live progress;
  • narrowing to the replica chosen on the cluster page;
  • live progress kept when the history fails to load;
  • the history waiting for the catalog before it classifies hydrations and counts new objects, and falling back to replica starts, with object timings by id, when the catalog fails to load;
  • live progress only on environments older than 26.43;
  • a new object's hydration dated from its creation rather than the replica's boot;
  • a cluster with no replicas.

hydrationView.test.ts covers ordering finished and unfinished hydrations, restarts counted only since a replica last finished, when the current hydration began, offline and starting replicas, and each alert, including none once every replica is hydrated.

ClusterDetail.test.tsx covers the Hydration tab in the tab list, opening it from its link, and the classic page's redirect.

🤖 Generated with Claude Code


Devin Review

@leedqin
leedqin requested a review from a team as a code owner October 21, 2026 22:05
@leedqin
leedqin requested a review from Alphadelta14 October 21, 2026 22:05
@linear-code

linear-code Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

CNS-173

CNS-209

@leedqin
leedqin added this pull request to stack #39509 October 21, 2026 22:05
@leedqin
leedqin force-pushed the cluster-page-hydration branch from e3d6504 to 2c9a170 Compare October 21, 2026 22:07
@leedqin
leedqin removed the request for review from Alphadelta14 October 21, 2026 22:25
@leedqin
leedqin force-pushed the cluster-page-hydration branch 2 times, most recently from e03b526 to d40a36c Compare October 21, 2026 23:19
@def-

def- commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- A replica hydrating a new object reports elapsed time since its last boot, often days

console/src/platform/clusters/ClusterPage/Hydration/hydrationView.ts:102

summarizeReplicasNow sets bootedAt to the replica's latest boot whatever the reason the replica is hydrating. Creating an index or MV on a running cluster is the most common hydration, and there the boot can be days old. The verdict, the Replicas Now row and the chart's in-progress mark then all report days of hydration, and the chart's y axis stretches to fit, which flattens every real hydration onto the baseline.

Details

Reproduced with the test file's default fixtures: both replicas booted 3 days ago and finished full hydrations, then r1 got one new object (progress: { u1: [8, 9] }). The tab rendered:

  • Verdict: r1 is hydrating: 8 of 9 objects, 72h so far.
  • In-progress mark: r1 is hydrating, 72h so far, with the y axis topping out at 72h. The 26s and 4m hydrations all sat at y≈204 on a 180px plot. The axis ticks read 0ms, 24.0h, 24.0h, 24.0h, because formatDurationForAxis caps at 24h.
  • Replicas Now detail: 72h so far · last full hydration took 4m 31s.

latestBoot's contract ("when a replica's current hydration began") only holds when no hydration of that replica has finished since the boot. When one has, the current hydration is of new objects. Its start could come from the earliest installedAt among unhydrated objects in the live object rows, or from the creation time of objects created since the last finish in createdObjects. Failing both, omit the elapsed text and the in-progress mark. The same bootedAt feeds ClusterHydration.tsx:192-202 and :434-435.

2. MEDIUM -- A cluster with no replicas shows a spinner forever and never gets its verdict

console/src/platform/clusters/ClusterPage/Hydration/useClusterHydrationData.ts:202

With no current replicas, useLiveHydrationProgress passes no subscribe, so the collection's status stays idle with snapshotComplete: false. That makes isProgressLoading true permanently. Replicas Now then spins forever, and the verdict never renders. Both "This cluster has no replicas" branches (hydrationView.ts:133, ClusterHydration.tsx:405) can't be reached from the tab.

Details

Clusters with replication factor 0 are a normal state: paused clusters, and scheduled (REFRESH) clusters between refreshes. A probe with replicas: [] showed the history card resolve to "No hydrations in this period.". The Replicas Now card still read Replicas Now · Live from each replica · Loading... with a spinner after 3s. Nothing would ever complete it, since useGlobalSubscribeCollection starts no session without a request.

The fix is to treat "no replicas" as complete. Either set snapshotComplete: replicaIdsKey === "" || status.snapshotComplete in useLiveHydrationProgress, or gate isProgressLoading on currentReplicaIds.length > 0. hydrationView.test.ts only covers the pure function, so a component test with an empty replica list would guard this.

@leedqin
leedqin force-pushed the cluster-page-hydration branch from d40a36c to 61113e6 Compare October 22, 2026 14:59
@leedqin

leedqin commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, both are fixed in 61113e6:

  1. Elapsed time: the current hydration's start now comes from the objects the replica hasn't hydrated. If they were all created after its latest boot, it's hydrating new objects, dated from the first one's creation. Otherwise it's dated from the boot, and the elapsed time is left out while creation times aren't known. I didn't key it on a hydration finishing since the boot: a zero-downtime upgrade records the boot after the replica already hydrated, so the next new index after a release would read as hydrating since the upgrade.
  2. No replicas: useLiveHydrationProgress now reports complete when there's nothing to subscribe to (in console: add data for the redesigned cluster page's hydration tab #40044), so the verdict and Replicas Now show the no-replicas text.

hydrationView.test.ts and ClusterHydration.test.tsx cover both, including your 72h scenario.

@leedqin
leedqin force-pushed the cluster-page-hydration branch from 61113e6 to 02a263c Compare October 22, 2026 15:48
note={`${hydration.objectCount} ${hydration.objectCount === 1 ? "object" : "objects"}`}
/>
<Stat
label="Peak memory"

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.

It's not clear that disk/swap is included in the memory number. Instead of putting that information in a tooltip, maybe add it in small text under the readings? Or maybe include the RAM number to make it clearer that "Memory" is the sum?

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.

Agreed. The detail now shows Peak memory, Peak disk and Peak heap (memory plus disk) separately, each as a share of its own limit with the bytes underneath.

@leedqin
leedqin force-pushed the cluster-page-hydration branch from 02a263c to 2acffa5 Compare October 23, 2026 19:19
@leedqin
leedqin force-pushed the cluster-page-hydration branch from 2acffa5 to 8386788 Compare October 23, 2026 20:09
@def-

def- commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- A replica that is offline or still starting reads as hydrated, and the verdict says every replica is hydrated

console/src/platform/clusters/ClusterPage/Hydration/hydrationView.ts:127

summarizeReplicasNow takes a replica's state only from its live hydration rows and ignores whether the replica is up. A replica that hasn't reported yet counts as 0 of 0, which makes it "Hydrated". An offline replica whose last rows said hydrated also reads as "Hydrated". Either way the headline says "Every replica is hydrated." This happens right after a resize, a scale-up, resuming a paused cluster, or a crash, which are the moments people open this tab.

Details

Both cases come from how mz_compute_hydration_times is fed. Its data comes from a subscribe that runs on the replica (src/adapter/src/coord/introspection.rs).

  • New replica: it has no rows until its clusterd runs and the subscribe returns a first batch. On Kubernetes that wait includes pod scheduling. useLiveHydrationProgress seeds each current replica with 0 of 0, and ClusterHydration.tsx:425 draws a full bar when objectCount === 0.
  • Offline replica: on disconnect the delete is deferred, so "the collection serves the previous subscribe's data" until the replica reports again (introspection.rs:433).

In the probe, r1 was at 9 of 9:

  • r2 had no progress rows. The tab showed r2 · Hydrated · 0 of 0 objects with a full bar, and the verdict said Every replica is hydrated.
  • r2 had an offline status row from 3 minutes earlier and its old 9-of-9 rows. The verdict still said Every replica is hydrated.

A replica that isn't online, per cluster.replicas[].statuses, needs a state of its own. So does a replica with no rows on a cluster that has compute objects. A 0 of 0 replica on a cluster with no compute objects, such as one running only sources, really is hydrated.

@leedqin
leedqin force-pushed the cluster-page-hydration branch from 8386788 to b9acf58 Compare October 23, 2026 21:36
@leedqin
leedqin force-pushed the cluster-page-hydration branch from b9acf58 to 0a76db4 Compare October 26, 2026 16:10

@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 3 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +185 to +190
{detail.isError ? (
<Alert variant="error" message="Object timings couldn't be loaded." />
) : detail.isLoading || !detail.spans || !detail.summary ? (
<Flex justifyContent="center" py={6}>
<Spinner />
</Flex>

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.

🟡 Catalog failure traps hydration detail in loading

When the object catalog fails, FinishedDetail keeps showing a spinner for a selected hydration. useHydrationDetail never clears isLoading without a catalog snapshot, even when object timings load.

Learn more

The history deliberately falls back to replica starts if the catalog fails, so a finished hydration remains selectable. useHydrationDetail keeps isLoading true until hasObjectNames, but does not read the catalog error. The failed catalog never completes its snapshot, leaving the detail spinner on screen even after the episode timings arrive.

Example: A catalog subscribe fails while the hydration history query succeeds. The history table offers a finished hydration, but selecting it leaves the detail spinner visible indefinitely.

Recommended fix: Read isError from useClusterObjectsLive in useHydrationDetail. On catalog failure, show the object IDs already used as fallback names instead of blocking the timings, or surface a terminal detail error.

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.

Fixed in 9db574d: object timings no longer wait on a catalog that failed, and objects go by their ids then. Covered in ClusterHydration.test.tsx.

Comment on lines +306 to +308
lastFull={hydrations.findLast(
({ kind }) => kind === "full",
)}

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.

🟡 Latest full hydration uses wrong finish

When replicas hydrate concurrently, findLast can choose a hydration that finished earlier. transformHydrations sorts by start time, so the next-hydration facts use the wrong cutoff.

Learn more

transformHydrations sorts episodes by startedAt. NextHydration counts objects created after the chosen episode's finish, but findLast on that sorted array selects the last start, not the latest finish. Concurrent full hydrations can finish in the opposite order, giving an incorrect creation count and memory reading.

Example: Replica A starts at 10:00 and finishes at 10:08. Replica B starts at 10:01 and finishes at 10:02. An object created at 10:05 is reported as created since the last full hydration, even though A finished at 10:08.

Recommended fix: Choose the full hydration with the maximum finishedAt before passing it to NextHydration, matching the finish-time selection already used in hydrationVerdict.

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.

Fixed in c809a93: latestFullHydration picks the full hydration that finished last, and Before the next full hydration uses it.

Comment on lines +185 to +190
{detail.isError ? (
<Alert variant="error" message="Object timings couldn't be loaded." />
) : detail.isLoading || !detail.spans || !detail.summary ? (
<Flex justifyContent="center" py={6}>
<Spinner />
</Flex>

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.

🟡 CPU failure hides available object timings

When the CPU peak request fails, FinishedDetail hides valid object timings and reports that they failed. useHydrationDetail.isError combines both requests, although only CPU failed.

Learn more

The detail runs separate requests for per-object timings and for the highest CPU sample. useHydrationDetail joins their error and loading states. The error branch here then hides the waterfall on a CPU-only error, and the loading branch delays it while the CPU query is pending.

Example: The episode-object query returns two hydrated objects, while the CPU peak query errors. The detail displays “Object timings couldn't be loaded” instead of the two objects and their waterfall.

Recommended fix: Expose separate timing and CPU loading/error states in useHydrationDetail. Gate the waterfall only on the object-timing request, and render a missing CPU sample separately.

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.

Fixed in 9db574d: useHydrationDetail reports timings and CPU separately. A CPU failure shows on the CPU stat and leaves the waterfall in place.

@leedqin
leedqin force-pushed the cluster-page-hydration branch 2 times, most recently from c5d1828 to 03c3a78 Compare October 26, 2026 18:03
@leedqin
leedqin force-pushed the cluster-page-hydration branch from 03c3a78 to 9db574d Compare October 26, 2026 18:40
@leedqin

leedqin commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, fixed in 9db574d. An offline or not-yet-reporting replica reading as hydrated: a replica whose status is offline now reads as offline, whatever its last progress was. A replica that hasn't reported on a cluster with indexes or materialized views reads as starting. Both get a warning instead of a hydrated verdict. A replica with nothing to hydrate, on a cluster without compute objects, still counts as hydrated. Tests in hydrationView.test.ts and ClusterHydration.test.tsx.

@def-

def- commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- Selecting a retired replica says the cluster has no replicas

console/src/platform/clusters/ClusterPage/Hydration/ClusterHydration.tsx:227

With a retired replica selected, the tab shows "This cluster has no replicas, so nothing on it is hydrated." on a cluster whose replicas are running. The verdict is computed from the narrowed replica list, which then holds no current replica. That also hides the cluster's real verdict, such as a current replica stuck restarting.

Details

replicas is narrowed to the selected name (ClusterHydration.tsx:180-182), and summarizeReplicasNow keeps only current replicas (hydrationView.ts:123). For a retired name nothing is left, and hydrationVerdict([]) returns the no-replicas alert (hydrationView.ts:199-206).

Reproduced with the scenario of the test "offers a retired replica while its history is in the window": ?replica=r1-burst, with r1 and r2 online and hydrated. The alert renders. That test checks the option and "No hydrations in this period." but not that the alert is absent, so it passes.

Show the no-replicas verdict only when cluster.replicas.length === 0. Either compute the verdict from all current replicas, or leave it out while the selection has no current replica.

2. LOW -- Replicas of a cluster with introspection disabled read as "Starting" forever

console/src/platform/clusters/ClusterPage/Hydration/hydrationView.ts:143

hasNotReported treats "no hydration rows on a cluster with indexes or materialized views" as a replica that hasn't started. A replica that never reports rows matches this forever. Every replica of such a cluster then shows "Starting", and the tab warns "r1 and r2 are starting. Hydration progress shows once the replica reports it."

Details

With INTROSPECTION INTERVAL = 0, the replica runs with enable_logging = false and its compute logs are empty (src/compute/src/logging/compute.rs:325), so mz_compute_hydration_times has no rows for it. I checked this on a native v26.38 build with CREATE CLUSTER c_off SIZE = 'scale=1,workers=1', INTROSPECTION INTERVAL = 0 and an index on it. The replica was online and served SELECTs but had 0 hydration rows. The same setup with the default interval had 1 hydrated row. A vitest render with no progress rows shows the two "starting" texts above.

A cluster whose only compute objects are REFRESH AT materialized views past their last refresh hits the same path. The controller skips CreateDataflow for an empty as_of (src/compute-client/src/controller/instance.rs:1638), so after a restart the replica never reports those views.

Inferring "starting" only until the replica's status turns online, or for a short time after that, keeps the new-replica case and avoids both of these.

@leedqin
leedqin force-pushed the cluster-page-hydration branch from 9db574d to 7030b0a Compare October 27, 2026 04:08
@leedqin

leedqin commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, both are fixed in 7030b0a:

  1. Retired replica selected: the alert is left out while the selection is a retired replica, since that view shows only its history. The no-replicas text still only follows from the cluster having none. The retired-replica test now asserts no alert, and it fails without the fix.
  2. Starting forever: a replica that hasn't reported reads as starting only for two minutes after it booted. After that, as with introspection turned off or a cluster of REFRESH AT views past their last refresh, it's no longer called starting. There's a regression test in hydrationView.test.ts.

@leedqin
leedqin requested a review from jdonelson October 27, 2026 15:24
@leedqin
leedqin force-pushed the cluster-page-hydration branch from 7030b0a to 3941f8f Compare October 28, 2026 20:42

@jdonelson jdonelson 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.

Thanks for fixing the memory, disk and heap split. The detail card reads much more clearly now. Unit tests pass (898) and typecheck is clean. I haven't seen it in a browser either.

The one I'd fix before merging is the first comment: an offline replica in a restart loop reads as still hydrating, with a timer that keeps ticking. That's the main case this tab should catch. The others are smaller, and the subtitle is a nit.

Comment on lines +187 to +207
const state: ReplicaHydrationState =
restarts.length > 0 && (isOffline || isHydrating || hasNotReported)
? "restarting"
: isOffline
? "offline"
: hasNotReported
? "starting"
: isHydrating
? "hydrating"
: "hydrated";
return {
replica,
state,
progress,
startedAt: isHydrating
? currentHydrationStart(
bootedAt,
progress.unhydratedObjectIds,
createdAtMsById,
)
: undefined,

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.

Two ways an offline replica gets misreported:

  1. With restarts, an offline replica reads as "restarting", and ClusterHydration treats that as working. It keeps showing its last progress (7 of 10) with a timer that keeps ticking, an in-progress mark on the chart, and a live subscribe pinned to the offline replica. Maybe an offline replica shouldn't get a start time or count as working, even when labelled restarting?
  2. The verdict returns on a hydrating replica before it checks for offline ones, so "r1 is hydrating" hides "r2 is offline".

Comment on lines 156 to 164
const unfinished = findUnfinishedHydrations({
transitions,
oomKills: statusHistory.data?.oomKills ?? [],
oomKills: [
...(currentStatus.data?.oomKills ?? []),
...(droppedStatus.data?.oomKills ?? []),
],
hydrations: allHydrations,
replicas,
bootTimes,

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.

Before 26.43 there's no history, so every restart in the window looks unfinished. A replica that crashed once 1.5 days ago and is now hydrating a new index gets "r1 hasn't finished hydrating since … It restarted once". Maybe skip restart inference when hasHistory is false, since there's nothing to tell a failed restart from a recovered one?

Comment on lines +765 to +771
{peakPercent !== null && (
<Fact
label="Memory used"
help={HYDRATION_HELP.lastFullMemory}
value={`${formatPercentage(peakPercent / 100, 0)} of the heap limit`}
note={`${formatPercentage(Math.max(0, 100 - peakPercent) / 100, 0)} headroom at its peak`}
/>

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.

Two cards describe the same hydration, but name the same number differently:

Before the Next Full Hydration
  Memory used    64% of the heap limit

Detail
  Peak memory    75%
  Peak heap      64%   ← the same 64%

"Memory used" is really the heap (memory plus disk), so the reader sees "memory 64%" in one card and "memory 75%" in the other. Could it be labeled "Peak heap", like the detail? Its tooltip also says "at the last full hydration", while the detail's says, correctly, that the peak is recorded since the process started.

Separately, the objects tooltip in the same card says new objects "hydrate during the next one", but they hydrate right away, as the additive hydrations on the chart.

Comment on lines +103 to +117
function currentHydrationStart(
bootedAt: Date | undefined,
unhydratedObjectIds: string[],
createdAtMsById: Map<string, number> | undefined,
) {
if (!createdAtMsById) return undefined;
const bootMs = bootedAt?.getTime() ?? -Infinity;
const createdMs = unhydratedObjectIds.map((id) => createdAtMsById.get(id));
const isNewObjects =
createdMs.length > 0 &&
createdMs.every((ms) => ms !== undefined && ms > bootMs);
if (!isNewObjects) return bootedAt;
const firstMs = min(createdMs, (ms) => ms);
return firstMs === undefined ? undefined : new Date(firstMs);
}

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.

The elapsed time can be far off when an unhydrated object's creation time isn't in the lookup. The code then assumes the object is older than the boot and starts the clock at the boot. Two everyday cases:

  1. Right after CREATE INDEX, before the new index's creation time loads. On a replica booted 3 days ago, the alert flashes "r1 is hydrating: 8 of 9 objects, 72h so far", then corrects itself to "30s so far". The chart's y axis briefly stretches to 72h too.
    Maybe leave the elapsed time out while a creation time is still loading?

  2. When the object was created before the selected range, since the lookup only covers that range. With a replica booted 3 days ago and an index created 30 hours ago still loading:

    • Last 7 days: "30h so far" (correct)
    • Last 24 hours: "72h so far"

    startDate is optional in buildObjectCreationTimesQuery, so maybe fetch creation times without the range? Every range would then give the same answer.

];
// Replicas often hydrate at the same moment, so each is drawn this far from
// the next to keep both visible.
const REPLICA_OFFSET_PX = 4;

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.

Replicas that hydrate at the same moment overlap on the chart. That's common with two replicas, since both restart after an upgrade and both hydrate a new index.

  • Each replica's dot is offset by REPLICA_OFFSET_PX = 4, but the dot is 12px wide, so the two read as one.
  • Each dot also has an invisible 24px click area (r={12}), so the one drawn on top takes the clicks for the one underneath. With a mouse, only one of the two can be selected.

Maybe offset replicas by at least a dot's width?

>
<SectionCard
title="Hydration History"
subtitle="From Materialize's hydration history"

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: the subtitle says "From Materialize's hydration history", but only the finished hydrations come from it:

  • Restarts that never finished are inferred from replica status (the legend's tooltip says "Inferred").
  • The in-progress dot comes from live progress.

Since the tab labels its inferences everywhere else, maybe something like "Finished hydrations from Materialize's history. Restarts inferred from replica status."?

@leedqin
leedqin force-pushed the cluster-page-hydration branch from 3941f8f to 9a1e80c Compare October 29, 2026 15:59
@leedqin
leedqin force-pushed the cluster-page-hydration branch from 9a1e80c to e3965e8 Compare October 29, 2026 16:15
@leedqin
leedqin force-pushed the cluster-page-hydration branch 4 times, most recently from 8c9affa to b668916 Compare October 29, 2026 19:34
Second of two PRs adding a Hydration tab to the redesigned cluster
page, on top of the one that adds its data.

- A Hydration tab after Objects. Its link carries the replica choice,
  as the Metrics and Workers tabs' do, and the classic page redirects
  a Hydration link to its overview.
- The replica select shared with the other tabs, and a time range of
  its own: 24 hours, 7 days or 30 days, the status history's retention.
- A verdict: every replica hydrated and the last full hydration, a
  replica hydrating and how far along it is, or a replica that keeps
  restarting before it finishes, with the troubleshooting guide.
- Replicas Now: live objects hydrated per replica, and, for a replica
  that is hydrating, the objects it is working on and how many wait for
  a hydration slot, read from the replica itself.
- History: a dot per hydration over the window, by replica, filled for
  a full hydration and hollow for new objects, with restarts that never
  finished in a lane below and the typical full duration as a line.
  Each mark selects its hydration.
- Before the next full hydration: the typical full duration on this
  size, objects created since the last one, and its memory peak.
- The selected hydration's duration, peaks and sampled CPU, the object
  that worked longest, and a waterfall of each object's time waiting
  for a slot and working.
- A table of every hydration, filterable by kind.
- Tooltips say what each figure is, and which are the Console's
  inferences rather than the database's records.
- Each section renders as soon as its own data arrives, and shows its
  own error.
@leedqin
leedqin force-pushed the cluster-page-hydration branch from b668916 to b11844a Compare October 29, 2026 20:22
@def-

def- commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- The history drops every restart without an error when the current replicas' status history is loading or failed

console/src/platform/clusters/ClusterPage/Hydration/useClusterHydrationData.ts:225

The history's restarts and boot times come from currentStatus, but this PR moved that query's loading and error flags into isProgressLoading/isProgressError only. While it loads or after it fails, the history card renders as if complete. It shows no "Restarted before finishing" rows or marks, and it gives no error, so a replica in a restart loop looks like it never restarted.

Details

I reproduced this with the scenario of "calls out a replica that keeps restarting before it finishes", with the current replicas' status query made to fail:

  • The table had 3 rows (Additive, Full, Full) instead of 5. Neither restart appeared, and the history card had no alert.
  • The verdict was hidden. Only the Hydration in Progress card said "Live hydration progress couldn't be loaded."
  • The chart still drew r1's in-progress mark, dated from the replica's creation 3 days earlier because no online rows were loaded. Its y axis ticked 0, 24h, 48h, 72h, 96h.

The same partial history shows during the reload after any replica is added or dropped, which happens on every resize or burst, and through the 3 production retries. useReplicaStatusHistory keeps no placeholder data when its id list changes (queries.ts:1287). Adding currentStatus.isError to isHistoryError fixes the silent failure. Keeping the previous answer while the old and new replica sets overlap would keep the history up during the reload, as useObjectCreationTimes already does.

@def-

def- commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- A replica that finished hydrating and then crashed is reported as restarting before it finished

console/src/platform/clusters/ClusterPage/Hydration/hydrationView.ts:188

The tab treats "no finished hydration recorded" as "didn't finish", but the collector only records a replica's finish if it samples that replica before the process restarts. A replica that hydrates and then crashes a few minutes later, as in an out-of-memory loop under steady-state load, gets "Restarted before finishing" rows and the red "hasn't finished hydrating" alert with a link to the hydration-failure docs. That is the crash-after-hydrating case the description says the history lets the tab tell apart.

Details

The collector (src/adapter/src/coord/hydration_history.rs) samples one replica per tick, 60s by default, rotating through every online replica in the environment, system replicas included (:197, :226). It reads the replica's own mz_introspection logs, which a restart resets, and records only the latest completed episode (LIMIT 1, :496). With R online replicas, a replica is sampled every R minutes. A finish followed by a restart within that gap is never recorded.

Example with 12 online replicas: r1 is created by a resize at 10:00, finishes hydrating at 10:04 and is OOM-killed at 10:09. Its samples were at 10:02, while it was still hydrating, and 10:14, after the restart. While it rehydrates, the tab says "r1 hasn't finished hydrating since 10:00. It restarted once, after running out of memory." The detail says it "ran out of memory and restarted before it finished hydrating."

The NOTE on findUnfinishedHydrations says this "can" happen. With the rotation it is the expected outcome whenever a crash comes within a few minutes of finishing. The alert could say only what was observed, for example that no finished hydration was recorded between the restarts, and not be framed as a hydration failure. Alternatively, the collector could record a replica's episode when the replica goes offline.

This branch has not been deployed

No deployments
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