compute: add a lifecycle event log for compute exports - #38403
Conversation
Design: a lifecycle event log instead of wide timestamp columnsWriting up a design we converged on offline, which supersedes the approach in this PR. The code here is sound, but the shape it extends is not the one we want, and the reasons are worth recording before the stacked The problem with wide columnsTwo structural issues surface as soon as the write stages are added. The first is that the stages do not share a grain. The second is that a timestamp column cannot say why the next stage has not happened. ShapeOne append-only relation, per replica, in memory.
Indexes emit the first three and stop, which is the index degeneracy of the lifecycle falling out of the model rather than being special cased. Subscribes and Why
|
67183be to
93ee52b
Compare
93ee52b to
c906ea5
Compare
QA LLM Review1. HIGH -- Adding a builtin log also moves
|
c906ea5 to
aff3e16
Compare
|
Confirmed and fixed in I had found the Also took the test suggestion, and it is sharper than it first looks. For the record on the rest of this PR's CI: the five Generated by Claude Code |
QA LLM Review1. MEDIUM -- The back-filled
|
aff3e16 to
581179d
Compare
|
Confirmed and fixed in Split it into The part worth dwelling on: I wrote the comment "The lifecycle log needs the same back-fill" directly above a call that did not perform the same back-fill. The Two notes on the surrounding state, so the next review has the current picture: The I hand-edited eight goldens for this relation and have since found two errors in my own edits, so I am not going to keep hand-editing them. A local Generated by Claude Code |
966cae4 to
d183b3b
Compare
Record each compute export's lifecycle as an append-only log,
`mz_introspection.mz_compute_lifecycle_events_per_worker`, rather than as more
timestamp columns on the hydration time relation.
Two things stop timestamp columns from carrying the lifecycle. The stages do not
share a grain: `installed`, `started` and `hydrated` are per-worker facts, since
each worker hydrates its own fragment of the dataflow, while whether the output
is durable is a property of the sink as a whole, maintained on one elected
worker. And a timestamp cannot say why the next stage has not happened, so a
NULL cannot tell a replacement materialized view waiting for a cutover apart
from an index that will never write.
export_id text not null
worker_id uint8 not null
event text not null
occurred_at timestamptz not null
reason text nullable
details jsonb nullable
`installed`, `started` and `hydrated` are logged by every worker. The write
stages are logged only by the worker that maintains the sink's shared write
frontier, so they appear once per object and the row records which worker was
elected. An index emits the first three and stops, which is the index degeneracy
of the lifecycle falling out of the model rather than being special-cased.
`hydrated` reads the dataflow's own progress frontier, the compute probe, not
the reported output frontier. The output frontier folds in the write frontier,
which makes it a measure of durability, and for a sink-backed collection it is
not even uniform across workers: `mint` clears the shared frontier on every
non-elected worker, where it is the empty antichain and contributes nothing to
the meet. `mz_compute_hydration_times_per_worker` is unchanged, so `hydrated_at`
and `time_ns` keep reporting exactly what they reported before, and the new
relation carries the dataflow reading alongside.
The write stages are gated on hydration. Before it the sink has produced nothing
and read-only mode is holding nothing back, and every collection starts
read-only, so reporting a block from installation would put a `write_blocked`
and a `write_unblocked` on essentially every materialized view, both ahead of
`hydrated`. Gating also keeps `written` ordered after `hydrated`, which it is not
otherwise: `apply_refresh` rounds a `REFRESH` materialized view's frontier up to
the next refresh time off its input frontier, before the dataflow computes
anything, so the sink writes an empty batch for the pre-refresh window and the
shard's upper passes the as-of while the dataflow is still hydrating. That also
means a refresh schedule advances writing rather than blocking it, so there is no
`refresh` cause for `write_blocked` to report.
`details` carries the dataflow's as-of, which every stage is defined relative to:
without it the interval between two stages says nothing about how much work was
done, since a replacement materialized view with a far behind as-of is a
completely different amount of work at the same duration.
Part of CPU-226
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ZVCMBSLdxzGus78ZKWhZz
d183b3b to
1a6f46f
Compare
Motivation
The compute half of improved hydration visibility needs to say, per maintained object, when it started to hydrate, when it hydrated, and when its output became durable, so that a query can take min/max to determine cluster replica hydration.
Timestamp columns cannot carry that. Two things get in the way. The stages do not share a grain:
installed,startedandhydratedare per-worker facts, since each worker hydrates its own fragment of the dataflow, while whether the output is durable is a property of the sink as a whole, maintained on one elected worker. And a timestamp cannot say why the next stage has not happened, so a NULL cannot tell a replacement materialized view awaiting a cutover apart from an index that will never write, or from a materialized view whose refresh is in the future.Design doc:
doc/developer/design/20260817_compute_hydration_timestamps.md, reworked in this change. Part of CPU-226.Description
One append-only log relation,
mz_introspection.mz_compute_lifecycle_events_per_worker, per replica, in memory:reasoninstalledstartedhydratedwrite_blockedread_onlywrite_unblockedwrittenAn index emits the first three and stops, which is the index degeneracy of the lifecycle falling out of the model rather than being special-cased. Subscribes and
COPY TOstop early for the same reason, and a metric sink folds its output into the metrics registry rather than into a shard, so it has no write stages either.mz_compute_hydration_times_per_workeris untouched.hydrated_atstill reads the reported output frontier andtime_nsstill fires on the same crossing, somz_compute_hydration_times,mz_compute_hydration_statuses,mz_hydration_statusesand the blue-green readiness query keep reporting exactly what they reported before. The change is purely additive. An earlier version of this PR redefinedhydrated_at; that is dropped.Grain.
worker_idis the worker that observed the event and is never NULL. The per-object events are observed by the single worker that maintains the sink's shared write frontier, so they appear once per object and the row records which worker was elected. Nothing else may read that shared frontier as a measure of writing:mintclears it on every non-elected worker, where it is then the empty antichain and would report having written everything immediately. The electionhashed(sink_id) % peersnow has one definition,sink::materialized_view::frontier_owner, called both bymintand by the code recording ownership, rather than being spelled out separately in each place that needs it.hydratedreads the compute probe, not the output frontier. The output frontier is the meet of write and compute frontier, which makes it durability, and per the point above it is not uniform across workers for a sink-backed collection. A collection with no compute probe produces its output by writing it, an index into its own trace, so there the write frontier is the progress.The write stages are gated on hydration, and this is the one place the shipped behaviour differs from the design sketched in the review comment.
Before hydration the sink has produced nothing, so read-only mode is holding nothing back. Every collection starts read-only and is released by the controller, so reporting a block from installation would put a
write_blockedand awrite_unblockedon essentially every materialized view, both ahead ofhydrated, makingwrite_unblocked - hydratednegative rather than zero. Gating means the pair appears only when something really was held back, which is the state the log exists to expose.Gating also keeps
writtenordered afterhydrated, which it is not otherwise.apply_refreshrounds aREFRESHmaterialized view's frontier up to the next refresh time, and does so off its input frontier, before the dataflow has computed anything. So the sink sees a desired frontier ahead of the as-of immediately, mints a description for the pre-refresh window, and appends an empty batch, advancing the shard's upper while the dataflow is still hydrating.That last point has a second consequence: a refresh schedule advances writing rather than blocking it, so there is no
refreshcause forwrite_blockedto report and therefreshvalue is not in the vocabulary.test/testdrive/materialized-view-refresh-options.td:376already demonstrates this from the outside, asserting that a materialized view whose first refresh is ~16 minutes out reportsmz_hydration_statuses.hydrated = true, and that flag istime_ns IS NOT NULL, which requires the write frontier to have passed the as-of.reasonis typed,detailsis not, followingmz_source_statusesandmz_sink_statuses. Whatdetailscarries is the dataflow's as-of, which every stage is defined relative to: without it the interval between two stages says nothing about how much work was done, since a replacement materialized view with a far behind as-of is a completely different amount of work at the same duration. The as-of is rendered as a JSON string rather than a number, becausemz_timestamphas no faithful JSON number counterpart and a string round-trips exactly.Only
read_onlyis attributed. It is the one cause of a write block compute can observe. Two attributions from the design sketch are not available and are called out in the doc as follow-up rather than silently dropped: distinguishing astartedthat waited on the hydration limiter from one that waited on its inputs needsSequentialHydrationto report which, since both appear to the replica asSchedulearriving late; and distinguishing a freshCreateDataflowfrom a dataflow retained across reconciliation is not observable here at all, because a retained dataflow emits no newinstalledevent.The relation is unkeyed, so
index_byarranges by the whole row.(export_id, worker_id, event)is unique today, but declaring it a key is a uniqueness claim the optimizer acts on, and a false key is a correctness hazard where a wide index is only a performance one.Rows are retracted when the export is dropped, and the emitted row pairs are kept on
ExportStateso they are retracted verbatim rather than re-derived.Verification
cargo checkis clean formz-compute,mz-compute-clientandmz-catalog, the last two with--all-targets.test_compute_event_sizepasses, which is the check that addingas_ofto theExportevent and aLifecyclevariant toComputeEventdid not grow the enum.test/testdrive/compute-lifecycle-events.tdis new. It asserts the per-worker stage counts for an index and the per-object count for a materialized view'swritten, the ordering within each worker,writtennever precedinghydrated,occurred_atlanding in the recent past rather than being an offset from an arbitrary origin,details->>'as_of'being present, retraction on drop, and three invariants underset-max-tries max-tries=1: the closedeventandreasonvocabularies, no stage without its predecessors, and no stage reported twice per export and worker.Two honest limitations.
The testdrive file has not been executed. Running it needs an
environmentdimage, which this environment cannot build, so CI is the first thing to run it. The assertions are conservative but "expected to pass" is the accurate description.write_blockedandwrite_unblockedare not covered, because reaching them needs a replica that is still read-only after a collection has hydrated, which is a 0dt cutover and not reachable from plain testdrive. The invariant assertions are written so that the pair may be absent or complete, never requiring it to be present. A platform-check or a cluster test that drives read-only mode would be the place to cover them.The goldens were hand-edited rather than produced by
--rewrite-results, since that needs a full build:oid.slt,information_schema_tables.slt,cluster.slt,mz_catalog_server_index_accounting.slt,autogenerated/mz_introspection.slt, bothtest/workload-replaylists, and themz_introspectiondocs page, where the relation is markedRELATION_SPEC_UNDOCUMENTEDalongsidemz_compute_hydration_times_per_workerand the other per-worker logs. Worth a reviewer's eye, and CI will catch any that are wrong.Generated by Claude Code