Skip to content

Commit 4ac3ffc

Browse files
committed
catalog: add mz_replica_hydration_history
Add the internal durable table for successful replica hydration episodes, including object counts and nullable memory and disk peaks. Protect the table from bootstrap resets and forced replacement migrations. Document episode boundaries, process-lifetime peak semantics, retention, and current failure-reporting limits.
1 parent 780c9c1 commit 4ac3ffc

16 files changed

Lines changed: 376 additions & 26 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Durable Replica Hydration History
2+
3+
## Context
4+
5+
Materialize durably records successful hydration of individual index and
6+
materialized-view dataflows. Users also need the replica-wide episode that says
7+
how many objects hydrated together, how long the replica was hydrating, and how
8+
close any process came to its resource limit.
9+
10+
Compute now exposes two replica-local inputs:
11+
12+
- `mz_compute_hydration_times_per_worker` reports installation and hydration
13+
timestamps for every compute export and worker.
14+
- `mz_cluster_replica_resource_usage` reports resource observations for every
15+
replica process, including kernel-maintained memory and swap high-water marks
16+
and a sampled scratch-filesystem high-water mark.
17+
18+
This design extends the existing hydration-history sweep to persist successful
19+
replica episodes from those inputs.
20+
21+
## Goals
22+
23+
- Record one row whenever a replica transitions from fully hydrated to
24+
hydrating and back.
25+
- Include every index or materialized-view dataflow installed while that
26+
transition is in progress.
27+
- Record the process peak relevant to a per-process replica resource limit.
28+
- Survive environmentd and replica restarts.
29+
- Stay idempotent across concurrent environmentd processes.
30+
- Share the object-history collection cadence and retention period.
31+
32+
## Non-goals
33+
34+
- Failed, canceled, or OOM-killed episodes. A replica cannot report its final
35+
retracting introspection state after its process exits. The controller also
36+
does not retain the process's final resource observations.
37+
- Storage objects. Storage does not publish equivalent lifecycle timestamps.
38+
- A resettable resource peak for an incremental episode. The available kernel
39+
peaks reset when the process restarts, not when a new object is installed.
40+
- A public stable catalog contract. The table starts in `mz_internal` while its
41+
semantics settle.
42+
43+
## History table
44+
45+
```text
46+
mz_internal.mz_replica_hydration_history
47+
replica_id text not null
48+
cluster_id text not null
49+
started_at timestamptz not null
50+
finished_at timestamptz null
51+
object_count uint8 not null
52+
peak_memory_bytes uint8 null
53+
peak_disk_bytes uint8 null
54+
status text not null
55+
```
56+
57+
An episode is identified operationally by `(replica_id, started_at)`. The table
58+
does not declare this as a relation key. The anti-join enforces uniqueness, and
59+
an accidental duplicate must remain visible rather than be optimized away.
60+
61+
Only successful episodes are recorded, so `finished_at` is populated and
62+
`status` is `hydrated`. The nullable finish and explicit status reserve a
63+
compatible shape for terminal failures once they become observable. Resource
64+
columns are nullable because cgroup peak files depend on the host kernel and a
65+
replica without disk reports no filesystem observation.
66+
67+
There is no index. Collection runs on the selected user replica, so it cannot
68+
use an index arranged on the catalog server. Such an index would pin the whole
69+
retained table without removing the recurring import and arrangement cost.
70+
71+
## Episode boundaries
72+
73+
Each live object contributes an interval from its earliest worker installation
74+
to its latest worker hydration. The collector waits until every visible worker
75+
of every relevant object has hydrated. This preserves the object collector's
76+
rule that a materialized view only finishes after its persist sink's active
77+
worker reports completion.
78+
79+
A replica episode is a connected component in the union of those object
80+
intervals. Two intervals belong to one episode if they overlap directly or
81+
through a chain of overlapping intervals. A gap means the replica was fully
82+
hydrated before the next object was installed, so the next interval starts a
83+
new episode.
84+
85+
The query sorts intervals by installation time and computes the running maximum
86+
finish among preceding intervals. An installation after that maximum starts a
87+
new component. The latest such start identifies the episode whose resources are
88+
currently observable. Its finish is the maximum object finish and its object
89+
count is the number of intervals in the component.
90+
91+
This definition handles both important cases without coordinator-local state:
92+
93+
- A replica restart replaces every live interval with fresh timestamps. Their
94+
connected component becomes a new episode.
95+
- `CREATE INDEX` on a fully hydrated replica installs an interval after the
96+
preceding component's finish. It becomes a new episode. Objects installed
97+
while it hydrates join that episode if their intervals overlap.
98+
99+
Sampling still limits completeness. An object that disappears before the sweep
100+
can be absent from the episode, and a process clock ahead of the read timestamp
101+
can hide a worker. These are the same accepted limits as durable object history.
102+
103+
## Resource interpretation
104+
105+
`peak_memory_bytes` is the maximum `cgroup memory_peak` across replica
106+
processes. This is the accounting that the cgroup memory limit and OOM killer
107+
act on. `peak_disk_bytes` is the maximum sampled `statvfs fs_used_peak` when a
108+
scratch filesystem is present. Otherwise it is the maximum kernel-maintained
109+
`cgroup swap_peak`.
110+
111+
Replica memory and disk limits apply independently to each process. The maximum
112+
process peak therefore answers whether any process needed a larger size. Adding
113+
process maxima would produce a number that may never have existed because the
114+
peaks need not be simultaneous.
115+
116+
The operating system's peaks cover the process lifetime through the collector's
117+
observation. They are not bounded by `finished_at`, so work after hydration and
118+
before collection can raise them even for the first episode after a process
119+
starts. In a later episode they can additionally include an earlier high-water
120+
mark. The table documents this instead of presenting a process-lifetime value as
121+
episode-scoped. A true episode peak requires a reset or a separately retained
122+
interval maximum at the replica.
123+
124+
The collector requires at least one resource observation from every configured
125+
process before writing. Individual peak metrics can still be absent, which is
126+
represented by `NULL` rather than a zero sentinel.
127+
128+
## Collection and concurrency
129+
130+
Collection is disabled by default. Setting
131+
`hydration_history_collection_interval` to a nonzero duration enables both the
132+
object and replica history sweeps.
133+
134+
The existing single-flight sweep visits one user replica per interval. It first
135+
collects object rows, then collects the latest replica episode, then runs
136+
retention for both tables on the catalog server. A failure in one step does not
137+
prevent the later steps from running.
138+
139+
The replica query anti-joins against the table it writes. Concurrent
140+
environmentd processes can compute the same candidate, but exact-timestamp OCC
141+
allows one write to commit. A losing subscribe observes that row and retracts
142+
its own candidate before retrying. The identity uses replica-stamped
143+
`started_at`, so an environmentd restart does not create a duplicate.
144+
145+
The same background isolation rules apply as for object history. Collection is
146+
replica-targeted, writes only a system table, depends only on system objects,
147+
does not take the user-DML OCC permit, and has a bounded attempt timeout.
148+
149+
## Retention and durability
150+
151+
Replica history uses `hydration_history_retention_period`, which defaults to 30
152+
days. Each sweep retracts one bounded batch of rows whose `finished_at` is older
153+
than the cutoff. Collection applies the same cutoff, so a live introspection row
154+
cannot resurrect an episode that retention removed.
155+
156+
The table is exempt from bootstrap system-table reset and forced shard
157+
replacement. Schema evolution keeps the shard and its rows. A migration guard
158+
rejects a replacement step unless the exemption and guard are deliberately
159+
removed together.
160+
161+
Collection remains best effort. A future schema replacement can intentionally
162+
clear it, and current-state introspection cannot recreate an episode after all
163+
of its evidence has disappeared.
164+
165+
## Future work
166+
167+
- Publish episode-scoped resettable peaks from the replica.
168+
- Retain lifecycle and resource events until environmentd acknowledges them.
169+
- Finalize open episodes from replica lifecycle events as canceled, failed, or
170+
OOM-killed.
171+
- Define equivalent lifecycle signals for storage objects.
172+
- Promote the catalog surface after its semantics and rollout have settled.

