dyncfg: require a ParameterScope on every Config - #38180
Conversation
The scope specifier was an optional `.scoped(..)` builder step, so a new dyncfg silently defaulted to environment-wide and a config that is actually realized per replica was easy to leave unannotated. An unannotated replica-local config is a silent no-op for the scoped feature flags layer: LaunchDarkly never evaluates a replica context for it, so a rule targeting a replica or size family does nothing. Make `scope` the fourth argument of `Config::new` and drop `Config::scoped`, so every declaration has to make the choice, then annotate all 276 existing dyncfgs from their read sites: 183 replica-local, 93 environment-wide. The rule, written down on `ParameterScope`: a config is replica-local when its value is realized inside a `clusterd` process. That covers the compute and storage worker config sets, the persist client config set, and `mz_metrics`, all four of which the per-replica dyncfg push reaches. `environmentd` reading such a config for its own process is fine, and sees the environment-wide value. A config realized in `environmentd` is environment-wide even when its effect concerns one replica, and a config whose value must agree across the replicas of a cluster stays environment-wide even when it is read on `clusterd`. Some configs are read in `environmentd` but shipped to a specific replica, which is the case that needs overrides constructed before the read. Those are now resolved against the replica's scoped overrides: * `Controller::provision_replica` builds a replica's `TimelyConfig` from `arrangement_exert_proportionality`, `enable_timely_zero_copy`, `enable_timely_zero_copy_lgalloc` and `timely_zero_copy_limit`. * `ComputeController::add_replica_to_instance` freezes `compute_replica_expiration_offset` and `enable_arrangement_dictionary_compression_alpha` into `ReplicaConfig`. To serve them, `Config::get_with_overrides` layers a replica's `ConfigUpdates` over a `ConfigSet`, the outer `Controller` keeps the pushed override map and fans it out to the compute and storage controllers, and the coordinator installs the map before creating replicas at bootstrap rather than only after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RZo8dwEyXbXwUq6wb7BkeU
The hydration limit is enforced in environmentd, by the controller's per-replica interceptor withholding `Schedule` commands, so declaring it replica-local only means something if the interceptor resolves its replica's override. Give `SequentialHydration` a config set of its own and feed it the `CreateInstance` and `UpdateConfiguration` commands it already absorbs. Those have been specialized for the replica by `Instance::specialize_command_for_replica`, so the set holds exactly what the replica itself reads. It has to be a set of its own rather than the controller's: a cloned `ConfigSet` shares its values, so applying the overrides to a clone would overwrite the environment-wide configuration for everyone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RZo8dwEyXbXwUq6wb7BkeU
Name the coarsest targeting granularity, not the process. balancerd's configs are Environment because balancerd resolves them against its own regional LaunchDarkly context, with nothing finer beneath it, not because balancerd is lumped in with environmentd. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RZo8dwEyXbXwUq6wb7BkeU
antiguru
left a comment
There was a problem hiding this comment.
We should ensure that the optimizer overrides are scoped to their cluster.
| let replica_dyncfg = mz_dyncfgs::all_dyncfgs(); | ||
| ConfigUpdates::from(dyncfg).apply(&replica_dyncfg); |
There was a problem hiding this comment.
Why do we specialize here instead of within the replica task, or where it's created?
There was a problem hiding this comment.
Agreed, moved into the replica task in b23e77a.
ReplicaTask now owns a replica_dyncfg next to the environment-wide dyncfg, seeded at task creation and kept current by apply_config_command in the message loop, right where specialize_command already runs. SequentialHydration takes it as an argument to absorb_command / observe_response and holds no configuration of its own, so it cannot read the environment-wide value by accident. It reads the replica's or nothing.
Not at creation time, though: the task would then need the overrides plumbed into ReplicaClient::spawn as a second channel for something the command stream already carries, and it would go stale on the next push. Tracking the commands keeps one source of truth, and it is what the replica itself sees.
The test moved with the logic. replica_dyncfg_tracks_config_commands covers the seed, the CreateInstance snapshot, the UpdateConfiguration delta and the environment-wide set staying untouched. The interceptor's test is now behavioral rather than a getter check: at concurrency 1 the second Schedule is held back, and raising the concurrency in the supplied set releases it.
Generated by Claude Code
| self.replica_dyncfg_overrides = overrides | ||
| .values() | ||
| .flat_map(|replicas| replicas.iter()) | ||
| .map(|(replica_id, updates)| (*replica_id, updates.clone())) | ||
| .collect(); |
There was a problem hiding this comment.
Could this leak updates for replicas that are dropped?
There was a problem hiding this comment.
Yes. Fixed in b23e77a.
The assignment replaces the whole map, so it does not accumulate across pushes, and I had assumed that was enough. It is not: the coordinator only calls push_replica_dyncfg_overrides when replica_scoped_config_changed, so dropping a replica does not rebuild the map. A dropped replica's entry survives until the next scoped-config change, and an environment that churns replicas while the scoped config sits still accumulates them indefinitely.
Now pruned in the drop path instead of relying on the coordinator to re-push: Controller::drop_replica, ComputeController::drop_replica, compute Instance::remove_replica and storage Instance::drop_replica, which is all four places holding the map.
No correctness consequence that I can find, since replica IDs come from a monotonic sequence and are not reused, so a stale entry could never be picked up by a later replica. It was memory retention only.
Generated by Claude Code
| /// Environment-scoped because it is read twice, once by the optimizer in | ||
| /// `environmentd` and once at render time on the replica, and the two reads must | ||
| /// agree. It also changes what a subscribe emits, which replicas of one cluster | ||
| /// may not disagree about. |
There was a problem hiding this comment.
What happens if they don't agree?
There was a problem hiding this comment.
Nothing bad, which means my justification was wrong. Rewritten in b23e77a.
The two reads are asymmetric. In environmentd the flag gates whether optimize_dataflow_snapshot runs, which may clear with_snapshot on an import. On the replica, render.rs:279 computes if import.with_snapshot || !subscribe_snapshot_optimization { Include } else { Exclude }, so the replica-side read can only ever put a snapshot back, never take one away.
So both directions are safe. Flag on in environmentd and off on the replica means the snapshot is fetched anyway, correct but unoptimized. Flag off in environmentd and on at the replica means with_snapshot is still true, so the snapshot is included. Two replicas of one cluster disagreeing costs work, not correctness, since elision only happens where the optimizer proved the snapshot is not needed for the sink's output.
The real reason it is environment-scoped is the plan-time read: OptimizerConfig::from(&SystemVars) has no replica in scope, and no cluster either. Cluster would be the honest scope, but this is a plain OptimizerConfig field rather than an OptimizerFeature, and OptimizerFeatureOverrides is the only thing plan sites resolve cluster overrides through. persist_fast_path_order on the line above is in exactly the same position. Happy to file a follow-up for extending cluster resolution to the non-feature fields of OptimizerConfig if you think it is worth it.
Generated by Claude Code
The replica task is where per-replica command specialization already lives, and its configuration is not the hydration interceptor's private business. Give `ReplicaTask` a `replica_dyncfg` holding what its replica reads, kept current from the configuration commands passing through, and hand it to `SequentialHydration` on each call. The interceptor now holds no configuration of its own, so it cannot read the environment-wide value by accident. It reads the replica's or nothing. Prune per-replica dyncfg overrides when a replica is dropped. The coordinator re-pushes the override map only when the scoped configuration itself changes, so a dropped replica's entry was otherwise retained until the next such change, and an environment churning replicas accumulated them. Correct the justification on `compute_subscribe_snapshot_optimization`. The two reads need not agree. The replica-side read only ever puts a snapshot back, so a disagreement costs work rather than correctness. It is environment-scoped because the plan-time read has no replica in scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RZo8dwEyXbXwUq6wb7BkeU
|
On the review summary, about the optimizer overrides being scoped to their cluster. I checked every plan site, and the cluster-scoped optimizer overrides are resolved everywhere a cluster is in scope. Two gaps worth naming, neither introduced by this PR:
Dyncfgs cannot be cluster-scoped at all today. That is why this PR annotates 276 dyncfgs as 184 replica and 92 environment, with zero cluster. Cluster resolution at plan time runs entirely through Generated by Claude Code |
Motivation
The scope specifier introduced by the scoped feature flags work
(
doc/developer/design/20260609_scoped_feature_flags.md) was an optional.scoped(..)builder step onConfig. A new dyncfg therefore defaultedsilently to environment-wide, and a config that is actually realized per replica
was easy to leave unannotated. That failure mode is invisible: the LaunchDarkly
sync loop never evaluates a replica context for an unannotated config, so a rule
targeting a replica or a size family is a no-op with no error anywhere.
Only 11 dyncfgs carried a scope. Several obviously replica-local ones did not,
including the whole
lgallocfamily apart fromenable_lgalloc, the memorylimiter, and every storage-worker and persist-client config.
Description
Make
scopethe fourth argument ofConfig::newand dropConfig::scoped, soevery declaration has to make the choice, then annotate all 276 existing dyncfgs
from their read sites: 184 replica-local, 92 environment-wide, none
cluster-coherent (no dyncfg feeds
OptimizerFeatures, which is the only place acluster-scoped value is resolved today).
The rule, now written down on
ParameterScope:clusterdprocess is replica-local. That coversthe compute worker config set, the storage worker config set, the persist
client config set and
mz_metrics— all four of which the per-replica dyncfgpush reaches (
handle_update_configurationapplies the pushed updates to thelast three; the storage worker's own set is served by the storage controller's
layer).
environmentdreading such a config for its own process is fine andsees the environment-wide value.
environmentdresolves for one specific replica, whether it shipsthe value there or acts on it itself, is also replica-local — but only if
the read site resolves that replica's overrides.
environmentdorbalancerdwith no single replica inscope is environment-wide.
environment-wide even when it is read on
clusterd.environmentd read sites, where the overrides have to be constructed first
Seven configs are read in
environmentdbut resolved per replica. They now readthrough that replica's overrides, which is what makes their
Replicadeclaration real rather than decorative:
Controller::provision_replicabuilds a replica'sTimelyConfigfromarrangement_exert_proportionality,enable_timely_zero_copy,enable_timely_zero_copy_lgallocandtimely_zero_copy_limit.ComputeController::add_replica_to_instancefreezescompute_replica_expiration_offsetandenable_arrangement_dictionary_compression_alphaintoReplicaConfig.compute_hydration_concurrency. It takes a different route: rather than apoint lookup,
SequentialHydrationkeeps a config set of its own, fed from theCreateInstanceandUpdateConfigurationcommands it already absorbs. Thosehave been specialized for the replica by
Instance::specialize_command_for_replica, so the set holds exactly what thereplica itself reads. It has to be a set of its own: a cloned
ConfigSetshares its values, so applying overrides to a clone would overwrite the
environment-wide configuration for everyone.
Supporting changes:
Config::get_with_overrideslayers a replica'sConfigUpdatesover aConfigSet(a mistyped override is logged and ignoredrather than panicking a controller path); the outer
Controllerkeeps the pushedoverride map and fans it out to the compute and storage controllers, so the
coordinator has a single push entry point; and the coordinator installs the map
before creating replicas during bootstrap, not only after, since the
provisioning-time values are frozen by then. The DDL path already pushed before
create_replica, so it needed no reordering.Deliberate environment-wide declarations worth a reviewer's eye
enable_compute_replica_expiration— the env-wide kill switch, read whenspecializing
CreateInstance;compute_replica_expiration_offsetis thereplica-scoped half of the pair.
compute_subscribe_snapshot_optimization— read at plan time and at rendertime, and it changes what a subscribe emits.
storage_enforce_external_addresses— a security control; a per-replicaoverride would weaken it for part of an environment.
persist_use_critical_since_catalog, the catalog and expression-cache forcedcompaction knobs,
txn_wal_apply_ensure_schema_match— read only fromenvironmentd.Verification
cargo check --workspace --all-targetsandcargo clippy --all-targetson thetouched crates are clean,
bin/fmtapplied.mz-dyncfgunit test forget_with_overrides: absent override, presentoverride, and a type-mismatched override falling back to the set's value.
mz-compute-clientunit test that the hydration interceptor's concurrencyfollows the replica's configuration, through both the
CreateInstancesnapshot and a later
UpdateConfiguration, and that the environment-wide setis left untouched.
environment-scoped config is read from a
clusterd-side crate except the twodocumented deliberate cases, and every replica-scoped config has a
clusterd-side read or is one of the seven resolved inenvironmentdabove.enable_scoped_system_parametersis off (thedefault): the sync loop evaluates no scoped contexts at all, so the wider
Replicasurface costs nothing until the feature is enabled.Not covered by an automated test: the controller wiring for the six
provisioning-time and replica-creation-time configs. Exercising it needs a
ComputeController/ orchestrator harness that does not exist today, so it isverified by construction and by the
get_with_overridestest underneath it.🤖 Generated with Claude Code
https://claude.ai/code/session_01RZo8dwEyXbXwUq6wb7BkeU