Skip to content

feat: Add the FDv2 data source orchestrator to the client SDK - #612

Open
beekld wants to merge 4 commits into
bklimt/SDK-3031/client-fdv2-streamingfrom
bklimt/SDK-3032/client-fdv2-orchestrator
Open

feat: Add the FDv2 data source orchestrator to the client SDK#612
beekld wants to merge 4 commits into
bklimt/SDK-3031/client-fdv2-streamingfrom
bklimt/SDK-3032/client-fdv2-orchestrator

Conversation

@beekld

@beekld beekld commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the client SDK's FDv2 data source -- the orchestrator that runs initializers to load a basis, then rotates synchronizers to keep it current.

Implemented here:

  • Runs initializers to a basis, then rotates synchronizers on fallback and recovery conditions.
  • Reports data-source status, records the environment id, and tags applied data as cache- or network-sourced.

Also moves the shared orchestration primitives out of the server SDK into libs/internal so both SDKs can share them. That server change is a behavior-neutral relocation.

Notes:

  • Start() blocks on cache initialization, so cached flags are immediately evaluable. This matches the behavior of the FDv1 client. It also matches Android. However, Flutter loads the cache asynchronously instead.
  • FDv1 fallback directive handling is out of scope.

Note

Overview
Introduces FDv2DataSource for the client SDK: an IDataSource that chains initializers (until a selector-bearing basis exists), then synchronizers with fallback/recovery conditions, applies changesets to the flag store (with from_cache tagging), publishes data-source status, and exposes EnvironmentId(). Start() synchronously runs cache initializers so cached flags are evaluable before the executor runs, including cache-only / offline success paths.

Refactors shared FDv2 orchestration into libs/internal (conditions, SourceManager, condition interfaces) under launchdarkly::internal::data_sources, so client and server both consume the same code. Conditions now take a SourceSignal (kChangeSet / kInterrupted / kOther) instead of full source results; orchestrators map results via ClassifyResult. SourceManager becomes a header-only template; the server SDK drops its local copies and source_manager.cpp.

Adds broad unit tests for the client orchestrator and updates internal/server tests for the new APIs.

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

@beekld
beekld added this pull request to stack #608 September 11, 2026 18:04
@beekld
beekld force-pushed the bklimt/SDK-3032/client-fdv2-orchestrator branch from e03fed7 to d3da070 Compare September 11, 2026 21:23
@beekld
beekld force-pushed the bklimt/SDK-3032/client-fdv2-orchestrator branch from d3da070 to 22c46d4 Compare September 11, 2026 21:47
@beekld
beekld force-pushed the bklimt/SDK-3032/client-fdv2-orchestrator branch from 22c46d4 to ae8dc46 Compare September 12, 2026 00:21
@beekld
beekld marked this pull request as ready for review September 12, 2026 00:21
@beekld
beekld requested a review from a team as a code owner September 12, 2026 00:21

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ae8dc46. Configure here.

}
sink_->Apply(context_, std::move(change_set.change_set), from_cache);
PublishState(DataSourceStatus::DataSourceState::kValid);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shutdown does not fence updates

High Severity

Close() does not stop in-flight work from writing flags or status. ApplyResult never consults closed_, OnInitializerResult applies a changeset before checking it, and PublishState drops the lock before SetState. ShutdownAsync then posts its completion immediately, so an identify restart can apply the old context over the new cache or clobber the replacement source's status.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ae8dc46. Configure here.

}
sink_->Apply(context_, std::move(change_set.change_set), from_cache);
PublishState(DataSourceStatus::DataSourceState::kValid);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Empty changeset marks source valid

Medium Severity

ApplyResult publishes kValid for every changeset, including a kNone result that carries no selector and no flags. That leaves initializing before a basis exists. The status manager then treats later errors as real interruptions, and a StartAsync waiter can resolve as successful before ReportExhausted moves the source to kShutdown.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ae8dc46. Configure here.

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