Skip to content

feat: Add the override marker to the flag and segment models and mark evaluations - #363

Merged
kinyoklion merged 2 commits into
dotnet-server-sdk-v9from
rlamb/overrides-dotnet-model-evaluator
Oct 28, 2026
Merged

kinyoklion merged 2 commits into
dotnet-server-sdk-v9from
rlamb/overrides-dotnet-model-evaluator

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds the override marker to the flag and segment models and makes the evaluator mark evaluations as override-affected, as defined by the OVERRIDE specification.

FeatureFlag and Segment gain an internal IsOverride property and an AsOverride() method that returns a marked copy sharing the immutable parts of the original. The marker lives on the model only: the JSON converters do not write it, so a marked definition serializes exactly like an unmarked one, and the original object handed to AsOverride() is never modified.

The evaluator marks an evaluation when any definition it read carried the marker: the evaluated flag, a prerequisite flag at any depth, or a segment consulted during clause matching, whether or not the segment matched. The marking propagates upward only. Each prerequisite evaluation is a scope of its own that starts from the prerequisite's marker, so the record for a prerequisite reflects only its own subtree, while the parent scope absorbs the nested marking when the nested evaluation returns or throws. Error results are marked too: a malformed override flag, a prerequisite cycle through an override flag, and an invalid context all produce an error reason that carries the indicator. A definition that cannot be resolved contributes nothing. The marking is reported through the reason's OverrideAffected indicator from the previous change in this series.

Nothing in this change produces marked definitions yet; the override store that does so is the next change. Every existing evaluation therefore behaves as before, which the existing evaluator tests confirm. The new tests cover each marking case, including the prerequisite tree in the specification where only one leaf is overridden.

This PR is based on the branch of the EvaluationReason indicator change, which it depends on.

SDK-3248


Note

Overview
Adds an internal override marker on FeatureFlag and Segment (IsOverride / AsOverride()). The marker is model-only and does not change serialized flag/segment JSON.

The evaluator tracks whether the current evaluation read any marked definition and sets EvaluationReason.OverrideAffected on the final result (and on prerequisite sub-records for their own scope). Marking applies when the evaluated flag is overridden, when a prerequisite subtree reads an override (nested scopes merge upward), or when a segment is loaded for a segment-match clause—even if the segment does not match or the clause is negated. Error paths (invalid context, malformed flag, exceptions) are marked when the evaluated flag is an override. Missing flags/segments that are never read do not mark.

Extensive new tests cover marking rules; existing evaluations stay unmarked until something supplies AsOverride() (override store is follow-up work).

Reviewed by Cursor Bugbot for commit 7e64644. Bugbot is set up for automated code reviews on this repo. Configure here.

@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from e4dfcf4 to 74be7f9 Compare September 28, 2026 20:39
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from aff3a67 to ae86267 Compare September 28, 2026 20:39
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from 74be7f9 to 93c9160 Compare October 1, 2026 23:43
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from ae86267 to 533647f Compare October 1, 2026 23:43
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from 93c9160 to 3996906 Compare October 3, 2026 01:34
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from 533647f to 511de7f Compare October 3, 2026 01:34
@kinyoklion

Copy link
Copy Markdown
Member Author

bugbot review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 511de7f. Configure here.

@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from 3996906 to 285612d Compare October 3, 2026 01:52
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from 511de7f to 33e2973 Compare October 3, 2026 01:52
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from 285612d to 5200c06 Compare October 5, 2026 21:55
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from 33e2973 to 22c3c01 Compare October 5, 2026 21:55
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from 5200c06 to 4038dce Compare October 5, 2026 22:12
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from 22c3c01 to 426dbe4 Compare October 5, 2026 22:12
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from 4038dce to dca375c Compare October 5, 2026 22:28
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from 426dbe4 to 167a54a Compare October 5, 2026 22:28
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from dca375c to 6b919c2 Compare October 5, 2026 23:56
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from 167a54a to 6894b3d Compare October 5, 2026 23:56
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from 6b919c2 to 8ef07c2 Compare October 13, 2026 21:03
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from 6894b3d to f0fca10 Compare October 13, 2026 21:03
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from 8ef07c2 to e8bc083 Compare October 13, 2026 22:13
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from f0fca10 to f083d90 Compare October 13, 2026 22:13
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from e8bc083 to b35db17 Compare October 23, 2026 23:49
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from f083d90 to c3adb5b Compare October 23, 2026 23:50
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from b35db17 to d7b86e2 Compare October 26, 2026 22:46
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from c3adb5b to ff43cdf Compare October 26, 2026 22:46
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from d7b86e2 to bae5720 Compare October 26, 2026 23:13
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from ff43cdf to bcbfead Compare October 26, 2026 23:13
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from bae5720 to 5bafb8e Compare October 27, 2026 20:23
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from bcbfead to 3c03873 Compare October 27, 2026 20:23
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-reason-marker branch from 5bafb8e to f1da988 Compare October 27, 2026 20:46
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from 3c03873 to 4456477 Compare October 27, 2026 20:46
@kinyoklion
kinyoklion marked this pull request as ready for review October 27, 2026 20:57
@kinyoklion
kinyoklion requested a review from a team as a code owner October 27, 2026 20:57
Base automatically changed from rlamb/overrides-dotnet-reason-marker to dotnet-server-sdk-v9 October 28, 2026 20:35
… evaluations

