|
| 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. |
0 commit comments