Scoped feature flags: resolution and gating#36959
Conversation
e2c7200 to
59a43c8
Compare
877ff6a to
cc7f88c
Compare
def-
left a comment
There was a problem hiding this comment.
Per-cluster LD feature flags in combination with a cluster-targeted optimizer flag crash:
Running cargo test -p mz-repr --lib bool_override_decodes_lenient_spellings with this diff:
diff --git a/src/repr/src/optimize.rs b/src/repr/src/optimize.rs
index 75d4a56c71..d0d3fcef05 100644
--- a/src/repr/src/optimize.rs
+++ b/src/repr/src/optimize.rs
@@ -189,3 +189,21 @@ macro_rules! impl_optimizer_feature_type {
// Implement `OptimizerFeatureType` for all types used in the
// `optimizer_feature_flags!(...)` call above.
impl_optimizer_feature_type![bool, usize];
+
+#[cfg(test)]
+mod tests {
+ use std::collections::BTreeMap;
+
+ use super::OptimizerFeatureOverrides;
+
+ #[mz_ore::test]
+ fn bool_override_decodes_lenient_spellings() {
+ for (stored, want) in [("true", true), ("false", false), ("on", true), ("off", false)] {
+ let map = BTreeMap::from([("enable_eager_delta_joins".to_string(), stored.to_string())]);
+ assert_eq!(
+ OptimizerFeatureOverrides::from(map).enable_eager_delta_joins,
+ Some(want),
+ );
+ }
+ }
+}panics:
thread 'optimize::tests::bool_override_decodes_lenient_spellings' (4146015) panicked at src/repr/src/optimize.rs:191:1:
called `Result::unwrap()` on an `Err` value: ParseBoolError
stack backtrace:
0: __rustc::rust_begin_unwind
1: core::panicking::panic_fmt
2: core::result::unwrap_failed
3: <bool as mz_repr::optimize::OptimizerFeatureType>::decode
4: <mz_repr::optimize::OptimizerFeatureOverrides as core::convert::From<alloc::collections::btree::map::BTreeMap<alloc::string::String, alloc::string::String>>>::from
5: mz_repr::optimize::tests::bool_override_decodes_lenient_spellings::test_impl
6: mz_repr::optimize::tests::bool_override_decodes_lenient_spellings
7: mz_repr::optimize::tests::bool_override_decodes_lenient_spellings::{closure#0}
8: <mz_repr::optimize::tests::bool_override_decodes_lenient_spellings::{closure#0} as core::ops::function::FnOnce<()>>::call_once
|
Good catch — fixed in |
cc7f88c to
75f521c
Compare
75f521c to
43c4d79
Compare
| let differs = match params.canonicalize(param_name, &value) { | ||
| Some(canonical) => canonical != base, | ||
| // Unparseable for this var: fall back to a raw comparison. | ||
| None => value != base, |
There was a problem hiding this comment.
There is a risk here. If LD gives us a badly typed response for a bool value (lets say maybe) canconicalize will return false maybe != off so we store maybe but then bool::decode will panic for every query plan I think. Probably better to just do None here.
43c4d79 to
7296095
Compare
ea2b4e1 to
44cd10a
Compare
…plumbing (#37079) ### Motivation First of a three-PR stack splitting #36959 (scoped feature flags) for review: **1/3 (this) → 2/3 #37080 → 3/3 #36959**. Design: doc/developer/design/20260609_scoped_feature_flags.md (#36947). This PR is the additive foundation: it introduces the vocabulary the later PRs build on, with no behavior change on its own. ### Description * `ParameterScope` (`Environment` / `Cluster` / `Replica`), declared on system vars and dyncfg `Config`s and carried through to the synced system vars. The declaration is the single source of truth for which contexts get evaluated. * The size-family taxonomy: `ReplicaAllocation::family` plus a size-map `family` field, with a `cc` / `legacy` fallback for sizes that don't set one. * The compute controller's per-replica dyncfg override layer (`update_replica_dyncfg_overrides` + per-replica command specialization), inert until the adapter wires it in 3/3. Nothing consumes the scope or the override layer yet, so this is a no-op. ### Verification `test_replica_allocation_family` covers the size→family fallback; the rest is exercised by the later PRs in the stack. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
7561342 to
d77eb1b
Compare
aljoscha
left a comment
There was a problem hiding this comment.
Reviewed the top commit only (d77eb1bc, the resolution layer), per the stack note.
Overall this is a solid, cohesive change and the correctness reasoning holds up well: the differs-from-env recording rule, the canonical bool bridging so on/off vs true/false don't register as spurious overrides, the lenient optimizer decode guarded by canonicalize so a stored value can't panic the optimizer, the create-time resolution ordered before render-freeze, and the revert-via-full-compute_config-re-push are all sound. The end-to-end LD test covers the interesting paths (differs-from-env guard, recreate-by-name, bidirectional reconcile, gating, durability, create-time). No blocking correctness issues found.
A few things to clean up, mostly around our writing/comment conventions:
Strong suggestions
-
Em-dashes and semicolons in comments. Our style is to avoid em-dashes and sentence-structuring semicolons everywhere, including code comments. This commit adds ~22 lines with em-dashes and a similar number of structuring semicolons (heaviest in
coord.rsandconfig/frontend.rs). Please do a pass replacing them with full stops and commas. Inline suggestions below mark representative spots. -
Doc comments that narrate / duplicate the body.
evaluate_scoped_overridesis the clearest case: the differs-from-env-vs-variation_detailrationale is spelled out almost verbatim in both the doc comment and the inline comment at the decision point. Our convention is for the doc comment to state the contract and for the reasoning to live inline at the decision point. Trim the doc comment (suggestion inline). -
scoped_ld_ctxis a dead public wrapper. It only forwards to the privateld_ctx, and the real callers (pull_cluster_overrides/pull_replica_overrides) already callld_ctxdirectly. Its only users are the unit tests, which live in the same module and can callld_ctxtoo. Suggest dropping both the wrapper and itspub usere-export.
Nits
push_replica_dyncfg_overrides: the "compute push reaches storage via the shared persistConfigSet" subtlety is genuinely worth recording, so keep it, but the doc comment is long and could be tightened (and is part of the em-dash pass).- Consider a unit-level test for the
evaluate_scoped_overridesdiffers-from-env decision itself. Thecanonicalizeanddecodeunits are good, but the core recording decision is currently only exercised by the live-LD mzcompose workflow. Not blocking given the LD client dependency.
| /// Evaluates each of `param_names` against `ctx`, returning only the values | ||
| /// that *differ* from the environment-wide value held in `params`. Shared by | ||
| /// the cluster and replica passes; the returned map is therefore sparse. | ||
| /// | ||
| /// The difference — not the `variation_detail` reason — is the signal that | ||
| /// the scope context produced a genuine opinion. The reason cannot say which | ||
| /// context kind's clause matched (an env-level rollout rule and a | ||
| /// cluster-specific rule both report `RULE_MATCH`), and `FALLTHROUGH` serves | ||
| /// the env-wide value to every object; either would record a row for every | ||
| /// object (dense) and let a coarse env-wide value override a finer manual | ||
| /// `CREATE CLUSTER ... FEATURES` pin. Comparing against the env-wide baseline | ||
| /// captures exactly "this scope context changed the answer". A silent LD | ||
| /// (flag absent / off / error / failed prerequisite) resolves to the `base` | ||
| /// default and is dropped. See the scoped feature flags design, §Resolution. |
There was a problem hiding this comment.
The differs-from-env rationale here is duplicated almost verbatim by the inline // Record iff ... comment at the recording decision below. Per our convention the doc comment states the contract and the reasoning lives inline at the decision point, so this paragraph can go. (It also carries several em-dashes/semicolons.) Suggested trim:
| /// Evaluates each of `param_names` against `ctx`, returning only the values | |
| /// that *differ* from the environment-wide value held in `params`. Shared by | |
| /// the cluster and replica passes; the returned map is therefore sparse. | |
| /// | |
| /// The difference — not the `variation_detail` reason — is the signal that | |
| /// the scope context produced a genuine opinion. The reason cannot say which | |
| /// context kind's clause matched (an env-level rollout rule and a | |
| /// cluster-specific rule both report `RULE_MATCH`), and `FALLTHROUGH` serves | |
| /// the env-wide value to every object; either would record a row for every | |
| /// object (dense) and let a coarse env-wide value override a finer manual | |
| /// `CREATE CLUSTER ... FEATURES` pin. Comparing against the env-wide baseline | |
| /// captures exactly "this scope context changed the answer". A silent LD | |
| /// (flag absent / off / error / failed prerequisite) resolves to the `base` | |
| /// default and is dropped. See the scoped feature flags design, §Resolution. | |
| /// Evaluates each of `param_names` against `ctx`, returning only the values | |
| /// that differ from the environment-wide value held in `params`. Shared by | |
| /// the cluster and replica passes, so the returned map is sparse. | |
| /// | |
| /// We record on the differs-from-env test, not the `variation_detail` | |
| /// reason. The inline comment at the recording decision explains why. |
There was a problem hiding this comment.
Applied your suggested trim — the doc now states the contract and points at the inline comment for the differs-from-env reasoning.
There was a problem hiding this comment.
Confirmed. The doc now states the contract and defers to the inline comment for the reasoning. Thanks.
| /// | ||
| /// The environment-wide pass passes `None` for both. This is the single entry | ||
| /// point the sync loop uses to evaluate each scoped pass. | ||
| pub fn scoped_ld_ctx( |
There was a problem hiding this comment.
scoped_ld_ctx only forwards to the private ld_ctx, and the real callers (pull_cluster_overrides / pull_replica_overrides) already call ld_ctx directly. Its only users are the unit tests in this module, which can call ld_ctx directly too. Suggest dropping this wrapper and the pub use frontend::{... scoped_ld_ctx} re-export in config.rs.
There was a problem hiding this comment.
Dropped the scoped_ld_ctx wrapper and its config.rs re-export; the unit tests call the private ld_ctx directly (same module), and its doc moved onto ld_ctx. Real callers were already on ld_ctx.
There was a problem hiding this comment.
Confirmed. The wrapper and the re-export are gone, the tests call ld_ctx directly, and the stray [scoped_ld_ctx] doc reference was updated too. Thanks.
| /// Resolves the replica-local scoped overrides from the catalog working copy | ||
| /// into the compute controller's per-replica dyncfg layer, then re-pushes | ||
| /// the environment-wide compute configuration so existing replicas observe | ||
| /// the new values. Called after reconcile and on bootstrap. | ||
| /// | ||
| /// Only the *compute* controller's per-replica dyncfg layer is pushed here, | ||
| /// but on `clusterd` that also reaches storage for the configs that matter. | ||
| /// Compute and storage share one process, and the compute worker's | ||
| /// `handle_update_configuration` applies the pushed dyncfg updates both to | ||
| /// compute's own worker `ConfigSet` *and* to the shared persist client | ||
| /// `ConfigSet` (`persist_clients.cfg()`), which the co-located storage server | ||
| /// reads from the same `Arc`. So persist-backed and process-global | ||
| /// replica-local configs — the design's examples: the persist pager, LZ4, | ||
| /// persist client tuning, `lgalloc` — take effect on storage as well, even | ||
| /// though the push goes through the compute controller. The only thing this | ||
| /// would miss is a future `Replica`-scoped config realized *solely* in the | ||
| /// storage worker's own `ConfigSet` (applied only via the storage | ||
| /// controller's `UpdateConfiguration`); none exists today. |
There was a problem hiding this comment.
nit: the "compute push reaches storage via the shared persist ConfigSet" subtlety is worth recording, so keep it, but this doc comment is long and stacks several em-dashes/semicolons. Consider tightening it and folding the example list into prose as part of the style pass.
There was a problem hiding this comment.
Tightened: folded the example list into prose, cut the em-dashes/semicolons, kept the shared-persist-ConfigSet storage-reach subtlety.
There was a problem hiding this comment.
Confirmed. Reads well now and keeps the shared-persist-ConfigSet storage-reach subtlety. Thanks.
d77eb1b to
c34249e
Compare
|
Re-reviewed the responses. The three inline fixes all look good, including the follow-on cleanup of the stray One item from the review summary is still open: the broader em-dash and structuring-semicolon pass across the new comments. The |
c34249e to
6f6a849
Compare
|
Done the em-dash and structuring-semicolon pass over the new comments. Removed the em-dashes (reworded into separate sentences or commas) and the structuring semicolons (split into sentences) across On the non-blocking suggestion: I agree a direct unit test of the differs-from-env decision would be nice, but |
|
Did a focused performance pass on the hot paths and continual triggers. Summary: the read/plan path is fine, the costs are all on the write side and only when the feature is enabled. Plan / peek path: no concern.
That early-out also removes the common case of a subtle transient: replica create-time overrides are not written back to the catalog working copy (in-memory/controller only, by design), so Continual trigger: per-tick O(fleet) evaluation when enabled. With the feature on, Minor: Net: nothing on the read path, and the write-path costs are gated off by default. The two worth acting on are the discarded cluster evaluation and the unconditional full compute re-push on replica creation. |
6f6a849 to
fc13ba4
Compare
|
Thanks for the perf pass. Acted on the two write-path items in
On the rest, agreed and no change:
|
|
Follow-up on the per-tick note, on whether this is new work or just more of what the sync loop already did. It is net-new, and it runs unconditionally, so worth calling out explicitly. Before this feature, the sync loop did two things per tick: This PR adds, on every tick:
So this is not the env-wide pass getting a bit bigger. The object factor is entirely additional. And unlike the env-wide pass, which gates its expensive work on "did anything change", the scoped pass runs the full fleet evaluation every tick regardless. The coordinator early-outs on the equality check in It is on the dedicated sync-loop task so it does not block the coordinator, and it is gated off by default. But it is continual extra work proportional to fleet size, so worth a deliberate look before this goes on by default, especially for large environments. A cheap guard would be to skip the pass when neither the set of live objects nor the relevant LD state has changed since the last tick, though detecting the latter is the hard part. |
fc13ba4 to
210c04f
Compare
|
Agreed, the object factor is net-new and the scoped pass runs every tick regardless of change. Two parts here: The off-path should not regress, since a patch release to undo it would be painful. Fixed in 210c04f: the scoped reconcile now reads the gate from the sync working copy ( The on-path full-fleet evaluation is the part worth optimizing, and since this will be on by default we will want it. I would rather do that in a follow-up than widen this PR: a cheap object-set/env-change guard is unsafe on its own (a scoped LD rule can change with neither the live-object set nor the env-wide context changing, which would silently freeze scoped overrides), so the real fix is either a dedicated slower scoped tick or hooking LD streaming changes. Tracking that separately. |
210c04f to
d3df8f0
Compare
edb3bb6 to
d75b222
Compare
d75b222 to
db631b9
Compare
#37151) ### Motivation Scoped feature flags need the per-replica controller push to be *derived from the committed catalog diff* rather than issued as a side effect in the sequencer. Deriving it removes the working-copy/controller divergence that lets a create-time replica override be wiped by a reconcile in the same DDL, and it makes the push fire for a follower `environmentd` that only replays the catalog diff. This is the pattern the catalog-implications framework (#29673, `TODO(aljoscha)`) exists to provide, which until now ignored config-type changes. ### Description `parse_state_update` no longer drops `ReplicaSystemConfiguration` updates; it emits a `ParsedStateUpdateKind::ReplicaSystemConfiguration`. When such an update is present in a transaction, `apply_catalog_implications` rebuilds the complete per-replica compute dyncfg layer from the catalog working copy and pushes it after clusters are created and before replicas are created, so a new replica's first configuration replays with its override. The same push runs once on bootstrap from the restored working copy. Because the push reads the working copy that the same transaction's catalog apply has already updated, the diff drives the push. The push mechanism (`push_replica_dyncfg_overrides`) lives here. Cluster-scoped overrides are read at plan time and have no controller effect, so they remain unparsed. This is the foundation under #36959, which adds the writers that drive the implication; the implication is dormant until that PR lands on top. ### Verification Adds a unit test asserting a `ReplicaSystemConfiguration` update is parsed rather than dropped. End-to-end controller behavior is covered by the LaunchDarkly mzcompose test in #36959. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FUorxx5m6B8TT2VAek2STo Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96a390d to
d790e90
Compare
aljoscha
left a comment
There was a problem hiding this comment.
Reviewed the resolution and gating layer locally against main. Overall the design is solid and the comments are unusually good. One blocking correctness item from the new prune_scope rework, plus a couple of suggestions. Main item is inline on catalog/transact.rs, the rest is below.
Blocking
See the inline comment on src/adapter/src/catalog/transact.rs: dropped clusters and replicas leak their scoped-config rows, and as a knock-on the periodic sync stops converging. Details and a suggested fix are there.
Strong suggestions
- No Rust unit test covers the
UpdateScopedSystemParametersdiff and prune semantics. The only coverage is thetest/launchdarklymzcompose workflow, which needs live credentials and is not in standard PR CI. A catalog-level unit test exercising upsert, remove,prune_scopebounding, and the dropped-object case would have caught the blocking item and guards this boundary going forward. The other new logic (canonicalize, lenient decode, context building) is unit tested, which is great. - The cluster-scoped optimizer override is wired in ad hoc at around a dozen plan-path call sites via
.override_from(&self.cluster_scoped_optimizer_overrides(cluster_id))(peek, index, MV, subscribe, introspection, bootstrap, frontend_peek). It looks complete today, but a future plan path that builds a per-clusterOptimizerConfigwill silently miss the scoped layer. Consider a single helper that assembles the per-cluster features (system config plusconfig.features()plus scoped overrides) so the precedence lives in one place. Partly pre-existing, sinceconfig.features()is already repeated, but this PR roughly doubles the override points.
Nits
bool::decodeinsrc/repr/src/optimize.rsstill panics on truly invalid input, even though the doc says decoding must not crash the optimizer. It is effectively unreachable, sincecanonicalizegates what gets stored and theFEATURESclause is bool typed, so it is a defensive assertion rather than a real hazard. The wording just oversells the guarantee.- Replica create-time resolution is not done here. Only the cluster paths call
resolve_scoped_for_new_objects, always with an empty replica set, so a freshly created replica's render-frozen flags use env-wide values until the next sync tick. The description documents this as deferred to #37158. Fine given the gate, just calling it out.
What is good
- The differs-from-env recording rule with canonical bool bridging is the right call and is well explained and tested.
- The gating works as intended: the loop reads the gate from
params, andbackend.pullseedsparamsfrom the catalog system vars every tick, soALTER SYSTEM SETandadditional_system_parameter_defaultsare observed even though the gate is not an LD flag. - The
prune_scopeprotection for concurrently created objects is sound, and the create-timemergecorrectly preserves existing overrides. - Create-time resolution does a local in-memory LD evaluation only, with no network round-trip on the coordinator loop, so it is cheap.
| if prune_cluster(&cluster_id) | ||
| && !desired_cluster.contains(&(cluster_id, name.clone())) | ||
| { | ||
| tx.remove_cluster_system_config(cluster_id, &name); |
There was a problem hiding this comment.
Blocking: this prune_scope change drops the lazy GC of orphaned rows for dropped objects, with a few knock-on effects.
The sync loop builds prune_scope from catalog.clusters(), so it only ever contains live objects. A dropped cluster or replica is never in prune_scope, so prune_cluster / prune_replica returns false for it and its rows are never removed. Nothing else GCs these rows either: remove_clusters / remove_cluster_replicas cascade only to cluster_replicas and introspection_sources, and the only callers of remove_*_system_config are right here. The previous code pruned any row not in the desired state, which lazily covered dropped objects (the old comment said exactly that).
Consequences:
- Durable and introspection leak.
mz_cluster_system_parameters/mz_replica_system_parametersare unfiltered projections of these collections, so orphan rows with a danglingcluster_id/replica_idaccumulate until the feature is toggled off (the only full wipe). - The per-tick early return is defeated.
reconcile_scoped_system_parametersskips the durable write whenworking_copy == scoped, but the sync loop'sscopedis live-only while the working copy retains orphans, so they can never be equal once an orphan exists. The coordinator then runs acatalog_transactevery tick that can never converge. - The recreate-then-clear case in
test/launchdarkly/mzcompose.pyshould fail: after DROP plus CREATEld_role, clearing the rules and assertingcount(*) ... = 0will see the orphan row from the dropped incarnation. I could not run it locally since it needs a live LD project.
Suggested fix, local to this handler: also prune rows whose owning object is not live. live_clusters / live_replicas are already computed just above, so something like (!live_clusters.contains(&cluster_id) || prune_cluster(&cluster_id)) && !desired_cluster.contains(...) (and the replica analogue). That restores dropped-object GC while keeping the concurrent-create protection that prune_scope was added for. Orphan pruning is always safe since ids are never reused.
| c.testdrive( | ||
| "\n".join( | ||
| [ | ||
| f"> SELECT count(*) FROM mz_internal.mz_cluster_system_parameters WHERE name = '{OPTIMIZER_PARAM}'", |
There was a problem hiding this comment.
This count(*) = 0 assertion is where the orphan-GC regression surfaces. After the DROP plus CREATE of ld_role above, the dropped incarnation's row is not pruned by the reconcile, since its id is no longer in prune_scope, so this count should be 1, not 0. See the comment on src/adapter/src/catalog/transact.rs. Worth re-running against the live LD project once the prune fix is in.
Replica ids were allocated in-apply by insert_cluster_replica, reading the IdAlloc counter from the snapshot the coordinator opened the transaction with. Cluster and item ids are instead allocated out-of-band by the durable allocator before the transaction, and the op carries the explicit id. That asymmetry breaks any caller that pre-allocates a replica id out-of-band: the coordinator does not apply IdAlloc updates to its in-memory catalog, so a later in-apply allocation reuses the id and a DuplicateReplica panic surfaces on the next bootstrap. Make replica ids durable-allocated and single-source like cluster ids. Op::CreateClusterReplica now carries a required replica_id; the apply path always uses insert_cluster_replica_with_id and never allocates. The runtime construction sites pre-allocate via the durable allocator, choosing user or system ids from the owning cluster. The catalog-open builtin-replica migration keeps a transaction-level system allocation, which is single-source inside the open transaction. The in-apply insert_cluster_replica and the unused transaction-level user allocator are removed. DB-147 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUorxx5m6B8TT2VAek2STo
…tion The resolution layer that consumes the durable scoped overrides, behind the `enable_scoped_system_parameters` dyncfg (off by default). * The sync loop evaluates the `cluster` and `replica` LaunchDarkly contexts and records an override only when the scoped value differs from the environment- wide value, comparing in canonical encoding so bool spellings match. The whole evaluation is skipped while the gate is off, and the working copy cleared. * Resolution boundaries: cluster-coherent overrides feed plan-time `OptimizerFeatureOverrides` (with a lenient bool decode so `on`/`off` cannot panic the optimizer); replica-local overrides feed the compute controller's per-replica dyncfg push. * Create-time evaluation resolves a freshly created cluster or replica through a shared `SystemParameterFrontend` so new objects get their overrides without waiting for the next sync tick. * Tests: canonical bool bridging, lenient optimizer decode, frontend context construction, the LaunchDarkly end-to-end cases, and workload-harness registration of the new gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d790e90 to
926f501
Compare
|
Thanks for the quick turnaround. The rebased revision (926f501) resolves the blocking item: the prune loop now reclaims orphaned rows for dropped objects ( Remaining items, all non-blocking:
LGTM once you are happy with those. Nice work on the orphan-prune test in particular. |
Motivation
The resolution layer for scoped feature flags. The foundation (#37079), the
durable persistence and introspection (#37080), and the catalog-implications
foundation (#37151, which derives the replica-scoped controller push from the
committed catalog diff) have all merged. Create-time render-frozen correctness
is completed in the follow-up #37158. Design:
doc/developer/design/20260609_scoped_feature_flags.md (#36947).
Behind the
enable_scoped_system_parametersdyncfg (off by default), this PRconsumes the durable scoped overrides and resolves them at the per-scope
boundaries.
Description
cluster(replica-free) andreplica(size, sizefamily, owning cluster) LaunchDarkly contexts and records an override only when
the scoped value differs from the environment-wide value, compared in
canonical encoding so bool spellings (
on/offvstrue/false) match. LDmulti-context evaluation cannot reveal which context produced a value, so the
differs-from-env test, not the
variation_detailreason, is the recordingrule. The evaluation is skipped while the gate is off, and the gate is read
from the sync working copy so a disabled environment takes no catalog snapshot
per tick.
OptimizerFeatureOverrides(with a lenient bool decode soon/offcannotpanic the optimizer). Replica-local overrides reach the compute controller
through the catalog implication from adapter: derive replica-scoped override push from catalog implications #37151.
into the working copy so it need not wait for the next sync tick. The follow-up
Scoped feature flags: create-time evaluation #37158 moves this into the create transaction so render-frozen replica flags are
set before
create_replica.Verification
Canonical bool bridging, lenient optimizer decode, and frontend context unit
tests, plus the
test/launchdarklymzcompose workflow extended withcluster/replica/durability/gating/create-time cases, validated end-to-end
against a live LaunchDarkly project.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FUorxx5m6B8TT2VAek2STo