Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
79c7cf0
adapter: simplify hydration replica readiness
aljoscha Aug 26, 2026
0efe63a
adapter: restrict read-then-write to transactional tables
aljoscha Aug 26, 2026
044c053
catalog: identify derived-relation dependencies
aljoscha Aug 26, 2026
d7151b9
adapter: declare hydration history dyncfgs at environment scope
aljoscha Aug 19, 2026
2ae3c7f
adapter: collect durable object hydration history
aljoscha Aug 19, 2026
22a9dcc
ci: gate hydration history defaults to v26.40
aljoscha Aug 24, 2026
e97ebaa
adapter: attribute a trailing replica frontier to the timeout
aljoscha Aug 20, 2026
7ccf114
adapter: offset the sweep schedule per environment
aljoscha Aug 23, 2026
4fdae34
adapter: seed hydration scheduling from the full environment id
aljoscha Aug 24, 2026
b1fe070
adapter: document hydration sampling under clock skew
aljoscha Aug 24, 2026
b5e8003
adapter: scope hydration timeout diagnostics to collection
aljoscha Aug 24, 2026
fd90638
test: verify the multi-worker hydration finish
aljoscha Aug 24, 2026
e52cdd5
adapter: drain expired hydration history in bounded batches
aljoscha Aug 24, 2026
237ce44
test: require a fresh hydration episode after restart
aljoscha Aug 24, 2026
63bc95a
test: state hydration retention coverage accurately
aljoscha Aug 24, 2026
7b0af29
test: validate the hydration timing query result
aljoscha Aug 24, 2026
060f651
adapter: name the write-attempt discriminator
aljoscha Aug 25, 2026
fd2048a
adapter: clarify hydration history completeness
aljoscha Aug 25, 2026
1a1bb16
adapter: expose hydration history maintenance metrics
aljoscha Aug 25, 2026
9eb528f
adapter: repair read-then-write rustdoc placement
aljoscha Aug 26, 2026
9dbc8dc
adapter: clarify background dependency validation
aljoscha Aug 26, 2026
23cf89c
adapter: document hydration episode key stability
aljoscha Aug 26, 2026
3e794ad
adapter: clarify the background OCC rollout flag
aljoscha Aug 26, 2026
065c91a
adapter: document hydration history freshness scaling
aljoscha Aug 26, 2026
3c2bdd0
adapter: state the hydration collection result bound
aljoscha Aug 26, 2026
1c3e235
adapter: bound hydration retention work per sweep
aljoscha Aug 26, 2026
bfe740b
adapter: make coordinator response drops fallible
aljoscha Aug 26, 2026
baaf216
adapter: propagate compute lookup shutdown
aljoscha Aug 26, 2026
c141b38
adapter: enforce background OCC system boundaries
aljoscha Aug 26, 2026
d4024bb
adapter: skip unready hydration replicas
aljoscha Aug 26, 2026
d3c4350
adapter: make hydration schedule offsets portable
aljoscha Aug 26, 2026
494a76f
adapter: remove an unused subscribe accessor
aljoscha Aug 26, 2026
956fe79
test: discover the off-zero hydration sink worker
aljoscha Aug 26, 2026
949496c
adapter: simplify hydration review contracts
aljoscha Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions doc/developer/design/20260817_durable_object_hydration_history.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ It installs an internal subscribe on that replica which aggregates every worker'
completed rows, anti-joins them against the history table, and writes the missing
ones through the timestamped OCC read-then-write path.

Collection has no explicit batch bound. It returns at most one row per
not-yet-recorded dataflow, and the OCC path rejects a result that exceeds
`max_result_size` or `max_query_result_size`. At their 1 GiB defaults that ceiling
only matters at millions of dataflows per replica.

Two dyncfgs control it. `hydration_history_collection_interval` sets the sweep
cadence and disables collection at zero, which is the production default.
`hydration_history_retention_period` bounds how long rows live, defaulting to 30
Expand Down Expand Up @@ -148,6 +153,13 @@ sampling race rather than depending on replica configuration for an expected wor
count. In that case the durable finish can precede the latest worker's finish. This
is separate from restart behavior, which discards the replica collection as a unit.

The missing worker cannot later change the episode key. Its logging clock stamps
both the Differential update and `installed_at`, so appearing after the read means
its installation stamp is later than the visible minimum. A later sweep therefore
keeps the same `min(installed_at)` and the anti-join matches the row already written.
The worker can raise `max(hydrated_at)`, but the durable row is not repaired after
its episode key has been recorded.

