-
Notifications
You must be signed in to change notification settings - Fork 23
chore: FDv2 Cache Initializer #342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
e044023
596bc97
563ec22
dbe5c65
70bd195
f856058
24c962f
e1617fe
7b5ed27
6aeea41
b8a50de
3f0066b
8f5353a
4e9746d
b9b1851
4a82402
e8caa9f
41ac382
9e51057
3050faf
2bdaf5a
5e9fa17
9fbd135
fc931a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,109 @@ | ||||||||||||||||||||
| package com.launchdarkly.sdk.android; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import androidx.annotation.NonNull; | ||||||||||||||||||||
| import androidx.annotation.Nullable; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import com.launchdarkly.logging.LDLogger; | ||||||||||||||||||||
| import com.launchdarkly.sdk.LDContext; | ||||||||||||||||||||
| import com.launchdarkly.sdk.android.DataModel.Flag; | ||||||||||||||||||||
| import com.launchdarkly.sdk.android.subsystems.FDv2SourceResult; | ||||||||||||||||||||
| import com.launchdarkly.sdk.android.subsystems.Initializer; | ||||||||||||||||||||
| import com.launchdarkly.sdk.fdv2.ChangeSet; | ||||||||||||||||||||
| import com.launchdarkly.sdk.fdv2.ChangeSetType; | ||||||||||||||||||||
| import com.launchdarkly.sdk.fdv2.Selector; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import java.util.Collections; | ||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||
| import java.util.concurrent.Executor; | ||||||||||||||||||||
| import java.util.concurrent.Future; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * FDv2 cache initializer: loads persisted flag data from the local cache as the first | ||||||||||||||||||||
| * step in the initializer chain. | ||||||||||||||||||||
| * <p> | ||||||||||||||||||||
| * Per CONNMODE 4.1.2, the cache initializer returns data with {@code persist=false} | ||||||||||||||||||||
| * and {@link Selector#EMPTY} (no selector), so the orchestrator continues to the next | ||||||||||||||||||||
| * initializer (polling) to obtain a verified selector from the server. This provides | ||||||||||||||||||||
| * immediate flag values from cache while the network initializer fetches fresh data. | ||||||||||||||||||||
|
aaron-zeisler marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
| * <p> | ||||||||||||||||||||
| * A cache miss (or missing persistent store) is returned as a {@link ChangeSetType#None} | ||||||||||||||||||||
| * changeset — 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 the | ||||||||||||||||||||
| * orchestrator records {@code anyDataReceived = true} and continues normally. This is | ||||||||||||||||||||
| * critical for OFFLINE mode where no synchronizers follow: without it, a cache miss | ||||||||||||||||||||
| * would leave the SDK in a failed initialization state. | ||||||||||||||||||||
|
aaron-zeisler marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
| */ | ||||||||||||||||||||
| final class FDv2CacheInitializer implements Initializer { | ||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's a summary of how the spec requirements and the js-core implementation were used when developing this class:
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| @Nullable | ||||||||||||||||||||
| private final PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData envData; | ||||||||||||||||||||
| private final LDContext context; | ||||||||||||||||||||
| private final Executor executor; | ||||||||||||||||||||
| private final LDLogger logger; | ||||||||||||||||||||
| private final LDAwaitFuture<FDv2SourceResult> shutdownFuture = new LDAwaitFuture<>(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| FDv2CacheInitializer( | ||||||||||||||||||||
| @Nullable PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData envData, | ||||||||||||||||||||
| @NonNull LDContext context, | ||||||||||||||||||||
| @NonNull Executor executor, | ||||||||||||||||||||
| @NonNull LDLogger logger | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| this.envData = envData; | ||||||||||||||||||||
| this.context = context; | ||||||||||||||||||||
| this.executor = executor; | ||||||||||||||||||||
| this.logger = logger; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Override | ||||||||||||||||||||
| @NonNull | ||||||||||||||||||||
| public Future<FDv2SourceResult> run() { | ||||||||||||||||||||
| LDAwaitFuture<FDv2SourceResult> resultFuture = new LDAwaitFuture<>(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| executor.execute(() -> { | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| if (envData == null) { | ||||||||||||||||||||
| logger.debug("No persistent store configured; skipping cache"); | ||||||||||||||||||||
| resultFuture.set(FDv2SourceResult.changeSet(new ChangeSet<>( | ||||||||||||||||||||
| ChangeSetType.None, | ||||||||||||||||||||
| Selector.EMPTY, | ||||||||||||||||||||
| Collections.emptyMap(), | ||||||||||||||||||||
| null, | ||||||||||||||||||||
| false), false)); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| String hashedContextId = LDUtil.urlSafeBase64HashedContextId(context); | ||||||||||||||||||||
| EnvironmentData stored = envData.getContextData(hashedContextId); | ||||||||||||||||||||
| if (stored == null) { | ||||||||||||||||||||
| logger.debug("Cache miss for context"); | ||||||||||||||||||||
| resultFuture.set(FDv2SourceResult.changeSet(new ChangeSet<>( | ||||||||||||||||||||
| ChangeSetType.None, | ||||||||||||||||||||
| Selector.EMPTY, | ||||||||||||||||||||
| Collections.emptyMap(), | ||||||||||||||||||||
| null, | ||||||||||||||||||||
| false), false)); | ||||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Map<String, Flag> flags = stored.getAll(); | ||||||||||||||||||||
| ChangeSet<Map<String, Flag>> changeSet = new ChangeSet<>( | ||||||||||||||||||||
| ChangeSetType.Full, | ||||||||||||||||||||
| Selector.EMPTY, | ||||||||||||||||||||
| flags, | ||||||||||||||||||||
| null, | ||||||||||||||||||||
| false); | ||||||||||||||||||||
| logger.debug("Cache hit: loaded {} flags for context", flags.size()); | ||||||||||||||||||||
| resultFuture.set(FDv2SourceResult.changeSet(changeSet, false)); | ||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||
| logger.warn("Cache initializer failed: {}", e.toString()); | ||||||||||||||||||||
| resultFuture.set(FDv2SourceResult.status( | ||||||||||||||||||||
| FDv2SourceResult.Status.interrupted(e), false)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
cursor[bot] marked this conversation as resolved.
|
||||||||||||||||||||
| }); | ||||||||||||||||||||
|
aaron-zeisler marked this conversation as resolved.
Outdated
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| return LDFutures.anyOf(shutdownFuture, resultFuture); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Override | ||||||||||||||||||||
| public void close() { | ||||||||||||||||||||
| shutdownFuture.set(FDv2SourceResult.status(FDv2SourceResult.Status.shutdown(), false)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package com.launchdarkly.sdk.android; | ||
|
|
||
| import androidx.annotation.NonNull; | ||
| import androidx.annotation.Nullable; | ||
|
|
||
| import com.launchdarkly.sdk.android.subsystems.ClientContext; | ||
| import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; | ||
|
|
@@ -115,7 +116,9 @@ public DataSource build(ClientContext clientContext) { | |
| } | ||
|
|
||
| DataSourceBuildInputs inputs = makeInputs(clientContext); | ||
| ResolvedModeDefinition resolved = resolve(modeDef, inputs); | ||
| PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData envData = | ||
| ClientContextImpl.get(clientContext).getPerEnvironmentDataIfAvailable(); | ||
| ResolvedModeDefinition resolved = resolve(modeDef, inputs, envData); | ||
|
|
||
| DataSourceUpdateSink baseSink = clientContext.getDataSourceUpdateSink(); | ||
| if (!(baseSink instanceof DataSourceUpdateSinkV2)) { | ||
|
|
@@ -149,28 +152,40 @@ public void close() { | |
| } | ||
|
|
||
| private DataSourceBuildInputs makeInputs(ClientContext clientContext) { | ||
| TransactionalDataStore store = ClientContextImpl.get(clientContext).getTransactionalDataStore(); | ||
| ClientContextImpl impl = ClientContextImpl.get(clientContext); | ||
| TransactionalDataStore store = impl.getTransactionalDataStore(); | ||
| SelectorSource selectorSource = store != null | ||
| ? new SelectorSourceFacade(store) | ||
| : () -> com.launchdarkly.sdk.fdv2.Selector.EMPTY; | ||
|
|
||
| return new DataSourceBuildInputs( | ||
| clientContext.getEvaluationContext(), | ||
| clientContext.getServiceEndpoints(), | ||
| clientContext.getHttp(), | ||
| clientContext.isEvaluationReasons(), | ||
| selectorSource, | ||
| sharedExecutor, | ||
| ClientContextImpl.get(clientContext).getPlatformState().getCacheDir(), | ||
| impl.getPlatformState().getCacheDir(), | ||
| clientContext.getBaseLogger() | ||
| ); | ||
| } | ||
|
|
||
| private static ResolvedModeDefinition resolve( | ||
| ModeDefinition def, DataSourceBuildInputs inputs | ||
| ModeDefinition def, DataSourceBuildInputs inputs, | ||
| @Nullable PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData envData | ||
| ) { | ||
| List<FDv2DataSource.DataSourceFactory<Initializer>> initFactories = new ArrayList<>(); | ||
| for (DataSourceBuilder<Initializer> builder : def.getInitializers()) { | ||
| initFactories.add(() -> builder.build(inputs)); | ||
| // The cache initializer's dependency (ReadOnlyPerEnvironmentData) is only | ||
| // available at build time, not when the static mode table is constructed, | ||
| // so we inject it here by replacing the placeholder with a wired copy. | ||
| final DataSourceBuilder<Initializer> effective; | ||
| if (builder instanceof DataSystemComponents.CacheInitializerBuilderImpl) { | ||
| effective = new DataSystemComponents.CacheInitializerBuilderImpl(envData); | ||
|
aaron-zeisler marked this conversation as resolved.
Outdated
|
||
| } else { | ||
| effective = builder; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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"
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated |
||
| initFactories.add(() -> effective.build(inputs)); | ||
| } | ||
| List<FDv2DataSource.DataSourceFactory<Synchronizer>> syncFactories = new ArrayList<>(); | ||
| for (DataSourceBuilder<Synchronizer> builder : def.getSynchronizers()) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.