Skip to content

adapter: Compare 0dt hydration against the leader - #39155

Merged
aljoscha merged 1 commit into
mainfrom
aljoscha/0dt-leader-hydration
Oct 9, 2026
Merged

aljoscha merged 1 commit into
mainfrom
aljoscha/0dt-leader-hydration

Conversation

@aljoscha

@aljoscha aljoscha commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Closes: SQL-696

A rollout can wait on hydration the leader does not have either: a new collection the leader has not hydrated, or an undersized replica that never hydrated on the leader. The incoming generation should not need more hydration than the leader.

Description

The caught-up check reads the leader's mz_compute_hydration_times next to its frontiers and waives local hydration only on explicit evidence:

  • Collections. A compute collection within the lag bound need not hydrate when every leader replica hosting it reports it unhydrated and none reports it hydrated. The stability gate ignores it too.
  • Replicas. A replica the leader reports unhydrated, for a collection the classification did not ignore, only has to be Online. A replica the leader has fully hydrated still has to hydrate here, so we keep the redundancy the leader actually has.

Missing reports never waive anything. Storage collections are unchanged. The hydration source is protected from builtin migration, so the check keeps reading the leader's shard. There is no flag: skip-catchup and manual promotion remain the escape hatch.

Verification

Unit tests pin both waiver rules, including mixed, missing, and absent reports. Two 0dt workflows exercise them with a nonzero stability period. A replica-scoped hydration-concurrency override suspends both leader replicas for the collection rule, and only one leader replica for the replica rule while the other hydrates the same index. Both verify promotion readiness while the corresponding incoming index remains unhydrated.

Release notes

  • Zero-downtime rollouts no longer wait for collections or replicas that the current deployment has not hydrated either.

@linear-code

linear-code Bot commented Sep 25, 2026

Copy link
Copy Markdown

SQL-696

@aljoscha
aljoscha added this pull request to stack #39156 September 25, 2026 12:46
@ggevay

ggevay commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

I've triggered a relevant subset of Nightly: https://buildkite.com/materialize/nightly/builds/18373

@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch from 0b1f93b to a69db78 Compare September 29, 2026 13:08
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch from a69db78 to 1b7c6fc Compare September 29, 2026 17:09
@aljoscha
aljoscha removed this pull request from stack #39156 September 30, 2026 06:55
@aljoscha
aljoscha changed the base branch from aljoscha/0dt-replica-age-budget to aljoscha/0dt-detect-drops September 30, 2026 06:56
@aljoscha
aljoscha added this pull request to stack #39360 September 30, 2026 06:56
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch from 1b7c6fc to 60b9489 Compare September 30, 2026 09:17
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch from 60b9489 to a0bccdb Compare September 30, 2026 09:44
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch from a0bccdb to dac07c9 Compare September 30, 2026 09:44
The incoming generation should not need more hydration than the leader
has. Read the leader's mz_compute_hydration_times and waive local
hydration where the leader explicitly reports none:

* A compute collection within the lag bound need not hydrate when every
  leader replica hosting it reports it unhydrated. The stability gate
  then ignores it too.
* A replica the leader reports unhydrated, for a collection the
  classification did not ignore, only has to be Online. A replica the
  leader has fully hydrated still has to hydrate here.

Missing reports never waive anything. Protect the hydration source from
builtin migration, so the check keeps reading the leader's shard.

