From 943229c0dda2bb8d33a474f03c0680a6a0605ee3 Mon Sep 17 00:00:00 2001 From: gthea Date: Wed, 4 Dec 2024 11:48:31 -0300 Subject: [PATCH 1/2] Rollout cache manager in SDK init (#718) --- .../java/fake/SynchronizerSpyImpl.java | 9 +- .../database/DatabaseInitializationTest.java | 5 + .../attributes/AttributesIntegrationTest.java | 6 +- .../encryption/EncryptionTest.java | 2 +- .../LargeSegmentsStreamingTest.java | 4 +- .../shared/SharedClientsIntegrationTest.java | 4 +- .../telemetry/TelemetryOccupancyTest.java | 24 ++++- .../android/client/SplitFactoryHelper.java | 97 ++++++++++++++++--- .../android/client/SplitFactoryImpl.java | 54 +++++------ .../attributes/AttributesManagerImpl.java | 4 +- .../service/executor/SplitTaskFactory.java | 5 + .../executor/SplitTaskFactoryImpl.java | 8 ++ .../synchronizer/RolloutCacheManager.java | 2 +- .../synchronizer/RolloutCacheManagerImpl.java | 41 ++++++-- .../synchronizer/SynchronizerImpl.java | 9 +- .../MySegmentsSynchronizerRegistry.java | 6 +- .../MySegmentsSynchronizerRegistryImpl.java | 13 +-- .../shared/ClientComponentsRegisterImpl.java | 4 +- .../storage/cipher/ApplyCipherTask.java | 3 +- .../android/client/SplitFactoryHelperTest.kt | 74 +++++++++----- .../attributes/AttributesManagerImplTest.java | 8 +- .../client/service/SynchronizerTest.java | 9 +- .../synchronizer/RolloutCacheManagerTest.kt | 31 ++++-- ...ySegmentsSynchronizerRegistryImplTest.java | 37 +++---- .../ClientComponentsRegisterImplTest.java | 4 +- 25 files changed, 316 insertions(+), 147 deletions(-) diff --git a/src/androidTest/java/fake/SynchronizerSpyImpl.java b/src/androidTest/java/fake/SynchronizerSpyImpl.java index 6971258eb..2203f3913 100644 --- a/src/androidTest/java/fake/SynchronizerSpyImpl.java +++ b/src/androidTest/java/fake/SynchronizerSpyImpl.java @@ -3,6 +3,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; +import io.split.android.client.api.Key; import io.split.android.client.dtos.Event; import io.split.android.client.impressions.Impression; import io.split.android.client.service.synchronizer.Synchronizer; @@ -122,12 +123,12 @@ public void unregisterAttributesSynchronizer(String userKey) { } @Override - public void registerMySegmentsSynchronizer(String userKey, MySegmentsSynchronizer mySegmentsSynchronizer) { - ((MySegmentsSynchronizerRegistry) mSynchronizer).registerMySegmentsSynchronizer(userKey, mySegmentsSynchronizer); + public void registerMySegmentsSynchronizer(Key key, MySegmentsSynchronizer mySegmentsSynchronizer) { + ((MySegmentsSynchronizerRegistry) mSynchronizer).registerMySegmentsSynchronizer(key, mySegmentsSynchronizer); } @Override - public void unregisterMySegmentsSynchronizer(String userKey) { - ((MySegmentsSynchronizerRegistry) mSynchronizer).unregisterMySegmentsSynchronizer(userKey); + public void unregisterMySegmentsSynchronizer(Key key) { + ((MySegmentsSynchronizerRegistry) mSynchronizer).unregisterMySegmentsSynchronizer(key); } } diff --git a/src/androidTest/java/tests/database/DatabaseInitializationTest.java b/src/androidTest/java/tests/database/DatabaseInitializationTest.java index 440dde5f5..a4201e1a9 100644 --- a/src/androidTest/java/tests/database/DatabaseInitializationTest.java +++ b/src/androidTest/java/tests/database/DatabaseInitializationTest.java @@ -182,6 +182,11 @@ public void usingNullPrefixResultsInIgnoredPrefix() { private static String[] getDbList(Context context) { // remove -journal dbs since we're not interested in them + try { + Thread.sleep(500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } return Arrays.stream(context.databaseList()).filter(db -> !db.endsWith("-journal")).toArray(String[]::new); } } diff --git a/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java b/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java index 9b9a47401..085d99cb3 100644 --- a/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java +++ b/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java @@ -108,7 +108,7 @@ public void testPersistentAttributes2() throws InterruptedException { // 3. Perform clear and verify there are no attributes on DB client.clearAttributes(); - countDownLatch.await(1, TimeUnit.SECONDS); + countDownLatch.await(7, TimeUnit.SECONDS); Assert.assertNull(mRoomDb.attributesDao().getByUserKey(userKey)); } @@ -153,7 +153,7 @@ public void testPersistentAttributesWithMultiClient2() throws InterruptedExcepti // 2. Clear second client's attributes and check DB entry has been cleared client2.clearAttributes(); - countDownLatch.await(1, TimeUnit.SECONDS); // waiting since DB operations are async + countDownLatch.await(7, TimeUnit.SECONDS); // waiting since DB operations are async Assert.assertNull(mRoomDb.attributesDao().getByUserKey("new_key")); // 3. Verify evaluation with first client uses attribute @@ -162,7 +162,7 @@ public void testPersistentAttributesWithMultiClient2() throws InterruptedExcepti // 4. Perform clear and verify there are no attributes on DB client.clearAttributes(); - countDownLatch.await(1, TimeUnit.SECONDS); + countDownLatch.await(7, TimeUnit.SECONDS); Assert.assertNull(mRoomDb.attributesDao().getByUserKey(matchingKey)); } diff --git a/src/androidTest/java/tests/integration/encryption/EncryptionTest.java b/src/androidTest/java/tests/integration/encryption/EncryptionTest.java index b8ef6be27..6a8561825 100644 --- a/src/androidTest/java/tests/integration/encryption/EncryptionTest.java +++ b/src/androidTest/java/tests/integration/encryption/EncryptionTest.java @@ -127,7 +127,7 @@ public void onPostExecutionView(SplitClient client) { } }); - assertTrue(latch.await(2, TimeUnit.SECONDS)); + assertTrue(latch.await(5, TimeUnit.SECONDS)); mLifecycleManager.simulateOnPause(); Thread.sleep(200); mLifecycleManager.simulateOnResume(); diff --git a/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java b/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java index 2b95f70ca..f5cfe3cdc 100644 --- a/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java +++ b/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java @@ -67,8 +67,8 @@ public void setUp() throws IOException, InterruptedException { public void unboundedLargeSegmentsUpdateTriggersSdkUpdate() throws IOException, InterruptedException { TestSetup testSetup = getTestSetup(); - boolean mySegmentsAwait = mLatches.get(MY_SEGMENTS).await(10, TimeUnit.SECONDS); - boolean splitsAwait = mLatches.get(SPLIT_CHANGES).await(10, TimeUnit.SECONDS); + boolean mySegmentsAwait = mLatches.get(MY_SEGMENTS).await(15, TimeUnit.SECONDS); + boolean splitsAwait = mLatches.get(SPLIT_CHANGES).await(15, TimeUnit.SECONDS); String initialSegmentList = testSetup.database.myLargeSegmentDao().getByUserKey(IntegrationHelper.dummyUserKey().matchingKey()).getSegmentList(); mRandomizeMyLargeSegments.set(true); diff --git a/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java b/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java index feecae9a8..a12642bee 100644 --- a/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java +++ b/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java @@ -159,8 +159,8 @@ public void onPostExecutionView(SplitClient client) { } }); insertSplitsIntoDb(); - readyLatch.await(10, TimeUnit.SECONDS); - readyLatch2.await(10, TimeUnit.SECONDS); + readyLatch.await(15, TimeUnit.SECONDS); + readyLatch2.await(15, TimeUnit.SECONDS); assertEquals(1, readyCount.get()); assertEquals(1, readyCount2.get()); diff --git a/src/androidTest/java/tests/integration/telemetry/TelemetryOccupancyTest.java b/src/androidTest/java/tests/integration/telemetry/TelemetryOccupancyTest.java index 87cf5669b..e13080d3a 100644 --- a/src/androidTest/java/tests/integration/telemetry/TelemetryOccupancyTest.java +++ b/src/androidTest/java/tests/integration/telemetry/TelemetryOccupancyTest.java @@ -6,16 +6,16 @@ import org.junit.Test; import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; import helper.TestableSplitConfigBuilder; import io.split.android.client.SplitClientConfig; -import io.split.android.client.storage.db.StorageFactory; import io.split.android.client.telemetry.model.streaming.OccupancyPriStreamingEvent; import io.split.android.client.telemetry.model.streaming.OccupancySecStreamingEvent; import io.split.android.client.telemetry.model.streaming.StreamingEvent; import io.split.android.client.telemetry.model.streaming.TokenRefreshStreamingEvent; -import io.split.android.client.telemetry.storage.TelemetryStorage; import tests.integration.streaming.OccupancyBaseTest; public class TelemetryOccupancyTest extends OccupancyBaseTest { @@ -35,12 +35,19 @@ public class TelemetryOccupancyTest extends OccupancyBaseTest { @Test public void telemetryOccupancyPriStreamingEvent() throws InterruptedException, IOException { + new CountDownLatch(1); getSplitFactory(mTelemetryEnabledConfig); pushOccupancy(PRIMARY_CHANNEL, 1); - sleep(2000); - List streamingEvents = mTelemetryStorage.popStreamingEvents(); + long startTime = System.currentTimeMillis(); + List streamingEvents = new ArrayList<>(); + streamingEvents = mTelemetryStorage.popStreamingEvents(); + while (System.currentTimeMillis() - startTime < 5000 && + !streamingEvents.stream().anyMatch(event -> event instanceof OccupancyPriStreamingEvent)) { + Thread.sleep(100); + streamingEvents = mTelemetryStorage.popStreamingEvents(); + } assertTrue(streamingEvents.stream().anyMatch(event -> event instanceof OccupancyPriStreamingEvent)); } @@ -51,7 +58,14 @@ public void telemetryOccupancySecStreamingEvent() throws InterruptedException, I pushOccupancy(SECONDARY_CHANNEL, 1); sleep(2000); - List streamingEvents = mTelemetryStorage.popStreamingEvents(); + long startTime = System.currentTimeMillis(); + List streamingEvents = new ArrayList<>(); + streamingEvents = mTelemetryStorage.popStreamingEvents(); + while (System.currentTimeMillis() - startTime < 5000 && + !streamingEvents.stream().anyMatch(event -> event instanceof OccupancySecStreamingEvent)) { + Thread.sleep(100); + streamingEvents = mTelemetryStorage.popStreamingEvents(); + } assertTrue(streamingEvents.stream().anyMatch(event -> event instanceof OccupancySecStreamingEvent)); } diff --git a/src/main/java/io/split/android/client/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index 3757b633c..bfb9b6bde 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.util.Pair; import androidx.work.WorkManager; @@ -22,11 +23,15 @@ import io.split.android.client.common.CompressionUtilProvider; import io.split.android.client.events.EventsManagerCoordinator; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.lifecycle.SplitLifecycleManager; import io.split.android.client.network.HttpClient; import io.split.android.client.network.SdkTargetPath; import io.split.android.client.network.SplitHttpHeadersBuilder; import io.split.android.client.service.ServiceFactory; import io.split.android.client.service.SplitApiFacade; +import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskFactory; @@ -56,6 +61,8 @@ import io.split.android.client.service.sseclient.sseclient.SseHandler; import io.split.android.client.service.sseclient.sseclient.SseRefreshTokenTimer; import io.split.android.client.service.sseclient.sseclient.StreamingComponents; +import io.split.android.client.service.synchronizer.RolloutCacheManager; +import io.split.android.client.service.synchronizer.RolloutCacheManagerImpl; import io.split.android.client.service.synchronizer.SyncGuardian; import io.split.android.client.service.synchronizer.SyncGuardianImpl; import io.split.android.client.service.synchronizer.SyncManager; @@ -70,7 +77,6 @@ import io.split.android.client.shared.ClientComponentsRegisterImpl; import io.split.android.client.shared.UserConsent; import io.split.android.client.storage.attributes.PersistentAttributesStorage; -import io.split.android.client.storage.cipher.EncryptionMigrationTask; import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.cipher.SplitCipherFactory; import io.split.android.client.storage.cipher.SplitEncryptionLevel; @@ -86,6 +92,7 @@ import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; import io.split.android.client.telemetry.storage.TelemetryStorage; import io.split.android.client.utils.Utils; +import io.split.android.client.utils.logger.Logger; class SplitFactoryHelper { private static final int DB_MAGIC_CHARS_COUNT = 4; @@ -399,21 +406,10 @@ public ProcessStrategy getImpressionStrategy(SplitTaskExecutor splitTaskExecutor .getStrategy(config.impressionsMode()); } - SplitCipher migrateEncryption(String apiKey, - SplitRoomDatabase splitDatabase, - SplitTaskExecutor splitTaskExecutor, - final boolean encryptionEnabled, - SplitTaskExecutionListener executionListener) { - - SplitCipher toCipher = SplitCipherFactory.create(apiKey, encryptionEnabled ? SplitEncryptionLevel.AES_128_CBC : + @Nullable + SplitCipher getCipher(String apiKey, boolean encryptionEnabled) { + return SplitCipherFactory.create(apiKey, encryptionEnabled ? SplitEncryptionLevel.AES_128_CBC : SplitEncryptionLevel.NONE); - splitTaskExecutor.submit(new EncryptionMigrationTask(apiKey, - splitDatabase, - encryptionEnabled, - toCipher), - executionListener); - - return toCipher; } @Nullable @@ -486,4 +482,75 @@ public URI build(String matchingKey) throws URISyntaxException { return SdkTargetPath.mySegments(mEndpoint, matchingKey); } } + + static class Initializer implements Runnable { + + private final RolloutCacheManager mRolloutCacheManager; + private final SplitTaskExecutionListener mListener; + + Initializer( + String apiToken, + SplitClientConfig config, + SplitTaskFactory splitTaskFactory, + SplitRoomDatabase splitDatabase, + SplitCipher splitCipher, + EventsManagerCoordinator eventsManagerCoordinator, + SplitTaskExecutor splitTaskExecutor, + SplitSingleThreadTaskExecutor splitSingleThreadTaskExecutor, + SplitStorageContainer storageContainer, + SyncManager syncManager, + SplitLifecycleManager lifecycleManager) { + + this(new RolloutCacheManagerImpl(config, + storageContainer, + splitTaskFactory.createCleanUpDatabaseTask(System.currentTimeMillis() / 1000), + splitTaskFactory.createEncryptionMigrationTask(apiToken, splitDatabase, config.encryptionEnabled(), splitCipher)), + new Listener(eventsManagerCoordinator, splitTaskExecutor, splitSingleThreadTaskExecutor, syncManager, lifecycleManager)); + } + + @VisibleForTesting + Initializer(RolloutCacheManager rolloutCacheManager, SplitTaskExecutionListener listener) { + mRolloutCacheManager = rolloutCacheManager; + mListener = listener; + } + + @Override + public void run() { + mRolloutCacheManager.validateCache(mListener); + } + + static class Listener implements SplitTaskExecutionListener { + + private final EventsManagerCoordinator mEventsManagerCoordinator; + private final SplitTaskExecutor mSplitTaskExecutor; + private final SplitSingleThreadTaskExecutor mSplitSingleThreadTaskExecutor; + private final SyncManager mSyncManager; + private final SplitLifecycleManager mLifecycleManager; + + Listener(EventsManagerCoordinator eventsManagerCoordinator, + SplitTaskExecutor splitTaskExecutor, + SplitSingleThreadTaskExecutor splitSingleThreadTaskExecutor, + SyncManager syncManager, + SplitLifecycleManager lifecycleManager) { + mEventsManagerCoordinator = eventsManagerCoordinator; + mSplitTaskExecutor = splitTaskExecutor; + mSplitSingleThreadTaskExecutor = splitSingleThreadTaskExecutor; + mSyncManager = syncManager; + mLifecycleManager = lifecycleManager; + } + + @Override + public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { + mEventsManagerCoordinator.notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); + + mSplitTaskExecutor.resume(); + mSplitSingleThreadTaskExecutor.resume(); + + mSyncManager.start(); + mLifecycleManager.register(mSyncManager); + + Logger.i("Android SDK initialized!"); + } + } + } } diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index b5c0252e3..34d3abd63 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -18,7 +18,6 @@ import io.split.android.client.api.Key; import io.split.android.client.common.CompressionUtilProvider; import io.split.android.client.events.EventsManagerCoordinator; -import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.factory.FactoryMonitor; import io.split.android.client.factory.FactoryMonitorImpl; import io.split.android.client.impressions.ImpressionListener; @@ -29,8 +28,6 @@ import io.split.android.client.network.HttpClientImpl; import io.split.android.client.service.SplitApiFacade; import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor; -import io.split.android.client.service.executor.SplitTaskExecutionInfo; -import io.split.android.client.service.executor.SplitTaskExecutionListener; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskExecutorImpl; import io.split.android.client.service.executor.SplitTaskFactory; @@ -84,9 +81,6 @@ public class SplitFactoryImpl implements SplitFactory { private final SplitClientContainer mClientContainer; private final UserConsentManager mUserConsentManager; - @SuppressWarnings("FieldCanBeLocal") // keeping the reference on purpose - private final SplitTaskExecutionListener mMigrationExecutionListener; - public SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull SplitClientConfig config, @NonNull Context context) throws URISyntaxException { this(apiToken, key, config, context, @@ -156,27 +150,17 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp } else { splitDatabase = testDatabase; Logger.d("Using test database"); - System.out.println("USING TEST DB: " + testDatabase); } defaultHttpClient.addHeaders(factoryHelper.buildHeaders(config, apiToken)); defaultHttpClient.addStreamingHeaders(factoryHelper.buildStreamingHeaders(apiToken)); SplitTaskExecutor splitTaskExecutor = new SplitTaskExecutorImpl(); + splitTaskExecutor.pause(); EventsManagerCoordinator mEventsManagerCoordinator = new EventsManagerCoordinator(); - mMigrationExecutionListener = new SplitTaskExecutionListener() { - @Override - public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { - mEventsManagerCoordinator.notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); - } - }; - SplitCipher splitCipher = factoryHelper.migrateEncryption(mApiKey, - splitDatabase, - splitTaskExecutor, - config.encryptionEnabled(), - mMigrationExecutionListener); + SplitCipher splitCipher = factoryHelper.getCipher(apiToken, config.encryptionEnabled()); ScheduledThreadPoolExecutor impressionsObserverExecutor = new ScheduledThreadPoolExecutor(1, new ThreadPoolExecutor.CallerRunsPolicy()); @@ -197,9 +181,9 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { config, splitApiFacade, mStorageContainer, splitsFilterQueryStringFromConfig, getFlagsSpec(testingConfig), mEventsManagerCoordinator, filters, flagSetsFilter, testingConfig); - cleanUpDabase(splitTaskExecutor, splitTaskFactory); WorkManagerWrapper workManagerWrapper = factoryHelper.buildWorkManagerWrapper(context, config, apiToken, databaseName, filters); SplitSingleThreadTaskExecutor splitSingleThreadTaskExecutor = new SplitSingleThreadTaskExecutor(); + splitSingleThreadTaskExecutor.pause(); ImpressionManager impressionManager = new StrategyImpressionManager(factoryHelper.getImpressionStrategy(splitTaskExecutor, splitTaskFactory, mStorageContainer, config)); final RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory = new RetryBackoffCounterTimerFactory(); @@ -254,8 +238,6 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { mLifecycleManager = testLifecycleManager; } - mLifecycleManager.register(mSyncManager); - ExecutorService impressionsLoggingTaskExecutor = factoryHelper.getImpressionsLoggingTaskExecutor(); final ImpressionListener splitImpressionListener = new SyncImpressionListener(mSyncManager, impressionsLoggingTaskExecutor); @@ -328,21 +310,35 @@ public void run() { } }); - // Initialize default client - client(); - SplitParser mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer(), mStorageContainer.getMyLargeSegmentsStorageContainer()); - mManager = new SplitManagerImpl( - mStorageContainer.getSplitsStorage(), - new SplitValidatorImpl(), mSplitParser); + // Set up async initialization + final SplitFactoryHelper.Initializer initializer = new SplitFactoryHelper.Initializer(apiToken, + config, + splitTaskFactory, + splitDatabase, + splitCipher, + mEventsManagerCoordinator, + splitTaskExecutor, + splitSingleThreadTaskExecutor, + mStorageContainer, + mSyncManager, + mLifecycleManager); - mSyncManager.start(); if (config.shouldRecordTelemetry()) { int activeFactoriesCount = mFactoryMonitor.count(mApiKey); mStorageContainer.getTelemetryStorage().recordActiveFactories(activeFactoriesCount); mStorageContainer.getTelemetryStorage().recordRedundantFactories(activeFactoriesCount - 1); } - Logger.i("Android SDK initialized!"); + // Run initializer + new Thread(initializer).start(); + + // Initialize default client + client(); + SplitParser mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer(), mStorageContainer.getMyLargeSegmentsStorageContainer()); + mManager = new SplitManagerImpl( + mStorageContainer.getSplitsStorage(), + new SplitValidatorImpl(), mSplitParser); + } private static String getFlagsSpec(@Nullable TestingConfig testingConfig) { diff --git a/src/main/java/io/split/android/client/attributes/AttributesManagerImpl.java b/src/main/java/io/split/android/client/attributes/AttributesManagerImpl.java index 24169f66b..5b2327883 100644 --- a/src/main/java/io/split/android/client/attributes/AttributesManagerImpl.java +++ b/src/main/java/io/split/android/client/attributes/AttributesManagerImpl.java @@ -118,13 +118,13 @@ public boolean clearAttributes() { private void submitUpdateTask(PersistentAttributesStorage persistentStorage, Map mInMemoryAttributes) { if (persistentStorage != null && mSplitTaskExecutor != null && mAttributeTaskFactory != null) { - mSplitTaskExecutor.submit(mAttributeTaskFactory.createAttributeUpdateTask(persistentStorage, mInMemoryAttributes), null); + mSplitTaskExecutor.schedule(mAttributeTaskFactory.createAttributeUpdateTask(persistentStorage, mInMemoryAttributes), 5L, null); } } private void submitClearTask(PersistentAttributesStorage persistentStorage) { if (persistentStorage != null && mSplitTaskExecutor != null && mAttributeTaskFactory != null) { - mSplitTaskExecutor.submit(mAttributeTaskFactory.createAttributeClearTask(persistentStorage), null); + mSplitTaskExecutor.schedule(mAttributeTaskFactory.createAttributeClearTask(persistentStorage), 5L, null); } } } diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java b/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java index e23407f55..989557cd9 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java +++ b/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java @@ -11,6 +11,9 @@ import io.split.android.client.service.splits.SplitsSyncTask; import io.split.android.client.service.splits.SplitsUpdateTask; import io.split.android.client.service.telemetry.TelemetryTaskFactory; +import io.split.android.client.storage.cipher.EncryptionMigrationTask; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.SplitRoomDatabase; public interface SplitTaskFactory extends TelemetryTaskFactory, ImpressionsTaskFactory { @@ -29,4 +32,6 @@ public interface SplitTaskFactory extends TelemetryTaskFactory, ImpressionsTaskF FilterSplitsInCacheTask createFilterSplitsInCacheTask(); CleanUpDatabaseTask createCleanUpDatabaseTask(long maxTimestamp); + + EncryptionMigrationTask createEncryptionMigrationTask(String sdkKey, SplitRoomDatabase splitRoomDatabase, boolean encryptionEnabled, SplitCipher splitCipher); } diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java b/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java index 2c167cda0..75ecc45fc 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java @@ -44,7 +44,10 @@ import io.split.android.client.service.telemetry.TelemetryStatsRecorderTask; import io.split.android.client.service.telemetry.TelemetryTaskFactory; import io.split.android.client.service.telemetry.TelemetryTaskFactoryImpl; +import io.split.android.client.storage.cipher.EncryptionMigrationTask; +import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.common.SplitStorageContainer; +import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; import io.split.android.client.telemetry.storage.TelemetryStorage; @@ -203,6 +206,11 @@ public SplitInPlaceUpdateTask createSplitsUpdateTask(Split featureFlag, long sin return new SplitInPlaceUpdateTask(mSplitsStorageContainer.getSplitsStorage(), mSplitChangeProcessor, mEventsManager, mTelemetryRuntimeProducer, featureFlag, since); } + @Override + public EncryptionMigrationTask createEncryptionMigrationTask(String sdkKey, SplitRoomDatabase splitRoomDatabase, boolean encryptionEnabled, SplitCipher splitCipher) { + return new EncryptionMigrationTask(sdkKey, splitRoomDatabase, encryptionEnabled, splitCipher); + } + @NonNull private TelemetryTaskFactory initializeTelemetryTaskFactory(@NonNull SplitClientConfig splitClientConfig, @Nullable Map filters, TelemetryStorage telemetryStorage) { final TelemetryTaskFactory mTelemetryTaskFactory; diff --git a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManager.java b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManager.java index 372b376d2..72cf4477e 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManager.java +++ b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManager.java @@ -2,7 +2,7 @@ import io.split.android.client.service.executor.SplitTaskExecutionListener; -interface RolloutCacheManager { +public interface RolloutCacheManager { void validateCache(SplitTaskExecutionListener listener); } diff --git a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java index 254ace2cc..f211ae62b 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java @@ -4,16 +4,18 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; import java.util.concurrent.TimeUnit; import io.split.android.client.SplitClientConfig; +import io.split.android.client.service.CleanUpDatabaseTask; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; -import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.storage.RolloutDefinitionsCache; +import io.split.android.client.storage.cipher.EncryptionMigrationTask; import io.split.android.client.storage.common.SplitStorageContainer; import io.split.android.client.storage.general.GeneralInfoStorage; import io.split.android.client.utils.logger.Logger; @@ -27,30 +29,53 @@ public class RolloutCacheManagerImpl implements RolloutCacheManager, SplitTask { @NonNull private final RolloutCacheManagerConfig mConfig; @NonNull - private final SplitTaskExecutor mTaskExecutor; - @NonNull private final RolloutDefinitionsCache[] mStorages; + @NonNull + private final CleanUpDatabaseTask mCleanUpDatabaseTask; + @NonNull + private final EncryptionMigrationTask mEncryptionMigrationTask; - public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull SplitTaskExecutor splitTaskExecutor, @NonNull SplitStorageContainer storageContainer) { + public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull SplitStorageContainer storageContainer, + @NonNull CleanUpDatabaseTask cleanUpDatabaseTask, + @NonNull EncryptionMigrationTask encryptionMigrationTask) { this(storageContainer.getGeneralInfoStorage(), RolloutCacheManagerConfig.from(splitClientConfig), - splitTaskExecutor, + cleanUpDatabaseTask, + encryptionMigrationTask, storageContainer.getSplitsStorage(), storageContainer.getMySegmentsStorageContainer(), storageContainer.getMyLargeSegmentsStorageContainer()); } @VisibleForTesting - RolloutCacheManagerImpl(@NonNull GeneralInfoStorage generalInfoStorage, @NonNull RolloutCacheManagerConfig config, @NonNull SplitTaskExecutor splitTaskExecutor, @NonNull RolloutDefinitionsCache... storages) { + RolloutCacheManagerImpl(@NonNull GeneralInfoStorage generalInfoStorage, + @NonNull RolloutCacheManagerConfig config, + @NonNull CleanUpDatabaseTask clean, + @NonNull EncryptionMigrationTask encryptionMigrationTask, + @NonNull RolloutDefinitionsCache... storages) { mGeneralInfoStorage = checkNotNull(generalInfoStorage); + mCleanUpDatabaseTask = checkNotNull(clean); + mEncryptionMigrationTask = checkNotNull(encryptionMigrationTask); mStorages = checkNotNull(storages); mConfig = checkNotNull(config); - mTaskExecutor = checkNotNull(splitTaskExecutor); } + @WorkerThread @Override public void validateCache(SplitTaskExecutionListener listener) { - mTaskExecutor.submit(this, listener); + try { + Logger.v("Rollout cache manager: Executing clearing task"); + mCleanUpDatabaseTask.execute(); + Logger.v("Rollout cache manager: Validating cache"); + execute(); + Logger.v("Rollout cache manager: Migrating encryption"); + mEncryptionMigrationTask.execute(); + Logger.v("Rollout cache manager: validation finished"); + listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); + } catch (Exception ex) { + Logger.e("Error occurred validating cache: " + ex.getMessage()); + listener.taskExecuted(SplitTaskExecutionInfo.error(SplitTaskType.GENERIC_TASK)); + } } @NonNull diff --git a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java index 5f069bee9..b18fd0b50 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java @@ -10,6 +10,7 @@ import io.split.android.client.RetryBackoffCounterTimerFactory; import io.split.android.client.SplitClientConfig; +import io.split.android.client.api.Key; import io.split.android.client.dtos.Event; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.impressions.Impression; @@ -259,13 +260,13 @@ public void pushImpression(Impression impression) { } @Override - public void registerMySegmentsSynchronizer(String userKey, MySegmentsSynchronizer mySegmentsSynchronizer) { - mMySegmentsSynchronizerRegistry.registerMySegmentsSynchronizer(userKey, mySegmentsSynchronizer); + public void registerMySegmentsSynchronizer(Key key, MySegmentsSynchronizer mySegmentsSynchronizer) { + mMySegmentsSynchronizerRegistry.registerMySegmentsSynchronizer(key, mySegmentsSynchronizer); } @Override - public void unregisterMySegmentsSynchronizer(String userKey) { - mMySegmentsSynchronizerRegistry.unregisterMySegmentsSynchronizer(userKey); + public void unregisterMySegmentsSynchronizer(Key key) { + mMySegmentsSynchronizerRegistry.unregisterMySegmentsSynchronizer(key); } @Override diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistry.java b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistry.java index a6cd8c763..d91c47f06 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistry.java +++ b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistry.java @@ -1,8 +1,10 @@ package io.split.android.client.service.synchronizer.mysegments; +import io.split.android.client.api.Key; + public interface MySegmentsSynchronizerRegistry { - void registerMySegmentsSynchronizer(String userKey, MySegmentsSynchronizer mySegmentsSynchronizer); + void registerMySegmentsSynchronizer(Key key, MySegmentsSynchronizer mySegmentsSynchronizer); - void unregisterMySegmentsSynchronizer(String userKey); + void unregisterMySegmentsSynchronizer(Key key); } diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImpl.java b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImpl.java index 5aba4ce82..8007e570e 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImpl.java @@ -6,6 +6,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; +import io.split.android.client.api.Key; import io.split.android.client.service.mysegments.MySegmentUpdateParams; public class MySegmentsSynchronizerRegistryImpl implements MySegmentsSynchronizerRegistry, @@ -15,23 +16,23 @@ public class MySegmentsSynchronizerRegistryImpl implements MySegmentsSynchronize private final AtomicBoolean mSynchronizedSegments = new AtomicBoolean(false); private final AtomicBoolean mScheduledSegmentsSyncTask = new AtomicBoolean(false); private final AtomicBoolean mStoppedPeriodicFetching = new AtomicBoolean(false); - private final ConcurrentMap mMySegmentsSynchronizers = new ConcurrentHashMap<>(); + private final ConcurrentMap mMySegmentsSynchronizers = new ConcurrentHashMap<>(); @Override - public synchronized void registerMySegmentsSynchronizer(String userKey, MySegmentsSynchronizer mySegmentsSynchronizer) { - mMySegmentsSynchronizers.put(userKey, mySegmentsSynchronizer); + public synchronized void registerMySegmentsSynchronizer(Key key, MySegmentsSynchronizer mySegmentsSynchronizer) { + mMySegmentsSynchronizers.put(key, mySegmentsSynchronizer); triggerPendingActions(mySegmentsSynchronizer); } @Override - public synchronized void unregisterMySegmentsSynchronizer(String userKey) { - MySegmentsSynchronizer mySegmentsSynchronizer = mMySegmentsSynchronizers.get(userKey); + public synchronized void unregisterMySegmentsSynchronizer(Key key) { + MySegmentsSynchronizer mySegmentsSynchronizer = mMySegmentsSynchronizers.get(key); if (mySegmentsSynchronizer != null) { mySegmentsSynchronizer.stopPeriodicFetching(); mySegmentsSynchronizer.destroy(); } - mMySegmentsSynchronizers.remove(userKey); + mMySegmentsSynchronizers.remove(key); } @Override diff --git a/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java b/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java index de1d53414..af0dc3449 100644 --- a/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java +++ b/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java @@ -95,7 +95,7 @@ public void registerComponents(Key key, SplitEventsManager eventsManager, MySegm @Override public void unregisterComponentsForKey(Key key) { mAttributesSynchronizerRegistry.unregisterAttributesSynchronizer(key.matchingKey()); - mMySegmentsSynchronizerRegistry.unregisterMySegmentsSynchronizer(key.matchingKey()); + mMySegmentsSynchronizerRegistry.unregisterMySegmentsSynchronizer(key); mEventsManagerRegistry.unregisterEventsManager(key); if (isSyncEnabled()) { @@ -116,7 +116,7 @@ private void registerAttributesSynchronizer(Key key, SplitEventsManager eventsMa } private void registerMySegmentsSynchronizer(Key key, MySegmentsSynchronizer mySegmentsSynchronizer) { - mMySegmentsSynchronizerRegistry.registerMySegmentsSynchronizer(key.matchingKey(), + mMySegmentsSynchronizerRegistry.registerMySegmentsSynchronizer(key, mySegmentsSynchronizer); } diff --git a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java b/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java index adb47d63a..e1e5928a1 100644 --- a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java +++ b/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java @@ -49,6 +49,7 @@ public SplitTaskExecutionInfo execute() { mSplitDatabase.runInTransaction(new Runnable() { @Override public void run() { + updateAttributes(mSplitDatabase.attributesDao()); updateSplits(mSplitDatabase.splitDao()); updateSegments(mSplitDatabase.mySegmentDao()); updateLargeSegments(mSplitDatabase.myLargeSegmentDao()); @@ -56,7 +57,6 @@ public void run() { updateEvents(mSplitDatabase.eventDao()); updateImpressionsCount(mSplitDatabase.impressionsCountDao()); updateUniqueKeys(mSplitDatabase.uniqueKeysDao()); - updateAttributes(mSplitDatabase.attributesDao()); } }); @@ -87,7 +87,6 @@ private void updateAttributes(AttributesDao attributesDao) { private void updateUniqueKeys(UniqueKeysDao uniqueKeysDao) { List items = uniqueKeysDao.getAll(); - for (UniqueKeyEntity item : items) { String fromUserKey = mFromCipher.decrypt(item.getUserKey()); String fromFeatureList = mFromCipher.decrypt(item.getFeatureList()); diff --git a/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt b/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt index 7c123c8dd..7ff5e004b 100644 --- a/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt +++ b/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt @@ -1,10 +1,17 @@ package io.split.android.client import android.content.Context +import io.split.android.client.SplitFactoryHelper.Initializer.Listener +import io.split.android.client.events.EventsManagerCoordinator +import io.split.android.client.events.SplitInternalEvent +import io.split.android.client.lifecycle.SplitLifecycleManager +import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor +import io.split.android.client.service.executor.SplitTaskExecutionInfo import io.split.android.client.service.executor.SplitTaskExecutionListener import io.split.android.client.service.executor.SplitTaskExecutor -import io.split.android.client.storage.cipher.EncryptionMigrationTask -import io.split.android.client.storage.db.SplitRoomDatabase +import io.split.android.client.service.executor.SplitTaskType +import io.split.android.client.service.synchronizer.RolloutCacheManager +import io.split.android.client.service.synchronizer.SyncManager import junit.framework.TestCase.assertEquals import org.junit.After import org.junit.Before @@ -12,7 +19,6 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.any -import org.mockito.Mockito.argThat import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` @@ -24,12 +30,6 @@ class SplitFactoryHelperTest { private lateinit var mocks: AutoCloseable - @Mock - private lateinit var splitRoomDatabase: SplitRoomDatabase - @Mock - private lateinit var splitTaskExecutor: SplitTaskExecutor - @Mock - private lateinit var taskListener: SplitTaskExecutionListener @Mock private lateinit var context: Context @@ -46,22 +46,6 @@ class SplitFactoryHelperTest { mocks.close() } - @Test - fun migrateEncryption() { - - helper.migrateEncryption( - "abcdedfghijklmnopqrstuvwxyz", - splitRoomDatabase, - splitTaskExecutor, - true, - taskListener, - ) - - verify(splitTaskExecutor).submit( - argThat { it is EncryptionMigrationTask }, - argThat { it?.equals(taskListener) == true }) - } - @Test fun generateDatabaseNameWithoutPrefixAndKeyLongerThan4() { val path = mock(File::class.java) @@ -152,4 +136,44 @@ class SplitFactoryHelperTest { verify(existingPath).renameTo(nonExistingPath) assertEquals("abcdwxyz", databaseName) } + + @Test + fun `Initializer test`() { + val rolloutCacheManager = mock(RolloutCacheManager::class.java) + val splitTaskExecutionListener = mock(SplitTaskExecutionListener::class.java) + + val initializer = SplitFactoryHelper.Initializer( + rolloutCacheManager, + splitTaskExecutionListener + ) + + initializer.run() + + verify(rolloutCacheManager).validateCache(splitTaskExecutionListener) + } + + @Test + fun `Initializer Listener test`() { + val eventsManagerCoordinator = mock(EventsManagerCoordinator::class.java) + val taskExecutor = mock(SplitTaskExecutor::class.java) + val singleThreadTaskExecutor = mock(SplitSingleThreadTaskExecutor::class.java) + val syncManager = mock(SyncManager::class.java) + val lifecycleManager = mock(SplitLifecycleManager::class.java) + + val listener = Listener( + eventsManagerCoordinator, + taskExecutor, + singleThreadTaskExecutor, + syncManager, + lifecycleManager + ) + + listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)) + + verify(eventsManagerCoordinator).notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE) + verify(taskExecutor).resume() + verify(singleThreadTaskExecutor).resume() + verify(syncManager).start() + verify(lifecycleManager).register(syncManager) + } } diff --git a/src/test/java/io/split/android/client/attributes/AttributesManagerImplTest.java b/src/test/java/io/split/android/client/attributes/AttributesManagerImplTest.java index 1e2bb618e..afcd377ed 100644 --- a/src/test/java/io/split/android/client/attributes/AttributesManagerImplTest.java +++ b/src/test/java/io/split/android/client/attributes/AttributesManagerImplTest.java @@ -92,7 +92,7 @@ public void setAttributeLaunchesAttributeUpdateTaskIfValueIsValid() { attributeClient.setAttribute(name, attribute); verify(attributeTaskFactory).createAttributeUpdateTask(persistentAttributesStorage, attributeMap); - verify(splitTaskExecutor).submit(updateAttributesInPersistentStorageTask, null); + verify(splitTaskExecutor).schedule(updateAttributesInPersistentStorageTask, 5L, null); } @Test @@ -170,7 +170,7 @@ public void setAttributesLaunchesAttributeUpdateTaskIfValuesAreValid() { attributeClient.setAttributes(attributeMap); verify(attributeTaskFactory).createAttributeUpdateTask(persistentAttributesStorage, attributeMap); - verify(splitTaskExecutor).submit(updateAttributesInPersistentStorageTask, null); + verify(splitTaskExecutor).schedule(updateAttributesInPersistentStorageTask, 5L, null); } @Test @@ -225,7 +225,7 @@ public void clearLaunchesAttributeClearTask() { attributeClient.clearAttributes(); verify(attributeTaskFactory).createAttributeClearTask(persistentAttributesStorage); - verify(splitTaskExecutor).submit(clearAttributesInPersistentStorageTask, null); + verify(splitTaskExecutor).schedule(clearAttributesInPersistentStorageTask, 5L, null); } @Test @@ -250,7 +250,7 @@ public void removeLaunchesAttributeUpdateTask() { attributeClient.removeAttribute("key"); verify(attributeTaskFactory).createAttributeUpdateTask(persistentAttributesStorage, attributeMap); - verify(splitTaskExecutor).submit(updateAttributesInPersistentStorageTask, null); + verify(splitTaskExecutor).schedule(updateAttributesInPersistentStorageTask, 5L, null); } private Map getDefaultValues() { diff --git a/src/test/java/io/split/android/client/service/SynchronizerTest.java b/src/test/java/io/split/android/client/service/SynchronizerTest.java index c0c32eabb..58b19db94 100644 --- a/src/test/java/io/split/android/client/service/SynchronizerTest.java +++ b/src/test/java/io/split/android/client/service/SynchronizerTest.java @@ -39,6 +39,7 @@ import io.split.android.client.RetryBackoffCounterTimerFactory; import io.split.android.client.SplitClientConfig; +import io.split.android.client.api.Key; import io.split.android.client.dtos.Event; import io.split.android.client.dtos.KeyImpression; import io.split.android.client.dtos.SplitChange; @@ -243,7 +244,7 @@ public void splitExecutorSchedule() { verify(mWorkManagerWrapper).removeWork(); verify(mWorkManagerWrapper, never()).scheduleWork(); - mSynchronizer.unregisterMySegmentsSynchronizer("userKey"); + mSynchronizer.unregisterMySegmentsSynchronizer(new Key("userKey")); } @Test @@ -544,7 +545,7 @@ public void loadLocalData() { when(loadMySegmentsTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_MY_SEGMENTS)); when(mMySegmentsTaskFactory.createLoadMySegmentsTask()).thenReturn(loadMySegmentsTask); - ((MySegmentsSynchronizerRegistry) mSynchronizer).registerMySegmentsSynchronizer("", mMySegmentsSynchronizer); + ((MySegmentsSynchronizerRegistry) mSynchronizer).registerMySegmentsSynchronizer(new Key(""), mMySegmentsSynchronizer); mSynchronizer.loadAndSynchronizeSplits(); mSynchronizer.loadMySegmentsFromCache(); @@ -775,9 +776,9 @@ public void reschedulingEventsTaskCancelsPreviousWhenCallingSequentially() { public void registerMySegmentsSynchronizerDelegatesToRegistry() { setup(SplitClientConfig.builder().synchronizeInBackground(false).build()); - mSynchronizer.registerMySegmentsSynchronizer("userKey", mMySegmentsSynchronizer); + mSynchronizer.registerMySegmentsSynchronizer(new Key("userKey"), mMySegmentsSynchronizer); - verify(mMySegmentsSynchronizerRegistry).registerMySegmentsSynchronizer("userKey", mMySegmentsSynchronizer); + verify(mMySegmentsSynchronizerRegistry).registerMySegmentsSynchronizer(new Key("userKey"), mMySegmentsSynchronizer); } @Test diff --git a/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt b/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt index 41dcc6422..20e1b2c24 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt +++ b/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt @@ -1,15 +1,14 @@ package io.split.android.client.service.synchronizer +import io.split.android.client.service.CleanUpDatabaseTask import io.split.android.client.service.executor.SplitTaskExecutionListener -import io.split.android.client.service.executor.SplitTaskExecutor import io.split.android.client.storage.RolloutDefinitionsCache +import io.split.android.client.storage.cipher.EncryptionMigrationTask import io.split.android.client.storage.general.GeneralInfoStorage -import io.split.android.fake.SplitTaskExecutorStub import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyLong -import org.mockito.ArgumentMatchers.argThat import org.mockito.Mockito.longThat import org.mockito.Mockito.mock import org.mockito.Mockito.times @@ -21,14 +20,16 @@ class RolloutCacheManagerTest { private lateinit var mRolloutCacheManager: RolloutCacheManager private lateinit var mGeneralInfoStorage: GeneralInfoStorage - private lateinit var mSplitTaskExecutor: SplitTaskExecutor private lateinit var mSplitsCache: RolloutDefinitionsCache private lateinit var mSegmentsCache: RolloutDefinitionsCache + private lateinit var mEncryptionMigrationTask: EncryptionMigrationTask + private lateinit var mCleanUpDatabaseTask: CleanUpDatabaseTask @Before fun setup() { mGeneralInfoStorage = mock(GeneralInfoStorage::class.java) - mSplitTaskExecutor = SplitTaskExecutorStub() + mEncryptionMigrationTask = mock(EncryptionMigrationTask::class.java) + mCleanUpDatabaseTask = mock(CleanUpDatabaseTask::class.java) mSplitsCache = mock(RolloutDefinitionsCache::class.java) mSegmentsCache = mock(RolloutDefinitionsCache::class.java) } @@ -132,8 +133,26 @@ class RolloutCacheManagerTest { verify(mGeneralInfoStorage, times(0)).setRolloutCacheLastClearTimestamp(anyLong()) } + @Test + fun `validateCache executes cleanUpDatabaseTask`() { + mRolloutCacheManager = getCacheManager(10L, false) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mCleanUpDatabaseTask).execute() + } + + @Test + fun `validateCache executes encryptionMigrationTask`() { + mRolloutCacheManager = getCacheManager(10L, false) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mEncryptionMigrationTask).execute() + } + private fun getCacheManager(expiration: Long, clearOnInit: Boolean): RolloutCacheManager { - return RolloutCacheManagerImpl(mGeneralInfoStorage, RolloutCacheManagerConfig(expiration, clearOnInit), mSplitTaskExecutor, mSplitsCache, mSegmentsCache) + return RolloutCacheManagerImpl(mGeneralInfoStorage, RolloutCacheManagerConfig(expiration, clearOnInit), mCleanUpDatabaseTask, mEncryptionMigrationTask, mSplitsCache, mSegmentsCache) } private fun createMockedTimestamp(period: Long): Long { diff --git a/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java b/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java index 9052de36a..a24909553 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java +++ b/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java @@ -6,6 +6,7 @@ import org.junit.Before; import org.junit.Test; +import io.split.android.client.api.Key; import io.split.android.client.service.mysegments.MySegmentUpdateParams; public class MySegmentsSynchronizerRegistryImplTest { @@ -21,7 +22,7 @@ public void setUp() { public void loadMySegmentsFromCacheGetCalledInEveryRegisteredSync() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); mRegistry.loadMySegmentsFromCache(); verify(syncMock).loadMySegmentsFromCache(); @@ -31,7 +32,7 @@ public void loadMySegmentsFromCacheGetCalledInEveryRegisteredSync() { public void synchronizeMySegmentsGetCalledInEveryRegisteredSync() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); mRegistry.synchronizeMySegments(); verify(syncMock).synchronizeMySegments(); @@ -42,7 +43,7 @@ public void forceMySegmentsSyncGetCalledInEveryRegisteredSync() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); MySegmentUpdateParams params = new MySegmentUpdateParams(4L, 1L, 2L); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); mRegistry.forceMySegmentsSync(params); verify(syncMock).forceMySegmentsSync(params); @@ -52,7 +53,7 @@ public void forceMySegmentsSyncGetCalledInEveryRegisteredSync() { public void destroyGetCalledInEveryRegisteredSync() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); mRegistry.destroy(); verify(syncMock).destroy(); @@ -62,7 +63,7 @@ public void destroyGetCalledInEveryRegisteredSync() { public void scheduleSegmentsSyncTaskGetCalledInEveryRegisteredSync() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); mRegistry.scheduleSegmentsSyncTask(); verify(syncMock).scheduleSegmentsSyncTask(); @@ -72,7 +73,7 @@ public void scheduleSegmentsSyncTaskGetCalledInEveryRegisteredSync() { public void submitMySegmentsLoadingTaskGetCalledInEveryRegisteredSync() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); mRegistry.submitMySegmentsLoadingTask(); verify(syncMock).submitMySegmentsLoadingTask(); @@ -82,7 +83,7 @@ public void submitMySegmentsLoadingTaskGetCalledInEveryRegisteredSync() { public void stopPeriodicFetchingGetCalledInEveryRegisteredSync() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); mRegistry.stopPeriodicFetching(); verify(syncMock).stopPeriodicFetching(); @@ -92,8 +93,8 @@ public void stopPeriodicFetchingGetCalledInEveryRegisteredSync() { public void unregisterStopsTasksBeforeRemovingSync() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); - mRegistry.unregisterMySegmentsSynchronizer("key"); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); + mRegistry.unregisterMySegmentsSynchronizer(new Key("key")); verify(syncMock).stopPeriodicFetching(); verify(syncMock).destroy(); @@ -105,10 +106,10 @@ public void callLoadSegmentsFromCacheForNewlyRegisteredSyncIfNecessary() { MySegmentsSynchronizer syncMock2 = mock(MySegmentsSynchronizer.class); MySegmentsSynchronizer syncMock3 = mock(MySegmentsSynchronizer.class); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); mRegistry.loadMySegmentsFromCache(); - mRegistry.registerMySegmentsSynchronizer("new_key", syncMock2); - mRegistry.registerMySegmentsSynchronizer("new_key", syncMock3); + mRegistry.registerMySegmentsSynchronizer(new Key("new_key"), syncMock2); + mRegistry.registerMySegmentsSynchronizer(new Key("new_key"), syncMock3); verify(syncMock2).loadMySegmentsFromCache(); verify(syncMock3).loadMySegmentsFromCache(); @@ -120,10 +121,10 @@ public void callSynchronizeMySegmentsForNewlyRegisteredSyncIfNecessary() { MySegmentsSynchronizer syncMock2 = mock(MySegmentsSynchronizer.class); MySegmentsSynchronizer syncMock3 = mock(MySegmentsSynchronizer.class); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); mRegistry.synchronizeMySegments(); - mRegistry.registerMySegmentsSynchronizer("new_key", syncMock2); - mRegistry.registerMySegmentsSynchronizer("new_key", syncMock3); + mRegistry.registerMySegmentsSynchronizer(new Key("new_key"), syncMock2); + mRegistry.registerMySegmentsSynchronizer(new Key("new_key"), syncMock3); verify(syncMock2).synchronizeMySegments(); verify(syncMock3).synchronizeMySegments(); @@ -135,10 +136,10 @@ public void callScheduleSegmentsSyncTaskForNewlyRegisteredSyncIfNecessary() { MySegmentsSynchronizer syncMock2 = mock(MySegmentsSynchronizer.class); MySegmentsSynchronizer syncMock3 = mock(MySegmentsSynchronizer.class); - mRegistry.registerMySegmentsSynchronizer("key", syncMock); + mRegistry.registerMySegmentsSynchronizer(new Key("key"), syncMock); mRegistry.scheduleSegmentsSyncTask(); - mRegistry.registerMySegmentsSynchronizer("new_key", syncMock2); - mRegistry.registerMySegmentsSynchronizer("new_key", syncMock3); + mRegistry.registerMySegmentsSynchronizer(new Key("new_key"), syncMock2); + mRegistry.registerMySegmentsSynchronizer(new Key("new_key"), syncMock3); verify(syncMock2).scheduleSegmentsSyncTask(); verify(syncMock3).scheduleSegmentsSyncTask(); diff --git a/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java b/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java index fac2fbd93..b78c13cd1 100644 --- a/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java +++ b/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java @@ -91,7 +91,7 @@ public void attributesSynchronizerIsRegistered() { public void mySegmentsSynchronizerIsRegistered() { register.registerComponents(mMatchingKey, mSplitEventsManager, mMySegmentsTaskFactory); - verify(mMySegmentsSynchronizerRegistry).registerMySegmentsSynchronizer("matching_key", mMySegmentsSynchronizer); + verify(mMySegmentsSynchronizerRegistry).registerMySegmentsSynchronizer(new Key("matching_key", "bucketing_key"), mMySegmentsSynchronizer); } @Test @@ -120,7 +120,7 @@ public void componentsAreCorrectlyUnregistered() { register.unregisterComponentsForKey(mMatchingKey); verify(mAttributesSynchronizerRegistry).unregisterAttributesSynchronizer("matching_key"); - verify(mMySegmentsSynchronizerRegistry).unregisterMySegmentsSynchronizer("matching_key"); + verify(mMySegmentsSynchronizerRegistry).unregisterMySegmentsSynchronizer(new Key("matching_key", "bucketing_key")); verify(mMySegmentsUpdateWorkerRegistry).unregisterMySegmentsUpdateWorker("matching_key"); verify(mMySegmentsNotificationProcessorRegistry).unregisterMembershipsProcessor("matching_key"); verify(mEventsManagerRegistry).unregisterEventsManager(mMatchingKey); From 61c484aaccd3372db72b386f24e37bde8e0d551c Mon Sep 17 00:00:00 2001 From: gthea Date: Wed, 4 Dec 2024 17:27:17 -0300 Subject: [PATCH 2/2] Rollout config (#719) --- .../helper/TestableSplitConfigBuilder.java | 10 ++- .../attributes/AttributesIntegrationTest.java | 33 ++++++++- .../client/RolloutCacheConfiguration.java | 69 +++++++++++++++++++ .../android/client/SplitClientConfig.java | 31 +++++++-- .../client/service/ServiceConstants.java | 1 + .../RolloutCacheManagerConfig.java | 29 -------- .../synchronizer/RolloutCacheManagerImpl.java | 25 ++++--- .../client/RolloutCacheConfigurationTest.java | 41 +++++++++++ .../android/client/SplitClientConfigTest.java | 32 +++++++++ .../synchronizer/RolloutCacheManagerTest.kt | 51 ++++++++++---- 10 files changed, 264 insertions(+), 58 deletions(-) create mode 100644 src/main/java/io/split/android/client/RolloutCacheConfiguration.java delete mode 100644 src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerConfig.java create mode 100644 src/test/java/io/split/android/client/RolloutCacheConfigurationTest.java diff --git a/src/androidTest/java/helper/TestableSplitConfigBuilder.java b/src/androidTest/java/helper/TestableSplitConfigBuilder.java index 9c577162c..34449f445 100644 --- a/src/androidTest/java/helper/TestableSplitConfigBuilder.java +++ b/src/androidTest/java/helper/TestableSplitConfigBuilder.java @@ -2,6 +2,7 @@ import java.lang.reflect.Constructor; +import io.split.android.client.RolloutCacheConfiguration; import io.split.android.client.ServiceEndpoints; import io.split.android.client.SplitClientConfig; import io.split.android.client.SyncConfig; @@ -64,6 +65,7 @@ public class TestableSplitConfigBuilder { private String mPrefix = ""; private CertificatePinningConfiguration mCertificatePinningConfiguration; private long mImpressionsDedupeTimeInterval = ServiceConstants.DEFAULT_IMPRESSIONS_DEDUPE_TIME_INTERVAL; + private RolloutCacheConfiguration mRolloutCacheConfiguration = RolloutCacheConfiguration.builder().build(); public TestableSplitConfigBuilder() { mServiceEndpoints = ServiceEndpoints.builder().build(); @@ -274,6 +276,11 @@ public TestableSplitConfigBuilder impressionsDedupeTimeInterval(long impressions return this; } + public TestableSplitConfigBuilder rolloutCacheConfiguration(RolloutCacheConfiguration rolloutCacheConfiguration) { + this.mRolloutCacheConfiguration = rolloutCacheConfiguration; + return this; + } + public SplitClientConfig build() { Constructor constructor = SplitClientConfig.class.getDeclaredConstructors()[0]; constructor.setAccessible(true); @@ -329,7 +336,8 @@ public SplitClientConfig build() { mPrefix, mObserverCacheExpirationPeriod, mCertificatePinningConfiguration, - mImpressionsDedupeTimeInterval); + mImpressionsDedupeTimeInterval, + mRolloutCacheConfiguration); Logger.instance().setLevel(mLogLevel); return config; diff --git a/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java b/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java index 085d99cb3..949d415aa 100644 --- a/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java +++ b/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java @@ -20,6 +20,7 @@ import helper.IntegrationHelper; import helper.SplitEventTaskHelper; import helper.TestableSplitConfigBuilder; +import io.split.android.client.ServiceEndpoints; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; @@ -32,15 +33,21 @@ import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.storage.db.attributes.AttributesEntity; import io.split.android.client.utils.Json; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; public class AttributesIntegrationTest { private Context mContext; private SplitRoomDatabase mRoomDb; private SplitFactory mSplitFactory; + private MockWebServer mWebServer; @Before public void setup() { + setupServer(); mContext = InstrumentationRegistry.getInstrumentation().getContext(); mRoomDb = DatabaseHelper.getTestDatabase(mContext); mRoomDb.clearAllTables(); @@ -51,7 +58,7 @@ public void testPersistentAttributes() throws InterruptedException { insertSplitsFromFileIntoDB(); CountDownLatch readyLatch = new CountDownLatch(1); SplitClient client = getSplitClient(readyLatch, true, null); - readyLatch.await(5, TimeUnit.SECONDS); + readyLatch.await(10, TimeUnit.SECONDS); // 1. Evaluate without attrs Assert.assertEquals("on", client.getTreatment("workm")); @@ -224,8 +231,12 @@ private void setAttributesInClientAndEvaluate(SplitClient client) { private SplitClient getSplitClient(CountDownLatch readyLatch, boolean persistenceEnabled, String matchingKey) { if (mSplitFactory == null) { + final String url = mWebServer.url("/").url().toString(); + ServiceEndpoints endpoints = ServiceEndpoints.builder() + .apiEndpoint(url).eventsEndpoint(url).build(); SplitClientConfig config = new TestableSplitConfigBuilder() .enableDebug() + .serviceEndpoints(endpoints) .featuresRefreshRate(9999) .segmentsRefreshRate(9999) .impressionsRefreshRate(9999) @@ -272,4 +283,24 @@ private List getSplitListFromJson() { return changes.splits; } + + private void setupServer() { + mWebServer = new MockWebServer(); + + final Dispatcher dispatcher = new Dispatcher() { + + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); + } else if (request.getPath().contains("/splitChanges")) { + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptySplitChanges(-1, 10000)); + } else { + return new MockResponse().setResponseCode(404); + } + } + }; + mWebServer.setDispatcher(dispatcher); + } } diff --git a/src/main/java/io/split/android/client/RolloutCacheConfiguration.java b/src/main/java/io/split/android/client/RolloutCacheConfiguration.java new file mode 100644 index 000000000..f1cd03cbd --- /dev/null +++ b/src/main/java/io/split/android/client/RolloutCacheConfiguration.java @@ -0,0 +1,69 @@ +package io.split.android.client; + +import io.split.android.client.service.ServiceConstants; +import io.split.android.client.utils.logger.Logger; + +public class RolloutCacheConfiguration { + + private final int mExpiration; + private final boolean mClearOnInit; + + private RolloutCacheConfiguration(int expiration, boolean clearOnInit) { + mExpiration = expiration; + mClearOnInit = clearOnInit; + } + + public int getExpiration() { + return mExpiration; + } + + public boolean clearOnInit() { + return mClearOnInit; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private static final int MIN_EXPIRATION_DAYS = 1; + + private int mExpiration = ServiceConstants.DEFAULT_ROLLOUT_CACHE_EXPIRATION; + private boolean mClearOnInit = false; + + private Builder() { + + } + + /** + * Set the expiration time for the rollout definitions cache, in days. Default is 10 days. + * @param expiration in days + * @return This builder + */ + public Builder expiration(int expiration) { + if (expiration < MIN_EXPIRATION_DAYS) { + Logger.w("Cache expiration must be at least 1 day. Using default value."); + mExpiration = ServiceConstants.DEFAULT_ROLLOUT_CACHE_EXPIRATION; + } else { + mExpiration = expiration; + } + + return this; + } + + /** + * Set if the rollout definitions cache should be cleared on initialization. Default is false. + * @param clearOnInit whether to clear cache on initialization. + * @return This builder + */ + public Builder clearOnInit(boolean clearOnInit) { + mClearOnInit = clearOnInit; + return this; + } + + public RolloutCacheConfiguration build() { + return new RolloutCacheConfiguration(mExpiration, mClearOnInit); + } + } +} diff --git a/src/main/java/io/split/android/client/SplitClientConfig.java b/src/main/java/io/split/android/client/SplitClientConfig.java index c2eff3e9b..b33567a32 100644 --- a/src/main/java/io/split/android/client/SplitClientConfig.java +++ b/src/main/java/io/split/android/client/SplitClientConfig.java @@ -131,6 +131,8 @@ public class SplitClientConfig { private final long mObserverCacheExpirationPeriod; private final CertificatePinningConfiguration mCertificatePinningConfiguration; private final long mImpressionsDedupeTimeInterval; + @NonNull + private final RolloutCacheConfiguration mRolloutCacheConfiguration; public static Builder builder() { return new Builder(); @@ -185,7 +187,8 @@ private SplitClientConfig(String endpoint, String prefix, long observerCacheExpirationPeriod, CertificatePinningConfiguration certificatePinningConfiguration, - long impressionsDedupeTimeInterval) { + long impressionsDedupeTimeInterval, + RolloutCacheConfiguration rolloutCacheConfiguration) { mEndpoint = endpoint; mEventsEndpoint = eventsEndpoint; mTelemetryEndpoint = telemetryEndpoint; @@ -243,6 +246,7 @@ private SplitClientConfig(String endpoint, mObserverCacheExpirationPeriod = observerCacheExpirationPeriod; mCertificatePinningConfiguration = certificatePinningConfiguration; mImpressionsDedupeTimeInterval = impressionsDedupeTimeInterval; + mRolloutCacheConfiguration = rolloutCacheConfiguration; } public String trafficType() { @@ -486,8 +490,8 @@ public long impressionsDedupeTimeInterval() { return mImpressionsDedupeTimeInterval; } - public boolean clearOnInit() { - return false; // TODO: to be implemented in the future + public RolloutCacheConfiguration rolloutCacheConfiguration() { + return mRolloutCacheConfiguration; } public static final class Builder { @@ -566,6 +570,8 @@ public static final class Builder { private long mImpressionsDedupeTimeInterval = ServiceConstants.DEFAULT_IMPRESSIONS_DEDUPE_TIME_INTERVAL; + private RolloutCacheConfiguration mRolloutCacheConfiguration = RolloutCacheConfiguration.builder().build(); + public Builder() { mServiceEndpoints = ServiceEndpoints.builder().build(); } @@ -1106,6 +1112,22 @@ public Builder impressionsDedupeTimeInterval(long impressionsDedupeTimeInterval) return this; } + /** + * Configuration for rollout definitions cache. + * + * @param rolloutCacheConfiguration Configuration object + * @return This builder + */ + public Builder rolloutCacheConfiguration(@NonNull RolloutCacheConfiguration rolloutCacheConfiguration) { + if (rolloutCacheConfiguration == null) { + Logger.w("Rollout cache configuration is null. Setting to default value."); + mRolloutCacheConfiguration = RolloutCacheConfiguration.builder().build(); + } else { + mRolloutCacheConfiguration = rolloutCacheConfiguration; + } + return this; + } + public SplitClientConfig build() { Logger.instance().setLevel(mLogLevel); @@ -1237,7 +1259,8 @@ public SplitClientConfig build() { mPrefix, mObserverCacheExpirationPeriod, mCertificatePinningConfiguration, - mImpressionsDedupeTimeInterval); + mImpressionsDedupeTimeInterval, + mRolloutCacheConfiguration); } private HttpProxy parseProxyHost(String proxyUri) { diff --git a/src/main/java/io/split/android/client/service/ServiceConstants.java b/src/main/java/io/split/android/client/service/ServiceConstants.java index 097b043b8..6f5c9f00c 100644 --- a/src/main/java/io/split/android/client/service/ServiceConstants.java +++ b/src/main/java/io/split/android/client/service/ServiceConstants.java @@ -11,6 +11,7 @@ public class ServiceConstants { public static final long MIN_INITIAL_DELAY = 5L; public static final int DEFAULT_RECORDS_PER_PUSH = 100; public static final long DEFAULT_SPLITS_CACHE_EXPIRATION_IN_SECONDS = TimeUnit.DAYS.toSeconds(10); // 10 days + public static final int DEFAULT_ROLLOUT_CACHE_EXPIRATION = 10; // 10 days public static final int MAX_ROWS_PER_QUERY = 100; diff --git a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerConfig.java b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerConfig.java deleted file mode 100644 index 1de13b297..000000000 --- a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.split.android.client.service.synchronizer; - -import androidx.annotation.VisibleForTesting; - -import io.split.android.client.SplitClientConfig; - -class RolloutCacheManagerConfig { - - private final long mCacheExpirationInDays; - private final boolean mClearOnInit; - - @VisibleForTesting - RolloutCacheManagerConfig(long cacheExpirationInDays, boolean clearOnInit) { - mCacheExpirationInDays = cacheExpirationInDays; - mClearOnInit = clearOnInit; - } - - static RolloutCacheManagerConfig from(SplitClientConfig splitClientConfig) { - return new RolloutCacheManagerConfig(splitClientConfig.cacheExpirationInSeconds(), splitClientConfig.clearOnInit()); - } - - long getCacheExpirationInDays() { - return mCacheExpirationInDays; - } - - boolean isClearOnInit() { - return mClearOnInit; - } -} diff --git a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java index f211ae62b..25500e998 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java @@ -8,6 +8,7 @@ import java.util.concurrent.TimeUnit; +import io.split.android.client.RolloutCacheConfiguration; import io.split.android.client.SplitClientConfig; import io.split.android.client.service.CleanUpDatabaseTask; import io.split.android.client.service.executor.SplitTask; @@ -27,7 +28,7 @@ public class RolloutCacheManagerImpl implements RolloutCacheManager, SplitTask { @NonNull private final GeneralInfoStorage mGeneralInfoStorage; @NonNull - private final RolloutCacheManagerConfig mConfig; + private final RolloutCacheConfiguration mConfig; @NonNull private final RolloutDefinitionsCache[] mStorages; @NonNull @@ -35,11 +36,12 @@ public class RolloutCacheManagerImpl implements RolloutCacheManager, SplitTask { @NonNull private final EncryptionMigrationTask mEncryptionMigrationTask; - public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull SplitStorageContainer storageContainer, + public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, + @NonNull SplitStorageContainer storageContainer, @NonNull CleanUpDatabaseTask cleanUpDatabaseTask, @NonNull EncryptionMigrationTask encryptionMigrationTask) { this(storageContainer.getGeneralInfoStorage(), - RolloutCacheManagerConfig.from(splitClientConfig), + splitClientConfig.rolloutCacheConfiguration(), cleanUpDatabaseTask, encryptionMigrationTask, storageContainer.getSplitsStorage(), @@ -49,12 +51,12 @@ public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, @No @VisibleForTesting RolloutCacheManagerImpl(@NonNull GeneralInfoStorage generalInfoStorage, - @NonNull RolloutCacheManagerConfig config, - @NonNull CleanUpDatabaseTask clean, + @NonNull RolloutCacheConfiguration config, + @NonNull CleanUpDatabaseTask cleanUpDatabaseTask, @NonNull EncryptionMigrationTask encryptionMigrationTask, @NonNull RolloutDefinitionsCache... storages) { mGeneralInfoStorage = checkNotNull(generalInfoStorage); - mCleanUpDatabaseTask = checkNotNull(clean); + mCleanUpDatabaseTask = checkNotNull(cleanUpDatabaseTask); mEncryptionMigrationTask = checkNotNull(encryptionMigrationTask); mStorages = checkNotNull(storages); mConfig = checkNotNull(config); @@ -70,7 +72,7 @@ public void validateCache(SplitTaskExecutionListener listener) { execute(); Logger.v("Rollout cache manager: Migrating encryption"); mEncryptionMigrationTask.execute(); - Logger.v("Rollout cache manager: validation finished"); + Logger.v("Rollout cache manager: Validation finished"); listener.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); } catch (Exception ex) { Logger.e("Error occurred validating cache: " + ex.getMessage()); @@ -95,15 +97,18 @@ public SplitTaskExecutionInfo execute() { } private boolean validateExpiration() { - // calculate elapsed time since last update long lastUpdateTimestamp = mGeneralInfoStorage.getSplitsUpdateTimestamp(); + // calculate elapsed time since last update long daysSinceLastUpdate = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - lastUpdateTimestamp); - if (daysSinceLastUpdate > mConfig.getCacheExpirationInDays()) { + if (lastUpdateTimestamp > 0 && daysSinceLastUpdate > mConfig.getExpiration()) { Logger.v("Clearing rollout definitions cache due to expiration"); return true; - } else if (mConfig.isClearOnInit()) { + } else if (mConfig.clearOnInit()) { long lastCacheClearTimestamp = mGeneralInfoStorage.getRolloutCacheLastClearTimestamp(); + if (lastCacheClearTimestamp < 1) { + return true; + } long daysSinceCacheClear = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - lastCacheClearTimestamp); // don't clear too soon diff --git a/src/test/java/io/split/android/client/RolloutCacheConfigurationTest.java b/src/test/java/io/split/android/client/RolloutCacheConfigurationTest.java new file mode 100644 index 000000000..ad1760fe8 --- /dev/null +++ b/src/test/java/io/split/android/client/RolloutCacheConfigurationTest.java @@ -0,0 +1,41 @@ +package io.split.android.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class RolloutCacheConfigurationTest { + + @Test + public void defaultValues() { + RolloutCacheConfiguration config = RolloutCacheConfiguration.builder().build(); + assertEquals(10, config.getExpiration()); + assertFalse(config.clearOnInit()); + } + + @Test + public void expirationIsCorrectlySet() { + RolloutCacheConfiguration.Builder builder = RolloutCacheConfiguration.builder(); + builder.expiration(1); + RolloutCacheConfiguration config = builder.build(); + assertEquals(1, config.getExpiration()); + } + + @Test + public void clearOnInitIsCorrectlySet() { + RolloutCacheConfiguration.Builder builder = RolloutCacheConfiguration.builder(); + builder.clearOnInit(true); + RolloutCacheConfiguration config = builder.build(); + assertTrue(config.clearOnInit()); + } + + @Test + public void negativeExpirationIsSetToDefault() { + RolloutCacheConfiguration.Builder builder = RolloutCacheConfiguration.builder(); + builder.expiration(-1); + RolloutCacheConfiguration config = builder.build(); + assertEquals(10, config.getExpiration()); + } +} diff --git a/src/test/java/io/split/android/client/SplitClientConfigTest.java b/src/test/java/io/split/android/client/SplitClientConfigTest.java index 8f3351ccb..83564c640 100644 --- a/src/test/java/io/split/android/client/SplitClientConfigTest.java +++ b/src/test/java/io/split/android/client/SplitClientConfigTest.java @@ -3,6 +3,7 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertTrue; import androidx.annotation.NonNull; @@ -224,6 +225,37 @@ public void observerCacheExpirationPeriodMatchesDedupeTimeIntervalWhenDedupeTime assertEquals(TimeUnit.HOURS.toMillis(4), config3.observerCacheExpirationPeriod()); } + @Test + public void rolloutCacheConfigurationDefaults() { + RolloutCacheConfiguration config = SplitClientConfig.builder().build().rolloutCacheConfiguration(); + + assertEquals(10, config.getExpiration()); + assertFalse(config.clearOnInit()); + } + + @Test + public void rolloutCacheConfigurationExpirationIsCorrectlySet() { + RolloutCacheConfiguration config = SplitClientConfig.builder() + .rolloutCacheConfiguration(RolloutCacheConfiguration.builder().expiration(1).clearOnInit(true).build()) + .build().rolloutCacheConfiguration(); + + assertEquals(1, config.getExpiration()); + assertTrue(config.clearOnInit()); + } + + @Test + public void nullRolloutCacheConfigurationSetsDefault() { + Queue logMessages = getLogMessagesQueue(); + RolloutCacheConfiguration config = SplitClientConfig.builder() + .logLevel(SplitLogLevel.WARNING) + .rolloutCacheConfiguration(null) + .build().rolloutCacheConfiguration(); + + assertEquals(10, config.getExpiration()); + assertFalse(config.clearOnInit()); + assertEquals(1, logMessages.size()); + } + @NonNull private static Queue getLogMessagesQueue() { Queue logMessages = new LinkedList<>(); diff --git a/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt b/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt index 20e1b2c24..aa710c378 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt +++ b/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt @@ -1,5 +1,6 @@ package io.split.android.client.service.synchronizer +import io.split.android.client.RolloutCacheConfiguration import io.split.android.client.service.CleanUpDatabaseTask import io.split.android.client.service.executor.SplitTaskExecutionListener import io.split.android.client.storage.RolloutDefinitionsCache @@ -36,7 +37,7 @@ class RolloutCacheManagerTest { @Test fun `validateCache calls listener`() { - mRolloutCacheManager = getCacheManager(10L, false) + mRolloutCacheManager = getCacheManager(10, false) val listener = mock(SplitTaskExecutionListener::class.java) mRolloutCacheManager.validateCache(listener) @@ -46,9 +47,9 @@ class RolloutCacheManagerTest { @Test fun `validateCache calls clear on storages when expiration is surpassed`() { - val mockedTimestamp = createMockedTimestamp(10L) + val mockedTimestamp = createMockedTimestamp(10) `when`(mGeneralInfoStorage.splitsUpdateTimestamp).thenReturn(mockedTimestamp) - mRolloutCacheManager = getCacheManager(9L, false) + mRolloutCacheManager = getCacheManager(9, false) mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) @@ -60,7 +61,7 @@ class RolloutCacheManagerTest { fun `validateCache does not call clear on storages when expiration is not surpassed and clearOnInit is false`() { val mockedTimestamp = createMockedTimestamp(1L) `when`(mGeneralInfoStorage.splitsUpdateTimestamp).thenReturn(mockedTimestamp) - mRolloutCacheManager = getCacheManager(10L, false) + mRolloutCacheManager = getCacheManager(10, false) mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) @@ -72,7 +73,7 @@ class RolloutCacheManagerTest { fun `validateCache calls clear on storages when expiration is not surpassed and clearOnInit is true`() { val mockedTimestamp = createMockedTimestamp(1L) `when`(mGeneralInfoStorage.splitsUpdateTimestamp).thenReturn(mockedTimestamp) - mRolloutCacheManager = getCacheManager(10L, true) + mRolloutCacheManager = getCacheManager(10, true) mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) @@ -85,7 +86,7 @@ class RolloutCacheManagerTest { val mockedTimestamp = createMockedTimestamp(1L) `when`(mGeneralInfoStorage.splitsUpdateTimestamp).thenReturn(mockedTimestamp) `when`(mGeneralInfoStorage.rolloutCacheLastClearTimestamp).thenReturn(0L).thenReturn(TimeUnit.HOURS.toMillis(TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis()) - 1)) - mRolloutCacheManager = getCacheManager(10L, true) + mRolloutCacheManager = getCacheManager(10, true) mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) @@ -99,7 +100,7 @@ class RolloutCacheManagerTest { val mockedTimestamp = createMockedTimestamp(1L) `when`(mGeneralInfoStorage.splitsUpdateTimestamp).thenReturn(mockedTimestamp) `when`(mGeneralInfoStorage.rolloutCacheLastClearTimestamp).thenReturn(0L).thenReturn(TimeUnit.HOURS.toMillis(TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis()) - 1)) - mRolloutCacheManager = getCacheManager(10L, true) + mRolloutCacheManager = getCacheManager(10, true) val listener = mock(SplitTaskExecutionListener::class.java) `when`(mSplitsCache.clear()).thenThrow(RuntimeException("Exception during clear")) @@ -114,7 +115,7 @@ class RolloutCacheManagerTest { val mockedTimestamp = createMockedTimestamp(1L) `when`(mGeneralInfoStorage.splitsUpdateTimestamp).thenReturn(mockedTimestamp) `when`(mGeneralInfoStorage.rolloutCacheLastClearTimestamp).thenReturn(0L).thenReturn(TimeUnit.HOURS.toMillis(TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis()) - 1)) - mRolloutCacheManager = getCacheManager(10L, true) + mRolloutCacheManager = getCacheManager(10, true) mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) @@ -126,7 +127,7 @@ class RolloutCacheManagerTest { val mockedTimestamp = createMockedTimestamp(1L) `when`(mGeneralInfoStorage.splitsUpdateTimestamp).thenReturn(mockedTimestamp) `when`(mGeneralInfoStorage.rolloutCacheLastClearTimestamp).thenReturn(0L).thenReturn(TimeUnit.HOURS.toMillis(TimeUnit.MILLISECONDS.toHours(System.currentTimeMillis()) - 1)) - mRolloutCacheManager = getCacheManager(10L, false) + mRolloutCacheManager = getCacheManager(10, false) mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) @@ -135,7 +136,7 @@ class RolloutCacheManagerTest { @Test fun `validateCache executes cleanUpDatabaseTask`() { - mRolloutCacheManager = getCacheManager(10L, false) + mRolloutCacheManager = getCacheManager(10, false) mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) @@ -144,15 +145,39 @@ class RolloutCacheManagerTest { @Test fun `validateCache executes encryptionMigrationTask`() { - mRolloutCacheManager = getCacheManager(10L, false) + mRolloutCacheManager = getCacheManager(10, false) mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) verify(mEncryptionMigrationTask).execute() } - private fun getCacheManager(expiration: Long, clearOnInit: Boolean): RolloutCacheManager { - return RolloutCacheManagerImpl(mGeneralInfoStorage, RolloutCacheManagerConfig(expiration, clearOnInit), mCleanUpDatabaseTask, mEncryptionMigrationTask, mSplitsCache, mSegmentsCache) + @Test + fun `default value for update timestamp does not clear cache`() { + `when`(mGeneralInfoStorage.splitsUpdateTimestamp).thenReturn(0L) + `when`(mGeneralInfoStorage.rolloutCacheLastClearTimestamp).thenReturn(0L) + mRolloutCacheManager = getCacheManager(10, false) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mSplitsCache, times(0)).clear() + verify(mSegmentsCache, times(0)).clear() + } + + @Test + fun `default value for last clear timestamp clears cache when clearOnInit is true`() { + `when`(mGeneralInfoStorage.splitsUpdateTimestamp).thenReturn(createMockedTimestamp(System.currentTimeMillis())) + `when`(mGeneralInfoStorage.rolloutCacheLastClearTimestamp).thenReturn(0L) + mRolloutCacheManager = getCacheManager(10, true) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mSplitsCache).clear() + verify(mSegmentsCache).clear() + } + + private fun getCacheManager(expiration: Int, clearOnInit: Boolean): RolloutCacheManager { + return RolloutCacheManagerImpl(mGeneralInfoStorage, RolloutCacheConfiguration.builder().expiration(expiration).clearOnInit(clearOnInit).build(), mCleanUpDatabaseTask, mEncryptionMigrationTask, mSplitsCache, mSegmentsCache) } private fun createMockedTimestamp(period: Long): Long {