FeatureFlag and Segment gain an internal IsOverride marker and an
AsOverride method that returns a marked copy sharing the immutable parts
of the original. The marker lives on the model only and is never
serialized.

The evaluator marks an evaluation as override-affected when any
definition it read carried the marker: the evaluated flag, a prerequisite
at any depth, or a segment consulted during matching, whether or not the
segment matched. The marking propagates upward only: a prerequisite
record reflects only the definitions its own subtree read. Error results
are marked too. The result is reported through the evaluation reason's
OverrideAffected indicator, as defined by the OVERRIDE specification.
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from 4456477 to 7e64644 Compare October 28, 2026 20:38
The override marker on the evaluation reason is released in CommonSdk 7.3.0-alpha from this
branch. The server's Debug and Release builds use the package, so they reference that version.
@kinyoklion
kinyoklion merged commit 21f666a into dotnet-server-sdk-v9 Oct 28, 2026
8 of 9 checks passed
@kinyoklion
kinyoklion deleted the rlamb/overrides-dotnet-model-evaluator branch October 28, 2026 21:19
kinyoklion added a commit that referenced this pull request Oct 29, 2026
…stem wiring (#364)

<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

Adds the override store, the overlay at the store read boundary, the
override source interfaces, and the data system wiring for flag
overrides, as defined by the OVERRIDE specification. With this change an
application can configure an override source on the FDv2 data system and
have its flags and segments take precedence over LaunchDarkly data at
evaluation time.

Configuration surface:
`DataSystemBuilder.Overrides(IComponentConfigurer<IOverrideSource>)` and
`DataSystemConfiguration.OverrideSource`. At most one source is
accepted. `IOverrideSource` (`Start(IOverrideSink)`, `IDisposable`) and
`IOverrideSink.SetOverrides(FullDataSet<ItemDescriptor>)` follow the
shape of the data source interfaces, so an application can supply a
custom source the same way it supplies a custom data source. The SDK
builds the source when the client is created, starts it before the data
sources so that its initial load completes inside the client
constructor, and disposes it with the client. An exception from the
source's `Dispose` is logged and the rest of the client still shuts
down, and a snapshot that the source supplies during shutdown is
ignored. A configuration error in the source propagates from the
constructor like any other invalid component configuration. An exception
from the source's `Start` does not: the SDK logs it, disposes the
source, removes any snapshot the source supplied before failing, and the
client runs without overrides, because a client in an incident is better
off without its overrides than not starting. The SDK's own file-based
source never throws from `Start`; it logs a failed load and retries. The
source is not built when the client is offline, so no overrides are
served and a configuration error in it is not reported; the SDK logs a
warning saying so. Without a configured source the SDK is unchanged.

The override layer (`Internal/Overrides`): `OverrideLayer` holds the
entries as an immutable map swapped on every snapshot, marking copies of
the supplied definitions so the source's own objects are never modified.
`OverrideOverlay` implements the data system's read-only store: a read
returns the override entry when one exists and the LaunchDarkly entry
otherwise, an enumeration returns the union with override entries
winning (a failure of the base store propagates, as it does without the
overlay), and initialization status still comes from the base store.
`OverrideSink` applies snapshots one at a time and computes the flags an
update may have changed, including dependency fan-out through
prerequisites and segments over both the old and the new merged views,
then sends the same flag change notifications a LaunchDarkly change
would.

Client behavior: the not-initialized short-circuit consults the override
layer first, so an overridden flag is served before the client has
LaunchDarkly data while a flag the layer does not hold takes exactly the
path it took before this feature. An evaluation that fails with an
unexpected exception keeps the marking when the flag came from the
layer. A malformed override definition is logged and skipped during
change computation so the other notifications still fire, a source whose
Build throws leaves nothing started, and a source whose Start throws is
disposed while the client runs without overrides. `AllFlagsState` reads
through the overlay, so it includes override-only flags. When neither
the client nor the store is initialized and the layer holds flags, it
returns a state of those flags alone (with a warning logged once), read
from the layer rather than the merged store so that LaunchDarkly data in
a store without an init marker stays out; with no override flags the
state is invalid, as it is without a source. Without a source this state
is empty, because the SDK reports no flags before the store is
initialized, so the override flags only add to what it reports;
overrides never remove a flag from the state. When the store fails to
list its flags, the failure is logged and the state holds the override
layer's flags and is invalid: without a source that state is invalid and
empty, and a client-side SDK bootstrapped from the state uses its flags
whatever its validity says, so the overridden values still reach it. A
flag whose evaluation is override-affected keeps its value, version, and
marked reason in the state but has `trackEvents` and `trackReason` off
and no `debugEventsUntilDate`, because a consumer of the state must not
send individual events for it. A wrong-type result keeps the marking on
its reason, including the reason `MigrationVariation` reports for an
unrecognized stage.

The specification's test vectors run as a unit test against the full
client stack (value, variation index, and reason; the summary-counter
contribution is asserted once the events change lands). Unit tests cover
the layer, overlay, sink, the builder, and the client behaviors above.

It depends on the model and evaluator marking change, #363, which is
merged into `dotnet-server-sdk-v9`.

[SDK-3248](https://launchdarkly.atlassian.net/browse/SDK-3248)


[SDK-3248]:
https://launchdarkly.atlassian.net/browse/SDK-3248?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

## Follow-up from review

A LaunchDarkly change now notifies the flags whose overrides depend on
it. The override layer keeps the dependency edges of its own entries,
rebuilt on every replacement, and `DataSourceUpdatesImpl` expands each
changed LaunchDarkly item through both its own tracker and the layer's
until nothing new is reached, so an override that matches a LaunchDarkly
segment or has a LaunchDarkly prerequisite is notified like the
LaunchDarkly flags that depend on it. An entry whose references cannot
be read contributes no edges, the same as the change computation skips
it.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Adds **experimental flag overrides** for the FDv2 data system:
`DataSystemBuilder.Overrides()` configures an optional `IOverrideSource`
that pushes full snapshots through `IOverrideSink`, without affecting
initialization or data-source status.
> 
> When a source is configured, the SDK builds an **override layer** and
wraps the read-only store with **`OverrideOverlay`** so evaluations,
prerequisites, segments, and `AllFlagsState` see override entries before
LaunchDarkly data. **`OverrideSink`** replaces the layer atomically and
notifies flag listeners using the same path as LaunchDarkly updates,
including dependency fan-out via **`OverrideChanges`** and
**`IExternalDependencies`** on **`DataSourceUpdatesImpl`** (LaunchDarkly
changes can now reach overridden flags that reference them).
> 
> **`LdClient`** is updated so overridden flags can be served before the
client initializes; **`AllFlagsState`** can return override-only flags
(with one-time warnings), fall back to overrides when `GetAll` fails,
and disable per-flag event tracking when **`overrideAffected`** is set.
Start/dispose failures on the source are logged and the client continues
without overrides; offline mode skips starting the source.
**`FDv1DataSystem`** exposes empty override APIs for parity. Extensive
unit tests and **`override-vectors.json`** cover the stack.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
58ef4ac. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=projects -->
<sub>Opened from termag</sub>
kinyoklion pushed a commit that referenced this pull request Oct 29, 2026
…ha.1 (#382)

🤖 I have created a release *beep* *boop*
---


##
[9.0.0-alpha.1](LaunchDarkly.ServerSdk-v9.0.0-alpha.0...LaunchDarkly.ServerSdk-v9.0.0-alpha.1)
(2026-10-29)


### Features

* Add a shared file data reloader, poller, and watcher
([#361](#361))
([4eed241](4eed241))
* Add the file-based override source
([#367](#367))
([b834131](b834131))
* Add the override marker to the flag and segment models and mark
evaluations
([#363](#363))
([21f666a](21f666a))
* Add the override store, overlay, source interfaces, and data system
wiring ([#364](#364))
([e1748a1](e1748a1))
* Mark evaluation events as override-affected
([#366](#366))
([7c873a0](7c873a0))
* Split summary counters by overrideAffected and suppress individual
events for marked evaluations
([#365](#365))
([c9b327b](c9b327b))
* Support the flag-overrides capability in the contract test service
([#368](#368))
([b80d6aa](b80d6aa))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> This is a **Release Please** cut that publishes
**LaunchDarkly.ServerSdk `9.0.0-alpha.1`**. The diff only updates
release metadata: `.release-please-manifest.json`,
`LaunchDarkly.ServerSdk.csproj` `<Version>`, `PROVENANCE.md`
verification example, and a new **`9.0.0-alpha.1`** section in
`CHANGELOG.md`.
> 
> The changelog entry documents what ships in this alpha (already merged
elsewhere): **local flag overrides** via override store/overlay/source
wiring and a **file-based override source** with shared file
reloader/poller/watcher; **override markers** on flag/segment models and
evaluations; **analytics** changes that mark override-affected
evaluations, split summary counters by `overrideAffected`, and suppress
individual events when appropriate; plus **contract-test** support for
the flag-overrides capability.
> 
> Reviewers can treat this as a **versioning and release-notes sanity
check**—no application logic changes appear in the diff itself.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
81f0588. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants