Skip to content

feat: Add the override store, overlay, source interfaces, and data system wiring - #364

Merged
kinyoklion merged 12 commits into
dotnet-server-sdk-v9from
rlamb/overrides-dotnet-override-layer
Oct 29, 2026
Merged

kinyoklion merged 12 commits into
dotnet-server-sdk-v9from
rlamb/overrides-dotnet-override-layer

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

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

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.


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.

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

Opened from termag

@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-override-layer branch from 49ba8e2 to d9e0211 Compare September 28, 2026 20:39
@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-override-layer branch from d9e0211 to 092ed5c Compare October 1, 2026 23:43
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-model-evaluator branch from 533647f to 511de7f Compare October 3, 2026 01:34
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-dotnet-override-layer branch from 092ed5c to 716b005 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.

Stale Bugbot comment from a previous run.

@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-override-layer branch from 716b005 to 041e7a5 Compare October 3, 2026 01:52
@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-override-layer branch from 041e7a5 to 91ecadb Compare October 5, 2026 21:55
@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-override-layer branch from 91ecadb to 5d664c1 Compare October 5, 2026 22:12
@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-override-layer branch from 5d664c1 to c875758 Compare October 5, 2026 22:28
@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-override-layer branch from c875758 to 8ad94ca Compare October 5, 2026 23:56
@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-override-layer branch from 8ad94ca to b47e5ff Compare October 13, 2026 21:03
@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-override-layer branch from b47e5ff to 0864b85 Compare October 13, 2026 22:13
@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.

Stale Bugbot comment from a previous run.

@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-override-layer branch from 625311a to 0d26639 Compare October 23, 2026 23:50
@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-override-layer branch from 0d26639 to c1ce57c Compare October 26, 2026 22:46
@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-override-layer branch from c1ce57c to c93aba9 Compare October 26, 2026 23:13
Base automatically changed from rlamb/overrides-dotnet-model-evaluator to dotnet-server-sdk-v9 October 28, 2026 21:19
…stem wiring

Adds IOverrideSource and IOverrideSink, the DataSystemBuilder.Overrides
option, and the override layer: an immutable store of marked entries, an
overlay at the store read boundary that returns the override entry for a
key in preference to LaunchDarkly data, and a sink that applies snapshots
one at a time and sends flag change notifications for the flags an update
may have changed, with dependency fan-out through prerequisites and
segments.

The FDv2 data system builds the configured source, starts it before the
data sources so its initial load completes inside the client constructor,
and disposes it with the client. The client consults the override store
before its not-initialized short-circuit, so an overridden flag is served
before LaunchDarkly data arrives. AllFlagsState reads through the overlay
and presents override-affected flags with event tracking off. The OVERRIDE
specification's test vectors run as a unit test.
A definition whose dependencies cannot be computed, such as a prerequisite
without a key or a segment match on a value that is not a string, is logged
and contributes no dependency edges, so every directly changed key is still
notified. A failure to determine or deliver the notifications is logged
instead of reaching the override source.
…uit and dispose on a failed start

Before the client is initialized, an evaluation proceeds only when the
override layer holds the flag; otherwise it takes the same path as without
an override source. The exception paths of evaluation and of the all-flags
state keep the override indicator when the definition came from the layer.
An override source whose Build or Start throws fails the client constructor
after the components created so far are disposed.
…n changes

A flag override can reference LaunchDarkly data: a rule that matches a LaunchDarkly segment, or a
prerequisite that is a LaunchDarkly flag. DataSourceUpdatesImpl computes flag change notifications
from a dependency tracker built from LaunchDarkly data alone, so when such a segment or flag
changed, the flags whose overrides depend on it were not notified. The override layer now keeps the
dependency edges of its own entries, rebuilt on every replacement, and the data source updates ask
it for the override entries that depend on a changed item. The chain can cross in both directions,
so each newly reached item is expanded through both sets of edges until nothing new is found. An
entry whose references cannot be read contributes no edges, the same as the change computation
skips it.
An exception from the override source's Start no longer fails the client constructor. The data
system logs it, disposes the source, and the client runs as it does without a source: an
application in an incident is better served by a client that runs without its overrides than by
one that does not start. The SDK's own file-based source never throws from Start, since it logs a
failed load and retries; a custom source could. The interface documents the behavior.
A source can supply a snapshot through the sink and then throw from Start. The data system now
closes the sink after disposing the failed source: the sink clears the layer, notifying the
flags that were briefly overridden, and ignores a late snapshot from the disposed source. The
client then runs without overrides, as the log says, and its all-flags state agrees.
…source down safely

AllFlagsState returns a state of overrides alone only when the client is
not initialized, the store is not initialized, and the override layer
holds flags, and it reads those flags from the layer itself. Before, a
configured source was enough: an empty layer over a store that held
LaunchDarkly data without an init marker produced a valid state of that
data, and with overrides present the same data entered the state. The
overlay no longer serves the layer's entries alone when the base store
fails to list its items, so an initialized client whose store fails
reports an invalid state and logs the failure, as it does without a
source.

Disposing the client closes the override sink before it disposes the
source, so a snapshot supplied during shutdown is ignored, and an
exception from the source's Dispose is logged instead of stopping the
rest of the shutdown.

MigrationVariation keeps the override-affected marking when it replaces
the reason of an unrecognized stage with WRONG_TYPE.
@kinyoklion

Copy link
Copy Markdown
Member Author

bugbot review

@kinyoklion

Copy link
Copy Markdown
Member Author

On the MigrationVariation point from the review summary: fixed in 1b6a23d. The WRONG_TYPE reason for an unrecognized stage now keeps overrideAffected from the evaluation, the same as the EvaluationAndFlag path (test: MigrationVariationKeepsTheMarkingWhenAnOverrideHasAnUnrecognizedStage). Go had the same gap, fixed in launchdarkly/go-server-sdk#492. Node already kept the marking.

Agent comment: written by Claude Code (an AI coding agent) on behalf of @kinyoklion.

An offline client does not build or start the override source, so its
overrides are not served and a configuration error in it is not
reported. The SDK now logs a warning when that happens, and the
Overrides configuration method documents it.
@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.

Stale Bugbot comment from a previous run.

A source logs and retries a failed load, and reports a configuration
error from Build. If Start throws anyway, the SDK logs it, disposes the
source, removes its snapshot, and the client runs without overrides.
@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.

Stale Bugbot comment from a previous run.

… store fails

When the store cannot list its flags, the state without overrides is
invalid and empty. Overrides only add to it, so the state now holds the
override layer's flags and stays invalid, and the failure is still
logged. A client-side SDK bootstrapped from the state uses its flags
whatever its validity says, so the overridden values reach it. With no
override flags the state is invalid and empty, as without a source.
@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 58ef4ac. Configure here.

@kinyoklion
kinyoklion merged commit e1748a1 into dotnet-server-sdk-v9 Oct 29, 2026
10 checks passed
@kinyoklion
kinyoklion deleted the rlamb/overrides-dotnet-override-layer branch October 29, 2026 20:11
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