Closes: SQL-696
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0d85f-8cd1-7078-afa3-21a9cce44857
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch 2 times, most recently from b562872 to 8044721 Compare October 5, 2026 11:44
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch from 8044721 to 8346e99 Compare October 5, 2026 13:18
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch from 8346e99 to c466c46 Compare October 5, 2026 13:32
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch 2 times, most recently from 3a1e9bc to 88b47a6 Compare October 5, 2026 16:20
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch from 88b47a6 to dfabbdf Compare October 5, 2026 16:23
aljoscha added a commit that referenced this pull request Oct 6, 2026
### Motivation
Closes: [SQL-691](https://linear.app/materializeinc/issue/SQL-691)

A DDL-triggered reboot of the read-only environmentd discarded the 0dt
stability streak, even when the incoming replicas kept running. With DDL
more frequent than the stability period, a deployment could never become
ready.

### Description
The stability gate now asks the replicas. Each tick queries every
replica's `mz_compute_hydration_times_per_worker` log with a one-row
query: the latest `hydrated_at` it reports, or NULL if any non-transient
export on it is not hydrated yet. Compute collections the caught-up
classification ignored (excluded new builtins and their dependents,
collections whose leader frontier is past the cutoff) are left out of
the query, because they may never hydrate in read-only mode. A caught-up
cluster is ready once every replica is Online and has been fully
hydrated for the stability period. A replica-pinned MV or a collection
without a dataflow never appears in a replica's log, so neither needs
special handling.

The queries are plain SQL through the frontend peek path
(`PeekClient::background_peek`), at serializable isolation so they read
the freshest log state even when the timestamp oracle does not advance.
The frontend peek path calls back into the coordinator, so the check now
runs in its own task, like the cluster controller: each tick it asks the
coordinator to classify the clusters, then queries the replicas and
applies the gate itself. The background `PeekClient` constructor is
shared with hydration history collection. Replicas are asked
concurrently, so a tick waits at most 5s for answers. A query that
misses the deadline is dropped. That does not cancel its peek: it keeps
its read holds until the replica answers, and in the rare case of a drop
between registration and issue the registration stays behind. We accept
that, like for user peeks: a stuck replica holds at most one peek per
tick, on a log index that barely changes.

The log lives in the replica process. A restarted environmentd
reconciles with the same replica processes, which keep compatible
dataflows, so it reads the original hydration times. A restarted replica
starts a new log and must hydrate again. Hydration times come from the
replica's clock: a replica clock ahead of environmentd's only lengthens
the wait, one behind shortens the period by the skew. The period counts
from the replica's latest hydration. On main it starts at the first tick
where the cluster is caught up and healthy. Hydration is a proxy for
that, so a long catch-up after hydration eats into the period.

Replicas without introspection logging cannot answer and only need to be
Online. The per-cluster status-flap and restart tracking is removed,
because replica-reported hydration covers restarts directly. No
orchestrator changes and no new durable state.

Behavior changes worth noting:
* Every replica of a caught-up cluster must have hydrated everything it
hosts. Previously each collection hydrated on some replica plus all
replicas Online sufficed. A replica that is Online but cannot hydrate
(for example undersized but not OOMing) now holds the cutover until
skip-catchup or `with_0dt_deployment_max_wait`. #39155 relaxes this for
replicas the leader reports unhydrated too.
* A cluster that gets new dataflows restarts its period when they
hydrate. Other clusters keep theirs.
* Source ingestions are not in the compute log. For them, the period
counts from replica start.
* An Offline-then-Online blip without a restart no longer restarts the
period.
* Replicas with introspection logging disabled are not held to the
period.

### Verification
Unit tests cover the stability fold and that a missing, NULL, or
pre-epoch `hydrated_at` reads as not hydrated. The new
`caught_up_stability_survives_restart` 0dt workflow triggers a DDL
restart of mz_new partway through the stability period, checks that its
replicas survived, and asserts it becomes ready before a restarted
period could end. The new `caught_up_stability_without_dataflows`
workflow checks that a sealed `REFRESH AT` MV and a replica-pinned MV on
a two-replica cluster do not block promotion. The existing
`caught_up_stability` and `caught_up_stability_crash_loop` workflows
cover the healthy and crash-looping cases.

### Release notes
* Zero-downtime deployments no longer restart the replica stability
period when environmentd reboots to pick up DDL.
* Zero-downtime deployments now wait for every replica of a cluster to
hydrate before cutting over. A replica that stays Online but never
hydrates holds the deployment until it is promoted with skip-catchup or
`with_0dt_deployment_max_wait` passes.
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch from dfabbdf to e7f907f Compare October 6, 2026 07:55
@aljoscha aljoscha changed the title adapter: Compare 0dt collection hydration against the leader adapter: Compare 0dt hydration against the leader Oct 6, 2026
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch 2 times, most recently from 9cb5696 to c042e35 Compare October 6, 2026 08:07
Base automatically changed from aljoscha/0dt-detect-drops to main October 6, 2026 10:06
@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch from c042e35 to 6850338 Compare October 6, 2026 10:06
}
}

