diff --git a/CHANGES.txt b/CHANGES.txt index 4a034b66a..c9a631aef 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +5.0.1 (Nov 22, 2024) +- Optimized persistence of impressions deduplication cache. + 5.0.0 (Nov 1, 2024) - Added support for targeting rules based on large segments. - BREAKING: Dropped support for Split Proxy below version 5.9.0. The SDK now requires Split Proxy 5.9.0 or above. diff --git a/build.gradle b/build.gradle index 0ebef0b78..a2245db98 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '5.0.0' + splitVersion = '5.0.1' } android { diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java index 6709488ae..105d47d3d 100644 --- a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java +++ b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java @@ -13,6 +13,7 @@ import org.junit.Test; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -24,6 +25,8 @@ import fake.HttpClientMock; import fake.HttpResponseMock; import fake.HttpResponseMockDispatcher; +import fake.LifecycleManagerStub; +import fake.SynchronizerSpyImpl; import helper.DatabaseHelper; import helper.FileHelper; import helper.IntegrationHelper; @@ -36,8 +39,10 @@ import io.split.android.client.impressions.Impression; import io.split.android.client.impressions.ImpressionListener; import io.split.android.client.service.impressions.ImpressionsMode; +import io.split.android.client.service.synchronizer.SynchronizerSpy; import io.split.android.client.storage.db.ImpressionEntity; import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheEntity; import io.split.android.client.utils.Json; import tests.integration.shared.TestingHelper; @@ -47,6 +52,8 @@ public class DedupeIntegrationTest { private AtomicInteger mImpressionsListenerCount; private HttpClientMock mHttpClient; private SplitRoomDatabase mDatabase; + private LifecycleManagerStub mLifecycleManager; + private SynchronizerSpy mSynchronizerSpy; @Before public void setUp() throws IOException { @@ -54,6 +61,9 @@ public void setUp() throws IOException { mDatabase = DatabaseHelper.getTestDatabase(mContext); mDatabase.clearAllTables(); mHttpClient = new HttpClientMock(getDispatcher()); + mLifecycleManager = new LifecycleManagerStub(); + mSynchronizerSpy = new SynchronizerSpyImpl(); + mLifecycleManager.register(mSynchronizerSpy); } @Test @@ -203,26 +213,34 @@ public void close() { @Test public void expiredObserverCacheValuesExistingInDatabaseAreRemovedOnStartup() throws InterruptedException { // prepopulate DB with 2000 entries + List entities = new ArrayList<>(); for (int i = 0; i < 2000; i++) { - mDatabase.impressionsObserverCacheDao().insert((long) i, (long) i, System.currentTimeMillis()); + entities.add(new ImpressionsObserverCacheEntity(i, i, System.currentTimeMillis())); } + mDatabase.impressionsObserverCacheDao().insert(entities); // wait for them to expire - Thread.sleep(1000); + Thread.sleep(2000); // initialize SDK SplitClient client = initSplitFactory(new TestableSplitConfigBuilder() .impressionsMode(ImpressionsMode.DEBUG) + .streamingEnabled(false) .enableDebug() .impressionsDedupeTimeInterval(1) .observerCacheExpirationPeriod(100), mHttpClient).client(); + Thread.sleep(200); client.getTreatment("FACUNDO_TEST"); - Thread.sleep(2000); + Thread.sleep(100); + mLifecycleManager.simulateOnPause(); + Thread.sleep(500); + mLifecycleManager.simulateOnResume(); + while (mDatabase.impressionsObserverCacheDao().getAll(5).size() > 1) { Thread.sleep(100); } - Thread.sleep(100); + Thread.sleep(1000); int count = mDatabase.impressionsObserverCacheDao().getAll(3000).size(); assertEquals(1, count); @@ -309,7 +327,8 @@ private SplitFactory initSplitFactory(TestableSplitConfigBuilder builder, HttpCl builder.build(), mContext, httpClient, - mDatabase); + mDatabase, mSynchronizerSpy, null, + mLifecycleManager); SplitClient client = factory.client(); client.on(SplitEvent.SDK_READY, new TestingHelper.TestEventTask(innerLatch)); diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplIntegrationTest.java b/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplIntegrationTest.java index b6e840e38..0dc569350 100644 --- a/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplIntegrationTest.java +++ b/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplIntegrationTest.java @@ -26,7 +26,7 @@ public class ImpressionsObserverCacheImplIntegrationTest { @Before public void setUp() { ImpressionsObserverCacheDao impressionsObserverCacheDao = DatabaseHelper.getTestDatabase(InstrumentationRegistry.getInstrumentation().getContext()).impressionsObserverCacheDao(); - mPersistentStorage = new SqlitePersistentImpressionsObserverCacheStorage(impressionsObserverCacheDao, 2000, 1, Executors.newSingleThreadScheduledExecutor(), new AtomicBoolean(false)); + mPersistentStorage = new SqlitePersistentImpressionsObserverCacheStorage(impressionsObserverCacheDao, 2000, Executors.newSingleThreadScheduledExecutor(), new AtomicBoolean(false)); mCache = new ListenableLruCache<>(5, mPersistentStorage); mImpressionsObserverCacheImpl = new ImpressionsObserverCacheImpl(mPersistentStorage, mCache); } @@ -88,6 +88,7 @@ public void getPutsValueInCacheWhenRetrievedFromPersistentStorage() throws Inter @Test public void putPutsValueInCacheAndPersistentStorage() throws InterruptedException { mImpressionsObserverCacheImpl.put(1L, 2L); + mImpressionsObserverCacheImpl.persist(); Thread.sleep(100); assertEquals(2L, mPersistentStorage.get(1L).longValue()); @@ -98,6 +99,7 @@ public void putPutsValueInCacheAndPersistentStorage() throws InterruptedExceptio public void putUpdatesValueInCacheAndPersistentStorage() throws InterruptedException { mImpressionsObserverCacheImpl.put(1L, 2L); mImpressionsObserverCacheImpl.put(1L, 3L); + mImpressionsObserverCacheImpl.persist(); Thread.sleep(100); assertEquals(3L, mPersistentStorage.get(1L).longValue()); @@ -106,6 +108,7 @@ public void putUpdatesValueInCacheAndPersistentStorage() throws InterruptedExcep private void putInStorageAndWait() throws InterruptedException { mPersistentStorage.put(1L, 2L); + mPersistentStorage.persist(); Thread.sleep(100); } } diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java b/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java index 506131728..250044137 100644 --- a/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java +++ b/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import androidx.test.platform.app.InstrumentationRegistry; @@ -32,7 +34,7 @@ public class ImpressionsObserverTest { @Before public void setUp() { ImpressionsObserverCacheDao dao = DatabaseHelper.getTestDatabase(InstrumentationRegistry.getInstrumentation().getContext()).impressionsObserverCacheDao(); - mStorage = new SqlitePersistentImpressionsObserverCacheStorage(dao, 2000, 1, Executors.newSingleThreadScheduledExecutor(), new AtomicBoolean(false)); + mStorage = new SqlitePersistentImpressionsObserverCacheStorage(dao, 2000, Executors.newSingleThreadScheduledExecutor(), new AtomicBoolean(false)); } private List generateImpressions(long count) { @@ -89,6 +91,7 @@ public void testValuesArePersistedAcrossInstances() throws InterruptedException // These are not in the cache, so they should return null Long firstImp = observer.testAndSet(imp); Long firstImp2 = observer.testAndSet(imp2); + observer.persist(); Thread.sleep(2); // These are in the cache, so they should return a value @@ -155,6 +158,15 @@ public void testAndSetWithNullImpressionReturnsNullPreviousTime() { assertNull(observer.testAndSet(null)); } + @Test + public void persistCallsPersistOnStorage() { + ImpressionsObserverCache cache = mock(ImpressionsObserverCache.class); + ImpressionsObserver observer = new ImpressionsObserverImpl(cache); + observer.persist(); + + verify(cache).persist(); + } + private void caller(ImpressionsObserver o, int count, ConcurrentLinkedQueue imps) { while (count-- > 0) { diff --git a/src/androidTest/java/tests/database/ImpressionsObserverCacheDaoTest.java b/src/androidTest/java/tests/database/ImpressionsObserverCacheDaoTest.java index 71b49438e..683a3b558 100644 --- a/src/androidTest/java/tests/database/ImpressionsObserverCacheDaoTest.java +++ b/src/androidTest/java/tests/database/ImpressionsObserverCacheDaoTest.java @@ -8,6 +8,7 @@ import org.junit.Before; import org.junit.Test; +import java.util.Arrays; import java.util.List; import helper.DatabaseHelper; @@ -34,8 +35,7 @@ public void tearDown() { @Test public void valuesAreInsertedCorrectly() { - mImpressionsObserverCacheDao.insert(1L, 2L, 3L); - mImpressionsObserverCacheDao.insert(3L, 2L, 5L); + insertIntoDao(); List all = mImpressionsObserverCacheDao.getAll(3); @@ -50,10 +50,17 @@ public void valuesAreInsertedCorrectly() { assertEquals(secondEntity.getCreatedAt(), 5L); } + private void insertIntoDao() { + List entities = Arrays.asList( + new ImpressionsObserverCacheEntity(1L, 2L, 3L), + new ImpressionsObserverCacheEntity(3L, 2L, 5L)); + mImpressionsObserverCacheDao.insert(entities); + } + @Test public void valueWithNewHashReplacesOldOne() { - mImpressionsObserverCacheDao.insert(1L, 2L, 3L); - mImpressionsObserverCacheDao.insert(1L, 4L, 5L); + insertIntoDaoOnce(1L, 2L, 3L); + insertIntoDaoOnce(1L, 4L, 5L); List all = mImpressionsObserverCacheDao.getAll(3); @@ -64,10 +71,14 @@ public void valueWithNewHashReplacesOldOne() { assertEquals(firstEntity.getCreatedAt(), 5L); } + private void insertIntoDaoOnce(long hash, long time, long createdAt) { + ImpressionsObserverCacheEntity entity = new ImpressionsObserverCacheEntity(hash, time, createdAt); + mImpressionsObserverCacheDao.insert(Arrays.asList(entity)); + } + @Test public void deleteRemovesCorrectHash() { - mImpressionsObserverCacheDao.insert(1L, 2L, 3L); - mImpressionsObserverCacheDao.insert(3L, 2L, 5L); + insertIntoDao(); mImpressionsObserverCacheDao.delete(1L); List all = mImpressionsObserverCacheDao.getAll(3); @@ -81,10 +92,9 @@ public void deleteRemovesCorrectHash() { @Test public void getAllWithLimitReturnsTheCorrectAmount() { - mImpressionsObserverCacheDao.insert(1L, 2L, 3L); - mImpressionsObserverCacheDao.insert(3L, 2L, 5L); - mImpressionsObserverCacheDao.insert(4L, 2L, 6L); - mImpressionsObserverCacheDao.insert(5L, 2L, 7L); + insertIntoDao(); + insertIntoDaoOnce(4L, 2L, 6L); + insertIntoDaoOnce(5L, 2L, 7L); List all = mImpressionsObserverCacheDao.getAll(2); @@ -93,10 +103,10 @@ public void getAllWithLimitReturnsTheCorrectAmount() { @Test public void getAllReturnsElementsOrderedByCreatedAtAsc() { - mImpressionsObserverCacheDao.insert(3L, 2L, 3L); - mImpressionsObserverCacheDao.insert(4L, 6L, 5L); - mImpressionsObserverCacheDao.insert(5L, 4L, 6L); - mImpressionsObserverCacheDao.insert(1L, 1L, 7L); + insertIntoDaoOnce(3L, 2L, 3L); + insertIntoDaoOnce(4L, 6L, 5L); + insertIntoDaoOnce(5L, 4L, 6L); + insertIntoDaoOnce(1L, 1L, 7L); List all = mImpressionsObserverCacheDao.getAll(4); @@ -109,13 +119,13 @@ public void getAllReturnsElementsOrderedByCreatedAtAsc() { @Test public void deleteOldestRemovesCorrectValues() { - mImpressionsObserverCacheDao.insert(3L, 2L, 3L); - mImpressionsObserverCacheDao.insert(4L, 6L, 5L); + insertIntoDaoOnce(3L, 2L, 3L); + insertIntoDaoOnce(4L, 6L, 5L); // only these ones should remain - mImpressionsObserverCacheDao.insert(5L, 4L, 6L); - mImpressionsObserverCacheDao.insert(12L, 4L, 7L); - mImpressionsObserverCacheDao.insert(21L, 3L, 8L); + insertIntoDaoOnce(5L, 4L, 6L); + insertIntoDaoOnce(12L, 4L, 7L); + insertIntoDaoOnce(21L, 3L, 8L); mImpressionsObserverCacheDao.deleteOldest(6); @@ -129,8 +139,8 @@ public void deleteOldestRemovesCorrectValues() { @Test public void getSingleValueReturnsCorrectValue() { - mImpressionsObserverCacheDao.insert(3L, 2L, 3L); - mImpressionsObserverCacheDao.insert(4L, 6L, 5L); + insertIntoDaoOnce(3L, 2L, 3L); + insertIntoDaoOnce(4L, 6L, 5L); ImpressionsObserverCacheEntity entity = mImpressionsObserverCacheDao.get(3L); diff --git a/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java b/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java index c3556e838..f7f9c6127 100644 --- a/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java +++ b/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java @@ -1,5 +1,7 @@ package tests.integration.streaming; +import static java.lang.Thread.sleep; + import android.content.Context; import androidx.core.util.Pair; @@ -12,6 +14,7 @@ import java.io.IOException; import java.net.URI; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; @@ -43,13 +46,11 @@ import io.split.android.client.storage.db.StorageRecordStatus; import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheDao; import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheEntity; -import io.split.android.client.utils.logger.Logger; import io.split.android.client.storage.db.impressions.unique.UniqueKeyEntity; import io.split.android.client.storage.db.impressions.unique.UniqueKeysDao; +import io.split.android.client.utils.logger.Logger; import io.split.android.client.utils.logger.SplitLogLevel; -import static java.lang.Thread.sleep; - public class CleanUpDatabaseTest { Context mContext; BlockingQueue mStreamingData; @@ -109,9 +110,11 @@ public void testCleanUp() throws IOException, InterruptedException { mUniqueKeysDao.insert(createUniqueKeyEntity(now() + 10, StorageRecordStatus.ACTIVE, "active")); mUniqueKeysDao.insert(createUniqueKeyEntity(expiratedTime(), StorageRecordStatus.ACTIVE, "expirated")); - mImpressionsObserverCacheDao.insert(1L, 2L, now()); - mImpressionsObserverCacheDao.insert(5L, 6L, now()); - mImpressionsObserverCacheDao.insert(3L, 4L, TimeUnit.SECONDS.toMillis(now()) - ServiceConstants.DEFAULT_OBSERVER_CACHE_EXPIRATION_PERIOD_MS); + List entities = new ArrayList<>(); + entities.add(new ImpressionsObserverCacheEntity(1L, 2L, now())); + entities.add(new ImpressionsObserverCacheEntity(5L, 6L, now())); + entities.add(new ImpressionsObserverCacheEntity(3L, 4L, TimeUnit.SECONDS.toMillis(now()) - ServiceConstants.DEFAULT_OBSERVER_CACHE_EXPIRATION_PERIOD_MS)); + mImpressionsObserverCacheDao.insert(entities); // Load records to check if inserted correctly on assert stage List insertedEvents = mEventDao.getBy(0, StorageRecordStatus.ACTIVE, 10); diff --git a/src/main/java/io/split/android/client/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index 45dacb73a..16f26d762 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -16,6 +16,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -153,7 +154,8 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, boolean shouldRecordTelemetry, SplitCipher splitCipher, TelemetryStorage telemetryStorage, - long observerCacheExpirationPeriod) { + long observerCacheExpirationPeriod, + ScheduledThreadPoolExecutor impressionsObserverExecutor) { boolean isPersistenceEnabled = userConsentStatus == UserConsent.GRANTED; PersistentEventsStorage persistentEventsStorage = @@ -174,7 +176,7 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, StorageFactory.getAttributesStorage(), StorageFactory.getPersistentAttributesStorage(splitRoomDatabase, splitCipher), getTelemetryStorage(shouldRecordTelemetry, telemetryStorage), - StorageFactory.getImpressionsObserverCachePersistentStorage(splitRoomDatabase, observerCacheExpirationPeriod)); + StorageFactory.getImpressionsObserverCachePersistentStorage(splitRoomDatabase, observerCacheExpirationPeriod, impressionsObserverExecutor)); } SplitApiFacade buildApiFacade(SplitClientConfig splitClientConfig, diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index da1db63ff..b5c0252e3 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -11,6 +11,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; import io.split.android.android_client.BuildConfig; import io.split.android.client.api.Key; @@ -176,8 +178,10 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { config.encryptionEnabled(), mMigrationExecutionListener); + ScheduledThreadPoolExecutor impressionsObserverExecutor = new ScheduledThreadPoolExecutor(1, + new ThreadPoolExecutor.CallerRunsPolicy()); mStorageContainer = factoryHelper.buildStorageContainer(config.userConsent(), - splitDatabase, config.shouldRecordTelemetry(), splitCipher, telemetryStorage, config.observerCacheExpirationPeriod()); + splitDatabase, config.shouldRecordTelemetry(), splitCipher, telemetryStorage, config.observerCacheExpirationPeriod(), impressionsObserverExecutor); Pair, String> filtersConfig = factoryHelper.getFilterConfiguration(config.syncConfig()); Map filters = filtersConfig.first; @@ -290,6 +294,7 @@ public void run() { telemetrySynchronizer.destroy(); Logger.d("Successful shutdown of telemetry"); impressionsLoggingTaskExecutor.shutdown(); + impressionsObserverExecutor.shutdown(); Logger.d("Successful shutdown of impressions logging executor"); mSyncManager.stop(); Logger.d("Flushing impressions and events"); diff --git a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactory.java b/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactory.java index 2f352b113..f4c046618 100644 --- a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactory.java +++ b/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactory.java @@ -7,4 +7,6 @@ public interface SplitParallelTaskExecutorFactory { SplitParallelTaskExecutor> createForList(Class type); SplitParallelTaskExecutor create(Class type); + + SplitParallelTaskExecutor create(Class type, int timeoutInSeconds); } diff --git a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java b/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java index 7171b7937..7b40dd611 100644 --- a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java @@ -32,4 +32,9 @@ public SplitParallelTaskExecutor> createForList(Class type) { public SplitParallelTaskExecutor create(Class type) { return new SplitParallelTaskExecutorImpl<>(mThreads, mScheduler); } + + @Override + public SplitParallelTaskExecutor create(Class type, int timeoutInSeconds) { + return new SplitParallelTaskExecutorImpl<>(mThreads, mScheduler, timeoutInSeconds); + } } diff --git a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImpl.java b/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImpl.java index 0a05e0a32..abacc3471 100644 --- a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImpl.java +++ b/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImpl.java @@ -15,22 +15,29 @@ public class SplitParallelTaskExecutorImpl implements SplitParallelTaskExecutor { - private static final int TIMEOUT_IN_SECONDS = 5; + private static final int TIMEOUT_IN_SECONDS = 60; private final int mThreads; private final ExecutorService mScheduler; - public SplitParallelTaskExecutorImpl(int threads, ExecutorService scheduler) { + private final int mTimeoutInSeconds; + + SplitParallelTaskExecutorImpl(int threads, ExecutorService scheduler) { + this(threads, scheduler, TIMEOUT_IN_SECONDS); + } + + SplitParallelTaskExecutorImpl(int threads, ExecutorService scheduler, int timeoutInSeconds) { mThreads = threads; mScheduler = scheduler; + mTimeoutInSeconds = timeoutInSeconds; } @Override @WorkerThread public List execute(Collection> splitDeferredTaskItems) { try { - List> futures = mScheduler.invokeAll(splitDeferredTaskItems, TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); + List> futures = mScheduler.invokeAll(splitDeferredTaskItems, mTimeoutInSeconds, TimeUnit.SECONDS); ArrayList results = new ArrayList<>(); for (Future future : futures) { diff --git a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserver.java b/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserver.java index cf7686644..0768de027 100644 --- a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserver.java +++ b/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserver.java @@ -5,6 +5,9 @@ import io.split.android.client.impressions.Impression; public interface ImpressionsObserver { + @Nullable Long testAndSet(Impression impression); + + void persist(); } diff --git a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCache.java b/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCache.java index dca1e3788..880da383a 100644 --- a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCache.java +++ b/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCache.java @@ -8,4 +8,6 @@ interface ImpressionsObserverCache { Long get(long hash); void put(long hash, long time); + + void persist(); } diff --git a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImpl.java b/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImpl.java index 762515fb5..a3e9ea108 100644 --- a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImpl.java +++ b/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImpl.java @@ -77,6 +77,11 @@ public void put(long hash, long time) { } } + @Override + public void persist() { + mPersistentStorage.persist(); + } + @Nullable private Long getFromCache(long hash) { try { diff --git a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java b/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java index bc51c0d59..1e3df51d1 100644 --- a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java +++ b/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java @@ -1,6 +1,9 @@ package io.split.android.client.service.impressions.observer; +import static io.split.android.client.utils.Utils.checkNotNull; + import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import io.split.android.client.impressions.Impression; import io.split.android.client.service.impressions.ImpressionHasher; @@ -10,7 +13,12 @@ public class ImpressionsObserverImpl implements ImpressionsObserver { private final ImpressionsObserverCache mCache; public ImpressionsObserverImpl(PersistentImpressionsObserverCacheStorage persistentStorage, int size) { - mCache = new ImpressionsObserverCacheImpl(persistentStorage, size); + this(new ImpressionsObserverCacheImpl(persistentStorage, size)); + } + + @VisibleForTesting + ImpressionsObserverImpl(ImpressionsObserverCache cache) { + mCache = checkNotNull(cache); } @Override @@ -27,4 +35,9 @@ public Long testAndSet(Impression impression) { return (previous == null ? null : Math.min(previous, impression.time())); } + + @Override + public void persist() { + mCache.persist(); + } } diff --git a/src/main/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTask.java b/src/main/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTask.java index 7a8c5e762..59c7dc3b5 100644 --- a/src/main/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTask.java +++ b/src/main/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTask.java @@ -1,9 +1,12 @@ package io.split.android.client.service.impressions.observer; import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheDao; +import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheEntity; import io.split.android.client.utils.logger.Logger; public class PeriodicPersistenceTask implements Runnable { @@ -12,8 +15,7 @@ public class PeriodicPersistenceTask implements Runnable { private final ImpressionsObserverCacheDao mImpressionsObserverCacheDao; private final WeakReference mOnExecutedListener; - PeriodicPersistenceTask(Map cache, ImpressionsObserverCacheDao impressionsObserverCacheDao, OnExecutedListener onExecutedListener - ) { + PeriodicPersistenceTask(Map cache, ImpressionsObserverCacheDao impressionsObserverCacheDao, OnExecutedListener onExecutedListener) { mCache = cache; mImpressionsObserverCacheDao = impressionsObserverCacheDao; mOnExecutedListener = new WeakReference<>(onExecutedListener); @@ -23,21 +25,28 @@ public class PeriodicPersistenceTask implements Runnable { public void run() { try { if (mCache != null) { - for (Map.Entry entry : mCache.entrySet()) { - try { - mImpressionsObserverCacheDao.insert(entry.getKey(), entry.getValue(), System.currentTimeMillis()); - } catch (Exception ex) { - Logger.e("Error while persisting element in observer cache: " + ex.getLocalizedMessage()); + try { + List entities = new ArrayList<>(); + for (Map.Entry entry : mCache.entrySet()) { + try { + entities.add(new ImpressionsObserverCacheEntity(entry.getKey(), entry.getValue(), System.currentTimeMillis())); + } catch (Exception ex) { + Logger.e("Error while creating observer cache entity"); + } } + + if (!entities.isEmpty()) { + mImpressionsObserverCacheDao.insert(entities); + } + + mCache.clear(); + } catch (Exception ex) { + Logger.e("Error while persisting elements in observer cache: " + ex.getLocalizedMessage()); } } } catch (Exception ex) { Logger.e("Error while persisting observer cache: " + ex.getLocalizedMessage()); } finally { - if (mCache != null) { - mCache.clear(); - } - if (mOnExecutedListener.get() != null) { mOnExecutedListener.get().onExecuted(); } diff --git a/src/main/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorage.java b/src/main/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorage.java index 7ee8eba64..b52ce12d2 100644 --- a/src/main/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorage.java +++ b/src/main/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorage.java @@ -11,8 +11,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheDao; @@ -21,35 +19,28 @@ public class SqlitePersistentImpressionsObserverCacheStorage implements PersistentImpressionsObserverCacheStorage { - private static final long DEFAULT_PERSISTENCE_DELAY = 1000; - private final ImpressionsObserverCacheDao mImpressionsObserverCacheDao; private final long mExpirationPeriod; - private final long mPersistenceDelay; private final ScheduledExecutorService mExecutorsService; private final Map mCache = new ConcurrentHashMap<>(); private final AtomicBoolean mDelayedSyncRunning; private final PeriodicPersistenceTask.OnExecutedListener mCallback; public SqlitePersistentImpressionsObserverCacheStorage(@NonNull ImpressionsObserverCacheDao impressionsObserverCacheDao, - long expirationPeriod) { + long expirationPeriod, ScheduledThreadPoolExecutor executorService) { this(impressionsObserverCacheDao, expirationPeriod, - DEFAULT_PERSISTENCE_DELAY, - new ScheduledThreadPoolExecutor(1, - new ThreadPoolExecutor.CallerRunsPolicy()), + executorService, new AtomicBoolean(false)); } @VisibleForTesting SqlitePersistentImpressionsObserverCacheStorage(@NonNull ImpressionsObserverCacheDao impressionsObserverCacheDao, long expirationPeriod, - long persistenceDelay, ScheduledExecutorService executorService, AtomicBoolean delayedSyncRunning) { mImpressionsObserverCacheDao = checkNotNull(impressionsObserverCacheDao); mExpirationPeriod = expirationPeriod; - mPersistenceDelay = persistenceDelay; mExecutorsService = executorService; mDelayedSyncRunning = delayedSyncRunning; mCallback = new PeriodicPersistenceTask.OnExecutedListener() { @@ -65,13 +56,6 @@ public void onExecuted() { @WorkerThread public void put(long hash, long time) { mCache.put(hash, time); - - // If the task is not running, schedule it - if (mDelayedSyncRunning.compareAndSet(false, true)) { - mExecutorsService.schedule(new PeriodicPersistenceTask(mCache, mImpressionsObserverCacheDao, mCallback), - mPersistenceDelay, - TimeUnit.MILLISECONDS); - } } @Override @@ -98,4 +82,11 @@ public void onRemoval(Long key) { mCache.remove(key); mImpressionsObserverCacheDao.delete(key); } + + @Override + public void persist() { + if (mDelayedSyncRunning.compareAndSet(false, true)) { + mExecutorsService.submit(new PeriodicPersistenceTask(mCache, mImpressionsObserverCacheDao, mCallback)); + } + } } diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java b/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java index 4cae56895..ffeeaa60f 100644 --- a/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java +++ b/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java @@ -108,6 +108,7 @@ public void startPeriodicRecording() { @Override public void stopPeriodicRecording() { mDebugTracker.stopPeriodicRecording(); + mImpressionsObserver.persist(); } @Override diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java b/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java index be5773ffa..e232e5ff6 100644 --- a/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java +++ b/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java @@ -152,6 +152,7 @@ public void startPeriodicRecording() { @Override public void stopPeriodicRecording() { mOptimizedTracker.stopPeriodicRecording(); + mImpressionsObserver.persist(); } @Override diff --git a/src/main/java/io/split/android/client/storage/db/StorageFactory.java b/src/main/java/io/split/android/client/storage/db/StorageFactory.java index f4d6b9be1..bd2445599 100644 --- a/src/main/java/io/split/android/client/storage/db/StorageFactory.java +++ b/src/main/java/io/split/android/client/storage/db/StorageFactory.java @@ -4,6 +4,8 @@ import androidx.annotation.RestrictTo; +import java.util.concurrent.ScheduledThreadPoolExecutor; + import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.impressions.observer.PersistentImpressionsObserverCacheStorage; import io.split.android.client.service.impressions.observer.SqlitePersistentImpressionsObserverCacheStorage; @@ -143,7 +145,7 @@ private static AttributesStorageContainer getAttributesStorageContainerInstance( return new AttributesStorageContainerImpl(); } - public static PersistentImpressionsObserverCacheStorage getImpressionsObserverCachePersistentStorage(SplitRoomDatabase splitRoomDatabase, long expirationPeriod) { - return new SqlitePersistentImpressionsObserverCacheStorage(splitRoomDatabase.impressionsObserverCacheDao(), expirationPeriod); + public static PersistentImpressionsObserverCacheStorage getImpressionsObserverCachePersistentStorage(SplitRoomDatabase splitRoomDatabase, long expirationPeriod, ScheduledThreadPoolExecutor executorService) { + return new SqlitePersistentImpressionsObserverCacheStorage(splitRoomDatabase.impressionsObserverCacheDao(), expirationPeriod, executorService); } } diff --git a/src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheDao.java b/src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheDao.java index 8e4cbf861..0cb3d2d45 100644 --- a/src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheDao.java +++ b/src/main/java/io/split/android/client/storage/db/impressions/observer/ImpressionsObserverCacheDao.java @@ -1,6 +1,8 @@ package io.split.android.client.storage.db.impressions.observer; import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; @@ -11,6 +13,9 @@ public interface ImpressionsObserverCacheDao { @Query("INSERT OR REPLACE INTO impressions_observer_cache (hash, time, created_at) VALUES (:hash, :time, :createdAt)") void insert(Long hash, Long time, Long createdAt); + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(List entities); + @Query("SELECT hash, time, created_at FROM impressions_observer_cache ORDER BY created_at ASC LIMIT :limit") List getAll(int limit); diff --git a/src/test/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImplTest.java b/src/test/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImplTest.java index db5e66c56..c414f215b 100644 --- a/src/test/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImplTest.java +++ b/src/test/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorImplTest.java @@ -17,7 +17,7 @@ public class SplitParallelTaskExecutorImplTest { @Before public void setUp() { - executor = new SplitParallelTaskExecutorFactoryImpl().create(String.class); + executor = new SplitParallelTaskExecutorFactoryImpl().create(String.class, 1); } @Test @@ -76,10 +76,10 @@ public void tasksStartExecutingSimultaneously() { } @Test - public void resultIsReturnedWhenComputationExceeds5Seconds() { + public void resultIsReturnedWhenComputationExceeds1Second() { List> splitDeferredTaskItems = Collections.singletonList( new SplitDeferredTaskItem<>(() -> { - Thread.sleep(6000); + Thread.sleep(1000); return "no"; }) @@ -88,6 +88,6 @@ public void resultIsReturnedWhenComputationExceeds5Seconds() { long startTime = System.currentTimeMillis(); executor.execute(splitDeferredTaskItems); - assertTrue(System.currentTimeMillis() - startTime < 6000); + assertTrue(System.currentTimeMillis() - startTime < 1010); } } diff --git a/src/test/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplTest.java b/src/test/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplTest.java index e0f8a0145..96e4b1691 100644 --- a/src/test/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplTest.java +++ b/src/test/java/io/split/android/client/service/impressions/observer/ImpressionsObserverCacheImplTest.java @@ -153,4 +153,11 @@ public void putStillPutsValueInCacheIfPutInPersistentStorageFails() { verify(mCache).put(1L, 2L); } + + @Test + public void persistCallsPersistOnStorage() { + mImpressionsObserverCacheImpl.persist(); + + verify(mPersistentStorage).persist(); + } } diff --git a/src/test/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTaskTest.java b/src/test/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTaskTest.java index 64bff8fe0..ad47e6887 100644 --- a/src/test/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTaskTest.java +++ b/src/test/java/io/split/android/client/service/impressions/observer/PeriodicPersistenceTaskTest.java @@ -1,25 +1,24 @@ package io.split.android.client.service.impressions.observer; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import org.junit.Before; import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import org.mockito.ArgumentMatcher; import java.util.HashMap; +import java.util.List; import java.util.Map; import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheDao; +import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheEntity; public class PeriodicPersistenceTaskTest { @@ -39,7 +38,7 @@ public void noInteractionsWithDaoWhenDelayedSyncRunningIsFalse() { PeriodicPersistenceTask task = new PeriodicPersistenceTask(mCache, mImpressionsObserverCacheDao, mOnExecutedListener); task.run(); - verify(mImpressionsObserverCacheDao, times(0)).insert(any(), any(), any()); + verify(mImpressionsObserverCacheDao, times(0)).insert(anyList()); } @Test @@ -50,8 +49,15 @@ public void valuesFromCacheArePersisted() { PeriodicPersistenceTask task = new PeriodicPersistenceTask(mCache, mImpressionsObserverCacheDao, mOnExecutedListener); task.run(); - verify(mImpressionsObserverCacheDao).insert(eq(1L), eq(1L), anyLong()); - verify(mImpressionsObserverCacheDao).insert(eq(2L), eq(2L), anyLong()); + verify(mImpressionsObserverCacheDao).insert(argThat(new ArgumentMatcher>() { + @Override + public boolean matches(List argument) { + return argument.size() == 2 && (argument.get(0).getHash() == 1L && argument.get(0).getTime() == 1L + && argument.get(1).getHash() == 2L && argument.get(1).getTime() == 2L || + argument.get(1).getHash() == 1L && argument.get(1).getTime() == 1L + && argument.get(0).getHash() == 2L && argument.get(0).getTime() == 2L); + } + })); } @Test @@ -89,7 +95,7 @@ public void callbackIsNotExecutedWhenItIsNull() { public void exceptionInInsertDoesNotThrow() { doAnswer(invocation -> { throw new RuntimeException(); - }).when(mImpressionsObserverCacheDao).insert(eq(1L), eq(1L), any()); + }).when(mImpressionsObserverCacheDao).insert(any()); mCache.put(1L, 1L); mCache.put(2L, 2L); @@ -97,6 +103,11 @@ public void exceptionInInsertDoesNotThrow() { PeriodicPersistenceTask task = new PeriodicPersistenceTask(mCache, mImpressionsObserverCacheDao, mOnExecutedListener); task.run(); - verify(mImpressionsObserverCacheDao).insert(eq(2L), eq(2L), any()); + verify(mImpressionsObserverCacheDao).insert(argThat(new ArgumentMatcher>() { + @Override + public boolean matches(List argument) { + return argument.size() == 2; + } + })); } } diff --git a/src/test/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorageTest.java b/src/test/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorageTest.java index ba58c3c21..94360337a 100644 --- a/src/test/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorageTest.java +++ b/src/test/java/io/split/android/client/service/impressions/observer/SqlitePersistentImpressionsObserverCacheStorageTest.java @@ -1,10 +1,7 @@ package io.split.android.client.service.impressions.observer; import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -14,7 +11,6 @@ import org.junit.Test; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheDao; @@ -33,16 +29,13 @@ public void setUp() { mImpressionsObserverCacheDao = mock(ImpressionsObserverCacheDao.class); mExecutorService = mock(ScheduledExecutorService.class); mDelayedSyncRunning = new AtomicBoolean(false); - when(mExecutorService.schedule( - (Runnable) argThat(argument -> argument instanceof PeriodicPersistenceTask), - anyLong(), - any())).thenAnswer(invocation -> { + when(mExecutorService.submit( + (Runnable) argThat(argument -> argument instanceof PeriodicPersistenceTask))).thenAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; }); mStorage = new SqlitePersistentImpressionsObserverCacheStorage(mImpressionsObserverCacheDao, EXPIRATION_PERIOD, - 1, mExecutorService, mDelayedSyncRunning); } @@ -88,21 +81,17 @@ public void onRemovalCallsDeleteOnDao() { } @Test - public void putCallsPeriodicSync() { + public void putDoesNotCallPeriodicSync() { mStorage.put(1, 2); - verify(mExecutorService).schedule( - (Runnable) argThat(argument -> argument instanceof PeriodicPersistenceTask), - eq(1L), - eq(TimeUnit.MILLISECONDS)); + verify(mExecutorService, times(0)).submit( + (Runnable) argThat(argument -> argument instanceof PeriodicPersistenceTask)); } @Test - public void multiplePutCallsScheduleOneTaskWhenPeriodicSyncIsTrue() { - when(mExecutorService.schedule( - (Runnable) argThat(argument -> argument instanceof PeriodicPersistenceTask), - anyLong(), - any())).thenAnswer(invocation -> { + public void multiplePersistCallsSubmitsOneTaskWhenPeriodicSyncIsTrue() { + when(mExecutorService.submit( + (Runnable) argThat(argument -> argument instanceof PeriodicPersistenceTask))).thenAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); mDelayedSyncRunning.set(true); //simulate task still running return null; @@ -110,10 +99,10 @@ public void multiplePutCallsScheduleOneTaskWhenPeriodicSyncIsTrue() { mStorage.put(1, 2); mStorage.put(3, 4); + mStorage.persist(); + mStorage.persist(); - verify(mExecutorService, times(1)).schedule( - (Runnable) argThat(argument -> argument instanceof PeriodicPersistenceTask), - eq(1L), - eq(TimeUnit.MILLISECONDS)); + verify(mExecutorService, times(1)).submit( + (Runnable) argThat(argument -> argument instanceof PeriodicPersistenceTask)); } } diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt b/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt index 48e35bc23..d7f4a8607 100644 --- a/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt +++ b/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt @@ -109,6 +109,13 @@ class DebugStrategyTest { verify(tracker).stopPeriodicRecording() } + @Test + fun `stopPeriodicRecording calls persist on observer`() { + strategy.stopPeriodicRecording() + + verify(impressionsObserver).persist() + } + @Test fun `enableTracking calls enableTracking on tracker`() { strategy.enableTracking(true) diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt b/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt index c8612d602..f404d9296 100644 --- a/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt +++ b/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt @@ -176,6 +176,13 @@ class OptimizedStrategyTest { verify(tracker).stopPeriodicRecording() } + @Test + fun `stopPeriodicRecording calls persist on ImpressionsObserver`() { + strategy.stopPeriodicRecording() + + verify(impressionsObserver).persist() + } + @Test fun `enableTracking calls enableTracking on tracker`() { strategy.enableTracking(true)