Skip to content

console: show subsource and source table freshness in Maintained Objects - #40105

Merged
leedqin merged 6 commits into
MaterializeInc:mainfrom
leedqin:console-subsource-freshness
Oct 23, 2026
Merged

leedqin merged 6 commits into
MaterializeInc:mainfrom
leedqin:console-subsource-freshness

Conversation

@leedqin

@leedqin leedqin commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Closes: CNS-210

Maintained Objects hid subsources and every table, so the only freshness a source showed was the source row's own. That number tracks the source's progress collection, which advances on a timer as it reads the upstream position, whether or not table data has been written. A source can therefore read 1s while its subsources and tables lag by a minute or more, and the console gave no way to see the per-table number. This also covers CNS-190.

The critical path had the same blind spot. It only followed edges between sources, MVs, indexes and sinks, so an MV reading CREATE TABLE ... FROM SOURCE tables stopped at the MV and never reached the lagging table. Sources had no critical path at all.

With Postgres sources now snapshotting a newly added table without holding back the rest, users need to see each table's freshness and snapshot status to see that improvement.

Description

List

  • Subsources and source-fed tables appear as rows with type subsource or table. Each carries its own freshness and ingestion status, its source's cluster, and its source's connector type. A table that is still snapshotting reads "Snapshotting" next to siblings that read "Running". The Object Type, Cluster and Status filters cover them.
  • Progress collections and tables written with INSERT stay hidden.

Drawer

  • A source's Freshness tab lists its subsources and tables with their freshness over the lookback window and their status. One line explains why the source's own number can read fresher than its tables.
  • A subsource or table shows a link to its source, its ingestion status, and its own freshness chart.

Critical path

  • A subsource or source-fed table walks back to its source. The breakdown strip then separates the source's lag from the table's own ingestion delay. An MV's path continues past the table to the source.
  • A source has no upstream inside Materialize, so its critical path draws the subsources and tables it writes to. The one that trails the source the most is marked as the bottleneck, and the source itself is marked when none trails it. The four slowest are drawn, with a toggle for the rest. An unreadable (still snapshotting) table always sorts first. The node panel shows cluster metrics, which replaces the separate "Source cluster metrics" section.
  • Node tags read source, subsource or table.
  • The critical path graphs and the object drawer show an error when their query fails, instead of spinning.

Data and cost

  • The subsource and source-table to source mapping is one builder, buildSourceExportsQuery. The hydration-aggregate SUBSCRIBE, the critical path and the workflow graph all use it.
  • The parent mapping rides on the page's existing hydration-aggregate SUBSCRIBE, which now also keeps subsource status rows. That adds an mz_tables_ind scan and an mz_object_dependencies join to that page-scoped subscribe. There is no new subscribe or poll.
  • The source critical path reads its exports' binned lag by id from mz_wallclock_global_lag_recent_history (an index lookup) and labels nodes from the list.
  • The drawer's freshness chart also moves to the id-keyed query, which drops its catalog joins. The query it used inner-joins mz_clusters on the object's own cluster, so it returned nothing for subsources and tables.

Fix in a shared component

  • Canvas now sizes the graph in pixels. Chakra read the graph's numeric height as a theme size token when one matched, so a single-row graph (56px, the token for 14rem) rendered 224px tall and clipped its own node.

Not in this PR:

  • The snapshot progress percentage in Source diagnostics is still missing for source-fed tables, because fetchSourceStatistics resolves stats through mz_sources. Subsources get a percentage.
  • While any one table snapshots, the source's own status pill also reads "Snapshotting", because that is what the source-level snapshot_committed reports. The per-table rows show which table it is.

Screenshots

From a local Materialize. shop_src writes four tables and is snapshotting a fifth, page_views. The 15M-row snapshot saturates the small local replica, which is why the other tables lag too. crm_src has a legacy subsource, and order_summary reads one table from each source.

Source: per-table freshness and status, and the critical path out to its tables.

Source critical path

Source-fed table: the critical path back to its source.

Table critical path

Materialized view: the path now runs through the subsource and table to their sources.

Materialized view critical path

Verification

Tests added or changed:

  • queries.test.ts (new) covers the list builder:
    • Subsources and source-fed tables carry their source's cluster and connector type.
    • Progress collections and written tables are hidden.
    • A subsource is listed before its parent is known.
  • MaintainedObjects.test.tsx: a snapshotting table renders next to a running one from the same source.
  • criticalPathRenderable.test.ts covers the source critical path:
    • The export that trails the source most is primary, or the source is when none trails it.
    • Each node reads the bucket containing the anchor.
    • An unreadable export stays visible past the cap.
    • The cap and toggle work, and a source with no exports draws alone.
  • hydrationAggregate.test.sql.ts:
    • Subsources are reported by status only.
    • Each subsource and source-fed table names its source and carries its own snapshot flag.
  • criticalPath.test.sql.ts:
    • The path follows a source-fed table and a subsource back to their source, and leaves out a written table.
    • A subsource walks back to its source.