Compute is adding an append-only lifecycle log for the same stages, currently
proposed in #38403: one row per export, worker and event, with a reason and the
dataflow's as-of. It leaves `mz_compute_hydration_times_per_worker` alone, so
Expand Down Expand Up @@ -190,16 +202,21 @@ writes their retractions at the observed frontier. Collection applies the same
cutoff, so a still-live log row cannot resurrect an episode retention just
retracted.

Retention deletes successive bounded batches until one is not full. The fixed
cutoff makes the eligible set finite, and collection refuses to insert rows behind
that cutoff, so a successful sweep drains the backlog even when more than one batch
expires at once. The bound is not a nicety. The OCC path refuses a selection larger
than `max_result_size` before submitting any write, so one unbounded delete over a
large backlog would fail identically forever and never shrink the table. The bound
has to sit inside a derived table, because a top-level `LIMIT` lands in the plan's
Retention deletes one bounded batch per sweep. The fixed cutoff makes the eligible
set finite, and collection refuses to insert rows behind that cutoff, so later
sweeps continue draining the same backlog without starving collection. The batch
bound is not a nicety. The OCC path refuses a selection larger than
`max_result_size` before submitting any write, so one unbounded delete over a large
backlog would fail identically forever and never shrink the table. The bound has to
sit inside a derived table, because a top-level `LIMIT` lands in the plan's
`RowSetFinishing`, which the OCC path deliberately discards, and the delete would be
silently unbounded again.

`mz_hydration_history_retention_batch_full_total` increments when the batch deletes
all 1,000 rows. Repeated increments mean retention may not be keeping up. An operator
can lower `hydration_history_collection_interval` to schedule sweeps more often,
then compare appended and deleted row rates to confirm the backlog is shrinking.

Retention runs on the catalog server, so it keeps working when there are no user
replicas at all, and it runs even when that sweep's collection failed. A
crash-looping replica must not be able to stop the table from shrinking. The
Expand Down Expand Up @@ -238,6 +255,11 @@ the interval is one fleet-wide setting and an unshifted grid would have every
environment sweep at the same instant. Using the full id also separates regions and
ordinals belonging to one organization.

One replica per interval means an environment with `N` eligible replicas revisits
each one approximately every `N * interval`. Freshness therefore degrades linearly
with replica count. Lowering the interval improves freshness at the cost of more
replica dataflow installs.

**Background mutations take no OCC write permit.** The permits are one semaphore
shared by every read-then-write in the process, not one per table. A session's wait
is bounded by its statement timeout, a sweep's is not, and a sweep's subscribe has
Expand Down Expand Up @@ -308,6 +330,12 @@ the sqllogictest runner defaults, against the usual preference for enabling new
paths in tests: the collector installs subscribes and writes a builtin table, while
those runs assert on catalog contents and plans.

Background collection always uses frontend OCC, independently of the session
`frontend_read_then_write` rollout flag. This is safe while the lock path remains
available because the target is a system table, which user DML can neither read nor
write. The background OCC entry point enforces that target contract rather than
relying on each maintenance caller to remember the rollout constraint.

## Future Work

- Record installation and start before completion, then finalize canceled and
Expand Down
9 changes: 9 additions & 0 deletions doc/developer/guide-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,15 @@ keep the change invisible to session-visible catalog reads (name resolution,
planning). Otherwise sessions serve stale catalogs where today they would see
the change.

### Background OCC must stay disjoint from the lock path

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.

pleaes remove this one, we'll remove the lock path so this will rot quickly

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.

Posted by AJ, Aljoscha's coding agent.

Removed the section, along with the lock-path wording in the design and implementation comments. The runtime target check remains as the boundary of the coordinator-owned system-maintenance API, without documenting the path being retired.


The frontend OCC and coordinator lock paths do not synchronize with each other.
Background maintenance that bypasses the frontend rollout flag must therefore
target a system table and read only system relations. User DML can neither read
nor write system relations, so the two paths cannot interleave on a target or
dependency. A background caller that needs a user relation requires a separate
contract that establishes every writer uses OCC across the fleet.

### Group commits and generation handover