doc/user/content/reference/system-catalog/mz_internal.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,45 @@ logical timestamp, so the recorded finish can precede the latest process's finis
743743
| `hydrated_at` | [`timestamp with time zone`] | When hydration finished. |
744744
| `status` | [`text`] | The terminal status. Currently always `hydrated`. |
745745

746+
## `mz_replica_hydration_history`
747+
748+
The `mz_replica_hydration_history` table records successful replica hydration
749+
episodes for indexes and materialized views. An episode begins when the first
750+
object dataflow is installed on a fully hydrated replica. Any object installed
751+
before every preceding object has hydrated belongs to the same episode. The
752+
episode finishes when all of those objects have hydrated.
753+
754+
Collection is disabled by default. Setting
755+
`hydration_history_collection_interval` to a nonzero duration enables it. Rows
756+
are retained for 30 days by default while collection is enabled. Disabling
757+
collection also suspends retention. Recording is best effort, so an episode can
758+
be missed if its objects or replica disappear before the collector observes
759+
completion. Failed, canceled, and OOM-killed episodes are not recorded.
760+
`cluster_id` and `replica_id` may name objects that no longer exist.
761+
762+
Resource peaks have process-lifetime scope because the operating system resets
763+
them when a replica process restarts, not when a hydration episode starts. They
764+
are read after the collector observes a completed episode, so they include all
765+
process work through collection rather than ending at `finished_at`. Even a
766+
first episode's value can include work after hydration. A later episode's value
767+
can additionally include an earlier peak. Kernel-maintained memory and swap
768+
peaks are exact for the process lifetime through collection, while the
769+
filesystem peak remains a sampled lower bound. Replica resource limits apply
770+
independently to each process, so the table records the largest process peak
771+
rather than a sum of peaks that may not have occurred at the same time.
772+
773+
<!-- RELATION_SPEC mz_internal.mz_replica_hydration_history -->
774+
| Field | Type | Meaning |
775+
| ------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
776+
| `replica_id` | [`text`] | The ID of the cluster replica. May name a replica that no longer exists. |
777+
| `cluster_id` | [`text`] | The ID of the replica's cluster. |
778+
| `started_at` | [`timestamp with time zone`] | When the first object in this hydration episode was installed on the replica. |
779+
| `finished_at` | [`timestamp with time zone`] | When every object installed during the episode had hydrated. |
780+
| `object_count` | [`uint8`] | The number of object dataflows that hydrated during the episode. |
781+
| `peak_memory_bytes` | [`uint8`] | The largest process-lifetime cgroup memory high-water mark reported by any process when the collector recorded the episode. `NULL` if the platform reports no cgroup memory peak. |
782+
| `peak_disk_bytes` | [`uint8`] | The largest process-lifetime scratch-filesystem or swap high-water mark reported by any process when the collector recorded the episode. Filesystem peaks are sampled lower bounds. `NULL` if neither measurement is available. |
783+
| `status` | [`text`] | The terminal status. Currently always `hydrated`. |
784+
746785
## `mz_object_transitive_dependencies`
747786

