Conversation
…he initializer Introduce a CachedFlagStore interface in the subsystems package that provides read access to cached flag data by evaluation context. Add this as a nullable field to DataSourceBuildInputs and wire it through from FDv2DataSourceBuilder using PerEnvironmentData. This plumbing enables the upcoming FDv2 cache initializer to load persisted flags without depending on package-private types. Made-with: Cursor
…uilderImpl Add FDv2CacheInitializer that loads persisted flag data from the local cache as the first step in the initializer chain. Per CONNMODE 4.1.2, the result uses Selector.EMPTY and persist=false so the orchestrator continues to the polling initializer for a verified selector. Cache miss and no-store cases return interrupted status to move on without delay. Add CacheInitializerBuilderImpl in DataSystemComponents and comprehensive tests covering cache hit, miss, no store, exceptions, and shutdown behavior. Made-with: Cursor
Prepend the cache initializer to all connection modes per CONNMODE 4.1.1. Every mode now starts with a cache read before any network initializer, providing immediate flag values from local storage while the polling initializer fetches fresh data with a verified selector. Update initializer count assertions in DataSystemBuilderTest and FDv2DataSourceBuilderTest to reflect the new cache initializer. Made-with: Cursor
363887c to
563ec22
Compare
| * A cache miss is reported as an {@link FDv2SourceResult.Status#interrupted} status, | ||
| * causing the orchestrator to move to the next initializer without delay. | ||
| */ | ||
| final class FDv2CacheInitializer implements Initializer { |
There was a problem hiding this comment.
Here's a summary of how the spec requirements and the js-core implementation were used when developing this class:
| Decision | Source | Reasoning |
|---|---|---|
Selector.EMPTY on cache result |
CONNMODE 4.1.2 | Cache is unverified; empty selector tells the orchestrator to continue to the polling initializer for a real selector |
persist=false on ChangeSet |
CONNMODE 4.1.2 | Don't re-write data we just read from cache |
Cache miss = interrupted |
js-core pattern | Fast failure so the orchestrator immediately moves on; interrupted is the correct signal (not terminalError, which would stop the chain) |
fdv1Fallback=false always |
Logic | Cache is local storage, no server headers are involved |
Nullable cachedFlagStore |
Testing pragmatism | Test contexts don't have a persistent store; graceful degradation avoids test setup burden |
In FDv2, the FDv2CacheInitializer handles cache loading as the first step in the initializer chain, making the cache load in ContextDataManager.switchToContext() redundant. Add a skipCacheLoad parameter to ContextDataManager and a setCurrentContext() method so that the FDv2 path sets the context without reading from cache, while the FDv1 path continues to load cached flags immediately. Made-with: Cursor
|
@tanderson-ld The 5th (and currently last) commit updates the code in LDClient and ContextDataManager. In FDv1, flags are loaded from the cache in ContextDataManager's constructor and also in LDClient's "identify" flow. But in FDv2, loading from cache in those places is redundant now that we have the cache initializer. I could defer this commit to a separate pull request if you feel that's cleaner. |
…entData Made-with: Cursor
…terrupted Cache miss and missing persistent store now return a "transfer of none" changeset (ChangeSetType.None with Selector.EMPTY) instead of an interrupted status. This fixes an OFFLINE mode regression where a cache miss left the SDK in a failed initialization state because no synchronizers follow to recover. Made-with: Cursor
|
We need to meet this requirement: For SDKs that are inherently waiting for cache today even with a timeout of 0 seconds, we need to ensure that in FDv2 that same SDK major version continues to wait for the cache initializer. |
…e exceptions as None Two fixes for the cache initializer: 1. Orchestrator: only complete initialization from the post-initializer loop when no synchronizers are available. When synchronizers exist, they are the authority on init completion. Fixes premature init in POLLING/STREAMING modes where a cache miss None changeset was completing start before the synchronizer fetched server data. 2. Cache initializer: return ChangeSetType.None on exceptions during cache read instead of interrupted status. A corrupt/unreadable cache is semantically equivalent to an empty cache, not a hard error. Made-with: Cursor
- Trim FDv2CacheInitializer Javadoc to only describe this class's responsibility; remove references to orchestrator, other initializers, HTTP status codes, and external spec sections. - Merge setCurrentContext() into switchToContext(context, skipCacheLoad) to eliminate the separate method and simplify call sites in the constructor and LDClient.identifyInternal(). Made-with: Cursor
….com:launchdarkly/android-client-sdk into aaronz/SDK-2070/cache-initializer
…or overhead Cache read now runs inline on the caller's thread instead of dispatching to an executor, removing ~300us of thread scheduling overhead per Todd's benchmarking. close() becomes a no-op since there is nothing to cancel. Made-with: Cursor
Add a default method `isRequiredBeforeStartup()` to the Initializer interface so FDv2DataSource can distinguish initializers that must run before the startup timeout begins. FDv2CacheInitializer overrides it to return true, matching FDv1 behavior where cache was always loaded before the timeout started. Made-with: Cursor
Build Initializer instances directly in resolve() instead of wrapping them in factory lambdas. This allows FDv2DataSource to inspect isRequiredBeforeStartup() and run pre-startup initializers synchronously before dispatching to the executor. ResolvedModeDefinition now carries List<Initializer> instead of List<DataSourceFactory<Initializer>>. Made-with: Cursor
… filtering Accept List<Initializer> instead of List<DataSourceFactory<Initializer>>. Add isRequiredBeforeStartup parameter to getNextInitializerAndSetActive() to filter initializers by their pre-startup requirement. Add resetInitializerIndex() for resetting between the eager and deferred passes. Remove the now-unnecessary getNextInitializer() helper. Made-with: Cursor
Run pre-startup initializers synchronously on the calling thread before dispatching to the executor, guaranteeing cached data is available even with a zero timeout. The deferred pass then runs remaining initializers on the executor thread. Both passes reuse the existing runInitializers() method with added isRequiredBeforeStartup filter and previousDataReceived seed parameters. Made-with: Cursor
Update FDv2DataSourceTest for pre-built initializer signatures (factory lambdas replaced with direct instances). Add tests verifying eager initializers run on the calling thread, deferred initializers run on the executor, both passes execute, OFFLINE cache miss still initializes, and cached data is available immediately. Add isRequiredBeforeStartup() test to FDv2CacheInitializerTest. Made-with: Cursor
… runInitializers() runInitializers() was calling tryCompleteStart() at the end of its loop when any data had been received, including unverified cache data. This prematurely marked initialization complete before synchronizers could run, causing end-to-end test failures in POLLING mode where cached data existed but the polling server returned 401. Moved the tryCompleteStart responsibility to start(), which already has the correct orchestration logic for the no-synchronizer case (e.g. OFFLINE mode). Changed runInitializers() to return a boolean indicating whether any initializer succeeded, letting start() decide the initialization outcome based on the full picture. Made-with: Cursor
…pattern for cache initializer dependencies Follow the ClientContext/ClientContextImpl pattern to pass ReadOnlyPerEnvironmentData through DataSourceBuildInputs instead of the instanceof/replacement hack in FDv2DataSourceBuilder.resolve(). Made-with: Cursor
|
|
||
| return LDFutures.anyOf(shutdownFuture, resultFuture); | ||
| LDAwaitFuture<FDv2SourceResult> future = new LDAwaitFuture<>(); | ||
| future.set(result); |
There was a problem hiding this comment.
There's a built-in future type other than LDAwaitFuture that's already complete. Composable futures were added in Android 24.
There was a problem hiding this comment.
Question about this. Our build.gradle files define the min SDK value at 21. Here's an example:
Since the CompletableFuture class was introduced in SDK version 24, I think the SDK will throw errors when it's run on API versions 21-23. Is this a big problem? What do you think about this? THose versions are pretty old and I wonder if we can update our min SDK value.
…cache-before-timeout' into aaronz/SDK-2070/cache-initializer Integrates pre-startup (eager) initializer execution with existing cache initializer improvements. Key merge resolution decisions: - Introduced RunInitializersResult to carry both anySucceeded and anyDataReceived from each initializer pass, replacing the single boolean return that differed between branches - Combined DataSourceBuildInputsInternal pattern (no instanceof hack) with eager initializer building in FDv2DataSourceBuilder.resolve() - Updated FDv2DataSourceBuilderTest to provide HttpConfiguration, needed now that initializers are eagerly built during resolve() Made-with: Cursor
…DataSource Replace the Initializer.isRequiredBeforeStartup() marker with an InitializerFromCache marker applied to the builder/factory. FDv2DataSource now partitions initializers at construction and runs cache initializers synchronously before executor dispatch (ensuring cache loads within a zero-timeout init), then runs general initializers on the executor. FDv2DataSourceBuilder wraps factories built from InitializerFromCache builders so the marker is preserved through the factory indirection. Co-authored-by: Todd Anderson <tanderson@launchdarkly.com> Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 5e9fa17. Configure here.
Made-with: Cursor
|
|
||
| return LDFutures.anyOf(shutdownFuture, resultFuture); | ||
| LDAwaitFuture<FDv2SourceResult> future = new LDAwaitFuture<>(); | ||
| future.set(result); |
|
|
||
| this.sourceManager = new SourceManager(allSynchronizers, new ArrayList<>(initializers)); | ||
| // note that the source manager only uses the initializers after the cache initializers and not the cache initializers | ||
| this.sourceManager = new SourceManager(allSynchronizers, new ArrayList<>(generalInitializers)); |
There was a problem hiding this comment.
new ArrayList isn't necessary (I wrote the code, sorry!)
|
|
||
| if (!sourceManager.hasAvailableSynchronizers()) { | ||
| if (!startCompleted.get()) { | ||
| // try to claim this is the cause of the shutdown, but it might have already been set by an intentional stop(). |
There was a problem hiding this comment.
This comment doesn't need to be removed. It is still valid.
| initFactories.add(new CacheInitializerFactory(() -> builder.build(inputs))); | ||
| } else { | ||
| initFactories.add(() -> builder.build(inputs)); | ||
| } |
There was a problem hiding this comment.
Add a comment to help explain what this logic is doing and that it is dealing with the DataSourceBuilder (public API) to FDv2DataSource.DataSourceFactory "handholding"
Made-with: Cursor

Goal
Implement the FDv2 cache initializer so that cached flag data is loaded from local storage as the first step in every connection mode's initializer chain, providing immediate flag values while the network initializer fetches fresh data.
Approach
The FDv1 production code loads cached flags synchronously during
ContextDataManager.switchToContext(). In FDv2, this cache loading is modeled as a dedicatedInitializer— the first in every mode's chain — per CONNMODE spec 4.1.1.Cache initializers are identified by the
InitializerFromCachemarker interface applied to their builder. At mode-resolution time,FDv2DataSourceBuilderwraps those builders' factories so the marker is preserved through the factory indirection, and at construction timeFDv2DataSourcepartitions its initializers into cache initializers and general initializers. Cache initializers run synchronously on the caller's thread before executor dispatch, matching FDv1 behavior where an SDK initializing withtimeout=0still receives cached values immediately. General initializers (e.g. polling) then run on the executor as before.The cache initializer's dependency on
ReadOnlyPerEnvironmentDatais wired through a new package-privateDataSourceBuildInputsInternalclass that extends the publicDataSourceBuildInputs, following the same pattern asClientContext/ClientContextImpl. Internal SDK components callDataSourceBuildInputsInternal.get(inputs)to unwrap internal-only properties, while the public API surface remains unchanged.Key Behaviors
CHANGE_SETwithChangeSetType.Full,Selector.EMPTY, andpersist=false, so the orchestrator applies the data immediately but continues to the next initializer for a verified selector from the serverChangeSetType.Nonechangeset — analogous to "transfer of none" / 304 Not Modified (CSFDV2 9.1.2). This signals "I checked the source and there is nothing new" rather than an error, so modes with no synchronizers (e.g. OFFLINE) still complete startup successfully on cache missrunCacheInitializers()applies changesets but ignores selectors/statuses and never completes start;runGeneralInitializers()is responsible for init completion and observes synchronizer availability via the existingstart()orchestrationContextDataManagerskips its prior cache-load-on-context-switch via askipCacheLoadparameter onswitchToContext(), keeping FDv1 behavior unchangedfdv1Fallbackis alwaysfalsesince the cache is local (no server headers involved)Not In Scope
Requirements
Related issues
Provide links to any issues in this repository or elsewhere relating to this pull request.
Describe the solution you've provided
Provide a clear and concise description of what you expect to happen.
Describe alternatives you've considered
Provide a clear and concise description of any alternative solutions or features you've considered.
Additional context
Add any other context about the pull request here.
Note
Medium Risk
Changes FDv2 startup/identify sequencing to apply cached flag data synchronously before executor-dispatched initialization, which can affect initialization timing and flag availability semantics across connection modes.
Overview
Implements an FDv2 cache initializer that reads persisted flag data and applies it as the first initializer step in every default connection mode, returning
Fullchangesets on cache hit andNoneon miss/no-store/errors.Updates the FDv2 orchestration to separate cache initializers from general initializers, running cache initializers synchronously on the caller thread before
start()dispatch, and adjusts success criteria to ignoreChangeSetType.Noneas “data received.”Adds internal-only plumbing (
DataSourceBuildInputsInternal,ReadOnlyPerEnvironmentData, andInitializerFromCache) to pass persistent-store access into built-in FDv2 components without expanding the public API, and avoids redundant cache loads by allowingContextDataManager.switchToContext(..., skipCacheLoad)when FDv2 is in use (wired throughLDClient).Reviewed by Cursor Bugbot for commit fc931a8. Bugbot is set up for automated code reviews on this repo. Configure here.