Skip to content

Commit dfce91f

Browse files
committed
compute: drop the duplicated instants from the hydration times relation
`installed_at` and `started_at` were maintained twice, in this relation and in `mz_compute_lifecycle_events_per_worker`, from the same handlers and the same event times, with the `installed_at <= started_at` invariant enforced in both representations. Keep them in one place: the lifecycle log's `installed` and `started` events. `time_ns` and `hydrated_at` stay. They are not duplicates of anything, because `hydrated_at` is the durability reading, taken when the reported output frontier passes the as-of. That frontier is the meet of the write and compute frontiers, so for a collection that sinks to persist it moves only once the output is durable, while for an index, which produces its output by writing its own trace, it coincides with computation. One column meant two things depending on the object. The lifecycle log answers that by using separate terms rather than redefining the column: its `hydrated` is always the dataflow-progress reading and its `written` is always the durability one, neither depending on the object type. Pivoting the log's `hydrated` into `hydrated_at` would therefore not be a refactor but a redefinition, and since `mz_compute_hydration_statuses.hydrated` is `time_ns IS NOT NULL` and feeds the blue-green readiness query, it would have readiness cut over on the earlier compute reading, before the output is durable. Comments at both relations record which reading each term carries. `handle_hydration_start` no longer writes to the hydration-time output, since nothing it changes is packed there any more; its retract-and-reinsert pair would have been a no-op. `started_at` survives as internal state only, to guard the handler and to back-fill `started` in `handle_hydration`. `test/testdrive/hydration-timestamps.td` moves its stage scenarios to the lifecycle relation and keeps the duration assertions here, so it now tests that the two relations agree rather than that one of them is self-consistent. The `time_ns` cross-check joins them on `(export_id, worker_id)`, both being per worker. No new migration step: the index key `(export_id, worker_id)` is unchanged, and the `mz_indexes` fingerprint move is covered by the existing replacement step at this dev version. Part of CPU-226 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ZVCMBSLdxzGus78ZKWhZz
1 parent 98e7e26 commit dfce91f

5 files changed

Lines changed: 195 additions & 123 deletions

File tree

