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
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>
kinyoklion added a commit to launchdarkly/js-core that referenced this pull request Nov 2, 2026
…pshots after close (#2133)

<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

Two follow-ups from the review of the same code in the .NET port
(launchdarkly/dotnet-core#364).

**Offline mode.** When the client is offline, a configured override
source is not built or started, and that stays so. Nothing said so
before: an application running offline with overrides saw default values
and no explanation, and a configuration error in the source went
unreported. The SDK now logs a warning once, at construction: `Offline
mode is enabled; the override source is not started and no overrides are
served`. The `overrides` option documents that the source is not built
or started offline, that no overrides are served, and that a
configuration error in the source is not reported then.

**Snapshots after close.** `close()` closed the override source, but the
sink kept accepting snapshots, so a custom source that supplied one
after the client was closed still replaced the layer and sent flag
change events to the application. The client now closes the sink before
the source. A closed sink ignores a snapshot (logged at debug level) and
keeps the layer as it is, so closing sends no change events of its own.
The SDK's file-based source stops supplying snapshots when it is closed,
so this only matters for a custom source.

## Tests

Three tests in `LDClientImpl.overrides.test.ts`: offline mode logs the
warning and never calls the source factory; offline mode without a
source logs no override warning; and a snapshot supplied through the
sink after `close()` produces no change event and no override. The first
and third fail without the change, and each fails again with its fix
reverted. The sdk-server suite passes (1562 passed, 7 skipped), and lint
and formatting are clean.
<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=js-core -->
<sub>Opened from termag</sub>

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> **Offline + overrides.** When `offline` is enabled but
`dataSystem.overrides` is set, the SDK still does not build or start the
override source (unchanged). It now logs a one-time construction
warning: *Offline mode is enabled; the override source is not started
and no overrides are served*, and `LDDataSystemOptions` documents that
behavior. Offline clients without an override source do not get that
warning.
> 
> **Shutdown.** `OverrideSink` gains a `close()` that stops accepting
snapshots; late `setOverrides` calls are ignored (debug log) and do not
update the layer or fire flag-change listeners. `LDClientImpl.close()`
closes the sink **before** the override source so custom sources that
keep pushing after close cannot mutate state post-shutdown.
> 
> Three tests cover the offline warning, absence of spurious warnings,
and post-close snapshot handling.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c07a82b. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

[SDK-3419]:
https://launchdarkly.atlassian.net/browse/SDK-3419?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
kinyoklion added a commit to launchdarkly/js-core that referenced this pull request Nov 2, 2026
…erride layer's flags (#2132)

<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

Before the client is initialized, and while the data store is not
initialized either, `allFlagsState` evaluates whatever flags the store
holds into an invalid state. A store can hold LaunchDarkly flags without
being initialized, for example a persistent store with data but without
the marker that a complete data set was written. With an override source
configured, the state was marked valid whenever the merged read through
the overlay returned any flag, so a source whose layer is empty turned
that invalid state into a valid one. Configuring a source that supplies
nothing must change nothing (OVERRIDE Requirement 1.2.2).

Whether the override layer holds flags now decides the validity, instead
of whether the merged read returned any. The read itself still goes
through the overlay, so the state holds the store's flags with the
override layer's entries taking precedence. Overrides only add to the
flags the state would report without them, and never remove one: a layer
with flags gives a valid state of the store's flags plus the overrides,
and a layer without flags gives exactly the state that a client without
an override source gives. The once-per-client warning now says that the
state holds flags from the override layer and the data store.

This was found while reviewing the same code in the .NET port
(launchdarkly/dotnet-core#364). In the other server-side SDKs this
branch returned an empty state before overrides, so a state of the
override flags alone adds to it there; in this SDK the store's flags
were already in the state, so they stay.

## Tests

Two tests in `LDClientImpl.overrides.test.ts` use a persistent store
that holds LaunchDarkly flags but reports that it is not initialized.
One checks that a client with an empty override source reports the same
invalid state as a client without a source. The other checks that a
client with overrides reports the store's flags with the overrides added
and taking precedence for a shared key. Reading the layer alone fails
the second; deciding validity from the merged read fails the first. The
sdk-server suite passes (1561 passed, 7 skipped), and lint and
formatting are clean.
<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=projects -->
<sub>Opened from termag</sub>

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> When **`allFlagsState`** runs before the client and persistent store
are initialized, the SDK still reads flags through the store overlay,
but **`LDFlagsState.valid`** no longer follows “any merged flag came
back.” **Validity now depends only on whether the override layer has
feature flags**, so an empty override source cannot turn an otherwise
invalid partial-store snapshot into a valid one (OVERRIDE 1.2.2).
> 
> If the override layer has flags, the state is **valid** and includes
**store flags plus overrides** (overrides win on key clashes); the
once-per-client warning text now says flags come from **both** the
override layer and the data store. If the layer is empty, behavior
matches a client with no override source: **invalid** state with
whatever the store returned, plus the existing “data store not
available” path when appropriate.
> 
> Tests add an uninitialized store that still holds LD flags and assert
the empty-source vs merged-override cases; existing override tests were
updated for the new log message.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
6c26c5c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

[SDK-3419]:
https://launchdarkly.atlassian.net/browse/SDK-3419?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
kinyoklion added a commit to launchdarkly/go-server-sdk that referenced this pull request Nov 2, 2026
<!-- TERMAG_PR_BODY_BEGIN -->
## Summary

When the client is offline, the data system does not start a configured
override source, and that stays so. Nothing said so before: an
application running offline with overrides saw default values and no
explanation. The SDK now logs a warning when it creates the data system:
`Offline mode is enabled; the override source is not started and no
overrides are served`.

The `Overrides` builder method documents the offline behavior. In Go the
data system configuration is built whether or not the client is offline,
so the source is built, and an invalid source configuration still fails
client creation offline; the documentation says so, and a test pins it.

This follows the review of the same code in the .NET port
(launchdarkly/dotnet-core#364). The Node change is
launchdarkly/js-core#2133.

## Tests

- `TestOverrideSourceIsNotStartedAndAWarningIsLoggedWhenOffline`: the
source is not started, the warning is logged once, and the overridden
flag serves the default. It fails without the change and fails again
with it reverted.
- `TestNoOverrideWarningIsLoggedWhenOfflineWithoutAnOverrideSource`:
offline without a source logs no override warning.
- `TestFileOverrideSourceConfigurationErrorIsReportedWhenOffline`: a
file source without paths still fails client creation offline.

`go test -race ./...` passes and `make lint` reports no issues.
<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=go-server-sdk -->
<sub>Opened from termag</sub>

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> **Offline clients with a configured override source no longer fail
silently.** When the data system is disabled for offline mode, the SDK
still does not start the override source or serve overrides, but it now
logs a warning at construction: `Offline mode is enabled; the override
source is not started and no overrides are served`.
> 
> The `DataSystemConfigurationBuilder.Overrides` docs describe this
behavior and note that invalid override configuration is still validated
at client creation (the source is built even when offline).
> 
> New tests cover the warning when an override source is present, no
warning when offline without one, and file-source config errors (e.g.
missing paths) still failing `MakeCustomClient` while offline.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
eb656a6. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

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

Co-authored-by: Devin AI <158243242+devin-ai-integration[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