748787
The `mz_object_transitive_dependencies` view describes the transitive dependency structure between

src/adapter/src/catalog/open/builtin_schema_migration.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use mz_catalog::builtin::{
3939
BUILTIN_LOOKUP, Builtin, Fingerprint, MZ_CATALOG_RAW, MZ_CATALOG_RAW_DESCRIPTION,
4040
MZ_CLUSTER_REPLICA_FRONTIERS_DESCRIPTION, MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION,
4141
MZ_OBJECT_HYDRATION_HISTORY, MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION,
42+
MZ_REPLICA_HYDRATION_HISTORY, MZ_REPLICA_HYDRATION_HISTORY_DESCRIPTION,
4243
MZ_STORAGE_USAGE_BY_SHARD, MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION,
4344
RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
4445
};
@@ -689,18 +690,20 @@ fn participates_in_forced_migration(
689690
match builtin {
690691
// A forced replacement allocates a fresh shard, which discards the
691692
// table's contents. Exclude the tables whose contents are the point:
692-
// storage usage is retained for billing, and hydration history cannot
693+
// storage usage is retained for billing, and hydration histories cannot
693694
// be rebuilt from any other source.
694695
//
695-
// Hydration history takes part in a forced `Evolution`, which keeps the
696-
// rows. It has to: dev upgrades force one for every object, and a table
697-
// left out of the plan never gets its new schema registered, so
698-
// `update_fingerprints` panics at open as soon as the desc changes. See
699-
// the tripwire in `validate_migration_steps` for how to give up the
696+
// Hydration history tables take part in a forced `Evolution`, which
697+
// keeps the rows. They have to: dev upgrades force one for every object,
698+
// and a table left out of the plan never gets its new schema registered.
699+
// `update_fingerprints` then panics at open as soon as the desc changes.
700+
// See the tripwire in `validate_migration_steps` for how to give up the
700701
// replacement exemption deliberately.
701702
Table(table) => {
702703
**table != *MZ_STORAGE_USAGE_BY_SHARD
703-
&& (mechanism != Mechanism::Replacement || **table != *MZ_OBJECT_HYDRATION_HISTORY)
704+
&& (mechanism != Mechanism::Replacement
705+
|| (**table != *MZ_OBJECT_HYDRATION_HISTORY
706+
&& **table != *MZ_REPLICA_HYDRATION_HISTORY))
704707
}
705708
MaterializedView(..) => true,
706709
Source(source) => **source != *MZ_CATALOG_RAW,
@@ -819,6 +822,10 @@ impl Migration {
819822
&*MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION, object,
820823
"replacing mz_object_hydration_history clears it, see the comment above"
821824
);
825+
assert_ne!(
826+
&*MZ_REPLICA_HYDRATION_HISTORY_DESCRIPTION, object,
827+
"replacing mz_replica_hydration_history clears it, see the comment above"
828+
);
822829
}
823830

824831
// `mz_catalog_raw` cannot be migrated because it contains the durable catalog and it

src/adapter/src/catalog/open/builtin_schema_migration_tests.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,21 @@ use super::*;
2929

3030
#[mz_ore::test]
3131
fn hydration_history_forced_migration_policy() {
32-
let hydration_history = Builtin::Table(&*MZ_OBJECT_HYDRATION_HISTORY);
33-
34-
assert!(participates_in_forced_migration(
35-
&hydration_history,
36-
Mechanism::Evolution
37-
));
38-
assert!(!participates_in_forced_migration(
39-
&hydration_history,
40-
Mechanism::Replacement
41-
));
32+
for table in [
33+
&*MZ_OBJECT_HYDRATION_HISTORY,
34+
&*MZ_REPLICA_HYDRATION_HISTORY,
35+
] {
36+
let hydration_history = Builtin::Table(table);
37+
38+
assert!(participates_in_forced_migration(
39+
&hydration_history,
40+
Mechanism::Evolution
41+
));
42+
assert!(!participates_in_forced_migration(
43+
&hydration_history,
44+
Mechanism::Replacement
45+
));
46+
}
4247
}
4348

4449
#[test] // allow(test-attribute)

src/adapter/src/coord.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ use mz_auth::password::Password;
100100
use mz_build_info::BuildInfo;
101101
use mz_catalog::builtin::{
102102
BUILTINS, BUILTINS_STATIC, MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY, MZ_OBJECT_HYDRATION_HISTORY,
103-
MZ_STORAGE_USAGE_BY_SHARD,
103+
MZ_REPLICA_HYDRATION_HISTORY, MZ_STORAGE_USAGE_BY_SHARD,
104104
};
105105
use mz_catalog::config::{AwsPrincipalContext, BuiltinItemMigrationConfig, ClusterReplicaSizeMap};
106106
use mz_catalog::durable::OpenableDurableCatalogState;
@@ -3186,6 +3186,8 @@ impl Coordinator {
31863186
.resolve_builtin_table(&MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY),
31873187
self.catalog()
31883188
.resolve_builtin_table(&MZ_OBJECT_HYDRATION_HISTORY),
3189+
self.catalog()
3190+
.resolve_builtin_table(&MZ_REPLICA_HYDRATION_HISTORY),
31893191
]);
31903192