doc/developer/design/20260817_compute_hydration_timestamps.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,28 @@ event, occurred_at` recovers the dataflow-level facts, and the redundancy is vis
236236
rather than implied. The column also means the relation carries the export-to-dataflow
237237
mapping that `mz_compute_exports_per_worker` holds, for eight bytes a row.
238238

239+
**One reading per term.** `mz_compute_hydration_times_per_worker.hydrated_at` is the
240+
durability reading, taken when the reported output frontier passes the as-of. That
241+
frontier is the meet of the write and compute frontiers, so for a collection that
242+
sinks to persist it moves only once the output is durable, while for an index, which
243+
produces its output by writing its own trace, it coincides with computation. One
244+
column therefore meant two different things depending on the object type, and a
245+
consumer could not tell which without knowing what it was looking at.
246+
247+
The lifecycle log splits that ambiguity into separate terms rather than redefining the
248+
column: `hydrated` is always the dataflow-progress reading and `written` is always the
249+
durability one, neither depending on the object type. `hydrated_at` and `time_ns` stay
250+
where they are, because `mz_compute_hydration_statuses` and the blue-green readiness
251+
query are defined on them, and moving readiness onto the earlier compute reading would
252+
have it cut over before the output is durable.
253+
254+
What does move is `installed_at` and `started_at`. Those were unambiguous, so keeping
255+
them in both relations meant two mechanisms maintaining the same instants and the same
256+
`installed_at <= started_at` invariant. They are now the lifecycle log's `installed`
257+
and `started` events alone. The two relations join on `(export_id, worker_id)`, both
258+
being per worker, so a consumer that wants the queueing interval and the duration
259+
together reads them from one join rather than from one row.
260+
239261
**`installed` is the denominator.** A consumer asking whether every worker has reached
240262
a stage cannot count rows and compare against a worker count it does not have, and it
241263
should not have to join the catalog to find one. The relation is append-only, so a

src/compute-client/src/logging.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -353,18 +353,24 @@ impl LogVariant {
353353
.with_key(vec![0, 1])
354354
.finish(),
355355

356+
// NOTE: `hydrated_at` here is the *durability* reading, taken when the reported
357+
// output frontier passes the as-of. That frontier is the meet of the write and
358+
// compute frontiers, so for a collection that sinks to persist it moves only once
359+
// the output is durable, while for an index, which produces its output by writing
360+
// its own trace, it coincides with computation. One column therefore meant two
361+
// things depending on the object.
362+
//
363+
// `mz_compute_lifecycle_events_per_worker` splits that ambiguity into separate
364+
// terms: its `hydrated` is the dataflow-progress reading and its `written` is the
365+
// durability one, so neither depends on the object type. This relation keeps the
366+
// combined reading, because `mz_compute_hydration_statuses` and the blue-green
367+
// readiness query are defined on it. The instants that were unambiguous,
368+
// `installed_at` and `started_at`, moved to the lifecycle relation rather than
369+
// being maintained in both.
356370
LogVariant::Compute(ComputeLog::HydrationTime) => RelationDesc::builder()
357371
.with_column("export_id", SqlScalarType::String.nullable(false))
358372
.with_column("worker_id", SqlScalarType::UInt64.nullable(false))
359373
.with_column("time_ns", SqlScalarType::UInt64.nullable(true))
360-
.with_column(
361-
"installed_at",
362-
SqlScalarType::TimestampTz { precision: None }.nullable(false),
363-
)
364-
.with_column(
365-
"started_at",
366-
SqlScalarType::TimestampTz { precision: None }.nullable(true),
367-
)
368374
.with_column(
369375
"hydrated_at",
370376
SqlScalarType::TimestampTz { precision: None }.nullable(true),

src/compute/src/logging/compute.rs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -735,10 +735,6 @@ impl DemuxState {
735735
make_string_datum(export_id, &mut self.scratch_string_a),
736736
Datum::UInt64(u64::cast_from(self.worker_id)),
737737
Datum::from(time_ns),
738-
epoch_offset_datum(timestamps.installed_at),
739-
timestamps
740-
.started_at
741-
.map_or(Datum::Null, epoch_offset_datum),
742738
timestamps
743739
.hydrated_at
744740
.map_or(Datum::Null, epoch_offset_datum),
@@ -861,6 +857,10 @@ impl DemuxState {
861857

862858
/// Wallclock instants of an export's hydration lifecycle, as durations since the Unix epoch.
863859
///
860+
/// Only `hydrated_at` reaches `mz_compute_hydration_times_per_worker`. `installed_at` and
861+
/// `started_at` are kept to order and back-fill the lifecycle log's `installed` and `started`
862+
/// events, which are where a consumer reads them.
863+
///
864864
/// These are sampled from compute logging event times, which advance off an `Instant` anchored to
865865
/// the epoch once per worker when logging is initialized. They are therefore monotone within a
866866
/// worker and unaffected by system clock steps, but they carry that worker's anchor, so comparing
@@ -1184,8 +1184,7 @@ impl DemuxHandler<'_, '_, '_> {
11841184
&mut self,
11851185
HydrationStartReference { export_id }: Ref<'_, HydrationStart>,
11861186
) {
1187-
let ts = self.ts();
1188-
// Stamp the event time rather than `ts`, as in `handle_export`.
1187+
// Stamp the event time rather than the rounded update timestamp, as in `handle_export`.
11891188
let started_at = self.time;
11901189
let export_id = Columnar::into_owned(export_id);
11911190

@@ -1200,22 +1199,12 @@ impl DemuxHandler<'_, '_, '_> {
12001199
return;
12011200
}
12021201

1203-
let old_timestamps = export.hydration_timestamps;
12041202
export.hydration_timestamps.started_at = Some(started_at);
1205-
let new_timestamps = export.hydration_timestamps;
1206-
let time_ns = export.hydration_time_ns;
1207-
1208-
let retraction = self
1209-
.state
1210-
.pack_hydration_time_update(export_id, time_ns, &old_timestamps);
1211-
self.output
1212-
.hydration_time
1213-
.give((retraction, ts, Diff::MINUS_ONE));
1214-
let insertion = self
1215-
.state
1216-
.pack_hydration_time_update(export_id, time_ns, &new_timestamps);
1217-
self.output.hydration_time.give((insertion, ts, Diff::ONE));
12181203

1204+
// No `hydration_time` output here. That relation carries only `time_ns` and
1205+
// `hydrated_at`, neither of which this event changes, so the retract-and-reinsert pair
1206+
// would be a no-op. `started_at` is now kept only to guard this handler and to
1207+
// back-fill `started` in `handle_hydration`.
12191208
self.log_lifecycle(export_id, LifecycleStage::Started);
12201209
}
12211210

test/sqllogictest/mz_catalog_server_index_accounting.slt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,6 @@ mz_compute_hydration_times replica_id
358358
mz_compute_hydration_times time_ns
359359
mz_compute_hydration_times_per_worker export_id
360360
mz_compute_hydration_times_per_worker hydrated_at
361-
mz_compute_hydration_times_per_worker installed_at
362-
mz_compute_hydration_times_per_worker started_at
363361
mz_compute_hydration_times_per_worker time_ns
364362
mz_compute_hydration_times_per_worker worker_id
365363
mz_compute_import_frontiers_per_worker export_id

0 commit comments

Comments
 (0)