diff --git a/build.gradle b/build.gradle index a2245db98..9e880ea12 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '5.0.1' + splitVersion = '5.1.0-alpha.2' } android { 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/helper/IntegrationHelper.java b/src/androidTest/java/helper/IntegrationHelper.java index 09a17e8b0..528f730fa 100644 --- a/src/androidTest/java/helper/IntegrationHelper.java +++ b/src/androidTest/java/helper/IntegrationHelper.java @@ -27,6 +27,7 @@ import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import fake.HttpClientMock; import fake.HttpResponseMock; @@ -390,40 +391,45 @@ public static Set asSet(T... elements) { return result; } - /** - * A simple interface to allow us to define the response for a given path - */ - public interface ResponseClosure { - HttpResponseMock onResponse(URI uri, - HttpMethod httpMethod, - String body); + public static long getTimestampDaysAgo(int days) { + return System.currentTimeMillis() - TimeUnit.DAYS.toMillis(days); + } - static String getSinceFromUri(URI uri) { - try { - return parse(uri.getQuery()).get("since"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + + public static String getSinceFromUri(URI uri) { + try { + return parse(uri.getQuery()).get("since"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); } + } - static Map parse(String query) throws UnsupportedEncodingException { - Map queryPairs = new HashMap<>(); - String[] pairs = query.split("&"); + static Map parse(String query) throws UnsupportedEncodingException { + Map queryPairs = new HashMap<>(); + String[] pairs = query.split("&"); - for (String pair : pairs) { - int idx = pair.indexOf("="); - try { - String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8"); - String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + try { + String key = URLDecoder.decode(pair.substring(0, idx), "UTF-8"); + String value = URLDecoder.decode(pair.substring(idx + 1), "UTF-8"); - queryPairs.put(key, value); - } catch (Exception e) { - e.printStackTrace(); - } + queryPairs.put(key, value); + } catch (Exception e) { + e.printStackTrace(); } - - return queryPairs; } + + return queryPairs; + } + + /** + * A simple interface to allow us to define the response for a given path + */ + public interface ResponseClosure { + HttpResponseMock onResponse(URI uri, + HttpMethod httpMethod, + String body); } /** @@ -435,5 +441,6 @@ public interface StreamingResponseClosure { public static class ServicePath { public static final String MEMBERSHIPS = "memberships"; + public static final String SPLIT_CHANGES = "splitChanges"; } } 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/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java index 105d47d3d..d3f9152f4 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 @@ -3,7 +3,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; + +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; 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/FlagsSpecInRequestTest.java b/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java index 12ecce898..c289ef64e 100644 --- a/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java +++ b/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java @@ -3,7 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/InitialChangeNumberTest.java b/src/androidTest/java/tests/integration/InitialChangeNumberTest.java index 8ae2f6c62..a8b64a13e 100644 --- a/src/androidTest/java/tests/integration/InitialChangeNumberTest.java +++ b/src/androidTest/java/tests/integration/InitialChangeNumberTest.java @@ -1,6 +1,6 @@ package tests.integration; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; @@ -95,7 +95,6 @@ public void firstRequestChangeNumber() throws Exception { String apiKey = apiKeyAndDb.first; SplitRoomDatabase splitRoomDatabase = DatabaseHelper.getTestDatabase(mContext); splitRoomDatabase.clearAllTables(); - splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, INITIAL_CHANGE_NUMBER)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis())); diff --git a/src/androidTest/java/tests/integration/IntegrationTest.java b/src/androidTest/java/tests/integration/IntegrationTest.java index 66f023e13..07f8d8c37 100644 --- a/src/androidTest/java/tests/integration/IntegrationTest.java +++ b/src/androidTest/java/tests/integration/IntegrationTest.java @@ -115,7 +115,6 @@ public void testAll() throws Exception { SplitRoomDatabase splitRoomDatabase = DatabaseHelper.getTestDatabase(mContext); splitRoomDatabase.clearAllTables(); - splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 2)); SplitClient client; SplitManager manager; @@ -249,7 +248,6 @@ public void testNoReadyFromCache() throws Exception { SplitRoomDatabase splitRoomDatabase = DatabaseHelper.getTestDatabase(mContext); splitRoomDatabase.clearAllTables(); - splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, -1)); SplitClient client; SplitManager manager; @@ -288,7 +286,7 @@ public void testNoReadyFromCache() throws Exception { latch.await(40, TimeUnit.SECONDS); Assert.assertTrue(client.isReady()); - Assert.assertFalse(readyFromCacheTask.isOnPostExecutionCalled); + Assert.assertTrue(readyFromCacheTask.isOnPostExecutionCalled); Assert.assertTrue(readyTask.isOnPostExecutionCalled); Assert.assertFalse(readyTimeOutTask.isOnPostExecutionCalled); @@ -304,7 +302,6 @@ public void testGetTreatmentFromCache() throws Exception { mContext = InstrumentationRegistry.getInstrumentation().getContext(); mRoomDb = DatabaseHelper.getTestDatabase(mContext); mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 10)); - mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, 1)); SplitChange change = Json.fromJson(mJsonChanges.get(0), SplitChange.class); List entities = new ArrayList<>(); diff --git a/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java b/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java index 25221044d..ab1dc376b 100644 --- a/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java +++ b/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java @@ -142,7 +142,6 @@ public void test() throws Exception { SplitRoomDatabase splitRoomDatabase = DatabaseHelper.getTestDatabase(mContext); splitRoomDatabase.clearAllTables(); - splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, -1)); ImpressionListenerHelper impListener = new ImpressionListenerHelper(); @@ -196,7 +195,7 @@ public void test() throws Exception { Assert.fail("Impressions request timeout"); } - Assert.assertFalse(readyFromCacheTask.isOnPostExecutionCalled); + Assert.assertTrue(readyFromCacheTask.isOnPostExecutionCalled); Assert.assertEquals("on_s1", treatments.get(0)); Assert.assertEquals("on_s1", treatments.get(1)); Assert.assertEquals("on_s1", treatments.get(2)); diff --git a/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java b/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java index 22e102082..a1a165599 100644 --- a/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java +++ b/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java @@ -106,7 +106,6 @@ public void testAll() throws Exception { SplitRoomDatabase.class) .build(); splitRoomDatabase.clearAllTables(); - splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 2)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING, expectedQs)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis())); diff --git a/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java b/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java index 1183f7d7a..548daad4d 100644 --- a/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java +++ b/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java @@ -2,7 +2,7 @@ import static java.lang.Thread.sleep; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/TrackTest.java b/src/androidTest/java/tests/integration/TrackTest.java index 2db076a2b..550aac3dd 100644 --- a/src/androidTest/java/tests/integration/TrackTest.java +++ b/src/androidTest/java/tests/integration/TrackTest.java @@ -35,7 +35,6 @@ import io.split.android.client.dtos.Event; import io.split.android.client.events.SplitEvent; import io.split.android.client.exceptions.SplitInstantiationException; -import io.split.android.client.storage.db.GeneralInfoEntity; import io.split.android.client.storage.db.SplitRoomDatabase; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; @@ -120,7 +119,6 @@ public void test() throws Exception { String apiKey = IntegrationHelper.dummyApiKey(); SplitRoomDatabase splitRoomDatabase = DatabaseHelper.getTestDatabase(mContext); splitRoomDatabase.clearAllTables(); - splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); ImpressionListenerHelper impListener = new ImpressionListenerHelper(); @@ -220,7 +218,6 @@ public void largeNumberInPropertiesTest() throws InterruptedException, SplitInst String apiKey = IntegrationHelper.dummyApiKey(); SplitRoomDatabase splitRoomDatabase = DatabaseHelper.getTestDatabase(mContext); splitRoomDatabase.clearAllTables(); - splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); final String url = mWebServer.url("/").url().toString(); diff --git a/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java b/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java index 62b4f987a..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")); @@ -108,7 +115,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 +160,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 +169,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)); } @@ -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) @@ -259,7 +270,6 @@ private void insertSplitsFromFileIntoDB() { return result; }).collect(Collectors.toList()); - mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 1)); mRoomDb.splitDao().insert(entities); @@ -273,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/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/matcher/SemverMatcherTest.java b/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java index d0e63c4e8..14fa837ae 100644 --- a/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java +++ b/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java @@ -2,7 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java b/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java index 928d1d93a..e11af1561 100644 --- a/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java +++ b/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java @@ -4,7 +4,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java b/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java new file mode 100644 index 000000000..2efb3d933 --- /dev/null +++ b/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java @@ -0,0 +1,305 @@ +package tests.integration.rollout; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static helper.IntegrationHelper.buildFactory; +import static helper.IntegrationHelper.dummyApiKey; +import static helper.IntegrationHelper.dummyUserKey; +import static helper.IntegrationHelper.getTimestampDaysAgo; +import static helper.IntegrationHelper.randomizedAllSegments; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import helper.DatabaseHelper; +import helper.FileHelper; +import helper.IntegrationHelper; +import io.split.android.client.RolloutCacheConfiguration; +import io.split.android.client.ServiceEndpoints; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.dtos.SegmentsChange; +import io.split.android.client.dtos.Split; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.storage.db.GeneralInfoEntity; +import io.split.android.client.storage.db.MyLargeSegmentEntity; +import io.split.android.client.storage.db.MySegmentEntity; +import io.split.android.client.storage.db.SplitEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.SplitLogLevel; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import tests.integration.shared.TestingHelper; + +public class RolloutCacheManagerIntegrationTest { + + private final AtomicReference mSinceFromUri = new AtomicReference<>(null); + private MockWebServer mWebServer; + private SplitRoomDatabase mRoomDb; + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private CountDownLatch mRequestCountdownLatch; + + @Before + public void setUp() { + mSinceFromUri.set(null); + setupServer(); + mRoomDb = DatabaseHelper.getTestDatabase(mContext); + mRoomDb.clearAllTables(); + mRequestCountdownLatch = new CountDownLatch(1); + } + + @Test + public void expirationPeriodIsUsed() throws InterruptedException { + test(getTimestampDaysAgo(1), RolloutCacheConfiguration.builder().expirationDays(1)); + } + + @Test + public void clearOnInitClearsCacheOnStartup() throws InterruptedException { + test(System.currentTimeMillis(), RolloutCacheConfiguration.builder().clearOnInit(true)); + } + + @Test + public void repeatedInitWithClearOnInitSetToTrueDoesNotClearIfMinDaysHasNotElapsed() throws InterruptedException { + // Preload DB with update timestamp of now + long oldTimestamp = System.currentTimeMillis(); + preloadDb(oldTimestamp, 0L, 8000L); + + // Track initial values + List initialFlags = mRoomDb.splitDao().getAll(); + List initialSegments = mRoomDb.mySegmentDao().getAll(); + List initialLargeSegments = mRoomDb.myLargeSegmentDao().getAll(); + long initialChangeNumber = mRoomDb.generalInfoDao().getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO).getLongValue(); + + CountDownLatch readyLatch = new CountDownLatch(1); + SplitFactory factory = getSplitFactory(RolloutCacheConfiguration.builder().clearOnInit(true).build()); + Thread.sleep(1000); + + // Track intermediate values + List intermediateFlags = mRoomDb.splitDao().getAll(); + List intermediateSegments = mRoomDb.mySegmentDao().getAll(); + List intermediateLargeSegments = mRoomDb.myLargeSegmentDao().getAll(); + long intermediateChangeNumber = mRoomDb.generalInfoDao().getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO).getLongValue(); + + // Resume server responses after tracking DB values + mRequestCountdownLatch.countDown(); + + // Wait for ready + factory.client().on(SplitEvent.SDK_READY, TestingHelper.testTask(readyLatch)); + boolean readyAwait = readyLatch.await(10, TimeUnit.SECONDS); + + // Destroy factory + factory.destroy(); + mRequestCountdownLatch = new CountDownLatch(1); + + preloadDb(null, null, null); + SplitFactory factory2 = getSplitFactory(RolloutCacheConfiguration.builder().clearOnInit(true).build()); + Thread.sleep(1000); + + // Track intermediate values + List factory2Flags = mRoomDb.splitDao().getAll(); + List factory2Segments = mRoomDb.mySegmentDao().getAll(); + List factory2LargeSegments = mRoomDb.myLargeSegmentDao().getAll(); + long factory2ChangeNumber = mRoomDb.generalInfoDao().getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO).getLongValue(); + + // initial values + assertTrue(readyAwait); + assertEquals(2, initialFlags.size()); + assertEquals(1, initialSegments.size()); + assertFalse(Json.fromJson(initialSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); + assertFalse(Json.fromJson(initialLargeSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); + assertEquals(8000L, initialChangeNumber); + + // values after clear + assertEquals(1, intermediateSegments.size()); + assertTrue(Json.fromJson(intermediateSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); + assertEquals(1, intermediateLargeSegments.size()); + assertEquals(0, intermediateFlags.size()); + assertTrue(Json.fromJson(intermediateLargeSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); + assertEquals(-1, intermediateChangeNumber); + + // values after second init (values were reinserted into DB); no clear + assertEquals(2, factory2Flags.size()); + assertEquals(1, factory2Segments.size()); + assertFalse(Json.fromJson(factory2Segments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); + assertFalse(Json.fromJson(factory2LargeSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); + assertEquals(10000L, factory2ChangeNumber); + assertTrue(0L < mRoomDb.generalInfoDao() + .getByName("rolloutCacheLastClearTimestamp").getLongValue()); + } + + @Test + public void sdkReadyFromCacheIsEmittedOnFreshInit() throws InterruptedException { + SplitFactory splitFactory = getSplitFactory(RolloutCacheConfiguration.builder().build()); + + CountDownLatch latch1 = new CountDownLatch(1); + CountDownLatch latch2 = new CountDownLatch(1); + splitFactory.client().on(SplitEvent.SDK_READY_FROM_CACHE, TestingHelper.testTask(latch1)); + splitFactory.client("two").on(SplitEvent.SDK_READY_FROM_CACHE, TestingHelper.testTask(latch2)); + mRequestCountdownLatch.countDown(); + + boolean await1 = latch1.await(10, TimeUnit.SECONDS); + boolean await2 = latch2.await(10, TimeUnit.SECONDS); + + assertTrue(await1); + assertTrue(await2); + } + + private void test(long timestampDaysAgo, RolloutCacheConfiguration.Builder configBuilder) throws InterruptedException { + // Preload DB with update timestamp of 1 day ago + long oldTimestamp = timestampDaysAgo; + preloadDb(oldTimestamp, 0L, 8000L); + + // Track initial values + List initialFlags = mRoomDb.splitDao().getAll(); + List initialSegments = mRoomDb.mySegmentDao().getAll(); + List initialLargeSegments = mRoomDb.myLargeSegmentDao().getAll(); + long initialChangeNumber = mRoomDb.generalInfoDao().getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO).getLongValue(); + + // Initialize SDK + CountDownLatch readyLatch = new CountDownLatch(1); + SplitFactory factory = getSplitFactory(configBuilder.build()); + Thread.sleep(1000); + + // Track final values + verify(factory, readyLatch, initialFlags, initialSegments, initialLargeSegments, initialChangeNumber); + } + + private void verify(SplitFactory factory, CountDownLatch readyLatch, List initialFlags, List initialSegments, List initialLargeSegments, long initialChangeNumber) throws InterruptedException { + // Track final values + List finalFlags = mRoomDb.splitDao().getAll(); + List finalSegments = mRoomDb.mySegmentDao().getAll(); + List finalLargeSegments = mRoomDb.myLargeSegmentDao().getAll(); + long finalChangeNumber = mRoomDb.generalInfoDao().getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO).getLongValue(); + + // Resume server responses after tracking DB values + mRequestCountdownLatch.countDown(); + + // Wait for ready + factory.client().on(SplitEvent.SDK_READY, TestingHelper.testTask(readyLatch)); + boolean readyAwait = readyLatch.await(10, TimeUnit.SECONDS); + + // Verify + assertTrue(readyAwait); + assertEquals(2, initialFlags.size()); + assertEquals(1, initialSegments.size()); + assertFalse(Json.fromJson(initialSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); + assertFalse(Json.fromJson(initialLargeSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); + assertEquals(8000L, initialChangeNumber); + assertEquals(1, finalSegments.size()); + assertTrue(Json.fromJson(finalSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); + assertEquals(0, finalFlags.size()); + assertEquals(1, finalLargeSegments.size()); + assertTrue(Json.fromJson(finalLargeSegments.get(0).getSegmentList(), SegmentsChange.class).getSegments().isEmpty()); + assertEquals(-1, finalChangeNumber); + assertTrue(0L < mRoomDb.generalInfoDao() + .getByName("rolloutCacheLastClearTimestamp").getLongValue()); + assertEquals("-1", mSinceFromUri.get()); + } + + private SplitFactory getSplitFactory(RolloutCacheConfiguration rolloutCacheConfiguration) { + final String url = mWebServer.url("/").url().toString(); + ServiceEndpoints endpoints = ServiceEndpoints.builder() + .apiEndpoint(url).eventsEndpoint(url).build(); + SplitClientConfig.Builder builder = new SplitClientConfig.Builder() + .serviceEndpoints(endpoints) + .streamingEnabled(false) + .featuresRefreshRate(9999) + .segmentsRefreshRate(9999) + .impressionsRefreshRate(9999) + .logLevel(SplitLogLevel.VERBOSE) + .streamingEnabled(false); + + if (rolloutCacheConfiguration != null) { + builder.rolloutCacheConfiguration(rolloutCacheConfiguration); + } + + SplitClientConfig config = builder + .build(); + + return buildFactory( + dummyApiKey(), dummyUserKey(), + config, mContext, null, mRoomDb); + } + + private void setupServer() { + mWebServer = new MockWebServer(); + + final Dispatcher dispatcher = new Dispatcher() { + + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + mRequestCountdownLatch.await(); + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(randomizedAllSegments()); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.SPLIT_CHANGES)) { + mSinceFromUri.compareAndSet(null, IntegrationHelper.getSinceFromUri(request.getRequestUrl().uri())); + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptySplitChanges(-1, 10000L)); + } else { + return new MockResponse().setResponseCode(404); + } + } + }; + mWebServer.setDispatcher(dispatcher); + } + + private void preloadDb(Long updateTimestamp, Long lastClearTimestamp, Long changeNumber) { + List splitListFromJson = getSplitListFromJson(); + List entities = splitListFromJson.stream() + .filter(split -> split.name != null) + .map(split -> { + SplitEntity result = new SplitEntity(); + result.setName(split.name); + result.setBody(Json.toJson(split)); + + return result; + }).collect(Collectors.toList()); + + if (updateTimestamp != null) { + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, updateTimestamp)); + } + if (lastClearTimestamp != null) { + mRoomDb.generalInfoDao().update(new GeneralInfoEntity("rolloutCacheLastClearTimestamp", lastClearTimestamp)); + } + if (changeNumber != null) { + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, changeNumber)); + } + + MyLargeSegmentEntity largeSegment = new MyLargeSegmentEntity(); + largeSegment.setSegmentList("{\"k\":[{\"n\":\"ls1\"},{\"n\":\"ls2\"}],\"cn\":null}"); + largeSegment.setUserKey(dummyUserKey().matchingKey()); + largeSegment.setUpdatedAt(System.currentTimeMillis()); + mRoomDb.myLargeSegmentDao().update(largeSegment); + + MySegmentEntity segment = new MySegmentEntity(); + segment.setSegmentList("{\"k\":[{\"n\":\"s1\"},{\"n\":\"s2\"}],\"cn\":null}"); + segment.setUserKey(dummyUserKey().matchingKey()); + segment.setUpdatedAt(System.currentTimeMillis()); + mRoomDb.mySegmentDao().update(segment); + mRoomDb.splitDao().insert(entities); + } + + private List getSplitListFromJson() { + FileHelper fileHelper = new FileHelper(); + String s = fileHelper.loadFileContent(mContext, "attributes_test_split_change.json"); + + SplitChange changes = Json.fromJson(s, SplitChange.class); + + return changes.splits; + } +} diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java index 86f42034f..5507e432b 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java @@ -2,7 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java index 059d7c97d..da3d18250 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java @@ -2,7 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; import android.database.Cursor; diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java index 6faaab513..06146259a 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java @@ -3,7 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java b/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java index 11d8af38e..a12642bee 100644 --- a/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java +++ b/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java @@ -61,7 +61,6 @@ public void setUp() { } insertSplitsIntoDb(); mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 10)); - mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, 1)); ServerMock mWebServer = new ServerMock(mJsonChanges); @@ -160,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/streaming/SplitsKillProcessTest.java b/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java index bf7fea927..45b0369d5 100644 --- a/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java +++ b/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java @@ -1,7 +1,7 @@ package tests.integration.streaming; import static java.lang.Thread.sleep; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java b/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java index 536a9c7d4..3675b4612 100644 --- a/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java +++ b/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java @@ -1,7 +1,7 @@ package tests.integration.streaming; import static java.lang.Thread.sleep; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; +import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; diff --git a/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java b/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java index 50d747591..8f9e2490a 100644 --- a/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java +++ b/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java @@ -365,7 +365,6 @@ private void insertSplitsFromFileIntoDB() { return result; }).collect(Collectors.toList()); - testDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); testDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 1)); testDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis())); 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/androidTest/java/tests/storage/GeneralInfoStorageTest.java b/src/androidTest/java/tests/storage/GeneralInfoStorageTest.java new file mode 100644 index 000000000..14a713f0d --- /dev/null +++ b/src/androidTest/java/tests/storage/GeneralInfoStorageTest.java @@ -0,0 +1,92 @@ +package tests.storage; + +import static org.junit.Assert.assertEquals; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import helper.DatabaseHelper; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.storage.general.GeneralInfoStorageImpl; + +public class GeneralInfoStorageTest { + + private SplitRoomDatabase mDb; + private GeneralInfoStorage mGeneralInfoStorage; + + @Before + public void setUp() { + mDb = DatabaseHelper.getTestDatabase(InstrumentationRegistry.getInstrumentation().getContext()); + mGeneralInfoStorage = new GeneralInfoStorageImpl(mDb.generalInfoDao()); + } + + @After + public void tearDown() { + mDb.clearAllTables(); + mDb.close(); + } + + @Test + public void setSplitsUpdateTimestamp() { + long initialValue = mGeneralInfoStorage.getSplitsUpdateTimestamp(); + mGeneralInfoStorage.setSplitsUpdateTimestamp(100L); + long finalValue = mGeneralInfoStorage.getSplitsUpdateTimestamp(); + + assertEquals(0L, initialValue); + assertEquals(100L, finalValue); + } + + @Test + public void setChangeNumber() { + long initialValue = mGeneralInfoStorage.getChangeNumber(); + mGeneralInfoStorage.setChangeNumber(100L); + long finalValue = mGeneralInfoStorage.getChangeNumber(); + + assertEquals(-1L, initialValue); + assertEquals(100L, finalValue); + } + + @Test + public void setSplitsFilterQueryString() { + String initialValue = mGeneralInfoStorage.getSplitsFilterQueryString(); + mGeneralInfoStorage.setSplitsFilterQueryString("queryString"); + String finalValue = mGeneralInfoStorage.getSplitsFilterQueryString(); + + assertEquals("", initialValue); + assertEquals("queryString", finalValue); + } + + @Test + public void setDatabaseEncryptionMode() { + String initialValue = mGeneralInfoStorage.getDatabaseEncryptionMode(); + mGeneralInfoStorage.setDatabaseEncryptionMode("MODE"); + String finalValue = mGeneralInfoStorage.getDatabaseEncryptionMode(); + + assertEquals("", initialValue); + assertEquals("MODE", finalValue); + } + + @Test + public void setFlagsSpec() { + String initialValue = mGeneralInfoStorage.getFlagsSpec(); + mGeneralInfoStorage.setFlagsSpec("4.4"); + String finalValue = mGeneralInfoStorage.getFlagsSpec(); + + assertEquals("", initialValue); + assertEquals("4.4", finalValue); + } + + @Test + public void setRolloutCacheLastClearTimestamp() { + long initialValue = mGeneralInfoStorage.getRolloutCacheLastClearTimestamp(); + mGeneralInfoStorage.setRolloutCacheLastClearTimestamp(100L); + long finalValue = mGeneralInfoStorage.getRolloutCacheLastClearTimestamp(); + + assertEquals(0L, initialValue); + assertEquals(100L, finalValue); + } +} diff --git a/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java b/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java index bc9867946..bf593bbf9 100644 --- a/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java +++ b/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java @@ -124,7 +124,6 @@ public void scheduleWorkSchedulesSplitsJob() { mWrapper.scheduleWork(); Data inputData = new Data.Builder() - .putLong("splitCacheExpiration", 864000) .putString("endpoint", "https://test.split.io/api") .putBoolean("shouldRecordTelemetry", true) .putStringArray("configuredFilterValues", new String[]{"set_1", "set_2"}) @@ -252,7 +251,6 @@ public void schedulingWithoutCertificatePinning() { mWrapper.scheduleWork(); Data inputData = new Data.Builder() - .putLong("splitCacheExpiration", 864000) .putString("endpoint", "https://test.split.io/api") .putBoolean("shouldRecordTelemetry", true) .putStringArray("configuredFilterValues", new String[]{"set_1", "set_2"}) 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..413034e26 --- /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 mExpirationDays; + private final boolean mClearOnInit; + + private RolloutCacheConfiguration(int expiration, boolean clearOnInit) { + mExpirationDays = expiration; + mClearOnInit = clearOnInit; + } + + public int getExpirationDays() { + return mExpirationDays; + } + + public boolean isClearOnInit() { + 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 expirationDays in days + * @return This builder + */ + public Builder expirationDays(int expirationDays) { + if (expirationDays < MIN_EXPIRATION_DAYS) { + Logger.w("Cache expiration must be at least 1 day. Using default value."); + mExpiration = ServiceConstants.DEFAULT_ROLLOUT_CACHE_EXPIRATION; + } else { + mExpiration = expirationDays; + } + + 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 15872b80b..00af53115 100644 --- a/src/main/java/io/split/android/client/SplitClientConfig.java +++ b/src/main/java/io/split/android/client/SplitClientConfig.java @@ -65,7 +65,6 @@ public class SplitClientConfig { // Data folder private static final String DEFAULT_DATA_FOLDER = "split_data"; - private static final long SPLITS_CACHE_EXPIRATION_IN_SECONDS = ServiceConstants.DEFAULT_SPLITS_CACHE_EXPIRATION_IN_SECONDS; private static final long OBSERVER_CACHE_EXPIRATION_PERIOD = ServiceConstants.DEFAULT_OBSERVER_CACHE_EXPIRATION_PERIOD_MS; private final String mEndpoint; @@ -131,6 +130,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 +186,8 @@ private SplitClientConfig(String endpoint, String prefix, long observerCacheExpirationPeriod, CertificatePinningConfiguration certificatePinningConfiguration, - long impressionsDedupeTimeInterval) { + long impressionsDedupeTimeInterval, + RolloutCacheConfiguration rolloutCacheConfiguration) { mEndpoint = endpoint; mEventsEndpoint = eventsEndpoint; mTelemetryEndpoint = telemetryEndpoint; @@ -243,14 +245,16 @@ private SplitClientConfig(String endpoint, mObserverCacheExpirationPeriod = observerCacheExpirationPeriod; mCertificatePinningConfiguration = certificatePinningConfiguration; mImpressionsDedupeTimeInterval = impressionsDedupeTimeInterval; + mRolloutCacheConfiguration = rolloutCacheConfiguration; } public String trafficType() { return mTrafficType; } + @Deprecated public long cacheExpirationInSeconds() { - return SPLITS_CACHE_EXPIRATION_IN_SECONDS; + return TimeUnit.DAYS.toSeconds(rolloutCacheConfiguration().getExpirationDays()); } public long eventFlushInterval() { @@ -486,6 +490,10 @@ public long impressionsDedupeTimeInterval() { return mImpressionsDedupeTimeInterval; } + public RolloutCacheConfiguration rolloutCacheConfiguration() { + return mRolloutCacheConfiguration; + } + public static final class Builder { static final int PROXY_PORT_DEFAULT = 80; @@ -562,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(); } @@ -1102,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); @@ -1233,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/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index 16f26d762..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; @@ -176,7 +183,8 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, StorageFactory.getAttributesStorage(), StorageFactory.getPersistentAttributesStorage(splitRoomDatabase, splitCipher), getTelemetryStorage(shouldRecordTelemetry, telemetryStorage), - StorageFactory.getImpressionsObserverCachePersistentStorage(splitRoomDatabase, observerCacheExpirationPeriod, impressionsObserverExecutor)); + StorageFactory.getImpressionsObserverCachePersistentStorage(splitRoomDatabase, observerCacheExpirationPeriod, impressionsObserverExecutor), + StorageFactory.getGeneralInfoStorage(splitRoomDatabase)); } SplitApiFacade buildApiFacade(SplitClientConfig splitClientConfig, @@ -398,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 @@ -485,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/events/SplitEventsManager.java b/src/main/java/io/split/android/client/events/SplitEventsManager.java index f6150157e..c443aa8e2 100644 --- a/src/main/java/io/split/android/client/events/SplitEventsManager.java +++ b/src/main/java/io/split/android/client/events/SplitEventsManager.java @@ -191,6 +191,9 @@ private void triggerSdkReadyIfNeeded() { if ((wasTriggered(SplitInternalEvent.MY_SEGMENTS_UPDATED) || wasTriggered(SplitInternalEvent.MY_SEGMENTS_FETCHED) || wasTriggered(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED)) && (wasTriggered(SplitInternalEvent.SPLITS_UPDATED) || wasTriggered(SplitInternalEvent.SPLITS_FETCHED)) && !isTriggered(SplitEvent.SDK_READY)) { + if (!isTriggered(SplitEvent.SDK_READY_FROM_CACHE)) { + trigger(SplitEvent.SDK_READY_FROM_CACHE); + } trigger(SplitEvent.SDK_READY); } } diff --git a/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java b/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java index 32f37c6f0..64304916b 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java @@ -17,4 +17,9 @@ public MySegmentsStorage getStorageForKey(String matchingKey) { public long getUniqueAmount() { return mEmptyMySegmentsStorage.getAll().size(); } + + @Override + public void clear() { + // No-op + } } 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..ecfa4ad05 100644 --- a/src/main/java/io/split/android/client/service/ServiceConstants.java +++ b/src/main/java/io/split/android/client/service/ServiceConstants.java @@ -10,7 +10,8 @@ public class ServiceConstants { public static final long DEFAULT_INITIAL_DELAY = 15L; 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 long DEFAULT_SPLITS_CACHE_EXPIRATION_IN_SECONDS = TimeUnit.DAYS.toSeconds(DEFAULT_ROLLOUT_CACHE_EXPIRATION); // 10 days public static final int MAX_ROWS_PER_QUERY = 100; @@ -27,7 +28,6 @@ public class ServiceConstants { public final static String WORKER_PARAM_ENDPOINT = "endpoint"; public final static String WORKER_PARAM_IMPRESSIONS_PER_PUSH = "impressionsPerPush"; public final static String WORKER_PARAM_EVENTS_PER_PUSH = "eventsPerPush"; - public final static String WORKER_PARAM_SPLIT_CACHE_EXPIRATION = "splitCacheExpiration"; public static final String WORKER_PARAM_UNIQUE_KEYS_PER_PUSH = "unique_keys_per_push"; public static final String WORKER_PARAM_UNIQUE_KEYS_ESTIMATED_SIZE_IN_BYTES = "unique_keys_estimated_size_in_bytes"; public static final String WORKER_PARAM_ENCRYPTION_ENABLED = "encryptionEnabled"; 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..934f9ac75 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; @@ -125,8 +128,8 @@ public ImpressionsRecorderTask createImpressionsRecorderTask() { @Override public SplitsSyncTask createSplitsSyncTask(boolean checkCacheExpiration) { - return SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorageContainer.getSplitsStorage(), checkCacheExpiration, - mSplitClientConfig.cacheExpirationInSeconds(), mSplitsFilterQueryStringFromConfig, mEventsManager, mSplitsStorageContainer.getTelemetryStorage()); + return SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorageContainer.getSplitsStorage(), + mSplitsFilterQueryStringFromConfig, mEventsManager, mSplitsStorageContainer.getTelemetryStorage()); } @Override @@ -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/splits/SplitsSyncHelper.java b/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java index f65adb969..17e136fb7 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java +++ b/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java @@ -185,17 +185,6 @@ private void updateStorage(boolean clearBeforeUpdate, SplitChange splitChange) { mSplitsStorage.update(mSplitChangeProcessor.process(splitChange)); } - public boolean cacheHasExpired(long storedChangeNumber, long updateTimestamp, long cacheExpirationInSeconds) { - long elapsed = now() - TimeUnit.MILLISECONDS.toSeconds(updateTimestamp); - return storedChangeNumber > -1 - && updateTimestamp > 0 - && (elapsed > cacheExpirationInSeconds); - } - - private long now() { - return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); - } - private void logError(String message) { Logger.e("Error while executing splits sync/update task: " + message); } diff --git a/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java b/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java index 1fe6d6cec..8a7a7bc6f 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java @@ -21,8 +21,6 @@ public class SplitsSyncTask implements SplitTask { private final String mSplitsFilterQueryStringFromConfig; private final SplitsStorage mSplitsStorage; - private final boolean mCheckCacheExpiration; - private final long mCacheExpirationInSeconds; private final SplitsSyncHelper mSplitsSyncHelper; @Nullable private final ISplitEventsManager mEventsManager; // Should only be null on background sync @@ -32,27 +30,21 @@ public class SplitsSyncTask implements SplitTask { public static SplitsSyncTask build(@NonNull SplitsSyncHelper splitsSyncHelper, @NonNull SplitsStorage splitsStorage, - boolean checkCacheExpiration, - long cacheExpirationInSeconds, String splitsFilterQueryString, @NonNull ISplitEventsManager eventsManager, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { - return new SplitsSyncTask(splitsSyncHelper, splitsStorage, checkCacheExpiration, cacheExpirationInSeconds, splitsFilterQueryString, telemetryRuntimeProducer, eventsManager, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + return new SplitsSyncTask(splitsSyncHelper, splitsStorage, splitsFilterQueryString, telemetryRuntimeProducer, eventsManager, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); } public static SplitTask buildForBackground(@NonNull SplitsSyncHelper splitsSyncHelper, @NonNull SplitsStorage splitsStorage, - boolean checkCacheExpiration, - long cacheExpirationInSeconds, - String splitsFilterQueryString, + String splitsFilterQueryString, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { - return new SplitsSyncTask(splitsSyncHelper, splitsStorage, checkCacheExpiration, cacheExpirationInSeconds, splitsFilterQueryString, telemetryRuntimeProducer, null, 1); + return new SplitsSyncTask(splitsSyncHelper, splitsStorage, splitsFilterQueryString, telemetryRuntimeProducer, null, 1); } private SplitsSyncTask(@NonNull SplitsSyncHelper splitsSyncHelper, @NonNull SplitsStorage splitsStorage, - boolean checkCacheExpiration, - long cacheExpirationInSeconds, String splitsFilterQueryString, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, @Nullable ISplitEventsManager eventsManager, @@ -60,8 +52,6 @@ private SplitsSyncTask(@NonNull SplitsSyncHelper splitsSyncHelper, mSplitsStorage = checkNotNull(splitsStorage); mSplitsSyncHelper = checkNotNull(splitsSyncHelper); - mCacheExpirationInSeconds = cacheExpirationInSeconds; - mCheckCacheExpiration = checkCacheExpiration; mSplitsFilterQueryStringFromConfig = splitsFilterQueryString; mEventsManager = eventsManager; mChangeChecker = new SplitsChangeChecker(); @@ -73,10 +63,6 @@ private SplitsSyncTask(@NonNull SplitsSyncHelper splitsSyncHelper, @NonNull public SplitTaskExecutionInfo execute() { long storedChangeNumber = mSplitsStorage.getTill(); - long updateTimestamp = mSplitsStorage.getUpdateTimestamp(); - - boolean shouldClearExpiredCache = mCheckCacheExpiration && - mSplitsSyncHelper.cacheHasExpired(storedChangeNumber, updateTimestamp, mCacheExpirationInSeconds); boolean splitsFilterHasChanged = splitsFilterHasChanged(mSplitsStorage.getSplitsFilterQueryString()); @@ -87,8 +73,8 @@ public SplitTaskExecutionInfo execute() { long startTime = System.currentTimeMillis(); SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(storedChangeNumber, - splitsFilterHasChanged || shouldClearExpiredCache, - splitsFilterHasChanged || shouldClearExpiredCache, mOnDemandFetchBackoffMaxRetries); + splitsFilterHasChanged, + splitsFilterHasChanged, mOnDemandFetchBackoffMaxRetries); mTelemetryRuntimeProducer.recordSyncLatency(OperationType.SPLITS, System.currentTimeMillis() - startTime); if (result.getStatus() == SplitTaskExecutionStatus.SUCCESS) { 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 new file mode 100644 index 000000000..72cf4477e --- /dev/null +++ b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManager.java @@ -0,0 +1,8 @@ +package io.split.android.client.service.synchronizer; + +import io.split.android.client.service.executor.SplitTaskExecutionListener; + +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 new file mode 100644 index 000000000..2c9b9db04 --- /dev/null +++ b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java @@ -0,0 +1,131 @@ +package io.split.android.client.service.synchronizer; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; + +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; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionListener; +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; + +public class RolloutCacheManagerImpl implements RolloutCacheManager, SplitTask { + + public static final int MIN_CACHE_CLEAR_DAYS = 1; // TODO + + @NonNull + private final GeneralInfoStorage mGeneralInfoStorage; + @NonNull + private final RolloutCacheConfiguration mConfig; + @NonNull + private final RolloutDefinitionsCache[] mStorages; + @NonNull + private final CleanUpDatabaseTask mCleanUpDatabaseTask; + @NonNull + private final EncryptionMigrationTask mEncryptionMigrationTask; + + public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, + @NonNull SplitStorageContainer storageContainer, + @NonNull CleanUpDatabaseTask cleanUpDatabaseTask, + @NonNull EncryptionMigrationTask encryptionMigrationTask) { + this(storageContainer.getGeneralInfoStorage(), + splitClientConfig.rolloutCacheConfiguration(), + cleanUpDatabaseTask, + encryptionMigrationTask, + storageContainer.getSplitsStorage(), + storageContainer.getMySegmentsStorageContainer(), + storageContainer.getMyLargeSegmentsStorageContainer()); + } + + @VisibleForTesting + RolloutCacheManagerImpl(@NonNull GeneralInfoStorage generalInfoStorage, + @NonNull RolloutCacheConfiguration config, + @NonNull CleanUpDatabaseTask cleanUpDatabaseTask, + @NonNull EncryptionMigrationTask encryptionMigrationTask, + @NonNull RolloutDefinitionsCache... storages) { + mGeneralInfoStorage = checkNotNull(generalInfoStorage); + mCleanUpDatabaseTask = checkNotNull(cleanUpDatabaseTask); + mEncryptionMigrationTask = checkNotNull(encryptionMigrationTask); + mStorages = checkNotNull(storages); + mConfig = checkNotNull(config); + } + + @WorkerThread + @Override + public void validateCache(SplitTaskExecutionListener 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 + @Override + public SplitTaskExecutionInfo execute() { + try { + boolean expired = validateExpiration(); + if (expired) { + clear(); + } + } catch (Exception e) { + Logger.e("Error occurred validating cache: " + e.getMessage()); + + return SplitTaskExecutionInfo.error(SplitTaskType.GENERIC_TASK); + } + return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK); + } + + private boolean validateExpiration() { + long lastUpdateTimestamp = mGeneralInfoStorage.getSplitsUpdateTimestamp(); + // calculate elapsed time since last update + long daysSinceLastUpdate = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - lastUpdateTimestamp); + + if (lastUpdateTimestamp > 0 && daysSinceLastUpdate >= mConfig.getExpirationDays()) { + Logger.v("Clearing rollout definitions cache due to expiration"); + return true; + } else if (mConfig.isClearOnInit()) { + long lastCacheClearTimestamp = mGeneralInfoStorage.getRolloutCacheLastClearTimestamp(); + if (lastCacheClearTimestamp < 1) { // 0 is default value for rollout cache timestamp + return true; + } + long daysSinceCacheClear = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - lastCacheClearTimestamp); + + // don't clear too soon + if (daysSinceCacheClear >= MIN_CACHE_CLEAR_DAYS) { + Logger.v("Forcing rollout definitions cache clear"); + return true; + } + } + + return false; + } + + private void clear() { + for (RolloutDefinitionsCache storage : mStorages) { + storage.clear(); + } + mGeneralInfoStorage.setRolloutCacheLastClearTimestamp(System.currentTimeMillis()); + Logger.v("Rollout definitions cache cleared"); + } +} 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/WorkManagerWrapper.java b/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java index c342f1cbc..c65042116 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java +++ b/src/main/java/io/split/android/client/service/synchronizer/WorkManagerWrapper.java @@ -199,7 +199,6 @@ private SplitTaskType taskTypeFromTags(Set tags) { private Data buildSplitSyncInputData() { Data.Builder dataBuilder = new Data.Builder(); - dataBuilder.putLong(ServiceConstants.WORKER_PARAM_SPLIT_CACHE_EXPIRATION, mSplitClientConfig.cacheExpirationInSeconds()); dataBuilder.putString(ServiceConstants.WORKER_PARAM_ENDPOINT, mSplitClientConfig.endpoint()); dataBuilder.putBoolean(ServiceConstants.SHOULD_RECORD_TELEMETRY, mSplitClientConfig.shouldRecordTelemetry()); dataBuilder.putString(ServiceConstants.WORKER_PARAM_CONFIGURED_FILTER_TYPE, (mFilter != null) ? mFilter.getType().queryStringField() : null); 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/service/workmanager/SplitWorker.java b/src/main/java/io/split/android/client/service/workmanager/SplitWorker.java index 1b51bdfa9..b24b231ce 100644 --- a/src/main/java/io/split/android/client/service/workmanager/SplitWorker.java +++ b/src/main/java/io/split/android/client/service/workmanager/SplitWorker.java @@ -23,7 +23,6 @@ public abstract class SplitWorker extends Worker { private final SplitRoomDatabase mDatabase; private final HttpClient mHttpClient; private final String mEndpoint; - private final long mCacheExpirationInSeconds; protected SplitTask mSplitTask; @@ -36,8 +35,6 @@ public SplitWorker(@NonNull Context context, String apiKey = inputData.getString(ServiceConstants.WORKER_PARAM_API_KEY); mEndpoint = inputData.getString(ServiceConstants.WORKER_PARAM_ENDPOINT); mDatabase = SplitRoomDatabase.getDatabase(context, databaseName); - mCacheExpirationInSeconds = inputData.getLong(ServiceConstants.WORKER_PARAM_SPLIT_CACHE_EXPIRATION, - ServiceConstants.DEFAULT_SPLITS_CACHE_EXPIRATION_IN_SECONDS); mHttpClient = buildHttpClient(apiKey, buildCertPinningConfig(inputData.getString(ServiceConstants.WORKER_PARAM_CERTIFICATE_PINS))); } @@ -64,10 +61,6 @@ public String getEndPoint() { return mEndpoint; } - public long getCacheExpirationInSeconds() { - return mCacheExpirationInSeconds; - } - private static HttpClient buildHttpClient(String apiKey, @Nullable CertificatePinningConfiguration certificatePinningConfiguration) { HttpClientImpl.Builder builder = new HttpClientImpl.Builder(); diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java b/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java index 6724aafe0..d5f3f6d27 100644 --- a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java +++ b/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java @@ -22,7 +22,6 @@ public SplitsSyncWorker(@NonNull Context context, new FetcherProvider(getHttpClient(), getEndPoint()), new SplitChangeProcessorProvider().provideSplitChangeProcessor(params.configuredFilterType(), params.configuredFilterValues()), new SyncHelperProvider(), - getCacheExpirationInSeconds(), params.flagsSpec()); mSplitTask = builder.getTask(); diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java b/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java index d1fa25c7a..9d0a8a026 100644 --- a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java +++ b/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java @@ -15,7 +15,6 @@ */ class SplitsSyncWorkerTaskBuilder { - private final long mCacheExpirationInSeconds; private final StorageProvider mStorageProvider; private final FetcherProvider mFetcherProvider; private final SplitChangeProcessor mSplitChangeProcessor; @@ -26,13 +25,11 @@ class SplitsSyncWorkerTaskBuilder { FetcherProvider fetcherProvider, SplitChangeProcessor splitChangeProcessor, SyncHelperProvider splitsSyncHelperProvider, - long cacheExpirationInSeconds, String flagsSpec) { mStorageProvider = storageProvider; mFetcherProvider = fetcherProvider; mSplitsSyncHelperProvider = splitsSyncHelperProvider; mSplitChangeProcessor = splitChangeProcessor; - mCacheExpirationInSeconds = cacheExpirationInSeconds; mFlagsSpec = flagsSpec; } @@ -51,8 +48,6 @@ SplitTask getTask() { return SplitsSyncTask.buildForBackground(splitsSyncHelper, splitsStorage, - false, - mCacheExpirationInSeconds, splitsFilterQueryString, telemetryStorage); } catch (URISyntaxException e) { 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/RolloutDefinitionsCache.java b/src/main/java/io/split/android/client/storage/RolloutDefinitionsCache.java new file mode 100644 index 000000000..912fafa97 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/RolloutDefinitionsCache.java @@ -0,0 +1,6 @@ +package io.split.android.client.storage; + +public interface RolloutDefinitionsCache { + + void clear(); +} 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/main/java/io/split/android/client/storage/common/SplitStorageContainer.java b/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java index af0f62f98..da911d720 100644 --- a/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java +++ b/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java @@ -1,5 +1,7 @@ package io.split.android.client.storage.common; +import static io.split.android.client.utils.Utils.checkNotNull; + import androidx.annotation.NonNull; import io.split.android.client.service.impressions.observer.PersistentImpressionsObserverCacheStorage; @@ -8,6 +10,7 @@ import io.split.android.client.storage.attributes.PersistentAttributesStorage; import io.split.android.client.storage.events.EventsStorage; import io.split.android.client.storage.events.PersistentEventsStorage; +import io.split.android.client.storage.general.GeneralInfoStorage; import io.split.android.client.storage.impressions.ImpressionsStorage; import io.split.android.client.storage.impressions.PersistentImpressionsCountStorage; import io.split.android.client.storage.impressions.PersistentImpressionsStorage; @@ -18,8 +21,6 @@ import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorage; -import static io.split.android.client.utils.Utils.checkNotNull; - public class SplitStorageContainer { private final SplitsStorage mSplitStorage; @@ -36,6 +37,7 @@ public class SplitStorageContainer { private final TelemetryStorage mTelemetryStorage; private final PersistentImpressionsUniqueStorage mPersistentImpressionsUniqueStorage; private final PersistentImpressionsObserverCacheStorage mPersistentImpressionsObserverCacheStorage; + private final GeneralInfoStorage mGeneralInfoStorage; public SplitStorageContainer(@NonNull SplitsStorage splitStorage, @NonNull MySegmentsStorageContainer mySegmentsStorageContainer, @@ -50,7 +52,8 @@ public SplitStorageContainer(@NonNull SplitsStorage splitStorage, @NonNull AttributesStorageContainer attributesStorageContainer, @NonNull PersistentAttributesStorage persistentAttributesStorage, @NonNull TelemetryStorage telemetryStorage, - @NonNull PersistentImpressionsObserverCacheStorage persistentImpressionsObserverCacheStorage) { + @NonNull PersistentImpressionsObserverCacheStorage persistentImpressionsObserverCacheStorage, + @NonNull GeneralInfoStorage generalInfoStorage) { mSplitStorage = checkNotNull(splitStorage); mMySegmentsStorageContainer = checkNotNull(mySegmentsStorageContainer); @@ -66,6 +69,7 @@ public SplitStorageContainer(@NonNull SplitsStorage splitStorage, mTelemetryStorage = checkNotNull(telemetryStorage); mPersistentImpressionsUniqueStorage = checkNotNull(persistentImpressionsUniqueStorage); mPersistentImpressionsObserverCacheStorage = checkNotNull(persistentImpressionsObserverCacheStorage); + mGeneralInfoStorage = checkNotNull(generalInfoStorage); } public SplitsStorage getSplitsStorage() { @@ -135,4 +139,8 @@ public PersistentImpressionsUniqueStorage getPersistentImpressionsUniqueStorage( public PersistentImpressionsObserverCacheStorage getImpressionsObserverCachePersistentStorage() { return mPersistentImpressionsObserverCacheStorage; } + + public GeneralInfoStorage getGeneralInfoStorage() { + return mGeneralInfoStorage; + } } diff --git a/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java b/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java index 52a1b10f2..ce7f25696 100644 --- a/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java +++ b/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java @@ -12,8 +12,6 @@ public class GeneralInfoEntity { public static final String SPLITS_UPDATE_TIMESTAMP = "splitsUpdateTimestamp"; public static final String CHANGE_NUMBER_INFO = "splitChangesChangeNumber"; public static final String SPLITS_FILTER_QUERY_STRING = "splitsFilterQueryString"; - public static final String DATBASE_MIGRATION_STATUS = "databaseMigrationStatus"; - public static final int DATBASE_MIGRATION_STATUS_DONE = 1; public static final String DATABASE_ENCRYPTION_MODE = "databaseEncryptionMode"; public static final String FLAGS_SPEC = "flagsSpec"; 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 bd2445599..2960f73f7 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 @@ -18,6 +18,8 @@ import io.split.android.client.storage.events.EventsStorage; import io.split.android.client.storage.events.PersistentEventsStorage; import io.split.android.client.storage.events.SqLitePersistentEventsStorage; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.storage.general.GeneralInfoStorageImpl; import io.split.android.client.storage.impressions.ImpressionsStorage; import io.split.android.client.storage.impressions.PersistentImpressionsCountStorage; import io.split.android.client.storage.impressions.PersistentImpressionsStorage; @@ -148,4 +150,8 @@ private static AttributesStorageContainer getAttributesStorageContainerInstance( public static PersistentImpressionsObserverCacheStorage getImpressionsObserverCachePersistentStorage(SplitRoomDatabase splitRoomDatabase, long expirationPeriod, ScheduledThreadPoolExecutor executorService) { return new SqlitePersistentImpressionsObserverCacheStorage(splitRoomDatabase.impressionsObserverCacheDao(), expirationPeriod, executorService); } + + public static GeneralInfoStorage getGeneralInfoStorage(SplitRoomDatabase splitRoomDatabase) { + return new GeneralInfoStorageImpl(splitRoomDatabase.generalInfoDao()); + } } diff --git a/src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java b/src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java new file mode 100644 index 000000000..7f8d763a9 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java @@ -0,0 +1,33 @@ +package io.split.android.client.storage.general; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface GeneralInfoStorage { + + long getSplitsUpdateTimestamp(); + + void setSplitsUpdateTimestamp(long timestamp); + + long getChangeNumber(); + + void setChangeNumber(long changeNumber); + + @NonNull + String getSplitsFilterQueryString(); + + void setSplitsFilterQueryString(String queryString); + + String getDatabaseEncryptionMode(); + + void setDatabaseEncryptionMode(String value); + + @Nullable + String getFlagsSpec(); + + void setFlagsSpec(String value); + + long getRolloutCacheLastClearTimestamp(); + + void setRolloutCacheLastClearTimestamp(long timestamp); +} diff --git a/src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java b/src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java new file mode 100644 index 000000000..b6c5c8423 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java @@ -0,0 +1,88 @@ +package io.split.android.client.storage.general; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.split.android.client.storage.db.GeneralInfoDao; +import io.split.android.client.storage.db.GeneralInfoEntity; + +public class GeneralInfoStorageImpl implements GeneralInfoStorage{ + + private static final String ROLLOUT_CACHE_LAST_CLEAR_TIMESTAMP = "rolloutCacheLastClearTimestamp"; + + private final GeneralInfoDao mGeneralInfoDao; + + public GeneralInfoStorageImpl(GeneralInfoDao generalInfoDao) { + mGeneralInfoDao = checkNotNull(generalInfoDao); + } + + @Override + public long getSplitsUpdateTimestamp() { + GeneralInfoEntity entity = mGeneralInfoDao.getByName(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP); + return entity != null ? entity.getLongValue() : 0L; + } + + @Override + public void setSplitsUpdateTimestamp(long timestamp) { + mGeneralInfoDao.update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, timestamp)); + } + + @Override + public long getChangeNumber() { + GeneralInfoEntity entity = mGeneralInfoDao.getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO); + return entity != null ? entity.getLongValue() : -1L; + } + + @Override + public void setChangeNumber(long changeNumber) { + mGeneralInfoDao.update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, changeNumber)); + } + + @Override + @NonNull + public String getSplitsFilterQueryString() { + GeneralInfoEntity entity = mGeneralInfoDao.getByName(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING); + return entity != null ? entity.getStringValue() : ""; + } + + @Override + public void setSplitsFilterQueryString(String queryString) { + mGeneralInfoDao.update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING, queryString)); + } + + @Override + public String getDatabaseEncryptionMode() { + GeneralInfoEntity entity = mGeneralInfoDao.getByName(GeneralInfoEntity.DATABASE_ENCRYPTION_MODE); + return entity != null ? entity.getStringValue() : ""; + } + + @Override + public void setDatabaseEncryptionMode(String value) { + mGeneralInfoDao.update(new GeneralInfoEntity(GeneralInfoEntity.DATABASE_ENCRYPTION_MODE, value)); + } + + @Override + @Nullable + public String getFlagsSpec() { + GeneralInfoEntity entity = mGeneralInfoDao.getByName(GeneralInfoEntity.FLAGS_SPEC); + return entity != null ? entity.getStringValue() : ""; + } + + @Override + public void setFlagsSpec(String value) { + mGeneralInfoDao.update(new GeneralInfoEntity(GeneralInfoEntity.FLAGS_SPEC, value)); + } + + @Override + public long getRolloutCacheLastClearTimestamp() { + GeneralInfoEntity entity = mGeneralInfoDao.getByName(ROLLOUT_CACHE_LAST_CLEAR_TIMESTAMP); + return entity != null ? entity.getLongValue() : 0L; + } + + @Override + public void setRolloutCacheLastClearTimestamp(long timestamp) { + mGeneralInfoDao.update(new GeneralInfoEntity(ROLLOUT_CACHE_LAST_CLEAR_TIMESTAMP, timestamp)); + } +} diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java index 351a82d79..2675fc5f1 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java +++ b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java @@ -1,12 +1,11 @@ package io.split.android.client.storage.mysegments; -import androidx.annotation.VisibleForTesting; - import java.util.Set; import io.split.android.client.dtos.SegmentsChange; +import io.split.android.client.storage.RolloutDefinitionsCache; -public interface MySegmentsStorage { +public interface MySegmentsStorage extends RolloutDefinitionsCache { void loadLocal(); Set getAll(); @@ -14,7 +13,4 @@ public interface MySegmentsStorage { void set(SegmentsChange segmentsChange); long getChangeNumber(); - - @VisibleForTesting - void clear(); } diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainer.java b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainer.java index 98e656071..9feeded60 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainer.java +++ b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainer.java @@ -1,6 +1,8 @@ package io.split.android.client.storage.mysegments; -public interface MySegmentsStorageContainer { +import io.split.android.client.storage.RolloutDefinitionsCache; + +public interface MySegmentsStorageContainer extends RolloutDefinitionsCache { MySegmentsStorage getStorageForKey(String matchingKey); diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java index 8a1ee9187..0a8f51f45 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java +++ b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java @@ -41,4 +41,13 @@ public long getUniqueAmount() { return segments.size(); } + + @Override + public void clear() { + synchronized (lock) { + for (MySegmentsStorage mySegmentsStorage : mStorageMap.values()) { + mySegmentsStorage.clear(); + } + } + } } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java b/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java index 70d228726..81ff4e5a5 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java @@ -9,8 +9,9 @@ import java.util.Set; import io.split.android.client.dtos.Split; +import io.split.android.client.storage.RolloutDefinitionsCache; -public interface SplitsStorage { +public interface SplitsStorage extends RolloutDefinitionsCache { void loadLocal(); Split get(@NonNull String name); @@ -38,8 +39,6 @@ public interface SplitsStorage { void updateFlagsSpec(String flagsSpec); - void clear(); - @NonNull Set getNamesByFlagSets(Collection flagSets); } 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..ba342d1c8 --- /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.getExpirationDays()); + assertFalse(config.isClearOnInit()); + } + + @Test + public void expirationIsCorrectlySet() { + RolloutCacheConfiguration.Builder builder = RolloutCacheConfiguration.builder(); + builder.expirationDays(1); + RolloutCacheConfiguration config = builder.build(); + assertEquals(1, config.getExpirationDays()); + } + + @Test + public void clearOnInitIsCorrectlySet() { + RolloutCacheConfiguration.Builder builder = RolloutCacheConfiguration.builder(); + builder.clearOnInit(true); + RolloutCacheConfiguration config = builder.build(); + assertTrue(config.isClearOnInit()); + } + + @Test + public void negativeExpirationIsSetToDefault() { + RolloutCacheConfiguration.Builder builder = RolloutCacheConfiguration.builder(); + builder.expirationDays(-1); + RolloutCacheConfiguration config = builder.build(); + assertEquals(10, config.getExpirationDays()); + } +} diff --git a/src/test/java/io/split/android/client/SplitClientConfigTest.java b/src/test/java/io/split/android/client/SplitClientConfigTest.java index 8f3351ccb..1e69b9c12 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.getExpirationDays()); + assertFalse(config.isClearOnInit()); + } + + @Test + public void rolloutCacheConfigurationExpirationIsCorrectlySet() { + RolloutCacheConfiguration config = SplitClientConfig.builder() + .rolloutCacheConfiguration(RolloutCacheConfiguration.builder().expirationDays(1).clearOnInit(true).build()) + .build().rolloutCacheConfiguration(); + + assertEquals(1, config.getExpirationDays()); + assertTrue(config.isClearOnInit()); + } + + @Test + public void nullRolloutCacheConfigurationSetsDefault() { + Queue logMessages = getLogMessagesQueue(); + RolloutCacheConfiguration config = SplitClientConfig.builder() + .logLevel(SplitLogLevel.WARNING) + .rolloutCacheConfiguration(null) + .build().rolloutCacheConfiguration(); + + assertEquals(10, config.getExpirationDays()); + assertFalse(config.isClearOnInit()); + assertEquals(1, logMessages.size()); + } + @NonNull private static Queue getLogMessagesQueue() { Queue logMessages = new LinkedList<>(); 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/events/EventsManagerTest.java b/src/test/java/io/split/android/client/events/EventsManagerTest.java index c9cca29c6..1f896825d 100644 --- a/src/test/java/io/split/android/client/events/EventsManagerTest.java +++ b/src/test/java/io/split/android/client/events/EventsManagerTest.java @@ -51,6 +51,7 @@ public void eventOnReady() { execute(shouldStop, intervalExecutionTime, maxExecutionTime, eventManager, SplitEvent.SDK_READY); assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY)); + assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE)); assertFalse(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)); } diff --git a/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java b/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java index 9c354f744..9bbe08901 100644 --- a/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java +++ b/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java @@ -67,7 +67,7 @@ public void correctExecution() throws HttpFetcherException { // And updateTimestamp is 0 // Retry is off, so splitSyncHelper.sync should be called mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, - false, 1000, mQueryString, mEventsManager, mTelemetryRuntimeProducer); + mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(-1L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); @@ -77,38 +77,6 @@ public void correctExecution() throws HttpFetcherException { verify(mSplitsSyncHelper, times(1)).sync(-1, false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); } - @Test - public void cleanOldCacheDisabled() throws HttpFetcherException { - // Cache should not be cleared when cache expired - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, - false, 100L, mQueryString, mEventsManager, mTelemetryRuntimeProducer); - when(mSplitsStorage.getTill()).thenReturn(300L); - when(mSplitsStorage.getUpdateTimestamp()).thenReturn(100L); - when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - // This value function checks that cache is expired, here we simulate cache expired - when(mSplitsSyncHelper.cacheHasExpired(anyLong(), anyLong(), anyLong())).thenReturn(true); - - mTask.execute(); - - verify(mSplitsStorage, never()).clear(); - } - - @Test - public void cleanOldCacheEnabled() throws HttpFetcherException { - - // Cache should be cleared when cache expired - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, - true, 100L, mQueryString, mEventsManager, mTelemetryRuntimeProducer); - when(mSplitsStorage.getTill()).thenReturn(100L); - when(mSplitsStorage.getUpdateTimestamp()).thenReturn(100L); // Dummy value clearing depends on cacheHasExpired function value - when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.cacheHasExpired(anyLong(), anyLong(), anyLong())).thenReturn(true); - - mTask.execute(); - - verify(mSplitsSyncHelper, times(1)).sync(100L, true, true, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); - } - @Test public void cleanSplitsWhenQueryStringHasChanged() throws HttpFetcherException { // Splits have to be cleared when query string on db is != than current one on current sdk client instance @@ -119,11 +87,10 @@ public void cleanSplitsWhenQueryStringHasChanged() throws HttpFetcherException { Map params = new HashMap<>(); params.put("since", 100L); mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, - true, 100L, otherQs, mEventsManager, mTelemetryRuntimeProducer); + otherQs, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(1111L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.cacheHasExpired(anyLong(), anyLong(), anyLong())).thenReturn(false); mTask.execute(); @@ -137,11 +104,10 @@ public void noClearSplitsWhenQueryStringHasNotChanged() throws HttpFetcherExcept // Setting up cache not expired mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, - true,100L, mQueryString, mEventsManager, mTelemetryRuntimeProducer); + mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(1111L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.cacheHasExpired(anyLong(), anyLong(), anyLong())).thenReturn(false); mTask.execute(); @@ -156,7 +122,7 @@ public void splitUpdatedNotified() throws HttpFetcherException { // And updateTimestamp is 0 // Retry is off, so splitSyncHelper.sync should be called mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, - false, 1000, mQueryString, mEventsManager, mTelemetryRuntimeProducer); + mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); @@ -174,7 +140,7 @@ public void splitFetchdNotified() throws HttpFetcherException { // And updateTimestamp is 0 // Retry is off, so splitSyncHelper.sync should be called mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, - false, 1000, mQueryString, mEventsManager, mTelemetryRuntimeProducer); + mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); @@ -188,7 +154,7 @@ public void splitFetchdNotified() throws HttpFetcherException { @Test public void syncIsTrackedInTelemetry() { mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, - false, 1000, mQueryString, mEventsManager, mTelemetryRuntimeProducer); + mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); @@ -202,7 +168,7 @@ public void syncIsTrackedInTelemetry() { @Test public void recordSuccessInTelemetry() { mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, - false, 1000, mQueryString, mEventsManager, mTelemetryRuntimeProducer); + mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(-1L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); diff --git a/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java b/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java index 1b2d9e104..2ff74c234 100644 --- a/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java +++ b/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java @@ -14,7 +14,6 @@ import static org.mockito.Mockito.when; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentMatcher; @@ -174,46 +173,6 @@ public void shouldClearStorageAfterFetch() throws HttpFetcherException { assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); } - @Test - public void cacheExpired() throws HttpFetcherException { - - // change number > -1 should clear cache - // when cache expired - - long cacheExpInSeconds = 10000; - long updateTimestamp = System.currentTimeMillis() / 1000 - cacheExpInSeconds - 1; - boolean expired = mSplitsSyncHelper.cacheHasExpired(100, updateTimestamp, cacheExpInSeconds); - - Assert.assertTrue(expired); - } - - @Test - public void cacheNotExpired() throws HttpFetcherException { - - // change number > -1 should clear cache - // only when cache expired - - long cacheExpInSeconds = 10000; - long updateTimestamp = System.currentTimeMillis() - cacheExpInSeconds * 1000 + 1000; - boolean expired = mSplitsSyncHelper.cacheHasExpired(100, updateTimestamp, cacheExpInSeconds); - - Assert.assertFalse(expired); - } - - @Test - public void cacheExpiredButChangeNumber() throws HttpFetcherException { - - // change number = -1 means no previous cache available - // so, should no clear cache - // even if it's expired - - long cacheExpInSeconds = 10000; - long updateTimestamp = System.currentTimeMillis() / 1000 - cacheExpInSeconds - 1000; - boolean expired = mSplitsSyncHelper.cacheHasExpired(-1, updateTimestamp, cacheExpInSeconds); - - Assert.assertFalse(expired); - } - @Test public void errorIsRecordedInTelemetry() throws HttpFetcherException { when(mSplitsFetcher.execute(mDefaultParams, null)) 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 new file mode 100644 index 000000000..b89da1649 --- /dev/null +++ b/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt @@ -0,0 +1,189 @@ +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 +import io.split.android.client.storage.cipher.EncryptionMigrationTask +import io.split.android.client.storage.general.GeneralInfoStorage +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.Mockito.longThat +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import java.util.concurrent.TimeUnit + +class RolloutCacheManagerTest { + + private lateinit var mRolloutCacheManager: RolloutCacheManager + private lateinit var mGeneralInfoStorage: GeneralInfoStorage + 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) + mEncryptionMigrationTask = mock(EncryptionMigrationTask::class.java) + mCleanUpDatabaseTask = mock(CleanUpDatabaseTask::class.java) + mSplitsCache = mock(RolloutDefinitionsCache::class.java) + mSegmentsCache = mock(RolloutDefinitionsCache::class.java) + } + + @Test + fun `validateCache calls listener`() { + mRolloutCacheManager = getCacheManager(10, false) + + val listener = mock(SplitTaskExecutionListener::class.java) + mRolloutCacheManager.validateCache(listener) + + verify(listener).taskExecuted(any()) + } + + @Test + fun `validateCache calls clear on storages when expiration is surpassed`() { + val mockedTimestamp = createMockedTimestamp(10) + `when`(mGeneralInfoStorage.splitsUpdateTimestamp).thenReturn(mockedTimestamp) + mRolloutCacheManager = getCacheManager(9, false) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mSplitsCache).clear() + verify(mSegmentsCache).clear() + } + + @Test + 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(10, false) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mSplitsCache, times(0)).clear() + verify(mSegmentsCache, times(0)).clear() + } + + @Test + 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(10, true) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mSplitsCache).clear() + verify(mSegmentsCache).clear() + } + + @Test + fun `validateCache calls clear on storage only once when executed consecutively`() { + 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(10, true) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mSplitsCache, times(1)).clear() + verify(mSegmentsCache, times(1)).clear() + } + + @Test + fun `exception during clear still calls listener`() { + 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(10, true) + + val listener = mock(SplitTaskExecutionListener::class.java) + `when`(mSplitsCache.clear()).thenThrow(RuntimeException("Exception during clear")) + + mRolloutCacheManager.validateCache(listener) + + verify(listener).taskExecuted(any()) + } + + @Test + fun `validateCache updates last clear timestamp when storages are cleared`() { + 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(10, true) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mGeneralInfoStorage).setRolloutCacheLastClearTimestamp(longThat { it > 0 }) + } + + @Test + fun `validateCache does not update last clear timestamp when storages are not cleared`() { + 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(10, false) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mGeneralInfoStorage, times(0)).setRolloutCacheLastClearTimestamp(anyLong()) + } + + @Test + fun `validateCache executes cleanUpDatabaseTask`() { + mRolloutCacheManager = getCacheManager(10, false) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mCleanUpDatabaseTask).execute() + } + + @Test + fun `validateCache executes encryptionMigrationTask`() { + mRolloutCacheManager = getCacheManager(10, false) + + mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) + + verify(mEncryptionMigrationTask).execute() + } + + @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().expirationDays(expiration).clearOnInit(clearOnInit).build(), mCleanUpDatabaseTask, mEncryptionMigrationTask, mSplitsCache, mSegmentsCache) + } + + private fun createMockedTimestamp(period: Long): Long { + val currentTimeMillis = System.currentTimeMillis() + val mockedTimestamp = + TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(currentTimeMillis) - period) + return mockedTimestamp + } +} 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/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java b/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java index 47f83e41f..1b950b365 100644 --- a/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java +++ b/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java @@ -56,7 +56,6 @@ public void getTaskUsesStorageProviderForSplitsStorage() { mFetcherProvider, mSplitChangeProcessor, mSplitsSyncHelperProvider, - 0, null); builder.getTask(); @@ -71,7 +70,6 @@ public void getTaskUsesFetcherProviderForFetcher() throws URISyntaxException { mFetcherProvider, mSplitChangeProcessor, mSplitsSyncHelperProvider, - 0, null); builder.getTask(); @@ -86,7 +84,6 @@ public void getTaskUsesStorageProviderForTelemetryStorage() { mFetcherProvider, mSplitChangeProcessor, mSplitsSyncHelperProvider, - 0, null); builder.getTask(); @@ -104,7 +101,6 @@ public void getTaskUsesSplitsSyncHelperProviderForSplitsSyncHelper() throws URIS mFetcherProvider, mSplitChangeProcessor, mSplitsSyncHelperProvider, - 0, "1.5"); builder.getTask(); @@ -127,7 +123,6 @@ public void getTaskReturnsNullWhenURISyntaxExceptionIsThrown() throws URISyntaxE mFetcherProvider, mSplitChangeProcessor, mSplitsSyncHelperProvider, - 0, null); SplitTask task = builder.getTask(); @@ -145,12 +140,11 @@ public void getTaskUsesSplitSyncTaskStaticMethod() { mFetcherProvider, mSplitChangeProcessor, mSplitsSyncHelperProvider, - 250, "2.5"); builder.getTask(); - mockedStatic.verify(() -> SplitsSyncTask.buildForBackground(splitsSyncHelper, mSplitsStorage, false, 250, "filterQueryString", mTelemetryStorage)); + mockedStatic.verify(() -> SplitsSyncTask.buildForBackground(splitsSyncHelper, mSplitsStorage, "filterQueryString", mTelemetryStorage)); } } } 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); diff --git a/src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java b/src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java new file mode 100644 index 000000000..aca7b1185 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java @@ -0,0 +1,169 @@ +package io.split.android.client.storage.general; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +import io.split.android.client.storage.db.GeneralInfoDao; +import io.split.android.client.storage.db.GeneralInfoEntity; + +public class GeneralInfoStorageImplTest { + + private GeneralInfoDao mGeneralInfoDao; + private GeneralInfoStorageImpl mGeneralInfoStorage; + + @Before + public void setUp() { + mGeneralInfoDao = mock(GeneralInfoDao.class); + mGeneralInfoStorage = new GeneralInfoStorageImpl(mGeneralInfoDao); + } + + @Test + public void setSplitsUpdateTimestampSetsValueOnDao() { + mGeneralInfoStorage.setSplitsUpdateTimestamp(123L); + + verify(mGeneralInfoDao).update(argThat(entity -> entity.getName().equals("splitsUpdateTimestamp") && entity.getLongValue() == 123L)); + } + + @Test + public void getSplitsUpdateTimestampGetsValueFromDao() { + when(mGeneralInfoDao.getByName("splitsUpdateTimestamp")).thenReturn(new GeneralInfoEntity("splitsUpdateTimestamp", 123L)); + long splitsUpdateTimestamp = mGeneralInfoStorage.getSplitsUpdateTimestamp(); + + assertEquals(123L, splitsUpdateTimestamp); + verify(mGeneralInfoDao).getByName("splitsUpdateTimestamp"); + } + + @Test + public void setChangeNumberSetsValueOnDao() { + mGeneralInfoStorage.setChangeNumber(123L); + + verify(mGeneralInfoDao).update(argThat(entity -> entity.getName().equals("splitChangesChangeNumber") && entity.getLongValue() == 123L)); + } + + @Test + public void getChangeNumberGetsValueFromDao() { + when(mGeneralInfoDao.getByName("splitChangesChangeNumber")).thenReturn(new GeneralInfoEntity("splitChangesChangeNumber", 123L)); + long changeNumber = mGeneralInfoStorage.getChangeNumber(); + + assertEquals(123L, changeNumber); + verify(mGeneralInfoDao).getByName("splitChangesChangeNumber"); + } + + @Test + public void getSplitsFilterQueryStringGetsValueFromDao() { + when(mGeneralInfoDao.getByName("splitsFilterQueryString")).thenReturn(new GeneralInfoEntity("splitsFilterQueryString", "queryString")); + String splitsFilterQueryString = mGeneralInfoStorage.getSplitsFilterQueryString(); + + assertEquals("queryString", splitsFilterQueryString); + verify(mGeneralInfoDao).getByName("splitsFilterQueryString"); + } + + @Test + public void setSplitsFilterQueryStringSetsValueOnDao() { + mGeneralInfoStorage.setSplitsFilterQueryString("queryString"); + + verify(mGeneralInfoDao).update(argThat(entity -> entity.getName().equals("splitsFilterQueryString") && entity.getStringValue().equals("queryString"))); + } + + @Test + public void getDatabaseEncryptionModeGetsValueFromDao() { + when(mGeneralInfoDao.getByName("databaseEncryptionMode")).thenReturn(new GeneralInfoEntity("databaseEncryptionMode", "value")); + String databaseEncryptionMode = mGeneralInfoStorage.getDatabaseEncryptionMode(); + + assertEquals("value", databaseEncryptionMode); + verify(mGeneralInfoDao).getByName("databaseEncryptionMode"); + } + + @Test + public void setDatabaseEncryptionModeSetsValueOnDao() { + mGeneralInfoStorage.setDatabaseEncryptionMode("value"); + + verify(mGeneralInfoDao).update(argThat(entity -> entity.getName().equals("databaseEncryptionMode") && entity.getStringValue().equals("value"))); + } + + @Test + public void getFlagsSpecGetsValueFromDao() { + when(mGeneralInfoDao.getByName("flagsSpec")).thenReturn(new GeneralInfoEntity("flagsSpec", "value")); + String flagsSpec = mGeneralInfoStorage.getFlagsSpec(); + + assertEquals("value", flagsSpec); + verify(mGeneralInfoDao).getByName("flagsSpec"); + } + + @Test + public void setFlagsSpecSetsValueOnDao() { + mGeneralInfoStorage.setFlagsSpec("value"); + + verify(mGeneralInfoDao).update(argThat(entity -> entity.getName().equals("flagsSpec") && entity.getStringValue().equals("value"))); + } + + @Test + public void getRolloutCacheLastClearTimestampGetsValueFromDao() { + when(mGeneralInfoDao.getByName("rolloutCacheLastClearTimestamp")).thenReturn(new GeneralInfoEntity("rolloutCacheLastClearTimestamp", 123L)); + long rolloutCacheLastClearTimestamp = mGeneralInfoStorage.getRolloutCacheLastClearTimestamp(); + + assertEquals(123L, rolloutCacheLastClearTimestamp); + verify(mGeneralInfoDao).getByName("rolloutCacheLastClearTimestamp"); + } + + @Test + public void setRolloutCacheLastClearTimestampSetsValueOnDao() { + mGeneralInfoStorage.setRolloutCacheLastClearTimestamp(123L); + + verify(mGeneralInfoDao).update(argThat(entity -> entity.getName().equals("rolloutCacheLastClearTimestamp") && entity.getLongValue() == 123L)); + } + + @Test + public void getChangeNumberReturnsMinusOneIfEntityIsNull() { + when(mGeneralInfoDao.getByName("splitChangesChangeNumber")).thenReturn(null); + long changeNumber = mGeneralInfoStorage.getChangeNumber(); + + assertEquals(-1L, changeNumber); + } + + @Test + public void getSplitsUpdateTimestampReturnsZeroIfEntityIsNull() { + when(mGeneralInfoDao.getByName("splitsUpdateTimestamp")).thenReturn(null); + long timestamp = mGeneralInfoStorage.getSplitsUpdateTimestamp(); + + assertEquals(0L, timestamp); + } + + @Test + public void getSplitsFilterQueryStringReturnsEmptyStringIfEntityIsNull() { + when(mGeneralInfoDao.getByName("splitsFilterQueryString")).thenReturn(null); + String queryString = mGeneralInfoStorage.getSplitsFilterQueryString(); + + assertEquals("", queryString); + } + + @Test + public void getDatabaseEncryptionModeReturnsEmptyStringIfEntityIsNull() { + when(mGeneralInfoDao.getByName("databaseEncryptionMode")).thenReturn(null); + String value = mGeneralInfoStorage.getDatabaseEncryptionMode(); + + assertEquals("", value); + } + + @Test + public void getFlagsSpecReturnsEmptyStringIfEntityIsNull() { + when(mGeneralInfoDao.getByName("flagsSpec")).thenReturn(null); + String value = mGeneralInfoStorage.getFlagsSpec(); + + assertEquals("", value); + } + + @Test + public void getRolloutCacheLastClearTimestampReturnsZeroIfEntityIsNull() { + when(mGeneralInfoDao.getByName("rolloutCacheLastClearTimestamp")).thenReturn(null); + long timestamp = mGeneralInfoStorage.getRolloutCacheLastClearTimestamp(); + + assertEquals(0L, timestamp); + } +}