31913193
let mut retraction_tasks = Vec::new();

src/catalog/src/builtin.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,14 @@ pub static MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION: LazyLock<SystemObjectDescrip
845845
object_name: MZ_OBJECT_HYDRATION_HISTORY.name.to_string(),
846846
});
847847

848+
/// Identifies [`MZ_REPLICA_HYDRATION_HISTORY`] for the schema-migration guard.
849+
pub static MZ_REPLICA_HYDRATION_HISTORY_DESCRIPTION: LazyLock<SystemObjectDescription> =
850+
LazyLock::new(|| SystemObjectDescription {
851+
schema_name: MZ_REPLICA_HYDRATION_HISTORY.schema.to_string(),
852+
object_type: CatalogItemType::Table,
853+
object_name: MZ_REPLICA_HYDRATION_HISTORY.name.to_string(),
854+
});
855+
848856
/// Identifies [`MZ_CLUSTER_REPLICA_FRONTIERS`] for the schema-migration guard in
849857
/// `builtin_schema_migration.rs`, which forbids migrating this source because the 0dt
850858
/// caught-up gate reads the leader's shard for it to learn the live frontiers.
@@ -1487,6 +1495,7 @@ pub static BUILTINS_STATIC: LazyLock<Vec<Builtin<NameReference>>> = LazyLock::ne
14871495
Builtin::View(&MZ_MCP_DATA_PRODUCTS),
14881496
Builtin::View(&MZ_MCP_DATA_PRODUCT_DETAILS),
14891497
Builtin::Table(&MZ_OBJECT_HYDRATION_HISTORY),
1498+
Builtin::Table(&MZ_REPLICA_HYDRATION_HISTORY),
14901499
];
14911500

14921501
builtin_items.extend(notice::builtins());

0 commit comments

Comments
 (0)