adapter: scope config-sync-file parameters with segments and rules - #38208
claude[bot] wants to merge 19 commits into
Conversation
Scoped (per-cluster and per-replica) system parameters have been running in production, so the kill-switch is dead weight. Its off-state is also actively harmful: with the gate off the sync loop pushed an empty desired state with an unbounded prune scope, which deleted every stored scoped override. Removing the gate makes the scoped path unconditional and drops the clearing branch in sync_scoped_params, along with the maybe_dirty bookkeeping that only existed to run that branch once. The create-time fold in scoped_overrides_create_op no longer consults the dyncfg either. With the clearing branch gone, every caller of update_scoped_system_parameters supplies a prune scope, so Op::UpdateScopedSystemParameters takes ScopedParametersScope by value instead of Option<ScopedParametersScope>. The prune predicates become direct set lookups. The LaunchDarkly integration test loses its gate flag and the toggle-off assertion that covered the clearing behaviour, which no longer exists. The bidirectional reconcile assertion just above it still covers override removal when LaunchDarkly stops serving a differing value.
Cluster-coherent and replica-local system parameters were resolvable only through LaunchDarkly. The file-backed config sync client, which is how self-managed deployments configure system parameters through a Kubernetes ConfigMap, returned an empty override map for both scoped passes, so a self-managed operator could only set a parameter for the whole environment. The config sync file gains two reserved top-level keys, `clusters` and `replicas`, holding overrides keyed by cluster name, and by cluster name then replica name. Every other top-level key keeps its environment-wide meaning, so a flat file is unchanged in both syntax and behaviour. `ConfigFile` models the parsed file and is the file client's single read per pass, replacing the per-parameter read the environment-wide pass did before. `file_cluster_overrides` and `file_replica_overrides` iterate the live eval contexts the sync loop already builds and match each object to its section by name, so a section naming an object that does not exist is never consulted rather than being an error. The recording decision is shared with the LaunchDarkly path in `classify_scoped_value`: a value is stored only when it parses for the parameter's type and differs from the environment-wide value in that parameter's canonical encoding. An unparseable value is dropped, keeping unparseable state out of the durable collections. The file path additionally warns on one, since the file is operator-authored and the typo is theirs to fix. Tests cover the parse (flat-only, both scoped sections, malformed sections, a non-object document) and the resolution (cluster and replica sections applied, unknown object names ignored, unparseable value dropped, value matching the environment dropped), plus a guard that no synced parameter is named after a reserved section. `test/dyncfg` gains an end-to-end pass over the file frontend asserting the rows in `mz_cluster_system_parameters` and `mz_replica_system_parameters` appear, ignore absent object names, and are pruned when the sections are removed.
The parent change, removal of the `enable_scoped_system_parameters` gate, landed on `main` as #38206, so this branch's own copy of it is now redundant with `main`. The only conflict was in `src/adapter-types/src/dyncfgs.rs`, at the tail of `all_dyncfgs`: this branch removed `ENABLE_SCOPED_SYSTEM_PARAMETERS` there while `main` added `FRONTEND_READ_THEN_WRITE` in the same place. Resolved to `main`'s version, which already has the removal.
kay-kim
left a comment
There was a problem hiding this comment.
good from the docs side mod 2 nits.
|
|
||
| Behavior worth knowing: | ||
|
|
||
| - **Not every parameter can be scoped.** Only parameters whose scope is |
There was a problem hiding this comment.
Question for the SM team (and I realize there's been an ongoing discussion since almost the beginning about how we want to curate these parameters): We don't currently provide the scope information. So, would people need to just try it out?
There was a problem hiding this comment.
Confirmed: today nothing surfaces a parameter's scope to an operator. SHOW ALL returns only name/setting/description, and mz_internal.mz_cluster_system_parameters / mz_replica_system_parameters are (cluster_id|replica_id, name, value), so they show overrides that already exist rather than which parameters accept scoping. ParameterScope lives only in the Rust definitions.
The failure mode if someone guesses wrong is quiet. file_section_overrides iterates the parameters whose declared scope is cluster/replica and looks each one up in the section, so a key in a scoped section for a parameter that is not scoped (or a misspelled name) is parsed and then simply never consulted. No error, no warning. Only an unparseable value for a genuinely scoped parameter gets a warning.
The fix I would suggest as a follow-up is exposing the scope in introspection, e.g. a scope column so an operator can query which parameters accept scoping instead of experimenting. That is not in this PR.
The curation-policy half of your question is the SM team's call, so I will leave that to them.
Generated by Claude Code
| - `replicas` is keyed by cluster name, then by replica name. The nesting is | ||
| required because a replica name is only unique within its cluster, and because | ||
| both names may themselves contain a `.`. | ||
|
|
There was a problem hiding this comment.
maybe an intro to the example:
For example, the following configuration sets environment-wide parameters and overrides parameters for a specific cluster and replica:
| } | ||
| ``` | ||
|
|
||
| In this example `max_connections` and `enable_lgalloc` apply environment-wide, |
There was a problem hiding this comment.
need a comma after "In this example"
Add an intro sentence before the scoped ConfigMap example and add the missing comma after "In this example".
Addresses review feedback on the scoped config-sync file support. A whole-document read or parse failure now expresses "no information" rather than "no overrides". `ConfigFile::parse` returns `Option`, and `sync_scoped_params` skips the reconcile when the frontend has no successful read behind it, so a JSON syntax error or a deleted ConfigMap no longer durably prunes every cluster- and replica-scoped override and restores it once the file is fixed. Per-section leniency is unchanged: a malformed section or an unparseable value is still dropped on its own. The parsed file is cached on the frontend, refreshed by `pull` and read under a lock by the scoped passes. That removes the two synchronous `fs::read_to_string` calls `scoped_overrides_create_op` was making on the coordinator loop for every `CREATE CLUSTER` / `CREATE CLUSTER REPLICA`, and cuts three reads per tick to one, which also removes the mixed-view note. Diagnostics are reported only when the file's contents change, so a standing mistake warns once instead of at the 1s tick rate. Scoped section keys that are not scopable at that position, a misspelled parameter name or a replica-scoped parameter in a `clusters` section, now warn instead of being dropped silently. `as_sections`' warning-context closure is replaced by a `FilePosition` formatted inside the `warn!`, which drops the per-object allocations on the happy path and fixes the nested `replicas` arm naming the wrong section. Tests: new unit tests for the read/parse failure semantics, the cache, and the section diagnostics; `test/dyncfg` now also creates a cluster after a section naming it exists, exercising create-time resolution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7xuTVRzsSesLpG7ZZ3Qt5
|
Thanks, all five acted on in 48cec17. Replying here rather than per-thread since points 2 and 3 share one fix. 1. Failure semantics. I went with "no information", not prune-on-failure. 2 and 3. Cache. The parsed file lives on the frontend now ( 4. Unscopable keys. 5. Create-time coverage. 6. Nit. On the praise: noted, thanks. The three-way enum is what made point 4 cheap to add, since the file path only needed a different consumer of Same verification caveat as the PR body, made worse by the size of this change: Generated by Claude Code |
jubrad
left a comment
There was a problem hiding this comment.
Awesome! A few things as I was going through the code with claude, nothing major.
Summary
4 considerations
1 nit
1 praise
| /// | ||
| /// Nested rather than keyed by a composite `"cluster.replica"` string | ||
| /// because cluster and replica names are SQL identifiers that may themselves | ||
| /// contain a `.`, which would make a composite key ambiguous. |
There was a problem hiding this comment.
consider — The contract stated here is only true for the environment-wide pass. pull maps a missing key back to params.get(param_name), so an empty ConfigFile really does leave every environment-wide value alone. The scoped passes have the opposite meaning: an empty ConfigFile is a complete desired state of "no overrides", and sync_scoped_params prunes accordingly across every live object.
So if an operator applies a ConfigMap with a JSON syntax error (or the ConfigMap is deleted, since the volume is mounted optional: true), the next tick keeps all environment-wide values but durably deletes every cluster- and replica-scoped override, pushing e.g. enable_lgalloc back to on on every replica via the compute controller, then restores them all when the file is fixed. Half the file's effect silently reverts on a typo while the other half stands.
Either make the failure path express "no information" rather than "no overrides" (e.g. read returns Option<ConfigFile> and sync_scoped_params skips update_scoped_system_parameters when the read or parse failed; the per-section leniency below is fine, it's only the whole-document failure that has this reach), or, if prune-on-failure is the intent, say so here instead of claiming every value is left alone.
| /// the config-sync file mentions that is not live is ignored. | ||
| pub fn pull_cluster_overrides( | ||
| &self, | ||
| params: &SynchronizedParameters, |
There was a problem hiding this comment.
consider — This warning is unconditional and the pass runs every tick. generation.rs passes --config-sync-loop-interval=1s for every self-managed deployment, so one unparseable entry emits ~86k identical lines per day, per (object, parameter), until someone edits the ConfigMap. The comment two arms up on the LaunchDarkly path rejects a warning for exactly this reason; the operator-authored argument justifies telling them once, not at 1Hz forever. The as_object warnings in parse have the same property.
The flip side is that the mistake an operator is most likely to make gets no message at all: file_section_overrides only looks up names in param_names, so a misspelled parameter, or a replica-scoped parameter placed in a clusters section, is silently dropped. Since there's no way to see a parameter's scope from SQL, there's nothing to debug against.
Both fall out of one change: keep the previously-parsed ConfigFile and emit scoped-file diagnostics only when it changes from the prior tick. That makes the warning affordable, and lets you also warn on section keys not in param_names ("ignoring enable_lgalloc for cluster "analytics": not a cluster-scoped parameter"), which is the signal an operator can actually act on.
| warn!("{diagnostic}"); | ||
| } | ||
| } | ||
| *cache = Some(CachedConfigFile { |
There was a problem hiding this comment.
consider — scoped_overrides_create_op runs on the coordinator task (sequence_create_cluster, cluster.rs:1427) and calls both pull_cluster_overrides and pull_replica_overrides, so every CREATE CLUSTER under a file frontend now performs two blocking fs::read_to_string calls inline on the loop that serializes all DDL and query sequencing. Previously the file client returned instantly there. It's a tmpfs-projected ConfigMap so it's normally microseconds, but synchronous I/O on the coordinator loop isn't a pattern this codebase otherwise has.
Caching the parsed ConfigFile on the frontend (refreshed by pull, read under a lock by the scoped passes) would remove the coordinator-loop I/O, cut the three reads per tick to one, and make the NOTE on read about a tick observing a mixed view unnecessary rather than merely documented. The create path is best-effort and re-reconciled every tick, so a value up to one tick old is fine there.
| }, | ||
| } | ||
|
|
||
| write_config(config_file, system_params_3) |
There was a problem hiding this comment.
consider — The cluster is deliberately created while the file is still flat, which makes the "no scoped rows yet" assertion meaningful. Good. But it means the create-time path is never exercised for the file client, and that's the newly-reachable path with the most machinery behind it: scoped_overrides_create_op folding the override into the create transaction so a new replica's first controller configuration carries it (the reason that fold exists, per the comment at cluster.rs:1417). A CREATE CLUSTER dyncfg_scoped_2 after system_params_3 is written, plus a section naming it, would cover it in a few lines and would also catch a regression in the create-path plumbing that the per-tick reconcile would otherwise paper over.
| /// keep distinct from a valid but empty document. An empty document is a | ||
| /// complete desired state of "no scoped overrides", which the reconcile | ||
| /// applies by durably pruning every override. See | ||
| /// [`SystemParameterFrontend::has_scoped_desired_state`]. |
There was a problem hiding this comment.
nit — as_sections takes a &str context plus an impl Fn(&str) -> String purely to build warning strings, and the call sites format eagerly: file_cluster_overrides/file_replica_overrides allocate a format!("cluster {name:?}") for every live object on every tick even when nothing warns. Passing the name pieces through and formatting inside the warn! would drop the closure parameter and the happy-path allocations.
Minor related wrinkle: the nested arm at line 155 passes format!("cluster {cluster:?}") as the context for entries inside replicas, so a malformed entry there warns about "cluster "prod"" and points an operator at the wrong section.
Replace the name-keyed `clusters` and `replicas` sections of the config sync file with segments, named predicates over a cluster's or replica's scope attributes, and an ordered `rules` array attaching parameters to them. Exact name targeting cannot express what LaunchDarkly targeting expresses, so a file written that way could not serve as a fallback for a LaunchDarkly outage without silently changing which objects a parameter applies to. A segment matches on the same attribute vocabulary the LaunchDarkly `cluster` and `replica` context kinds carry, so the two can express the same thing. Attributes in a segment are ANDed, values of one attribute are ORed, and matching is exact, which mirrors LaunchDarkly's `in` operator. Rules are an array because an object's key order is lost on parse, and for each (object, parameter) the first matching rule wins. A cluster-coherent parameter may not be supplied through a segment matching on a replica attribute, since honouring that would let it resolve differently across one cluster's replicas. Such a parameter is dropped from that rule with a diagnostic naming the segment, the parameter and the attribute. A predicate entry this binary cannot evaluate, an unknown attribute name or a value list of the wrong shape, makes its segment match nothing rather than being dropped: dropping it would leave the surviving ANDed attributes matching a wider set of objects than the operator wrote. Flat top-level keys keep their environment-wide meaning, so existing config maps are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7xuTVRzsSesLpG7ZZ3Qt5
…derive from A scoped value is recorded as an override only when it differs from the environment-wide value, so which rows exist depends on that baseline. The scoped assertions derived it from the two picked parameters' compiled-in defaults, but a plain `Materialized()` renders `MZ_SYSTEM_PARAMETER_DEFAULT` from `get_default_system_parameters`, which supplies both of them as the opposite of their compiled-in default. Every scoped row the test expected was therefore unreachable: the cluster's `enable_eager_delta_joins true` matched the environment-wide value, `r2` took no `enable_lgalloc` row while `r1` did (inverting the only end-to-end first-match-wins check), and for `dyncfg_scoped_2` both values matched their bases so the create-time fold found nothing to commit. Pin both parameters as flat top-level keys instead. Flat keys are applied to the sync loop's parameter set in the same tick that refreshes the file cache, before the scoped passes read it, and the create path resolves against the catalog's system config, so both the reconcile and the create-time fold see the intended baseline whatever the harness passes. The expected rows, re-derived from that baseline, are the ones the test already asserted. `enable_join_prioritize_arranged` is pinned too, so the rules that must have no effect are judged against a stated baseline rather than a compiled-in default that the harness could later override. Also assert the baseline itself once it has synced, so a future harness change surfaces as the premise it breaks rather than as an unexplained row mismatch, and state first-match-wins per replica rather than by the absence of a row, so reversing the two rules fails the assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7xuTVRzsSesLpG7ZZ3Qt5
There was a problem hiding this comment.
Review
One thing needed fixing before this merges, and it was in the test rather than the shipped code: the new scoped assertions in test/dyncfg/mzcompose.py asserted the opposite of what the resolver does under the mzcompose harness. The Rust looks sound; the semantics I probed match what the docs and doc comments claim, including the fail-closed segment handling, the skip-on-no-information guard, and the caching rewrite.
The new dyncfg scoped assertions are inverted and will red the nightly
The test reasons from the compiled-in defaults of the two parameters it picked, as its own header comment says. But the harness overrides both: test/dyncfg runs a plain Materialized(), which renders MZ_SYSTEM_PARAMETER_DEFAULT from get_default_system_parameters, setting enable_eager_delta_joins=true and enable_lgalloc=false. Neither name appears in get_variable_system_parameters, so the environment-wide values inside the test are the inverse of what the assertions assume.
That matters because resolution records an override only when it differs from the environment-wide value, and that baseline comes from get_system_vars(), i.e. effective values including the CLI defaults. So under the harness the cluster row for enable_eager_delta_joins true never exists, r2 gets no enable_lgalloc row while r1 gets one, which is exactly inverted, and for dyncfg_scoped_2 both values equal their bases so scoped_overrides_create_op returns None and zero rows exist. The create-path coverage added in the previous round therefore asserts nothing reachable, and the first-match-wins check, the reason the second replica was added, tests the opposite of the intent.
dyncfg runs only in the nightly pipeline, never in the PR pipeline, so the green PR run never executed any of it. Merging as-is would turn the nightly dyncfg step red deterministically.
What I changed
Everything above describes 7911049d. Fixed in a3ab5cc, test only, no Rust change, since the resolver's behaviour is the correct one and it was the test's premise that was wrong.
test/dyncfg/mzcompose.py now pins the environment-wide baseline it reasons from, as flat top-level keys in the config file it writes, instead of inheriting whatever the harness passes: enable_eager_delta_joins to false and enable_lgalloc to true, in the same flat section as max_connections, present in every version of the file the test writes. Flat keys are applied to the sync loop's parameter set in the same pull that refreshes the file cache, before sync_scoped_params runs, and the create path resolves from the catalog's system config, so the reconcile and the create-time fold both see the intended baseline. enable_join_prioritize_arranged, the parameter whose leak the rules that must have no effect are checked against, is pinned to false for the same reason, so those negative assertions rest on a stated baseline rather than a compiled-in default a future harness change could invert.
Every expected row was then re-derived from that baseline, and they come out as the rows the test already asserted: the cluster's enable_eager_delta_joins true and r2's enable_lgalloc false are now genuine overrides, r1 keeps the environment-wide value through first-match-wins, and both dyncfg_scoped_2 create-path rows are reachable. The header comment that stated the wrong premise is rewritten, and the rule comments now say which value differs from the baseline and which repeats it.
Two additions while there. First, the baseline itself is asserted once it has synced, read as mz_system since none of these parameters is user-visible, so a future harness or resolver change surfaces as the premise it breaks rather than as an unexplained row mismatch. Second, first-match-wins is now stated per replica rather than by the absence of a row: a LEFT JOIN over both replicas of dyncfg_scoped asserts r1 env-wide and r2 false, so reversing the two rules makes scoped-cluster decide both replicas, turns r1 into a false row, and fails the assertion.
Coverage
The resolution semantics themselves are covered by the unit tests in src/adapter/src/config/frontend.rs, which drive the file frontend against a SynchronizedParameters built from compiled-in defaults and so are unaffected by the harness: segment matching, the AND/OR and exact-match rules, the coherence guard, fail-closed handling of an uninterpretable predicate entry, rule ordering, the differs-from-environment and unparseable classifications, and the diagnostics. test/dyncfg is the only end-to-end coverage of the file path, and it runs in the nightly pipeline only, never on a PR.
I could not execute test/dyncfg here: it needs Docker images and a full build, and this environment has no Docker daemon and cannot resolve two Cargo git submodules. The corrected assertions are derived by reading the resolver, the sync loop and the create-time fold, not observed running. bin/fmt (ruff, ruff-dbt, black, rustfmt), python3 -m py_compile on the changed file, bin/mzcompose --find dyncfg list-workflows to confirm the composition loads, and MZDEV_NO_SHELLCHECK=1 bin/lint's check-python-files.sh all pass; the remaining bin/lint checks fail identically on the tree without this change, for missing tooling and blocked egress. So the first nightly after this merges is what actually confirms the fix, and it is worth watching.
Merge decision: merge. The shipped code needs no change, and the one blocking issue is fixed on this branch.
Generated by Claude Code
A segment predicate could only match exactly, which is LaunchDarkly's `in` operator. Real targeting rules select clusters and replicas by name pattern, and a pattern over names cannot be pre-expanded into an exact list: a cluster or replica that does not exist yet must still be targetable. An attribute's predicate now accepts either the existing bare array or an object of an optional `in` array and an optional `matches` array of regular expressions, ORed against each other. Patterns are compiled when the file is parsed, which the parse cache makes once per change to the file rather than once per tick. An invalid pattern, and an unknown key inside the predicate object, fail closed through the existing `RejectedAttribute` mechanism: the segment matches nothing and the mistake is warned about once, naming the segment, the attribute and the error. Dropping either instead would widen the predicate past what the operator wrote. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7xuTVRzsSesLpG7ZZ3Qt5
A segment predicate was an attribute-to-values map with an ad-hoc operator
object. That expressed less than a LaunchDarkly targeting rule and expressed it
differently, so the file could not stand in for LaunchDarkly without a
translation that changes meaning.
A segment now hoists a `contextKind`, `cluster` or `replica`, and carries an
array of LaunchDarkly clauses, `{attribute, op, values, negate}`. Clauses are
ANDed, values within a clause are ORed, and `negate` inverts the clause after
that OR, as it does upstream. The five string operators are supported: `in`,
`startsWith`, `endsWith`, `contains` and `matches`.
Hoisting the context kind onto the segment makes the cluster-coherence rule
structural rather than inferred from which attributes a predicate happens to
name. A cluster-coherent parameter is supplied only through a `cluster`
segment, and a `cluster` segment cannot name a replica attribute at all.
Everything this binary will not evaluate fails closed through the existing
rejected-attribute path, so the segment matches nothing and the rules naming it
never apply: an unknown attribute or context kind, a malformed clause, an
invalid `matches` pattern, an unknown key in a segment or clause, and each of
the ten LaunchDarkly operators that could only ever be false over a
string-valued attribute. The last are recognised and refused with their own
reason rather than reported as typos. LaunchDarkly's REST-only `_id` is the one
clause key ignored instead of refused, so a clause copied out of the API works
as written.
`matches` patterns are compiled when the file is parsed, which the parse cache
makes once per change to the file rather than once per tick. They are
unanchored, which is both the regex crate's default and what LaunchDarkly does,
it being the same crate.
The operator vocabulary is mirrored rather than imported: the SDK's `Op` enum
and `Clause` fields are `pub(crate)`. `test_operator_vocabulary_matches_launchdarkly`
keeps our copy honest, since the compiler cannot.
Also softens the `ld_ctx` comment about conflicting-rule ordering to what the
SDK actually does, targets then rules in array order, rather than the
unverified claim about flag variation definition order.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7xuTVRzsSesLpG7ZZ3Qt5
`cargo clippy --all-targets -- -D warnings`, which CI runs, makes an unused private item a hard error. Moving the coherence diagnostic onto the segment's context kind left `ScopeAttribute::as_str` with no caller. `ClauseDefect::AttributeOutsideContext` now carries the `ScopeAttribute` rather than the name the file spells, which is the same string, since the attribute parsed. That drops a redundant `String` and gives `as_str` its caller back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7xuTVRzsSesLpG7ZZ3Qt5
…ring The `ld_ctx` note and the `test/launchdarkly` remark both described LaunchDarkly's evaluation order more confidently than either could be checked against. What the locked `launchdarkly-server-sdk-evaluation` source does show: the order contexts are added to the multi-context is irrelevant, because a clause or target names its context kind and the SDK resolves that kind by lookup (`Context::as_kind`) rather than by an ordered scan over the registered contexts, and precedence within a flag is individual targets, then rules, then the fallthrough, array order winning within each. What it does not show is that ordering between conflicting entries follows the definition order of the flag's variations. The SDK evaluates `contextTargets` in array order, so that would require LaunchDarkly's server to reorder the array it serves. That is an inference from a single empirical observation in this test, so it is now marked as an unconfirmed observation about the server rather than as SDK behaviour. Comments only, no code path changes.
…ater one Once `scoped-replicas` is widened to `startsWith`, it matches every replica of both clusters, including `r1` of `dyncfg_scoped`, which the narrower `scoped-r1` rule ahead of it is supposed to keep deciding. The existing assertion at that step only queried `dyncfg_scoped_2`, so a regression that let the widened rule take the parameter over would have gone unnoticed there. Assert both clusters' replicas, which pins first-match-wins across the widening rather than only before it.
…form
The `systemParameterConfigmapName` doc comment still showed the terse
segment form, `{"cluster_name": ["analytics"]}`. A segment now declares a
`contextKind` and a list of clauses, so that example parses as a segment
with two defects, a missing context kind and a missing `clauses` array,
which makes it match nothing. The documented ConfigMap would have been
accepted and then quietly done nothing.
Rewrite the example as a `cluster` segment with one `in` clause, and say
in the prose that a segment names a context kind and its clauses. Both
copies of the doc comment, `v1alpha1` and `v1`, and both generated
`materialize_crd_descriptions_*.json` files, whose `description` is the
doc comment verbatim, are updated together.
def-
left a comment
There was a problem hiding this comment.
QA LLM review:
1. MEDIUM -- Publish scoped rules only with their environment-wide baseline
src/adapter/src/config/frontend.rs:1164
refresh_config_file publishes the newly parsed rules to the coordinator before the same file's top-level parameter changes are pushed to the catalog. A concurrent cluster or replica creation can therefore classify scoped values against an old or partially updated environment-wide baseline and omit an override that its first configuration requires.
SystemParameterFrontend::pull refreshes this shared cache first, then system_parameter_sync awaits backend.push, which updates modified parameters one at a time, before starting the scoped reconcile. Meanwhile scoped_overrides_create_op reads the new cache but constructs SynchronizedParameters from catalog.system_config(). For example, if the file changes enable_lgalloc from off to on while a matching rule keeps it off, a create in this window sees off as both the base and rule value and records no replica override. The subsequent environment-wide push changes the replica to on; only the later reconcile restores off. The create-time fold exists specifically because that later repair is too late for render-frozen settings. Publish the parsed file only after its baseline is committed, or cache and evaluate an atomic file-plus-baseline snapshot.
2. MEDIUM -- Keep the last valid rules available to create-time evaluation
src/adapter/src/config/frontend.rs:1155
On any read or whole-document parse failure, the cache replaces the last valid ConfigFile with None. Existing objects retain their durable overrides because has_scoped_desired_state skips reconciliation, but objects created during the failure receive no matching override at all, so preserved policy becomes inconsistent across otherwise identical replicas.
Both scoped pull methods return an empty map when cached_config_file() is None, and all four cluster create/recreate paths reach them through scoped_overrides_create_op. If a ConfigMap temporarily disappears or contains partial JSON while a matching replica is created, that replica starts at the environment-wide value and remains there until the file recovers. For a render-frozen parameter, the eventual reconciliation cannot repair work rendered under the wrong initial value. Keep a separate last-known-valid parse for create-time evaluation while retaining a current-read-valid bit to prevent the periodic reconciler from pruning on failure.
3. MEDIUM -- The create-time test can be satisfied by periodic reconciliation
test/dyncfg/mzcompose.py:336
The purported create-time assertion only queries durable override rows after CREATE CLUSTER. The independent config-sync loop runs every 100 ms and can populate those rows after creation but before the following SELECT, so the test can pass even if the create transaction stops folding scoped overrides into the replica's first configuration.
The test's own comment acknowledges the 100 ms reconciler, but omitting an explicit sleep does not exclude it. CREATE CLUSTER includes a durable catalog transaction, and a sync snapshot/update queued while that command runs can be processed before testdrive sends or the coordinator handles the next query. Removing scoped_overrides_create_op would therefore still permit the expected rows to appear, while render-frozen settings would already have been missed. Disable or block the periodic reconcile across this assertion, or assert an observable value captured from the replica's first controller configuration rather than eventual catalog state.
A rule is recorded as an override only where it differs from the environment-wide value, so a file's rules and the baseline they are judged against have to come from the same file. Two windows broke that, both of them the create path reading a cache whose freshness it could not reason about. `refresh_config_file` published a parse the moment it read it, while the sync loop pushes that same read's top-level parameters afterwards, one parameter at a time. A create landing in between paired the new rules with the old baseline. If the file moved `enable_lgalloc` from `off` to `on` while a matching rule held a replica at `off`, the rule value and the baseline were both `off`, so no override was recorded -- and the push then took the replica to `on`, the one value its rule forbids. The next reconcile repairs the durable row, but the create-time fold exists precisely because that is too late for a render-frozen parameter. On any read or whole-document parse failure the cache replaced the last valid parse with `None`. Existing objects kept their durable overrides, since `has_scoped_desired_state` skips the reconcile, but an object created in that window got no override at all: a ConfigMap that briefly vanished or was caught half-written left otherwise identical replicas under different policy depending on which side of it they were created. Split the cache into the current read and the published parse. `refresh_config_file` sets the current read and returns it for the environment-wide pass, leaving the published parse alone; the sync loop calls `publish_config_file` after the push and before the scoped reconcile, which advances it -- and only for a valid parse, so the last valid policy also survives a failed read. `has_scoped_desired_state` reads the current read, so a failed read still holds the prune back: not acting on a desired state we do not have, while the create path is better served by the last valid one than by nothing. Both windows are pinned by tests that fail without the split: `test_rules_are_published_with_their_baseline` and the extended `test_read_failure_keeps_scoped_overrides`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7xuTVRzsSesLpG7ZZ3Qt5
… assertion The create-time assertion only queried the durable override rows after `CREATE CLUSTER`, with the sync loop reconciling every 100ms alongside it. That loop populates the same rows, and a snapshot queued while the create transaction runs can be applied before testdrive sends the next query, so the assertion passed whether or not the create transaction folded anything in. Deleting `scoped_overrides_create_op` would still have left it green, while a render-frozen parameter would already have been missed. The comment conceded as much rather than fixing it. `config_sync_loop_interval` is a startup argument, so make it settable on the `Materialized` service (still 100ms by default) and restart materialized with it set to an hour for this one block. The loop's first tick fires immediately on startup, which is what installs the shared frontend the fold needs and what reconciles the objects that already exist; the next tick is an hour away, so every row asserted for `dyncfg_scoped_2` can only have come from its own create transaction. If that first tick has not run, the rows are absent and the test fails loudly rather than flakily. The final prune assertion is the reconcile's job, so it restarts back onto the short interval. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7xuTVRzsSesLpG7ZZ3Qt5
QA LLM Review1. MEDIUM -- Parking the sync loop at 1h leaves testdrive's
|
`bin/doc` runs rustdoc with `-D warnings`, and `rustdoc::private_intra_doc_links` fires when a public item's documentation links a private one -- even under `--document-private-items`, which only widens what resolves, not what is warned about. `publish_config_file` and `has_scoped_desired_state` are both public and both linked `Self::published_config_file`, which is private, so the doc build failed. Name the accessor in prose in those two comments instead. The links from `CachedConfigFile`, which is itself private, are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7xuTVRzsSesLpG7ZZ3Qt5
Parking the sync interval at an hour removed the thing that had been quietly repairing testdrive's reset. `c.testdrive()` runs `ALTER SYSTEM RESET ALL` before every script unless the service declares `no_reset`, which returns each parameter to its `--system-parameter-default`. Those defaults come from `get_default_system_parameters`, which sets both scoped parameters to the opposite of `ENV_WIDE_PARAMS` -- so the reset baseline is exactly the two rule values. `scoped_overrides_create_op` builds its baseline from `catalog.system_config()`, `classify_scoped_value` calls both `MatchesEnvironment`, `evaluated.is_empty()` holds, and the op is `None`: the rows the block asserts would simply be absent. Everywhere else in this file the 100ms loop repairs the reset inside testdrive's retry window, which is the only reason the `SHOW max_connections` assertions pass at all. Give the block `Testdrive(no_reset=True)` alongside the long interval, so the environment-wide baseline it classifies against stays the file-supplied one. The alternative of re-asserting the flat keys with `ALTER SYSTEM SET` would make the baseline come from SQL rather than from the file, which is most of what the assertion is for. Also make the block fail at its premise rather than at its conclusion: it now asserts `SHOW max_connections` ahead of the `CREATE CLUSTER`, since `max_connections` is one of the same flat keys and is user-visible, so a baseline that got reset after all fails on that line instead of surfacing as an unexplained missing row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7xuTVRzsSesLpG7ZZ3Qt5
|
@def- thanks — all four findings held up against the code, and all four are fixed. One consolidated reply rather than one per finding. 1 & 2 (stale baseline on create; no override while the file is unreadable) —
The same promotion only advances on a valid parse, which is finding 2: the last valid parse stays available to create-time evaluation across a failed read or a half-written ConfigMap. Both windows have regression tests that fail without the split: 3 (create-time test satisfiable by the reconciler) — 4 ( Fixed with Caveats worth knowing. On the red CI: Generated by Claude Code |
|
@claude please rebase this PR. |
|
Claude finished @antiguru's task in 1m 53s —— View job Rebasing PR
Done. The branch couldn't be cleanly rebased (it contains prior merge commits from main, causing the rebase to conflict at the repository's root commit with 37,746 commits to replay). Merged |
jubrad
left a comment
There was a problem hiding this comment.
Summary
2 praise
Other
praise — src/adapter/src/config/frontend.rs — Round two: all six of my findings are addressed, and I'm satisfied on each.
1 (failure semantics) went the way I'd have chosen, and the reasoning for "no information" over prune-on-failure is now in the code rather than in the PR thread. 2 (warning spam) and 3 (coordinator-loop I/O) fell out of the cache, and keying it on the raw contents turns a 1 Hz warning into one per edit. The unscopable-key diagnostic is the part an operator can actually act on, and returning the messages so a test can assert them is better than what I asked for. 4 (create-time coverage) is covered, and the block's premise is now asserted ahead of the CREATE rather than inferred from its result. 5 (as_sections) is gone.
I ran what your environment could not, at 6f20d2b: cargo check -p mz-adapter --all-targets and cargo clippy -p mz-adapter --all-targets are both clean, and all 33 config::frontend unit tests pass, including test_read_failure_keeps_scoped_overrides, test_rules_are_published_with_their_baseline and test_config_file_cached_until_it_changes. So the Rust in this PR does compile and its tests do pass. I did not run test/dyncfg (nightly-only, no mzcompose run here), so that half stays reasoned rather than observed.
written by claude on behalf of @jubrad
| /// replicas do not diverge on when they happened to be created. Pruning is | ||
| /// held back over that window separately, by | ||
| /// [`Self::has_scoped_desired_state`], which tracks the current read. | ||
| fn published_config_file(&self) -> Option<Arc<ConfigFile>> { |
There was a problem hiding this comment.
praise — Splitting the cache into current and published is a better answer than either of the two findings that led here asked for: one mechanism decides both "may we prune?" (the current read) and "which rules does a create see?" (the last read whose baseline is committed), and the doc comment says why the two questions get different halves. Worth keeping the publish_config_file call site in sync.rs next to the push it depends on, since the invariant is an ordering that nothing in the type system enforces.
written by claude on behalf of @jubrad
Requested by Moritz Hoffmann · Slack thread
Builds on the gate removal from #38206, which has landed in
main.Motivation
Before. A self-managed deployment configures system parameters by pointing
systemParameterConfigmapNameat a ConfigMap holding a flat{parameter: value}JSON object. Every key in it applies to the entireenvironment, so a setting can only be turned on for everything at once. There is
no way to say "this value, but only for the
analyticscluster" or "only for thereplicas of a legacy size family".
After. The same ConfigMap can also scope parameters to a subset of clusters
and replicas, alongside the environment-wide keys. Existing flat ConfigMaps keep
working exactly as before, with the same syntax and the same meaning.
Cluster-coherent and replica-local system parameters already existed and worked:
parameters declare
ParameterScope::{Cluster,Replica}, overrides live in thedurable
cluster_system_configurations/replica_system_configurationscollections, and resolution happens at plan time for
clusterand via thecompute controller's per-replica dyncfg push for
replica. They were reachableonly through LaunchDarkly. Both scoped passes in
src/adapter/src/config/frontend.rsearly-returned an empty map for the fileclient, because a flat JSON object has no key in which to express a scope.
Self-managed deployments never build a LaunchDarkly client, so the whole feature
was unavailable to them.
Why segments, not names. The file must be able to serve as a fallback for
when LaunchDarkly is down. Exact cluster and replica name targeting cannot
express what LaunchDarkly targeting expresses, so a file written that way would
silently change which objects a parameter applies to at the moment it took
over. A segment matches on the same attribute vocabulary that the LaunchDarkly
clusterandreplicacontext kinds already carry, so the two sources canexpress the same intent. Scope for this PR is attributes only: no percentage
rollouts and no bucketing. An id is just an attribute you can match on.
Description
The config sync file gains two reserved top-level keys,
segmentsandrules.Everything else about the file is unchanged.
{ "max_connections": 1000, "segments": { "legacy-replicas": { "contextKind": "replica", "clauses": [ {"attribute": "replica_size_family", "op": "in", "values": ["legacy"]}, {"attribute": "replica_name", "op": "matches", "values": ["^scratch-"], "negate": true} ] }, "analytics-cluster": { "contextKind": "cluster", "clauses": [{"attribute": "cluster_name", "op": "in", "values": ["analytics"]}] } }, "rules": [ {"segment": "analytics-cluster", "parameters": {"enable_eager_delta_joins": true}}, {"segment": "legacy-replicas", "parameters": {"enable_lgalloc": true}} ] }A segment is a named predicate selecting the clusters or replicas a rule
applies to. Its shape is LaunchDarkly's: a context kind and an array of clauses,
{attribute, op, values, negate}. Clauses are ANDed, values within a clause areORed, and
negateinverts the clause after that OR, so a negatedinover twovalues means "neither of them" rather than "not the first one".
negatedefaultsto
falsewhen absent, which is more lenient than upstream, where the field isrequired; this file is hand-authored.
Adopting LaunchDarkly's clause rather than inventing a predicate object buys three
things: the whole string operator family in one go instead of just one new
operator, a cloud-side translator that is close to a copy at the clause level, and
a file whose predicates agree with what
environmentdalready evaluates throughLaunchDarkly by construction rather than by review. A clause pasted out of the
LaunchDarkly API works as written, its REST-only
_idbeing ignored rather thanrefused.
The context kind is on the segment, not the clause. LaunchDarkly puts
contextKindon each clause. Hoisting it spares the repetition, and moreimportantly it makes the cluster-coherence rule structural: a
clustersegmentis the only kind consulted for a cluster and it cannot name a replica attribute
at all, so a cluster-coherent parameter cannot be targeted by something that
varies per replica. A per-clause
contextKindis therefore refused as an unknownclause key.
contextKindis required and must beclusterorreplica.Operators. The five string ones are supported:
in,startsWith,endsWith,containsandmatches. Every one of the seven attributes is string-valued, sothe numeric (
lessThan,lessThanOrEqual,greaterThan,greaterThanOrEqual),date (
before,after) and semantic-version (semVerEqual,semVerGreaterThan,semVerLessThan) operators could only ever evaluate false. They are recognisedand refused with that reason rather than reported as typos, which is the
difference between an author fixing the clause and an author hunting for a
misspelling that is not there.
segmentMatchis refused separately: this filealready names its segments in the
rulesarray, and a clause-level segmentreference would be a second mechanism for the same thing. An operator string that
is not LaunchDarkly's at all is refused as unknown, unlike the SDK, which maps it
to a variant that never matches.
matches. Compiled once when theConfigFileis parsed, the compiledRegexliving in the parsed structure beside the values, so evaluation never compiles;
the parse cache makes that happen once per change to the file rather than once per
one-second tick. Patterns are unanchored, which is both the
regexcrate'sdefault and what LaunchDarkly's
matchesdoes, it being literally the same crate,so a pattern means the same thing in the file and in LaunchDarkly.
^...$is howa whole-value match is asked for, and the docs say so because it is the kind of
thing that silently surprises people. A
matchesvalue must be a string: a numberor boolean there is a value rather than a pattern.
Mirrored, not imported. The SDK's
Openum and everyClausefield arepub(crate)inlaunchdarkly-server-sdk-evaluation, so these types can neitherbe imported nor constructed here.
Operatoris a hand-mirrored copy with acomment saying so, and
test_operator_vocabulary_matches_launchdarklykeeps ithonest: all fifteen LaunchDarkly operator strings are accounted for exactly once
as either supported or refused with a reason, and every supported name round-trips
through the parse. An operator added to the SDK will not fail that, which is the
limit of what is possible without the compiler; what it does catch is our own list
drifting.
Removed. The old terse form,
"legacy-replicas": {"replica_size_family": ["legacy"]}, is gone rather than kept alongside. Nothing is released yet, sothere is no compatibility to keep, and two forms would double the typo surface.
Every doc example, unit test and the end-to-end test is rewritten.
mz-adaptergains aregex.workspace = truedependency; theCargo.lockchangeis the single line adding
regexto themz-adapterentry, hand-written becausecargocannot run in this environment, andregexwas already in the lock forother crates.
A rule attaches parameters to a segment. Targeting a single cluster is a
clustersegment with oneinclause oncluster_name.Why an operator family at all. Real targeting rules select clusters and
replicas by name pattern, and a pattern over names cannot be pre-expanded into an
exact list: a cluster or replica that does not exist yet must still be targetable,
and a pattern re-applies to one created later. Operators are accepted on all seven
attributes rather than restricted to the name ones; a
startsWithonreplica_sizeis harmless even where an exact list would also work.Ordering.
rulesis an ordered array, and for each (object, parameter) thefirst matching rule wins. An array is the only shape that can carry the order:
the parse collects a JSON object into a
BTreeMap, so document order in anobject is discarded, and there is no other ordering mechanism in the file. A rule
that does not mention a parameter does not affect it, so a later rule may still
set it. The fallthrough is the environment-wide value. The win is decided before
the value is judged, so a rule that supplies a value equal to the environment-wide
value shadows the later rules for that parameter rather than falling through:
whether an override lands should not depend on a fallthrough that only a malformed
value could trigger.
nullin a rule means "no opinion", exactly as at the toplevel, and leaves the parameter to a later rule.
The coherence guard. A cluster-coherent parameter is consumed once per
cluster, at plan time, so it must resolve identically for every replica of that
cluster.
contextKindcarries most of this structurally: the cluster passconsults only
clustersegments, and aclustersegment cannot name a replicaattribute. On top of that, a rule supplying a
Cluster-scoped parameter througha
replicasegment has that parameter dropped, with a warning naming thesegment and the parameter, while any replica-local parameter in the same rule
still applies, and the dropped parameter is left for a later rule rather than
consumed. Being honest about the mechanics: the segment match already fails for
such a segment in the cluster pass, so the guard is not what produces today's
behaviour. It is in the code so the invariant does not rest on the callers pairing
the parameter scope and the context kind correctly, and its operator-facing half,
the warning, is the part that actually earns its keep. Replica-scoped parameters
may use either kind of segment.
Removed. The name-keyed
clustersandreplicassections are gone. Segmentsreplace them, and one mechanism is better than two with an interacting precedence
rule that would then have to be defined, documented and tested. This PR has not
merged, so there is no compatibility obligation.
Fail-safe segments. Anything this binary cannot evaluate makes the whole
segment match nothing, so the rules naming it do not apply: a missing or
unrecognised
contextKind, a missingclausesarray, an unknown attribute, areplica attribute in a
clustersegment, an unsupported or unknownop,valuesthat is not an array of scalars, a
negatethat is not a boolean, an invalidmatchespattern, or an unknown key in a segment or a clause. Dropping theoffending clause instead would leave the surviving ANDed clauses matching a
wider set of objects than the author wrote, and a segment whose every clause was
dropped would match everything, which is how a typo in an attribute name would
turn a one-cluster rule into an environment-wide one. A segment with an unusable
contextKindis kept and matched against nothing rather than dropped fromsegments, so the rules naming it report the real reason instead of a misleading"no segment named".
Diagnosed, not fatal. Each of the operator's realistic mistakes gets a
warning naming what to fix: each of the segment and clause defects above, by
segment name and clause position (the invalid-pattern warning carries the regex
error, collapsed onto one line so a warning stays one log line), a rule naming a
segment that does not exist, a parameter that is not scopable at all (which also
catches a misspelled parameter name), the coherence violation above, and a value
that does not parse for its parameter's type. They are reported once per change to the file, off the back of
the parsed-file cache, not once per one-second tick.
Everything else from the previous rounds stands unchanged: a whole-document read
or parse failure means "no information" and skips the scoped reconcile rather
than pruning every override; per-section leniency, so one bad segment or rule
cannot strand the rest of the file; the parsed-file cache that keeps the
synchronous read off the coordinator loop; and the shared
classify_scoped_value, so the file path records only values that differ fromthe environment-wide value in the parameter's canonical encoding (a file's
falsematches a var-formattedoffand is therefore not recorded) and neverstores an unparseable one, which would poison resolution. The per-object output
contract is untouched:
ScopedParameters, the catalog op, the durablecollections, the introspection views and both resolution boundaries are all as
they were. This is purely a change to how the file becomes per-object overrides.
Key collision. The reserved keys
segmentsandrulesshadow any systemparameter of the same name. No
VarDefinitionor dyncfg is named either, andtest_no_synced_parameter_shadows_a_reserved_sectionnow asserts that for both,so introducing one fails CI with a message telling the author to rename the
parameter rather than silently reinterpreting operators' ConfigMaps.
Docs: the "Scoping Parameters to Clusters or Replicas" section of
doc/user/content/self-managed-deployments/configuration-system-parameters.mdis rewritten for the segment and rule shape, with the attribute table, the
AND/OR rules, a "Matching by pattern" subsection covering the object form, the
OR between
inandmatches, that patterns are unanchored, and that an invalidpattern makes the segment match nothing, plus first-match-wins, the guard, the
fail-safe behaviour, and the introspection queries. The
systemParameterConfigmapNameCRD doc comment is updated in both thev1alpha1and
v1copies, and the two generatedmaterialize_crd_descriptions_*.jsonfiles are hand-patched to match byte for byte (the description field is the doc
comment verbatim) because their generator,
cargo run -p mz-cloud-resources --bin crd-writer, needs a workingcargo,which this environment does not have.
Nothing under
doc/developer/generated/is touched.Two comment-only corrections about LaunchDarkly's own ordering, since this PR is
where the question came up. The
ld_ctxcomment now says only what the lockedlaunchdarkly-server-sdk-evaluationsource says: the order contexts are added tothe multi-context does not affect evaluation, because a clause or target names its
context kind and the SDK resolves that kind by lookup (
Context::as_kind) ratherthan by an ordered scan, and precedence within a flag is individual targets, then
rules, then the fallthrough, array order winning within each.
test/launchdarkly/mzcompose.pycarried a stronger claim, that evaluation order follows the definition order of the
flag's variations. The SDK evaluates
contextTargetsin array order, so that wouldrequire LaunchDarkly's server to reorder what it serves; it is an unconfirmed
observation about the server rather than SDK behaviour, and the comment now says so.
Neither change affects any code path.
Verification
Tests updated and added. Rust unit tests in
src/adapter/src/config/frontend.rs, keeping the previous round's coverage in thenew format and adding the new cases:
renders to the expected raw string, with
nullmeaning "no opinion"); segmentsand rules parse with the reserved keys excluded from the environment-wide map,
a boolean attribute written either as
trueor as"true"normalizing to thesame value; a malformed segment or rule element is dropped while the entries
beside it survive and the surviving rules keep their position in the file, so a
diagnostic's rule number still matches what the operator wrote; a document that
is not a JSON object yields nothing.
ANDing a replica and a cluster attribute applies to neither object that matches
just one of them; a cluster-attribute segment reaches every replica of that
cluster; an unparseable value is dropped; a value matching the
environment-wide value records no override; a segment matching nothing live has
no effect.
its value agrees with the environment-wide one, so it shadows a later catch-all,
while the parameter that first rule does not mention is still decided by the
catch-all. A second cluster-scoped parameter was needed to observe both halves
in one file.
replica-discriminating rule is dropped and decided by a later rule, the
replica-local parameter in that same rule still applies, and the diagnostic
names the segment, the parameter and the attribute.
contextKind, a missingclausesarray, a per-clausecontextKind, and areplica attribute in a
clustersegment each mark their segmentuninterpretable, resolve to nothing rather than to everything, and are diagnosed
by segment name and clause position, asserted as exact strings in a
deterministic order.
attribute, missingop, missing / non-array / non-scalarvalues, non-booleannegate, unknown key) each rejected with their own reason, and LaunchDarkly's_idaccepted and ignored.cluster_name, including that values withina clause are ORed, that
matchesis unanchored, and that an emptyvalueslistsatisfies nothing. Each of the ten refused LaunchDarkly operators asserted to
produce its own reason string, and
starts_with/INrefused as unknown.negate: inverts after the OR, so a negatedinover two values excludesboth. Its absence meaning "not negated" is what every other clause in these
tests relies on, and a non-boolean
negateis refused.for exactly once as supported or refused, and every supported name round-trips
through the parse. This is the test the mirrored-not-imported comment promises.
pattern's rule holding a parameter at the environment-wide value against the
broader one; an invalid pattern resolves to nothing rather than to what its
segment's surviving clause allows, and is diagnosed.
rule naming a segment that does not exist, and an unparseable value, asserted
as exact strings and in a deterministic order.
segmentsandrules.Test updated end to end.
test/dyncfg/mzcompose.pycreates its cluster withtwo replicas, so a rule targeting one replica and a rule targeting the whole
cluster contend for the same parameter, and asserts that
r1keeps theenvironment-wide value while
r2takes the whole-cluster rule's value: ruleordering, observed through the durable collections. Because a segment now declares
one context kind, the cluster and its replicas take one segment each, which the
file spells out. The same file exercises a cluster-scoped parameter on a
replicarule (the guard), a segment whose only clause names a misspelled attribute, a
segment whose
matchespattern does not compile, a rule naming a segment that doesnot exist, a segment matching no live cluster, and an unparseable value, each with
a parameter whose leak would show up as a row that must not exist. The two segments
that are widened so a later-created cluster also matches them use
startsWithrather than a longer exact list, which is the case an exact list cannot express, so
the create-time fold is exercised through a clause authored before that cluster
existed; both clusters' rows are asserted together to pin that the clause still
matches the one the exact list named, and, for the replica half, that widening the
later rule does not take the parameter away from the narrower
scoped-r1ruleahead of it. It still asserts environment-wide keys keep
working beside the scoped sections, that a cluster created while a segment already
matches it gets its overrides folded into the create transaction, and that removing
the sections prunes every row. Its environment-wide baseline stays pinned as flat
keys in the file, which is what every scoped expectation is derived from, rather
than inherited from the parameters' compiled-in defaults, which the harness's
MZ_SYSTEM_PARAMETER_DEFAULTinverts.Ran locally
bin/fmt: ruff, ruff-dbt, black and rustfmt all pass, nothing rewritten.bufis not installed here and reports failed; no.protofile is touched.MZDEV_NO_SHELLCHECK=1 bin/lint: every check that can run passes, includingcheck-copyright.sh,check-whitespace.sh,check-rust-test-attributes.sh,check-test-flags.sh,check-python-discouraged.sh,check-mzcompose-files.sh,check-python-files.shandcheck-formatting.shapart frombuf.rustfmt --checkon both changed Rust files,python3 -m py_compileon thechanged Python, and
bin/pyactivate -m materialize.cli.mzcompose --find dyncfg list-workflowstoconfirm the composition still loads.
The clause logic, actually run.
cargocannot build this workspace here (seebelow), so
ContextKind,ScopeAttribute,Segment,Clause,Operator,unsupported_operator, both defect enums with theirDisplayimpls,Clause::parse,Segment::parse,ConfigFile::parse,Rule::parseand thehelpers were extracted verbatim, by line range out of the committed file into a
scratch lib crate depending only on
serde_jsonandregex, together with astubbed copy of the rule-resolution fold and of a
SynchronizedParametersthatpins the same environment-wide baseline the in-repo expectations use. 45 tests
pass there and
cargo clippy --all-targets -- -D warningsis clean on it,under a copy of the workspace lint table so that is meaningful. 25 of those
tests are this PR's own
mod testsbodies, byte for byte, every config-filetest in the diff, lifted in with only
#[mz_ore::test]rewritten to#[test],so the assertions that run there are the assertions CI runs. The remaining 20
are extra coverage written against the extraction: the operators and anchoring,
negateafter the OR, ANDed clauses,contextKindselecting the pass and nevercrossing kinds, every fail-closed path, and the exact rendered text of every
diagnostic. They also parse the
test/dyncfgsegments and assert the propertiesthat file's SQL assertions rest on, since the composition cannot run here.
This is how the one genuine compile error in the change was caught:
serde_json::Maphas nointo_keys, which I had used in two places.What the extraction does not cover is the surrounding frontend and the in-repo
test module, which remain uncompiled.
A script that extracts all 44 JSON literals from the new unit tests, applies
format!'s brace rules and the substitutions, and parses each one, plus the sameover every
jsonfence and embeddedsystem-params.jsonin the docs page (10)and every literal dict in the mzcompose file (16). A brace-escaping typo in a
raw format string would otherwise present as a mysterious test failure.
Could not run locally, so CI must cover it
cargo check/cargo clippy, at any scope including a single crate. Thisenvironment's egress policy refuses two third-party git submodules that Cargo
resolves before it will build anything,
duckdb/duckdbandconfluentinc/librdkafka, and there is no cached registry or target directoryto fall back on. The Rust in this PR was never compiled as part of this
workspace locally, only in CI, though the extracted scratch crate above
compiles and runs the predicate and resolution code verbatim. I re-read it by hand for imports,
lifetimes, borrow scopes and trait bounds, and in particular avoided a
let ... elsethat would have moved the value it then needs to name in thewarning, and wrote the
scopable_paramsfold as a plain loop rather than anested-closure
flat_mapover&self, but the compiler has not confirmed anyof it.
parameter's environment-wide default is
offso that a future default flipfails there rather than quietly weakening the test.
test/dyncfg, which needs a container build.bin/lintchecks fail here for missing tooling only, unrelated to thischange:
check-cargo.sh(nocargo-deny/cargo-deplint),check-helm-docs.sh,check-zizmor.sh,check-trufflehog.sh,check-protobuf.sh,check-generation.sh(needscargo), andcheck-python-docs.sh, whose 37 errors are allcargo build -p mz-sql-anonymizefailing inmz_workload_anonymize_test.py, present on thebase commit too.
Deliberately not in this PR
segmentMatchoperators are recognised and refused rather than implemented,since every scope attribute is a string. An unknown operator makes its segment
match nothing rather than being misread, so supporting one later is additive and
safe for an older binary reading a newer file.
bucketing. The clause vocabulary is adopted; the rest of LaunchDarkly's flag
model is not.
rulesstays this file's own ordered first-match-wins array.file format only; LaunchDarkly does its own matching.
parameters accept a scoped value, so an operator putting an environment-only
parameter in a rule gets a log warning and nothing more. Exposing
ParameterScopeinSHOW ALLormz_internalwould be a genuine improvementbut is a separate change.
build_replica_eval_contextsonlyyields managed replicas, since LaunchDarkly targeting needs a size and size
family. The file path inherits that, so a segment matching an unmanaged replica
has no effect. Pre-existing, and self-managed replicas are managed.
test/orchestratordSystemParamConfigMapmodification, a realKubernetes-cluster test, was left alone: extending its combinatorial matrix to
create a Materialize cluster and assert scoped rows is much heavier than the
test/dyncfgroute and would duplicate that coverage.