Skip to content

feat!: Require feature stores to report whether an upsert or delete applied - #482

Merged
keelerm84 merged 4 commits into
feat/9.xfrom
mk/SDK-81/store-write-result
Nov 4, 2026
Merged

keelerm84 merged 4 commits into
feat/9.xfrom
mk/SDK-81/store-write-result

Conversation

@keelerm84

@keelerm84 keelerm84 commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

FeatureStore#upsert and FeatureStore#delete must now return true when the
store applies the write, and false when the store already has an item with
the same or a higher version. FeatureStoreCore#upsert_internal, used with
CachingStoreWrapper, must return the same boolean instead of the final item.

Before this change, the SDK sent flag change events after every upsert or
delete from the data source, even when the store rejected a stale write. A
repeated or out-of-order update therefore produced change events for flags
that did not change. The SDK now updates its dependency tracking and sends
change events only when the store reports that it applied the write.

When a core reports that it did not apply an upsert, CachingStoreWrapper
reads the current item from the core and caches it. This matches the Go
SDK's persistent store wrapper.

Custom store implementers:

  • A FeatureStore that returns nil or another falsy value from upsert or
    delete stops flag change events for those writes. The default methods in
    LaunchDarkly::Interfaces::FeatureStore return nil, so implementations
    must return the result explicitly.
  • A FeatureStoreCore must return true or false from upsert_internal. The
    wrapper no longer accepts the final item as the return value.

Note

Overview
Breaking change: FeatureStore#upsert and #delete (and FeatureStoreCore#upsert_internal) now return true when the write is applied and false when the store already has the same or a newer version, instead of returning void or the final entity.

The data source UpdateSink uses that signal so dependency tracking and flag change listeners run only after a successful store write. Duplicate or out-of-order streaming updates no longer emit spurious change events.

Built-in stores (in-memory, Redis, DynamoDB, Consul) and CachingStoreWrapper were updated: on a rejected upsert, the wrapper re-reads the current item from the core and refreshes the cache (aligned with the Go SDK), rather than caching the stale payload. DynamoDB no longer performs an extra read when a conditional write fails.

Custom store authors must return explicit booleans; default interface stubs still return nil, which is treated as falsy and suppresses change events.

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

The SDK no longer checks with respond_to? for the optional feature store
methods. Custom stores must now implement them.

A custom LaunchDarkly::Interfaces::FeatureStore must implement:

- monitoring_enabled? -- return true only if the store publishes status
  updates when it becomes unavailable and when it recovers. In-memory
  stores return false.
- available? -- return true if the underlying data store is reachable.
  The SDK polls this method after a store operation fails, while
  monitoring is enabled. Stores that cannot become unavailable return
  true.
- stop -- release resources. The SDK calls it when the client closes.

A custom FeatureStoreCore used with CachingStoreWrapper must implement
available? and stop. CachingStoreWrapper now always reports
monitoring_enabled? as true and delegates available? to the core.

Before this change, a store without these methods silently lost status
monitoring, and a core without available? turned monitoring off. The
interface docs also disagreed on whether available? would stay. Making
the methods required gives one clear contract and removes the probing.

The lifecycle method stays named stop. All built-in stores, the big
segment store, data sources and event processors already use stop, so
renaming only the feature store to close would make the SDK less
consistent.
…pplied

FeatureStore#upsert and FeatureStore#delete must now return true when the
store applies the write, and false when the store already has an item with
the same or a higher version. FeatureStoreCore#upsert_internal, used with
CachingStoreWrapper, must return the same boolean instead of the final item.

Before this change, the SDK sent flag change events after every upsert or
delete from the data source, even when the store rejected a stale write. A
repeated or out-of-order update therefore produced change events for flags
that did not change. The SDK now updates its dependency tracking and sends
change events only when the store reports that it applied the write.

When a core reports that it did not apply an upsert, CachingStoreWrapper
reads the current item from the core and caches it. This matches the Go
SDK's persistent store wrapper.

Custom store implementers:

- A FeatureStore that returns nil or another falsy value from upsert or
  delete stops flag change events for those writes. The default methods in
  LaunchDarkly::Interfaces::FeatureStore return nil, so implementations
  must return the result explicitly.
- A FeatureStoreCore must return true or false from upsert_internal. The
  wrapper no longer accepts the final item as the return value.
@keelerm84
keelerm84 marked this pull request as ready for review November 4, 2026 18:20
@keelerm84
keelerm84 requested a review from a team as a code owner November 4, 2026 18:20
@keelerm84
keelerm84 merged commit 3efec88 into feat/9.x Nov 4, 2026
16 of 17 checks passed
@keelerm84
keelerm84 deleted the mk/SDK-81/store-write-result branch November 4, 2026 18:33
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