From 24ce9422f8c10bc61be3159680c7600c0cd27df4 Mon Sep 17 00:00:00 2001 From: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:59:04 -0500 Subject: [PATCH] fix: improved LDClient.identify(...) behavior when offline (#261) **Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** SC-237349 **Describe the solution you've provided** Updated code so that ContextDataManager switches context independently of ConnectivityManager and ConnectivityManager is not in the path of context switching. --- .../sdk/android/LDClientEndToEndTest.java | 34 ++++ .../sdk/android/AndroidPlatformState.java | 7 +- .../sdk/android/ClientContextImpl.java | 15 ++ .../sdk/android/ConnectivityManager.java | 61 ++++--- .../sdk/android/ContextDataManager.java | 166 +++++++----------- .../launchdarkly/sdk/android/LDClient.java | 59 +++---- .../sdk/android/StreamingDataSource.java | 22 +-- .../sdk/android/integrations/TestData.java | 12 +- .../sdk/android/subsystems/ClientContext.java | 23 +++ .../subsystems/DataSourceUpdateSink.java | 5 +- .../sdk/android/ConnectivityManagerTest.java | 12 +- .../ContextDataManagerContextCachingTest.java | 4 + .../ContextDataManagerFlagDataTest.java | 25 +-- .../ContextDataManagerListenersTest.java | 28 ++- .../sdk/android/MockComponents.java | 7 +- 15 files changed, 273 insertions(+), 207 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEndToEndTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEndToEndTest.java index f10b7cf4..0440be88 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEndToEndTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientEndToEndTest.java @@ -144,4 +144,38 @@ public void clientUsesStoredFlagsIfInitializationTimesOutInPollingMode() throws assertEquals(flagValue, client.stringVariation(flagKey, "default")); } } + + @Test + public void identifyWhenPollingFailsAndCacheAlreadyExists() throws Exception { + // set up data store with flag data for ContextA and ContextB + // insert ContextA's flags + LDContext contextA = LDContext.create("ContextA"); + String flagKeyA = "flag-keyA", flagValueA = "stored-valueA"; + Flag flagA = new FlagBuilder(flagKeyA).version(1).value(LDValue.of(flagValueA)).build(); + TestUtil.writeFlagUpdateToStore(store, MOBILE_KEY, contextA, flagA); + // insert contextB's flags + LDContext contextB = LDContext.create("ContextB"); + String flagKeyB = "flag-keyB", flagValueB = "stored-valueB"; + Flag flagB = new FlagBuilder(flagKeyB).version(1).value(LDValue.of(flagValueB)).build(); + TestUtil.writeFlagUpdateToStore(store, MOBILE_KEY, contextB, flagB); + + // response to initialization for ContextA + mockPollingServer.enqueue(new MockResponse().setResponseCode(401)); + + // response to contextB's identify + mockPollingServer.enqueue(new MockResponse().setResponseCode(401)); + + LDConfig config = baseConfig() + .dataSource(Components.pollingDataSource()) + .serviceEndpoints(Components.serviceEndpoints().polling(mockPollingServerUri)) + .build(); + + LDClient client = LDClient.init(application, config, contextA, 1); + assertFalse("client should not have been initialized", client.isInitialized()); + assertFalse("client was offline", client.isOffline()); + assertEquals(flagValueA, client.stringVariation(flagKeyA, "defaultA")); + + client.identify(contextB).get(); + assertEquals(flagValueB, client.stringVariation(flagKeyB, "defaultB")); + } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AndroidPlatformState.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AndroidPlatformState.java index 597dd891..cccee355 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AndroidPlatformState.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AndroidPlatformState.java @@ -160,7 +160,12 @@ public File getCacheDir() { public void close() { connectivityChangeListeners.clear(); foregroundChangeListeners.clear(); - application.unregisterReceiver(connectivityReceiver); + try { + application.unregisterReceiver(connectivityReceiver); + } catch (IllegalArgumentException e) { + // getting here just means the receiver wasn't registered, which is our objective + } + application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java index 7cf87a00..134b04dc 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java @@ -123,6 +123,21 @@ public static ClientContextImpl forDataSource( ); } + /** + * Sets the evaluation context and returns a new instance of {@link ClientContextImpl} + * @param context to now use as the evaluation context + * @return a new instance + */ + public ClientContextImpl setEvaluationContext(LDContext context) { + return new ClientContextImpl( + super.setEvaluationContext(context), + this.diagnosticStore, + this.fetcher, + this.platformState, + this.taskExecutor + ); + } + public DiagnosticStore getDiagnosticStore() { return diagnosticStore; } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java index e660c5f6..f91e824d 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java @@ -54,7 +54,6 @@ class ConnectivityManager { private final DataSourceUpdateSink dataSourceUpdateSink; private final ConnectionInformationState connectionInformation; private final PersistentDataStoreWrapper.PerEnvironmentData environmentStore; - private final ContextDataManager contextDataManager; private final EventProcessor eventProcessor; private final PlatformState.ForegroundChangeListener foregroundListener; private final PlatformState.ConnectivityChangeListener connectivityChangeListener; @@ -66,7 +65,7 @@ class ConnectivityManager { private final AtomicBoolean started = new AtomicBoolean(); private final AtomicBoolean closed = new AtomicBoolean(); private final AtomicReference currentDataSource = new AtomicReference<>(); - private final AtomicReference currentEvaluationContext = new AtomicReference<>(); + private final AtomicReference currentContext = new AtomicReference<>(); private final AtomicReference previouslyInBackground = new AtomicReference<>(); private final LDLogger logger; private volatile boolean initialized = false; @@ -76,19 +75,23 @@ class ConnectivityManager { // data is stored; 2. to implement additional logic that does not depend on what kind of data // source we're using, like "if there was an error, update the ConnectionInformation." private class DataSourceUpdateSinkImpl implements DataSourceUpdateSink { + private final ContextDataManager contextDataManager; private final AtomicReference connectionMode = new AtomicReference<>(null); private final AtomicReference lastFailure = new AtomicReference<>(null); + DataSourceUpdateSinkImpl(ContextDataManager contextDataManager) { + this.contextDataManager = contextDataManager; + } + @Override - public void init(Map items) { - contextDataManager.initData(currentEvaluationContext.get(), - EnvironmentData.usingExistingFlagsMap(items)); + public void init(LDContext context, Map items) { + contextDataManager.initData(context, EnvironmentData.usingExistingFlagsMap(items)); // Currently, contextDataManager is responsible for firing any necessary flag change events. } @Override - public void upsert(DataModel.Flag item) { - contextDataManager.upsert(item); + public void upsert(LDContext context, DataModel.Flag item) { + contextDataManager.upsert(context, item); // Currently, contextDataManager is responsible for firing any necessary flag change events. } @@ -148,15 +151,14 @@ public void shutDown() { ) { this.baseClientContext = clientContext; this.dataSourceFactory = dataSourceFactory; - this.dataSourceUpdateSink = new DataSourceUpdateSinkImpl(); + this.dataSourceUpdateSink = new DataSourceUpdateSinkImpl(contextDataManager); this.platformState = ClientContextImpl.get(clientContext).getPlatformState(); this.eventProcessor = eventProcessor; - this.contextDataManager = contextDataManager; this.environmentStore = environmentStore; this.taskExecutor = ClientContextImpl.get(clientContext).getTaskExecutor(); this.logger = clientContext.getBaseLogger(); - currentEvaluationContext.set(clientContext.getEvaluationContext()); + currentContext.set(clientContext.getEvaluationContext()); forcedOffline.set(clientContext.isSetOffline()); LDConfig ldConfig = clientContext.getConfig(); @@ -172,20 +174,28 @@ public void shutDown() { foregroundListener = foreground -> { DataSource dataSource = currentDataSource.get(); if (dataSource == null || dataSource.needsRefresh(!foreground, - currentEvaluationContext.get())) { + currentContext.get())) { updateDataSource(true, LDUtil.noOpCallback()); } }; platformState.addForegroundChangeListener(foregroundListener); } - void setEvaluationContext(@NonNull LDContext newContext, @NonNull Callback onCompletion) { + /** + * Switches the {@link ConnectivityManager} to begin fetching/receiving information + * relevant to the context provided. This is likely to result in the teardown of existing + * connections, but the timing of that is not guaranteed. + * + * @param context to swtich to + * @param onCompletion callback that indicates when the switching is done + */ + void switchToContext(@NonNull LDContext context, @NonNull Callback onCompletion) { DataSource dataSource = currentDataSource.get(); - LDContext oldContext = currentEvaluationContext.getAndSet(newContext); - if (oldContext == newContext || oldContext.equals(newContext)) { + LDContext oldContext = currentContext.getAndSet(context); + if (oldContext == context || oldContext.equals(context)) { onCompletion.onSuccess(null); } else { - if (dataSource == null || dataSource.needsRefresh(!platformState.isForeground(), newContext)) { + if (dataSource == null || dataSource.needsRefresh(!platformState.isForeground(), context)) { updateDataSource(true, onCompletion); } else { onCompletion.onSuccess(null); @@ -204,7 +214,7 @@ private synchronized boolean updateDataSource( boolean forceOffline = forcedOffline.get(); boolean networkEnabled = platformState.isNetworkAvailable(); boolean inBackground = !platformState.isForeground(); - LDContext evaluationContext = currentEvaluationContext.get(); + LDContext context = currentContext.get(); eventProcessor.setOffline(forceOffline || !networkEnabled); eventProcessor.setInBackground(inBackground); @@ -241,7 +251,7 @@ private synchronized boolean updateDataSource( ClientContext clientContext = ClientContextImpl.forDataSource( baseClientContext, dataSourceUpdateSink, - evaluationContext, + context, inBackground, previouslyInBackground.get() ); @@ -357,13 +367,6 @@ synchronized boolean startUp(@NonNull Callback onCompletion) { return false; } initialized = false; - - // Calling initFromStoredData updates the current flag state *if* stored flags exist for - // this context. If they don't, it has no effect. Currently we do *not* return early from - // initialization just because stored flags exist; we're just making them available in case - // initialization times out or otherwise fails. - contextDataManager.initFromStoredData(currentEvaluationContext.get()); - return updateDataSource(true, onCompletion); } @@ -400,12 +403,12 @@ synchronized ConnectionInformation getConnectionInformation() { static void fetchAndSetData( FeatureFetcher fetcher, - LDContext currentContext, + LDContext contextToFetch, DataSourceUpdateSink dataSourceUpdateSink, Callback resultCallback, LDLogger logger ) { - fetcher.fetch(currentContext, new Callback() { + fetcher.fetch(contextToFetch, new Callback() { @Override public void onSuccess(String flagsJson) { EnvironmentData data; @@ -417,15 +420,15 @@ public void onSuccess(String flagsJson) { e, LDFailure.FailureType.INVALID_RESPONSE_BODY)); return; } - dataSourceUpdateSink.init(data.getAll()); + dataSourceUpdateSink.init(contextToFetch, data.getAll()); resultCallback.onSuccess(true); } @Override public void onError(Throwable e) { logger.error("Error when attempting to get flag data: [{}] [{}]: {}", - LDUtil.base64Url(currentContext), - currentContext, + LDUtil.base64Url(contextToFetch), + contextToFetch, LogValues.exceptionSummary(e)); resultCallback.onError(e); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ContextDataManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ContextDataManager.java index d004d85c..e997f033 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ContextDataManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ContextDataManager.java @@ -2,11 +2,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.launchdarkly.logging.LDLogLevel; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.LDContext; -import com.launchdarkly.sdk.android.subsystems.Callback; import com.launchdarkly.sdk.android.subsystems.ClientContext; import com.launchdarkly.sdk.android.DataModel.Flag; @@ -49,62 +49,55 @@ final class ContextDataManager { private final CopyOnWriteArrayList allFlagsListeners = new CopyOnWriteArrayList<>(); private final LDLogger logger; - private final Object writerLock = new Object(); + + /** + * This lock is to protect context, flag, and persistence operations. + */ + private final Object lock = new Object(); @NonNull private volatile LDContext currentContext; @NonNull private volatile EnvironmentData flags = new EnvironmentData(); @NonNull private volatile ContextIndex index = null; - private volatile String flagsContextId = null; ContextDataManager( @NonNull ClientContext clientContext, @NonNull PersistentDataStoreWrapper.PerEnvironmentData environmentStore, int maxCachedContexts ) { - this.currentContext = clientContext.getEvaluationContext(); this.environmentStore = environmentStore; this.maxCachedContexts = maxCachedContexts; this.taskExecutor = ClientContextImpl.get(clientContext).getTaskExecutor(); this.logger = clientContext.getBaseLogger(); + switchToContext(clientContext.getEvaluationContext()); } /** - * Returns the current context. + * Switches to providing flag data for the provided context. *

- * This piece of state is shared between LDClient and other components. The current context is - * set at initialization time, and also whenever {@link LDClient#identify(LDContext)} is - * called. The fact that the current context has changed does NOT necessarily mean we have flag - * data for that context yet; that is updated separately by {@link #initData(LDContext, EnvironmentData)}. + * If the context provided is different than the current state, switches to internally + * stored flag data and notifies flag listeners. * - * @return the current context + * @param context the to switch to */ - public @NonNull LDContext getCurrentContext() { - return currentContext; - } - - /** - * Sets the current context. - *

- * This piece of state is shared between LDClient and other components. Changing the current - * context affects only the return value of {@link #getCurrentContext()}. It does NOT mean we - * have flag data for that context yet; that is updated separately by - * {@link #initData(LDContext, EnvironmentData)}. + public void switchToContext(@NonNull LDContext context) { + synchronized (lock) { + if (context.equals(currentContext)) { + return; + } + currentContext = context; + } - * @param newContext the new context - */ - public void setCurrentContext(@NonNull LDContext newContext) { - currentContext = newContext; - } + EnvironmentData storedData = getStoredData(currentContext); + if (storedData == null) { + logger.debug("No stored flag data is available for this context"); + // here we return to not alter current in memory flag state as + // current flag state is better than empty flag state in most + // customer use cases. + return; + } - /** - * Attempts to retrieve data for the specified context, if any, from the persistent store. This - * does not affect the current context/flags state. - * - * @param context the context - * @return that context's {@link EnvironmentData} from the persistent store, or null if none - */ - public @Nullable EnvironmentData getStoredData(LDContext context) { - return environmentStore.getContextData(hashedContextId(context)); + logger.debug("Using stored flag data for this context"); + initDataInternal(context, storedData, false); } /** @@ -122,26 +115,6 @@ public void initData( initDataInternal(context, newData, true); } - /** - * Attempts to initialize the flag data state from the persistent store. If there was a set of - * stored flag data for this context, it updates the current flag state and returns true; also, - * the context is added to the list of stored contexts-- evicting old context data if necessary. - * If there was no stored data, it leaves the current flag state as is and returns false. - * - * @param context the new context - * @return true if successful - */ - public boolean initFromStoredData(@NonNull LDContext context) { - EnvironmentData storedData = getStoredData(context); - if (storedData == null) { - logger.debug("No stored flag data is available for this context"); - return false; - } - logger.debug("Using stored flag data for this context"); - initDataInternal(context, storedData, false); - return true; - } - private void initDataInternal( @NonNull LDContext context, @NonNull EnvironmentData newData, @@ -152,8 +125,12 @@ private void initDataInternal( EnvironmentData oldData; ContextIndex newIndex; - synchronized (writerLock) { - currentContext = context; + synchronized (lock) { + if (!context.equals(currentContext)) { + // if incoming new data is not for the current context, reject it. + return; + } + oldData = flags; flags = newData; if (index == null) { @@ -162,21 +139,21 @@ private void initDataInternal( newIndex = index.updateTimestamp(contextId, System.currentTimeMillis()) .prune(maxCachedContexts, removedContextIds); index = newIndex; - flagsContextId = contextId; - } - for (String removedContextId: removedContextIds) { - environmentStore.removeContextData(removedContextId); - logger.debug("Removed flag data for context {} from persistent store", removedContextId); - } - if (writeFlagsToPersistentStore && maxCachedContexts != 0) { - environmentStore.setContextData(contextId, newData); - logger.debug("Updated flag data for context {} in persistent store", contextId); + for (String removedContextId: removedContextIds) { + environmentStore.removeContextData(removedContextId); + logger.debug("Removed flag data for context {} from persistent store", removedContextId); + } + if (writeFlagsToPersistentStore && maxCachedContexts != 0) { + environmentStore.setContextData(contextId, newData); + logger.debug("Updated flag data for context {} in persistent store", contextId); + } + environmentStore.setIndex(newIndex); } + if (logger.isEnabled(LDLogLevel.DEBUG)) { logger.debug("Stored context index is now: {}", newIndex.toJson()); } - environmentStore.setIndex(newIndex); // Determine which flags were updated and notify listeners, if any Set updatedFlagKeys = new HashSet<>(); @@ -202,32 +179,6 @@ private void initDataInternal( notifyFlagListeners(updatedFlagKeys); } - /** - * Parses JSON flag data and, if successful, updates the current flag state from it and writes - * it to persistent storage; then calls the callback with a success or error result. - * - * @param context the new context - * @param newDataJson the new flag data as JSON - * @param onCompleteListener the listener to call on success or failure - */ - public void initDataFromJson( - @NonNull LDContext context, - @NonNull String newDataJson, - Callback onCompleteListener - ) { - EnvironmentData data; - try { - data = EnvironmentData.fromJson(newDataJson); - } catch (Exception e) { - logger.debug("Received invalid JSON flag data: {}", newDataJson); - onCompleteListener.onError(new LDFailure("Invalid JSON received from flags endpoint", - e, LDFailure.FailureType.INVALID_RESPONSE_BODY)); - return; - } - initData(currentContext, data); - onCompleteListener.onSuccess(null); - } - /** * Attempts to get a flag by key from the current flags. This always uses the in-memory cache, * not persistent storage. @@ -273,21 +224,26 @@ public void initDataFromJson( * implementations do not need to implement their own version checking. * * @param flag the updated flag data or deleted item placeholder - * @return true if the update was done; false if it was not done due to a too-low version + * @return true if the update was done; false if it was not done */ - public boolean upsert(@NonNull Flag flag) { + public boolean upsert(@NonNull LDContext context, @NonNull Flag flag) { EnvironmentData updatedFlags; - String contextId; - synchronized (writerLock) { + synchronized (lock) { + if (!context.equals(currentContext)) { + // if incoming data is not for the current context, reject it. + return false; + } + Flag oldFlag = flags.getFlag(flag.getKey()); if (oldFlag != null && oldFlag.getVersion() >= flag.getVersion()) { return false; } updatedFlags = flags.withFlagUpdatedOrAdded(flag); flags = updatedFlags; - contextId = flagsContextId; + + String contextId = hashedContextId(context); + environmentStore.setContextData(contextId, updatedFlags); } - environmentStore.setContextData(contextId, updatedFlags); Collection updatedFlag = Collections.singletonList(flag.getKey()); @@ -340,6 +296,18 @@ public static String hashedContextId(final LDContext context) { return HASHER.hash(context.getFullyQualifiedKey()); } + /** + * Attempts to retrieve data for the specified context, if any, from the persistent store. This + * does not affect the current context/flags state. + * + * @param context the context + * @return that context's {@link EnvironmentData} from the persistent store, or null if none + */ + @VisibleForTesting + public @Nullable EnvironmentData getStoredData(LDContext context) { + return environmentStore.getContextData(hashedContextId(context)); + } + private void notifyFlagListeners(Collection updatedFlagKeys) { if (updatedFlagKeys == null || updatedFlagKeys.isEmpty()) { return; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index 2da16942..d07bdabe 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -46,17 +46,14 @@ public class LDClient implements LDClientInterface, Closeable { // A map of each LDClient (one per environment), or null if `init` hasn't been called yet. // Will only be set once, during initialization, and the map is considered immutable. static volatile Map instances = null; - private static volatile PlatformState sharedPlatformState; - private static volatile IEnvironmentReporter environmentReporter; - private static volatile TaskExecutor sharedTaskExecutor; - private static volatile IContextModifier autoEnvContextModifier; - private static volatile IContextModifier anonymousKeyContextModifier; + private volatile ClientContextImpl clientContextImpl; + private static IContextModifier autoEnvContextModifier; + private static IContextModifier anonymousKeyContextModifier; // A lock to ensure calls to `init()` are serialized. static Object initLock = new Object(); private static volatile LDLogger sharedLogger; - private final LDConfig config; private final ContextDataManager contextDataManager; private final EventProcessor eventProcessor; @@ -115,8 +112,8 @@ public static Future init(@NonNull Application application, return new LDSuccessFuture<>(instances.get(LDConfig.primaryEnvironmentName)); } - sharedTaskExecutor = new AndroidTaskExecutor(application, logger); - sharedPlatformState = new AndroidPlatformState(application, sharedTaskExecutor, logger); + TaskExecutor sharedTaskExecutor = new AndroidTaskExecutor(application, logger); + PlatformState sharedPlatformState = new AndroidPlatformState(application, sharedTaskExecutor, logger); PersistentDataStore store = config.getPersistentDataStore() == null ? new SharedPreferencesPersistentDataStore(application, logger) : @@ -133,7 +130,7 @@ public static Future init(@NonNull Application application, if (config.isAutoEnvAttributes()) { reporterBuilder.enableCollectionFromPlatform(application); } - environmentReporter = reporterBuilder.build(); + IEnvironmentReporter environmentReporter = reporterBuilder.build(); if (config.isAutoEnvAttributes()) { autoEnvContextModifier = new AutoEnvContextModifier(persistentData, environmentReporter, logger); @@ -320,7 +317,8 @@ protected LDClient( ); fetcher = new HttpFeatureFlagFetcher(minimalContext); } - ClientContextImpl clientContext = ClientContextImpl.fromConfig( + + clientContextImpl = ClientContextImpl.fromConfig( config, mobileKey, environmentName, @@ -333,15 +331,15 @@ protected LDClient( ); this.contextDataManager = new ContextDataManager( - clientContext, + clientContextImpl, environmentStore, config.getMaxCachedContexts() ); - eventProcessor = config.events.build(clientContext); + eventProcessor = config.events.build(clientContextImpl); connectivityManager = new ConnectivityManager( - clientContext, + clientContextImpl, config.dataSource, eventProcessor, contextDataManager, @@ -365,7 +363,7 @@ public void track(String eventName) { } private void trackInternal(String eventName, LDValue data, Double metricValue) { - eventProcessor.recordCustomEvent(contextDataManager.getCurrentContext(), eventName, + eventProcessor.recordCustomEvent(clientContextImpl.getEvaluationContext(), eventName, data, metricValue); } @@ -402,8 +400,15 @@ public Future identify(LDContext context) { private void identifyInternal(@NonNull LDContext context, Callback onCompleteListener) { - contextDataManager.setCurrentContext(context); - connectivityManager.setEvaluationContext(context, onCompleteListener); + + clientContextImpl = clientContextImpl.setEvaluationContext(context); + + // Calling initFromStoredData updates the current flag state *if* stored flags exist for + // this context. If they don't, it has no effect. Currently we do *not* return early from + // initialization just because stored flags exist; we're just making them available in case + // initialization times out or otherwise fails. + contextDataManager.switchToContext(context); + connectivityManager.switchToContext(context, onCompleteListener); eventProcessor.recordIdentifyEvent(context); } @@ -497,18 +502,18 @@ private EvaluationDetail convertDetailType(EvaluationDetail deta } private EvaluationDetail variationDetailInternal(@NonNull String key, @NonNull LDValue defaultValue, boolean checkType, boolean needsReason) { + LDContext context = clientContextImpl.getEvaluationContext(); Flag flag = contextDataManager.getNonDeletedFlag(key); // returns null for nonexistent *or* deleted flag EvaluationDetail result; - LDValue value = defaultValue; if (flag == null) { logger.info("Unknown feature flag \"{}\"; returning default value", key); - eventProcessor.recordEvaluationEvent(contextDataManager.getCurrentContext(), key, + eventProcessor.recordEvaluationEvent(context, key, EventProcessor.NO_VERSION, EvaluationDetail.NO_VARIATION, defaultValue, null, defaultValue, false, null); result = EvaluationDetail.fromValue(defaultValue, EvaluationDetail.NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)); } else { - value = flag.getValue(); + LDValue value = flag.getValue(); int variation = flag.getVariation() == null ? EvaluationDetail.NO_VARIATION : flag.getVariation(); if (value.isNull()) { logger.warn("Feature flag \"{}\" retrieved with no value; returning default value", key); @@ -522,7 +527,7 @@ private EvaluationDetail variationDetailInternal(@NonNull String key, @ result = EvaluationDetail.fromValue(value, variation, flag.getReason()); } eventProcessor.recordEvaluationEvent( - contextDataManager.getCurrentContext(), + context, key, flag.getVersionForEvents(), flag.getVariation() == null ? -1 : flag.getVariation().intValue(), @@ -534,8 +539,7 @@ private EvaluationDetail variationDetailInternal(@NonNull String key, @ ); } - logger.debug("returning variation: {} flagKey: {} context key: {}", result, key, - contextDataManager.getCurrentContext().getKey()); + logger.debug("returning variation: {} flagKey: {} context key: {}", result, key, context.getKey()); return result; } @@ -549,15 +553,8 @@ public void close() throws IOException { closeInstances(); synchronized (initLock) { - if (sharedTaskExecutor != null) { - sharedTaskExecutor.close(); - } - sharedTaskExecutor = null; - - if (sharedPlatformState != null) { - sharedPlatformState.close(); - } - sharedPlatformState = null; + clientContextImpl.getTaskExecutor().close(); + clientContextImpl.getPlatformState().close(); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamingDataSource.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamingDataSource.java index a429e8df..972a1282 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamingDataSource.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamingDataSource.java @@ -51,7 +51,7 @@ final class StreamingDataSource implements DataSource { // an expectation that the server will send heartbeats at a shorter interval than that private EventSource es; - private final LDContext currentContext; + private final LDContext context; private final HttpProperties httpProperties; private final boolean evaluationReasons; final int initialReconnectDelayMillis; // visible for testing @@ -68,13 +68,13 @@ final class StreamingDataSource implements DataSource { StreamingDataSource( @NonNull ClientContext clientContext, - @NonNull LDContext currentContext, + @NonNull LDContext context, @NonNull DataSourceUpdateSink dataSourceUpdateSink, @NonNull FeatureFetcher fetcher, int initialReconnectDelayMillis, boolean streamEvenInBackground ) { - this.currentContext = currentContext; + this.context = context; this.dataSourceUpdateSink = dataSourceUpdateSink; this.fetcher = fetcher; this.streamUri = clientContext.getServiceEndpoints().getStreamingBaseUri(); @@ -121,7 +121,7 @@ public void onComment(String comment) { public void onError(Throwable t) { LDUtil.logExceptionAtErrorLevel(logger, t, "Encountered EventStream error connecting to URI: {}", - getUri(currentContext)); + getUri(context)); if (t instanceof UnsuccessfulResponseException) { if (diagnosticStore != null) { diagnosticStore.recordStreamInit(eventSourceStarted, (int) (System.currentTimeMillis() - eventSourceStarted), true); @@ -146,7 +146,7 @@ public void onError(Throwable t) { } }; - EventSource.Builder builder = new EventSource.Builder(handler, getUri(currentContext)); + EventSource.Builder builder = new EventSource.Builder(handler, getUri(context)); builder.reconnectTime(initialReconnectDelayMillis, TimeUnit.MILLISECONDS); builder.clientBuilderActions(new EventSource.Builder.ClientConfigurer() { public void configure(OkHttpClient.Builder clientBuilder) { @@ -163,7 +163,7 @@ public void configure(OkHttpClient.Builder clientBuilder) { if (useReport) { builder.method(METHOD_REPORT); - builder.body(getRequestBody(currentContext)); + builder.body(getRequestBody(context)); } builder.maxReconnectTime(MAX_RECONNECT_TIME_MS, TimeUnit.MILLISECONDS); @@ -214,7 +214,7 @@ void handle(final String name, final String eventData, e, LDFailure.FailureType.INVALID_RESPONSE_BODY)); return; } - dataSourceUpdateSink.init(data.getAll()); + dataSourceUpdateSink.init(context, data.getAll()); resultCallback.onSuccess(true); break; case PATCH: @@ -224,7 +224,7 @@ void handle(final String name, final String eventData, applyDelete(eventData, resultCallback); break; case PING: - ConnectivityManager.fetchAndSetData(fetcher, currentContext, dataSourceUpdateSink, + ConnectivityManager.fetchAndSetData(fetcher, context, dataSourceUpdateSink, resultCallback, logger); break; default: @@ -258,7 +258,7 @@ public void stop(final @NonNull Callback onCompleteListener) { @Override public boolean needsRefresh(boolean newInBackground, LDContext newEvaluationContext) { - return !newEvaluationContext.equals(currentContext) || + return !newEvaluationContext.equals(context) || (newInBackground && !streamEvenInBackground); } @@ -284,7 +284,7 @@ private void applyPatch(String json, @NonNull final Callback onComplete if (flag == null) { return; } - dataSourceUpdateSink.upsert(flag); + dataSourceUpdateSink.upsert(context, flag); onCompleteListener.onSuccess(null); } @@ -301,7 +301,7 @@ private void applyDelete(String json, @NonNull final Callback onComplet if (deleteMessage == null) { return; } - dataSourceUpdateSink.upsert(Flag.deletedItemPlaceholder(deleteMessage.key, deleteMessage.version)); + dataSourceUpdateSink.upsert(context, Flag.deletedItemPlaceholder(deleteMessage.key, deleteMessage.version)); onCompleteListener.onSuccess(null); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/TestData.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/TestData.java index ee15a167..389da4ea 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/TestData.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/TestData.java @@ -518,17 +518,17 @@ private static int variationForBoolean(boolean value) { } private final class DataSourceImpl implements DataSource { - final LDContext currentContext; + final LDContext context; final DataSourceUpdateSink updates; - DataSourceImpl(LDContext currentContext, DataSourceUpdateSink updates) { - this.currentContext = currentContext; + DataSourceImpl(LDContext context, DataSourceUpdateSink updates) { + this.context = context; this.updates = updates; } @Override public void start(@NonNull Callback resultCallback) { - updates.init(makeInitData(currentContext)); + updates.init(context, makeInitData(context)); updates.setStatus(ConnectionInformation.ConnectionMode.STREAMING, null); resultCallback.onSuccess(true); } @@ -539,8 +539,8 @@ public void stop(@NonNull Callback completionCallback) { } void doUpdate(FlagBuilder flagBuilder, int version) { - Flag flag = flagBuilder.createFlag(version ,currentContext); - updates.upsert(flag); + Flag flag = flagBuilder.createFlag(version , context); + updates.upsert(context, flag); } } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java index c1974ef0..49dd58a9 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java @@ -105,6 +105,29 @@ protected ClientContext(ClientContext copy) { ); } + /** + * Sets the evaluation context and returns a new instance of {@link ClientContext} + * @param context to now use as the evaluation context + * @return a new instance + */ + protected ClientContext setEvaluationContext(LDContext context) { + return new ClientContext( + this.mobileKey, + this.environmentReporter, + this.baseLogger, + this.config, + this.dataSourceUpdateSink, + this.environmentName, + this.evaluationReasons, + context, + this.http, + this.inBackground, + this.previouslyInBackground, + this.serviceEndpoints, + this.setOffline + ); + } + /** * @return the {@link IEnvironmentReporter} for this client context */ diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSourceUpdateSink.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSourceUpdateSink.java index 26df47b5..c1d8be9a 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSourceUpdateSink.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSourceUpdateSink.java @@ -3,6 +3,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.android.ConnectionInformation; import com.launchdarkly.sdk.android.DataModel; @@ -19,7 +20,7 @@ public interface DataSourceUpdateSink { * * @param items a map of flag keys to flag evaluation results */ - void init(@NonNull Map items); + void init(@NonNull LDContext context, @NonNull Map items); /** * Updates or inserts an item. If an item already exists with the same key, the operation will @@ -30,7 +31,7 @@ public interface DataSourceUpdateSink { * * @param item the new evaluation result data (or a deleted item placeholder) */ - void upsert(@NonNull DataModel.Flag item); + void upsert(@NonNull LDContext context, @NonNull DataModel.Flag item); /** * Informs the SDK of a change in the data source's status or the connection mode. diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java index 9ec6a413..903f7751 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java @@ -494,9 +494,9 @@ public void refreshDataSourceForNewContext() throws Exception { replayAll(); LDContext context2 = LDContext.create("context2"); - contextDataManager.setCurrentContext(context2); + contextDataManager.switchToContext(context2); AwaitableCallback done = new AwaitableCallback<>(); - connectivityManager.setEvaluationContext(context2, done); + connectivityManager.switchToContext(context2, done); done.await(); verifyAll(); // verifies eventProcessor calls @@ -524,8 +524,8 @@ public void refreshDataSourceWhileOffline() { replayAll(); LDContext context2 = LDContext.create("context2"); - contextDataManager.setCurrentContext(context2); - connectivityManager.setEvaluationContext(context2, LDUtil.noOpCallback()); + contextDataManager.switchToContext(context2); + connectivityManager.switchToContext(context2, LDUtil.noOpCallback()); verifyAll(); // verifies eventProcessor calls verifyNoMoreDataSourcesWereCreated(); @@ -552,8 +552,8 @@ public void refreshDataSourceWhileInBackgroundWithBackgroundPollingDisabled() { replayAll(); LDContext context2 = LDContext.create("context2"); - contextDataManager.setCurrentContext(context2); - connectivityManager.setEvaluationContext(context2, LDUtil.noOpCallback()); + contextDataManager.switchToContext(context2); + connectivityManager.switchToContext(context2, LDUtil.noOpCallback()); verifyAll(); // verifies eventProcessor calls verifyNoMoreDataSourcesWereCreated(); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerContextCachingTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerContextCachingTest.java index ce40828a..8693166c 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerContextCachingTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerContextCachingTest.java @@ -33,6 +33,7 @@ public void canCacheManyContextsWithNegativeMaxCachedContexts() { int numContexts = 20; for (int i = 1; i <= numContexts; i++) { + manager.switchToContext(makeContext(i)); manager.initData(makeContext(i), makeFlagData(i)); } @@ -48,6 +49,7 @@ public void deletesExcessContexts() { ContextDataManager manager = createDataManager(maxCachedContexts); for (int i = 1; i <= maxCachedContexts + excess; i++) { + manager.switchToContext(makeContext(i)); manager.initData(makeContext(i), makeFlagData(i)); } @@ -64,11 +66,13 @@ public void deletesExcessContextsFromPreviousManagerInstance() { ContextDataManager manager = createDataManager(1); for (int i = 1; i <= 2; i++) { + manager.switchToContext(makeContext(i)); manager.initData(makeContext(i), makeFlagData(i)); assertContextIsCached(makeContext(i), makeFlagData(i)); } ContextDataManager newManagerInstance = createDataManager(1); + newManagerInstance.switchToContext(makeContext(3)); newManagerInstance.initData(makeContext(3), makeFlagData(3)); assertContextIsNotCached(makeContext(1)); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerFlagDataTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerFlagDataTest.java index 8c5502f8..b2606e25 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerFlagDataTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerFlagDataTest.java @@ -23,9 +23,8 @@ public void getStoredDataNotFound() { public void initDataUpdatesStoredData() { EnvironmentData data = new DataSetBuilder().add(new FlagBuilder("flag1").build()).build(); ContextDataManager manager = createDataManager(); + manager.switchToContext(CONTEXT); manager.initData(CONTEXT, data); - - assertSame(CONTEXT, manager.getCurrentContext()); assertDataSetsEqual(data, createDataManager().getStoredData(CONTEXT)); } @@ -33,19 +32,18 @@ public void initDataUpdatesStoredData() { public void initFromStoredData() { EnvironmentData data = new DataSetBuilder().add(new FlagBuilder("flag1").build()).build(); ContextDataManager manager1 = createDataManager(); + manager1.switchToContext(CONTEXT); manager1.initData(CONTEXT, data); ContextDataManager manager2 = createDataManager(); - assertTrue(manager2.initFromStoredData(CONTEXT)); + manager2.switchToContext(CONTEXT); assertDataSetsEqual(data, manager2.getAllNonDeleted()); - assertSame(CONTEXT, manager2.getCurrentContext()); } @Test public void initFromStoredDataNotFound() { ContextDataManager manager = createDataManager(); - assertFalse(manager.initFromStoredData(CONTEXT)); - assertSame(INITIAL_CONTEXT, manager.getCurrentContext()); + manager.switchToContext(CONTEXT); } @Test @@ -67,6 +65,7 @@ public void getKnownFlag() { Flag flag = new FlagBuilder("flag1").build(); EnvironmentData data = new DataSetBuilder().add(flag).build(); ContextDataManager manager = createDataManager(); + manager.switchToContext(CONTEXT); manager.initData(CONTEXT, data); assertSame(flag, manager.getNonDeletedFlag(flag.getKey())); @@ -107,6 +106,7 @@ public void getAllReturnsFlags() { flag2 = new FlagBuilder("flag2").version(2).build(); EnvironmentData initialData = new DataSetBuilder().add(flag1).add(flag2).build(); ContextDataManager manager = createDataManager(); + manager.switchToContext(CONTEXT); manager.initData(CONTEXT, initialData); EnvironmentData actualData = manager.getAllNonDeleted(); @@ -120,6 +120,7 @@ public void getAllFiltersOutDeletedFlags() { deletedFlag = Flag.deletedItemPlaceholder("flag2", 2); EnvironmentData initialData = new DataSetBuilder().add(flag1).add(deletedFlag).build(); ContextDataManager manager = createDataManager(); + manager.switchToContext(CONTEXT); manager.initData(CONTEXT, initialData); EnvironmentData expectedData = new DataSetBuilder().add(flag1).build(); @@ -132,9 +133,10 @@ public void upsertAddsFlag() { flag2 = new FlagBuilder("flag2").version(2).build(); EnvironmentData initialData = new DataSetBuilder().add(flag1).build(); ContextDataManager manager = createDataManager(); + manager.switchToContext(CONTEXT); manager.initData(CONTEXT, initialData); - manager.upsert(flag2); + manager.upsert(CONTEXT, flag2); assertFlagsEqual(flag2, manager.getNonDeletedFlag(flag2.getKey())); @@ -149,9 +151,10 @@ public void upsertUpdatesFlag() { flag1b = new FlagBuilder(flag1a.getKey()).version(2).value(false).build(); EnvironmentData initialData = new DataSetBuilder().add(flag1a).build(); ContextDataManager manager = createDataManager(); + manager.switchToContext(CONTEXT); manager.initData(CONTEXT, initialData); - manager.upsert(flag1b); + manager.upsert(CONTEXT, flag1b); assertFlagsEqual(flag1b, manager.getNonDeletedFlag(flag1a.getKey())); @@ -183,9 +186,10 @@ public void upsertDeletesFlag() { deletedFlag2 = Flag.deletedItemPlaceholder(flag2.getKey(), 2); EnvironmentData initialData = new DataSetBuilder().add(flag1).add(flag2).build(); ContextDataManager manager = createDataManager(); + manager.switchToContext(CONTEXT); manager.initData(CONTEXT, initialData); - manager.upsert(deletedFlag2); + manager.upsert(CONTEXT, deletedFlag2); assertFlagsEqual(flag1, manager.getNonDeletedFlag(flag1.getKey())); assertNull(manager.getNonDeletedFlag(flag2.getKey())); @@ -215,9 +219,10 @@ public void upsertDoesNotDeleteFlagWithLowerVersion() { private void upsertDoesNotUpdateFlag(Flag initialFlag, Flag updatedFlag) { EnvironmentData initialData = new DataSetBuilder().add(initialFlag).build(); ContextDataManager manager = createDataManager(); + manager.switchToContext(CONTEXT); manager.initData(CONTEXT, initialData); - manager.upsert(updatedFlag); + manager.upsert(CONTEXT, updatedFlag); assertFlagsEqual(initialFlag, manager.getNonDeletedFlag(initialFlag.getKey())); assertDataSetsEqual(initialData, manager.getAllNonDeleted()); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerListenersTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerListenersTest.java index e6550290..5b176507 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerListenersTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerListenersTest.java @@ -44,7 +44,8 @@ public void listenerIsCalledOnUpdate() throws InterruptedException { manager.registerListener(flag.getKey(), listener); manager.registerAllFlagsListener(allFlagsListener); - manager.upsert(flag); + manager.switchToContext(CONTEXT); + manager.upsert(CONTEXT, flag); assertEquals(flag.getKey(), listener.expectUpdate(5, TimeUnit.SECONDS)); assertEquals(flag.getKey(), allFlagsListener.expectUpdate(5, TimeUnit.SECONDS)); @@ -60,7 +61,8 @@ public void listenerIsCalledOnDelete() throws InterruptedException { manager.registerListener(flag.getKey(), listener); manager.registerAllFlagsListener(allFlagsListener); - manager.upsert(flag); + manager.switchToContext(CONTEXT); + manager.upsert(CONTEXT, flag); assertEquals(flag.getKey(), listener.expectUpdate(5, TimeUnit.SECONDS)); assertEquals(flag.getKey(), allFlagsListener.expectUpdate(5, TimeUnit.SECONDS)); @@ -74,12 +76,16 @@ public void listenerIsNotCalledAfterUnregistering() throws InterruptedException AwaitableFlagListener allFlagsListener = new AwaitableFlagListener(); manager.registerListener(flag.getKey(), listener); - manager.unregisterListener(flag.getKey(), listener); manager.registerAllFlagsListener(allFlagsListener); - manager.unregisterAllFlagsListener(allFlagsListener); - manager.upsert(flag); + manager.upsert(INITIAL_CONTEXT, flag); + assertEquals(flag.getKey(), listener.expectUpdate(5, TimeUnit.SECONDS)); + assertEquals(flag.getKey(), allFlagsListener.expectUpdate(5, TimeUnit.SECONDS)); + + manager.unregisterListener(flag.getKey(), listener); + manager.unregisterAllFlagsListener(allFlagsListener); + manager.upsert(INITIAL_CONTEXT, flag); // Unfortunately we are testing that an asynchronous method is *not* called, we just have to // wait a bit to be sure. listener.expectNoUpdates(100, TimeUnit.MILLISECONDS); @@ -91,7 +97,8 @@ public void listenerIsNotCalledAfterUnregistering() throws InterruptedException @Test public void listenerIsCalledAfterInitData() { - LDContext context = LDContext.create("user"); + LDContext context1 = LDContext.create("user1"); + LDContext context2 = LDContext.create("user2"); final String FLAG_KEY = "key"; Flag flagState1 = new FlagBuilder(FLAG_KEY).value(LDValue.of(1)).build(); @@ -104,7 +111,8 @@ public void listenerIsCalledAfterInitData() { manager.registerAllFlagsListener(all1); // change the data - manager.upsert(flagState1); + manager.switchToContext(context1); + manager.upsert(context1, flagState1); // verify callbacks assertEquals(FLAG_KEY, specific1.expectUpdate(5, TimeUnit.SECONDS)); @@ -119,7 +127,8 @@ public void listenerIsCalledAfterInitData() { // simulate switching context Flag flagState2 = new FlagBuilder(FLAG_KEY).value(LDValue.of(2)).build(); EnvironmentData envData = new EnvironmentData().withFlagUpdatedOrAdded(flagState2); - manager.initData(context, envData); + manager.switchToContext(context2); + manager.initData(context2, envData); // verify callbacks assertEquals(FLAG_KEY, specific2.expectUpdate(5, TimeUnit.SECONDS)); @@ -136,7 +145,8 @@ public void listenerIsCalledOnMainThread() throws InterruptedException { manager.registerListener(flag.getKey(), listener); manager.registerAllFlagsListener(allFlagsListener); - manager.upsert(flag); + manager.switchToContext(CONTEXT); + manager.upsert(CONTEXT, flag); listener.expectUpdate(5, TimeUnit.SECONDS); allFlagsListener.expectUpdate(5, TimeUnit.SECONDS); diff --git a/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockComponents.java b/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockComponents.java index 87dd008a..23eb3e07 100644 --- a/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockComponents.java +++ b/shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockComponents.java @@ -11,6 +11,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.android.env.IEnvironmentReporter; import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; import com.launchdarkly.sdk.android.subsystems.Callback; @@ -61,7 +62,7 @@ public static class MockDataSourceUpdateSink implements DataSourceUpdateSink { public final BlockingQueue upserts = new LinkedBlockingQueue<>(); @Override - public void init(@NonNull Map items) { + public void init(@NonNull LDContext context, @NonNull Map items) { inits.add(items); } @@ -70,7 +71,7 @@ public Map expectInit() { } @Override - public void upsert(@NonNull DataModel.Flag item) { + public void upsert(@NonNull LDContext context, @NonNull DataModel.Flag item) { upserts.add(item); } @@ -115,7 +116,7 @@ public static DataSource successfulDataSource( @Override public void start(@NonNull Callback resultCallback) { new Thread(() -> { - clientContext.getDataSourceUpdateSink().init(data.getAll()); + clientContext.getDataSourceUpdateSink().init(clientContext.getEvaluationContext(), data.getAll()); clientContext.getDataSourceUpdateSink().setStatus(connectionMode, null); resultCallback.onSuccess(true); }).start();