At runtime, one group committer per `environmentd` serializes txns-shard operations:
Expand Down
33 changes: 32 additions & 1 deletion doc/user/data/metrics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ metrics:
source: src/adapter/src/metrics.rs
visibility: internal
- name: mz_active_internal_subscribes
help: The number of active internal subscribes, which serve frontend-sequenced read-then-write.
help: The number of active internal subscribes used by read-then-write operations and background maintenance.
labels:
- session_type
source: src/adapter/src/metrics.rs
Expand Down Expand Up @@ -955,6 +955,37 @@ metrics:
help: The time it takes to advance the catalog shard upper for a txns-shard write (group commits and table register/forget).
source: src/adapter/src/metrics.rs
visibility: internal
- name: mz_hydration_history_mutations_total
help: Total hydration-history collection and retention mutations since process start.
labels:
- operation
- outcome
source: src/adapter/src/metrics.rs
visibility: internal
- name: mz_hydration_history_retention_batch_full_total
help: Total hydration-history sweeps whose retention batch was full. Repeated increments mean retention may not be keeping up with its schedule.
source: src/adapter/src/metrics.rs
visibility: internal
- name: mz_hydration_history_rows_affected_total
help: Total rows changed by hydration-history maintenance since process start.
labels:
- action
source: src/adapter/src/metrics.rs
visibility: internal
- name: mz_hydration_history_sweep_duration_seconds_bucket
help: Wall time of a complete hydration-history collection and retention sweep.
labels:
- le
source: src/adapter/src/metrics.rs
visibility: internal
- name: mz_hydration_history_sweep_duration_seconds_count
help: Wall time of a complete hydration-history collection and retention sweep.
source: src/adapter/src/metrics.rs
visibility: internal
- name: mz_hydration_history_sweep_duration_seconds_sum
help: Wall time of a complete hydration-history collection and retention sweep.
source: src/adapter/src/metrics.rs
visibility: internal
- name: mz_index_peek_cursor_setup_seconds_bucket
help: Time setting up cursor and literal constraints.
labels:
Expand Down
14 changes: 14 additions & 0 deletions misc/python/materialize/mzcompose/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ def get_minimal_system_parameters(
if version < MzVersion.parse_mz("v26.25.0-dev"):
config["enable_multi_replica_sources"] = "true"

if version >= MzVersion.parse_mz("v26.40.0-dev"):
config["hydration_history_collection_interval"] = "60s"

if sanitizer_enabled():
config["with_0dt_deployment_max_wait"] = "18000s"

Expand Down Expand Up @@ -495,6 +498,17 @@ def get_variable_system_parameters(
VariableSystemParameter(
"arrangement_size_history_retention_period", "7d", ["1min", "1h", "7d"]
),
*(
[
VariableSystemParameter(
"hydration_history_retention_period",
"30d",
["1min", "1h", "30d"],
)
]
if version >= MzVersion.parse_mz("v26.40.0-dev")
else []
),
VariableSystemParameter(
"persist_validate_part_bounds_on_read", "false", ["true", "false"]
),
Expand Down
10 changes: 10 additions & 0 deletions misc/python/materialize/parallel_workload/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -3009,6 +3009,16 @@ def __init__(
"'1h'",
"'7d'",
]
self.flags_with_values["hydration_history_collection_interval"] = [
"'0s'",
"'1s'",
"'1min'",
]
self.flags_with_values["hydration_history_retention_period"] = [
"'1min'",
"'1h'",
"'30d'",
]
# Keep these generous: a tight timeout would abort the oracle's own
# queries (they are retried, but it adds noise). "0s" leaves it unset.
self.flags_with_values["pg_timestamp_oracle_statement_timeout"] = [
Expand Down
18 changes: 18 additions & 0 deletions src/adapter-types/src/dyncfgs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,22 @@ pub const ARRANGEMENT_SIZE_HISTORY_RETENTION_PERIOD: Config<Duration> = Config::
ParameterScope::Environment,
);

/// How often to sweep replicas for completed object hydration episodes.
pub const HYDRATION_HISTORY_COLLECTION_INTERVAL: Config<Duration> = Config::new(
"hydration_history_collection_interval",
Duration::ZERO,
"How often to record completed object hydration episodes. A zero duration disables collection.",
ParameterScope::Environment,
);

/// How long to retain completed object hydration episodes.
pub const HYDRATION_HISTORY_RETENTION_PERIOD: Config<Duration> = Config::new(
"hydration_history_retention_period",
Duration::from_hours(30 * 24),
"How long to retain rows in mz_internal.mz_object_hydration_history.",
ParameterScope::Environment,
);

/// How frequently the catalog `*_info` metrics (`mz_object_info`,
/// `mz_cluster_info`, …) are reconciled with the catalog. A zero duration
/// disables reconciliation.
Expand Down Expand Up @@ -557,6 +573,8 @@ pub fn all_dyncfgs(configs: ConfigSet) -> ConfigSet {
.add(&CONSOLE_OIDC_SCOPES)
.add(&ARRANGEMENT_SIZE_HISTORY_COLLECTION_INTERVAL)
.add(&ARRANGEMENT_SIZE_HISTORY_RETENTION_PERIOD)
.add(&HYDRATION_HISTORY_COLLECTION_INTERVAL)
.add(&HYDRATION_HISTORY_RETENTION_PERIOD)
.add(&CATALOG_INFO_METRICS_RECONCILE_INTERVAL)
.add(&PG_TIMESTAMP_ORACLE_STATEMENT_TIMEOUT)
.add(&FRONTEND_READ_THEN_WRITE)
Expand Down
54 changes: 47 additions & 7 deletions src/adapter/src/active_compute_sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ impl ActiveComputeSink {
}

/// Reports the ID of the connection which created the sink.
pub fn connection_id(&self) -> &ConnectionId {
pub fn connection_id(&self) -> Option<&ConnectionId> {
match &self {
ActiveComputeSink::Subscribe(subscribe) => &subscribe.conn_id,
ActiveComputeSink::CopyTo(copy_to) => &copy_to.conn_id,
ActiveComputeSink::Subscribe(subscribe) => subscribe.connection_id(),
ActiveComputeSink::CopyTo(copy_to) => Some(&copy_to.conn_id),
}
}

Expand Down Expand Up @@ -147,13 +147,26 @@ impl SubscribeBacklogAccounting {
}
}

/// Ownership and cleanup scope of an active subscribe.
#[derive(Debug)]
pub enum ActiveSubscribeOwner {
/// The subscribe belongs to a SQL session.
Session {
conn_id: ConnectionId,
session_uuid: Uuid,
},
/// The subscribe belongs to a coordinator background task.
///
/// Always `internal`, since there is no session to attribute a
/// `mz_subscriptions` row to.
Background,
}

/// A description of an active subscribe from coord's perspective
#[derive(Debug)]
pub struct ActiveSubscribe {
/// The ID of the connection which created the subscribe.
pub conn_id: ConnectionId,
/// The UUID of the session which created the subscribe.
pub session_uuid: Uuid,
/// The owner responsible for retiring the subscribe.
pub owner: ActiveSubscribeOwner,
/// The ID of the cluster on which the subscribe is running.
pub cluster_id: ClusterId,
/// The IDs of the objects on which the subscribe depends.
Expand Down Expand Up @@ -189,6 +202,33 @@ pub struct ActiveSubscribe {
}

impl ActiveSubscribe {
/// The session uuid for this subscribe's `mz_subscriptions` row, or `None`
/// if it does not appear there.
pub fn introspection_session_uuid(&self) -> Option<Uuid> {
match &self.owner {
ActiveSubscribeOwner::Session { session_uuid, .. } if !self.internal => {
Some(*session_uuid)
}
_ => None,
}
}

/// Returns the owning connection, if this is a session subscribe.
pub fn connection_id(&self) -> Option<&ConnectionId> {
match &self.owner {
ActiveSubscribeOwner::Session { conn_id, .. } => Some(conn_id),
ActiveSubscribeOwner::Background => None,
}
}

/// Returns the owning session UUID, if this is a session subscribe.
pub fn session_uuid(&self) -> Option<Uuid> {

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.

nit: no callers; introspection_session_uuid() is the one in use.

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.

Posted by AJ, Aljoscha's coding agent.

Removed the unused session_uuid accessor. introspection_session_uuid remains the single accessor with the required internal-subscribe semantics.

match self.owner {
ActiveSubscribeOwner::Session { session_uuid, .. } => Some(session_uuid),
ActiveSubscribeOwner::Background => None,
}
}

/// Initializes the subscription.
///
/// This method must be called exactly once, after constructing an
Expand Down
4 changes: 3 additions & 1 deletion src/adapter/src/catalog/builtin_table_updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ use mz_sql::names::SchemaSpecifier;
use mz_sql_parser::ast::display::AstDisplay;
use mz_storage_client::client::TableData;
use smallvec::smallvec;
use uuid::Uuid;

// DO NOT add any more imports from `crate` outside of `crate::catalog`.
use crate::active_compute_sink::ActiveSubscribe;
Expand Down Expand Up @@ -811,12 +812,13 @@ impl CatalogState {
&self,
id: GlobalId,
subscribe: &ActiveSubscribe,
session_uuid: Uuid,
diff: Diff,
) -> BuiltinTableUpdate<&'static BuiltinTable> {
let mut row = Row::default();
let mut packer = row.packer();
packer.push(Datum::String(&id.to_string()));
packer.push(Datum::Uuid(subscribe.session_uuid));
packer.push(Datum::Uuid(session_uuid));
packer.push(Datum::String(&subscribe.cluster_id.to_string()));

let start_dt = mz_ore::now::to_datetime(subscribe.start_time);
Expand Down
Loading