/// The leader's hydration reports, read from its `mz_compute_hydration_times`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this across all replicas of some cluster, so the unhydrated/hydrated is mixed, or hydration status of one specific replica. Need to be specific in the code docs and naming here, please

@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch 2 times, most recently from 5a49495 to 5abda6d Compare October 7, 2026 10:37
@aljoscha
aljoscha marked this pull request as ready for review October 7, 2026 10:39
@aljoscha
aljoscha requested a review from a team as a code owner October 7, 2026 10:39
@aljoscha
aljoscha requested review from antiguru and ggevay October 7, 2026 10:39

@ggevay ggevay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, minor comments

use ReplicaHealth::{Exempt, HealthySince, NotHydrated, NotOnline};

#[mz_ore::test]
fn collection_hydration_waiver_requires_complete_leader_reports() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing in the 0dt suite creates a collection or replica the leader hasn't hydrated, and CI runs with a 0 s stability period, so a no-op would pass. Could we add a workflow for each rule (e.g. a leader whose hydration of one object is stalled, and the crash-loop workflow with a replica the leader hasn't fully hydrated)?

Comment thread src/adapter/src/coord/caught_up.rs Outdated
/// one collection outside `ignored` as unhydrated.
///
/// This exempts the incoming replica from hydration stability checks: it
/// need not provide redundancy the leader lacks. Missing reports grant no

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this say why one unhydrated collection is enough to exempt the whole replica, i.e. that we don't count a replica as useful until it's fully hydrated?

@aljoscha
aljoscha force-pushed the aljoscha/0dt-leader-hydration branch 3 times, most recently from 942a933 to ea704c4 Compare October 8, 2026 15:25
@def-

def- commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- A leader-waived collection that the incoming generation already hydrated exempts its replicas from the stability gate

src/adapter/src/coord/caught_up.rs:903

A compute collection that every leader replica reports unhydrated only goes into ignored_compute_collections while the incoming generation has not hydrated it either. Once the incoming side hydrates it on any replica, the leader's NULL rows for it reach replica_has_unhydrated_collection (caught_up.rs:642), and every replica hosting it becomes Exempt. On a single-replica cluster that switches the stability gate off at the first tick where the incoming replica is momentarily fully hydrated, which is the crash-loop case the gate exists to catch.

Details

Scenario: a single-replica cluster with indexes A and X. On the leader, A is hydrated and X is still hydrating, either because it was created during the rollout or because the leader replica restarted. Either way it is within the 2 h cutoff. The incoming r1 runs the new version and OOMs a few minutes after it hydrates.

  • On ticks where incoming r1 has not hydrated X, X is waived and ignored. r1 is Asked about A, and the 10-minute period holds the cluster while r1 keeps restarting. This is the intended behavior.
  • On the tick where incoming r1 has hydrated both A and X, collection_hydrated is true, so X is not inserted at :903-908. The leader's (X, r1) NULL row then makes r1 Exempt at :642. observe_stability([Exempt]) yields stable_for_ms = now, so the cluster is ready and promotion lands on a crash-looping replica while the leader was serving A.

So more progress on the incoming side gets the replica less scrutiny. That contradicts the description's rule that the stability gate ignores a leader-waived collection. In multi-replica clusters, every replica hosting such an X is exempted even though no leader replica hydrated X, so the redundancy argument behind the replica rule does not apply. Neither new workflow covers this, because both keep the incoming index unhydrated.

Suggested fix: ignore the collection whenever the leader waiver applies, regardless of local hydration. Then the replica rule only fires on collections that some leader replica did hydrate. This is a superset of today's condition, because readiness == Ready && !collection_hydrated already implies unhydrated_on_leader.

-                if (readiness != CollectionReadiness::Ready || !collection_hydrated)
+                if (readiness != CollectionReadiness::Ready || unhydrated_on_leader)
                     && matches!(collection_type, CollectionType::Compute)

@aljoscha
aljoscha merged commit 0de56a5 into main Oct 9, 2026
80 checks passed
@aljoscha
aljoscha deleted the aljoscha/0dt-leader-hydration branch October 9, 2026 09:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants