Skip to content

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

Draft
kinyoklion wants to merge 6 commits into
rlamb/overrides-dotnet-model-evaluatorfrom
rlamb/overrides-dotnet-override-layer
Draft

kinyoklion wants to merge 6 commits into
rlamb/overrides-dotnet-model-evaluatorfrom
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. A configuration error in the source propagates from the constructor like any other invalid component configuration. The source is not built when the client is offline. 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, 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, and a source whose Build or Start throws leaves nothing started. AllFlagsState reads through the overlay, so it includes override-only flags and, before initialization, returns a state built from overrides alone (with a warning logged once). 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.

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.

This PR is based on the branch of the model and evaluator marking change, which it depends on.

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 to the FDv2 data system so operators can force per-key flag/segment definitions that win over LaunchDarkly at read time, without affecting initialization or data-source status.

Configuration: DataSystemBuilder.Overrides(...) and IOverrideSource / IOverrideSink plug into DataSystemConfiguration; the source starts synchronously before data sources and is skipped when offline.

Runtime: OverrideLayer + OverrideOverlay sit on the read-only store boundary; OverrideSink applies full snapshots and fans out flag change notifications (including dependency tracking across LaunchDarkly data and override entries via IExternalDependencies). LdClient can evaluate overridden flags before the client is initialized, adjusts AllFlagsState (override-only when needed, disables tracking fields when overrideAffected), and disposes cleanly on build/start failures.

Tests: Unit coverage for layer/overlay/sink/builder/client plus OVERRIDE spec vectors in override-vectors.json.

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

@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.

Comment thread pkgs/sdk/server/src/Internal/DataSystem/FDv2DataSystem.cs
@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
…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.
@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
…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.
@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 625311a. Configure here.

This branch has not been deployed

No deployments
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.

1 participant