I checked all views in a browser against a local Materialize with Postgres sources. I also ran EXPLAIN on mz_catalog_server for the changed queries.

🤖 Generated with Claude Code

Maintained Objects hid subsources and every table, so a source's per-table
freshness was invisible. The source row's own freshness tracks its progress
collection, which advances on a timer, so a source could look fresh while
its tables lagged by minutes. The critical path also dropped table inputs,
so an MV over source-fed tables never reached the lagging table.

List subsources and tables created with CREATE TABLE ... FROM SOURCE with
their own freshness and ingestion status, on their source's cluster. Add a
per-table freshness section to the source drawer, follow source-fed tables
in the critical path, and read the drawer's lag history without the
cluster join that dropped these objects.

Closes: CNS-210
@leedqin
leedqin requested a review from a team as a code owner October 22, 2026 18:00
@leedqin
leedqin requested a review from jdonelson October 22, 2026 18:00
A subsource or source-fed table now walks back to its source, so its
breakdown separates the source's lag from its own ingestion delay, and an
MV's path continues past the table to the source. A source has no upstream
inside Materialize, so its critical path draws the subsources and tables it
writes to, with the one trailing it the most marked as the bottleneck. The
graph's node panel carries the cluster metrics the separate source section
used to show.
The source view's per-export lag came from a catalog query that scanned
the whole lag history and the object catalog to label its rows. It now
reads the exports' binned lag history by id, an index lookup, and labels
the nodes from the list. An unreadable export sorts first so the node
cap cannot hide a table that is still snapshotting.

The subsource and source-table to source mapping is one builder shared
by the hydration feed, the critical path and the workflow graph. The
critical path graphs and the object drawer show an error instead of
spinning when their query fails.
The source node took its lag from the list, which can describe a later
minute than the bucket its subsources and tables are read from.
Canvas passed the graph's numeric size to Chakra, which reads a bare
number as a theme size token when one matches. A single-row critical path
is 56px tall, the size token for 14rem, so it rendered 224px tall and
clipped its own node. The source critical path draws a single row for any
source without subsources or tables.
@leedqin
leedqin requested a review from martykulma October 23, 2026 18:48

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

Really nice to see subsources and tables get their own freshness, and moving the parent lookup into one buildSourceExportsQuery cleans up three copies. Unit tests pass (39/39) and typecheck is clean. I didn't run the SQL tests. One design question on the critical path edge and one nit, otherwise looking good.

WHERE src.type IN (${MAINTAINED_OBJECT_TYPES_SQL})
OR t.source_id IS NOT NULL
UNION ALL
SELECT source_id, export_id FROM source_exports

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.

I wonder if the edge from a source to its subsources and tables should look different from the other edges in the critical path. The other edges mean "computed from": an MV can't be fresher than its input, so a slow input holds it back. A subsource isn't computed from its source. Both are written by the same ingestion, and the source's lag doesn't carry down to it. With the same arrow, the graph reads as if it does.

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.

Good call, it isn't a "computed from" edge, so it's now dotted, with a one-line legend under the graph (8389c8d). The source's lag does carry down for CREATE TABLE … FROM SOURCE tables, though. The source's own collection is the ingestion's progress collection (progress_id() in src/catalog/src/memory/objects.rs), and its tables are reclocked against it, so a table can't be fresher than its source. That's what the "1s from source + 2s self-delay" split relies on. It doesn't hold for legacy subsources, where the source is just another export and progress lives in <source>_progress, so those no longer get the edge.

Comment on lines +110 to +112
* True for a source and for the subsources and tables it ingests into. These
* report ingestion status in place of replica hydration, and have no upstream
* dataflow for the critical path to walk.

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: subsources and tables do have an upstream for the critical path now (their source). Since this only picks the status to show, maybe drop the last clause?

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, dropped it.

The edge from a source to one of its subsources or tables isn't "computed
from". For a source whose tables come from CREATE TABLE ... FROM SOURCE,
the source's own collection is the ingestion's progress collection and its
tables are reclocked against it, so the source's lag bounds theirs. Those
edges are now marked and drawn dotted, with a legend under the graph.

A legacy source keeps its progress in a separate progress subsource and is
itself another export of the same ingestion, so its lag says nothing about
its subsources. Legacy sources no longer get these edges, and a subsource
without one still draws alone with its source's cluster metrics.
@leedqin
leedqin enabled auto-merge (squash) October 23, 2026 20:52

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

lgtm, thanks @leedqin!

@leedqin
leedqin merged commit 4ab8c2b into MaterializeInc:main Oct 23, 2026
86 checks passed
@leedqin leedqin added the A-CONSOLE Area: Console label Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CONSOLE Area: Console

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants