[PoC] cluster-controller: gate graceful cut-over on lag, not hydration alone - #38615
[PoC] cluster-controller: gate graceful cut-over on lag, not hydration alone#38615bosconi wants to merge 1 commit into
Conversation
A graceful ALTER CLUSTER reconfiguration cut over to its pending replicas as soon as every collection reported hydrated on them. Hydration is `as_of < output_frontier`, and the as-of is pinned to the collection's read frontier when the replica is added. A collection whose initial snapshot takes hours therefore reports hydrated the instant the snapshot lands, with everything since the as-of still to replay. The outgoing replicas keep the cluster's frontiers current until the cut-over, so the lag is invisible; at the cut-over they are dropped and the frontiers freeze until the new replicas catch up. On 2026-08-28 a production cluster cut over after a 4h08m reconfiguration with roughly 15 minutes of index lag remaining. The cluster-autoscaling design chose hydration-only deliberately, calling the stronger check "a possible later refinement" that "would only avoid a brief post-cut-over latency bump". The bump is not brief: it scales with hydration time. Gate the cut-over on readiness instead. A pending replica is ready when every collection is hydrated on it and its output frontier for the collection is within `cluster_reconfiguration_allowed_lag` (60s) of the furthest output frontier any replica of the cluster reports for it, which while the outgoing replicas are present is theirs. `enable_cluster_reconfiguration_lag_gate` is the break-glass back to hydration alone. Output frontiers, not write frontiers: a materialized view's replica-reported write frontier is the persist shard upper, shared by every replica writing it, and for a REFRESH materialized view it jumps to the next refresh time. The output frontier is the meet of the write frontier and the dataflow's compute probe, so it is the replica's own progress. For indexes the two coincide. The hydration burst keeps its hydration-only signal: its durable `steady_hydrated_at` field means exactly "hydration was observed", so the reconfiguration strategy gets a separate `ready_replicas` signal rather than a redefined `hydrated_replicas`. The storage-side check is unchanged and hydration-only. Timeout semantics are unchanged: ON TIMEOUT = COMMIT still cuts over at the deadline and is recorded as forced. The lag predicate `reference <= frontier + allowed_lag` is extracted to `mz_repr::frontier_within_lag`, replacing the two open-coded copies in the 0dt caught-up check, and saturates rather than panicking on overflow. The per-collection decision is a pure function, `classify_collection_readiness`, tested over plain frontiers. The readiness probe now logs "unhydrated" and "lagging" separately; in the incident the two were indistinguishable. Linear: https://linear.app/materializeinc/issue/SQL-672 Co-Authored-By: Claude Code <noreply@anthropic.com>
QA LLM Review1. MEDIUM -- Downsizing a saturated cluster can no longer complete; it rolls back at the deadline
The cut-over now requires the target replicas to hold the live frontier the outgoing replicas are setting, so DetailsThe reference is the Memory-bound downsizes were already blocked (the replica never hydrates at all). The new class is CPU-bound: the smaller replica fits the arrangement and completes its snapshot but cannot keep up afterwards. Accepting lag in exchange for a smaller size is a legitimate trade for a batch or dev cluster and is expressible today; after this change it needs The cost of discovering this is the full deadline. The per-collection lag is already computed on every tick, so a target whose lag is not shrinking across ticks is detectably unachievable and could be shed early (audit event plus a terminal record, as 2. LOW -- A concurrent hydration-burst replica raises a cut-over bar the target is not expected to meet
The lag reference is the join over every replica hosting the collection, which includes a hydration-burst replica running at DetailsBurst and graceful reconfiguration coexist by design, and Worth noting that the two docs disagree on the intended reference. |
Motivation
Linear: SQL-672
A graceful
ALTER CLUSTERreconfiguration cuts over to its pending replicas as soon as every collection reports hydrated on them. Hydration isas_of < output_frontier, with the as-of pinned to the collection's read frontier at the moment the replica is added. A collection whose initial snapshot takes hours therefore reports hydrated the instant the snapshot lands, with everything since the as-of still to replay. The outgoing replicas hold the cluster's frontiers current until the cut-over, so the lag is invisible; at the cut-over they are dropped and the frontiers freeze until the new replicas drain the backlog. On 2026-08-28 a production cluster cut over after a 4h08m reconfiguration with roughly 15 minutes of index lag remaining.The cluster-autoscaling design deferred a caught-up check as a later refinement; this PR adds it. The 0dt-upgrade readiness check in
caught_up.rsalready applies an equivalent lag term.Description
User-visible effect. A graceful
ALTER CLUSTER(resize, availability zones, logging,EXPERIMENTAL ARRANGEMENT COMPRESSION) now retires the old replicas only once the new replicas are both hydrated and caught up to them, within a configurable allowance. Previously the old replicas could be retired while the new ones were still minutes behind, producing a period of stale results after the cut-over. Two new system parameters:cluster_reconfiguration_allowed_lag(default60s) andenable_cluster_reconfiguration_lag_gate(default on; disabling it restores the hydration-only gate). A reconfiguration whose new replicas cannot catch up within the timeout now rolls back under the defaultON TIMEOUT = ROLLBACK;ON TIMEOUT = COMMITstill cuts over at the deadline regardless.Approach.
REFRESHMV it jumps to the next refresh time. A gate on write frontiers would be a no-op for the former and wedge on the latter. The output frontier ismeet(write_frontier, compute_probe), the replica's own progress, and is whathydrated()already measures. For indexes the two coincide.LiveSignals::hydrated_replicasalso feeds the hydration burst, whose durablesteady_hydrated_atfield means exactly "hydration was observed". The reconfiguration strategy gets its ownready_replicas; the burst is untouched and its probe path is behaviorally identical to before.Instance::collections_hydrated_on_replicasbecomescollections_ready_on_replicas(.., allowed_lag: Option<Timestamp>). The per-collection decision is a pure function,classify_collection_readiness, that also derives the reference, so it is testable over plain frontiers. The probe logsunhydratedandlaggingseparately.reference <= frontier + allowed_lagis extracted tomz_repr::frontier_within_lagand used bycaught_up.rsas well; it saturates rather than panicking on overflow.The design doc paragraph and the
ALTER CLUSTERuser docs are updated to match.Verification
New unit tests:
mz_repr::timestamp::frontier_within_lag_tests: boundary, monotone in the allowance, empty-antichain semantics, saturation instead of panic.compute_client::controller::instance::testsonclassify_collection_readiness: hydrated-but-lagging is not ready; the reference is the furthest hosting replica; one ready target suffices;Noneis a true no-op; a complete collection has nothing to lag behind; a sole replica is its own reference; no targets reads unhydrated.mz_cluster_controller::tests:graceful_holds_when_target_is_hydrated_but_lagging,graceful_commit_on_timeout_cuts_over_lagging_target; the full-flow seam test asserts the graceful path pulls only the readiness signal.The new parameters are registered with the test harnesses; LaunchDarkly flags still need to be created.
The 60s default allowance matches
with_0dt_caught_up_check_allowed_lagand has not been tuned for this use.🤖 Generated with Claude Code