diff --git a/.github/workflows/instrumented.yml b/.github/workflows/instrumented.yml index e9633d532..1e96f9128 100644 --- a/.github/workflows/instrumented.yml +++ b/.github/workflows/instrumented.yml @@ -43,6 +43,7 @@ jobs: uses: reactivecircus/android-emulator-runner@v2.28.0 with: api-level: ${{ matrix.api-level }} + avd-name: macOS-avd-arm64-v8a-29 force-avd-creation: false emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true diff --git a/CHANGES.txt b/CHANGES.txt index 1de3274e6..51c5c684e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +3.3.0 (Jul 18, 2023) +- Improved streaming architecture implementation to apply feature flag updates from the notification received which is now enhanced, improving efficiency and reliability of the whole update system. +- Added logic to do a full check of feature flags immediately when the app comes back to foreground, limited to once per minute. + 3.2.2 (Jun 7, 2023) - Refactored cipher creation to avoid NPE scenarios. diff --git a/build.gradle b/build.gradle index 9074c632f..8e1e9b273 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'signing' apply plugin: 'kotlin-android' ext { - splitVersion = '3.2.2' + splitVersion = '3.3.0' } android { @@ -98,8 +98,8 @@ dependencies { def lifecycleVersion = '2.5.1' def annotationVersion = '1.2.0' def gsonVersion = '2.9.1' - def guavaVersion = '31.1-android' - def snakeYamlVersion = '1.32' + def guavaVersion = '32.0.0-android' + def snakeYamlVersion = '2.0' def jetBrainsAnnotationsVersion = '22.0.0' def okHttpVersion = '3.12.13' def playServicesVersion = '17.6.0' diff --git a/split-proguard-rules.pro b/split-proguard-rules.pro index 7165edd00..8dc624df9 100644 --- a/split-proguard-rules.pro +++ b/split-proguard-rules.pro @@ -20,3 +20,35 @@ -dontwarn java.beans.IntrospectionException -dontwarn java.beans.Introspector -dontwarn java.beans.PropertyDescriptor + +##---------------Begin: proguard configuration for Gson ---------- +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Application classes that will be serialized/deserialized over Gson +-keep class com.google.gson.examples.android.model.** { ; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken +##---------------End: proguard configuration for Gson ---------- diff --git a/src/androidTest/java/helper/IntegrationHelper.java b/src/androidTest/java/helper/IntegrationHelper.java index 068428eca..243b46dbe 100644 --- a/src/androidTest/java/helper/IntegrationHelper.java +++ b/src/androidTest/java/helper/IntegrationHelper.java @@ -182,16 +182,31 @@ public static SplitClientConfig lowRefreshRateConfig() { } public static SplitClientConfig lowRefreshRateConfig(boolean streamingEnabled) { - return lowRefreshRateConfig(streamingEnabled, false); + return lowRefreshRateConfig(streamingEnabled, false, true, 60L, 2L); } public static SplitClientConfig lowRefreshRateConfig(boolean streamingEnabled, boolean telemetryEnabled) { + return lowRefreshRateConfig(streamingEnabled, telemetryEnabled, true, 60L, 2L); + } + + public static SplitClientConfig syncDisabledConfig() { + return lowRefreshRateConfig(true, false, false, 60L, 2L); + } + + public static SplitClientConfig customSseConnectionDelayConfig(boolean streamingEnabled, long delay, long disconnectionDelay) { + return lowRefreshRateConfig(streamingEnabled, false, true, delay, disconnectionDelay); + } + + public static SplitClientConfig lowRefreshRateConfig(boolean streamingEnabled, boolean telemetryEnabled, boolean syncEnabled, long delay, long sseDisconnectionDelay) { TestableSplitConfigBuilder builder = new TestableSplitConfigBuilder() .ready(30000) .featuresRefreshRate(3) .segmentsRefreshRate(3) .impressionsRefreshRate(3) .impressionsChunkSize(999999) + .syncEnabled(syncEnabled) + .defaultSSEConnectionDelayInSecs(delay) + .sseDisconnectionDelayInSecs(sseDisconnectionDelay) .streamingEnabled(streamingEnabled) .shouldRecordTelemetry(telemetryEnabled) .enableDebug() @@ -200,10 +215,14 @@ public static SplitClientConfig lowRefreshRateConfig(boolean streamingEnabled, b } public static String streamingEnabledToken() { + return streamingEnabledToken(0); + } + + public static String streamingEnabledToken(int delay) { // This token expires in 2040 return "{" + " \"pushEnabled\": true," + - " \"connDelay\": 0," + + " \"connDelay\": " + delay + "," + " \"token\": \"eyJhbGciOiJIUzI1NiIsImtpZCI6IjVZOU05US45QnJtR0EiLCJ0eXAiOiJKV1QifQ.eyJ4LWFibHktY2FwYWJpbGl0eSI6IntcIk16TTVOamMwT0RjeU5nPT1fTVRFeE16Z3dOamd4X01UY3dOVEkyTVRNME1nPT1fbXlTZWdtZW50c1wiOltcInN1YnNjcmliZVwiXSxcIk16TTVOamMwT0RjeU5nPT1fTVRFeE16Z3dOamd4X3NwbGl0c1wiOltcInN1YnNjcmliZVwiXSxcImNvbnRyb2xfcHJpXCI6W1wic3Vic2NyaWJlXCIsXCJjaGFubmVsLW1ldGFkYXRhOnB1Ymxpc2hlcnNcIl0sXCJjb250cm9sX3NlY1wiOltcInN1YnNjcmliZVwiLFwiY2hhbm5lbC1tZXRhZGF0YTpwdWJsaXNoZXJzXCJdfSIsIngtYWJseS1jbGllbnRJZCI6ImNsaWVudElkIiwiZXhwIjoyMjA4OTg4ODAwLCJpYXQiOjE1ODc0MDQzODh9.LcKAXnkr-CiYVxZ7l38w9i98Y-BMAv9JlGP2i92nVQY\"" + "}"; @@ -217,6 +236,33 @@ public static String streamingEnabledV1Token() { return "{\"connDelay\":0,\"pushEnabled\":true,\"token\":\"eyJhbGciOiJIUzI1NiIsImtpZCI6IjVZOU05US5pSGZUUmciLCJ0eXAiOiJKV1QifQ.eyJ4LWFibHktY2FwYWJpbGl0eSI6IntcIk56TTJNREk1TXpjMF9NVGd5TlRnMU1UZ3dOZz09X01Ua3pOamd3TURFNE1BPT1fbXlTZWdtZW50c1wiOltcInN1YnNjcmliZVwiXSxcIk56TTJNREk1TXpjMF9NVGd5TlRnMU1UZ3dOZz09X01qWXhNRE0yTkRjd09RPT1fbXlTZWdtZW50c1wiOltcInN1YnNjcmliZVwiXSxcIk56TTJNREk1TXpjMF9NVGd5TlRnMU1UZ3dOZz09X3NwbGl0c1wiOltcInN1YnNjcmliZVwiXSxcImNvbnRyb2xfcHJpXCI6W1wic3Vic2NyaWJlXCIsXCJjaGFubmVsLW1ldGFkYXRhOnB1Ymxpc2hlcnNcIl0sXCJjb250cm9sX3NlY1wiOltcInN1YnNjcmliZVwiLFwiY2hhbm5lbC1tZXRhZGF0YTpwdWJsaXNoZXJzXCJdfSIsIngtYWJseS1jbGllbnRJZCI6ImNsaWVudElkIiwiZXhwIjoxNjQ4NjU2MjU4LCJpYXQiOjE2NDg2NTI2NTh9.MWwudv3kafKr-gVeqt-ClLAkCngZsDhdWx-dwqM9rxs\"}"; } + public static String splitChangeV2CompressionType2() { + return splitChangeV2("9999999999999", + "1000", + "2", + "eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU="); + } + + public static String splitChangeV2CompressionType1() { + return splitChangeV2("9999999999999", + "1000", + "1", + "H4sIAAAAAAAA/8yT327aTBDFXyU612vJxoTgvUMfKB8qcaSapqoihAZ7DNusvWi9TpUiv3tl/pdQVb1qL+cwc3bOj/EGzlKeq3T6tuaYCoZEXbGFgMogkXXDIM0y31v4C/aCgMnrU9/3gl7Pp4yilMMIAuVusqDamvlXeiWIg/FAa5OSU6aEDHz/ip4wZ5Be1AmjoBsFAtVOCO56UXh31/O7ApUjV1eQGPw3HT+NIPCitG7bctIVC2ScU63d1DK5gksHCZPnEEhXVC45rosFW8ig1++GYej3g85tJEB6aSA7Aqkpc7Ws7XahCnLTbLVM7evnzalsUUHi8//j6WgyTqYQKMilK7b31tRryLa3WKiyfRCDeHhq2Dntiys+JS/J8THUt5VyrFXlHnYTQ3LU2h91yGdQVqhy+0RtTeuhUoNZ08wagTVZdxbBndF5vYVApb7z9m9pZgKaFqwhT+6coRHvg398nEweP/157Bd+S1hz6oxtm88O73B0jbhgM47nyej+YRRfgdNODDlXJWcJL9tUF5SqnRqfbtPr4LdcTHnk4rfp3buLOkG7+Pmp++vRM9w/wVblzX7Pm8OGfxf5YDKZfxh9SS6B/2Pc9t/7ja01o5k1PwIAAP//uTipVskEAAA="); + } + + public static String splitChangeV2CompressionType0() { + return splitChangeV2("9999999999999", + "1000", + "0", + "eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTY4NDMyOTg1NDM4NSwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiV0hJVEVMSVNUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7Im1hdGNoZXJUeXBlIjoiV0hJVEVMSVNUIiwibmVnYXRlIjpmYWxzZSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOnsid2hpdGVsaXN0IjpbImFkbWluIiwibWF1cm8iLCJuaWNvIl19fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9XSwibGFiZWwiOiJ3aGl0ZWxpc3RlZCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibWF1ci0yIn19XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbWF1ci0yIn0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0="); + } + + private static String splitChangeV2(String changeNumber, String previousChangeNumber, String compressionType, String compressedPayload) { + return "id: vQQ61wzBRO:0:0\n" + + "event: message\n" + + "data: {\"id\":\"m2T85LA4fQ:0:0\",\"clientId\":\"pri:NzIyNjY1MzI4\",\"timestamp\":"+System.currentTimeMillis()+",\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":"+changeNumber+",\\\"pcn\\\":"+previousChangeNumber+",\\\"c\\\":"+compressionType+",\\\"d\\\":\\\""+compressedPayload+"\\\"}\"}\n"; + } + /** * Builds a dispatcher with the given responses. * diff --git a/src/androidTest/java/helper/TestableSplitConfigBuilder.java b/src/androidTest/java/helper/TestableSplitConfigBuilder.java index d28226e26..ac73eaaee 100644 --- a/src/androidTest/java/helper/TestableSplitConfigBuilder.java +++ b/src/androidTest/java/helper/TestableSplitConfigBuilder.java @@ -7,6 +7,7 @@ import io.split.android.client.SyncConfig; import io.split.android.client.impressions.ImpressionListener; import io.split.android.client.network.DevelopmentSslConfig; +import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.impressions.ImpressionsMode; import io.split.android.client.shared.UserConsent; import io.split.android.client.utils.logger.Logger; @@ -56,6 +57,8 @@ public class TestableSplitConfigBuilder { private int mMtkRefreshRate = 1800; private UserConsent mUserConsent = UserConsent.GRANTED; private boolean mEncryptionEnabled; + private long mDefaultSSEConnectionDelayInSecs = ServiceConstants.DEFAULT_SSE_CONNECTION_DELAY_SECS; + private long mSSEDisconnectionDelayInSecs = 60L; public TestableSplitConfigBuilder() { mServiceEndpoints = ServiceEndpoints.builder().build(); @@ -102,7 +105,7 @@ public TestableSplitConfigBuilder ready(int ready) { } public TestableSplitConfigBuilder enableDebug() { - this.mLogLevel = SplitLogLevel.DEBUG; + this.mLogLevel = SplitLogLevel.VERBOSE; return this; } @@ -236,6 +239,16 @@ public TestableSplitConfigBuilder encryptionEnabled(boolean enabled) { return this; } + public TestableSplitConfigBuilder defaultSSEConnectionDelayInSecs(long seconds) { + this.mDefaultSSEConnectionDelayInSecs = seconds; + return this; + } + + public TestableSplitConfigBuilder sseDisconnectionDelayInSecs(long seconds) { + this.mSSEDisconnectionDelayInSecs = seconds; + return this; + } + public SplitClientConfig build() { Constructor constructor = SplitClientConfig.class.getDeclaredConstructors()[0]; constructor.setAccessible(true); @@ -285,7 +298,9 @@ public SplitClientConfig build() { mMtkPerPush, mMtkRefreshRate, mUserConsent, - mEncryptionEnabled); + mEncryptionEnabled, + mDefaultSSEConnectionDelayInSecs, + mSSEDisconnectionDelayInSecs); return config; } catch (Exception e) { Logger.e("Error creating Testable Split client builder: " diff --git a/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2MultiClientTest.java b/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2MultiClientTest.java index cb9b8c3d9..6790672fe 100644 --- a/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2MultiClientTest.java +++ b/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2MultiClientTest.java @@ -39,6 +39,7 @@ import io.split.android.client.storage.db.MySegmentDao; import io.split.android.client.storage.db.MySegmentEntity; import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.telemetry.storage.InMemoryTelemetryStorage; import io.split.android.client.utils.logger.Logger; import tests.integration.shared.TestingData; import tests.integration.shared.TestingHelper; @@ -61,6 +62,7 @@ public class MySegmentsChangeV2MultiClientTest { SplitClient mClient; SynchronizerSpyImpl mSynchronizerSpy; SplitRoomDatabase mDb; + private InMemoryTelemetryStorage mTelemetryStorage; @Before public void setup() { @@ -71,6 +73,7 @@ public void setup() { mMySegmentsSyncLatch2 = new CountDownLatch(1); mMySegmentsUpdateLatch2 = new CountDownLatch(1); mDb = DatabaseHelper.getTestDatabase(mContext); + mTelemetryStorage = new InMemoryTelemetryStorage(); } @Test @@ -102,7 +105,8 @@ public void onInvalidated(@NonNull Set tables) { mFactory = IntegrationHelper.buildFactory( mApiKey, new Key(userKey), - config, mContext, httpClientMock, mDb, mSynchronizerSpy); + config, mContext, httpClientMock, mDb, mSynchronizerSpy, + null, null, mTelemetryStorage); mClient = mFactory.client(); SplitClient client2 = mFactory.client(new Key(userKey2)); @@ -166,8 +170,8 @@ mApiKey, new Key(userKey), Assert.assertTrue(mySegmentEntity.getSegmentList().contains("new_segment_added")); Assert.assertFalse(mySegmentEntity.getSegmentList().contains("segment1")); + Assert.assertEquals(4, mTelemetryStorage.popUpdatesFromSSE().getMySegments()); Assert.assertEquals("new_segment_added", mySegmentEntity2.getSegmentList()); - } @After diff --git a/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2Test.java b/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2Test.java index 85c56d35d..374ec0e81 100644 --- a/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2Test.java +++ b/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2Test.java @@ -36,6 +36,8 @@ import io.split.android.client.network.HttpMethod; import io.split.android.client.storage.db.MySegmentEntity; import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.telemetry.storage.InMemoryTelemetryStorage; +import io.split.android.client.telemetry.storage.TelemetryStorage; import io.split.android.client.utils.logger.Logger; import fake.HttpStreamResponseMock; import tests.integration.shared.TestingHelper; @@ -57,6 +59,7 @@ public class MySegmentsChangeV2Test { SplitFactory mFactory; SplitClient mClient; SynchronizerSpyImpl mSynchronizerSpy; + private TelemetryStorage mTelemetryStorage; @Before public void setup() { @@ -68,6 +71,7 @@ public void setup() { mApiKey = apiKeyAndDb.first; mMySegmentsSyncLatch2 = new CountDownLatch(1); mMySegmentsUpdateLatch2 = new CountDownLatch(1); + mTelemetryStorage = new InMemoryTelemetryStorage(); } @Test @@ -88,7 +92,7 @@ public void mySegmentsUpdate() throws IOException, InterruptedException { mFactory = IntegrationHelper.buildFactory( mApiKey, new Key(userKey), - config, mContext, httpClientMock, db, mSynchronizerSpy); + config, mContext, httpClientMock, db, mSynchronizerSpy, null, null, mTelemetryStorage); mClient = mFactory.client(); @@ -125,6 +129,7 @@ mApiKey, new Key(userKey), Assert.assertTrue(mySegmentEntity.getSegmentList().contains("new_segment_added")); Assert.assertFalse(mySegmentEntity.getSegmentList().contains("segment1")); + Assert.assertEquals(2, mTelemetryStorage.popUpdatesFromSSE().getMySegments()); mFactory.destroy(); } diff --git a/src/androidTest/java/tests/integration/streaming/SplitChangeNotificationIntegrationTest.java b/src/androidTest/java/tests/integration/streaming/SplitChangeNotificationIntegrationTest.java new file mode 100644 index 000000000..8d3086279 --- /dev/null +++ b/src/androidTest/java/tests/integration/streaming/SplitChangeNotificationIntegrationTest.java @@ -0,0 +1,235 @@ +package tests.integration.streaming; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.content.Context; + +import androidx.core.util.Pair; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import fake.HttpStreamResponseMock; +import fake.LifecycleManagerStub; +import helper.IntegrationHelper; +import helper.SplitEventTaskHelper; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.network.HttpMethod; +import io.split.android.client.telemetry.model.UpdatesFromSSE; +import io.split.android.client.telemetry.storage.InMemoryTelemetryStorage; +import io.split.android.client.utils.logger.Logger; + +public class SplitChangeNotificationIntegrationTest { + private Context mContext; + private BlockingQueue mStreamingData; + private CountDownLatch mMySegmentsHitsCountLatch; + private CountDownLatch mSplitsHitsCountLatch; + + private CountDownLatch mIsStreamingConnected; + private AtomicInteger mSplitsHitsCountHit; + private AtomicInteger mSseAuthHits; + private final LifecycleManagerStub mLifecycleManager = new LifecycleManagerStub(); + private InMemoryTelemetryStorage mTelemetryStorage; + + @Before + public void setup() { + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + mStreamingData = new LinkedBlockingDeque<>(); + mSplitsHitsCountLatch = new CountDownLatch(1); + mMySegmentsHitsCountLatch = new CountDownLatch(1); + mIsStreamingConnected = new CountDownLatch(1); + mSplitsHitsCountHit = new AtomicInteger(0); + mSseAuthHits = new AtomicInteger(0); + mTelemetryStorage = new InMemoryTelemetryStorage(); + } + + @Test + public void notificationWithCompressionType0IsCorrectlySaved() throws IOException, InterruptedException { + testSplitNotification(IntegrationHelper.splitChangeV2CompressionType0()); + } + + @Test + public void notificationWithCompressionType1IsCorrectlySaved() throws IOException, InterruptedException { + testSplitNotification(IntegrationHelper.splitChangeV2CompressionType1()); + } + + @Test + public void notificationWithCompressionType2IsCorrectlySaved() throws IOException, InterruptedException { + testSplitNotification(IntegrationHelper.splitChangeV2CompressionType2()); + } + + @Test + public void telemetryForSplitsIsRecorded() throws IOException, InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch updateLatch = new CountDownLatch(1); + + Pair pair = getClient(latch, IntegrationHelper.streamingEnabledToken()); + SplitClient client = pair.first; + SplitEventTaskHelper readyTask = pair.second; + client.on(SplitEvent.SDK_UPDATE, new SplitEventTaskHelper(updateLatch)); + awaitInitialization(latch); + + // wait for SSE to connect + boolean sseConnectionAwait = mIsStreamingConnected.await(10, TimeUnit.SECONDS); + + // get first treatment; feature flag is not present + String firstTreatment = client.getTreatment("mauro_java"); + + // simulate SSE notification + pushMessage(IntegrationHelper.splitChangeV2CompressionType0()); + + boolean updateLatchAwait = updateLatch.await(10, TimeUnit.SECONDS); + + String secondTreatment = client.getTreatment("mauro_java"); + + assertTrue(readyTask.isOnPostExecutionCalled); + assertTrue(updateLatchAwait); + assertTrue(sseConnectionAwait); + assertEquals("control", firstTreatment); + assertEquals("off", secondTreatment); + Thread.sleep(500); + UpdatesFromSSE updatesFromSSE = mTelemetryStorage.popUpdatesFromSSE(); + assertEquals(1, updatesFromSSE.getSplits()); + assertEquals(0, updatesFromSSE.getMySegments()); + client.destroy(); + } + + private void testSplitNotification(String notificationMessage) throws IOException, InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch updateLatch = new CountDownLatch(1); + + Pair pair = getClient(latch, IntegrationHelper.streamingEnabledToken()); + SplitClient client = pair.first; + SplitEventTaskHelper readyTask = pair.second; + client.on(SplitEvent.SDK_UPDATE, new SplitEventTaskHelper(updateLatch)); + awaitInitialization(latch); + + // wait for SSE to connect + boolean sseConnectionAwait = mIsStreamingConnected.await(10, TimeUnit.SECONDS); + + // get first treatment; feature flag is not present + String firstTreatment = client.getTreatment("mauro_java"); + + // simulate SSE notification + pushMessage(notificationMessage); + + boolean updateLatchAwait = updateLatch.await(10, TimeUnit.SECONDS); + + String secondTreatment = client.getTreatment("mauro_java"); + + assertTrue(readyTask.isOnPostExecutionCalled); + assertTrue(updateLatchAwait); + assertTrue(sseConnectionAwait); + assertEquals("control", firstTreatment); + assertEquals("off", secondTreatment); + + client.destroy(); + } + + private Pair getClient(CountDownLatch latch, String sseResponse) throws IOException, InterruptedException { + HttpClientMock httpClientMock = new HttpClientMock(createStreamingResponseDispatcher(sseResponse)); + + SplitClientConfig config = IntegrationHelper.customSseConnectionDelayConfig(true, 0, 5L); + + SplitFactory splitFactory = IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + IntegrationHelper.dummyUserKey(), + config, + mContext, + httpClientMock, + null, + null, + null, + mLifecycleManager, + mTelemetryStorage); + + SplitClient client = splitFactory.client(); + SplitEventTaskHelper readyTask = new SplitEventTaskHelper(latch); + client.on(SplitEvent.SDK_READY, readyTask); + + return new Pair<>(client, readyTask); + } + + private void awaitInitialization(CountDownLatch latch) throws InterruptedException { + if (!mMySegmentsHitsCountLatch.await(10, TimeUnit.SECONDS)) { + Logger.e("MySegments hits not received"); + fail(); + } + + if (!mSplitsHitsCountLatch.await(10, TimeUnit.SECONDS)) { + Logger.e("Splits hits not received"); + fail(); + } + + if (!latch.await(10, TimeUnit.SECONDS)) { + Logger.e("SDK_READY event not received"); + } + } + + private HttpResponseMock createResponse(String data) { + return new HttpResponseMock(200, data); + } + + private HttpStreamResponseMock createStreamResponse(BlockingQueue streamingResponseData) throws IOException { + return new HttpStreamResponseMock(200, streamingResponseData); + } + + private HttpResponseMockDispatcher createStreamingResponseDispatcher(final String sseResponse) { + return new HttpResponseMockDispatcher() { + @Override + public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { + if (uri.getPath().contains("mySegments")) { + mMySegmentsHitsCountLatch.countDown(); + return createResponse(IntegrationHelper.dummyMySegments()); + } else if (uri.getPath().contains("/splitChanges")) { + mSplitsHitsCountLatch.countDown(); + mSplitsHitsCountHit.incrementAndGet(); + String data = IntegrationHelper.emptySplitChanges(-1, 1000); + return createResponse(data); + } else if (uri.getPath().contains("/auth")) { + mSseAuthHits.incrementAndGet(); + return createResponse(sseResponse); + } else { + return new HttpResponseMock(200); + } + } + + @Override + public HttpStreamResponseMock getStreamResponse(URI uri) { + try { + mIsStreamingConnected.countDown(); + return createStreamResponse(mStreamingData); + } catch (Exception e) { + } + return null; + } + }; + } + + private void pushMessage(String message) { + + try { + mStreamingData.put(message + "" + "\n"); + + Logger.d("Pushed message: " + message); + } catch (InterruptedException e) { + } + } +} diff --git a/src/androidTest/java/tests/integration/streaming/SyncGuardianIntegrationTest.java b/src/androidTest/java/tests/integration/streaming/SyncGuardianIntegrationTest.java new file mode 100644 index 000000000..38918e8cb --- /dev/null +++ b/src/androidTest/java/tests/integration/streaming/SyncGuardianIntegrationTest.java @@ -0,0 +1,327 @@ +package tests.integration.streaming; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.content.Context; + +import androidx.core.util.Pair; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import fake.HttpStreamResponseMock; +import fake.LifecycleManagerStub; +import helper.IntegrationHelper; +import helper.SplitEventTaskHelper; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.network.HttpMethod; +import io.split.android.client.utils.logger.Logger; + +public class SyncGuardianIntegrationTest { + + private Context mContext; + private BlockingQueue mStreamingData; + private CountDownLatch mMySegmentsHitsCountLatch; + private CountDownLatch mSplitsHitsCountLatch; + + private CountDownLatch mIsStreamingConnected; + private AtomicInteger mSplitsHitsCountHit; + private AtomicInteger mSseAuthHits; + private final LifecycleManagerStub mLifecycleManager = new LifecycleManagerStub(); + + @Before + public void setup() { + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + mStreamingData = new LinkedBlockingDeque<>(); + mSplitsHitsCountLatch = new CountDownLatch(1); + mMySegmentsHitsCountLatch = new CountDownLatch(1); + mIsStreamingConnected = new CountDownLatch(1); + mSplitsHitsCountHit = new AtomicInteger(0); + mSseAuthHits = new AtomicInteger(0); + } + + @Test + public void splitsAreNotFetchedWhenSSEConnectionIsActive() throws IOException, InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + + Pair pair = getClient(latch, true, false, IntegrationHelper.streamingEnabledToken()); + SplitClient client = pair.first; + SplitEventTaskHelper readyTask = pair.second; + + awaitInitialization(latch); + + boolean sseConnectionAwait = mIsStreamingConnected.await(10, TimeUnit.SECONDS); + int initialSplitsHit = mSplitsHitsCountHit.get(); + + sendToBackgroundFor(0); + + int finalSplitsHit = mSplitsHitsCountHit.get(); + assertTrue(readyTask.isOnPostExecutionCalled); + assertTrue(sseConnectionAwait); + assertEquals(initialSplitsHit, finalSplitsHit); + + client.destroy(); + } + + @Test + public void splitsAreFetchedWhenSSEConnectionIsNotActiveAndSyncShouldBeTriggered() throws IOException, InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + + Pair pair = getClient(latch, true, false, IntegrationHelper.streamingEnabledToken()); + SplitClient client = pair.first; + SplitEventTaskHelper readyTask = pair.second; + + awaitInitialization(latch); + + boolean sseConnectionAwait = mIsStreamingConnected.await(10, TimeUnit.SECONDS); + int initialSplitsHit = mSplitsHitsCountHit.get(); + + sendToBackgroundFor(3000); + + int finalSplitsHit = mSplitsHitsCountHit.get(); + assertTrue(readyTask.isOnPostExecutionCalled); + assertTrue(sseConnectionAwait); + assertEquals(1, initialSplitsHit); + assertEquals(2, finalSplitsHit); + + client.destroy(); + } + + @Test + public void splitsAreNotFetchedWhenSSEConnectionIsNotActiveAndSyncShouldNotBeTriggered() throws IOException, InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + + Pair pair = getClient(latch, true, false, IntegrationHelper.streamingEnabledToken()); + SplitClient client = pair.first; + SplitEventTaskHelper readyTask = pair.second; + + awaitInitialization(latch); + + boolean sseConnectionAwait = mIsStreamingConnected.await(10, TimeUnit.SECONDS); + int initialSplitsHit = mSplitsHitsCountHit.get(); + + sendToBackgroundFor(1000); + + Thread.sleep(200); + int finalSplitsHit = mSplitsHitsCountHit.get(); + assertTrue(readyTask.isOnPostExecutionCalled); + assertTrue(sseConnectionAwait); + assertEquals(initialSplitsHit, finalSplitsHit); + + client.destroy(); + } + + @Test + public void changeInStreamingDelayAffectsSyncBehaviour() throws IOException, InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + + Pair pair = getClient(latch, + true, + false, + IntegrationHelper.streamingEnabledToken(6), + 1L, + 0L); + SplitClient client = pair.first; + SplitEventTaskHelper readyTask = pair.second; + + awaitInitialization(latch); + + boolean sseConnectionAwait = mIsStreamingConnected.await(20, TimeUnit.SECONDS); + int initialSplitsHit = mSplitsHitsCountHit.get(); + + sendToBackgroundFor(3000); + int secondSplitsHit = mSplitsHitsCountHit.get(); + + sendToBackgroundFor(3000); + int thirdSplitsHit = mSplitsHitsCountHit.get(); + + Thread.sleep(5000); + + assertTrue(readyTask.isOnPostExecutionCalled); + assertTrue(sseConnectionAwait); + assertEquals(1, initialSplitsHit); + assertEquals(2, secondSplitsHit); + assertEquals(2, thirdSplitsHit); + + client.destroy(); + } + + @Test + public void splitsAreNotFetchedWhenSSEConnectionIsInactiveAndTimeHasNotElapsed() throws IOException, InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + + Pair pair = getClient(latch, true, false, IntegrationHelper.streamingEnabledToken()); + SplitClient client = pair.first; + SplitEventTaskHelper readyTask = pair.second; + + awaitInitialization(latch); + + boolean sseConnectionAwait = mIsStreamingConnected.await(10, TimeUnit.SECONDS); + int initialSplitsHit = mSplitsHitsCountHit.get(); + + sendToBackgroundFor(0); + + int finalSplitsHit = mSplitsHitsCountHit.get(); + assertTrue(readyTask.isOnPostExecutionCalled); + assertTrue(sseConnectionAwait); + assertEquals(initialSplitsHit, finalSplitsHit); + + client.destroy(); + } + + @Test + public void splitsAreNotFetchedOnResumeWhenStreamingIsDisabled() throws InterruptedException, IOException { + CountDownLatch latch = new CountDownLatch(1); + + Pair pair = getClient(latch, false, false, IntegrationHelper.streamingEnabledToken()); + SplitClient client = pair.first; + SplitEventTaskHelper readyTask = pair.second; + + awaitInitialization(latch); + + int initialSplitsHit = mSplitsHitsCountHit.get(); + + sendToBackgroundFor(0); + + int finalSplitsHit = mSplitsHitsCountHit.get(); + assertTrue(readyTask.isOnPostExecutionCalled); + assertEquals(0, mSseAuthHits.get()); + assertEquals(initialSplitsHit, finalSplitsHit); + + client.destroy(); + } + + @Test + public void splitsAreNotFetchedOnResumeWhenSingleSyncIsEnabled() throws InterruptedException, IOException { + CountDownLatch latch = new CountDownLatch(1); + + Pair pair = getClient(latch, false, true, IntegrationHelper.streamingEnabledToken()); + SplitClient client = pair.first; + SplitEventTaskHelper readyTask = pair.second; + + awaitInitialization(latch); + + int initialSplitsHit = mSplitsHitsCountHit.get(); + + sendToBackgroundFor(0); + + int finalSplitsHit = mSplitsHitsCountHit.get(); + assertTrue(readyTask.isOnPostExecutionCalled); + assertEquals(0, mSseAuthHits.get()); + assertEquals(initialSplitsHit, finalSplitsHit); + + client.destroy(); + } + + private Pair getClient(CountDownLatch latch, boolean streamingEnabled, boolean singleSync, String sseResponse) throws IOException { + return getClient(latch, streamingEnabled, singleSync, sseResponse, 2L, 2L); + } + + private Pair getClient(CountDownLatch latch, boolean streamingEnabled, boolean singleSync, String sseResponse, long defaultConnectionDelay, long disconnectionDelay) throws IOException { + HttpClientMock httpClientMock = new HttpClientMock(createStreamingResponseDispatcher(sseResponse)); + + SplitClientConfig config = (singleSync) ? IntegrationHelper.syncDisabledConfig() : IntegrationHelper.customSseConnectionDelayConfig(streamingEnabled, defaultConnectionDelay, disconnectionDelay); + + SplitFactory splitFactory = IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + IntegrationHelper.dummyUserKey(), + config, + mContext, + httpClientMock, + null, + null, + null, + mLifecycleManager); + + SplitClient client = splitFactory.client(); + SplitEventTaskHelper readyTask = new SplitEventTaskHelper(latch); + client.on(SplitEvent.SDK_READY, readyTask); + + return new Pair<>(client, readyTask); + } + + private void awaitInitialization(CountDownLatch latch) throws InterruptedException { + boolean mySegmentsAwait = mMySegmentsHitsCountLatch.await(10, TimeUnit.SECONDS); + if (!mySegmentsAwait) { + Logger.e("MySegments hits not received"); + fail(); + } + + boolean splitsAwait = mSplitsHitsCountLatch.await(10, TimeUnit.SECONDS); + if (!splitsAwait) { + Logger.e("Splits hits not received"); + fail(); + } + + boolean readyAwait = latch.await(10, TimeUnit.SECONDS); + if (!readyAwait) { + Logger.e("SDK_READY event not received"); + } + } + + private HttpResponseMock createResponse(String data) { + return new HttpResponseMock(200, data); + } + + private HttpStreamResponseMock createStreamResponse(BlockingQueue streamingResponseData) throws IOException { + return new HttpStreamResponseMock(200, streamingResponseData); + } + + private HttpResponseMockDispatcher createStreamingResponseDispatcher(final String sseResponse) { + return new HttpResponseMockDispatcher() { + @Override + public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { + if (uri.getPath().contains("mySegments")) { + mMySegmentsHitsCountLatch.countDown(); + return createResponse(IntegrationHelper.dummyMySegments()); + } else if (uri.getPath().contains("/splitChanges")) { + mSplitsHitsCountLatch.countDown(); + mSplitsHitsCountHit.incrementAndGet(); + String data = IntegrationHelper.emptySplitChanges(-1, 1000); + return createResponse(data); + } else if (uri.getPath().contains("/auth")) { + mSseAuthHits.incrementAndGet(); + return createResponse(sseResponse); + } else { + return new HttpResponseMock(200); + } + } + + @Override + public HttpStreamResponseMock getStreamResponse(URI uri) { + try { + mIsStreamingConnected.countDown(); + return createStreamResponse(mStreamingData); + } catch (Exception e) { + } + return null; + } + }; + } + + private void sendToBackgroundFor(int millis) throws InterruptedException { + mLifecycleManager.simulateOnPause(); + Logger.i("Test: sending app to background for " + millis + " millis"); + Thread.sleep(millis); + mLifecycleManager.simulateOnResume(); + Logger.i("Test: resuming app from background after " + millis + " millis"); + Thread.sleep(500); + } +} diff --git a/src/main/java/io/split/android/client/SplitClientConfig.java b/src/main/java/io/split/android/client/SplitClientConfig.java index 3247a8f17..22f79f417 100644 --- a/src/main/java/io/split/android/client/SplitClientConfig.java +++ b/src/main/java/io/split/android/client/SplitClientConfig.java @@ -117,6 +117,8 @@ public class SplitClientConfig { private int mLogLevel = SplitLogLevel.NONE; private UserConsent mUserConsent; private boolean mEncryptionEnabled = false; + private final long mDefaultSSEConnectionDelayInSecs; + private final long mSSEDisconnectionDelayInSecs; // To be set during startup public static String splitSdkVersion; @@ -168,7 +170,9 @@ private SplitClientConfig(String endpoint, int mtkPerPush, int mtkRefreshRate, UserConsent userConsent, - boolean encryptionEnabled) { + boolean encryptionEnabled, + long defaultSSEConnectionDelayInSecs, + long sseDisconnectionDelayInSecs) { mEndpoint = endpoint; mEventsEndpoint = eventsEndpoint; mTelemetryEndpoint = telemetryEndpoint; @@ -220,6 +224,8 @@ private SplitClientConfig(String endpoint, mMtkPerPush = mtkPerPush; mEncryptionEnabled = encryptionEnabled; + mDefaultSSEConnectionDelayInSecs = defaultSSEConnectionDelayInSecs; + mSSEDisconnectionDelayInSecs = sseDisconnectionDelayInSecs; Logger.instance().setLevel(mLogLevel); } @@ -437,6 +443,14 @@ public boolean encryptionEnabled() { return mEncryptionEnabled; } + public long defaultSSEConnectionDelay() { + return mDefaultSSEConnectionDelayInSecs; + } + + public long sseDisconnectionDelay() { + return mSSEDisconnectionDelayInSecs; + } + private void enableTelemetry() { mShouldRecordTelemetry = true; } public static final class Builder { @@ -503,6 +517,10 @@ public static final class Builder { private boolean mEncryptionEnabled = false; + private final long mDefaultSSEConnectionDelayInSecs = ServiceConstants.DEFAULT_SSE_CONNECTION_DELAY_SECS; + + private final long mSSEDisconnectionDelayInSecs = 60L; + public Builder() { mServiceEndpoints = ServiceEndpoints.builder().build(); } @@ -1107,7 +1125,9 @@ public SplitClientConfig build() { mMtkPerPush, mMtkRefreshRate, mUserConsent, - mEncryptionEnabled); + mEncryptionEnabled, + mDefaultSSEConnectionDelayInSecs, + mSSEDisconnectionDelayInSecs); } 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 353cc750e..91489591d 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -3,6 +3,7 @@ import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.work.WorkManager; import java.io.File; @@ -45,6 +46,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.SyncGuardian; +import io.split.android.client.service.synchronizer.SyncGuardianImpl; import io.split.android.client.service.synchronizer.SyncManager; import io.split.android.client.service.synchronizer.SyncManagerImpl; import io.split.android.client.service.synchronizer.Synchronizer; @@ -56,16 +59,17 @@ import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerRegistry; 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; import io.split.android.client.storage.common.SplitStorageContainer; -import io.split.android.client.storage.attributes.PersistentAttributesStorage; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.storage.db.StorageFactory; import io.split.android.client.storage.events.PersistentEventsStorage; import io.split.android.client.storage.impressions.PersistentImpressionsStorage; +import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.TelemetrySynchronizer; import io.split.android.client.telemetry.TelemetrySynchronizerImpl; import io.split.android.client.telemetry.TelemetrySynchronizerStub; @@ -198,37 +202,42 @@ SyncManager buildSyncManager(SplitClientConfig config, SplitTaskExecutor splitTaskExecutor, Synchronizer synchronizer, TelemetrySynchronizer telemetrySynchronizer, - PushNotificationManager pushNotificationManager, - BlockingQueue splitsUpdateNotificationQueue, - PushManagerEventBroadcaster pushManagerEventBroadcaster) { + @Nullable PushNotificationManager pushNotificationManager, + @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster, + @Nullable SplitUpdatesWorker splitUpdatesWorker, + @Nullable SyncGuardian syncGuardian) { + - SplitUpdatesWorker updateWorker = null; BackoffCounterTimer backoffCounterTimer = null; if (config.syncEnabled()) { - updateWorker = new SplitUpdatesWorker(synchronizer, splitsUpdateNotificationQueue); backoffCounterTimer = new BackoffCounterTimer(splitTaskExecutor, new ReconnectBackoffCounter(1)); } return new SyncManagerImpl(config, synchronizer, pushNotificationManager, - updateWorker, + splitUpdatesWorker, pushManagerEventBroadcaster, backoffCounterTimer, + syncGuardian, telemetrySynchronizer); } @NonNull - PushNotificationManager getPushNotificationManager(SplitTaskExecutor _splitTaskExecutor, + PushNotificationManager getPushNotificationManager(SplitTaskExecutor splitTaskExecutor, SseAuthenticator sseAuthenticator, PushManagerEventBroadcaster pushManagerEventBroadcaster, SseClient sseClient, - TelemetryRuntimeProducer telemetryRuntimeProducer) { + TelemetryRuntimeProducer telemetryRuntimeProducer, + long defaultSseConnectionDelayInSecs, + long sseDisconnectionDelayInSecs) { return new PushNotificationManager(pushManagerEventBroadcaster, sseAuthenticator, sseClient, - new SseRefreshTokenTimer(_splitTaskExecutor, pushManagerEventBroadcaster), + new SseRefreshTokenTimer(splitTaskExecutor, pushManagerEventBroadcaster), telemetryRuntimeProducer, + defaultSseConnectionDelayInSecs, + sseDisconnectionDelayInSecs, null); } @@ -270,7 +279,8 @@ public ClientComponentsRegisterImpl getClientComponentsRegister(SplitClientConfi NotificationProcessor notificationProcessor, SseAuthenticator sseAuthenticator, SplitStorageContainer storageContainer, - SyncManager syncManager) { + SyncManager syncManager, + CompressionUtilProvider compressionProvider) { MySegmentsV2PayloadDecoder mySegmentsV2PayloadDecoder = new MySegmentsV2PayloadDecoder(); PersistentAttributesStorage attributesStorage = null; @@ -287,7 +297,7 @@ public ClientComponentsRegisterImpl getClientComponentsRegister(SplitClientConfi mySegmentsNotificationProcessorFactory = new MySegmentsNotificationProcessorFactoryImpl(notificationParser, taskExecutor, mySegmentsV2PayloadDecoder, - new CompressionUtilProvider()); + compressionProvider); } return new ClientComponentsRegisterImpl( @@ -316,6 +326,7 @@ public StreamingComponents buildStreamingComponents(@NonNull SplitTaskExecutor s if (!config.syncEnabled()) { return new StreamingComponents(); } + BlockingQueue splitsUpdateNotificationQueue = new LinkedBlockingDeque<>(); NotificationParser notificationParser = new NotificationParser(); @@ -338,14 +349,19 @@ public StreamingComponents buildStreamingComponents(@NonNull SplitTaskExecutor s sseAuthenticator, pushManagerEventBroadcaster, sseClient, - storageContainer.getTelemetryStorage()); + storageContainer.getTelemetryStorage(), + config.defaultSSEConnectionDelay(), + config.sseDisconnectionDelay()); + + SyncGuardian syncGuardian = new SyncGuardianImpl(config); return new StreamingComponents(pushNotificationManager, splitsUpdateNotificationQueue, notificationParser, notificationProcessor, sseAuthenticator, - pushManagerEventBroadcaster); + pushManagerEventBroadcaster, + syncGuardian); } public ProcessStrategy getImpressionStrategy(SplitTaskExecutor splitTaskExecutor, @@ -381,6 +397,26 @@ SplitCipher migrateEncryption(String apiKey, return toCipher; } + @Nullable + SplitUpdatesWorker getSplitUpdatesWorker(SplitClientConfig config, + SplitTaskExecutor splitTaskExecutor, + SplitTaskFactory splitTaskFactory, + Synchronizer mSynchronizer, + BlockingQueue splitsUpdateNotificationQueue, + SplitsStorage splitsStorage, + CompressionUtilProvider compressionProvider) { + if (config.syncEnabled()) { + return new SplitUpdatesWorker(mSynchronizer, + splitsUpdateNotificationQueue, + splitsStorage, + compressionProvider, + splitTaskExecutor, + splitTaskFactory); + } + + return null; + } + private TelemetryStorage getTelemetryStorage(boolean shouldRecordTelemetry, TelemetryStorage telemetryStorage) { if (telemetryStorage != null) { return telemetryStorage; diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index cc1b6365f..215e596ba 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -9,6 +9,7 @@ import java.util.List; 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; @@ -30,6 +31,7 @@ import io.split.android.client.service.impressions.ImpressionManager; import io.split.android.client.service.impressions.StrategyImpressionManager; import io.split.android.client.service.sseclient.sseclient.StreamingComponents; +import io.split.android.client.service.synchronizer.FeatureFlagsSynchronizerImpl; import io.split.android.client.service.synchronizer.SyncManager; import io.split.android.client.service.synchronizer.Synchronizer; import io.split.android.client.service.synchronizer.SynchronizerImpl; @@ -182,26 +184,31 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { SplitSingleThreadTaskExecutor splitSingleThreadTaskExecutor = new SplitSingleThreadTaskExecutor(); ImpressionManager impressionManager = new StrategyImpressionManager(factoryHelper.getImpressionStrategy(splitTaskExecutor, splitTaskFactory, mStorageContainer, config)); + final RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory = new RetryBackoffCounterTimerFactory(); + + StreamingComponents streamingComponents = factoryHelper.buildStreamingComponents(splitTaskExecutor, + splitTaskFactory, config, defaultHttpClient, splitApiFacade, mStorageContainer); Synchronizer mSynchronizer = new SynchronizerImpl( config, splitTaskExecutor, splitSingleThreadTaskExecutor, - mStorageContainer, splitTaskFactory, - mEventsManagerCoordinator, workManagerWrapper, - new RetryBackoffCounterTimerFactory(), + retryBackoffCounterTimerFactory, mStorageContainer.getTelemetryStorage(), new AttributesSynchronizerRegistryImpl(), new MySegmentsSynchronizerRegistryImpl(), - impressionManager); + impressionManager, + mStorageContainer.getEventsStorage(), + mEventsManagerCoordinator, + streamingComponents.getPushManagerEventBroadcaster()); // Only available for integration tests if (synchronizerSpy != null) { synchronizerSpy.setSynchronizer(mSynchronizer); mSynchronizer = synchronizerSpy; } - StreamingComponents streamingComponents = factoryHelper.buildStreamingComponents(splitTaskExecutor, - splitTaskFactory, config, defaultHttpClient, splitApiFacade, mStorageContainer); + + CompressionUtilProvider compressionProvider = new CompressionUtilProvider(); TelemetrySynchronizer telemetrySynchronizer = factoryHelper.getTelemetrySynchronizer(splitTaskExecutor, splitTaskFactory, config.telemetryRefreshRate(), config.shouldRecordTelemetry()); @@ -212,9 +219,15 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { mSynchronizer, telemetrySynchronizer, streamingComponents.getPushNotificationManager(), - streamingComponents.getSplitsUpdateNotificationQueue(), - streamingComponents.getPushManagerEventBroadcaster() - ); + streamingComponents.getPushManagerEventBroadcaster(), + factoryHelper.getSplitUpdatesWorker(config, + splitTaskExecutor, + splitTaskFactory, + mSynchronizer, + streamingComponents.getSplitsUpdateNotificationQueue(), + mStorageContainer.getSplitsStorage(), + compressionProvider), + streamingComponents.getSyncGuardian()); if (testLifecycleManager == null) { mLifecycleManager = new SplitLifecycleManagerImpl(); @@ -241,10 +254,11 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { mStorageContainer.getImpressionsStorage(), mStorageContainer.getEventsStorage(), mSyncManager, eventsTracker, impressionManager, splitTaskExecutor); + ClientComponentsRegister componentsRegister = factoryHelper.getClientComponentsRegister(config, splitTaskExecutor, mEventsManagerCoordinator, mSynchronizer, streamingComponents.getNotificationParser(), streamingComponents.getNotificationProcessor(), streamingComponents.getSseAuthenticator(), - mStorageContainer, mSyncManager); + mStorageContainer, mSyncManager, compressionProvider); mClientContainer = new SplitClientContainerImpl( mDefaultClientKey.matchingKey(), this, config, mSyncManager, telemetrySynchronizer, mStorageContainer, splitTaskExecutor, splitApiFacade, diff --git a/src/main/java/io/split/android/client/common/CompressionUtilProvider.java b/src/main/java/io/split/android/client/common/CompressionUtilProvider.java index 11d0787dd..d6b8721c6 100644 --- a/src/main/java/io/split/android/client/common/CompressionUtilProvider.java +++ b/src/main/java/io/split/android/client/common/CompressionUtilProvider.java @@ -24,6 +24,13 @@ public CompressionUtil get(CompressionType type) { @Nullable private CompressionUtil create(CompressionType type) { switch (type) { + case NONE: + return new CompressionUtil() { + @Override + public byte[] decompress(byte[] compressed) { + return compressed; + } + }; case GZIP: return new Gzip(); case ZLIB: 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 f658436d6..e23407f55 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 @@ -6,6 +6,7 @@ import io.split.android.client.service.impressions.ImpressionsTaskFactory; import io.split.android.client.service.splits.FilterSplitsInCacheTask; import io.split.android.client.service.splits.LoadSplitsTask; +import io.split.android.client.service.splits.SplitInPlaceUpdateTask; import io.split.android.client.service.splits.SplitKillTask; import io.split.android.client.service.splits.SplitsSyncTask; import io.split.android.client.service.splits.SplitsUpdateTask; @@ -23,6 +24,8 @@ public interface SplitTaskFactory extends TelemetryTaskFactory, ImpressionsTaskF SplitsUpdateTask createSplitsUpdateTask(long since); + SplitInPlaceUpdateTask createSplitsUpdateTask(Split featureFlag, long since); + FilterSplitsInCacheTask createFilterSplitsInCacheTask(); CleanUpDatabaseTask createCleanUpDatabaseTask(long maxTimestamp); 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 217e0d221..0bee62e7c 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 @@ -2,6 +2,8 @@ import static com.google.common.base.Preconditions.checkNotNull; +import android.annotation.SuppressLint; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -31,6 +33,7 @@ import io.split.android.client.service.splits.FilterSplitsInCacheTask; import io.split.android.client.service.splits.LoadSplitsTask; import io.split.android.client.service.splits.SplitChangeProcessor; +import io.split.android.client.service.splits.SplitInPlaceUpdateTask; import io.split.android.client.service.splits.SplitKillTask; import io.split.android.client.service.splits.SplitsSyncHelper; import io.split.android.client.service.splits.SplitsSyncTask; @@ -41,6 +44,8 @@ import io.split.android.client.service.telemetry.TelemetryTaskFactory; import io.split.android.client.service.telemetry.TelemetryTaskFactoryImpl; import io.split.android.client.storage.common.SplitStorageContainer; +import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; +import io.split.android.client.telemetry.storage.TelemetryStorage; public class SplitTaskFactoryImpl implements SplitTaskFactory { @@ -51,7 +56,10 @@ public class SplitTaskFactoryImpl implements SplitTaskFactory { private final String mSplitsFilterQueryString; private final ISplitEventsManager mEventsManager; private final TelemetryTaskFactory mTelemetryTaskFactory; + private final SplitChangeProcessor mSplitChangeProcessor; + private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; + @SuppressLint("VisibleForTests") public SplitTaskFactoryImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull SplitApiFacade splitApiFacade, @NonNull SplitStorageContainer splitStorageContainer, @@ -64,23 +72,26 @@ public SplitTaskFactoryImpl(@NonNull SplitClientConfig splitClientConfig, mSplitsStorageContainer = checkNotNull(splitStorageContainer); mSplitsFilterQueryString = splitsFilterQueryString; mEventsManager = eventsManager; + mSplitChangeProcessor = new SplitChangeProcessor(); + TelemetryStorage telemetryStorage = mSplitsStorageContainer.getTelemetryStorage(); + mTelemetryRuntimeProducer = telemetryStorage; if (testingConfig != null) { mSplitsSyncHelper = new SplitsSyncHelper(mSplitApiFacade.getSplitFetcher(), mSplitsStorageContainer.getSplitsStorage(), - new SplitChangeProcessor(), - mSplitsStorageContainer.getTelemetryStorage(), + mSplitChangeProcessor, + mTelemetryRuntimeProducer, new ReconnectBackoffCounter(1, testingConfig.getCdnBackoffTime())); } else { mSplitsSyncHelper = new SplitsSyncHelper(mSplitApiFacade.getSplitFetcher(), mSplitsStorageContainer.getSplitsStorage(), new SplitChangeProcessor(), - mSplitsStorageContainer.getTelemetryStorage()); + mTelemetryRuntimeProducer); } mTelemetryTaskFactory = new TelemetryTaskFactoryImpl(mSplitApiFacade.getTelemetryConfigRecorder(), mSplitApiFacade.getTelemetryStatsRecorder(), - mSplitsStorageContainer.getTelemetryStorage(), + telemetryStorage, splitClientConfig, mSplitsStorageContainer.getSplitsStorage(), mSplitsStorageContainer.getMySegmentsStorageContainer()); @@ -181,4 +192,9 @@ public TelemetryConfigRecorderTask getTelemetryConfigRecorderTask() { public TelemetryStatsRecorderTask getTelemetryStatsRecorderTask() { return mTelemetryTaskFactory.getTelemetryStatsRecorderTask(); } + + @Override + public SplitInPlaceUpdateTask createSplitsUpdateTask(Split featureFlag, long since) { + return new SplitInPlaceUpdateTask(mSplitsStorageContainer.getSplitsStorage(), mSplitChangeProcessor, mEventsManager, mTelemetryRuntimeProducer, featureFlag, since); + } } diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryImpl.java b/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryImpl.java index 093402192..c94799e92 100644 --- a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryImpl.java @@ -40,6 +40,6 @@ public MySegmentsOverwriteTask createMySegmentsOverwriteTask(List segmen @Override public MySegmentsUpdateTask createMySegmentsUpdateTask(boolean add, String segmentName) { - return new MySegmentsUpdateTask(mConfiguration.getStorage(), add, segmentName, mConfiguration.getEventsManager()); + return new MySegmentsUpdateTask(mConfiguration.getStorage(), add, segmentName, mConfiguration.getEventsManager(), mTelemetryRuntimeProducer); } } diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java b/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java index 346dea3f7..1b7241700 100644 --- a/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java +++ b/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTask.java @@ -11,6 +11,8 @@ import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.storage.mysegments.MySegmentsStorage; +import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum; +import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; import io.split.android.client.utils.logger.Logger; import static com.google.common.base.Preconditions.checkNotNull; @@ -21,15 +23,18 @@ public class MySegmentsUpdateTask implements SplitTask { private final MySegmentsStorage mMySegmentsStorage; private final SplitEventsManager mEventsManager; private final boolean mIsAddOperation; + private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; public MySegmentsUpdateTask(@NonNull MySegmentsStorage mySegmentsStorage, boolean add, @NonNull String segmentName, - @NonNull SplitEventsManager eventsManager) { + @NonNull SplitEventsManager eventsManager, + @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { mMySegmentsStorage = checkNotNull(mySegmentsStorage); mSegmentName = checkNotNull(segmentName); mIsAddOperation = add; - mEventsManager = eventsManager; + mEventsManager = checkNotNull(eventsManager); + mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); } @Override @@ -48,6 +53,7 @@ private SplitTaskExecutionInfo add() { segments.add(mSegmentName); updateAndNotify(segments); } + mTelemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.MY_SEGMENTS); } catch (Exception e) { logError("Unknown error while adding segment " + mSegmentName + ": " + e.getLocalizedMessage()); return SplitTaskExecutionInfo.error(SplitTaskType.MY_SEGMENTS_UPDATE); @@ -62,6 +68,7 @@ public SplitTaskExecutionInfo remove() { if(segments.remove(mSegmentName)) { updateAndNotify(segments); } + mTelemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.MY_SEGMENTS); } catch (Exception e) { logError("Unknown error while removing segment " + mSegmentName + ": " + e.getLocalizedMessage()); return SplitTaskExecutionInfo.error(SplitTaskType.MY_SEGMENTS_UPDATE); diff --git a/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java b/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java index b59c2012b..38e643af9 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java +++ b/src/main/java/io/split/android/client/service/splits/SplitChangeProcessor.java @@ -1,6 +1,9 @@ package io.split.android.client.service.splits; +import androidx.annotation.NonNull; + import java.util.ArrayList; +import java.util.Collections; import java.util.List; import io.split.android.client.dtos.Split; @@ -14,10 +17,18 @@ public ProcessedSplitChange process(SplitChange splitChange) { return new ProcessedSplitChange(new ArrayList<>(), new ArrayList<>(), -1L, 0); } + return buildProcessedSplitChange(splitChange.splits, splitChange.till); + } + + public ProcessedSplitChange process(Split split, long changeNumber) { + return buildProcessedSplitChange(Collections.singletonList(split), changeNumber); + } + + @NonNull + private static ProcessedSplitChange buildProcessedSplitChange(List featureFlags, long changeNumber) { List activeSplits = new ArrayList<>(); List archivedSplits = new ArrayList<>(); - List splits = splitChange.splits; - for (Split split : splits) { + for (Split split : featureFlags) { if (split.name == null) { continue; } @@ -27,6 +38,7 @@ public ProcessedSplitChange process(SplitChange splitChange) { archivedSplits.add(split); } } - return new ProcessedSplitChange(activeSplits, archivedSplits, splitChange.till, System.currentTimeMillis() / 100); + + return new ProcessedSplitChange(activeSplits, archivedSplits, changeNumber, System.currentTimeMillis() / 100); } } diff --git a/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java b/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java new file mode 100644 index 000000000..e9d072cf6 --- /dev/null +++ b/src/main/java/io/split/android/client/service/splits/SplitInPlaceUpdateTask.java @@ -0,0 +1,60 @@ +package io.split.android.client.service.splits; + +import static com.google.common.base.Preconditions.checkNotNull; + +import androidx.annotation.NonNull; + +import io.split.android.client.dtos.Split; +import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTask; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.storage.splits.ProcessedSplitChange; +import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum; +import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; +import io.split.android.client.utils.logger.Logger; + +public class SplitInPlaceUpdateTask implements SplitTask { + + private final SplitsStorage mSplitsStorage; + private final long mChangeNumber; + private final Split mSplit; + private final SplitChangeProcessor mSplitChangeProcessor; + private final ISplitEventsManager mEventsManager; + private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; + + public SplitInPlaceUpdateTask(@NonNull SplitsStorage splitsStorage, + @NonNull SplitChangeProcessor splitChangeProcessor, + @NonNull ISplitEventsManager eventsManager, + @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, + @NonNull Split split, + long changeNumber) { + mSplitsStorage = checkNotNull(splitsStorage); + mSplitChangeProcessor = checkNotNull(splitChangeProcessor); + mEventsManager = checkNotNull(eventsManager); + mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + mSplit = checkNotNull(split); + mChangeNumber = changeNumber; + } + + @NonNull + @Override + public SplitTaskExecutionInfo execute() { + try { + ProcessedSplitChange processedSplitChange = mSplitChangeProcessor.process(mSplit, mChangeNumber); + mSplitsStorage.update(processedSplitChange); + + mEventsManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + mTelemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + + Logger.d("Updated feature flag: " + mSplit.name); + return SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC); + } catch (Exception ex) { + Logger.e("Could not update feature flag"); + + return SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC); + } + } +} 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 d4108f81c..9693f4875 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 @@ -78,16 +78,16 @@ private SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolea attemptSplitSync(till, clearBeforeUpdate, avoidCache, true, resetChangeNumber); } } catch (HttpFetcherException e) { - logError("Network error while fetching splits" + e.getLocalizedMessage()); + logError("Network error while fetching feature flags" + e.getLocalizedMessage()); mTelemetryRuntimeProducer.recordSyncError(OperationType.SPLITS, e.getHttpStatus()); return SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC); } catch (Exception e) { - logError("Unexpected while fetching splits" + e.getLocalizedMessage()); + logError("Unexpected while fetching feature flags" + e.getLocalizedMessage()); return SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC); } - Logger.d("Features have been updated"); + Logger.d("Feature flags have been updated"); return SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC); } diff --git a/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/DelayStatusEvent.java b/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/DelayStatusEvent.java new file mode 100644 index 000000000..9f8ebbc2a --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/DelayStatusEvent.java @@ -0,0 +1,15 @@ +package io.split.android.client.service.sseclient.feedbackchannel; + +public class DelayStatusEvent extends PushStatusEvent { + + private final long mDelay; + + public DelayStatusEvent(long delay) { + super(EventType.PUSH_DELAY_RECEIVED); + mDelay = delay; + } + + public Long getDelay() { + return mDelay; + } +} diff --git a/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushManagerEventBroadcaster.java b/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushManagerEventBroadcaster.java index 65ec32f5d..de5041209 100644 --- a/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushManagerEventBroadcaster.java +++ b/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushManagerEventBroadcaster.java @@ -8,7 +8,7 @@ public class PushManagerEventBroadcaster { - private List> mListeners; + private final List> mListeners; public PushManagerEventBroadcaster() { mListeners = new CopyOnWriteArrayList<>(); diff --git a/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushStatusEvent.java b/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushStatusEvent.java index 40e0a62be..51b5ed7e3 100644 --- a/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushStatusEvent.java +++ b/src/main/java/io/split/android/client/service/sseclient/feedbackchannel/PushStatusEvent.java @@ -5,23 +5,24 @@ public class PushStatusEvent { * This class represents a message to be pushed in the feedback channel */ - public static enum EventType { + public enum EventType { /*** * Types of messages that can be pushed to the * Synchronization feedback channel */ PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, PUSH_NON_RETRYABLE_ERROR, PUSH_DISABLED, - PUSH_RESET + PUSH_RESET, + SUCCESSFUL_SYNC, + PUSH_DELAY_RECEIVED, } - final private EventType message; + private final EventType mMessage; public PushStatusEvent(EventType message) { - this.message = message; + mMessage = message; } public EventType getMessage() { - return message; + return mMessage; } - } diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentChangeV2Notification.java b/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentChangeV2Notification.java index 9e16b611b..b1e9cc515 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentChangeV2Notification.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentChangeV2Notification.java @@ -11,16 +11,17 @@ public class MySegmentChangeV2Notification extends IncomingNotification { private static final String FIELD_UPDATE_STRATEGY = "u"; private static final String FIELD_COMPRESSION = "c"; private static final String FIELD_DATE = "d"; + private static final String FIELD_CHANGE_NUMBER = "changeNumber"; + private static final String FIELD_SEGMENT_NAME = "segmentName"; + @SerializedName(FIELD_CHANGE_NUMBER) private Long changeNumber; + @SerializedName(FIELD_SEGMENT_NAME) private String segmentName; - @SerializedName(FIELD_COMPRESSION) private CompressionType compression; - @SerializedName(FIELD_UPDATE_STRATEGY) private MySegmentUpdateStrategy updateStrategy; - @SerializedName(FIELD_DATE) private String data; @@ -48,4 +49,4 @@ public MySegmentUpdateStrategy getUpdateStrategy() { public String getData() { return data; } -} \ No newline at end of file +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java index 5e9c619c0..97f63d2d5 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java @@ -16,7 +16,6 @@ public class NotificationParser { private final static String EVENT_TYPE_ERROR = "error"; private static final String EVENT_TYPE_FIELD = "event"; - @NonNull public IncomingNotification parseIncoming(String jsonData) throws JsonSyntaxException { NotificationType type; RawNotification rawNotification = null; @@ -31,7 +30,7 @@ public IncomingNotification parseIncoming(String jsonData) throws JsonSyntaxExce IncomingNotificationType notificationType = Json.fromJson(rawNotification.getData(), IncomingNotificationType.class); type = notificationType.getType(); - if(type == null) { + if (type == null) { type = OCCUPANCY; } } catch (JsonSyntaxException e) { @@ -41,6 +40,7 @@ public IncomingNotification parseIncoming(String jsonData) throws JsonSyntaxExce Logger.e("Unexpected error while parsing incomming notification: " + e.getLocalizedMessage()); return null; } + return new IncomingNotification(type, rawNotification.getChannel(), rawNotification.getData(), rawNotification.getTimestamp()); } diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java index c412c472f..27e9f8080 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java @@ -59,7 +59,7 @@ public void process(IncomingNotification incomingNotification) { processMySegmentUpdateV2(mNotificationParser.parseMySegmentUpdateV2(notificationJson)); break; default: - Logger.e("Unknow notification arrived: " + notificationJson); + Logger.e("Unknown notification arrived: " + notificationJson); } } catch (JsonSyntaxException e) { Logger.e("Error processing incoming push notification: " + @@ -81,6 +81,7 @@ public void unregisterMySegmentsProcessor(String matchingKey) { } private void processSplitUpdate(SplitsChangeNotification notification) { + Logger.d("Received split change notification"); mSplitsUpdateNotificationsQueue.offer(notification); } diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/RawNotification.java b/src/main/java/io/split/android/client/service/sseclient/notifications/RawNotification.java index ab12d11aa..ad74a87a7 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/RawNotification.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/RawNotification.java @@ -1,11 +1,18 @@ package io.split.android.client.service.sseclient.notifications; +import com.google.gson.annotations.SerializedName; + public class RawNotification { + @SerializedName("clientId") private String clientId; + @SerializedName("name") private String name; + @SerializedName("data") private String data; + @SerializedName("channel") private String channel; + @SerializedName("timestamp") private long timestamp; public String getClientId() { diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/SplitKillNotification.java b/src/main/java/io/split/android/client/service/sseclient/notifications/SplitKillNotification.java index 91e16dea0..c5c7e9c2a 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/SplitKillNotification.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/SplitKillNotification.java @@ -1,8 +1,13 @@ package io.split.android.client.service.sseclient.notifications; +import com.google.gson.annotations.SerializedName; + public class SplitKillNotification extends IncomingNotification { + @SerializedName("changeNumber") private long changeNumber; + @SerializedName("splitName") private String splitName; + @SerializedName("defaultTreatment") private String defaultTreatment; public long getChangeNumber() { @@ -16,4 +21,4 @@ public String getSplitName() { public String getDefaultTreatment() { return defaultTreatment; } -} \ No newline at end of file +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java b/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java index af3b968b8..3c22888fe 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java @@ -1,8 +1,31 @@ package io.split.android.client.service.sseclient.notifications; +import androidx.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +import io.split.android.client.common.CompressionType; + public class SplitsChangeNotification extends IncomingNotification { + + @SerializedName("changeNumber") private long changeNumber; + @SerializedName("pcn") + @Nullable + private Long previousChangeNumber; + + @SerializedName("d") + @Nullable + private String data; + + @SerializedName("c") + @Nullable + private Integer compressionType; + + public SplitsChangeNotification() { + + } public SplitsChangeNotification(long changeNumber) { this.changeNumber = changeNumber; @@ -11,5 +34,29 @@ public SplitsChangeNotification(long changeNumber) { public long getChangeNumber() { return changeNumber; } -} + @Nullable + public Long getPreviousChangeNumber() { + return previousChangeNumber; + } + + @Nullable + public String getData() { + return data; + } + + @Nullable + public CompressionType getCompressionType() { + if (compressionType != null) { + if (compressionType == 0) { + return CompressionType.NONE; + } else if (compressionType == 1) { + return CompressionType.GZIP; + } else if (compressionType == 2) { + return CompressionType.ZLIB; + } + } + + return null; + } +} diff --git a/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java b/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java index 3108fe5e0..a4e2f54a6 100644 --- a/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java +++ b/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java @@ -3,11 +3,24 @@ import static com.google.common.base.Preconditions.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import java.util.concurrent.BlockingQueue; +import io.split.android.client.common.CompressionUtilProvider; +import io.split.android.client.dtos.Split; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionListener; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.executor.SplitTaskExecutor; +import io.split.android.client.service.executor.SplitTaskFactory; import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; import io.split.android.client.service.synchronizer.Synchronizer; +import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.client.utils.Base64Util; +import io.split.android.client.utils.CompressionUtil; +import io.split.android.client.utils.Json; import io.split.android.client.utils.logger.Logger; public class SplitUpdatesWorker extends UpdateWorker { @@ -18,24 +31,147 @@ public class SplitUpdatesWorker extends UpdateWorker { private final BlockingQueue mNotificationsQueue; private final Synchronizer mSynchronizer; + private final SplitsStorage mSplitsStorage; + private final CompressionUtilProvider mCompressionUtilProvider; + private final SplitTaskExecutor mSplitTaskExecutor; + private final SplitTaskFactory mSplitTaskFactory; + private final Base64Decoder mBase64Decoder; public SplitUpdatesWorker(@NonNull Synchronizer synchronizer, - @NonNull BlockingQueue notificationsQueue) { + @NonNull BlockingQueue notificationsQueue, + @NonNull SplitsStorage splitsStorage, + @NonNull CompressionUtilProvider compressionUtilProvider, + @NonNull SplitTaskExecutor splitTaskExecutor, + @NonNull SplitTaskFactory splitTaskFactory) { + this(synchronizer, + notificationsQueue, + splitsStorage, + compressionUtilProvider, + splitTaskExecutor, + splitTaskFactory, + new Base64DecoderImpl()); + } + + @VisibleForTesting + public SplitUpdatesWorker(@NonNull Synchronizer synchronizer, + @NonNull BlockingQueue notificationsQueue, + @NonNull SplitsStorage splitsStorage, + @NonNull CompressionUtilProvider compressionUtilProvider, + @NonNull SplitTaskExecutor splitTaskExecutor, + @NonNull SplitTaskFactory splitTaskFactory, + @NonNull Base64Decoder base64Decoder) { super(); mSynchronizer = checkNotNull(synchronizer); mNotificationsQueue = checkNotNull(notificationsQueue); + mSplitsStorage = checkNotNull(splitsStorage); + mCompressionUtilProvider = checkNotNull(compressionUtilProvider); + mSplitTaskExecutor = checkNotNull(splitTaskExecutor); + mSplitTaskFactory = checkNotNull(splitTaskFactory); + mBase64Decoder = checkNotNull(base64Decoder); } @Override protected void onWaitForNotificationLoop() throws InterruptedException { try { SplitsChangeNotification notification = mNotificationsQueue.take(); - mSynchronizer.synchronizeSplits(notification.getChangeNumber()); - Logger.d("A new notification to update splits has been received. " + - "Enqueuing polling task."); + Logger.d("A new notification to update feature flags has been received"); + + long storageChangeNumber = mSplitsStorage.getTill(); + if (notification.getChangeNumber() <= storageChangeNumber) { + Logger.d("Notification change number is lower than the current one. Ignoring notification"); + return; + } + + if (isLegacyNotification(notification) || isInvalidChangeNumber(notification, storageChangeNumber)) { + handleLegacyNotification(notification.getChangeNumber()); + } else { + handleNotification(notification); + } } catch (InterruptedException e) { - Logger.d("Splits update worker has been interrupted"); + Logger.d("Feature flags update worker has been interrupted"); throw (e); } } + + private static boolean isInvalidChangeNumber(SplitsChangeNotification notification, long storageChangeNumber) { + return notification.getPreviousChangeNumber() == null || + notification.getPreviousChangeNumber() == 0 || + storageChangeNumber != notification.getPreviousChangeNumber(); + } + + private static boolean isLegacyNotification(SplitsChangeNotification notification) { + return notification.getData() == null || + notification.getCompressionType() == null; + } + + private void handleLegacyNotification(long changeNumber) { + mSynchronizer.synchronizeSplits(changeNumber); + Logger.d("Enqueuing polling task"); + } + + private void handleNotification(SplitsChangeNotification notification) { + String decompressed = decompressData(notification.getData(), + mCompressionUtilProvider.get(notification.getCompressionType())); + + if (decompressed == null) { + handleLegacyNotification(notification.getChangeNumber()); + return; + } + + try { + Split split = Json.fromJson(decompressed, Split.class); + + mSplitTaskExecutor.submit( + mSplitTaskFactory.createSplitsUpdateTask(split, notification.getChangeNumber()), + new SplitTaskExecutionListener() { + @Override + public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { + if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { + handleLegacyNotification(notification.getChangeNumber()); + } + } + }); + } catch (Exception e) { + Logger.e("Could not parse feature flag"); + handleLegacyNotification(notification.getChangeNumber()); + } + } + + @Nullable + private String decompressData(String data, CompressionUtil compressionUtil) { + try { + if (compressionUtil == null) { + Logger.e("Compression type not supported"); + return null; + } + + byte[] decoded = mBase64Decoder.decode(data); + if (decoded == null) { + Logger.e("Could not decode payload"); + return null; + } + + byte[] decompressed = compressionUtil.decompress(decoded); + if (decompressed == null) { + Logger.e("Decompressed payload is null"); + return null; + } + + return new String(decompressed); + } catch (Exception e) { + Logger.e("Could not decompress payload"); + return null; + } + } + + public interface Base64Decoder { + byte[] decode(String data); + } + + private static class Base64DecoderImpl implements Base64Decoder { + @Override + public byte[] decode(String data) { + return Base64Util.bytesDecode(data); + } + } } diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java index 60a752765..d5c6f07b7 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java @@ -15,11 +15,13 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import io.split.android.client.service.ServiceConstants; +import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; -import io.split.android.client.service.executor.SplitTaskExecutorImpl; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.sseclient.SseJwtToken; +import io.split.android.client.service.sseclient.feedbackchannel.DelayStatusEvent; import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent; import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent.EventType; @@ -44,19 +46,24 @@ public class PushNotificationManager { private final AtomicBoolean mIsPaused; private final AtomicBoolean mIsStopped; private Future mConnectionTask; + private final SplitTask mBackgroundDisconnectionTask; + private final long mDefaultSSEConnectionDelayInSecs; public PushNotificationManager(@NonNull PushManagerEventBroadcaster pushManagerEventBroadcaster, @NonNull SseAuthenticator sseAuthenticator, @NonNull SseClient sseClient, @NonNull SseRefreshTokenTimer refreshTokenTimer, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, + long defaultSSEConnectionDelayInSecs, + long sseDisconnectionDelayInSecs, @Nullable ScheduledExecutorService executorService) { this(pushManagerEventBroadcaster, sseAuthenticator, sseClient, refreshTokenTimer, - new SseDisconnectionTimer(new SplitTaskExecutorImpl()), + new SseDisconnectionTimer(new SplitSingleThreadTaskExecutor(), Math.toIntExact(sseDisconnectionDelayInSecs)), telemetryRuntimeProducer, + defaultSSEConnectionDelayInSecs, executorService); } @@ -67,6 +74,7 @@ public PushNotificationManager(@NonNull PushManagerEventBroadcaster broadcasterC @NonNull SseRefreshTokenTimer refreshTokenTimer, @NonNull SseDisconnectionTimer disconnectionTimer, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, + long defaultSSEConnectionDelayInSecs, @Nullable ScheduledExecutorService executor) { mBroadcasterChannel = checkNotNull(broadcasterChannel); mSseAuthenticator = checkNotNull(sseAuthenticator); @@ -76,7 +84,8 @@ public PushNotificationManager(@NonNull PushManagerEventBroadcaster broadcasterC mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); mIsStopped = new AtomicBoolean(false); mIsPaused = new AtomicBoolean(false); - + mBackgroundDisconnectionTask = new BackgroundDisconnectionTask(mSseClient, mRefreshTokenTimer); + mDefaultSSEConnectionDelayInSecs = defaultSSEConnectionDelayInSecs; if (executor != null) { mExecutor = executor; } else { @@ -91,30 +100,24 @@ public synchronized void start() { } public void pause() { - Logger.d("Push notification manager paused"); mIsPaused.set(true); - mDisconnectionTimer.schedule(new SplitTask() { - @NonNull - @Override - public SplitTaskExecutionInfo execute() { - Logger.d("Disconnecting streaming while in background"); - mSseClient.disconnect(); - mRefreshTokenTimer.cancel(); - return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK); - } - }); + mDisconnectionTimer.schedule(mBackgroundDisconnectionTask); + Logger.d("Push notification manager paused"); } public void resume() { - if (!mIsPaused.get()) { + if (!mIsPaused.compareAndSet(true, false)) { return; } - Logger.d("Push notification manager resumed"); - mIsPaused.set(false); mDisconnectionTimer.cancel(); - if (mSseClient.status() == SseClient.DISCONNECTED && !mIsStopped.get()) { + if (isSseClientDisconnected() && !mIsStopped.get()) { connect(); } + Logger.d("Push notification manager resumed"); + } + + public boolean isSseClientDisconnected() { + return mSseClient.status() == SseClient.DISCONNECTED; } public synchronized void stop() { @@ -138,7 +141,7 @@ public void connect() { if (mConnectionTask != null && (!mConnectionTask.isDone() || !mConnectionTask.isCancelled())) { mConnectionTask.cancel(true); } - mConnectionTask = mExecutor.submit(new StreamingConnection()); + mConnectionTask = mExecutor.submit(new StreamingConnection(mDefaultSSEConnectionDelayInSecs)); } private void shutdownAndAwaitTermination() { @@ -169,55 +172,51 @@ public void uncaughtException(Thread t, Throwable e) { return new ScheduledThreadPoolExecutor(POOL_SIZE, threadFactoryBuilder.build()); } - private class StreamingConnection implements Runnable { + private final long mDefaultSSEConnectionDelayInSecs; + + public StreamingConnection(long defaultSseConnectionDelaySecs) { + mDefaultSSEConnectionDelayInSecs = defaultSseConnectionDelaySecs; + } + @Override public void run() { long startTime = System.currentTimeMillis(); - SseAuthenticationResult authResult = mSseAuthenticator.authenticate(); + SseAuthenticationResult authResult = mSseAuthenticator.authenticate(mDefaultSSEConnectionDelayInSecs); mTelemetryRuntimeProducer.recordSyncLatency(OperationType.TOKEN, System.currentTimeMillis() - startTime); if (authResult.isSuccess() && !authResult.isPushEnabled()) { - Logger.d("Streaming disabled for SDK key"); - mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_SUBSYSTEM_DOWN)); - mIsStopped.set(true); + handlePushDisabled(); return; } if (!authResult.isSuccess() && !authResult.isErrorRecoverable()) { - Logger.d("Streaming no recoverable auth error."); - mTelemetryRuntimeProducer.recordAuthRejections(); - if (authResult.getHttpStatus() != null) { - mTelemetryRuntimeProducer.recordSyncError(OperationType.TOKEN, authResult.getHttpStatus()); - } - mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_NON_RETRYABLE_ERROR)); - mIsStopped.set(true); + handleNonRetryableError(authResult); + recordNonRetryableError(authResult); return; } if (!authResult.isSuccess() && authResult.isErrorRecoverable()) { - mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_RETRYABLE_ERROR)); + handleRetryableError(); return; } SseJwtToken token = authResult.getJwtToken(); if (token == null || token.getChannels() == null || token.getRawJwt() == null) { - Logger.d("Streaming auth error. Retrying"); - mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_RETRYABLE_ERROR)); + handleAuthError(); return; } - mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.TOKEN, System.currentTimeMillis()); - mTelemetryRuntimeProducer.recordStreamingEvents(new TokenRefreshStreamingEvent(token.getExpirationTime(), System.currentTimeMillis())); - mTelemetryRuntimeProducer.recordTokenRefreshes(); + recordSuccessfulSyncAndTokenRefreshes(token); long delay = authResult.getSseConnectionDelay(); // Delay returns false if some error occurs if (delay > 0 && !delay(delay)) { return; } + mBroadcasterChannel.pushMessage(new DelayStatusEvent(delay)); // If host app is in bg or push manager stopped, abort the process if (mIsPaused.get() || mIsStopped.get()) { @@ -233,6 +232,40 @@ public void onConnectionSuccess() { }); } + private void recordSuccessfulSyncAndTokenRefreshes(SseJwtToken token) { + mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.TOKEN, System.currentTimeMillis()); + mTelemetryRuntimeProducer.recordStreamingEvents(new TokenRefreshStreamingEvent(token.getExpirationTime(), System.currentTimeMillis())); + mTelemetryRuntimeProducer.recordTokenRefreshes(); + } + + private void handlePushDisabled() { + Logger.d("Streaming disabled"); + mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_SUBSYSTEM_DOWN)); + mIsStopped.set(true); + } + + private void handleNonRetryableError(SseAuthenticationResult authResult) { + Logger.d("Streaming no recoverable auth error."); + mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_NON_RETRYABLE_ERROR)); + mIsStopped.set(true); + } + + private void recordNonRetryableError(SseAuthenticationResult authResult) { + mTelemetryRuntimeProducer.recordAuthRejections(); + if (authResult.getHttpStatus() != null) { + mTelemetryRuntimeProducer.recordSyncError(OperationType.TOKEN, authResult.getHttpStatus()); + } + } + + private void handleAuthError() { + Logger.d("Streaming auth error. Retrying"); + handleRetryableError(); + } + + private void handleRetryableError() { + mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_RETRYABLE_ERROR)); + } + private boolean delay(long seconds) { try { sleep(seconds * 1000L); @@ -243,4 +276,25 @@ private boolean delay(long seconds) { return true; } } + + public static class BackgroundDisconnectionTask implements SplitTask { + + private final SseClient mSseClient; + private final SseRefreshTokenTimer mRefreshTokenTimer; + + @VisibleForTesting + public BackgroundDisconnectionTask(SseClient sseClient, SseRefreshTokenTimer refreshTokenTimer) { + mSseClient = sseClient; + mRefreshTokenTimer = refreshTokenTimer; + } + + @NonNull + @Override + public SplitTaskExecutionInfo execute() { + Logger.d("Disconnecting streaming while in background"); + mSseClient.disconnect(); + mRefreshTokenTimer.cancel(); + return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK); + } + } } diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java index 16c1142c6..abde8ca3c 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseAuthenticator.java @@ -8,7 +8,6 @@ import java.util.Map; import java.util.Set; -import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.sseclient.InvalidJwtTokenException; @@ -31,7 +30,7 @@ public SseAuthenticator(@NonNull HttpFetcher authFetc mJwtParser = checkNotNull(jwtParser); } - public SseAuthenticationResult authenticate() { + public SseAuthenticationResult authenticate(long defaultSseConnectionDelaySecs) { SseAuthenticationResponse authResponse; try { Map params = new HashMap<>(); @@ -62,7 +61,8 @@ public SseAuthenticationResult authenticate() { } try { - long sseConnectionDelay = authResponse.getSseConnectionDelay() != null ? authResponse.getSseConnectionDelay().longValue() : ServiceConstants.DEFAULT_SSE_CONNECTION_DELAY_SECS; + long sseConnectionDelay = authResponse.getSseConnectionDelay() != null ? authResponse.getSseConnectionDelay() : defaultSseConnectionDelaySecs; + Logger.d("SSE token parsed successfully"); return new SseAuthenticationResult(true, true, true, sseConnectionDelay, mJwtParser.parse(authResponse.getToken())); } catch (InvalidJwtTokenException e) { diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClient.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClient.java index 809214766..62f190a86 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClient.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClient.java @@ -4,9 +4,9 @@ public interface SseClient { - final static int CONNECTING = 0; - final static int DISCONNECTED = 2; - final static int CONNECTED = 1; + int CONNECTING = 0; + int CONNECTED = 1; + int DISCONNECTED = 2; int status(); @@ -14,7 +14,7 @@ public interface SseClient { void connect(SseJwtToken token, ConnectionListener connectionListener); - public static interface ConnectionListener { - public void onConnectionSuccess(); + interface ConnectionListener { + void onConnectionSuccess(); } } diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java index e31d62de4..c94739e61 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseClientImpl.java @@ -25,15 +25,14 @@ public class SseClientImpl implements SseClient { private final URI mTargetUrl; - private AtomicInteger mStatus; + private final AtomicInteger mStatus; private final HttpClient mHttpClient; - private EventStreamParser mEventStreamParser; - private AtomicBoolean isDisconnectCalled; - private SseHandler mSseHandler; + private final EventStreamParser mEventStreamParser; + private final AtomicBoolean mIsDisconnectCalled; + private final SseHandler mSseHandler; private final StringHelper mStringHelper; - private BufferedReader mBufferedReader; private HttpStreamRequest mHttpStreamRequest = null; private static final String PUSH_NOTIFICATION_CHANNELS_PARAM = "channel"; @@ -50,18 +49,19 @@ public SseClientImpl(@NonNull URI uri, mEventStreamParser = checkNotNull(eventStreamParser); mSseHandler = checkNotNull(sseHandler); mStatus = new AtomicInteger(DISCONNECTED); - isDisconnectCalled = new AtomicBoolean(false); + mIsDisconnectCalled = new AtomicBoolean(false); mStringHelper = new StringHelper(); mStatus.set(DISCONNECTED); } + @Override public int status() { return mStatus.get(); } @Override public void disconnect() { - isDisconnectCalled.set(true); + mIsDisconnectCalled.set(true); close(); } @@ -77,13 +77,13 @@ private void close() { @Override public void connect(SseJwtToken token, ConnectionListener connectionListener) { - boolean isConnectionConfirmed = false; - isDisconnectCalled.set(false); + mIsDisconnectCalled.set(false); mStatus.set(CONNECTING); + boolean isConnectionConfirmed = false; String channels = mStringHelper.join(",", token.getChannels()); String rawToken = token.getRawJwt(); boolean isErrorRetryable = true; - mBufferedReader = null; + BufferedReader bufferedReader = null; try { URI url = new URIBuilder(mTargetUrl) .addParameter(PUSH_NOTIFICATION_VERSION_PARAM, PUSH_NOTIFICATION_VERSION_VALUE) @@ -93,13 +93,13 @@ public void connect(SseJwtToken token, ConnectionListener connectionListener) { mHttpStreamRequest = mHttpClient.streamRequest(url); HttpStreamResponse response = mHttpStreamRequest.execute(); if (response.isSuccess()) { - mBufferedReader = response.getBufferedReader(); - if (mBufferedReader != null) { + bufferedReader = response.getBufferedReader(); + if (bufferedReader != null) { Logger.d("Streaming connection opened"); mStatus.set(CONNECTED); String inputLine; Map values = new HashMap<>(); - while ((inputLine = mBufferedReader.readLine()) != null) { + while ((inputLine = bufferedReader.readLine()) != null) { if (mEventStreamParser.parseLineAndAppendValue(inputLine, values)) { if(!isConnectionConfirmed) { if(mEventStreamParser.isKeepAlive(values) || mSseHandler.isConnectionConfirmed(values)) { @@ -127,16 +127,16 @@ public void connect(SseJwtToken token, ConnectionListener connectionListener) { isErrorRetryable = !response.isClientRelatedError(); } } catch (URISyntaxException e) { - logError("An error has ocurred while creating stream Url ", e); + logError("An error has occurred while creating stream Url ", e); isErrorRetryable = false; } catch (IOException e) { - logError("An error has ocurred while parsing stream from: ", e); + logError("An error has occurred while parsing stream from: ", e); isErrorRetryable = true; } catch (Exception e) { - logError("An unexpected error has ocurred while receiving stream events from: ", e); + logError("An unexpected error has occurred while receiving stream events from: ", e); isErrorRetryable = true; } finally { - if (!isDisconnectCalled.getAndSet(false)) { + if (!mIsDisconnectCalled.getAndSet(false)) { mSseHandler.handleError(isErrorRetryable); close(); } diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java index df6ed58bd..619a28bab 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimer.java @@ -1,38 +1,36 @@ package io.split.android.client.service.sseclient.sseclient; +import static com.google.gson.internal.$Gson$Preconditions.checkNotNull; + import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; import io.split.android.client.service.executor.SplitTaskExecutor; -import io.split.android.client.service.executor.SplitTaskType; -import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; -import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent; -import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent.EventType; - -import static com.google.gson.internal.$Gson$Preconditions.checkNotNull; -import static java.lang.reflect.Modifier.PRIVATE; +import io.split.android.client.utils.logger.Logger; -// TODO: This disconnection timer should use an executor that is not paused on app background public class SseDisconnectionTimer implements SplitTaskExecutionListener { - private final static int DISCONNECT_ON_BG_TIME_IN_SECONDS = 60; - SplitTaskExecutor mTaskExecutor; - String mTaskId; + private final SplitTaskExecutor mTaskExecutor; + private final int mInitialDelayInSeconds; + private String mTaskId; - public SseDisconnectionTimer(@NonNull SplitTaskExecutor taskExecutor) { + public SseDisconnectionTimer(@NonNull SplitTaskExecutor taskExecutor, int initialDelayInSeconds) { mTaskExecutor = checkNotNull(taskExecutor); + mInitialDelayInSeconds = initialDelayInSeconds; } public void cancel() { - mTaskExecutor.stopTask(mTaskId); + if (mTaskId != null) { + mTaskExecutor.stopTask(mTaskId); + } } public void schedule(SplitTask task) { + Logger.v("Scheduling disconnection in " + mInitialDelayInSeconds + " seconds"); cancel(); - mTaskId = mTaskExecutor.schedule(task, DISCONNECT_ON_BG_TIME_IN_SECONDS, this); + mTaskId = mTaskExecutor.schedule(task, mInitialDelayInSeconds, this); } @Override diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java index b9e04f9c4..fc05691aa 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java @@ -6,14 +6,17 @@ import io.split.android.client.service.sseclient.notifications.NotificationParser; import io.split.android.client.service.sseclient.notifications.NotificationProcessor; import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; +import io.split.android.client.service.synchronizer.SyncGuardian; public class StreamingComponents { - private PushNotificationManager pushNotificationManager; - private BlockingQueue splitsUpdateNotificationQueue; - private PushManagerEventBroadcaster pushManagerEventBroadcaster; - private NotificationParser notificationParser; - private NotificationProcessor notificationProcessor; - private SseAuthenticator sseAuthenticator; + + private PushNotificationManager mPushNotificationManager; + private BlockingQueue mSplitsUpdateNotificationQueue; + private PushManagerEventBroadcaster mPushManagerEventBroadcaster; + private NotificationParser mNotificationParser; + private NotificationProcessor mNotificationProcessor; + private SseAuthenticator mSseAuthenticator; + private SyncGuardian mSyncGuardian; public StreamingComponents() { } @@ -23,36 +26,42 @@ public StreamingComponents(PushNotificationManager pushNotificationManager, NotificationParser notificationParser, NotificationProcessor notificationProcessor, SseAuthenticator sseAuthenticator, - PushManagerEventBroadcaster pushManagerEventBroadcaster) { - this.pushNotificationManager = pushNotificationManager; - this.splitsUpdateNotificationQueue = splitsUpdateNotificationQueue; - this.notificationParser = notificationParser; - this.notificationProcessor = notificationProcessor; - this.sseAuthenticator = sseAuthenticator; - this.pushManagerEventBroadcaster = pushManagerEventBroadcaster; + PushManagerEventBroadcaster pushManagerEventBroadcaster, + SyncGuardian syncManager) { + mPushNotificationManager = pushNotificationManager; + mSplitsUpdateNotificationQueue = splitsUpdateNotificationQueue; + mNotificationParser = notificationParser; + mNotificationProcessor = notificationProcessor; + mSseAuthenticator = sseAuthenticator; + mPushManagerEventBroadcaster = pushManagerEventBroadcaster; + mSyncGuardian = syncManager; } public PushNotificationManager getPushNotificationManager() { - return pushNotificationManager; + return mPushNotificationManager; } public BlockingQueue getSplitsUpdateNotificationQueue() { - return splitsUpdateNotificationQueue; + return mSplitsUpdateNotificationQueue; } public PushManagerEventBroadcaster getPushManagerEventBroadcaster() { - return pushManagerEventBroadcaster; + return mPushManagerEventBroadcaster; } public NotificationParser getNotificationParser() { - return notificationParser; + return mNotificationParser; } public NotificationProcessor getNotificationProcessor() { - return notificationProcessor; + return mNotificationProcessor; } public SseAuthenticator getSseAuthenticator() { - return sseAuthenticator; + return mSseAuthenticator; + } + + public SyncGuardian getSyncGuardian() { + return mSyncGuardian; } } diff --git a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java new file mode 100644 index 000000000..04b87e777 --- /dev/null +++ b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java @@ -0,0 +1,22 @@ +package io.split.android.client.service.synchronizer; + +import io.split.android.client.service.executor.SplitTaskExecutionListener; + +public interface FeatureFlagsSynchronizer { + + void loadAndSynchronize(); + + void loadFromCache(); + + void synchronize(long since); + + void synchronize(); + + void startPeriodicFetching(); + + void stopPeriodicFetching(); + + void stopSynchronization(); + + void submitLoadingTask(SplitTaskExecutionListener listener); +} diff --git a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java new file mode 100644 index 000000000..2e8a66dc1 --- /dev/null +++ b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java @@ -0,0 +1,131 @@ +package io.split.android.client.service.synchronizer; + +import static com.google.common.base.Preconditions.checkNotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import io.split.android.client.RetryBackoffCounterTimerFactory; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTaskBatchItem; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionListener; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.executor.SplitTaskExecutor; +import io.split.android.client.service.executor.SplitTaskFactory; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; +import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent; +import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; + +public class FeatureFlagsSynchronizerImpl implements FeatureFlagsSynchronizer { + + private final SplitTaskExecutor mTaskExecutor; + private final SplitTaskExecutor mSplitsTaskExecutor; + private final SplitClientConfig mSplitClientConfig; + private final SplitTaskFactory mSplitTaskFactory; + + private final LoadLocalDataListener mLoadLocalSplitsListener; + + private String mSplitsFetcherTaskId; + private final RetryBackoffCounterTimer mSplitsSyncRetryTimer; + private final RetryBackoffCounterTimer mSplitsUpdateRetryTimer; + @Nullable + private final SplitTaskExecutionListener mSplitsSyncListener; + + public FeatureFlagsSynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, + @NonNull SplitTaskExecutor taskExecutor, + @NonNull SplitTaskExecutor splitSingleThreadTaskExecutor, + @NonNull SplitTaskFactory splitTaskFactory, + @NonNull ISplitEventsManager splitEventsManager, + @NonNull RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory, + @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster) { + + mTaskExecutor = checkNotNull(taskExecutor); + mSplitsTaskExecutor = splitSingleThreadTaskExecutor; + mSplitClientConfig = checkNotNull(splitClientConfig); + mSplitTaskFactory = checkNotNull(splitTaskFactory); + mSplitsSyncRetryTimer = retryBackoffCounterTimerFactory.create(mSplitsTaskExecutor, 1); + mSplitsUpdateRetryTimer = retryBackoffCounterTimerFactory.create(mSplitsTaskExecutor, 1); + + if (pushManagerEventBroadcaster != null) { + mSplitsSyncListener = new SplitTaskExecutionListener() { + @Override + public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { + if (taskInfo.getStatus() == SplitTaskExecutionStatus.SUCCESS) { + pushManagerEventBroadcaster.pushMessage(new PushStatusEvent(PushStatusEvent.EventType.SUCCESSFUL_SYNC)); + } + } + }; + } else { + mSplitsSyncListener = null; + } + + mSplitsSyncRetryTimer.setTask(mSplitTaskFactory.createSplitsSyncTask(true), mSplitsSyncListener); + mLoadLocalSplitsListener = new LoadLocalDataListener( + splitEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + } + + @Override + public void loadFromCache() { + submitLoadingTask(mLoadLocalSplitsListener); + } + + @Override + public void loadAndSynchronize() { + List enqueued = new ArrayList<>(); + enqueued.add(new SplitTaskBatchItem(mSplitTaskFactory.createFilterSplitsInCacheTask(), null)); + enqueued.add(new SplitTaskBatchItem(mSplitTaskFactory.createLoadSplitsTask(), mLoadLocalSplitsListener)); + enqueued.add(new SplitTaskBatchItem(() -> { + synchronize(); + return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK); + }, null)); + mTaskExecutor.executeSerially(enqueued); + } + + @Override + public void synchronize(long since) { + mSplitsUpdateRetryTimer.setTask(mSplitTaskFactory.createSplitsUpdateTask(since), mSplitsSyncListener); + mSplitsUpdateRetryTimer.start(); + } + + @Override + public void synchronize() { + mSplitsSyncRetryTimer.start(); + } + + @Override + public void startPeriodicFetching() { + scheduleSplitsFetcherTask(); + } + + @Override + public void stopPeriodicFetching() { + mSplitsTaskExecutor.stopTask(mSplitsFetcherTaskId); + } + + @Override + public void stopSynchronization() { + mSplitsSyncRetryTimer.stop(); + mSplitsUpdateRetryTimer.stop(); + } + + @Override + public void submitLoadingTask(SplitTaskExecutionListener listener) { + mTaskExecutor.submit(mSplitTaskFactory.createLoadSplitsTask(), + listener); + } + + private void scheduleSplitsFetcherTask() { + mSplitsFetcherTaskId = mSplitsTaskExecutor.schedule( + mSplitTaskFactory.createSplitsSyncTask(false), + mSplitClientConfig.featuresRefreshRate(), + mSplitClientConfig.featuresRefreshRate(), + mSplitsSyncListener); + } +} diff --git a/src/main/java/io/split/android/client/service/synchronizer/SyncGuardian.java b/src/main/java/io/split/android/client/service/synchronizer/SyncGuardian.java new file mode 100644 index 000000000..6b5fb0ab4 --- /dev/null +++ b/src/main/java/io/split/android/client/service/synchronizer/SyncGuardian.java @@ -0,0 +1,12 @@ +package io.split.android.client.service.synchronizer; + +public interface SyncGuardian { + + void setMaxSyncPeriod(long maxSyncPeriod); + + void updateLastSyncTimestamp(); + + boolean mustSync(); + + void initialize(); +} diff --git a/src/main/java/io/split/android/client/service/synchronizer/SyncGuardianImpl.java b/src/main/java/io/split/android/client/service/synchronizer/SyncGuardianImpl.java new file mode 100644 index 000000000..e928a433e --- /dev/null +++ b/src/main/java/io/split/android/client/service/synchronizer/SyncGuardianImpl.java @@ -0,0 +1,66 @@ +package io.split.android.client.service.synchronizer; + +import androidx.annotation.VisibleForTesting; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import io.split.android.client.SplitClientConfig; +import io.split.android.client.utils.logger.Logger; + +public class SyncGuardianImpl implements SyncGuardian { + + private final AtomicLong mDefaultMaxSyncPeriod; + private final AtomicLong mMaxSyncPeriod; + private final AtomicLong mLastSyncTimestamp; + private final TimestampProvider mNewTimestamp; + private final boolean mSyncEnabled; + private final boolean mStreamingEnabled; + private boolean mIsInitialized = false; + + public SyncGuardianImpl(SplitClientConfig splitConfig) { + this(splitConfig, null); + } + + @VisibleForTesting + SyncGuardianImpl(SplitClientConfig splitConfig, TimestampProvider timestampProvider) { + long maxSyncPeriod = splitConfig.defaultSSEConnectionDelay(); + + mDefaultMaxSyncPeriod = new AtomicLong(maxSyncPeriod); + mMaxSyncPeriod = new AtomicLong(maxSyncPeriod); + mLastSyncTimestamp = new AtomicLong(0); + mSyncEnabled = splitConfig.syncEnabled(); + mStreamingEnabled = splitConfig.streamingEnabled(); + mNewTimestamp = timestampProvider != null ? timestampProvider : () -> TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); + } + + @Override + public void updateLastSyncTimestamp() { + mLastSyncTimestamp.set(mNewTimestamp.get()); + } + + @Override + public boolean mustSync() { + return mIsInitialized && mSyncEnabled && mStreamingEnabled && + mNewTimestamp.get() - mLastSyncTimestamp.get() >= mMaxSyncPeriod.get(); + } + + @Override + public void setMaxSyncPeriod(long newPeriod) { + mMaxSyncPeriod.set(Math.max(newPeriod, mDefaultMaxSyncPeriod.get())); + Logger.v("Setting new max sync period: " + mMaxSyncPeriod.get() + " seconds"); + } + + @Override + public void initialize() { + if (mIsInitialized) { + return; + } + mIsInitialized = true; + } + + interface TimestampProvider { + + long get(); + } +} diff --git a/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java index 4b9a88e66..cbed89b25 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java @@ -14,6 +14,7 @@ import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.sseclient.feedbackchannel.BroadcastedEventListener; +import io.split.android.client.service.sseclient.feedbackchannel.DelayStatusEvent; import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent; import io.split.android.client.service.sseclient.reactor.MySegmentsUpdateWorker; @@ -33,12 +34,19 @@ public class SyncManagerImpl implements SyncManager, BroadcastedEventListener, M private final AtomicBoolean mIsPaused; private final TelemetrySynchronizer mTelemetrySynchronizer; - private AtomicBoolean isPollingEnabled; - private PushManagerEventBroadcaster mPushManagerEventBroadcaster; - private MySegmentsUpdateWorkerRegistry mMySegmentsUpdateWorkerRegistry; - private PushNotificationManager mPushNotificationManager; - private SplitUpdatesWorker mSplitUpdateWorker; - private BackoffCounterTimer mStreamingReconnectTimer; + private final AtomicBoolean mIsPollingEnabled; + @Nullable + private final PushManagerEventBroadcaster mPushManagerEventBroadcaster; + @Nullable + private final MySegmentsUpdateWorkerRegistry mMySegmentsUpdateWorkerRegistry; + @Nullable + private final PushNotificationManager mPushNotificationManager; + @Nullable + private final SplitUpdatesWorker mSplitUpdateWorker; + @Nullable + private final BackoffCounterTimer mStreamingReconnectTimer; + @Nullable + private final SyncGuardian mSyncGuardian; public SyncManagerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull Synchronizer synchronizer, @@ -46,13 +54,14 @@ public SyncManagerImpl(@NonNull SplitClientConfig splitClientConfig, @Nullable SplitUpdatesWorker splitUpdateWorker, @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster, @Nullable BackoffCounterTimer streamingReconnectTimer, + @Nullable SyncGuardian syncGuardian, @NonNull TelemetrySynchronizer telemetrySynchronizer) { mSynchronizer = checkNotNull(synchronizer); mSplitClientConfig = checkNotNull(splitClientConfig); mTelemetrySynchronizer = checkNotNull(telemetrySynchronizer); mIsPaused = new AtomicBoolean(false); - isPollingEnabled = new AtomicBoolean(false); + mIsPollingEnabled = new AtomicBoolean(false); if (isSyncEnabled()) { mPushNotificationManager = pushNotificationManager; @@ -60,6 +69,14 @@ public SyncManagerImpl(@NonNull SplitClientConfig splitClientConfig, mPushManagerEventBroadcaster = pushManagerEventBroadcaster; mStreamingReconnectTimer = streamingReconnectTimer; mMySegmentsUpdateWorkerRegistry = new MySegmentsUpdateWorkerRegistryImpl(); + mSyncGuardian = syncGuardian; + } else { + mPushNotificationManager = null; + mSplitUpdateWorker = null; + mPushManagerEventBroadcaster = null; + mStreamingReconnectTimer = null; + mMySegmentsUpdateWorkerRegistry = null; + mSyncGuardian = null; } } @@ -79,7 +96,7 @@ public void start() { return; } - isPollingEnabled.set(!mSplitClientConfig.streamingEnabled()); + mIsPollingEnabled.set(!mSplitClientConfig.streamingEnabled()); if (mSplitClientConfig.streamingEnabled()) { mPushManagerEventBroadcaster.register(this); mSplitUpdateWorker.start(); @@ -107,8 +124,11 @@ public void pause() { if (isSyncEnabled()) { if (mSplitClientConfig.streamingEnabled()) { mPushNotificationManager.pause(); + if (mSyncGuardian != null) { + mSyncGuardian.initialize(); + } } - if (isPollingEnabled.get()) { + if (mIsPollingEnabled.get()) { mSynchronizer.stopPeriodicFetching(); } } @@ -122,8 +142,10 @@ public void resume() { if (isSyncEnabled()) { if (mSplitClientConfig.streamingEnabled()) { mPushNotificationManager.resume(); + triggerFeatureFlagsSyncIfNeeded(); } - if (isPollingEnabled.get()) { + + if (mIsPollingEnabled.get()) { mSynchronizer.startPeriodicFetching(); } } @@ -173,7 +195,7 @@ public void onEvent(PushStatusEvent message) { mSynchronizer.synchronizeMySegments(); mSynchronizer.stopPeriodicFetching(); mStreamingReconnectTimer.cancel(); - isPollingEnabled.set(false); + mIsPollingEnabled.set(false); break; case PUSH_SUBSYSTEM_DOWN: @@ -186,7 +208,7 @@ public void onEvent(PushStatusEvent message) { Logger.d("Push Subsystem recoverable error received."); enablePolling(); // If sdk is paused (host app in bg) push manager should reconnect on resume - if(!mIsPaused.get()) { + if (!mIsPaused.get()) { mStreamingReconnectTimer.schedule(); } break; @@ -209,11 +231,30 @@ public void onEvent(PushStatusEvent message) { Logger.d("Push Subsystem reset received."); // If sdk is paused (host app in bg) push manager should reconnect on resume mPushNotificationManager.disconnect(); - if(!mIsPaused.get()) { + if (!mIsPaused.get()) { mStreamingReconnectTimer.schedule(); } break; + case SUCCESSFUL_SYNC: + if (mSyncGuardian != null) { + Logger.v("Successful sync event received, updating last sync timestamp"); + mSyncGuardian.updateLastSyncTimestamp(); + } + break; + + case PUSH_DELAY_RECEIVED: + try { + DelayStatusEvent delayEvent = (DelayStatusEvent) message; + if (mSyncGuardian != null) { + Logger.v("Streaming delay event received"); + mSyncGuardian.setMaxSyncPeriod(delayEvent.getDelay()); + } + } catch (ClassCastException ex) { + Logger.w("Invalid streaming delay event received"); + } + break; + default: Logger.e("Invalid SSE event received: " + message.getMessage()); } @@ -235,7 +276,7 @@ public void setupUserConsent(UserConsent status) { Logger.v("User consent status is granted now. Starting recorders"); mSynchronizer.startPeriodicRecording(); } else { - Logger.v("User consent status is " + status + " now. Stopping recorders"); + Logger.v("User consent status is " + status + " now. Stopping recorders"); mSynchronizer.stopPeriodicRecording(); } } @@ -245,15 +286,27 @@ private boolean isSyncEnabled() { } private void enablePolling() { - if (!isSyncEnabled()) { return; } - if (!isPollingEnabled.get()) { - isPollingEnabled.set(true); + if (!mIsPollingEnabled.get()) { + mIsPollingEnabled.set(true); mSynchronizer.startPeriodicFetching(); Logger.i("Polling enabled."); } } + + private void triggerFeatureFlagsSyncIfNeeded() { + if (mPushNotificationManager.isSseClientDisconnected()) { + if (mSyncGuardian.mustSync()) { + Logger.v("Must sync, synchronizing splits"); + mSynchronizer.synchronizeSplits(); + } else { + Logger.v("No need to sync"); + } + } else { + Logger.v("SSE client is connected, no need to trigger sync"); + } + } } 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 933c8d006..ad4ad8f9a 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 @@ -3,25 +3,22 @@ import static com.google.common.base.Preconditions.checkNotNull; import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.List; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import io.split.android.client.RetryBackoffCounterTimerFactory; import io.split.android.client.SplitClientConfig; import io.split.android.client.dtos.Event; import io.split.android.client.events.ISplitEventsManager; -import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.impressions.Impression; import io.split.android.client.service.ServiceConstants; -import io.split.android.client.service.executor.SplitTask; -import io.split.android.client.service.executor.SplitTaskBatchItem; 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; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.impressions.ImpressionManager; +import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; import io.split.android.client.service.synchronizer.attributes.AttributesSynchronizer; import io.split.android.client.service.synchronizer.attributes.AttributesSynchronizerRegistry; @@ -30,7 +27,7 @@ import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerRegistry; import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerRegistryImpl; import io.split.android.client.shared.UserConsent; -import io.split.android.client.storage.common.SplitStorageContainer; +import io.split.android.client.storage.common.StoragePusher; import io.split.android.client.telemetry.model.EventsDataRecordsEnum; import io.split.android.client.telemetry.model.streaming.SyncModeUpdateStreamingEvent; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; @@ -39,22 +36,16 @@ public class SynchronizerImpl implements Synchronizer, SplitTaskExecutionListener, MySegmentsSynchronizerRegistry, AttributesSynchronizerRegistry { private final SplitTaskExecutor mTaskExecutor; - private final SplitTaskExecutor mSplitsTaskExecutor; - private final SplitStorageContainer mSplitsStorageContainer; + private final SplitTaskExecutor mSingleThreadTaskExecutor; + private final StoragePusher mEventsStorage; private final SplitClientConfig mSplitClientConfig; - private final ISplitEventsManager mSplitEventsManager; private final SplitTaskFactory mSplitTaskFactory; - private final WorkManagerWrapper mWorkManagerWrapper; private final ImpressionManager mImpressionManager; + private final FeatureFlagsSynchronizer mFeatureFlagsSynchronizer; private RecorderSyncHelper mEventsSyncHelper; - private LoadLocalDataListener mLoadLocalSplitsListener; - - private String mSplitsFetcherTaskId; private String mEventsRecorderTaskId; - private final RetryBackoffCounterTimer mSplitsSyncRetryTimer; - private final RetryBackoffCounterTimer mSplitsUpdateRetryTimer; private final RetryBackoffCounterTimer mEventsRecorderUpdateRetryTimer; private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; private final AttributesSynchronizerRegistryImpl mAttributesSynchronizerRegistry; @@ -63,49 +54,77 @@ public class SynchronizerImpl implements Synchronizer, SplitTaskExecutionListene public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull SplitTaskExecutor taskExecutor, @NonNull SplitTaskExecutor splitSingleThreadTaskExecutor, - @NonNull SplitStorageContainer splitStorageContainer, @NonNull SplitTaskFactory splitTaskFactory, - @NonNull ISplitEventsManager splitEventsManager, @NonNull WorkManagerWrapper workManagerWrapper, @NonNull RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, @NonNull AttributesSynchronizerRegistryImpl attributesSynchronizerRegistry, @NonNull MySegmentsSynchronizerRegistryImpl mySegmentsSynchronizerRegistry, - @NonNull ImpressionManager impressionManager) { + @NonNull ImpressionManager impressionManager, + @NonNull StoragePusher eventsStorage, + @NonNull ISplitEventsManager eventsManagerCoordinator, + @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster) { + this(splitClientConfig, + taskExecutor, + splitSingleThreadTaskExecutor, + splitTaskFactory, + workManagerWrapper, + retryBackoffCounterTimerFactory, + telemetryRuntimeProducer, + attributesSynchronizerRegistry, + mySegmentsSynchronizerRegistry, + impressionManager, + new FeatureFlagsSynchronizerImpl(splitClientConfig, taskExecutor, + splitSingleThreadTaskExecutor, + splitTaskFactory, + eventsManagerCoordinator, + retryBackoffCounterTimerFactory, + pushManagerEventBroadcaster), + eventsStorage); + } + + @VisibleForTesting + public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, + @NonNull SplitTaskExecutor taskExecutor, + @NonNull SplitTaskExecutor splitSingleThreadTaskExecutor, + @NonNull SplitTaskFactory splitTaskFactory, + @NonNull WorkManagerWrapper workManagerWrapper, + @NonNull RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory, + @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, + @NonNull AttributesSynchronizerRegistryImpl attributesSynchronizerRegistry, + @NonNull MySegmentsSynchronizerRegistryImpl mySegmentsSynchronizerRegistry, + @NonNull ImpressionManager impressionManager, + @NonNull FeatureFlagsSynchronizer featureFlagsSynchronizer, + @NonNull StoragePusher eventsStorage) { mTaskExecutor = checkNotNull(taskExecutor); - mSplitsTaskExecutor = splitSingleThreadTaskExecutor; - mSplitsStorageContainer = checkNotNull(splitStorageContainer); + mSingleThreadTaskExecutor = checkNotNull(splitSingleThreadTaskExecutor); + mEventsStorage = checkNotNull(eventsStorage); mSplitClientConfig = checkNotNull(splitClientConfig); - mSplitEventsManager = checkNotNull(splitEventsManager); mSplitTaskFactory = checkNotNull(splitTaskFactory); - mWorkManagerWrapper = checkNotNull(workManagerWrapper); + mFeatureFlagsSynchronizer = checkNotNull(featureFlagsSynchronizer); mAttributesSynchronizerRegistry = attributesSynchronizerRegistry; - mSplitsSyncRetryTimer = retryBackoffCounterTimerFactory.create(mSplitsTaskExecutor, 1); - mSplitsUpdateRetryTimer = retryBackoffCounterTimerFactory.create(mSplitsTaskExecutor, 1); mEventsRecorderUpdateRetryTimer = retryBackoffCounterTimerFactory.createWithFixedInterval( - mSplitsTaskExecutor, + mSingleThreadTaskExecutor, ServiceConstants.TELEMETRY_CONFIG_RETRY_INTERVAL_SECONDS, ServiceConstants.UNIQUE_KEYS_MAX_RETRY_ATTEMPTS); - mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); mMySegmentsSynchronizerRegistry = checkNotNull(mySegmentsSynchronizerRegistry); mImpressionManager = checkNotNull(impressionManager); setupListeners(); - mSplitsSyncRetryTimer.setTask(mSplitTaskFactory.createSplitsSyncTask(true), null); if (mSplitClientConfig.synchronizeInBackground()) { - mWorkManagerWrapper.setFetcherExecutionListener(this); - mWorkManagerWrapper.scheduleWork(); + workManagerWrapper.setFetcherExecutionListener(this); + workManagerWrapper.scheduleWork(); } else { - mWorkManagerWrapper.removeWork(); + workManagerWrapper.removeWork(); } } @Override public void loadSplitsFromCache() { - submitSplitLoadingTask(mLoadLocalSplitsListener); + mFeatureFlagsSynchronizer.loadFromCache(); } @Override @@ -120,29 +139,17 @@ public void loadAttributesFromCache() { @Override public void loadAndSynchronizeSplits() { - List enqueued = new ArrayList<>(); - enqueued.add(new SplitTaskBatchItem(mSplitTaskFactory.createFilterSplitsInCacheTask(), null)); - enqueued.add(new SplitTaskBatchItem(mSplitTaskFactory.createLoadSplitsTask(), mLoadLocalSplitsListener)); - enqueued.add(new SplitTaskBatchItem(new SplitTask() { - @NonNull - @Override - public SplitTaskExecutionInfo execute() { - synchronizeSplits(); - return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK); - } - }, null)); - mTaskExecutor.executeSerially(enqueued); + mFeatureFlagsSynchronizer.loadAndSynchronize(); } @Override public void synchronizeSplits(long since) { - mSplitsUpdateRetryTimer.setTask(mSplitTaskFactory.createSplitsUpdateTask(since), null); - mSplitsUpdateRetryTimer.start(); + mFeatureFlagsSynchronizer.synchronize(since); } @Override public void synchronizeSplits() { - mSplitsSyncRetryTimer.start(); + mFeatureFlagsSynchronizer.synchronize(); } @Override @@ -157,7 +164,7 @@ public void forceMySegmentsSync() { @Override synchronized public void startPeriodicFetching() { - scheduleSplitsFetcherTask(); + mFeatureFlagsSynchronizer.startPeriodicFetching(); scheduleMySegmentsFetcherTask(); mTelemetryRuntimeProducer.recordStreamingEvents(new SyncModeUpdateStreamingEvent(SyncModeUpdateStreamingEvent.Mode.POLLING, System.currentTimeMillis())); Logger.i("Periodic fetcher tasks scheduled"); @@ -165,7 +172,7 @@ synchronized public void startPeriodicFetching() { @Override synchronized public void stopPeriodicFetching() { - mSplitsTaskExecutor.stopTask(mSplitsFetcherTaskId); + mFeatureFlagsSynchronizer.stopPeriodicFetching(); mMySegmentsSynchronizerRegistry.stopPeriodicFetching(); } @@ -186,25 +193,25 @@ public void stopPeriodicRecording() { private void setupListeners() { mEventsSyncHelper = new RecorderSyncHelperImpl<>( SplitTaskType.EVENTS_RECORDER, - mSplitsStorageContainer.getEventsStorage(), + mEventsStorage, mSplitClientConfig.eventsQueueSize(), ServiceConstants.MAX_EVENTS_SIZE_BYTES, mTaskExecutor ); - mLoadLocalSplitsListener = new LoadLocalDataListener( - mSplitEventsManager, SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); } public void pause() { stopPeriodicRecording(); + stopPeriodicFetching(); + mTaskExecutor.pause(); - mSplitsTaskExecutor.pause(); + mSingleThreadTaskExecutor.pause(); } public void resume() { mTaskExecutor.resume(); - mSplitsTaskExecutor.resume(); + mSingleThreadTaskExecutor.resume(); if (mSplitClientConfig.userConsent() == UserConsent.GRANTED) { startPeriodicRecording(); } @@ -212,8 +219,7 @@ public void resume() { @Override public void destroy() { - mSplitsSyncRetryTimer.stop(); - mSplitsUpdateRetryTimer.stop(); + mFeatureFlagsSynchronizer.stopSynchronization(); mMySegmentsSynchronizerRegistry.destroy(); flush(); } @@ -261,14 +267,6 @@ public void unregisterAttributesSynchronizer(String userKey) { mAttributesSynchronizerRegistry.unregisterAttributesSynchronizer(userKey); } - private void scheduleSplitsFetcherTask() { - mSplitsFetcherTaskId = mSplitsTaskExecutor.schedule( - mSplitTaskFactory.createSplitsSyncTask(false), - mSplitClientConfig.featuresRefreshRate(), - mSplitClientConfig.featuresRefreshRate(), - null); - } - private void scheduleMySegmentsFetcherTask() { mMySegmentsSynchronizerRegistry.scheduleSegmentsSyncTask(); } @@ -280,17 +278,11 @@ private void scheduleEventsRecorderTask() { mSplitClientConfig.eventFlushInterval(), mEventsSyncHelper); } - private void submitSplitLoadingTask(SplitTaskExecutionListener listener) { - mTaskExecutor.submit(mSplitTaskFactory.createLoadSplitsTask(), - listener); - } - @Override public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { switch (taskInfo.getTaskType()) { case SPLITS_SYNC: - Logger.d("Loading split definitions updated in background"); - submitSplitLoadingTask(null); + mFeatureFlagsSynchronizer.submitLoadingTask(null); break; case MY_SEGMENTS_SYNC: Logger.d("Loading my segments updated in background"); 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 b0683198a..e80480315 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 @@ -36,7 +36,6 @@ import io.split.android.client.utils.logger.Logger; import io.split.android.client.service.workmanager.UniqueKeysRecorderWorker; -@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public class WorkManagerWrapper implements MySegmentsWorkManagerWrapper { final private WorkManager mWorkManager; final private String mDatabaseName; 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 b2d2dbb71..03160a4a3 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 @@ -59,7 +59,6 @@ public void synchronizeMySegments() { } @Override - @VisibleForTesting public void forceMySegmentsSync() { for (MySegmentsSynchronizer mySegmentsSynchronizer : mMySegmentsSynchronizers.values()) { mySegmentsSynchronizer.forceMySegmentsSync(); diff --git a/src/main/java/io/split/android/client/telemetry/model/Config.java b/src/main/java/io/split/android/client/telemetry/model/Config.java index df5d925bd..25ec3ee42 100644 --- a/src/main/java/io/split/android/client/telemetry/model/Config.java +++ b/src/main/java/io/split/android/client/telemetry/model/Config.java @@ -28,7 +28,7 @@ public class Config { private long eventsQueueSize; @SerializedName("iM") - private ImpressionsMode impressionsMode; + private int impressionsMode; @SerializedName("iL") private boolean impressionsListenerEnabled; @@ -108,11 +108,11 @@ public void setEventsQueueSize(long eventsQueueSize) { this.eventsQueueSize = eventsQueueSize; } - public ImpressionsMode getImpressionsMode() { + public int getImpressionsMode() { return impressionsMode; } - public void setImpressionsMode(ImpressionsMode impressionsMode) { + public void setImpressionsMode(int impressionsMode) { this.impressionsMode = impressionsMode; } diff --git a/src/main/java/io/split/android/client/telemetry/model/ImpressionsMode.java b/src/main/java/io/split/android/client/telemetry/model/ImpressionsMode.java index c13faa97f..efa5f37a1 100644 --- a/src/main/java/io/split/android/client/telemetry/model/ImpressionsMode.java +++ b/src/main/java/io/split/android/client/telemetry/model/ImpressionsMode.java @@ -7,5 +7,21 @@ public enum ImpressionsMode { OPTIMIZED, @SerializedName("1") - DEBUG + DEBUG, + + @SerializedName("2") + NONE; + + public int intValue() { + switch (this) { + case OPTIMIZED: + return 0; + case DEBUG: + return 1; + case NONE: + return 2; + } + + return 0; + } } diff --git a/src/main/java/io/split/android/client/telemetry/model/Stats.java b/src/main/java/io/split/android/client/telemetry/model/Stats.java index 4108817e7..7b1a169e4 100644 --- a/src/main/java/io/split/android/client/telemetry/model/Stats.java +++ b/src/main/java/io/split/android/client/telemetry/model/Stats.java @@ -1,5 +1,7 @@ package io.split.android.client.telemetry.model; +import androidx.annotation.VisibleForTesting; + import com.google.gson.annotations.SerializedName; import java.util.List; @@ -62,6 +64,9 @@ public class Stats { @SerializedName("t") private List tags; + @SerializedName("ufs") + private UpdatesFromSSE updatesFromSSE; + public void setLastSynchronizations(LastSync lastSynchronizations) { this.lastSynchronizations = lastSynchronizations; } @@ -130,7 +135,95 @@ public void setTags(List tags) { this.tags = tags; } + public void setUpdatesFromSSE(UpdatesFromSSE updatesFromSSE) { + this.updatesFromSSE = updatesFromSSE; + } + public List getTags() { return tags; } + + public UpdatesFromSSE getUpdatesFromSSE() { + return updatesFromSSE; + } + + @VisibleForTesting + public LastSync getLastSynchronizations() { + return lastSynchronizations; + } + + @VisibleForTesting + public MethodLatencies getMethodLatencies() { + return methodLatencies; + } + + @VisibleForTesting + public MethodExceptions getMethodExceptions() { + return methodExceptions; + } + + @VisibleForTesting + public HttpErrors getHttpErrors() { + return httpErrors; + } + + @VisibleForTesting + public HttpLatencies getHttpLatencies() { + return httpLatencies; + } + + @VisibleForTesting + public long getTokenRefreshes() { + return tokenRefreshes; + } + + @VisibleForTesting + public long getAuthRejections() { + return authRejections; + } + + @VisibleForTesting + public long getImpressionsQueued() { + return impressionsQueued; + } + + @VisibleForTesting + public long getImpressionsDeduped() { + return impressionsDeduped; + } + + @VisibleForTesting + public long getImpressionsDropped() { + return impressionsDropped; + } + + @VisibleForTesting + public long getSplitCount() { + return splitCount; + } + + @VisibleForTesting + public long getSegmentCount() { + return segmentCount; + } + + @VisibleForTesting + public long getSessionLengthMs() { + return sessionLengthMs; + } + + @VisibleForTesting + public long getEventsQueued() { + return eventsQueued; + } + + @VisibleForTesting + public long getEventsDropped() { + return eventsDropped; + } + + @VisibleForTesting + public List getStreamingEvents() { + return streamingEvents; + } } diff --git a/src/main/java/io/split/android/client/telemetry/model/UpdatesFromSSE.java b/src/main/java/io/split/android/client/telemetry/model/UpdatesFromSSE.java new file mode 100644 index 000000000..48b30c9a5 --- /dev/null +++ b/src/main/java/io/split/android/client/telemetry/model/UpdatesFromSSE.java @@ -0,0 +1,25 @@ +package io.split.android.client.telemetry.model; + +import com.google.gson.annotations.SerializedName; + +public class UpdatesFromSSE { + + @SerializedName("sp") + private long mSplits; + + @SerializedName("ms") + private long mMySegments; + + public UpdatesFromSSE(long splits, long mySegments) { + mSplits = splits; + mMySegments = mySegments; + } + + public long getSplits() { + return mSplits; + } + + public long getMySegments() { + return mMySegments; + } +} diff --git a/src/main/java/io/split/android/client/telemetry/model/streaming/UpdatesFromSSEEnum.java b/src/main/java/io/split/android/client/telemetry/model/streaming/UpdatesFromSSEEnum.java new file mode 100644 index 000000000..1e4a1acaf --- /dev/null +++ b/src/main/java/io/split/android/client/telemetry/model/streaming/UpdatesFromSSEEnum.java @@ -0,0 +1,7 @@ +package io.split.android.client.telemetry.model.streaming; + +public enum UpdatesFromSSEEnum { + + SPLITS, + MY_SEGMENTS, +} diff --git a/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java b/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java index 798afb635..5f22b414c 100644 --- a/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java +++ b/src/main/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorage.java @@ -21,8 +21,9 @@ import io.split.android.client.telemetry.model.MethodLatencies; import io.split.android.client.telemetry.model.OperationType; import io.split.android.client.telemetry.model.PushCounterEvent; +import io.split.android.client.telemetry.model.UpdatesFromSSE; import io.split.android.client.telemetry.model.streaming.StreamingEvent; -import io.split.android.client.telemetry.util.AtomicLongArray; +import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum; public class InMemoryTelemetryStorage implements TelemetryStorage { @@ -50,10 +51,12 @@ public class InMemoryTelemetryStorage implements TelemetryStorage { private final Object streamingEventsLock = new Object(); private List streamingEvents = new ArrayList<>(); + private Map updatesFromSSE = Maps.newConcurrentMap(); private final Object tagsLock = new Object(); private final Object httpLatenciesLock = new Object(); private final Object methodLatenciesLock = new Object(); + private final Object updatesFromSSELock = new Object(); private final Set tags = new HashSet<>(); @@ -248,6 +251,26 @@ public long getSessionLength() { return sessionLength.get(); } + @Override + public UpdatesFromSSE popUpdatesFromSSE() { + synchronized (updatesFromSSELock) { + long sCount = 0L; + long mCount = 0L; + + AtomicLong splits = updatesFromSSE.get(UpdatesFromSSEEnum.SPLITS); + if (splits != null) { + sCount = splits.getAndSet(0L); + } + + AtomicLong mySegments = updatesFromSSE.get(UpdatesFromSSEEnum.MY_SEGMENTS); + if (mySegments != null) { + mCount = mySegments.getAndSet(0L); + } + + return new UpdatesFromSSE(sCount, mCount); + } + } + @Override public void addTag(String tag) { synchronized (tagsLock) { @@ -324,6 +347,11 @@ public void recordSessionLength(long sessionLength) { this.sessionLength.set(sessionLength); } + @Override + public void recordUpdatesFromSSE(UpdatesFromSSEEnum sseUpdate) { + updatesFromSSE.get(sseUpdate).incrementAndGet(); + } + private void initializeProperties() { initializeMethodExceptionsCounter(); initializeHttpLatenciesCounter(); @@ -334,6 +362,7 @@ private void initializeProperties() { initializeHttpErrors(); initializeHttpLatencies(); initializePushCounters(); + initializeUpdatesFromSSE(); } private void initializeHttpLatenciesCounter() { @@ -406,6 +435,11 @@ private void initializePushCounters() { pushCounters.put(PushCounterEvent.TOKEN_REFRESHES, new AtomicLong()); } + private void initializeUpdatesFromSSE() { + updatesFromSSE.put(UpdatesFromSSEEnum.SPLITS, new AtomicLong()); + updatesFromSSE.put(UpdatesFromSSEEnum.MY_SEGMENTS, new AtomicLong()); + } + private List popLatencies(OperationType operationType) { long[] latencies = httpLatencies.get(operationType).getLatencies(); httpLatencies.get(operationType).clear(); diff --git a/src/main/java/io/split/android/client/telemetry/storage/NoOpTelemetryStorage.java b/src/main/java/io/split/android/client/telemetry/storage/NoOpTelemetryStorage.java index b3bbd1204..cb2bae0c1 100644 --- a/src/main/java/io/split/android/client/telemetry/storage/NoOpTelemetryStorage.java +++ b/src/main/java/io/split/android/client/telemetry/storage/NoOpTelemetryStorage.java @@ -11,7 +11,9 @@ import io.split.android.client.telemetry.model.MethodExceptions; import io.split.android.client.telemetry.model.MethodLatencies; import io.split.android.client.telemetry.model.OperationType; +import io.split.android.client.telemetry.model.UpdatesFromSSE; import io.split.android.client.telemetry.model.streaming.StreamingEvent; +import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum; public class NoOpTelemetryStorage implements TelemetryStorage { @@ -136,6 +138,11 @@ public long getSessionLength() { return 0; } + @Override + public UpdatesFromSSE popUpdatesFromSSE() { + return null; + } + @Override public void addTag(String tag) { @@ -185,4 +192,9 @@ public void recordStreamingEvents(StreamingEvent streamingEvent) { public void recordSessionLength(long sessionLength) { } + + @Override + public void recordUpdatesFromSSE(UpdatesFromSSEEnum sseUpdate) { + + } } diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java b/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java index 99410ad9d..adfb6f97f 100644 --- a/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java +++ b/src/main/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImpl.java @@ -11,6 +11,7 @@ import androidx.annotation.NonNull; import io.split.android.client.SplitClientConfig; +import io.split.android.client.service.impressions.ImpressionsMode; import io.split.android.client.telemetry.model.Config; import io.split.android.client.telemetry.model.RefreshRates; import io.split.android.client.telemetry.model.UrlOverrides; @@ -43,6 +44,13 @@ public Config getConfigTelemetry() { config.setImpressionsQueueSize(mSplitClientConfig.impressionsQueueSize()); config.setEventsQueueSize(mSplitClientConfig.eventsQueueSize()); config.setUserConsent(mSplitClientConfig.userConsent().intValue()); + if (mSplitClientConfig.impressionsMode() == ImpressionsMode.DEBUG) { + config.setImpressionsMode(io.split.android.client.telemetry.model.ImpressionsMode.DEBUG.intValue()); + } else if (mSplitClientConfig.impressionsMode() == ImpressionsMode.OPTIMIZED) { + config.setImpressionsMode(io.split.android.client.telemetry.model.ImpressionsMode.OPTIMIZED.intValue()); + } else { + config.setImpressionsMode(io.split.android.client.telemetry.model.ImpressionsMode.NONE.intValue()); + } return config; } diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeConsumer.java b/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeConsumer.java index b7c62c089..c61b40c99 100644 --- a/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeConsumer.java +++ b/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeConsumer.java @@ -7,6 +7,7 @@ import io.split.android.client.telemetry.model.HttpLatencies; import io.split.android.client.telemetry.model.ImpressionsDataType; import io.split.android.client.telemetry.model.LastSync; +import io.split.android.client.telemetry.model.UpdatesFromSSE; import io.split.android.client.telemetry.model.streaming.StreamingEvent; public interface TelemetryRuntimeConsumer { @@ -30,4 +31,6 @@ public interface TelemetryRuntimeConsumer { List popTags(); long getSessionLength(); + + UpdatesFromSSE popUpdatesFromSSE(); } diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeProducer.java b/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeProducer.java index 0b110333c..83be91481 100644 --- a/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeProducer.java +++ b/src/main/java/io/split/android/client/telemetry/storage/TelemetryRuntimeProducer.java @@ -4,6 +4,7 @@ import io.split.android.client.telemetry.model.ImpressionsDataType; import io.split.android.client.telemetry.model.OperationType; import io.split.android.client.telemetry.model.streaming.StreamingEvent; +import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum; public interface TelemetryRuntimeProducer { @@ -26,4 +27,6 @@ public interface TelemetryRuntimeProducer { void recordStreamingEvents(StreamingEvent streamingEvent); void recordSessionLength(long sessionLength); + + void recordUpdatesFromSSE(UpdatesFromSSEEnum sseUpdate); } diff --git a/src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImpl.java b/src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImpl.java index b5c2497e5..1117b3d31 100644 --- a/src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImpl.java +++ b/src/main/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImpl.java @@ -15,7 +15,8 @@ public class TelemetryStatsProviderImpl implements TelemetryStatsProvider { private final TelemetryStorageConsumer mTelemetryStorageConsumer; private final SplitsStorage mSplitsStorage; private final MySegmentsStorageContainer mMySegmentsStorageContainer; - private Stats pendingStats = null; + private volatile Stats pendingStats = null; + private final Object mLock = new Object(); public TelemetryStatsProviderImpl(@NonNull TelemetryStorageConsumer telemetryStorageConsumer, @NonNull SplitsStorage splitsStorage, @@ -28,7 +29,11 @@ public TelemetryStatsProviderImpl(@NonNull TelemetryStorageConsumer telemetrySto @Override public Stats getTelemetryStats() { if (pendingStats == null) { - pendingStats = buildStats(); + synchronized (mLock) { + if (pendingStats == null) { + pendingStats = buildStats(); + } + } } return pendingStats; @@ -58,7 +63,8 @@ private Stats buildStats() { stats.setTokenRefreshes(mTelemetryStorageConsumer.popTokenRefreshes()); stats.setAuthRejections(mTelemetryStorageConsumer.popAuthRejections()); stats.setEventsQueued(mTelemetryStorageConsumer.getEventsStats(EventsDataRecordsEnum.EVENTS_QUEUED)); - stats.setEventsQueued(mTelemetryStorageConsumer.getEventsStats(EventsDataRecordsEnum.EVENTS_DROPPED)); + stats.setEventsDropped(mTelemetryStorageConsumer.getEventsStats(EventsDataRecordsEnum.EVENTS_DROPPED)); + stats.setUpdatesFromSSE(mTelemetryStorageConsumer.popUpdatesFromSSE()); return stats; } diff --git a/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java b/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java index 7bbe3a184..9acb9614a 100644 --- a/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java +++ b/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java @@ -21,6 +21,8 @@ import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.mysegments.MySegmentsUpdateTask; import io.split.android.client.storage.mysegments.MySegmentsStorage; +import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum; +import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; @@ -28,6 +30,7 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; public class MySegmentsUpdateTaskTest { @@ -40,12 +43,15 @@ public class MySegmentsUpdateTaskTest { MySegmentsUpdateTask mTask; + @Mock + private TelemetryRuntimeProducer mTelemetryRuntimeProducer; + String mSegmentToRemove = "MS_TO_REMOVE"; String mCustomerSegment = "CUSTOMER_ID"; @Before public void setup() { - Set oldSegments = new HashSet<>(); + Set oldSegments = new HashSet<>(); oldSegments.add(mCustomerSegment); oldSegments.add(mSegmentToRemove); @@ -55,7 +61,7 @@ public void setup() { @Test public void correctExecution() throws HttpFetcherException { - mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, mSegmentToRemove, mEventsManager); + mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, mSegmentToRemove, mEventsManager, mTelemetryRuntimeProducer); ArgumentCaptor> segmentsCaptor = ArgumentCaptor.forClass(List.class); @@ -71,7 +77,7 @@ public void correctExecution() throws HttpFetcherException { @Test public void correctExecutionToEraseNotInSegments() throws HttpFetcherException { String otherSegment = "OtherSegment"; - mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, otherSegment, mEventsManager); + mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, otherSegment, mEventsManager, mTelemetryRuntimeProducer); ArgumentCaptor> segmentsCaptor = ArgumentCaptor.forClass(List.class); SplitTaskExecutionInfo result = mTask.execute(); @@ -84,12 +90,31 @@ public void correctExecutionToEraseNotInSegments() throws HttpFetcherException { @Test public void storageException() { - mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, mSegmentToRemove, mEventsManager); + mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, mSegmentToRemove, mEventsManager, mTelemetryRuntimeProducer); doThrow(NullPointerException.class).when(mySegmentsStorage).set(any()); SplitTaskExecutionInfo result = mTask.execute(); Assert.assertEquals(SplitTaskExecutionStatus.ERROR, result.getStatus()); + verifyNoInteractions(mTelemetryRuntimeProducer); + } + + @Test + public void successfulAddOperationIsRecordedInTelemetry() { + mTask = new MySegmentsUpdateTask(mySegmentsStorage, true, mSegmentToRemove, mEventsManager, mTelemetryRuntimeProducer); + + mTask.execute(); + + verify(mTelemetryRuntimeProducer).recordUpdatesFromSSE(UpdatesFromSSEEnum.MY_SEGMENTS); + } + + @Test + public void successfulRemoveOperationIsRecordedInTelemetry() { + mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, mSegmentToRemove, mEventsManager, mTelemetryRuntimeProducer); + + mTask.execute(); + + verify(mTelemetryRuntimeProducer).recordUpdatesFromSSE(UpdatesFromSSEEnum.MY_SEGMENTS); } @After @@ -100,4 +125,4 @@ public void tearDown() { private boolean isSegmentRemoved(List segments, String segment) { return !(new HashSet<>(segments).contains(segment)); } -} \ No newline at end of file +} diff --git a/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java b/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java new file mode 100644 index 000000000..1ba725bbb --- /dev/null +++ b/src/test/java/io/split/android/client/service/SplitInPlaceUpdateTaskTest.java @@ -0,0 +1,101 @@ +package io.split.android.client.service; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; + +import io.split.android.client.dtos.Split; +import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.splits.SplitChangeProcessor; +import io.split.android.client.service.splits.SplitInPlaceUpdateTask; +import io.split.android.client.storage.splits.ProcessedSplitChange; +import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum; +import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; + +public class SplitInPlaceUpdateTaskTest { + + @Mock + private SplitsStorage mSplitsStorage; + @Mock + private SplitChangeProcessor mSplitChangeProcessor; + @Mock + private ISplitEventsManager mEventsManager; + @Mock + private TelemetryRuntimeProducer mTelemetryRuntimeProducer; + @Mock + private Split mSplit; + + private SplitInPlaceUpdateTask mSplitInPlaceUpdateTask; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + long changeNumber = 123L; + mSplitInPlaceUpdateTask = new SplitInPlaceUpdateTask( + mSplitsStorage, mSplitChangeProcessor, mEventsManager, + mTelemetryRuntimeProducer, mSplit, changeNumber + ); + } + + @Test + public void sseUpdateIsRecordedInTelemetryWhenOperationIsSuccessful() { + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(new ArrayList<>(), new ArrayList<>(), 0L, 0); + + when(mSplitChangeProcessor.process(mSplit, 123L)).thenReturn(processedSplitChange); + + SplitTaskExecutionInfo result = mSplitInPlaceUpdateTask.execute(); + + verify(mSplitChangeProcessor).process(mSplit, 123L); + verify(mSplitsStorage).update(processedSplitChange); + verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + verify(mTelemetryRuntimeProducer).recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + + assertEquals(result.getStatus(), SplitTaskExecutionStatus.SUCCESS); + } + + @Test + public void exceptionDuringProcessingReturnsErrorExecutionInfo() { + + doThrow(new RuntimeException()).when(mSplitChangeProcessor).process(mSplit, 123L); + + SplitTaskExecutionInfo result = mSplitInPlaceUpdateTask.execute(); + + verify(mSplitChangeProcessor).process(mSplit, 123L); + verify(mSplitsStorage, never()).update(any()); + verify(mEventsManager, never()).notifyInternalEvent(any()); + verify(mTelemetryRuntimeProducer, never()).recordUpdatesFromSSE(any()); + + assertEquals(result.getStatus(), SplitTaskExecutionStatus.ERROR); + } + + @Test + public void exceptionDuringStorageUpdateReturnsErrorExecutionInfo() { + ProcessedSplitChange processedSplitChange = new ProcessedSplitChange(new ArrayList<>(), new ArrayList<>(), 0L, 0); + + when(mSplitChangeProcessor.process(mSplit, 123L)).thenReturn(processedSplitChange); + doThrow(new RuntimeException()).when(mSplitsStorage).update(processedSplitChange); + + SplitTaskExecutionInfo result = mSplitInPlaceUpdateTask.execute(); + + verify(mSplitChangeProcessor).process(mSplit, 123L); + verify(mSplitsStorage).update(processedSplitChange); + verify(mEventsManager, never()).notifyInternalEvent(any()); + verify(mTelemetryRuntimeProducer, never()).recordUpdatesFromSSE(any()); + + assertEquals(result.getStatus(), SplitTaskExecutionStatus.ERROR); + } +} 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 ad610de85..571fd4dba 100644 --- a/src/test/java/io/split/android/client/service/SynchronizerTest.java +++ b/src/test/java/io/split/android/client/service/SynchronizerTest.java @@ -56,7 +56,6 @@ import io.split.android.client.service.impressions.ImpressionsMode; import io.split.android.client.service.impressions.ImpressionsRecorderTask; import io.split.android.client.service.impressions.SaveImpressionsCountTask; -import io.split.android.client.service.impressions.unique.UniqueKeysTracker; import io.split.android.client.service.mysegments.LoadMySegmentsTask; import io.split.android.client.service.mysegments.MySegmentsSyncTask; import io.split.android.client.service.mysegments.MySegmentsTaskFactory; @@ -65,6 +64,7 @@ import io.split.android.client.service.splits.SplitsSyncTask; import io.split.android.client.service.splits.SplitsUpdateTask; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; +import io.split.android.client.service.synchronizer.FeatureFlagsSynchronizer; import io.split.android.client.service.synchronizer.RecorderSyncHelper; import io.split.android.client.service.synchronizer.SynchronizerImpl; import io.split.android.client.service.synchronizer.WorkManagerWrapper; @@ -89,53 +89,53 @@ public class SynchronizerTest { SplitTaskExecutor mTaskExecutor; @Mock - SplitTaskExecutor mSingleThreadedTaskExecutor; + private SplitTaskExecutor mSingleThreadedTaskExecutor; @Mock - SplitApiFacade mSplitApiFacade; + private SplitApiFacade mSplitApiFacade; @Mock - SplitStorageContainer mSplitStorageContainer; + private SplitStorageContainer mSplitStorageContainer; @Mock - PersistentSplitsStorage mPersistentSplitsStorageContainer; + private PersistentSplitsStorage mPersistentSplitsStorageContainer; @Mock - EventsStorage mEventsStorage; + private EventsStorage mEventsStorage; @Mock - ImpressionsStorage mImpressionsStorage; + private ImpressionsStorage mImpressionsStorage; @Mock - TelemetryRuntimeProducer mTelemetryRuntimeProducer; + private TelemetryRuntimeProducer mTelemetryRuntimeProducer; @Mock - RetryBackoffCounterTimerFactory mRetryBackoffFactory; + private RetryBackoffCounterTimerFactory mRetryBackoffFactory; @Mock - RetryBackoffCounterTimer mRetryTimerSplitsSync; + private RetryBackoffCounterTimer mRetryTimerSplitsSync; @Mock - RetryBackoffCounterTimer mRetryTimerSplitsUpdate; + private RetryBackoffCounterTimer mRetryTimerSplitsUpdate; @Mock - RetryBackoffCounterTimer mRetryTimerMySegmentsSync; + private RetryBackoffCounterTimer mRetryTimerMySegmentsSync; @Mock - RetryBackoffCounterTimer mRetryTimerEventsRecorder; + private RetryBackoffCounterTimer mRetryTimerEventsRecorder; @Mock - WorkManager mWorkManager; + private WorkManager mWorkManager; @Mock - SplitTaskFactory mTaskFactory; + private SplitTaskFactory mTaskFactory; @Mock - SplitEventsManager mEventsManager; + private SplitEventsManager mEventsManager; @Mock - WorkManagerWrapper mWorkManagerWrapper; + private WorkManagerWrapper mWorkManagerWrapper; @Mock - MySegmentsTaskFactory mMySegmentsTaskFactory; + private MySegmentsTaskFactory mMySegmentsTaskFactory; @Mock - MySegmentsSynchronizer mMySegmentsSynchronizer; + private MySegmentsSynchronizer mMySegmentsSynchronizer; @Mock - MySegmentsSynchronizerRegistryImpl mMySegmentsSynchronizerRegistry; + private MySegmentsSynchronizerRegistryImpl mMySegmentsSynchronizerRegistry; @Mock - AttributesSynchronizerRegistryImpl mAttributesSynchronizerRegistry; + private AttributesSynchronizerRegistryImpl mAttributesSynchronizerRegistry; @Mock - UniqueKeysTracker mUniqueKeysTracker; + private FeatureFlagsSynchronizer mFeatureFlagsSynchronizer; ImpressionManager mImpressionManager; private final String mUserKey = "user_key"; @@ -189,7 +189,7 @@ public void setup(SplitClientConfig splitClientConfig, ImpressionManagerConfig.M mImpressionManager = Mockito.mock(ImpressionManager.class); mSynchronizer = new SynchronizerImpl(splitClientConfig, mTaskExecutor, mSingleThreadedTaskExecutor, - mSplitStorageContainer, mTaskFactory, mEventsManager, mWorkManagerWrapper, mRetryBackoffFactory, mTelemetryRuntimeProducer, mAttributesSynchronizerRegistry, mMySegmentsSynchronizerRegistry, mImpressionManager); + mTaskFactory, mWorkManagerWrapper, mRetryBackoffFactory, mTelemetryRuntimeProducer, mAttributesSynchronizerRegistry, mMySegmentsSynchronizerRegistry, mImpressionManager, mFeatureFlagsSynchronizer, mSplitStorageContainer.getEventsStorage()); } @Test @@ -201,11 +201,11 @@ public void splitExecutorSchedule() { .impressionsQueueSize(3) .build(); setup(config); + mSynchronizer.startPeriodicFetching(); mSynchronizer.startPeriodicRecording(); - verify(mSingleThreadedTaskExecutor).schedule( - any(SplitsSyncTask.class), anyLong(), anyLong(), - any()); + + verify(mFeatureFlagsSynchronizer).startPeriodicFetching(); verify(mMySegmentsSynchronizerRegistry).scheduleSegmentsSyncTask(); verify(mTaskExecutor).schedule( any(EventsRecorderTask.class), anyLong(), anyLong(), @@ -515,7 +515,8 @@ public void loadLocalData() { .thenReturn(mRetryTimerSplitsUpdate); mSynchronizer = new SynchronizerImpl(config, executor, executor, - mSplitStorageContainer, mTaskFactory, mEventsManager, mWorkManagerWrapper, mRetryBackoffFactory, mTelemetryRuntimeProducer, mAttributesSynchronizerRegistry, mMySegmentsSynchronizerRegistry, mImpressionManager); + mTaskFactory, mWorkManagerWrapper, mRetryBackoffFactory, mTelemetryRuntimeProducer, mAttributesSynchronizerRegistry, mMySegmentsSynchronizerRegistry, mImpressionManager, + mFeatureFlagsSynchronizer, mSplitStorageContainer.getEventsStorage()); LoadMySegmentsTask loadMySegmentsTask = mock(LoadMySegmentsTask.class); when(loadMySegmentsTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_MY_SYGMENTS)); @@ -528,8 +529,7 @@ public void loadLocalData() { mSynchronizer.loadAttributesFromCache(); verify(mMySegmentsSynchronizerRegistry).loadMySegmentsFromCache(); verify(mAttributesSynchronizerRegistry).loadAttributesFromCache(); - verify(mEventsManager) - .notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + verify(mFeatureFlagsSynchronizer).loadFromCache(); } @Test @@ -550,11 +550,12 @@ public void loadAndSynchronizeSplits() { .thenReturn(mRetryTimerSplitsUpdate); mSynchronizer = new SynchronizerImpl(config, executor, executor, - mSplitStorageContainer, mTaskFactory, mEventsManager, mWorkManagerWrapper, mRetryBackoffFactory, mTelemetryRuntimeProducer, mAttributesSynchronizerRegistry, mMySegmentsSynchronizerRegistry, mImpressionManager); + mTaskFactory, mWorkManagerWrapper, mRetryBackoffFactory, mTelemetryRuntimeProducer, mAttributesSynchronizerRegistry, mMySegmentsSynchronizerRegistry, mImpressionManager, + mFeatureFlagsSynchronizer, mSplitStorageContainer.getEventsStorage()); mSynchronizer.loadAndSynchronizeSplits(); - verify(mEventsManager, times(1)) - .notifyInternalEvent(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); - verify(mRetryTimerSplitsSync, times(1)).start(); + + + verify(mFeatureFlagsSynchronizer).loadAndSynchronize(); } @Test @@ -568,14 +569,13 @@ public void destroy() { .thenReturn(mRetryTimerSplitsSync) .thenReturn(mRetryTimerSplitsUpdate); mSynchronizer = new SynchronizerImpl(config, mTaskExecutor, mSingleThreadedTaskExecutor, - mSplitStorageContainer, mTaskFactory, mEventsManager, mWorkManagerWrapper, + mTaskFactory, mWorkManagerWrapper, mRetryBackoffFactory, mTelemetryRuntimeProducer, mAttributesSynchronizerRegistry, - mMySegmentsSynchronizerRegistry, impressionManager); + mMySegmentsSynchronizerRegistry, impressionManager, mFeatureFlagsSynchronizer, mSplitStorageContainer.getEventsStorage()); mSynchronizer.destroy(); - verify(mRetryTimerSplitsUpdate).stop(); - verify(mRetryTimerSplitsSync).stop(); + verify(mFeatureFlagsSynchronizer).stopSynchronization(); verify(impressionManager).flush(); verify(mRetryTimerEventsRecorder).setTask(any(EventsRecorderTask.class)); verify(mRetryTimerEventsRecorder).start(); @@ -672,7 +672,7 @@ public void beingNotifiedOfSplitsSyncTaskTriggersSplitsLoad() { mSynchronizer.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); - verify(mTaskExecutor).submit(task, null); + verify(mFeatureFlagsSynchronizer).submitLoadingTask(null); } @Test @@ -692,8 +692,7 @@ public void synchronizeSplitsWithSince() { mSynchronizer.synchronizeSplits(1000); - verify(mRetryTimerSplitsUpdate).setTask(task, null); - verify(mRetryTimerSplitsUpdate).start(); + verify(mFeatureFlagsSynchronizer).synchronize(1000); } diff --git a/src/test/java/io/split/android/client/service/sseclient/BackgroundDisconnectionTaskTest.java b/src/test/java/io/split/android/client/service/sseclient/BackgroundDisconnectionTaskTest.java new file mode 100644 index 000000000..8b5c7cf51 --- /dev/null +++ b/src/test/java/io/split/android/client/service/sseclient/BackgroundDisconnectionTaskTest.java @@ -0,0 +1,45 @@ +package io.split.android.client.service.sseclient; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Test; + +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.service.sseclient.sseclient.PushNotificationManager; +import io.split.android.client.service.sseclient.sseclient.SseClient; +import io.split.android.client.service.sseclient.sseclient.SseRefreshTokenTimer; + +public class BackgroundDisconnectionTaskTest { + + private SseClient mSseClient; + private SseRefreshTokenTimer mTimer; + private PushNotificationManager.BackgroundDisconnectionTask mTask; + + @Before + public void setUp() { + mSseClient = mock(SseClient.class); + mTimer = mock(SseRefreshTokenTimer.class); + mTask = new PushNotificationManager.BackgroundDisconnectionTask(mSseClient, mTimer); + } + + @Test + public void executionDisconnectsClientAndCancelsTimer() { + mTask.execute(); + + verify(mSseClient).disconnect(); + verify(mTimer).cancel(); + } + + @Test + public void executionReturnsCorrectResult() { + SplitTaskExecutionInfo result = mTask.execute(); + + assertEquals(SplitTaskType.GENERIC_TASK, result.getTaskType()); + assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); + } +} diff --git a/src/test/java/io/split/android/client/service/sseclient/PushNotificationManagerTest.java b/src/test/java/io/split/android/client/service/sseclient/PushNotificationManagerTest.java index 1f88dd9dd..25d22fc2b 100644 --- a/src/test/java/io/split/android/client/service/sseclient/PushNotificationManagerTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/PushNotificationManagerTest.java @@ -1,5 +1,17 @@ package io.split.android.client.service.sseclient; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.longThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -25,6 +37,7 @@ import io.split.android.client.service.sseclient.sseclient.PushNotificationManager; import io.split.android.client.service.sseclient.sseclient.SseAuthenticationResult; import io.split.android.client.service.sseclient.sseclient.SseAuthenticator; +import io.split.android.client.service.sseclient.sseclient.SseClient; import io.split.android.client.service.sseclient.sseclient.SseDisconnectionTimer; import io.split.android.client.service.sseclient.sseclient.SseRefreshTokenTimer; import io.split.android.client.telemetry.model.OperationType; @@ -32,45 +45,31 @@ import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; import io.split.android.fake.SseClientMock; -import static java.lang.Thread.sleep; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.longThat; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - public class PushNotificationManagerTest { private static final String DUMMY_TOKEN = "DUMMY_TOKEN"; private static final int POOL_SIZE = 1; @Mock - ScheduledThreadPoolExecutor mExecutor; - - @Mock - SseAuthenticator mAuthenticator; + private SseAuthenticator mAuthenticator; @Mock - PushManagerEventBroadcaster mBroadcasterChannel; + private PushManagerEventBroadcaster mBroadcasterChannel; @Mock - SseRefreshTokenTimer mRefreshTokenTimer; + private SseRefreshTokenTimer mRefreshTokenTimer; @Mock - SseDisconnectionTimer mDisconnectionTimer; + private SseDisconnectionTimer mDisconnectionTimer; @Mock - SseJwtToken mJwt; + private SseJwtToken mJwt; @Mock - SseAuthenticationResult mResult; + private SseAuthenticationResult mResult; @Mock - TelemetryRuntimeProducer mTelemetryRuntimeProducer; + private TelemetryRuntimeProducer mTelemetryRuntimeProducer; PushNotificationManager mPushManager; @@ -78,7 +77,7 @@ public class PushNotificationManagerTest { @Before public void setup() throws URISyntaxException { - MockitoAnnotations.initMocks(this); + MockitoAnnotations.openMocks(this); mUri = new URI("http://api/sse"); } @@ -88,22 +87,21 @@ public void connectOk() throws InterruptedException { SseClientMock sseClient = new SseClientMock(); sseClient.mConnectLatch = new CountDownLatch(1); mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, - mDisconnectionTimer, mTelemetryRuntimeProducer, new ScheduledThreadPoolExecutor(POOL_SIZE)); + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); long time = System.currentTimeMillis(); mPushManager.start(); sseClient.mConnectLatch.await(2, TimeUnit.SECONDS); time = System.currentTimeMillis() - time; - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(PushStatusEvent.class); - verify(mBroadcasterChannel, times(1)).pushMessage(messageCaptor.capture()); - Assert.assertEquals(messageCaptor.getValue().getMessage(), PushStatusEvent.EventType.PUSH_SUBSYSTEM_UP); + verify(mBroadcasterChannel).pushMessage(argThat(argument -> argument.getMessage().equals(PushStatusEvent.EventType.PUSH_SUBSYSTEM_UP))); + verify(mBroadcasterChannel).pushMessage(argThat(argument -> argument.getMessage().equals(PushStatusEvent.EventType.PUSH_DELAY_RECEIVED))); ArgumentCaptor issuedAt = ArgumentCaptor.forClass(Long.class); ArgumentCaptor expirationTime = ArgumentCaptor.forClass(Long.class); verify(mRefreshTokenTimer, times(1)).schedule(issuedAt.capture(), expirationTime.capture()); Assert.assertEquals(1000L, issuedAt.getValue().longValue()); Assert.assertEquals(10000L, expirationTime.getValue().longValue()); - Assert.assertTrue(time < 2000); + assertTrue(time < 2000); } @Test @@ -112,23 +110,21 @@ public void connectOkWithDelay() throws InterruptedException, HttpException { sseClient.mConnectLatch = new CountDownLatch(1); setupOkAuthResponse(4); mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, - mDisconnectionTimer, mTelemetryRuntimeProducer, new ScheduledThreadPoolExecutor(POOL_SIZE)); + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); long time = System.currentTimeMillis(); mPushManager.start(); sseClient.mConnectLatch.await(10, TimeUnit.SECONDS); time = System.currentTimeMillis() - time; - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(PushStatusEvent.class); - verify(mBroadcasterChannel, times(1)).pushMessage(messageCaptor.capture()); - Assert.assertEquals(messageCaptor.getValue().getMessage(), PushStatusEvent.EventType.PUSH_SUBSYSTEM_UP); - + verify(mBroadcasterChannel, times(1)).pushMessage(argThat(argument -> argument.getMessage().equals(PushStatusEvent.EventType.PUSH_SUBSYSTEM_UP))); + verify(mBroadcasterChannel).pushMessage(argThat(argument -> argument.getMessage().equals(PushStatusEvent.EventType.PUSH_DELAY_RECEIVED))); ArgumentCaptor issuedAt = ArgumentCaptor.forClass(Long.class); ArgumentCaptor expirationTime = ArgumentCaptor.forClass(Long.class); - verify(mRefreshTokenTimer, times(1)).schedule(issuedAt.capture(), expirationTime.capture()); + verify(mRefreshTokenTimer).schedule(issuedAt.capture(), expirationTime.capture()); Assert.assertEquals(1000L, issuedAt.getValue().longValue()); Assert.assertEquals(10000L, expirationTime.getValue().longValue()); - Assert.assertTrue(time > 3000); + assertTrue(time > 3000); } @Test @@ -136,11 +132,11 @@ public void connectClientError() throws InterruptedException, HttpException { SseClientMock sseClient = new SseClientMock(); sseClient.mConnectLatch = new CountDownLatch(1); mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, - mDisconnectionTimer, mTelemetryRuntimeProducer, new ScheduledThreadPoolExecutor(POOL_SIZE)); + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); SseAuthenticationResult result = new SseAuthenticationResult(false, false, false, 0, null); - when(mAuthenticator.authenticate()).thenReturn(result); + when(mAuthenticator.authenticate(60L)).thenReturn(result); mPushManager.start(); sseClient.mConnectLatch.await(2, TimeUnit.SECONDS); @@ -154,11 +150,11 @@ public void connectStreamingDisabled() throws InterruptedException, HttpExceptio SseClientMock sseClient = new SseClientMock(); sseClient.mConnectLatch = new CountDownLatch(1); mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, - mDisconnectionTimer, mTelemetryRuntimeProducer, new ScheduledThreadPoolExecutor(POOL_SIZE)); + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); SseAuthenticationResult result = new SseAuthenticationResult(true, false, false, 0, null); - when(mAuthenticator.authenticate()).thenReturn(result); + when(mAuthenticator.authenticate(60L)).thenReturn(result); mPushManager.start(); sseClient.mConnectLatch.await(2, TimeUnit.SECONDS); @@ -172,11 +168,11 @@ public void connectOtherError() throws InterruptedException, HttpException { SseClientMock sseClient = new SseClientMock(); sseClient.mConnectLatch = new CountDownLatch(1); mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, - mDisconnectionTimer, mTelemetryRuntimeProducer, new ScheduledThreadPoolExecutor(POOL_SIZE)); + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); SseAuthenticationResult result = new SseAuthenticationResult(false, true, false, 0, null); - when(mAuthenticator.authenticate()).thenReturn(result); + when(mAuthenticator.authenticate(60L)).thenReturn(result); mPushManager.start(); sseClient.mConnectLatch.await(2, TimeUnit.SECONDS); @@ -190,7 +186,7 @@ public void pause() throws InterruptedException, HttpException { SseClientMock sseClient = new SseClientMock(); sseClient.mConnectLatch = new CountDownLatch(1); mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, - mDisconnectionTimer, mTelemetryRuntimeProducer, new ScheduledThreadPoolExecutor(POOL_SIZE)); + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); setupOkAuthResponse(); mPushManager.start(); sseClient.mConnectLatch.await(2, TimeUnit.SECONDS); @@ -206,7 +202,7 @@ public void resume() throws InterruptedException, HttpException { SseClientMock sseClient = new SseClientMock(); sseClient.mConnectLatch = new CountDownLatch(1); mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, - mDisconnectionTimer, mTelemetryRuntimeProducer, new ScheduledThreadPoolExecutor(POOL_SIZE)); + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); setupOkAuthResponse(); mPushManager.start(); sseClient.mConnectLatch.await(2, TimeUnit.SECONDS); @@ -231,11 +227,11 @@ public void connectErrorTracksAuthRejectionInTelemetry() throws InterruptedExcep SseClientMock sseClient = new SseClientMock(); sseClient.mConnectLatch = new CountDownLatch(1); mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, - mDisconnectionTimer, mTelemetryRuntimeProducer, new ScheduledThreadPoolExecutor(POOL_SIZE)); + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); SseAuthenticationResult result = new SseAuthenticationResult(false, false, false, 0, null); - when(mAuthenticator.authenticate()).thenReturn(result); + when(mAuthenticator.authenticate(60L)).thenReturn(result); mPushManager.start(); sseClient.mConnectLatch.await(2, TimeUnit.SECONDS); @@ -248,12 +244,12 @@ public void connectErrorTracksSyncErrorInTelemetryWhenThereIsHttpStatus() throws SseClientMock sseClient = new SseClientMock(); sseClient.mConnectLatch = new CountDownLatch(1); mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, - mDisconnectionTimer, mTelemetryRuntimeProducer, new ScheduledThreadPoolExecutor(POOL_SIZE)); + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); SseAuthenticationResult result = new SseAuthenticationResult( false, false, false, 0, null, 500); - when(mAuthenticator.authenticate()).thenReturn(result); + when(mAuthenticator.authenticate(60L)).thenReturn(result); mPushManager.start(); sseClient.mConnectLatch.await(2, TimeUnit.SECONDS); @@ -269,12 +265,25 @@ public void authenticationLatencyIsTracked() throws InterruptedException { verify(mTelemetryRuntimeProducer).recordSyncLatency(eq(OperationType.TOKEN), anyLong()); } + @Test + public void stopDisconnectsClient() { + SseClient sseClient = mock(SseClient.class); + mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); + + mPushManager.stop(); + + verify(mDisconnectionTimer).cancel(); + verify(mRefreshTokenTimer).cancel(); + verify(sseClient).disconnect(); + } + private void performSuccessfulConnection() throws InterruptedException { setupOkAuthResponse(); SseClientMock sseClient = new SseClientMock(); sseClient.mConnectLatch = new CountDownLatch(1); mPushManager = new PushNotificationManager(mBroadcasterChannel, mAuthenticator, sseClient, mRefreshTokenTimer, - mDisconnectionTimer, mTelemetryRuntimeProducer, new ScheduledThreadPoolExecutor(POOL_SIZE)); + mDisconnectionTimer, mTelemetryRuntimeProducer, 60L, new ScheduledThreadPoolExecutor(POOL_SIZE)); mPushManager.start(); sseClient.mConnectLatch.await(2, TimeUnit.SECONDS); @@ -289,7 +298,6 @@ private void setupOkAuthResponse(long delay) { when(mJwt.getIssuedAtTime()).thenReturn(1000L); when(mJwt.getExpirationTime()).thenReturn(10000L); - when(mJwt.getRawJwt()).thenReturn(DUMMY_TOKEN); when(mResult.isSuccess()).thenReturn(true); @@ -298,11 +306,11 @@ private void setupOkAuthResponse(long delay) { when(mResult.getJwtToken()).thenReturn(mJwt); when(mResult.getSseConnectionDelay()).thenReturn(delay); - when(mAuthenticator.authenticate()).thenReturn(mResult); + when(mAuthenticator.authenticate(60L)).thenReturn(mResult); } private BufferedReader dummyData() { - InputStream inputStream = new ByteArrayInputStream("hola".getBytes(Charset.forName("UTF-8"))); + InputStream inputStream = new ByteArrayInputStream("hola" .getBytes(Charset.forName("UTF-8"))); return new BufferedReader(new InputStreamReader(inputStream)); } diff --git a/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java b/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java index ebbeff9a5..03de536c7 100644 --- a/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java @@ -1,25 +1,40 @@ package io.split.android.client.service.sseclient; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.Objects; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; +import io.split.android.client.common.CompressionType; +import io.split.android.client.common.CompressionUtilProvider; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutor; +import io.split.android.client.service.executor.SplitTaskFactory; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.service.splits.SplitInPlaceUpdateTask; import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; import io.split.android.client.service.sseclient.reactor.SplitUpdatesWorker; import io.split.android.client.service.synchronizer.Synchronizer; - -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.client.utils.CompressionUtil; +import io.split.android.fake.SplitTaskExecutorStub; public class SplitUpdateWorkerTest { @@ -29,23 +44,41 @@ public class SplitUpdateWorkerTest { @Mock Synchronizer mSynchronizer; + @Mock + private SplitsStorage mSplitsStorage; + @Mock + private CompressionUtilProvider mCompressionUtilProvider; + @Mock + private SplitTaskExecutor mSplitTaskExecutor; + @Mock + private SplitTaskFactory mSplitTaskFactory; + @Mock + private SplitUpdatesWorker.Base64Decoder mBase64Decoder; + + private static final String TEST_SPLIT = "{\"trafficTypeName\":\"account\",\"name\":\"android_test_2\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1955610140,\"seed\":-633015570,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733409158,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SPLIT_TREATMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":{\"split\":\"android_test_3\",\"treatments\":[\"on\"]},\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in split android_test_3 treatment [on]\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}"; @Before public void setup() { MockitoAnnotations.initMocks(this); mNotificationsQueue = new ArrayBlockingQueue<>(50); - mWorker = new SplitUpdatesWorker(mSynchronizer, mNotificationsQueue); + mWorker = new SplitUpdatesWorker(mSynchronizer, + mNotificationsQueue, + mSplitsStorage, + mCompressionUtilProvider, + mSplitTaskExecutor, + mSplitTaskFactory, + mBase64Decoder); mWorker.start(); } @Test public void splitsUpdateReceived() throws InterruptedException { Long changeNumber = 1000L; - SplitsChangeNotification notification = Mockito.mock(SplitsChangeNotification.class); + SplitsChangeNotification notification = getLegacyNotification(); when(notification.getChangeNumber()).thenReturn(changeNumber); mNotificationsQueue.offer(notification); - Thread.sleep(2000); + Thread.sleep(500); ArgumentCaptor changeNumberCaptor = ArgumentCaptor.forClass(Long.class); verify(mSynchronizer, times(1)) @@ -56,14 +89,14 @@ public void splitsUpdateReceived() throws InterruptedException { @Test public void severalSplitsUpdateReceived() throws InterruptedException { Long changeNumber = 1000L; - SplitsChangeNotification notification = Mockito.mock(SplitsChangeNotification.class); + SplitsChangeNotification notification = getLegacyNotification(); when(notification.getChangeNumber()).thenReturn(changeNumber); mNotificationsQueue.offer(notification); mNotificationsQueue.offer(notification); mNotificationsQueue.offer(notification); mNotificationsQueue.offer(notification); - Thread.sleep(2000); + Thread.sleep(500); verify(mSynchronizer, times(4)) .synchronizeSplits(anyLong()); @@ -73,13 +106,235 @@ public void severalSplitsUpdateReceived() throws InterruptedException { public void stopped() throws InterruptedException { mWorker.stop(); Long changeNumber = 1000L; - SplitsChangeNotification notification = Mockito.mock(SplitsChangeNotification.class); + SplitsChangeNotification notification = getLegacyNotification(); + when(notification.getChangeNumber()).thenReturn(changeNumber); + mNotificationsQueue.offer(notification); + + Thread.sleep(500); + + verify(mSynchronizer, never()) + .synchronizeSplits(anyLong()); + } + + @Test + public void lowerChangeNumberThanStoredDoesNothing() { + long changeNumber = 1000L; + when(mSplitsStorage.getTill()).thenReturn(changeNumber + 1); + SplitsChangeNotification notification = getLegacyNotification(); when(notification.getChangeNumber()).thenReturn(changeNumber); mNotificationsQueue.offer(notification); - Thread.sleep(2000); + verify(mSynchronizer, never()) + .synchronizeSplits(anyLong()); + } + + @Test + public void nullPreviousChangeNumberDoesNothing() { + when(mSplitsStorage.getTill()).thenReturn(1000L); + SplitsChangeNotification notification = getNewNotification(); + when(notification.getPreviousChangeNumber()).thenReturn(null); + mNotificationsQueue.offer(notification); + + verify(mSynchronizer, never()) + .synchronizeSplits(anyLong()); + } + + @Test + public void zeroPreviousChangeNumberDoesNothing() { + when(mSplitsStorage.getTill()).thenReturn(1000L); + SplitsChangeNotification notification = getNewNotification(); + when(notification.getPreviousChangeNumber()).thenReturn(0L); + mNotificationsQueue.offer(notification); + + verify(mSynchronizer, never()) + .synchronizeSplits(anyLong()); + } + + @Test + public void legacyNotificationDoesNotSubmitTaskInExecutor() { + long changeNumber = 1000L; + when(mSplitsStorage.getTill()).thenReturn(changeNumber - 1); + SplitsChangeNotification notification = getLegacyNotification(); + when(notification.getChangeNumber()).thenReturn(changeNumber); + mNotificationsQueue.offer(notification); + + verify(mSplitTaskExecutor, never()) + .submit(any(), any()); + } + + @Test + public void newNotificationSubmitsTaskInExecutor() throws InterruptedException { + long changeNumber = 1000L; + byte[] bytes = TEST_SPLIT.getBytes(); + SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); + SplitsChangeNotification notification = getNewNotification(); + CompressionUtil mockCompressor = mock(CompressionUtil.class); + + when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); + when(mSplitsStorage.getTill()).thenReturn(changeNumber); + when(notification.getChangeNumber()).thenReturn(changeNumber + 1); + when(notification.getCompressionType()).thenReturn(CompressionType.NONE); + when(mockCompressor.decompress(any())).thenReturn(bytes); + + when(mCompressionUtilProvider.get(any())).thenReturn(mockCompressor); + when(mBase64Decoder.decode(anyString())).thenReturn(bytes); + mNotificationsQueue.offer(notification); + Thread.sleep(500); + + verify(mSplitTaskExecutor).submit(eq(updateTask), argThat(Objects::nonNull)); verify(mSynchronizer, never()) .synchronizeSplits(anyLong()); } + + @Test + public void synchronizeSplitsIsCalledOnSynchronizerWhenTaskFails() throws InterruptedException { + initWorkerWithStubExecutor(); + + long changeNumber = 1000L; + SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); + SplitsChangeNotification notification = getNewNotification(); + CompressionUtil mockCompressor = mock(CompressionUtil.class); + + when(updateTask.execute()).thenAnswer(invocation -> SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC)); + when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); + when(mSplitsStorage.getTill()).thenReturn(changeNumber); + when(notification.getChangeNumber()).thenReturn(changeNumber + 1); + when(notification.getCompressionType()).thenReturn(CompressionType.NONE); + when(mockCompressor.decompress(any())).thenReturn(TEST_SPLIT.getBytes()); + when(mCompressionUtilProvider.get(any())).thenReturn(mockCompressor); + + mNotificationsQueue.offer(notification); + Thread.sleep(500); + + verify(mSynchronizer).synchronizeSplits(changeNumber + 1); + } + + @Test + public void synchronizeSplitsIsCalledOnSynchronizerWhenParsingFails() throws InterruptedException { + initWorkerWithStubExecutor(); + + long changeNumber = 1000L; + SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); + SplitsChangeNotification notification = getNewNotification(); + CompressionUtil mockCompressor = mock(CompressionUtil.class); + + when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); + when(mSplitsStorage.getTill()).thenReturn(changeNumber); + when(notification.getChangeNumber()).thenReturn(changeNumber + 1); + when(notification.getCompressionType()).thenReturn(CompressionType.NONE); + when(mockCompressor.decompress(any())).thenReturn("malformed_split".getBytes()); + when(mCompressionUtilProvider.get(any())).thenReturn(mockCompressor); + + mNotificationsQueue.offer(notification); + Thread.sleep(800); + + verify(mSynchronizer).synchronizeSplits(changeNumber + 1); + } + + @Test + public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToException() throws InterruptedException { + long changeNumber = 1000L; + SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); + SplitsChangeNotification notification = getNewNotification(); + CompressionUtil mockCompressor = mock(CompressionUtil.class); + + when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); + when(mSplitsStorage.getTill()).thenReturn(changeNumber); + when(notification.getChangeNumber()).thenReturn(changeNumber + 1); + when(notification.getCompressionType()).thenReturn(CompressionType.NONE); + when(mockCompressor.decompress(any())).thenThrow(new RuntimeException("test")); + when(mCompressionUtilProvider.get(any())).thenReturn(mockCompressor); + + mNotificationsQueue.offer(notification); + Thread.sleep(500); + + verify(mSynchronizer).synchronizeSplits(changeNumber + 1); + } + + @Test + public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToNullDecompressor() throws InterruptedException { + long changeNumber = 1000L; + SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); + SplitsChangeNotification notification = getNewNotification(); + + when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); + when(mSplitsStorage.getTill()).thenReturn(changeNumber); + when(notification.getChangeNumber()).thenReturn(changeNumber + 1); + when(notification.getCompressionType()).thenReturn(CompressionType.NONE); + when(mCompressionUtilProvider.get(any())).thenReturn(null); + + mNotificationsQueue.offer(notification); + Thread.sleep(500); + + verify(mSynchronizer).synchronizeSplits(changeNumber + 1); + } + + @Test + public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToNullDecompressedBytes() throws InterruptedException { + long changeNumber = 1000L; + SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); + SplitsChangeNotification notification = getNewNotification(); + CompressionUtil mockCompressor = mock(CompressionUtil.class); + + when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); + when(mSplitsStorage.getTill()).thenReturn(changeNumber); + when(notification.getChangeNumber()).thenReturn(changeNumber + 1); + when(notification.getCompressionType()).thenReturn(CompressionType.NONE); + when(mockCompressor.decompress(any())).thenReturn(null); + when(mCompressionUtilProvider.get(any())).thenReturn(mockCompressor); + + mNotificationsQueue.offer(notification); + Thread.sleep(500); + + verify(mSynchronizer).synchronizeSplits(changeNumber + 1); + } + + @Test + public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToFailedBase64Decoding() throws InterruptedException { + long changeNumber = 1000L; + SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); + SplitsChangeNotification notification = getNewNotification(); + CompressionUtil mockCompressor = mock(CompressionUtil.class); + + when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); + when(mSplitsStorage.getTill()).thenReturn(changeNumber); + when(notification.getChangeNumber()).thenReturn(changeNumber + 1); + when(notification.getCompressionType()).thenReturn(CompressionType.NONE); + when(mockCompressor.decompress(any())).thenReturn("malformed_split".getBytes()); + when(mCompressionUtilProvider.get(any())).thenReturn(mockCompressor); + when(mBase64Decoder.decode(any())).thenReturn(null); + + mNotificationsQueue.offer(notification); + Thread.sleep(500); + + verify(mSynchronizer).synchronizeSplits(changeNumber + 1); + } + + private void initWorkerWithStubExecutor() { + mWorker.stop(); + mWorker = new SplitUpdatesWorker(mSynchronizer, + mNotificationsQueue, + mSplitsStorage, + mCompressionUtilProvider, + new SplitTaskExecutorStub(), + mSplitTaskFactory, + mBase64Decoder); + mWorker.start(); + } + + private static SplitsChangeNotification getLegacyNotification() { + SplitsChangeNotification mock = mock(SplitsChangeNotification.class); + when(mock.getChangeNumber()).thenReturn(1000L); + return mock; + } + + private static SplitsChangeNotification getNewNotification() { + SplitsChangeNotification mock = mock(SplitsChangeNotification.class); + when(mock.getCompressionType()).thenReturn(CompressionType.ZLIB); + when(mock.getData()).thenReturn(TEST_SPLIT); + when(mock.getPreviousChangeNumber()).thenReturn(1000L); + when(mock.getChangeNumber()).thenReturn(2000L); + return mock; + } } diff --git a/src/test/java/io/split/android/client/service/sseclient/SseAuthenticatorTest.java b/src/test/java/io/split/android/client/service/sseclient/SseAuthenticatorTest.java index c81c4e43c..351e31f6f 100644 --- a/src/test/java/io/split/android/client/service/sseclient/SseAuthenticatorTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/SseAuthenticatorTest.java @@ -5,8 +5,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import androidx.core.util.Pair; - import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -20,6 +18,7 @@ import java.util.Map; import java.util.Set; +import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.http.HttpSseAuthTokenFetcher; import io.split.android.client.service.sseclient.sseclient.SseAuthenticationResult; @@ -55,7 +54,7 @@ public void successfulRequest() throws InvalidJwtTokenException, HttpFetcherExce when(mFetcher.execute(any(), any())).thenReturn(mResponse); SseAuthenticator authenticator = new SseAuthenticator(mFetcher, mJwtParser); - SseAuthenticationResult result = authenticator.authenticate(); + SseAuthenticationResult result = authenticator.authenticate(60L); SseJwtToken respToken = result.getJwtToken(); Assert.assertTrue(result.isPushEnabled()); @@ -75,7 +74,7 @@ public void tokenParseError() throws InvalidJwtTokenException, HttpFetcherExcept when(mFetcher.execute(any(), any())).thenReturn(mResponse); SseAuthenticator authenticator = new SseAuthenticator(mFetcher, mJwtParser); - SseAuthenticationResult result = authenticator.authenticate(); + SseAuthenticationResult result = authenticator.authenticate(60L); Assert.assertFalse(result.isPushEnabled()); Assert.assertFalse(result.isSuccess()); @@ -91,7 +90,7 @@ public void recoverableError() throws HttpFetcherException { when(mFetcher.execute(any(), any())).thenThrow(HttpFetcherException.class); SseAuthenticator authenticator = new SseAuthenticator(mFetcher, mJwtParser); - SseAuthenticationResult result = authenticator.authenticate(); + SseAuthenticationResult result = authenticator.authenticate(60L); Assert.assertFalse(result.isPushEnabled()); Assert.assertFalse(result.isSuccess()); @@ -108,7 +107,7 @@ public void nonRecoverableError() throws HttpFetcherException { when(mFetcher.execute(any(), any())).thenReturn(mResponse); SseAuthenticator authenticator = new SseAuthenticator(mFetcher, mJwtParser); - SseAuthenticationResult result = authenticator.authenticate(); + SseAuthenticationResult result = authenticator.authenticate(60L); Assert.assertFalse(result.isPushEnabled()); Assert.assertFalse(result.isSuccess()); @@ -130,7 +129,7 @@ public void registeredKeysAreUsedInFetcher() throws HttpFetcherException { usersSet.add("user2"); map.put("users", usersSet); - authenticator.authenticate(); + authenticator.authenticate(60L); verify(mFetcher).execute(map, null); } @@ -151,7 +150,7 @@ public void unregisteredKeysAreNotUsedInFetcher() throws HttpFetcherException { usersSet.add("user3"); map.put("users", usersSet); - authenticator.authenticate(); + authenticator.authenticate(60L); verify(mFetcher).execute(map, null); } diff --git a/src/test/java/io/split/android/client/service/sseclient/SyncManagerTest.java b/src/test/java/io/split/android/client/service/sseclient/SyncManagerTest.java index 7e4988b1c..1fb9ff16b 100644 --- a/src/test/java/io/split/android/client/service/sseclient/SyncManagerTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/SyncManagerTest.java @@ -15,6 +15,7 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.service.sseclient.feedbackchannel.BroadcastedEventListener; +import io.split.android.client.service.sseclient.feedbackchannel.DelayStatusEvent; import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent; import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent.EventType; @@ -23,7 +24,7 @@ import io.split.android.client.service.sseclient.reactor.SplitUpdatesWorker; import io.split.android.client.service.sseclient.sseclient.BackoffCounterTimer; import io.split.android.client.service.sseclient.sseclient.PushNotificationManager; -import io.split.android.client.service.synchronizer.SyncManager; +import io.split.android.client.service.synchronizer.SyncGuardian; import io.split.android.client.service.synchronizer.SyncManagerImpl; import io.split.android.client.service.synchronizer.Synchronizer; import io.split.android.client.shared.UserConsent; @@ -32,32 +33,24 @@ public class SyncManagerTest { @Mock - SplitClientConfig mConfig; - + private SplitClientConfig mConfig; @Mock - Synchronizer mSynchronizer; - + private Synchronizer mSynchronizer; @Mock - PushNotificationManager mPushNotificationManager; - + private PushNotificationManager mPushNotificationManager; @Spy - PushManagerEventBroadcaster mPushManagerEventBroadcaster; - + private PushManagerEventBroadcaster mPushManagerEventBroadcaster; @Mock - SplitUpdatesWorker mSplitsUpdateWorker; - + private SplitUpdatesWorker mSplitsUpdateWorker; @Mock - MySegmentsUpdateWorker mMySegmentUpdateWorker; - + private MySegmentsUpdateWorker mMySegmentUpdateWorker; @Mock - BackoffCounterTimer mBackoffTimer; - + private BackoffCounterTimer mBackoffTimer; @Mock - TelemetrySynchronizer mTelemetrySynchronizer; - - - SyncManager mSyncManager; - + private SyncGuardian mSyncGuardian; + @Mock + private TelemetrySynchronizer mTelemetrySynchronizer; + private SyncManagerImpl mSyncManager; @Before public void setup() { @@ -66,7 +59,7 @@ public void setup() { mSyncManager = new SyncManagerImpl( mConfig, mSynchronizer, mPushNotificationManager, mSplitsUpdateWorker, mPushManagerEventBroadcaster, - mBackoffTimer, mTelemetrySynchronizer); + mBackoffTimer, mSyncGuardian, mTelemetrySynchronizer); ((MySegmentsUpdateWorkerRegistry) mSyncManager).registerMySegmentsUpdateWorker("user_key", mMySegmentUpdateWorker); when(mConfig.streamingEnabled()).thenReturn(true); @@ -142,7 +135,6 @@ public void stopCallsDestroyOnTelemetrySynchronizer() { @Test public void pauseCallsFlushOnTelemetrySynchronizer() { mSyncManager.pause(); - verify(mTelemetrySynchronizer).flush(); } @@ -217,6 +209,66 @@ public void stopUserConsentUnknown() { testStopUserConsentNotGranted(UserConsent.UNKNOWN); } + @Test + public void resumeCallsSynchronizeSplitsWhenSseClientIsDisconnectedAndSyncGuardianMustSyncIsTrue() { + when(mPushNotificationManager.isSseClientDisconnected()).thenReturn(true); + when(mSyncGuardian.mustSync()).thenReturn(true); + + mSyncManager.resume(); + + verify(mSynchronizer).synchronizeSplits(); + } + + @Test + public void pauseInitializesSyncGuardian() { + mSyncManager.pause(); + verify(mSyncGuardian).initialize(); + } + + @Test + public void resumeDoesNotCallSynchronizeSplitsWhenSyncGuardianMustSyncIsNotTrue() { + when(mPushNotificationManager.isSseClientDisconnected()).thenReturn(true); + when(mSyncGuardian.mustSync()).thenReturn(false); + + mSyncManager.resume(); + + verify(mSynchronizer, never()).synchronizeSplits(); + } + + @Test + public void syncGuardianIsNotCheckedWhenStreamingIsDisabled() { + when(mConfig.streamingEnabled()).thenReturn(false); + + mSyncManager.resume(); + + verify(mSyncGuardian, never()).mustSync(); + verify(mPushNotificationManager, never()).isSseClientDisconnected(); + } + + @Test + public void syncGuardianIsNotCheckedWhenSyncIsDisabled() { + when(mConfig.syncEnabled()).thenReturn(false); + + mSyncManager.resume(); + + verify(mSyncGuardian, never()).mustSync(); + verify(mPushNotificationManager, never()).isSseClientDisconnected(); + } + + @Test + public void pushDelayReceivedEventUpdatesMaxSyncPeriodInGuardian() { + mSyncManager.onEvent(new DelayStatusEvent(546)); + + verify(mSyncGuardian).setMaxSyncPeriod(546); + } + + @Test + public void successfulSyncEventUpdatesLastSyncInGuardian() { + mSyncManager.onEvent(new PushStatusEvent(EventType.SUCCESSFUL_SYNC)); + + verify(mSyncGuardian).updateLastSyncTimestamp(); + } + private void testStartUserConsentNotGranted(UserConsent userConsent) { when(mConfig.userConsent()).thenReturn(userConsent); mSyncManager.start(); diff --git a/src/test/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimerTest.java b/src/test/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimerTest.java new file mode 100644 index 000000000..8254dd202 --- /dev/null +++ b/src/test/java/io/split/android/client/service/sseclient/sseclient/SseDisconnectionTimerTest.java @@ -0,0 +1,65 @@ +package io.split.android.client.service.sseclient.sseclient; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +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.SplitClientConfig; +import io.split.android.client.service.executor.SplitTask; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutor; +import io.split.android.client.service.executor.SplitTaskType; + +public class SseDisconnectionTimerTest { + + private SplitTaskExecutor mTaskExecutor; + private SplitTask mTask; + private SseDisconnectionTimer mSseDisconnectionTimer; + + @Before + public void setUp() { + mTaskExecutor = mock(SplitTaskExecutor.class); + mTask = mock(SplitTask.class); + when(mTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); + mSseDisconnectionTimer = new SseDisconnectionTimer(mTaskExecutor, 0); + } + + @Test + public void cancelDoesNothingWhenTaskHasNotBeenScheduled() { + mSseDisconnectionTimer.cancel(); + + verify(mTaskExecutor, times(0)).stopTask(any()); + } + + @Test + public void scheduleSchedulesTaskInTaskExecutor() { + mSseDisconnectionTimer.schedule(mTask); + + verify(mTaskExecutor).schedule(eq(mTask), eq(0L), eq(mSseDisconnectionTimer)); + } + + @Test + public void cancelCancelsTaskWithCorrectTaskId() { + when(mTaskExecutor.schedule(eq(mTask), anyLong(), any())).thenReturn("id"); + + mSseDisconnectionTimer.schedule(mTask); + mSseDisconnectionTimer.cancel(); + + verify(mTaskExecutor).stopTask("id"); + } + + @Test + public void scheduleInitialDelayInSecondsDefaultValueIs60() { + mSseDisconnectionTimer = new SseDisconnectionTimer(mTaskExecutor, 60); + + mSseDisconnectionTimer.schedule(mTask); + verify(mTaskExecutor).schedule(mTask, 60L, mSseDisconnectionTimer); + } +} diff --git a/src/test/java/io/split/android/client/service/sseclient/sseclient/notifications/SplitsChangeNotificationTest.java b/src/test/java/io/split/android/client/service/sseclient/sseclient/notifications/SplitsChangeNotificationTest.java new file mode 100644 index 000000000..ae89e70d2 --- /dev/null +++ b/src/test/java/io/split/android/client/service/sseclient/sseclient/notifications/SplitsChangeNotificationTest.java @@ -0,0 +1,43 @@ +package io.split.android.client.service.sseclient.sseclient.notifications; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +import io.split.android.client.common.CompressionType; +import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; +import io.split.android.client.utils.Json; + +public class SplitsChangeNotificationTest { + + private static final String FULL_NOTIFICATION_C0 = "{\"type\":\"SPLIT_UPDATE\",\"changeNumber\":1684265694505,\"pcn\":0,\"c\":0,\"d\":\"redacted=\"}"; + private static final String FULL_NOTIFICATION_C1 = "{\"type\":\"SPLIT_UPDATE\",\"changeNumber\":1684265694505,\"pcn\":0,\"c\":1,\"d\":\"redacted=\"}"; + private static final String FULL_NOTIFICATION_C2 = "{\"type\":\"SPLIT_UPDATE\",\"changeNumber\":1684265694505,\"pcn\":0,\"c\":2,\"d\":\"redacted=\"}"; + + private static final String LEGACY_NOTIFICATION = "{\"type\":\"SPLIT_UPDATE\",\"changeNumber\":1684265694505}"; + + @Test + public void nullValuesAreAllowed() { + SplitsChangeNotification splitsChangeNotification = Json.fromJson(LEGACY_NOTIFICATION, SplitsChangeNotification.class); + + assertEquals(1684265694505L, splitsChangeNotification.getChangeNumber()); + assertNull(splitsChangeNotification.getPreviousChangeNumber()); + assertNull(splitsChangeNotification.getData()); + assertNull(splitsChangeNotification.getCompressionType()); + } + + @Test + public void valuesAreCorrectlyDeserialized() { + SplitsChangeNotification c0Notification = Json.fromJson(FULL_NOTIFICATION_C0, SplitsChangeNotification.class); + SplitsChangeNotification c1Notification = Json.fromJson(FULL_NOTIFICATION_C1, SplitsChangeNotification.class); + SplitsChangeNotification c2Notification = Json.fromJson(FULL_NOTIFICATION_C2, SplitsChangeNotification.class); + + assertEquals(CompressionType.NONE, c0Notification.getCompressionType()); + assertEquals(CompressionType.GZIP, c1Notification.getCompressionType()); + assertEquals(1684265694505L, c2Notification.getChangeNumber()); + assertEquals(0L, c2Notification.getPreviousChangeNumber().longValue()); + assertEquals("redacted=", c2Notification.getData()); + assertEquals(CompressionType.ZLIB, c2Notification.getCompressionType()); + } +} diff --git a/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java b/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java new file mode 100644 index 000000000..e4d3b2217 --- /dev/null +++ b/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java @@ -0,0 +1,161 @@ +package io.split.android.client.service.synchronizer; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.List; +import java.util.Objects; + +import io.split.android.client.RetryBackoffCounterTimerFactory; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.service.executor.SplitTaskBatchItem; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutor; +import io.split.android.client.service.executor.SplitTaskFactory; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.service.splits.FilterSplitsInCacheTask; +import io.split.android.client.service.splits.LoadSplitsTask; +import io.split.android.client.service.splits.SplitsSyncTask; +import io.split.android.client.service.splits.SplitsUpdateTask; +import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; +import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent; +import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; + +public class FeatureFlagsSynchronizerImplTest { + + private SplitClientConfig mConfig; + private SplitTaskExecutor mTaskExecutor; + private SplitTaskExecutor mSingleThreadTaskExecutor; + private SplitTaskFactory mTaskFactory; + private ISplitEventsManager mEventsManager; + private RetryBackoffCounterTimerFactory mRetryBackoffCounterFactory; + private RetryBackoffCounterTimer mRetryTimerSplitsUpdate; + private RetryBackoffCounterTimer mRetryTimerSplitsSync; + private PushManagerEventBroadcaster mPushManagerEventBroadcaster; + + private FeatureFlagsSynchronizerImpl mFeatureFlagsSynchronizer; + + @Before + public void setUp() { + mConfig = mock(SplitClientConfig.class); + mTaskExecutor = mock(SplitTaskExecutor.class); + mSingleThreadTaskExecutor = mock(SplitTaskExecutor.class); + mTaskFactory = mock(SplitTaskFactory.class); + mEventsManager = mock(ISplitEventsManager.class); + mRetryBackoffCounterFactory = mock(RetryBackoffCounterTimerFactory.class); + mRetryTimerSplitsUpdate = mock(RetryBackoffCounterTimer.class); + mRetryTimerSplitsSync = mock(RetryBackoffCounterTimer.class); + mPushManagerEventBroadcaster = mock(PushManagerEventBroadcaster.class); + when(mRetryBackoffCounterFactory.create(mSingleThreadTaskExecutor, 1)) + .thenReturn(mRetryTimerSplitsSync) + .thenReturn(mRetryTimerSplitsUpdate); + + mFeatureFlagsSynchronizer = new FeatureFlagsSynchronizerImpl(mConfig, + mTaskExecutor, mSingleThreadTaskExecutor, mTaskFactory, + mEventsManager, mRetryBackoffCounterFactory, mPushManagerEventBroadcaster); + } + + @Test + public void synchronizeSplitsWithSince() { + SplitsUpdateTask task = mock(SplitsUpdateTask.class); + when(mTaskFactory.createSplitsUpdateTask(1000)).thenReturn(task); + + mFeatureFlagsSynchronizer.synchronize(1000); + + verify(mRetryTimerSplitsUpdate).setTask(eq(task), argThat(Objects::nonNull)); + verify(mRetryTimerSplitsUpdate).start(); + } + + @Test + public void loadLocalData() { + LoadSplitsTask mockTask = mock(LoadSplitsTask.class); + when(mockTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); + when(mTaskFactory.createLoadSplitsTask()).thenReturn(mockTask); + when(mRetryBackoffCounterFactory.create(any(), anyInt())) + .thenReturn(mRetryTimerSplitsSync) + .thenReturn(mRetryTimerSplitsUpdate); + + mFeatureFlagsSynchronizer.loadFromCache(); + + verify(mTaskExecutor).submit(eq(mockTask), argThat(Objects::nonNull)); + } + + @Test + public void loadAndSynchronizeSplits() { + LoadSplitsTask mockLoadTask = mock(LoadSplitsTask.class); + when(mockLoadTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); + when(mTaskFactory.createLoadSplitsTask()).thenReturn(mockLoadTask); + + FilterSplitsInCacheTask mockFilterTask = mock(FilterSplitsInCacheTask.class); + when(mockFilterTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.FILTER_SPLITS_CACHE)); + when(mTaskFactory.createFilterSplitsInCacheTask()).thenReturn(mockFilterTask); + + SplitsSyncTask mockSplitSyncTask = mock(SplitsSyncTask.class); + when(mockSplitSyncTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mTaskFactory.createSplitsSyncTask(true)).thenReturn(mockSplitSyncTask); + + when(mRetryBackoffCounterFactory.create(any(), anyInt())) + .thenReturn(mRetryTimerSplitsSync) + .thenReturn(mRetryTimerSplitsUpdate); + + mFeatureFlagsSynchronizer.loadAndSynchronize(); + + verify(mTaskFactory).createFilterSplitsInCacheTask(); + verify(mTaskFactory).createLoadSplitsTask(); + + ArgumentCaptor> argument = ArgumentCaptor.forClass(List.class); + verify(mTaskExecutor).executeSerially(argument.capture()); + assertEquals(3, argument.getValue().size()); + } + + @Test + public void splitExecutorSchedule() { + SplitsSyncTask mockTask = mock(SplitsSyncTask.class); + when(mTaskFactory.createSplitsSyncTask(false)).thenReturn(mockTask); + mFeatureFlagsSynchronizer.startPeriodicFetching(); + verify(mSingleThreadTaskExecutor).schedule( + eq(mockTask), anyLong(), anyLong(), + any()); + } + + @Test + public void stopSynchronization() { + + mFeatureFlagsSynchronizer.stopSynchronization(); + + verify(mRetryTimerSplitsSync).stop(); + verify(mRetryTimerSplitsUpdate).stop(); + } + + @Test + public void synchronize() { + mFeatureFlagsSynchronizer.synchronize(); + + verify(mRetryTimerSplitsSync).start(); + } + + @Test + public void stopPeriodicFetching() { + SplitsSyncTask mockTask = mock(SplitsSyncTask.class); + when(mTaskFactory.createSplitsSyncTask(false)).thenReturn(mockTask); + when(mSingleThreadTaskExecutor.schedule(eq(mockTask), anyLong(), anyLong(), any())).thenReturn("12"); + + // start periodic fetching to populate task id + mFeatureFlagsSynchronizer.startPeriodicFetching(); + mFeatureFlagsSynchronizer.stopPeriodicFetching(); + + verify(mSingleThreadTaskExecutor).stopTask("12"); + } +} diff --git a/src/test/java/io/split/android/client/service/synchronizer/SyncGuardianImplTest.java b/src/test/java/io/split/android/client/service/synchronizer/SyncGuardianImplTest.java new file mode 100644 index 000000000..5322126b6 --- /dev/null +++ b/src/test/java/io/split/android/client/service/synchronizer/SyncGuardianImplTest.java @@ -0,0 +1,117 @@ +package io.split.android.client.service.synchronizer; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +import io.split.android.client.SplitClientConfig; + +public class SyncGuardianImplTest { + + private SyncGuardianImpl mSyncGuardian; + private SyncGuardianImpl.TimestampProvider mTimestampProvider; + private SplitClientConfig mSplitConfig; + + @Before + public void setUp() { + mTimestampProvider = mock(SyncGuardianImpl.TimestampProvider.class); + mSplitConfig = mock(SplitClientConfig.class); + } + + @Test + public void mustSyncReturnsFalseWhenSyncIsDisabled() { + when(mSplitConfig.syncEnabled()).thenReturn(false); + when(mSplitConfig.streamingEnabled()).thenReturn(true); + when(mTimestampProvider.get()).thenReturn(2000L); + mSyncGuardian = new SyncGuardianImpl(mSplitConfig, mTimestampProvider); + mSyncGuardian.initialize(); + mSyncGuardian.updateLastSyncTimestamp(); + + assertFalse(mSyncGuardian.mustSync()); + } + + @Test + public void mustSyncReturnsFalseWhenStreamingIsDisabled() { + when(mSplitConfig.syncEnabled()).thenReturn(true); + when(mSplitConfig.streamingEnabled()).thenReturn(false); + when(mTimestampProvider.get()).thenReturn(2000L); + mSyncGuardian = new SyncGuardianImpl(mSplitConfig, mTimestampProvider); + mSyncGuardian.initialize(); + mSyncGuardian.updateLastSyncTimestamp(); + + assertFalse(mSyncGuardian.mustSync()); + } + + @Test + public void mustSyncReturnsFalseWhenDiffBetweenLastSyncIsLessThanMaxSyncPeriod() { + when(mSplitConfig.syncEnabled()).thenReturn(true); + when(mSplitConfig.streamingEnabled()).thenReturn(true); + when(mTimestampProvider.get()).thenReturn(100L, 150L); + mSyncGuardian = new SyncGuardianImpl(mSplitConfig, mTimestampProvider); + + mSyncGuardian.updateLastSyncTimestamp(); + + assertFalse(mSyncGuardian.mustSync()); + } + + @Test + public void mustSyncReturnsTrueWhenDiffBetweenLastSyncIsGreaterThanMaxSyncPeriod() { + when(mSplitConfig.syncEnabled()).thenReturn(true); + when(mSplitConfig.streamingEnabled()).thenReturn(true); + when(mTimestampProvider.get()).thenReturn(100L, 201L); + mSyncGuardian = new SyncGuardianImpl(mSplitConfig, mTimestampProvider); + + mSyncGuardian.initialize(); + mSyncGuardian.updateLastSyncTimestamp(); + + assertTrue(mSyncGuardian.mustSync()); + } + + @Test + public void setMaxSyncPeriodDoesNotChangeMaxSyncPeriodWhenItIsLowerThanTheDefault() { + when(mSplitConfig.syncEnabled()).thenReturn(true); + when(mSplitConfig.streamingEnabled()).thenReturn(true); + when(mSplitConfig.defaultSSEConnectionDelay()).thenReturn(60L); + when(mTimestampProvider.get()).thenReturn(100L, 150L); + mSyncGuardian = new SyncGuardianImpl(mSplitConfig, mTimestampProvider); + mSyncGuardian.initialize(); + mSyncGuardian.setMaxSyncPeriod(50L); + mSyncGuardian.updateLastSyncTimestamp(); + + assertFalse(mSyncGuardian.mustSync()); + } + + @Test + public void setMaxSyncPeriodChangesMaxSyncPeriodWhenItIsHigherThanTheDefault() { + when(mSplitConfig.syncEnabled()).thenReturn(true); + when(mSplitConfig.streamingEnabled()).thenReturn(true); + when(mSplitConfig.defaultSSEConnectionDelay()).thenReturn(60L); + when(mTimestampProvider.get()).thenReturn(100L, 180L); + mSyncGuardian = new SyncGuardianImpl(mSplitConfig, mTimestampProvider); + mSyncGuardian.initialize(); + mSyncGuardian.setMaxSyncPeriod(80L); + mSyncGuardian.updateLastSyncTimestamp(); + + assertTrue(mSyncGuardian.mustSync()); + } + + @Test + public void mustSyncAlwaysReturnsFalseWhenSyncGuardianHasNotBeenInitialized() { + when(mSplitConfig.syncEnabled()).thenReturn(true); + when(mSplitConfig.streamingEnabled()).thenReturn(true); + when(mTimestampProvider.get()).thenReturn(100L, 300L); + mSyncGuardian = new SyncGuardianImpl(mSplitConfig, mTimestampProvider); + + boolean firstAttempt = mSyncGuardian.mustSync(); + boolean secondAttempt = mSyncGuardian.mustSync(); + mSyncGuardian.initialize(); + boolean thirdAttempt = mSyncGuardian.mustSync(); + assertFalse(firstAttempt); + assertFalse(secondAttempt); + assertTrue(thirdAttempt); + } +} diff --git a/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java b/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java index 7e2e9ce9c..ff4e6c69f 100644 --- a/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java +++ b/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java @@ -25,7 +25,9 @@ import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.impressions.ImpressionManager; import io.split.android.client.service.splits.SplitsSyncTask; +import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; +import io.split.android.client.service.synchronizer.FeatureFlagsSynchronizerImpl; import io.split.android.client.service.synchronizer.SynchronizerImpl; import io.split.android.client.service.synchronizer.WorkManagerWrapper; import io.split.android.client.service.synchronizer.attributes.AttributesSynchronizerRegistryImpl; @@ -54,6 +56,8 @@ public class SynchronizerImplTelemetryTest { MySegmentsSynchronizerRegistryImpl mMySegmentsSynchronizerRegistry; @Mock ImpressionManager mImpressionManager; + @Mock + PushManagerEventBroadcaster mPushManagerEventBroadcaster; private SynchronizerImpl mSynchronizer; @@ -86,15 +90,21 @@ public void setUp() { mConfig, mTaskExecutor, mSingleThreadTaskExecutor, - mSplitStorageContainer, mTaskFactory, - mEventsManager, mWorkManagerWrapper, mRetryBackoffCounterFactory, mTelemetryRuntimeProducer, mAttributesSynchronizerRegistry, mMySegmentsSynchronizerRegistry, - mImpressionManager); + mImpressionManager, + new FeatureFlagsSynchronizerImpl(mConfig, + mTaskExecutor, + mSingleThreadTaskExecutor, + mTaskFactory, + mEventsManager, + mRetryBackoffCounterFactory, + mPushManagerEventBroadcaster), + mSplitStorageContainer.getEventsStorage()); } @Test diff --git a/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java b/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java index 9d8c0c5e2..183ea2f42 100644 --- a/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java +++ b/src/test/java/io/split/android/client/telemetry/TelemetryConfigBodySerializerTest.java @@ -24,7 +24,7 @@ public void setUp() { @Test public void jsonIsBuiltAsExpected() { - final String expectedJson = "{\"oM\":0,\"st\":\"memory\",\"sE\":true,\"rR\":{\"sp\":4000,\"ms\":5000,\"im\":3000,\"ev\":2000,\"te\":1000},\"uO\":{\"s\":true,\"e\":true,\"a\":true,\"st\":true,\"t\":true},\"iQ\":4000,\"eQ\":3000,\"iM\":\"1\",\"iL\":true,\"hP\":true,\"aF\":1,\"rF\":0,\"tR\":300,\"tC\":0,\"nR\":3,\"uC\":1,\"t\":[\"tag1\",\"tag2\"],\"i\":[\"integration1\",\"integration2\"]}"; + final String expectedJson = "{\"oM\":0,\"st\":\"memory\",\"sE\":true,\"rR\":{\"sp\":4000,\"ms\":5000,\"im\":3000,\"ev\":2000,\"te\":1000},\"uO\":{\"s\":true,\"e\":true,\"a\":true,\"st\":true,\"t\":true},\"iQ\":4000,\"eQ\":3000,\"iM\":1,\"iL\":true,\"hP\":true,\"aF\":1,\"rF\":0,\"tR\":300,\"tC\":0,\"nR\":3,\"uC\":1,\"t\":[\"tag1\",\"tag2\"],\"i\":[\"integration1\",\"integration2\"]}"; final String serializedConfig = telemetryConfigBodySerializer.serialize(buildMockConfig()); assertEquals(expectedJson, serializedConfig); @@ -33,7 +33,7 @@ public void jsonIsBuiltAsExpected() { @Test public void nullValuesAreIgnoredForJson() { - final String expectedJson = "{\"oM\":0,\"st\":\"memory\",\"sE\":true,\"iQ\":4000,\"eQ\":3000,\"iM\":\"1\",\"iL\":true,\"hP\":true,\"aF\":1,\"rF\":0,\"tR\":300,\"tC\":0,\"nR\":3,\"uC\":0,\"t\":[\"tag1\",\"tag2\"],\"i\":[\"integration1\",\"integration2\"]}"; + final String expectedJson = "{\"oM\":0,\"st\":\"memory\",\"sE\":true,\"iQ\":4000,\"eQ\":3000,\"iM\":1,\"iL\":true,\"hP\":true,\"aF\":1,\"rF\":0,\"tR\":300,\"tC\":0,\"nR\":3,\"uC\":0,\"t\":[\"tag1\",\"tag2\"],\"i\":[\"integration1\",\"integration2\"]}"; final String serializedConfig = telemetryConfigBodySerializer.serialize(buildMockConfigWithNulls()); assertEquals(expectedJson, serializedConfig); @@ -61,7 +61,7 @@ private Config buildMockConfig() { config.setUrlOverrides(urlOverrides); config.setImpressionsQueueSize(4000); config.setEventsQueueSize(3000); - config.setImpressionsMode(ImpressionsMode.DEBUG); + config.setImpressionsMode(ImpressionsMode.DEBUG.intValue()); config.setImpressionsListenerEnabled(true); config.setHttpProxyDetected(true); config.setActiveFactories(1); @@ -81,7 +81,7 @@ private Config buildMockConfigWithNulls() { config.setStreamingEnabled(true); config.setImpressionsQueueSize(4000); config.setEventsQueueSize(3000); - config.setImpressionsMode(ImpressionsMode.DEBUG); + config.setImpressionsMode(ImpressionsMode.DEBUG.intValue()); config.setImpressionsListenerEnabled(true); config.setHttpProxyDetected(true); config.setActiveFactories(1); diff --git a/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java b/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java index 5aef32a61..a0dc2bdbe 100644 --- a/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java +++ b/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java @@ -13,6 +13,7 @@ import io.split.android.client.telemetry.model.MethodExceptions; import io.split.android.client.telemetry.model.MethodLatencies; import io.split.android.client.telemetry.model.Stats; +import io.split.android.client.telemetry.model.UpdatesFromSSE; import io.split.android.client.telemetry.model.streaming.ConnectionEstablishedStreamingEvent; import io.split.android.client.telemetry.model.streaming.OccupancySecStreamingEvent; @@ -29,7 +30,7 @@ public void setUp() { public void jsonIsBuiltAsExpected() { String serializedStats = telemetryStatsBodySerializer.serialize(getMockStats()); - assertEquals("{\"lS\":{\"sp\":1000,\"ms\":2000,\"im\":3000,\"ic\":4000,\"ev\":5000,\"te\":6000,\"to\":7000},\"mL\":{\"t\":[0,0,2,0],\"ts\":[0,0,3,0],\"tc\":[0,0,5,0],\"tcs\":[0,0,4,0],\"tr\":[0,0,1,0]},\"mE\":{\"t\":2,\"ts\":3,\"tc\":5,\"tcs\":4,\"tr\":1},\"hE\":{},\"hL\":{\"sp\":[0,0,3,0],\"ms\":[0,0,5,0],\"im\":[0,0,1,0],\"ic\":[0,0,4,0],\"ev\":[0,0,2,0],\"te\":[1,0,0,0],\"to\":[0,0,6,0]},\"tR\":4,\"aR\":5,\"iQ\":2,\"iDe\":5,\"iDr\":4,\"spC\":456,\"seC\":4,\"skC\":1,\"sL\":2000,\"eQ\":4,\"eD\":2,\"sE\":[{\"e\":0,\"t\":5000},{\"e\":20,\"d\":4,\"t\":2000}],\"t\":[\"tag1\",\"tag2\"]}", serializedStats); + assertEquals("{\"lS\":{\"sp\":1000,\"ms\":2000,\"im\":3000,\"ic\":4000,\"ev\":5000,\"te\":6000,\"to\":7000},\"mL\":{\"t\":[0,0,2,0],\"ts\":[0,0,3,0],\"tc\":[0,0,5,0],\"tcs\":[0,0,4,0],\"tr\":[0,0,1,0]},\"mE\":{\"t\":2,\"ts\":3,\"tc\":5,\"tcs\":4,\"tr\":1},\"hE\":{},\"hL\":{\"sp\":[0,0,3,0],\"ms\":[0,0,5,0],\"im\":[0,0,1,0],\"ic\":[0,0,4,0],\"ev\":[0,0,2,0],\"te\":[1,0,0,0],\"to\":[0,0,6,0]},\"tR\":4,\"aR\":5,\"iQ\":2,\"iDe\":5,\"iDr\":4,\"spC\":456,\"seC\":4,\"skC\":1,\"sL\":2000,\"eQ\":4,\"eD\":2,\"sE\":[{\"e\":0,\"t\":5000},{\"e\":20,\"d\":4,\"t\":2000}],\"t\":[\"tag1\",\"tag2\"],\"ufs\":{\"sp\":4,\"ms\":8}}", serializedStats); } private Stats getMockStats() { @@ -85,6 +86,7 @@ private Stats getMockStats() { stats.setTags(Arrays.asList("tag1", "tag2")); stats.setTokenRefreshes(4); stats.setStreamingEvents(Arrays.asList(new ConnectionEstablishedStreamingEvent(5000), new OccupancySecStreamingEvent(4, 2000))); + stats.setUpdatesFromSSE(new UpdatesFromSSE(4L, 8L)); return stats; } diff --git a/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java b/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java index d5346f199..97c6adbe5 100644 --- a/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java +++ b/src/test/java/io/split/android/client/telemetry/storage/InMemoryTelemetryStorageTest.java @@ -23,12 +23,14 @@ import io.split.android.client.telemetry.model.MethodExceptions; import io.split.android.client.telemetry.model.MethodLatencies; import io.split.android.client.telemetry.model.OperationType; +import io.split.android.client.telemetry.model.UpdatesFromSSE; import io.split.android.client.telemetry.model.streaming.AblyErrorStreamingEvent; import io.split.android.client.telemetry.model.streaming.ConnectionEstablishedStreamingEvent; 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.StreamingStatusStreamingEvent; import io.split.android.client.telemetry.model.streaming.TokenRefreshStreamingEvent; +import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum; public class InMemoryTelemetryStorageTest { @@ -531,9 +533,40 @@ public void timeUntilReadyFromCacheIsRecordedCorrectly() { assertEquals(300, telemetryStorage.getTimeUntilReadyFromCache()); } + @Test public void recordSdkReadyFromCacheValueIsStoreCorrectly() { telemetryStorage.recordTimeUntilReadyFromCache(500); assertEquals(500, telemetryStorage.getTimeUntilReadyFromCache()); } + + @Test + public void updatesSSEEventsForSplitsIsSetCorrectly() { + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + + UpdatesFromSSE firstPop = telemetryStorage.popUpdatesFromSSE(); + UpdatesFromSSE secondPop = telemetryStorage.popUpdatesFromSSE(); + + assertEquals(4, firstPop.getSplits()); + assertEquals(0, firstPop.getMySegments()); + assertEquals(0, secondPop.getSplits()); + assertEquals(0, secondPop.getMySegments()); + } + + @Test + public void updatesSSEEventsForMySegmentsIsSetCorrectly() { + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.MY_SEGMENTS); + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.MY_SEGMENTS); + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.MY_SEGMENTS); + + UpdatesFromSSE firstPop = telemetryStorage.popUpdatesFromSSE(); + UpdatesFromSSE secondPop = telemetryStorage.popUpdatesFromSSE(); + assertEquals(3, firstPop.getMySegments()); + assertEquals(0, firstPop.getSplits()); + assertEquals(0, secondPop.getMySegments()); + assertEquals(0, secondPop.getSplits()); + } } diff --git a/src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java b/src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java new file mode 100644 index 000000000..6047c2c20 --- /dev/null +++ b/src/test/java/io/split/android/client/telemetry/storage/TelemetryConfigProviderImplTest.java @@ -0,0 +1,85 @@ +package io.split.android.client.telemetry.storage; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import io.split.android.client.ServiceEndpoints; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.impressions.Impression; +import io.split.android.client.impressions.ImpressionListener; +import io.split.android.client.service.impressions.ImpressionsMode; +import io.split.android.client.shared.UserConsent; +import io.split.android.client.telemetry.model.Config; + +public class TelemetryConfigProviderImplTest { + + private TelemetryConfigProvider mTelemetryConfigProvider; + private TelemetryStorageConsumer mTelemetryStorageConsumer = mock(TelemetryStorageConsumer.class); + + @Test + public void test() { + + int splitRefreshRate = 101; + int impRefreshRate = 102; + int segmentsRate = 103; + int telemetryRefreshRate = 104; + int eventsRate = 105; + SplitClientConfig mSplitClientConfig = new SplitClientConfig.Builder() + .streamingEnabled(true) + .featuresRefreshRate(splitRefreshRate) + .impressionsRefreshRate(impRefreshRate) + .segmentsRefreshRate(segmentsRate) + .telemetryRefreshRate(telemetryRefreshRate) + .eventFlushInterval(eventsRate) + .proxyHost("proxyHost") + .serviceEndpoints(ServiceEndpoints.builder() + .apiEndpoint("asdas") + .eventsEndpoint("asdas") + .sseAuthServiceEndpoint("asdas") + .streamingServiceEndpoint("asdas") + .telemetryServiceEndpoint("asdas") + .build()) + .impressionsQueueSize(200) + .eventsQueueSize(300) + .userConsent(UserConsent.DECLINED) + .impressionsMode(ImpressionsMode.OPTIMIZED) + .impressionListener(new ImpressionListener() { + @Override + public void log(Impression impression) { + + } + + @Override + public void close() { + + } + }) + .build(); + mTelemetryConfigProvider = new TelemetryConfigProviderImpl(mTelemetryStorageConsumer, mSplitClientConfig); + + Config configTelemetry = mTelemetryConfigProvider.getConfigTelemetry(); + + assertTrue(mSplitClientConfig.streamingEnabled()); + assertEquals(splitRefreshRate, configTelemetry.getRefreshRates().getSplits()); + assertEquals(impRefreshRate, configTelemetry.getRefreshRates().getImpressions()); + assertEquals(segmentsRate, configTelemetry.getRefreshRates().getMySegments()); + assertEquals(telemetryRefreshRate, configTelemetry.getRefreshRates().getTelemetry()); + assertEquals(eventsRate, configTelemetry.getRefreshRates().getEvents()); + assertTrue(configTelemetry.isHttpProxyDetected()); + assertEquals(200, configTelemetry.getImpressionsQueueSize()); + assertEquals(300, configTelemetry.getEventsQueueSize()); + assertEquals(3, configTelemetry.getUserConsent()); + assertEquals(io.split.android.client.telemetry.model.ImpressionsMode.OPTIMIZED.intValue(), configTelemetry.getImpressionsMode()); + assertTrue(configTelemetry.isImpressionsListenerEnabled()); + assertTrue(configTelemetry.getUrlOverrides().isTelemetry()); + assertTrue(configTelemetry.getUrlOverrides().isSdkUrl()); + assertTrue(configTelemetry.getUrlOverrides().isEvents()); + assertTrue(configTelemetry.getUrlOverrides().isAuth()); + assertTrue(configTelemetry.getUrlOverrides().isStream()); + } +} diff --git a/src/test/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImplTest.java b/src/test/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImplTest.java index 69465c010..1776d6c6a 100644 --- a/src/test/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImplTest.java +++ b/src/test/java/io/split/android/client/telemetry/storage/TelemetryStatsProviderImplTest.java @@ -10,11 +10,26 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import io.split.android.client.dtos.Split; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.client.telemetry.model.EventsDataRecordsEnum; +import io.split.android.client.telemetry.model.HttpErrors; +import io.split.android.client.telemetry.model.HttpLatencies; +import io.split.android.client.telemetry.model.ImpressionsDataType; +import io.split.android.client.telemetry.model.LastSync; +import io.split.android.client.telemetry.model.MethodExceptions; +import io.split.android.client.telemetry.model.MethodLatencies; import io.split.android.client.telemetry.model.Stats; +import io.split.android.client.telemetry.model.UpdatesFromSSE; +import io.split.android.client.telemetry.model.streaming.OccupancyPriStreamingEvent; +import io.split.android.client.telemetry.model.streaming.StreamingEvent; +import io.split.android.client.telemetry.model.streaming.StreamingStatusStreamingEvent; public class TelemetryStatsProviderImplTest { @@ -44,4 +59,80 @@ public void clearRemovesExistingStatsFromProvider() { telemetryStatsProvider.clearStats(); assertEquals(Collections.emptyList(), telemetryStatsProvider.getTelemetryStats().getTags()); } + + @Test + public void statsAreCorrectlyBuilt() { + + long mySegmentsUniqueCount = 3; + int splitsCount = 40; + + List streamingEvents = Arrays.asList( + new OccupancyPriStreamingEvent(2, 23232323L), + new StreamingStatusStreamingEvent(StreamingStatusStreamingEvent.Status.ENABLED, 23232323L)); + List tags = Arrays.asList("asd1", "asd2"); + MethodLatencies methodLatencies = new MethodLatencies(); + long sessionLength = 25250L; + LastSync lastSync = new LastSync(); + lastSync.setLastEventSync(4242L); + lastSync.setLastImpressionSync(22L); + lastSync.setLastSplitSync(9090L); + lastSync.setLastMySegmentSync(27182L); + long impQueued = 212L; + long impDropped = 22L; + long impDeduped = 66L; + MethodExceptions methodExceptions = new MethodExceptions(); + HttpLatencies httpLatencies = new HttpLatencies(); + HttpErrors httpErrors = new HttpErrors(); + long authTokenRefreshes = 2L; + long authRejections = 24L; + long eventsQueued = 12L; + long eventsDropped = 21L; + long sseSplits = 2L; + long sseMySegments = 4L; + + Map splits = new HashMap<>(); + for (int i = 0; i < splitsCount; i++) { + splits.put("split" + i, new Split()); + } + + when(splitsStorage.getAll()).thenReturn(splits); + when(mySegmentsStorageContainer.getUniqueAmount()).thenReturn(mySegmentsUniqueCount); + when(telemetryStorageConsumer.popStreamingEvents()).thenReturn(streamingEvents); + when(telemetryStorageConsumer.popTags()).thenReturn(tags); + when(telemetryStorageConsumer.popLatencies()).thenReturn(methodLatencies); + when(telemetryStorageConsumer.getSessionLength()).thenReturn(sessionLength); + when(telemetryStorageConsumer.getLastSynchronization()).thenReturn(lastSync); + when(telemetryStorageConsumer.getImpressionsStats(ImpressionsDataType.IMPRESSIONS_QUEUED)).thenReturn(impQueued); + when(telemetryStorageConsumer.getImpressionsStats(ImpressionsDataType.IMPRESSIONS_DROPPED)).thenReturn(impDropped); + when(telemetryStorageConsumer.getImpressionsStats(ImpressionsDataType.IMPRESSIONS_DEDUPED)).thenReturn(impDeduped); + when(telemetryStorageConsumer.popExceptions()).thenReturn(methodExceptions); + when(telemetryStorageConsumer.popHttpLatencies()).thenReturn(httpLatencies); + when(telemetryStorageConsumer.popHttpErrors()).thenReturn(httpErrors); + when(telemetryStorageConsumer.popTokenRefreshes()).thenReturn(authTokenRefreshes); + when(telemetryStorageConsumer.popAuthRejections()).thenReturn(authRejections); + when(telemetryStorageConsumer.getEventsStats(EventsDataRecordsEnum.EVENTS_QUEUED)).thenReturn(eventsQueued); + when(telemetryStorageConsumer.getEventsStats(EventsDataRecordsEnum.EVENTS_DROPPED)).thenReturn(eventsDropped); + when(telemetryStorageConsumer.popUpdatesFromSSE()).thenReturn(new UpdatesFromSSE(sseSplits, sseMySegments)); + + Stats stats = telemetryStatsProvider.getTelemetryStats(); + assertEquals(streamingEvents, stats.getStreamingEvents()); + assertEquals(splitsCount, stats.getSplitCount()); + assertEquals(tags, stats.getTags()); + assertEquals(methodLatencies, stats.getMethodLatencies()); + assertEquals(mySegmentsUniqueCount, stats.getSegmentCount()); + assertEquals(sessionLength, stats.getSessionLengthMs()); + assertEquals(lastSync, stats.getLastSynchronizations()); + assertEquals(impDropped, stats.getImpressionsDropped()); + assertEquals(impQueued, stats.getImpressionsQueued()); + assertEquals(impDeduped, stats.getImpressionsDeduped()); + assertEquals(methodExceptions, stats.getMethodExceptions()); + assertEquals(httpLatencies, stats.getHttpLatencies()); + assertEquals(httpErrors, stats.getHttpErrors()); + assertEquals(authTokenRefreshes, stats.getTokenRefreshes()); + assertEquals(authRejections, stats.getAuthRejections()); + assertEquals(eventsQueued, stats.getEventsQueued()); + assertEquals(eventsDropped, stats.getEventsDropped()); + assertEquals(sseSplits, stats.getUpdatesFromSSE().getSplits()); + assertEquals(sseMySegments, stats.getUpdatesFromSSE().getMySegments()); + } } diff --git a/src/test/java/io/split/android/engine/splits/SplitChangeProcessorTest.java b/src/test/java/io/split/android/engine/splits/SplitChangeProcessorTest.java index 987be97f8..624cf214b 100644 --- a/src/test/java/io/split/android/engine/splits/SplitChangeProcessorTest.java +++ b/src/test/java/io/split/android/engine/splits/SplitChangeProcessorTest.java @@ -87,6 +87,55 @@ public void processNullNames() { Assert.assertEquals(9, result.getActiveSplits().size()); } + @Test + public void processSingleActiveSplit() { + Split activeSplit = createSplits(1, 1, Status.ACTIVE).get(0); + + ProcessedSplitChange result = mProcessor.process(activeSplit, 14500); + + Assert.assertEquals(0, result.getArchivedSplits().size()); + Assert.assertEquals(1, result.getActiveSplits().size()); + Assert.assertEquals(14500, result.getChangeNumber()); + + Split split = result.getActiveSplits().get(0); + Assert.assertEquals(activeSplit.name, split.name); + Assert.assertEquals(activeSplit.status, split.status); + Assert.assertEquals(activeSplit.trafficTypeName, split.trafficTypeName); + Assert.assertEquals(activeSplit.trafficAllocation, split.trafficAllocation); + Assert.assertEquals(activeSplit.trafficAllocationSeed, split.trafficAllocationSeed); + Assert.assertEquals(activeSplit.seed, split.seed); + Assert.assertEquals(activeSplit.conditions, split.conditions); + Assert.assertEquals(activeSplit.defaultTreatment, split.defaultTreatment); + Assert.assertEquals(activeSplit.configurations, split.configurations); + Assert.assertEquals(activeSplit.algo, split.algo); + Assert.assertEquals(activeSplit.changeNumber, split.changeNumber); + Assert.assertEquals(activeSplit.killed, split.killed); + } + + @Test + public void processSingleArchivedSplit() { + Split archivedSplit = createSplits(1, 1, Status.ARCHIVED).get(0); + + ProcessedSplitChange result = mProcessor.process(archivedSplit, 14500); + + Assert.assertEquals(1, result.getArchivedSplits().size()); + Assert.assertEquals(0, result.getActiveSplits().size()); + Assert.assertEquals(14500, result.getChangeNumber()); + + Split split = result.getArchivedSplits().get(0); + Assert.assertEquals(archivedSplit.name, split.name); + Assert.assertEquals(archivedSplit.status, split.status); + Assert.assertEquals(archivedSplit.trafficTypeName, split.trafficTypeName); + Assert.assertEquals(archivedSplit.trafficAllocation, split.trafficAllocation); + Assert.assertEquals(archivedSplit.trafficAllocationSeed, split.trafficAllocationSeed); + Assert.assertEquals(archivedSplit.seed, split.seed); + Assert.assertEquals(archivedSplit.conditions, split.conditions); + Assert.assertEquals(archivedSplit.defaultTreatment, split.defaultTreatment); + Assert.assertEquals(archivedSplit.configurations, split.configurations); + Assert.assertEquals(archivedSplit.algo, split.algo); + Assert.assertEquals(archivedSplit.changeNumber, split.changeNumber); + Assert.assertEquals(archivedSplit.killed, split.killed); + } private List createSplits(int from, int count, Status status) { List splits = new ArrayList<>(); diff --git a/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java b/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java index 6228139ee..a197b0cee 100644 --- a/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java +++ b/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java @@ -7,6 +7,7 @@ import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskBatchItem; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; import io.split.android.client.service.executor.SplitTaskExecutor; @@ -23,7 +24,12 @@ public String schedule(@NonNull SplitTask task, long initialDelayInSecs, @Nullab @Override public void submit(@NonNull SplitTask task, @Nullable SplitTaskExecutionListener executionListener) { - task.execute(); + SplitTaskExecutionInfo execute = task.execute(); + if (executionListener != null) { + if (execute != null) { + executionListener.taskExecuted(execute); + } + } } @Override