From f1c599fb9298fa9d701c1d20682b711e2afa9a92 Mon Sep 17 00:00:00 2001 From: gthea Date: Thu, 31 Oct 2024 19:09:46 -0300 Subject: [PATCH 1/3] LS feature (#710) --- .github/workflows/deploy.yml | 3 +- build.gradle | 9 +- .../6.json | 376 ++++++++++++++++++ spec.gradle | 2 +- .../assets/attributes_test_split_change.json | 2 +- src/androidTest/assets/bucket_split_test.json | 2 +- .../assets/push_msg-largesegment_update.txt | 3 + src/androidTest/assets/simple_split.json | 2 +- src/androidTest/assets/split_changes_1.json | 2 +- .../assets/split_changes_flag_set-2.json | 2 +- .../split_changes_large_segments-0.json | 49 +++ .../assets/splitchanges_int_test.json | 2 +- .../java/fake/SynchronizerSpyImpl.java | 6 - .../java/helper/IntegrationHelper.java | 68 +++- .../observer/DedupeIntegrationTest.java | 6 +- .../tests/database/MyLargeSegmentDaoTest.java | 79 ++++ .../integration/FlagsSpecInRequestTest.java | 4 +- .../integration/InitialChangeNumberTest.java | 15 +- .../tests/integration/IntegrationTest.java | 4 +- .../integration/MySegmentUpdatedTest.java | 11 +- .../MySegmentsServerErrorTest.java | 7 +- .../tests/integration/ProxyFactoryTest.java | 8 +- .../tests/integration/SingleSyncTest.java | 2 +- .../SplitChangesCdnBypassTest.java | 7 +- .../SplitChangesServerErrorTest.java | 5 +- .../tests/integration/SplitChangesTest.java | 11 +- .../SplitFetchSpecificSplitTest.java | 6 +- .../SplitsTwoDifferentApiKeyTest.java | 16 +- .../java/tests/integration/TrackTest.java | 6 +- .../encryption/EncryptionTest.java | 41 +- .../largesegments/LargeSegmentTestHelper.java | 160 ++++++++ .../LargeSegmentsStreamingTest.java | 245 ++++++++++++ .../largesegments/LargeSegmentsTest.java | 168 ++++++++ .../matcher/SemverMatcherTest.java | 2 +- .../matcher/UnsupportedMatcherTest.java | 2 +- .../integration/pin/CertPinningTest.java | 8 +- .../sets/FlagSetsEvaluationTest.java | 2 +- .../sets/FlagSetsMultipleFactoryTest.java | 2 +- .../integration/sets/FlagSetsPollingTest.java | 2 +- .../sets/FlagSetsStreamingTest.java | 2 +- .../shared/InterdependentSplitsTest.java | 9 +- .../shared/MySegmentsBeforeSplitsTest.java | 9 +- .../shared/SharedClientsIntegrationTest.java | 9 +- .../tests/integration/shared/TestingData.java | 87 ++-- .../streaming/AblyErrorBaseTest.java | 4 +- .../streaming/CleanUpDatabaseTest.java | 11 +- .../integration/streaming/ControlTest.java | 96 ++--- .../streaming/ImpressionsCountTest.java | 4 +- .../MySegmentsChangeV2MultiClientTest.java | 22 +- .../streaming/MySegmentsChangeV2Test.java | 6 +- .../streaming/MySegmentsSyncProcessTest.java | 81 ++-- .../streaming/OccupancyBaseTest.java | 4 +- .../streaming/SdkUpdateStreamingTest.java | 54 +-- ...plitChangeNotificationIntegrationTest.java | 4 +- .../streaming/SplitsKillProcessTest.java | 13 +- .../streaming/SplitsSyncProcessTest.java | 15 +- .../streaming/SseAuthFail4xxTest.java | 4 +- .../streaming/SseAuthFail5xxTest.java | 4 +- .../SseConnectionExpiredTokenTest.java | 4 +- .../streaming/SseConnectionFail5xxTest.java | 10 +- .../StreamingDisabledInConfigTest.java | 4 +- .../streaming/StreamingDisabledTest.java | 4 +- .../StreamingInitializationTest.java | 4 +- .../SyncGuardianIntegrationTest.java | 4 +- .../telemetry/TelemetryIntegrationTest.java | 26 +- .../userconsent/UserConsentModeDebugTest.kt | 4 +- .../userconsent/UserConsentModeNoneTest.kt | 4 +- .../UserConsentModeOptimizedTest.kt | 4 +- .../tests/service/ImpressionsRequestTest.java | 2 +- .../tests/service/SdkUpdatePollingTest.java | 21 +- .../tests/service/SseJwtTokenParserTest.java | 30 ++ .../service/UniqueKeysIntegrationTest.java | 7 +- .../tests/storage/LoadMySegmentsTaskTest.java | 32 +- .../tests/storage/MySegmentsStorageTest.java | 60 +-- .../PersistentMyLargeSegmentStorageTest.java | 116 ++++++ .../PersistentMySegmentStorageTest.java | 81 ++-- .../workmanager/WorkManagerWrapperTest.java | 117 +++++- .../android/client/SplitClientConfig.java | 3 +- .../client/SplitClientFactoryImpl.java | 2 +- .../android/client/SplitFactoryHelper.java | 38 +- .../android/client/SplitFactoryImpl.java | 5 +- .../client/dtos/AllSegmentsChange.java | 53 +++ .../io/split/android/client/dtos/Matcher.java | 15 + .../android/client/dtos/MatcherType.java | 2 + .../split/android/client/dtos/MySegment.java | 21 - .../io/split/android/client/dtos/Segment.java | 17 + .../android/client/dtos/SegmentChange.java | 12 - .../android/client/dtos/SegmentResponse.java | 12 + .../android/client/dtos/SegmentsChange.java | 67 ++++ .../UserDefinedLargeSegmentMatcherData.java | 8 + .../dtos/UserDefinedSegmentMatcherData.java | 3 + .../client/events/SplitEventsManager.java | 24 +- .../client/events/SplitInternalEvent.java | 1 + .../localhost/LocalhostSplitFactory.java | 2 +- .../LocalhostSplitClientContainerImpl.java | 2 +- .../android/client/network/SdkTargetPath.java | 12 +- .../client/service/ServiceConstants.java | 1 + .../client/service/ServiceFactory.java | 8 +- .../client/service/SplitApiFacade.java | 4 +- .../service/events/EventsRecorderTask.java | 2 +- .../executor/SplitTaskFactoryImpl.java | 1 + .../service/executor/SplitTaskType.java | 5 +- .../client/service/http/HttpFetcherImpl.java | 1 + .../client/service/http/HttpStatus.java | 6 + .../mysegments/MySegmentsFetcherFactory.java | 12 +- .../MySegmentsFetcherFactoryImpl.java | 21 +- .../mysegments/AllSegmentsResponseParser.java | 22 + .../mysegments/LoadMySegmentsTask.java | 16 +- .../mysegments/LoadMySegmentsTaskConfig.java | 22 + .../mysegments/MySegmentUpdateParams.java | 26 ++ .../mysegments/MySegmentsOverwriteTask.java | 65 --- .../mysegments/MySegmentsResponseParser.java | 32 -- .../mysegments/MySegmentsSyncTask.java | 249 ++++++++++-- .../mysegments/MySegmentsSyncTaskConfig.java | 51 +++ .../mysegments/MySegmentsTaskFactory.java | 8 +- .../MySegmentsTaskFactoryConfiguration.java | 76 +++- .../mysegments/MySegmentsTaskFactoryImpl.java | 22 +- .../mysegments/MySegmentsUpdateTask.java | 65 ++- .../MySegmentsUpdateTaskConfig.java | 51 +++ .../service/splits/SplitChangeProcessor.java | 2 +- .../service/splits/SplitsSyncHelper.java | 28 +- .../client/service/splits/SplitsSyncTask.java | 12 +- .../service/splits/SplitsUpdateTask.java | 3 +- .../service/sseclient/SseJwtParser.java | 10 +- .../notifications/ControlNotification.java | 12 +- .../notifications/HashingAlgorithm.java | 12 + .../IncomingNotificationType.java | 3 + .../notifications/MembershipNotification.java | 71 ++++ .../MySegmentChangeNotification.java | 21 - .../MySegmentChangeV2Notification.java | 52 --- .../MySegmentsPayloadDecoder.java | 30 -- .../notifications/NotificationParser.java | 18 +- .../notifications/NotificationProcessor.java | 50 +-- .../notifications/NotificationType.java | 18 +- .../MembershipsNotificationProcessor.java | 10 + .../MembershipsNotificationProcessorImpl.java | 148 +++++++ ...mbershipsNotificationProcessorFactory.java | 8 + ...shipsNotificationProcessorFactoryImpl.java | 35 ++ .../MySegmentsNotificationProcessor.java | 11 - ...ntsNotificationProcessorConfiguration.java | 26 +- ...ySegmentsNotificationProcessorFactory.java | 6 - ...mentsNotificationProcessorFactoryImpl.java | 37 -- .../MySegmentsNotificationProcessorImpl.java | 122 ------ ...SegmentsNotificationProcessorRegistry.java | 6 +- .../mysegments/SyncDelayCalculator.java | 8 + .../mysegments/SyncDelayCalculatorImpl.java | 31 ++ .../reactor/MySegmentsUpdateWorker.java | 10 +- .../MySegmentsUpdateWorkerRegistryImpl.java | 10 +- .../sseclient/NotificationManagerKeeper.java | 5 +- .../sseclient/PushNotificationManager.java | 2 +- .../sseclient/RetryBackoffCounterTimer.java | 49 ++- .../sseclient/sseclient/SseHandler.java | 10 +- .../synchronizer/MySegmentsChangeChecker.java | 11 +- .../service/synchronizer/Synchronizer.java | 2 - .../synchronizer/SynchronizerImpl.java | 8 +- .../synchronizer/WorkManagerWrapper.java | 17 +- .../mysegments/MySegmentsSynchronizer.java | 4 +- .../MySegmentsSynchronizerFactory.java | 3 +- .../MySegmentsSynchronizerFactoryImpl.java | 11 +- .../MySegmentsSynchronizerImpl.java | 29 +- .../MySegmentsSynchronizerRegistryImpl.java | 70 ++-- .../MySegmentsWorkManagerWrapper.java | 1 + .../telemetry/TelemetryTaskFactoryImpl.java | 3 +- .../workmanager/BaseSegmentsSyncWorker.java | 68 ++++ .../workmanager/MySegmentsSyncWorker.java | 67 +--- .../splits/SplitsSyncWorkerTaskBuilder.java | 2 - .../shared/ClientComponentsRegister.java | 2 +- .../shared/ClientComponentsRegisterImpl.java | 45 ++- .../shared/SplitClientContainerImpl.java | 10 +- .../storage/cipher/ApplyCipherTask.java | 19 +- .../storage/common/SplitStorageContainer.java | 11 + .../client/storage/db/MyLargeSegmentDao.java | 30 ++ .../storage/db/MyLargeSegmentEntity.java | 23 ++ .../client/storage/db/MySegmentDao.java | 14 +- .../client/storage/db/MySegmentEntity.java | 57 +-- .../android/client/storage/db/SegmentDao.java | 14 + .../client/storage/db/SegmentEntity.java | 57 +++ .../client/storage/db/SplitRoomDatabase.java | 7 +- .../client/storage/db/StorageFactory.java | 14 +- .../mysegments/EmptyMySegmentsStorage.java | 12 +- .../storage/mysegments/MySegmentsStorage.java | 10 +- .../mysegments/MySegmentsStorageImpl.java | 53 ++- .../PersistentMySegmentsStorage.java | 8 +- .../SqLitePersistentMySegmentsStorage.java | 67 ++-- .../client/telemetry/model/Config.java | 22 + .../client/telemetry/model/HttpErrors.java | 11 + .../client/telemetry/model/HttpLatencies.java | 11 + .../client/telemetry/model/LastSync.java | 25 +- .../client/telemetry/model/OperationType.java | 3 +- .../client/telemetry/model/RefreshRates.java | 11 + .../android/client/telemetry/model/Stats.java | 12 + .../telemetry/model/UpdatesFromSSE.java | 10 +- .../model/streaming/UpdatesFromSSEEnum.java | 1 + .../storage/InMemoryTelemetryStorage.java | 15 +- .../storage/TelemetryConfigProviderImpl.java | 20 + .../storage/TelemetryStatsProviderImpl.java | 24 +- .../android/client/utils/StringHelper.java | 1 + .../io/split/android/client/utils/Utils.java | 6 + .../engine/experiments/SplitParser.java | 18 +- src/sharedTest/java/helper/TestingData.java | 48 +-- .../client/MySegmentsUriBuildersTest.java | 25 ++ .../client/SplitClientImplBaseTest.java | 4 +- .../android/client/SplitManagerImplTest.java | 4 +- .../android/client/TreatmentManagerTest.java | 3 +- .../client/events/EventsManagerTest.java | 224 ++++++----- .../client/network/HttpClientTest.java | 23 +- .../client/network/SdkTargetPathTest.java | 6 +- .../client/service/HttpFetcherTest.java | 44 +- .../service/MySegmentsOverwriteTaskTest.java | 87 ---- .../service/MySegmentsSyncTaskTest.java | 235 ++++++++++- .../service/MySegmentsUpdateTaskTest.java | 118 +++++- .../client/service/SplitSyncTaskTest.java | 16 +- .../client/service/SplitTaskExecutorTest.java | 27 +- .../client/service/SplitUpdateTaskTest.java | 20 +- .../client/service/SplitsSyncHelperTest.java | 38 +- .../client/service/SynchronizerTest.java | 54 ++- .../MySegmentsFetcherFactoryImplTest.java | 40 ++ .../AllSegmentsResponseParserTest.java | 38 ++ .../LoadMySegmentsTaskConfigTest.java | 17 + .../MySegmentsSyncTaskConfigTest.java | 22 + ...ySegmentsTaskFactoryConfigurationTest.java | 45 +++ .../MySegmentsUpdateTaskConfigTest.java | 21 + .../sseclient/MySegmentsUpdateWorkerTest.java | 30 +- .../sseclient/NotificationParserTest.java | 87 ++-- .../sseclient/NotificationProcessorTest.java | 46 +-- .../service/sseclient/SseHandlerTest.java | 25 +- .../service/sseclient/SyncManagerTest.java | 14 + .../MySegmentsPayloadDecoderTest.java | 23 -- ...SegmentsNotificationProcessorImplTest.java | 199 ++++----- .../mysegments/SyncDelayCalculatorTest.java | 81 ++++ .../RetryBackoffCounterTimerTest.java | 20 +- .../MySegmentsSynchronizerImplTest.java | 45 ++- ...ySegmentsSynchronizerRegistryImplTest.java | 16 +- .../ClientComponentsRegisterImplTest.java | 60 +-- .../shared/SplitClientContainerImplTest.java | 1 + .../storage/cipher/ApplyCipherTaskTest.kt | 277 ++++++++++++- .../MySegmentsStorageContainerImplTest.java | 7 +- ...ePersistentMyLargeSegmentsStorageTest.java | 113 ++++++ ...SqLitePersistentMySegmentsStorageTest.java | 57 ++- .../TelemetryConfigBodySerializerTest.java | 7 +- .../TelemetryStatsBodySerializerTest.java | 7 +- .../storage/InMemoryTelemetryStorageTest.java | 8 + .../TelemetryStatsProviderImplTest.java | 10 +- .../client/utils/SplitClientImplFactory.java | 6 +- .../engine/experiments/EvaluatorTest.java | 34 +- .../engine/experiments/SplitParserTest.java | 63 ++- .../UnsupportedMatcherSplitParserTest.java | 4 +- .../android/fake/SplitTaskExecutorStub.java | 2 +- src/test/resources/split_changes_1.json | 46 +++ 249 files changed, 5595 insertions(+), 2051 deletions(-) create mode 100644 schemas/io.split.android.client.storage.db.SplitRoomDatabase/6.json create mode 100644 src/androidTest/assets/push_msg-largesegment_update.txt create mode 100644 src/androidTest/assets/split_changes_large_segments-0.json create mode 100644 src/androidTest/java/tests/database/MyLargeSegmentDaoTest.java create mode 100644 src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java create mode 100644 src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java create mode 100644 src/androidTest/java/tests/integration/largesegments/LargeSegmentsTest.java create mode 100644 src/androidTest/java/tests/storage/PersistentMyLargeSegmentStorageTest.java create mode 100644 src/main/java/io/split/android/client/dtos/AllSegmentsChange.java delete mode 100644 src/main/java/io/split/android/client/dtos/MySegment.java create mode 100644 src/main/java/io/split/android/client/dtos/Segment.java delete mode 100644 src/main/java/io/split/android/client/dtos/SegmentChange.java create mode 100644 src/main/java/io/split/android/client/dtos/SegmentResponse.java create mode 100644 src/main/java/io/split/android/client/dtos/SegmentsChange.java create mode 100644 src/main/java/io/split/android/client/dtos/UserDefinedLargeSegmentMatcherData.java create mode 100644 src/main/java/io/split/android/client/service/mysegments/AllSegmentsResponseParser.java create mode 100644 src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java create mode 100644 src/main/java/io/split/android/client/service/mysegments/MySegmentUpdateParams.java delete mode 100644 src/main/java/io/split/android/client/service/mysegments/MySegmentsOverwriteTask.java delete mode 100644 src/main/java/io/split/android/client/service/mysegments/MySegmentsResponseParser.java create mode 100644 src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java create mode 100644 src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java create mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/HashingAlgorithm.java create mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java delete mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentChangeNotification.java delete mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentChangeV2Notification.java delete mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsPayloadDecoder.java create mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessor.java create mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java create mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactory.java create mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java delete mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessor.java delete mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorFactory.java delete mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorFactoryImpl.java delete mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImpl.java create mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculator.java create mode 100644 src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorImpl.java create mode 100644 src/main/java/io/split/android/client/service/workmanager/BaseSegmentsSyncWorker.java create mode 100644 src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java create mode 100644 src/main/java/io/split/android/client/storage/db/MyLargeSegmentEntity.java create mode 100644 src/main/java/io/split/android/client/storage/db/SegmentDao.java create mode 100644 src/main/java/io/split/android/client/storage/db/SegmentEntity.java create mode 100644 src/test/java/io/split/android/client/MySegmentsUriBuildersTest.java delete mode 100644 src/test/java/io/split/android/client/service/MySegmentsOverwriteTaskTest.java create mode 100644 src/test/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImplTest.java create mode 100644 src/test/java/io/split/android/client/service/mysegments/AllSegmentsResponseParserTest.java create mode 100644 src/test/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfigTest.java create mode 100644 src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java create mode 100644 src/test/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfigurationTest.java create mode 100644 src/test/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfigTest.java delete mode 100644 src/test/java/io/split/android/client/service/sseclient/notifications/MySegmentsPayloadDecoderTest.java create mode 100644 src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorTest.java create mode 100644 src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fa1639912..908cacdfd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,8 @@ name: Internal deploy on: push: branches: - - development + - 'development' + - '*_baseline' jobs: build-app: diff --git a/build.gradle b/build.gradle index d7cf463c8..e7a7ca5ec 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '4.2.2' + splitVersion = '5.0.0-alpha.1' } android { @@ -323,3 +323,10 @@ task printReleaseDependenciesToFile { } preBuild.dependsOn printReleaseDependenciesToFile + +tasks.withType(Test) { + systemProperties['junit.jupiter.execution.parallel.enabled'] = true + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 + forkEvery = 100 + maxHeapSize = "1024m" +} diff --git a/schemas/io.split.android.client.storage.db.SplitRoomDatabase/6.json b/schemas/io.split.android.client.storage.db.SplitRoomDatabase/6.json new file mode 100644 index 000000000..dde7c5c56 --- /dev/null +++ b/schemas/io.split.android.client.storage.db.SplitRoomDatabase/6.json @@ -0,0 +1,376 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "9e30c6e6f20f45fcf324c68c2d98920f", + "entities": [ + { + "tableName": "my_segments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_key` TEXT NOT NULL, `segment_list` TEXT NOT NULL, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`user_key`))", + "fields": [ + { + "fieldPath": "userKey", + "columnName": "user_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "segmentList", + "columnName": "segment_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "splits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `body` TEXT NOT NULL, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `body` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `status` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "impressions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `test_name` TEXT NOT NULL, `body` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `status` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "testName", + "columnName": "test_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "general_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `stringValue` TEXT, `longValue` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stringValue", + "columnName": "stringValue", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "longValue", + "columnName": "longValue", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "impressions_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `body` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `status` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "body", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attributes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_key` TEXT NOT NULL, `attributes` TEXT, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`user_key`))", + "fields": [ + { + "fieldPath": "userKey", + "columnName": "user_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "unique_keys", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_key` TEXT NOT NULL, `feature_list` TEXT, `created_at` INTEGER NOT NULL, `status` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userKey", + "columnName": "user_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "featureList", + "columnName": "feature_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "impressions_observer_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hash` INTEGER NOT NULL, `time` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`hash`))", + "fields": [ + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "hash" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "my_large_segments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_key` TEXT NOT NULL, `segment_list` TEXT NOT NULL, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`user_key`))", + "fields": [ + { + "fieldPath": "userKey", + "columnName": "user_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "segmentList", + "columnName": "segment_list", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9e30c6e6f20f45fcf324c68c2d98920f')" + ] + } +} \ No newline at end of file diff --git a/spec.gradle b/spec.gradle index 00232a7ca..b2e667495 100644 --- a/spec.gradle +++ b/spec.gradle @@ -1,5 +1,5 @@ ext { // flags spec contains a String with the feature flags specification - flagsSpec = '1.1' + flagsSpec = '1.2' } diff --git a/src/androidTest/assets/attributes_test_split_change.json b/src/androidTest/assets/attributes_test_split_change.json index 8345a08ae..980b0a0fb 100644 --- a/src/androidTest/assets/attributes_test_split_change.json +++ b/src/androidTest/assets/attributes_test_split_change.json @@ -177,6 +177,6 @@ "label":"rule 3" } ], - "since":1602796401438, + "since":1602796638344, "till":1602796638344 } \ No newline at end of file diff --git a/src/androidTest/assets/bucket_split_test.json b/src/androidTest/assets/bucket_split_test.json index b420fdab2..1a9d3e212 100644 --- a/src/androidTest/assets/bucket_split_test.json +++ b/src/androidTest/assets/bucket_split_test.json @@ -434,6 +434,6 @@ "killed": false } ], - "since":-1, + "since":1506703262916, "till":1506703262916 } diff --git a/src/androidTest/assets/push_msg-largesegment_update.txt b/src/androidTest/assets/push_msg-largesegment_update.txt new file mode 100644 index 000000000..168a77782 --- /dev/null +++ b/src/androidTest/assets/push_msg-largesegment_update.txt @@ -0,0 +1,3 @@ +id:cf74eb42-f687-48e4-ad18-af2125110aac +event:message +data:{"id":"x2dE2TEiJL:0:0","clientId":"NDEzMTY5Mzg0MA==:OTc5Nzc4NDYz","timestamp":1584647533288,"encoding":"json","channel":"MzM5´Njc0ODcyNg==_MTExMzgwNjgx_MTcwNTI2MTM0Mg==_mySegments","data":"[NOTIFICATION_DATA]"} diff --git a/src/androidTest/assets/simple_split.json b/src/androidTest/assets/simple_split.json index abdbe4a1b..c3e17e20b 100644 --- a/src/androidTest/assets/simple_split.json +++ b/src/androidTest/assets/simple_split.json @@ -102,6 +102,6 @@ ] } ], - "since": 1602796401438, + "since": 1602796638344, "till": 1602796638344 } diff --git a/src/androidTest/assets/split_changes_1.json b/src/androidTest/assets/split_changes_1.json index 5c886ea97..7eee38b79 100644 --- a/src/androidTest/assets/split_changes_1.json +++ b/src/androidTest/assets/split_changes_1.json @@ -2528,6 +2528,6 @@ "changeNumber":1494593336752 } ], - "since":-1, + "since":1506703262916, "till":1506703262916 } \ No newline at end of file diff --git a/src/androidTest/assets/split_changes_flag_set-2.json b/src/androidTest/assets/split_changes_flag_set-2.json index a96e3e209..0e7576af6 100644 --- a/src/androidTest/assets/split_changes_flag_set-2.json +++ b/src/androidTest/assets/split_changes_flag_set-2.json @@ -1 +1 @@ -{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_1","set_2"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","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":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]},{"trafficTypeName":"client","name":"workm_set_3","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","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":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":-1,"till":1602796638344} +{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_1","set_2"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","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":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]},{"trafficTypeName":"client","name":"workm_set_3","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","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":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602796638344,"till":1602796638344} diff --git a/src/androidTest/assets/split_changes_large_segments-0.json b/src/androidTest/assets/split_changes_large_segments-0.json new file mode 100644 index 000000000..975f211d0 --- /dev/null +++ b/src/androidTest/assets/split_changes_large_segments-0.json @@ -0,0 +1,49 @@ +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "ls_split", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_LARGE_SEGMENT", + "negate": false, + "userDefinedLargeSegmentMatcherData": { + "largeSegmentName": "large-segment1" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ], + "label": "whitelisted segment" + } + ] + } + ], + "since": 1506703262916, + "till": 1506703262916 +} diff --git a/src/androidTest/assets/splitchanges_int_test.json b/src/androidTest/assets/splitchanges_int_test.json index c89d4305f..cd02900d2 100644 --- a/src/androidTest/assets/splitchanges_int_test.json +++ b/src/androidTest/assets/splitchanges_int_test.json @@ -52,6 +52,6 @@ ] } ], - "since":-1, + "since":1567456937865, "till":1567456937865 } \ No newline at end of file diff --git a/src/androidTest/java/fake/SynchronizerSpyImpl.java b/src/androidTest/java/fake/SynchronizerSpyImpl.java index 88a22b8c3..6971258eb 100644 --- a/src/androidTest/java/fake/SynchronizerSpyImpl.java +++ b/src/androidTest/java/fake/SynchronizerSpyImpl.java @@ -55,12 +55,6 @@ public void synchronizeMySegments() { mSynchronizer.synchronizeMySegments(); } - @Override - public void forceMySegmentsSync() { - mSynchronizer.forceMySegmentsSync(); - mForceMySegmentSyncCalledCount.addAndGet(1); - } - @Override public void startPeriodicFetching() { mSynchronizer.startPeriodicFetching(); diff --git a/src/androidTest/java/helper/IntegrationHelper.java b/src/androidTest/java/helper/IntegrationHelper.java index 8837da304..09a17e8b0 100644 --- a/src/androidTest/java/helper/IntegrationHelper.java +++ b/src/androidTest/java/helper/IntegrationHelper.java @@ -3,6 +3,7 @@ import android.content.Context; import android.util.Base64; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; @@ -20,9 +21,12 @@ import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; import fake.HttpClientMock; import fake.HttpResponseMock; @@ -153,12 +157,27 @@ public static SplitFactory buildFactory(String apiToken, Key key, SplitClientCon return factory; } - public static String dummyMySegments() { - return "{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"; + @Deprecated + public static String emptyMySegments() { + return emptyAllSegments(); } - public static String emptyMySegments() { - return "{\"mySegments\":[]}"; + public static String emptyAllSegments() { + return "{\"ms\":{\"k\":[],\"cn\":null},\"ls\":{\"k\":[],\"cn\":1702507130121}}"; + } + + public static String dummyAllSegments() { + return "{\"ms\":{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}],\"cn\":null},\"ls\":{\"k\":[{\"n\":\"large-segment1\"},{\"n\":\"large-segment2\"},{\"n\":\"large-segment3\"}],\"cn\":1702507130121}}"; + } + + public static String randomizedAllSegments() { + int randIntOne = (int) (Math.random() * 100); + int randIntTwo = (int) (Math.random() * 100); + return "{\"ms\":{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}],\"cn\":null},\"ls\":{\"k\":[{\"n\":\"large-segment" + randIntOne + "\"},{\"n\":\"large-segment" + randIntTwo + "\"}],\"cn\":1702507130121}}"; + } + + public static String dummySingleSegment(String segment) { + return "{\"ms\":{\"k\":[{\"n\":\"" + segment + "\"}],\"cn\":null},\"ls\":{\"k\":[],\"cn\":1702507130121}}"; } public static String dummyApiKey() { @@ -182,7 +201,7 @@ public static SplitClientConfig basicConfig() { return SplitClientConfig.builder() .ready(30000) .streamingEnabled(true) - .logLevel(SplitLogLevel.DEBUG) + .logLevel(SplitLogLevel.VERBOSE) .trafficType("account") .build(); } @@ -244,6 +263,15 @@ public static String streamingEnabledToken(int delay) { } + @Deprecated + public static String streamingEnabledTokenLargeSegments() { + return "{" + + " \"pushEnabled\": true," + + " \"connDelay\": " + 0 + "," + + " \"token\": \"eyJhbGciOiJIUzI1NiIsImtpZCI6IjVZOU05US45QnJtR0EiLCJ0eXAiOiJKV1QifQ.ewogICJ4LWFibHktY2FwYWJpbGl0eSI6ICJ7XCJNek01TmpjME9EY3lOZz09X01URXhNemd3TmpneF9NVGN3TlRJMk1UTTBNZz09X215U2VnbWVudHNcIjpbXCJzdWJzY3JpYmVcIl0sXCJNek01TmpjME9EY3lOZz09X01URXhNemd3TmpneF9NVGN3TlRJMk1UTTBNZz09X215bGFyZ2VzZWdtZW50c1wiOltcInN1YnNjcmliZVwiXSxcIk16TTVOamMwT0RjeU5nPT1fTVRFeE16Z3dOamd4X3NwbGl0c1wiOltcInN1YnNjcmliZVwiXSxcImNvbnRyb2xfcHJpXCI6W1wic3Vic2NyaWJlXCIsXCJjaGFubmVsLW1ldGFkYXRhOnB1Ymxpc2hlcnNcIl0sXCJjb250cm9sX3NlY1wiOltcInN1YnNjcmliZVwiLFwiY2hhbm5lbC1tZXRhZGF0YTpwdWJsaXNoZXJzXCJdfSIsCiAgIngtYWJseS1jbGllbnRJZCI6ICJjbGllbnRJZCIsCiAgImV4cCI6IDIyMDg5ODg4MDAsCiAgImlhdCI6IDE1ODc0MDQzODgKfQ==.LcKAXnkr-CiYVxZ7l38w9i98Y-BMAv9JlGP2i92nVQY\"" + + "}"; + } + public static String streamingDisabledToken() { return "{\"pushEnabled\": false }"; } @@ -276,13 +304,13 @@ public static String splitChangeV2CompressionType0() { public 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"; + "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"; } public static String splitKill(String changeNumber, String splitName) { return "id:cf74eb42-f687-48e4-ad18-af2125110aac\n" + "event:message\n" + - "data:{\"id\":\"-OT-rGuSwz:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:NDIxNjU0NTUyNw==\",\"timestamp\":"+System.currentTimeMillis()+",\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_KILL\\\",\\\"changeNumber\\\":" + changeNumber + ",\\\"defaultTreatment\\\":\\\"off\\\",\\\"splitName\\\":\\\"" + splitName + "\\\"}\"}\n"; + "data:{\"id\":\"-OT-rGuSwz:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:NDIxNjU0NTUyNw==\",\"timestamp\":" + System.currentTimeMillis() + ",\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_KILL\\\",\\\"changeNumber\\\":" + changeNumber + ",\\\"defaultTreatment\\\":\\\"off\\\",\\\"splitName\\\":\\\"" + splitName + "\\\"}\"}\n"; } public static String loadSplitChanges(Context context, String fileName) { @@ -303,14 +331,18 @@ public static HttpResponseMockDispatcher buildDispatcher(Map responses, @Nullable BlockingQueue streamingQueue) { + return buildDispatcher(responses, streamingQueue, null); + } + /** * Builds a dispatcher with the given responses. * - * @param responses The responses to be returned by the dispatcher. The keys are url paths. + * @param responses The responses to be returned by the dispatcher. The keys are url paths. * @param streamingQueue The streaming responses to be returned by the dispatcher. * @return The dispatcher to be used in {@link HttpClientMock} */ - public static HttpResponseMockDispatcher buildDispatcher(Map responses, @Nullable BlockingQueue streamingQueue) { + public static HttpResponseMockDispatcher buildDispatcher(Map responses, @Nullable BlockingQueue streamingQueue, CountDownLatch sseLatch) { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { @@ -330,6 +362,9 @@ public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { @Override public HttpStreamResponseMock getStreamResponse(URI uri) { try { + if (sseLatch != null) { + sseLatch.countDown(); + } return new HttpStreamResponseMock(200, streamingQueue); } catch (IOException e) { e.printStackTrace(); @@ -344,6 +379,17 @@ public static String sha256(byte[] encoded) throws NoSuchAlgorithmException { return Base64.encodeToString(digest.digest(encoded), Base64.NO_WRAP); } + @NonNull + public static Set asSet(T... elements) { + if (elements.length == 0) { + return Collections.emptySet(); + } + Set result = new HashSet<>(); + Collections.addAll(result, elements); + + return result; + } + /** * A simple interface to allow us to define the response for a given path */ @@ -386,4 +432,8 @@ static Map parse(String query) throws UnsupportedEncodingExcepti public interface StreamingResponseClosure { HttpStreamResponseMock onResponse(URI uri); } + + public static class ServicePath { + public static final String MEMBERSHIPS = "memberships"; + } } diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java index 2fb949d71..6709488ae 100644 --- a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java +++ b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java @@ -1,8 +1,6 @@ package io.split.android.client.service.impressions.observer; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; @@ -297,8 +295,8 @@ private HttpResponseMockDispatcher getDispatcher() { } }); - responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); - responses.put("mySegments/key2", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/key2", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); return IntegrationHelper.buildDispatcher(responses); } diff --git a/src/androidTest/java/tests/database/MyLargeSegmentDaoTest.java b/src/androidTest/java/tests/database/MyLargeSegmentDaoTest.java new file mode 100644 index 000000000..e4e41cd16 --- /dev/null +++ b/src/androidTest/java/tests/database/MyLargeSegmentDaoTest.java @@ -0,0 +1,79 @@ +package tests.database; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.split.android.client.storage.db.MyLargeSegmentEntity; + +public class MyLargeSegmentDaoTest extends GenericDaoTest { + + @Test + public void insertRetrieve() { + long timestamp = System.currentTimeMillis(); + mRoomDb.myLargeSegmentDao().update(generateData("key1", 1, 100, timestamp)); + mRoomDb.myLargeSegmentDao().update(generateData("key2",101, 200, timestamp)); + mRoomDb.myLargeSegmentDao().update(generateData("key3",201, 300, timestamp)); + + MyLargeSegmentEntity msKey1 = mRoomDb.myLargeSegmentDao().getByUserKey("key1"); + MyLargeSegmentEntity msKey2 = mRoomDb.myLargeSegmentDao().getByUserKey("key2"); + MyLargeSegmentEntity msKey3 = mRoomDb.myLargeSegmentDao().getByUserKey("key3"); + + Assert.assertEquals("key1", msKey1.getUserKey()); + Assert.assertEquals("key2", msKey2.getUserKey()); + Assert.assertEquals("key3", msKey3.getUserKey()); + + Assert.assertEquals(timestamp + 1, msKey1.getUpdatedAt()); + Assert.assertEquals(timestamp + 101, msKey2.getUpdatedAt()); + Assert.assertEquals(timestamp + 201, msKey3.getUpdatedAt()); + } + + @Test + public void insertUpdateRetrieve() { + long timestamp = System.currentTimeMillis(); + mRoomDb.myLargeSegmentDao().update(generateData("key1", 1, 10, timestamp)); + mRoomDb.myLargeSegmentDao().update(generateData("key1", 500, 505, timestamp)); + + MyLargeSegmentEntity msKey1 = mRoomDb.myLargeSegmentDao().getByUserKey("key1"); + Set segments = new HashSet(Arrays.asList(msKey1.getSegmentList().split(","))); + + Assert.assertEquals("key1", msKey1.getUserKey()); + Assert.assertEquals(timestamp + 500, msKey1.getUpdatedAt()); + Assert.assertFalse(segments.contains("segment1")); + Assert.assertTrue(segments.contains("segment500")); + Assert.assertTrue(segments.contains("segment505")); + } + + @Test + public void segmentsIntegrity() { + long timestamp = System.currentTimeMillis(); + mRoomDb.myLargeSegmentDao().update(generateData("key1", 1, 10, timestamp)); + + MyLargeSegmentEntity mySegmentEntity = mRoomDb.myLargeSegmentDao().getByUserKey("key1"); + Set segments = new HashSet(Arrays.asList(mySegmentEntity.getSegmentList().split(","))); + + Assert.assertEquals("key1", mySegmentEntity.getUserKey()); + Assert.assertEquals(timestamp + 1, mySegmentEntity.getUpdatedAt()); + Assert.assertEquals(10, segments.size()); + Assert.assertTrue(segments.contains("segment1")); + Assert.assertTrue(segments.contains("segment5")); + Assert.assertTrue(segments.contains("segment10")); + } + + private MyLargeSegmentEntity generateData(String key, int from, int to, long timestamp) { + MyLargeSegmentEntity segmentEntity = new MyLargeSegmentEntity(); + List mySegmentList = new ArrayList<>(); + for(int i = from; i<=to; i++) { + mySegmentList.add("segment" + i); + } + segmentEntity.setUserKey(key); + segmentEntity.setSegmentList(String.join(",", mySegmentList)); + segmentEntity.setUpdatedAt(timestamp + from); + return segmentEntity; + } +} diff --git a/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java b/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java index 8b06e403b..12ecce898 100644 --- a/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java +++ b/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java @@ -122,7 +122,7 @@ public void authContainsFlagsSpec() throws InterruptedException { TestingConfig testingConfig = new TestingConfig(); initSplitFactory(new TestableSplitConfigBuilder(), mHttpClient, testingConfig); - assertEquals("s=1.1&users=CUSTOMER_ID", mAuthUrl.get().getQuery()); + assertEquals("s=1.2&users=CUSTOMER_ID", mAuthUrl.get().getQuery()); } @Test @@ -156,7 +156,7 @@ private HttpResponseMockDispatcher getDispatcher() { } }); - responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); responses.put("v2/auth", (uri, httpMethod, body) -> { mAuthUrl.set(uri); diff --git a/src/androidTest/java/tests/integration/InitialChangeNumberTest.java b/src/androidTest/java/tests/integration/InitialChangeNumberTest.java index b724aab38..8ae2f6c62 100644 --- a/src/androidTest/java/tests/integration/InitialChangeNumberTest.java +++ b/src/androidTest/java/tests/integration/InitialChangeNumberTest.java @@ -62,8 +62,8 @@ private void setupServer() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("/mySegments")) { - return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); } else if (request.getPath().contains("/splitChanges")) { long changeNumber = -1; @@ -74,7 +74,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio mIsFirstChangeNumber = false; } return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\":" + changeNumber + ", \"till\":" + (changeNumber + 1000) + "}"); + .setBody("{\"splits\":[], \"since\":" + changeNumber + ", \"till\":" + (changeNumber) + "}"); } else if (request.getPath().contains("/events/bulk")) { String trackRequestBody = request.getBody().readUtf8(); @@ -97,7 +97,7 @@ public void firstRequestChangeNumber() throws Exception { splitRoomDatabase.clearAllTables(); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, INITIAL_CHANGE_NUMBER)); - splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis() / 1000)); + splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis())); SplitClient client; @@ -114,7 +114,7 @@ public void firstRequestChangeNumber() throws Exception { .segmentsRefreshRate(30) .impressionsRefreshRate(99999) .streamingEnabled(false) - .logLevel(SplitLogLevel.DEBUG) + .logLevel(SplitLogLevel.VERBOSE) .build(); @@ -139,9 +139,4 @@ public void firstRequestChangeNumber() throws Exception { Assert.assertTrue(readyFromCacheTask.isOnPostExecutionCalled); Assert.assertEquals(INITIAL_CHANGE_NUMBER, mFirstChangeNumberReceived); // Checks that change number is the bigger number from cached splitss } - - private void log(String m) { - System.out.println("FACTORY_TEST: " + m); - } - } diff --git a/src/androidTest/java/tests/integration/IntegrationTest.java b/src/androidTest/java/tests/integration/IntegrationTest.java index 3d4b00759..66f023e13 100644 --- a/src/androidTest/java/tests/integration/IntegrationTest.java +++ b/src/androidTest/java/tests/integration/IntegrationTest.java @@ -83,8 +83,8 @@ private void setupServer() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("/mySegments")) { - return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); } else if (request.getPath().contains("/splitChanges")) { int r = mCurSplitReqId; mCurSplitReqId++; diff --git a/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java b/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java index 2c2e6ce26..34b5dcc25 100644 --- a/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java +++ b/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java @@ -26,8 +26,8 @@ import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; -import io.split.android.client.SplitFactoryBuilder; import io.split.android.client.api.Key; +import io.split.android.client.dtos.Condition; import io.split.android.client.dtos.ConditionType; import io.split.android.client.dtos.KeyImpression; import io.split.android.client.dtos.Matcher; @@ -39,7 +39,6 @@ import io.split.android.client.dtos.SplitChange; import io.split.android.client.dtos.TestImpressions; import io.split.android.client.dtos.UserDefinedSegmentMatcherData; -import io.split.android.client.dtos.Condition; import io.split.android.client.events.SplitEvent; import io.split.android.client.service.impressions.ImpressionsMode; import io.split.android.client.storage.db.SplitRoomDatabase; @@ -89,19 +88,19 @@ private void setupServer() throws IOException { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("/mySegments")) { + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { String data; int index = mCurReqId; switch (index) { case 1: - data = "{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}]}"; + data = IntegrationHelper.dummySingleSegment("segment1"); break; case 2: - data = "{\"mySegments\":[{ \"id\":\"id2\", \"name\":\"segment2\"}]}"; + data = IntegrationHelper.dummySingleSegment("segment2"); break; default: - data = "{\"mySegments\":[]}"; + data = IntegrationHelper.emptyAllSegments(); } if(index > 0 && index <= mLatchs.size()) { diff --git a/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java b/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java index 852cfb55e..25221044d 100644 --- a/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java +++ b/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java @@ -24,7 +24,6 @@ import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; -import io.split.android.client.SplitFactoryBuilder; import io.split.android.client.api.Key; import io.split.android.client.dtos.Condition; import io.split.android.client.dtos.ConditionType; @@ -86,14 +85,14 @@ private void setupServer() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("/mySegments")) { + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { int code = 200; String data = null; int index = mCurReqId; switch (index) { case 0: - data = "{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}]}"; + data = IntegrationHelper.dummySingleSegment("segment1"); break; case 1: case 2: @@ -101,7 +100,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio data = ""; break; case 3: - data = "{\"mySegments\":[{ \"id\":\"id2\", \"name\":\"segment2\"}]}"; + data = IntegrationHelper.dummySingleSegment("segment2"); } if(index > 0 && index <= mLatchs.size()) { diff --git a/src/androidTest/java/tests/integration/ProxyFactoryTest.java b/src/androidTest/java/tests/integration/ProxyFactoryTest.java index 2864d8460..28f80e19d 100644 --- a/src/androidTest/java/tests/integration/ProxyFactoryTest.java +++ b/src/androidTest/java/tests/integration/ProxyFactoryTest.java @@ -92,9 +92,9 @@ public void settingProxyReplacesAllUrls() throws IOException, InterruptedExcepti @Override public MockResponse dispatch(RecordedRequest request) { String requestLine = request.getRequestLine(); - if (requestLine.contains("/mySegments")) { + if (requestLine.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { mySegmentsHit.set(true); - return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); } else if (requestLine.contains("splitChanges")) { splitChangesHit.set(true); return new MockResponse().setResponseCode(200).setBody(mSplitChanges); @@ -198,9 +198,9 @@ public MockResponse dispatch(RecordedRequest request) { if (request.getHeader("Proxy-Authorization") != null) { proxyAuthorizationHeaderLatch.countDown(); } - if (requestLine.contains("/mySegments")) { + if (requestLine.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { mySegmentsHit.set(true); - return new MockResponse().setResponseCode((request.getHeader("Proxy-Authorization") != null) ? 200 : 407).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + return new MockResponse().setResponseCode((request.getHeader("Proxy-Authorization") != null) ? 200 : 407).setBody(IntegrationHelper.dummyAllSegments()); } else if (requestLine.contains("splitChanges")) { splitChangesHit.set(true); return new MockResponse().setResponseCode((request.getHeader("Proxy-Authorization") != null) ? 200 : 407).setBody(mSplitChanges); diff --git a/src/androidTest/java/tests/integration/SingleSyncTest.java b/src/androidTest/java/tests/integration/SingleSyncTest.java index 205e8769a..4dd623b3b 100644 --- a/src/androidTest/java/tests/integration/SingleSyncTest.java +++ b/src/androidTest/java/tests/integration/SingleSyncTest.java @@ -279,7 +279,7 @@ public HttpStreamResponseMock getStreamResponse(URI uri) { public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { System.out.println("Path is " + uri.getPath()); - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { mMySegmentsHitCount++; return new HttpResponseMock(200, IntegrationHelper.emptyMySegments()); } else if (uri.getPath().contains("/splitChanges")) { diff --git a/src/androidTest/java/tests/integration/SplitChangesCdnBypassTest.java b/src/androidTest/java/tests/integration/SplitChangesCdnBypassTest.java index 17837d003..2edadda5b 100644 --- a/src/androidTest/java/tests/integration/SplitChangesCdnBypassTest.java +++ b/src/androidTest/java/tests/integration/SplitChangesCdnBypassTest.java @@ -57,7 +57,7 @@ public void setup() throws IOException { SplitRoomDatabase splitRoomDatabase = DatabaseHelper.getTestDatabase(mContext); splitRoomDatabase.clearAllTables(); splitRoomDatabase.generalInfoDao().update( - new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis() / 1000 - 30)); + new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis() - 30)); SplitClientConfig config = new TestableSplitConfigBuilder().ready(30000) .streamingEnabled(true) .enableDebug() @@ -116,9 +116,8 @@ public HttpStreamResponseMock getStreamResponse(URI uri) { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { - return new HttpResponseMock(200, "{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, " + - "{ \"id\":\"id1\", \"name\":\"segment2\"}]}"); + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new HttpResponseMock(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { System.out.println("URL HIT: " + uri.getPath()); diff --git a/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java b/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java index 369b9be9a..562939151 100644 --- a/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java +++ b/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java @@ -79,9 +79,8 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { - return new HttpResponseMock(200, "{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, " + - "{ \"id\":\"id1\", \"name\":\"segment2\"}]}"); + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new HttpResponseMock(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { int currReq = mCurSplitReqId; diff --git a/src/androidTest/java/tests/integration/SplitChangesTest.java b/src/androidTest/java/tests/integration/SplitChangesTest.java index 0d4c91cb5..754854d4c 100644 --- a/src/androidTest/java/tests/integration/SplitChangesTest.java +++ b/src/androidTest/java/tests/integration/SplitChangesTest.java @@ -81,11 +81,10 @@ private void setupServer() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("/mySegments")) { + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { return new MockResponse() .setResponseCode(200) - .setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, " + - "{ \"id\":\"id1\", \"name\":\"segment2\"}]}"); + .setBody(IntegrationHelper.dummyAllSegments()); } else if (request.getPath().contains("/splitChanges")) { int currReq = mCurSplitReqId; @@ -100,7 +99,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio mLatchs.get(currReq - 1).countDown(); } return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\": 9567456937869, \"till\": 9567456937869 }"); + .setBody("{\"splits\":[], \"since\": 1567456938865, \"till\": 1567456938865 }"); } else if (request.getPath().contains("/testImpressions/bulk")) { @@ -170,12 +169,12 @@ public void test() throws Exception { } treatments.add(client.getTreatment("test_feature")); } + Thread.sleep(1000); + client.destroy(); boolean impAwait = mImpLatch.await(10, TimeUnit.SECONDS); if (!impAwait) { Assert.fail("Impressions not received"); } - client.destroy(); - Thread.sleep(1000); ArrayList impLis = new ArrayList<>(); impLis.add(impListener.getImpression(impListener.buildKey( diff --git a/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java b/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java index 076eda287..22e102082 100644 --- a/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java +++ b/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java @@ -75,8 +75,8 @@ private void setupServer() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("/mySegments")) { - return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); } else if (request.getPath().contains("/splitChanges")) { Logger.d("Req: " + mCurSplitReqId + " -> qs =" + mReceivedQueryString); if (mCurSplitReqId == 1) { @@ -109,7 +109,7 @@ public void testAll() throws Exception { splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 2)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING, expectedQs)); - splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis() / 1000)); + splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis())); SplitClient client; final String url = mWebServer.url("/").url().toString(); diff --git a/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java b/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java index 6301a628d..1183f7d7a 100644 --- a/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java +++ b/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java @@ -170,12 +170,12 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher(int factoryNumb return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { int hit = 0; - long changeNumber = Long.parseLong(getSinceFromUri(uri));//new Integer(uri.getQuery().split("&")[1].split("=")[1]); + long changeNumber = Long.parseLong(getSinceFromUri(uri)); if (factoryNumber == 1) { System.out.println("hit 1 cn: " + changeNumber); f1ChangeNumbers.add(changeNumber); @@ -191,9 +191,13 @@ public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { return createResponse(200, getSplitChanges(factoryNumber, hit)); } String data = IntegrationHelper.emptySplitChanges(respChangeNumber, respChangeNumber); - CountDownLatch latch = mSplitsUpdateLatch.get(factoryNumber - 1); - if (latch != null) { - latch.countDown(); + try { + CountDownLatch latch = mSplitsUpdateLatch.get(factoryNumber - 1); + if (latch != null) { + latch.countDown(); + } + } catch (Exception ex) { + ex.printStackTrace(); } return createResponse(200, data); } else if (uri.getPath().contains("/auth")) { diff --git a/src/androidTest/java/tests/integration/TrackTest.java b/src/androidTest/java/tests/integration/TrackTest.java index fcb436666..2db076a2b 100644 --- a/src/androidTest/java/tests/integration/TrackTest.java +++ b/src/androidTest/java/tests/integration/TrackTest.java @@ -13,7 +13,6 @@ import org.junit.Test; import java.io.IOException; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -21,7 +20,6 @@ import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import helper.DatabaseHelper; import helper.ImpressionListenerHelper; @@ -77,8 +75,8 @@ private void setupServer() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("/mySegments")) { - return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[]}"); + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.emptyAllSegments()); } else if (request.getPath().contains("/splitChanges")) { return new MockResponse().setResponseCode(200) diff --git a/src/androidTest/java/tests/integration/encryption/EncryptionTest.java b/src/androidTest/java/tests/integration/encryption/EncryptionTest.java index 03bc9a759..b8ef6be27 100644 --- a/src/androidTest/java/tests/integration/encryption/EncryptionTest.java +++ b/src/androidTest/java/tests/integration/encryption/EncryptionTest.java @@ -33,10 +33,10 @@ import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.service.impressions.ImpressionsMode; -import io.split.android.client.shared.UserConsent; import io.split.android.client.storage.db.EventEntity; import io.split.android.client.storage.db.ImpressionEntity; import io.split.android.client.storage.db.ImpressionsCountEntity; +import io.split.android.client.storage.db.MyLargeSegmentEntity; import io.split.android.client.storage.db.MySegmentEntity; import io.split.android.client.storage.db.SplitEntity; import io.split.android.client.storage.db.SplitRoomDatabase; @@ -70,6 +70,7 @@ public void onPostExecutionView(SplitClient client) { verifySplits(testDatabase); verifySegments(testDatabase); + verifyLargeSegments(testDatabase); verifyEvents(testDatabase); } @@ -118,7 +119,7 @@ public void onPostExecutionView(SplitClient client) { } } try { - Thread.sleep(200); + Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } @@ -148,12 +149,16 @@ public void onPostExecutionView(SplitClient client) { client.getTreatment("FACUNDO_TEST"); client.getTreatment("testing"); factory.destroy(); - latch.countDown(); + try { + Thread.sleep(1000); + latch.countDown(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } } }); - assertTrue(latch.await(2, TimeUnit.SECONDS)); - Thread.sleep(250); + assertTrue(latch.await(5, TimeUnit.SECONDS)); verifyUniqueKeys(testDatabase); } @@ -213,8 +218,8 @@ private static void verifySplits(SplitRoomDatabase testDatabase) { fail("Split name not encrypted, it was " + splitEntity.getName()); } - boolean bodyCondition = splitEntity.getBody().trim().endsWith("="); - if (!bodyCondition) { + boolean bodyCondition = splitEntity.getBody().trim().endsWith("}"); + if (bodyCondition) { fail("Body not encrypted, it was " + splitEntity.getBody()); } } @@ -230,7 +235,7 @@ private static void verifySegments(SplitRoomDatabase testDatabase) { fail("Segment user key not encrypted, it was " + segmentEntity.getUserKey()); } - boolean bodyCondition = segmentEntity.getSegmentList().trim().endsWith("="); + boolean bodyCondition = !segmentEntity.getSegmentList().trim().endsWith("}"); if (!bodyCondition) { fail("Segment list not encrypted, it was " + segmentEntity.getSegmentList()); } @@ -239,6 +244,23 @@ private static void verifySegments(SplitRoomDatabase testDatabase) { assertEquals(1, all.size()); } + private static void verifyLargeSegments(SplitRoomDatabase testDatabase) { + List all = testDatabase.myLargeSegmentDao().getAll(); + for (MyLargeSegmentEntity segmentEntity : all) { + boolean nameCondition = segmentEntity.getUserKey().trim().contains("="); + if (!nameCondition) { + fail("Large segment user key not encrypted, it was " + segmentEntity.getUserKey()); + } + + boolean bodyCondition = segmentEntity.getSegmentList().trim().contains("large-segment"); + if (bodyCondition) { + fail("Large segment list not encrypted, it was " + segmentEntity.getSegmentList()); + } + } + + assertEquals(1, all.size()); + } + private static void verifyEvents(SplitRoomDatabase testDatabase) { List all = testDatabase.eventDao().getAll(); for (EventEntity entity : all) { @@ -308,6 +330,7 @@ private SplitFactory createFactory( .impressionsRefreshRate(1000) .impressionsCountersRefreshRate(1000) .streamingEnabled(false) + .enableDebug() .eventFlushInterval(1000) .encryptionEnabled(encryptionEnabled) .build(); @@ -327,7 +350,7 @@ private SplitFactory createFactory( private Map getResponses() { Map responses = new HashMap<>(); responses.put("splitChanges", (uri, httpMethod, body) -> new HttpResponseMock(200, loadSplitChanges())); - responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.dummyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.dummyAllSegments())); responses.put("events/bulk", (uri, httpMethod, body) -> new HttpResponseMock(404, "")); responses.put("testImpressions/bulk", (uri, httpMethod, body) -> new HttpResponseMock(404, "")); return responses; diff --git a/src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java b/src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java new file mode 100644 index 000000000..f8d876a69 --- /dev/null +++ b/src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java @@ -0,0 +1,160 @@ +package tests.integration.largesegments; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import helper.FileHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.ServiceEndpoints; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitFactory; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import tests.integration.shared.TestingHelper; + +public class LargeSegmentTestHelper { + + protected final FileHelper mFileHelper = new FileHelper(); + protected final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + protected MockWebServer mWebServer; + protected Map mEndpointHits; + protected Map mLatches; + protected final AtomicLong mMySegmentsDelay = new AtomicLong(0L); + protected final AtomicBoolean mRandomizeMyLargeSegments = new AtomicBoolean(false); + protected final AtomicBoolean mEmptyMyLargeSegments = new AtomicBoolean(false); + protected final AtomicBoolean mBrokenApi = new AtomicBoolean(false); + + @Before + public void setUp() throws IOException { + mEndpointHits = new ConcurrentHashMap<>(); + mMySegmentsDelay.set(0L); + mRandomizeMyLargeSegments.set(false); + mEmptyMyLargeSegments.set(false); + mBrokenApi.set(false); + initializeLatches(); + + mWebServer = new MockWebServer(); + mWebServer.setDispatcher(new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + System.out.println("Receiving request to " + request.getRequestUrl().toString()); + + if (mBrokenApi.get()) { + return new MockResponse().setResponseCode(500); + } + + if (request.getRequestUrl().encodedPathSegments().contains("splitChanges")) { + updateEndpointHit("splitChanges"); + return new MockResponse().setResponseCode(200).setBody(splitChangesLargeSegments(1602796638344L, 1602796638344L)); + } else if (request.getRequestUrl().encodedPathSegments().contains(IntegrationHelper.ServicePath.MEMBERSHIPS)) { + Thread.sleep(mMySegmentsDelay.get()); + updateEndpointHit(IntegrationHelper.ServicePath.MEMBERSHIPS); + + String body = (mEmptyMyLargeSegments.get()) ? IntegrationHelper.emptyAllSegments() : IntegrationHelper.dummyAllSegments(); + if (mRandomizeMyLargeSegments.get()) { + body = IntegrationHelper.randomizedAllSegments(); + } + + return new MockResponse().setResponseCode(200).setBody(body); + } else { + return new MockResponse().setResponseCode(404); + } + } + }); + mWebServer.start(); + } + + @After + public void tearDown() throws IOException { + mWebServer.close(); + } + + private void initializeLatches() { + mLatches = new ConcurrentHashMap<>(); + mLatches.put("splitChanges", new CountDownLatch(1)); + mLatches.put(IntegrationHelper.ServicePath.MEMBERSHIPS, new CountDownLatch(1)); + } + + private void updateEndpointHit(String splitChanges) { + if (mEndpointHits.containsKey(splitChanges)) { + mEndpointHits.get(splitChanges).getAndIncrement(); + } else { + mEndpointHits.put(splitChanges, new AtomicInteger(1)); + } + + if (mLatches.containsKey(splitChanges)) { + mLatches.get(splitChanges).countDown(); + } + } + + protected SplitFactory getFactory() { + return getFactory(null, 2500, null); + } + + protected SplitFactory getFactory(Integer segmentsRefreshRate, + Integer ready, SplitRoomDatabase database) { + TestableSplitConfigBuilder configBuilder = new TestableSplitConfigBuilder() + .enableDebug() + .serviceEndpoints(ServiceEndpoints.builder() + .apiEndpoint("http://" + mWebServer.getHostName() + ":" + mWebServer.getPort()) + .build()); + + if (segmentsRefreshRate != null) { + configBuilder.streamingEnabled(false); + configBuilder.segmentsRefreshRate(segmentsRefreshRate); + } + if (ready != null) { + configBuilder.ready(ready); + } + return IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + IntegrationHelper.dummyUserKey(), + configBuilder.build(), + mContext, + null, database, null, null, null); + } + + protected SplitClient getReadyClient(String matchingKey, SplitFactory factory) { + CountDownLatch countDownLatch = new CountDownLatch(1); + + SplitClient client = factory.client(matchingKey); + client.on(SplitEvent.SDK_READY, TestingHelper.testTask(countDownLatch)); + try { + countDownLatch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + return client; + } + + private String splitChangesLargeSegments(long since, long till) { + String change = mFileHelper.loadFileContent(mContext, "split_changes_large_segments-0.json"); + SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + parsedChange.since = since; + parsedChange.till = till; + + return Json.toJson(parsedChange); + } +} diff --git a/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java b/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java new file mode 100644 index 000000000..2b95f70ca --- /dev/null +++ b/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java @@ -0,0 +1,245 @@ +package tests.integration.largesegments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import helper.DatabaseHelper; +import helper.FileHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitFactory; +import io.split.android.client.dtos.SegmentsChange; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; +import tests.integration.shared.TestingData; +import tests.integration.shared.TestingHelper; + +public class LargeSegmentsStreamingTest { + + public static final String SPLIT_CHANGES = "splitChanges"; + public static final String MY_SEGMENTS = IntegrationHelper.ServicePath.MEMBERSHIPS; + public static final String AUTH = "v2/auth"; + public static final String SSE = "sse"; + private final FileHelper mFileHelper = new FileHelper(); + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private Map mEndpointHits; + private Map mLatches; + private final AtomicInteger mMyLargeSegmentsStatusCode = new AtomicInteger(200); + private final AtomicBoolean mRandomizeMyLargeSegments = new AtomicBoolean(false); + private BlockingQueue mStreamingData; + + @Before + public void setUp() throws IOException, InterruptedException { + mStreamingData = new LinkedBlockingQueue<>(); + mEndpointHits = new ConcurrentHashMap<>(); + mMyLargeSegmentsStatusCode.set(200); + mRandomizeMyLargeSegments.set(false); + initializeLatches(); + } + + @Test + public void unboundedLargeSegmentsUpdateTriggersSdkUpdate() throws IOException, InterruptedException { + TestSetup testSetup = getTestSetup(); + + boolean mySegmentsAwait = mLatches.get(MY_SEGMENTS).await(10, TimeUnit.SECONDS); + boolean splitsAwait = mLatches.get(SPLIT_CHANGES).await(10, TimeUnit.SECONDS); + String initialSegmentList = testSetup.database.myLargeSegmentDao().getByUserKey(IntegrationHelper.dummyUserKey().matchingKey()).getSegmentList(); + mRandomizeMyLargeSegments.set(true); + + pushMyLargeSegmentsMessage(TestingData.largeSegmentsUnboundedNoCompression("100")); + boolean updateAwait = testSetup.updateLatch.await(15, TimeUnit.SECONDS); + + assertTrue(testSetup.await); + assertTrue(testSetup.authAwait); + assertTrue(mySegmentsAwait); + assertTrue(splitsAwait); + assertTrue(updateAwait); + assertEquals(2, mEndpointHits.get(SPLIT_CHANGES).get()); + assertTrue(initialSegmentList.contains("large-segment1") && initialSegmentList.contains("large-segment2") && initialSegmentList.contains("large-segment3")); + assertEquals(2, Json.fromJson(testSetup.database.myLargeSegmentDao().getByUserKey(IntegrationHelper.dummyUserKey().matchingKey()).getSegmentList(), SegmentsChange.class).getSegments().size()); + assertEquals(3, mEndpointHits.get(MY_SEGMENTS).get()); + } + + @Test + public void segmentRemovalTriggersSdkUpdateAndRemovesSegmentFromStorage() throws IOException, InterruptedException { + TestSetup testSetup = getTestSetup(); + + boolean mySegmentsAwait = mLatches.get(MY_SEGMENTS).await(10, TimeUnit.SECONDS); + boolean splitsAwait = mLatches.get(SPLIT_CHANGES).await(10, TimeUnit.SECONDS); + + SplitRoomDatabase db = testSetup.database; + String initialLargeSegmentsSize = db.myLargeSegmentDao() + .getByUserKey(IntegrationHelper.dummyUserKey().matchingKey()) + .getSegmentList(); + + pushMyLargeSegmentsMessage(TestingData.largeSegmentsRemoval()); + boolean updateAwait = testSetup.updateLatch.await(10, TimeUnit.SECONDS); + + assertTrue(testSetup.await); + assertTrue(testSetup.authAwait); + assertTrue(mySegmentsAwait); + assertTrue(splitsAwait); + assertTrue(updateAwait); + assertTrue(initialLargeSegmentsSize.contains("large-segment1") && initialLargeSegmentsSize.contains("large-segment2") && initialLargeSegmentsSize.contains("large-segment3")); + String body = db.myLargeSegmentDao().getByUserKey(IntegrationHelper.dummyUserKey().matchingKey()).getSegmentList(); + SegmentsChange segmentsChange = Json.fromJson(body, SegmentsChange.class); + assertEquals(1702507130121L, segmentsChange.getChangeNumber().longValue()); + assertEquals(1, segmentsChange.getSegments().size()); + } + + @NonNull + private TestSetup getTestSetup() throws IOException, InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch updateLatch = new CountDownLatch(1); + SplitRoomDatabase database = DatabaseHelper.getTestDatabase(mContext); + SplitFactory factory = getFactory(database); + + SplitClient client = factory.client(); + client.on(SplitEvent.SDK_READY, TestingHelper.testTask(latch)); + client.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(updateLatch)); + boolean await = latch.await(5, TimeUnit.SECONDS); + boolean authAwait = mLatches.get(AUTH).await(6, TimeUnit.SECONDS); + + // Wait for streaming connection + mLatches.get(SSE).await(15, TimeUnit.SECONDS); + + // Keep alive on streaming channel confirms connection + // so full sync is fired + TestingHelper.pushKeepAlive(mStreamingData); + return new TestSetup(updateLatch, database, await, authAwait); + } + + private SplitFactory getFactory(SplitRoomDatabase database) throws IOException { + if (database == null) { + database = DatabaseHelper.getTestDatabase(mContext); + } + TestableSplitConfigBuilder configBuilder = new TestableSplitConfigBuilder() + .streamingEnabled(true) + .enableDebug(); + + return IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + IntegrationHelper.dummyUserKey(), + configBuilder.build(), + mContext, + new HttpClientMock(buildDispatcher()), database, null, null, null); + } + + private HttpResponseMockDispatcher buildDispatcher() { + Map responses = new HashMap<>(); + responses.put(SPLIT_CHANGES, (path, query, body) -> { + updateEndpointHit(SPLIT_CHANGES); + return new HttpResponseMock(200, splitChangesLargeSegments(1602796638344L, 1602796638344L)); + }); + + String key = IntegrationHelper.dummyUserKey().matchingKey(); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + key, (path, query, body) -> { + updateEndpointHit(MY_SEGMENTS); + if (mMyLargeSegmentsStatusCode.get() != 200) { + return new HttpResponseMock(mMyLargeSegmentsStatusCode.get()); + } else { + String responseBody = IntegrationHelper.dummyAllSegments(); + if (mRandomizeMyLargeSegments.get()) { + responseBody = IntegrationHelper.randomizedAllSegments(); + } + return new HttpResponseMock(200, responseBody); + } + }); + + responses.put(AUTH, (path, query, body) -> { + try { + return new HttpResponseMock(200, IntegrationHelper.streamingEnabledToken()); + } finally { + updateEndpointHit(AUTH); + } + }); + + return IntegrationHelper.buildDispatcher(responses, mStreamingData, mLatches.get(SSE)); + } + + private String splitChangesLargeSegments(long since, long till) { + String change = mFileHelper.loadFileContent(mContext, "split_changes_large_segments-0.json"); + SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + parsedChange.since = since; + parsedChange.till = till; + + return Json.toJson(parsedChange); + } + + private void initializeLatches() { + mLatches = new ConcurrentHashMap<>(); + mLatches.put(SPLIT_CHANGES, new CountDownLatch(2)); + mLatches.put(MY_SEGMENTS, new CountDownLatch(2)); + mLatches.put(AUTH, new CountDownLatch(1)); + mLatches.put(SSE, new CountDownLatch(1)); + } + + private void updateEndpointHit(String splitChanges) { + if (mEndpointHits.containsKey(splitChanges)) { + mEndpointHits.get(splitChanges).getAndIncrement(); + } else { + mEndpointHits.put(splitChanges, new AtomicInteger(1)); + } + + if (mLatches.containsKey(splitChanges)) { + mLatches.get(splitChanges).countDown(); + } + } + + private void pushMyLargeSegmentsMessage(String msg) { + String MSG_SEGMENT_UPDATE_TEMPLATE = "push_msg-largesegment_update.txt"; + BlockingQueue queue = mStreamingData; + String message = loadMockedData(MSG_SEGMENT_UPDATE_TEMPLATE); + message = message.replace("$TIMESTAMP$", String.valueOf(System.currentTimeMillis())); + message = message.replace(TestingHelper.MSG_DATA_FIELD, msg); + try { + queue.put(message + "" + "\n"); + Logger.d("Pushed message: " + message); + } catch (InterruptedException e) { + } + } + + private String loadMockedData(String fileName) { + return mFileHelper.loadFileContent(mContext, fileName); + } + + private static class TestSetup { + public final CountDownLatch updateLatch; + public final SplitRoomDatabase database; + public final boolean await; + public final boolean authAwait; + + public TestSetup(CountDownLatch updateLatch, SplitRoomDatabase database, boolean await, boolean authAwait) { + this.updateLatch = updateLatch; + this.database = database; + this.await = await; + this.authAwait = authAwait; + } + } +} diff --git a/src/androidTest/java/tests/integration/largesegments/LargeSegmentsTest.java b/src/androidTest/java/tests/integration/largesegments/LargeSegmentsTest.java new file mode 100644 index 000000000..a2f286827 --- /dev/null +++ b/src/androidTest/java/tests/integration/largesegments/LargeSegmentsTest.java @@ -0,0 +1,168 @@ +package tests.integration.largesegments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import helper.DatabaseHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitFactory; +import io.split.android.client.SplitFactoryBuilder; +import io.split.android.client.dtos.SegmentsChange; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.exceptions.SplitInstantiationException; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; +import tests.integration.shared.TestingHelper; + +public class LargeSegmentsTest extends LargeSegmentTestHelper { + + @Test + public void sdkReadyIsEmittedAfterLargeSegmentsAreSynced() { + SplitFactory factory = getFactory(); + + SplitClient readyClient = getReadyClient(IntegrationHelper.dummyUserKey().matchingKey(), factory); + + assertEquals(1, mEndpointHits.get("splitChanges").get()); + assertEquals(1, mEndpointHits.get(IntegrationHelper.ServicePath.MEMBERSHIPS).get()); + } + + @Test + public void sdkReadyIsEmittedAfterLargeSegmentsAreSyncedWhenWaitForLargeSegmentsIsFalse() { + mMySegmentsDelay.set(4000); + SplitFactory factory = getFactory(); + getReadyClient(IntegrationHelper.dummyUserKey().matchingKey(), factory); + + + assertEquals(1, mEndpointHits.get("splitChanges").get()); + assertEquals(1, mEndpointHits.get(IntegrationHelper.ServicePath.MEMBERSHIPS).get()); + } + + @Test + public void sdkUpdateIsEmittedForLargeSegmentsWhenLargeSegmentsChange() throws InterruptedException { + mMySegmentsDelay.set(0L); + mRandomizeMyLargeSegments.set(true); + CountDownLatch updateLatch = new CountDownLatch(3); + CountDownLatch readyLatch = new CountDownLatch(1); + SplitFactory factory = getFactory(1, null, null); + + SplitClient client = factory.client(); + client.on(SplitEvent.SDK_READY, TestingHelper.testTask(readyLatch)); + client.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(updateLatch)); + + boolean readyAwait = readyLatch.await(5, TimeUnit.SECONDS); + boolean await = updateLatch.await(5, TimeUnit.SECONDS); + + assertTrue(readyAwait); + assertTrue(await); + } + + @Test + public void sdkReadyFromCacheIsEmittedAfterLargeSegmentsAreSynced() throws InterruptedException { + SplitRoomDatabase testDatabase = DatabaseHelper.getTestDatabase(mContext); + // first, prepopulate local cache + SplitFactory factory = getFactory(null, null, testDatabase); + SplitClient readyClient = getReadyClient(IntegrationHelper.dummyUserKey().matchingKey(), factory); + + String firstEval = readyClient.getTreatment("ls_split"); + + // make all api requests fail, we only want sdk_ready_from_cache + mBrokenApi.set(true); + + factory = getFactory(null, 1000, testDatabase); + CountDownLatch fromCacheLatch = new CountDownLatch(1); + CountDownLatch timeoutLatch = new CountDownLatch(1); + SplitClient client = factory.client(); + client.on(SplitEvent.SDK_READY_FROM_CACHE, TestingHelper.testTask(fromCacheLatch)); + client.on(SplitEvent.SDK_READY_TIMED_OUT, TestingHelper.testTask(timeoutLatch)); + + boolean fromCacheAwait = fromCacheLatch.await(5, TimeUnit.SECONDS); + boolean timeoutAwait = timeoutLatch.await(5, TimeUnit.SECONDS); + + String lsSplit = client.getTreatment("ls_split"); + + assertTrue(fromCacheAwait); + assertTrue(timeoutAwait); + assertEquals("on", firstEval); + assertEquals("on", lsSplit); + + } + + @Test + public void successfulSyncOfLargeSegmentsContainsSegmentsInDatabase() { + SplitRoomDatabase testDatabase = DatabaseHelper.getTestDatabase(mContext); + + SplitFactory factory = getFactory(null, null, testDatabase); + getReadyClient(IntegrationHelper.dummyUserKey().matchingKey(), factory); + + SegmentsChange largeSegments = Json.fromJson(testDatabase.myLargeSegmentDao() + .getByUserKey(IntegrationHelper.dummyUserKey().matchingKey()) + .getSegmentList(), SegmentsChange.class); + List mySegments = largeSegments.getNames(); + assertEquals(3, mySegments.size()); + assertTrue(mySegments.contains("large-segment1") && mySegments.contains("large-segment2") && mySegments.contains("large-segment3")); + assertEquals(1702507130121L, largeSegments.getChangeNumber().longValue()); + } + + @Test + public void syncOfLargeSegmentsForMultiClient() throws InterruptedException { + mRandomizeMyLargeSegments.set(true); + SplitRoomDatabase testDatabase = DatabaseHelper.getTestDatabase(mContext); + SplitFactory factory = getFactory(10, null, testDatabase); + + SplitClient client1 = factory.client(); + SplitClient client2 = factory.client("key2"); + + CountDownLatch latch = new CountDownLatch(2); + client1.on(SplitEvent.SDK_READY, TestingHelper.testTask(latch)); + client2.on(SplitEvent.SDK_READY, TestingHelper.testTask(latch)); + + latch.await(10, TimeUnit.SECONDS); + + SegmentsChange segmentList1 = Json.fromJson(testDatabase.myLargeSegmentDao().getByUserKey("CUSTOMER_ID").getSegmentList(), SegmentsChange.class); + SegmentsChange segmentList2 = Json.fromJson(testDatabase.myLargeSegmentDao().getByUserKey("key2").getSegmentList(), SegmentsChange.class); + + assertEquals(2, segmentList1.getNames().size()); + assertEquals(2, segmentList2.getNames().size()); + assertNotEquals(segmentList1, + segmentList2); + assertEquals(1702507130121L, segmentList1.getChangeNumber().longValue()); + assertEquals(1702507130121L, segmentList2.getChangeNumber().longValue()); + } + + @Test + public void emptyMyLargeSegmentsSdkIsReady() throws InterruptedException { + mMySegmentsDelay.set(0L); + mEmptyMyLargeSegments.set(true); + SplitFactory factory = getFactory(null, null, null); + SplitClient client = factory.client(); + CountDownLatch readyLatch = new CountDownLatch(1); + client.on(SplitEvent.SDK_READY, TestingHelper.testTask(readyLatch)); + boolean readyAwait = readyLatch.await(5, TimeUnit.SECONDS); + + assertEquals(1, mEndpointHits.get("splitChanges").get()); + assertEquals(1, mEndpointHits.get(IntegrationHelper.ServicePath.MEMBERSHIPS).get()); + assertTrue(readyAwait); + } + + @Test + public void localhostModeIsReadyWhenWaitForLargeSegmentsIsTrue() throws SplitInstantiationException, InterruptedException { + SplitFactory factory = SplitFactoryBuilder.build("localhost", IntegrationHelper.dummyUserKey(), + new TestableSplitConfigBuilder().build(), mContext); + + CountDownLatch readyLatch = new CountDownLatch(1); + factory.client().on(SplitEvent.SDK_READY, TestingHelper.testTask(readyLatch)); + + boolean await = readyLatch.await(5, TimeUnit.SECONDS); + + assertTrue(await); + } +} diff --git a/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java b/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java index d09de02e6..d0e63c4e8 100644 --- a/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java +++ b/src/androidTest/java/tests/integration/matcher/SemverMatcherTest.java @@ -147,7 +147,7 @@ private HttpResponseMockDispatcher getDispatcher() { } }); - responses.put("mySegments/test", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/test", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); responses.put("v2/auth", (uri, httpMethod, body) -> { mAuthLatch.countDown(); diff --git a/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java b/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java index e0d40995d..928d1d93a 100644 --- a/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java +++ b/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java @@ -128,7 +128,7 @@ private HttpResponseMockDispatcher getDispatcher() { } }); - responses.put("mySegments/test", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/test", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); responses.put("v2/auth", (uri, httpMethod, body) -> { mAuthLatch.countDown(); diff --git a/src/androidTest/java/tests/integration/pin/CertPinningTest.java b/src/androidTest/java/tests/integration/pin/CertPinningTest.java index 088167a40..16f6b45cc 100644 --- a/src/androidTest/java/tests/integration/pin/CertPinningTest.java +++ b/src/androidTest/java/tests/integration/pin/CertPinningTest.java @@ -79,11 +79,11 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio mEndpointHits.put("splitChanges", new AtomicInteger(1)); } return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.emptySplitChanges(1602796638344L, 1602796638344L)); - } else if (request.getRequestUrl().encodedPathSegments().contains("mySegments")) { - if (mEndpointHits.containsKey("mySegments")) { - mEndpointHits.get("mySegments").getAndIncrement(); + } else if (request.getRequestUrl().encodedPathSegments().contains(IntegrationHelper.ServicePath.MEMBERSHIPS)) { + if (mEndpointHits.containsKey(IntegrationHelper.ServicePath.MEMBERSHIPS)) { + mEndpointHits.get(IntegrationHelper.ServicePath.MEMBERSHIPS).getAndIncrement(); } else { - mEndpointHits.put("mySegments", new AtomicInteger(1)); + mEndpointHits.put(IntegrationHelper.ServicePath.MEMBERSHIPS, new AtomicInteger(1)); } return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.emptyMySegments()); } else { diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java index 51fe77397..86f42034f 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java @@ -136,7 +136,7 @@ private SplitClient getClient( } }); - responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); HttpResponseMockDispatcher httpResponseMockDispatcher = IntegrationHelper.buildDispatcher(responses); diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java index 932c505b9..059d7c97d 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java @@ -142,7 +142,7 @@ private HttpResponseMockDispatcher getDispatcher(int setsCount) { } }); - responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); return IntegrationHelper.buildDispatcher(responses); } diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java index f9bedbf44..6faaab513 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java @@ -211,7 +211,7 @@ private SplitFactory createFactory( } }); - responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); HttpResponseMockDispatcher httpResponseMockDispatcher = IntegrationHelper.buildDispatcher(responses); diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java index 20d3afa06..50f478d44 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsStreamingTest.java @@ -227,7 +227,7 @@ private SplitClient getReadyClient( mSplitChangesHits.incrementAndGet(); return new HttpResponseMock(200, IntegrationHelper.emptySplitChanges(-1, 1)); }); - responses.put("mySegments/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); responses.put("v2/auth", (uri, httpMethod, body) -> { authLatch.countDown(); return new HttpResponseMock(200, IntegrationHelper.streamingEnabledToken()); diff --git a/src/androidTest/java/tests/integration/shared/InterdependentSplitsTest.java b/src/androidTest/java/tests/integration/shared/InterdependentSplitsTest.java index 56a7b8662..65167f866 100644 --- a/src/androidTest/java/tests/integration/shared/InterdependentSplitsTest.java +++ b/src/androidTest/java/tests/integration/shared/InterdependentSplitsTest.java @@ -9,7 +9,6 @@ import java.util.concurrent.TimeUnit; import helper.IntegrationHelper; -import helper.TestingHelper; import io.split.android.client.SplitClient; import io.split.android.client.api.Key; import io.split.android.client.events.SplitEvent; @@ -29,16 +28,16 @@ protected Dispatcher getDispatcher() { return new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("/mySegments/key1")) { + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS + "/key1")) { return new MockResponse() .setResponseCode(200) - .setBody("{\"mySegments\":[{ \"id\":\"id0\", \"name\":\"android_test\"}]}"); - } else if (request.getPath().contains("/mySegments/key2")) { + .setBody(IntegrationHelper.dummySingleSegment("android_test")); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS + "/key2")) { return new MockResponse() .setResponseCode(200) .setBody(IntegrationHelper.emptyMySegments()); } else if (request.getPath().contains("/splitChanges")) { - String splitChange = "{\"splits\":[{\"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\"}]},{\"trafficTypeName\":\"account\",\"name\":\"android_test_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-397942789,\"seed\":1852089605,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733496087,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"android_test\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in segment android_test\"},{\"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\"}]}],\"since\":-1,\"till\":1648733409158}"; + String splitChange = "{\"splits\":[{\"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\"}]},{\"trafficTypeName\":\"account\",\"name\":\"android_test_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-397942789,\"seed\":1852089605,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733496087,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"android_test\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in segment android_test\"},{\"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\"}]}],\"since\":1648733409158,\"till\":1648733409158}"; return new MockResponse().setResponseCode(200) .setBody(splitChange); diff --git a/src/androidTest/java/tests/integration/shared/MySegmentsBeforeSplitsTest.java b/src/androidTest/java/tests/integration/shared/MySegmentsBeforeSplitsTest.java index c78ea56f1..7b5423f8d 100644 --- a/src/androidTest/java/tests/integration/shared/MySegmentsBeforeSplitsTest.java +++ b/src/androidTest/java/tests/integration/shared/MySegmentsBeforeSplitsTest.java @@ -8,7 +8,6 @@ import java.util.concurrent.TimeUnit; import helper.IntegrationHelper; -import helper.TestingHelper; import io.split.android.client.SplitClient; import io.split.android.client.api.Key; import io.split.android.client.events.SplitEvent; @@ -23,16 +22,16 @@ public Dispatcher getDispatcher() { return new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("/mySegments/key1")) { + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS + "/key1")) { return new MockResponse() .setResponseCode(200) - .setBody("{\"mySegments\":[{ \"id\":\"id0\", \"name\":\"android_test\"}]}"); - } else if (request.getPath().contains("/mySegments/key2")) { + .setBody(IntegrationHelper.dummySingleSegment("android_test")); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS + "/key2")) { return new MockResponse() .setResponseCode(200) .setBody(IntegrationHelper.emptyMySegments()); } else if (request.getPath().contains("/splitChanges")) { - String splitChange = "{\"splits\":[{\"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\"}]},{\"trafficTypeName\":\"account\",\"name\":\"android_test_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-397942789,\"seed\":1852089605,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733496087,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"android_test\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in segment android_test\"}]}],\"since\":-1,\"till\":1648733409158}"; + String splitChange = "{\"splits\":[{\"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\"}]},{\"trafficTypeName\":\"account\",\"name\":\"android_test_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-397942789,\"seed\":1852089605,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733496087,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"android_test\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in segment android_test\"}]}],\"since\":1648733409158,\"till\":1648733409158}"; new CountDownLatch(1).await(500, TimeUnit.MILLISECONDS); return new MockResponse().setResponseCode(200) .setBody(splitChange); diff --git a/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java b/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java index 034311c4b..11d8af38e 100644 --- a/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java +++ b/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java @@ -39,7 +39,6 @@ import io.split.android.client.storage.db.SplitEntity; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.utils.Json; -import io.split.android.client.utils.logger.Logger; import io.split.android.client.utils.logger.SplitLogLevel; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; @@ -292,14 +291,14 @@ private void setupServer() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().contains("/mySegments/key1")) { + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS + "/key1")) { return new MockResponse() .setResponseCode(200) - .setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); - } else if (request.getPath().contains("/mySegments/key2")) { + .setBody(IntegrationHelper.dummyAllSegments()); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS + "/key2")) { return new MockResponse() .setResponseCode(200) - .setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment2\"}]}"); + .setBody(IntegrationHelper.dummySingleSegment("segment2")); } else if (request.getPath().contains("/splitChanges")) { return new MockResponse().setResponseCode(200) .setBody(splitsPerRequest()); diff --git a/src/androidTest/java/tests/integration/shared/TestingData.java b/src/androidTest/java/tests/integration/shared/TestingData.java index eab9b6852..2a096a950 100644 --- a/src/androidTest/java/tests/integration/shared/TestingData.java +++ b/src/androidTest/java/tests/integration/shared/TestingData.java @@ -1,26 +1,23 @@ package tests.integration.shared; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeV2Notification; -import io.split.android.client.utils.Json; - public class TestingData { public final static String UNBOUNDED_NOTIFICATION = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + "\\\"u\\\": 0," + "\\\"c\\\": 0," + "\\\"d\\\": \\\"\\\"," + - "\\\"segmentName\\\": \\\"pepe\\\"," + - "\\\"changeNumber\\\": 28" + + "\\\"n\\\": [\\\"pepe\\\"]," + + "\\\"cn\\\": 28" + "}"; public final static String SEGMENT_REMOVAL_NOTIFICATION = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + "\\\"u\\\": 3," + "\\\"c\\\": 0," + "\\\"d\\\": \\\"\\\"," + - "\\\"segmentName\\\": \\\"segment1\\\"," + - "\\\"changeNumber\\\": 28" + + "\\\"n\\\": [\\\"segment1\\\"]," + + "\\\"cn\\\": 28" + "}"; /** @@ -39,28 +36,28 @@ public class TestingData { 8492584437244343049 11382796718859679607 11383137936375052427 17699699514337596928 17001541343685934583 8355202062888946034] */ public final static String BOUNDED_NOTIFICATION_GZIP = "{" + - "\"type\": \"MY_SEGMENTS_UPDATE_V2\"," + + "\"type\": \"MEMBERSHIPS_MS_UPDATE\"," + "\"u\": 1," + "\"c\": 1," + "\"d\": \"H4sIAAAAAAAA/2IYBfgAx0A7YBTgB4wD7YABAAID7QC6g5EYy8MEMA20A+gMFAbaAYMZDPXqlGWgHTAKRsEoGAWjgCzQQFjJkKqiiPAPAQAIAAD//5L7VQwAEAAA\"" + "}"; public final static String ESCAPED_BOUNDED_NOTIFICATION_GZIP = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + "\\\"u\\\": 1," + "\\\"c\\\": 1," + "\\\"d\\\": \\\"H4sIAAAAAAAA/2IYBfgAx0A7YBTgB4wD7YABAAID7QC6g5EYy8MEMA20A+gMFAbaAYMZDPXqlGWgHTAKRsEoGAWjgCzQQFjJkKqiiPAPAQAIAAD//5L7VQwAEAAA\\\"" + "}"; public final static String BOUNDED_NOTIFICATION_ZLIB = "{" + - "\"type\": \"MY_SEGMENTS_UPDATE_V2\"," + + "\"type\": \"MEMBERSHIPS_MS_UPDATE\"," + "\"u\": 1," + "\"c\": 2," + "\"d\": \"eJxiGAX4AMdAO2AU4AeMA+2AAQACA+0AuoORGMvDBDANtAPoDBQG2gGDGQz16pRloB0wCkbBKBgFo4As0EBYyZCqoojwDwEACAAA//+W/QFR\"" + "}"; public final static String ESCAPED_BOUNDED_NOTIFICATION_ZLIB = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + "\\\"u\\\": 1," + "\\\"c\\\": 2," + "\\\"d\\\": \\\"eJxiGAX4AMdAO2AU4AeMA+2AAQACA+0AuoORGMvDBDANtAPoDBQG2gGDGQz16pRloB0wCkbBKBgFo4As0EBYyZCqoojwDwEACAAA//+W/QFR\\\"" + @@ -68,7 +65,7 @@ public class TestingData { public final static String ESCAPED_BOUNDED_NOTIFICATION_MALFORMED = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + "\\\"u\\\": 1," + "\\\"c\\\": 1," + "\\\"d\\\": \\\"H4sIAAAAAAAAg5EYy8MEMA20A+//5L7VQwAEAAA\\\"" + @@ -80,49 +77,75 @@ public class TestingData { * = a: [key1, key2] , r: [key3, key4] */ public final static String KEY_LIST_NOTIFICATION_GZIP = "{" + - "\"type\": \"MY_SEGMENTS_UPDATE_V2\"," + + "\"type\": \"MEMBERSHIPS_MS_UPDATE\"," + "\"u\": 2," + "\"c\": 1," + "\"d\": \"H4sIAAAAAAAA/wTAsRHDUAgD0F2ofwEIkPAqPhdZIW0uu/v97GPXHU004ULuMGrYR6XUbIjlXULPPse+dt1yhJibBODjrTmj3GJ4emduuDDP/w0AAP//18WLsl0AAAA=\"" + "}"; public final static String ESCAPED_KEY_LIST_NOTIFICATION_GZIP = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + - "\\\"segmentName\\\": \\\"new_segment_added\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + + "\\\"n\\\": [\\\"new_segment_added\\\"]," + "\\\"u\\\": 2," + "\\\"c\\\": 1," + "\\\"d\\\": \\\"H4sIAAAAAAAA/wTAsRHDUAgD0F2ofwEIkPAqPhdZIW0uu/v97GPXHU004ULuMGrYR6XUbIjlXULPPse+dt1yhJibBODjrTmj3GJ4emduuDDP/w0AAP//18WLsl0AAAA=\\\"" + "}"; public final static String BOUNDED_NOTIFICATION_ZLIB_2 = "{" + - "\"changeNumber\": 1629754722111, " + - "\"type\": \"MY_SEGMENTS_UPDATE_V2\"," + + "\"cn\": 1629754722111, " + + "\"type\": \"MEMBERSHIPS_MS_UPDATE\"," + "\"u\": 1," + "\"c\": 2," + "\"d\": \"eJzMVk3OhDAIVdNFl9/22zVzEo8yR5mjT6LGsRTKg2LiW8yPUnjQB+2kIwM2ThTIKtVU1oknFcRzufz+YGYM/phnHW8sdPvs9EzXW2I+HFzhNyTNgCD/PpW9xpGiHD0Bw1U5HLSS644FbGZgoPovmjpmX5wAzhIxJyN7IAnFQWX1htj+LUl6ZQRV3umMqYG1LCrOJGLPV8+IidBQZFt6sOUA6CqsX5iEFY2gqufs2mfqRtsVWytRnO+iYMN7xIBqJhDqAydV+HidkGOGEJYvk4fhe/8iIukphG/XfFcfVxnMVcALCOF77qL/EU7ODepxlLST6qxFLYRdOyW8EBY4BqVjObnm3V5ZMkZIKf++8+hM7zM1Kd3aFqVZeSHzDQAA//+QUQ3a\"" + "}"; // c: 2 -// changeNumber: 1629754722111 +// cn: 1629754722111 // d: "eJzMVk3OhDAIVdNFl9/22zVzEo8yR5mjT6LGsRTKg2LiW8yPUnjQB+2kIwM2ThTIKtVU1oknFcRzufz+YGYM/phnHW8sdPvs9EzXW2I+HFzhNyTNgCD/PpW9xpGiHD0Bw1U5HLSS644FbGZgoPovmjpmX5wAzhIxJyN7IAnFQWX1htj+LUl6ZQRV3umMqYG1LCrOJGLPV8+IidBQZFt6sOUA6CqsX5iEFY2gqufs2mfqRtsVWytRnO+iYMN7xIBqJhDqAydV+HidkGOGEJYvk4fhe/8iIukphG/XfFcfVxnMVcALCOF77qL/EU7ODepxlLST6qxFLYRdOyW8EBY4BqVjObnm3V5ZMkZIKf++8+hM7zM1Kd3aFqVZeSHzDQAA//+QUQ3a" -// segmentName: "" -// type: "MY_SEGMENTS_UPDATE_V2" +// n: "" +// type: "MEMBERSHIPS_MS_UPDATE" // u: 1 public final static String DECOMPRESSED_KEY_LIST_PAYLOAD_GZIP = "{\"a\":[1573573083296714675,8482869187405483569],\"r\":[8031872927333060586,6829471020522910836]}"; - public static String encodedKeyListPayloadGzip() { - return (Json.fromJson(KEY_LIST_NOTIFICATION_GZIP, MySegmentChangeV2Notification.class)).getData(); - } - - public static String encodedBoundedPayloadZlib() { - return (Json.fromJson(BOUNDED_NOTIFICATION_ZLIB, MySegmentChangeV2Notification.class)).getData(); + public static String segmentsUnboundedNoCompression(String intervalMs) { + return "{" + + "\\\"type\\\":\\\"MEMBERSHIPS_MS_UPDATE\\\"" + + ",\\\"cn\\\":1702507130121," + + "\\\"n\\\":[\\\"android_test\\\",\\\"ios_test\\\"]," + + "\\\"c\\\":0," + + "\\\"u\\\":0," + + "\\\"d\\\":\\\"\\\"," + + "\\\"i\\\":" + intervalMs + "," + + "\\\"h\\\":0," + + "\\\"s\\\":0" + + "}"; } - public static String encodedBoundedPayloadZlib2() { - return (Json.fromJson(BOUNDED_NOTIFICATION_ZLIB_2, MySegmentChangeV2Notification.class)).getData(); + public static String largeSegmentsUnboundedNoCompression(String intervalMs) { + return "{" + + "\\\"type\\\":\\\"MEMBERSHIPS_LS_UPDATE\\\"" + + ",\\\"cn\\\":1702507130121," + + "\\\"n\\\":[\\\"android_test\\\",\\\"ios_test\\\"]," + + "\\\"c\\\":0," + + "\\\"u\\\":0," + + "\\\"d\\\":\\\"\\\"," + + "\\\"i\\\":" + intervalMs + "," + + "\\\"h\\\":0," + + "\\\"s\\\":0" + + "}"; } - public static String encodedBoundedPayloadGzip() { - return (Json.fromJson(BOUNDED_NOTIFICATION_GZIP, MySegmentChangeV2Notification.class)).getData(); + public static String largeSegmentsRemoval() { + return "{" + + "\\\"type\\\":\\\"MEMBERSHIPS_LS_UPDATE\\\"" + + ",\\\"cn\\\":1702507130121," + + "\\\"n\\\":[\\\"large-segment1\\\",\\\"large-segment2\\\"]," + + "\\\"c\\\":0," + + "\\\"u\\\":3," + + "\\\"d\\\":\\\"\\\"," + + "\\\"i\\\":100," + + "\\\"h\\\":0," + + "\\\"s\\\":0" + + "}"; } } diff --git a/src/androidTest/java/tests/integration/streaming/AblyErrorBaseTest.java b/src/androidTest/java/tests/integration/streaming/AblyErrorBaseTest.java index e9cd41a9b..407c612e3 100644 --- a/src/androidTest/java/tests/integration/streaming/AblyErrorBaseTest.java +++ b/src/androidTest/java/tests/integration/streaming/AblyErrorBaseTest.java @@ -120,10 +120,10 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { mMySegmentsSyncLatch.countDown(); - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { mSplitsSyncLatch.countDown(); String data = IntegrationHelper.emptySplitChanges(1000, 1000); diff --git a/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java b/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java index 244d85f67..c3556e838 100644 --- a/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java +++ b/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java @@ -244,8 +244,8 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { - return createResponse(200, IntegrationHelper.dummyMySegments()); + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { String data = IntegrationHelper.emptySplitChanges(-1, 1000); return createResponse(200, data); @@ -284,11 +284,4 @@ private void pushMessage(String fileName) { } catch (InterruptedException e) { } } - - private String updatedMySegments() { - return "{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, " + - " { \"id\":\"id1\", \"name\":\"segment2\"}, " + - "{ \"id\":\"id3\", \"name\":\"segment3\"}]}"; - } - } diff --git a/src/androidTest/java/tests/integration/streaming/ControlTest.java b/src/androidTest/java/tests/integration/streaming/ControlTest.java index 200fb5138..a2eb0cb8e 100644 --- a/src/androidTest/java/tests/integration/streaming/ControlTest.java +++ b/src/androidTest/java/tests/integration/streaming/ControlTest.java @@ -1,7 +1,7 @@ package tests.integration.streaming; import static junit.framework.Assert.assertTrue; -import static junit.framework.TestCase.assertEquals; +import static junit.framework.Assert.fail; import static java.lang.Thread.sleep; import android.content.Context; @@ -9,7 +9,6 @@ import androidx.core.util.Pair; import androidx.test.platform.app.InstrumentationRegistry; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -20,10 +19,12 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import fake.HttpClientMock; import fake.HttpResponseMock; import fake.HttpResponseMockDispatcher; +import fake.HttpStreamResponseMock; import fake.SynchronizerSpyImpl; import helper.DatabaseHelper; import helper.FileHelper; @@ -43,7 +44,7 @@ import io.split.android.client.telemetry.storage.TelemetryStorage; import io.split.android.client.utils.Json; import io.split.android.client.utils.logger.Logger; -import fake.HttpStreamResponseMock; +import tests.integration.shared.TestingData; import tests.integration.shared.TestingHelper; public class ControlTest { @@ -66,6 +67,8 @@ public class ControlTest { private int mSseConnectionCount; String mSplitChange; + private final AtomicReference mCurrentResponse = new AtomicReference<>(); + private CountDownLatch mSplitChangesLatch = null; @Before public void setup() { @@ -80,6 +83,7 @@ public void setup() { mApiKey = apiKeyAndDb.first; mUserKey = IntegrationHelper.dummyUserKey(); + mCurrentResponse.set(IntegrationHelper.emptyAllSegments()); } @Test @@ -90,9 +94,8 @@ public void controlNotification() throws IOException, InterruptedException { SplitRoomDatabase db = DatabaseHelper.getTestDatabase(mContext); db.clearAllTables(); - CountDownLatch readyLatch = new CountDownLatch(1); - CountDownLatch updateLatch = new CountDownLatch(3); + CountDownLatch updateLatch = new CountDownLatch(1); SplitEventTaskHelper updateTask = new SplitEventTaskHelper(updateLatch); HttpClientMock httpClientMock = new HttpClientMock(createBasicResponseDispatcher()); @@ -104,7 +107,6 @@ public void controlNotification() throws IOException, InterruptedException { mApiKey, mUserKey, config, mContext, httpClientMock, db, synchronizerSpy, null, null, telemetryStorage); -// mClient = mFactory.client(); SplitClient mClient = mFactory.client(); String splitName = "workm"; @@ -120,31 +122,36 @@ public void controlNotification() throws IOException, InterruptedException { String treatmentReady = mClient.getTreatment(splitName); // Pause streaming - synchronizerSpy.startPeriodicFetchLatch = new CountDownLatch(1); pushControl("STREAMING_PAUSED"); - synchronizerSpy.startPeriodicFetchLatch.await(10, TimeUnit.SECONDS); - - pushMySegmentsUpdatePayload("new_segment"); - sleep(1000); + pushMySegmentsUpdatePayload(); String treatmentPaused = mClient.getTreatment(splitName); // Enable streaming, push a new my segments payload update and check data again - synchronizerSpy.stopPeriodicFetchLatch = new CountDownLatch(1); + mSplitChangesLatch = new CountDownLatch(1); pushControl("STREAMING_RESUMED"); - synchronizerSpy.stopPeriodicFetchLatch.await(10, TimeUnit.SECONDS); - pushMySegmentsUpdatePayload("new_segment"); - updateLatch.await(10, TimeUnit.SECONDS); + boolean await1 = mSplitChangesLatch.await(10, TimeUnit.SECONDS); + if (!await1) { + fail("Split changes latch count is " + mSplitChangesLatch.getCount()); + } + + mClient.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(updateLatch)); + mCurrentResponse.set(IntegrationHelper.dummySingleSegment("new_segment")); + pushMySegmentsUpdatePayload(); + boolean await = updateLatch.await(10, TimeUnit.SECONDS); + if (!await) { + fail("Update latch count is " + updateLatch.getCount()); + } String treatmentEnabled = mClient.getTreatment(splitName); //Enable streaming, push a new my segments payload update and check data again - updateLatch = new CountDownLatch(1); + mCurrentResponse.set(IntegrationHelper.emptyAllSegments()); pushControl("STREAMING_DISABLED"); - updateLatch.await(5, TimeUnit.SECONDS); - pushMySegmentsUpdatePayload("new_segment"); - updateLatch.await(5, TimeUnit.SECONDS); + + pushMySegmentsUpdatePayload(); + String treatmentDisabled = mClient.getTreatment(splitName); assertTrue(telemetryStorage.popStreamingEvents().stream().anyMatch(event -> { @@ -203,19 +210,12 @@ public void streamResetControlNotification() throws IOException, InterruptedExce mClient.destroy(); } - private void pushMySegmentsUpdatePayload(String segmentName) throws IOException, InterruptedException { + private void pushMySegmentsUpdatePayload() throws InterruptedException { mPushLatch = new CountDownLatch(1); - pushMySegmentMessage(segmentName); + pushMySegmentMessage(); mPushLatch.await(10, TimeUnit.SECONDS); } - @After - public void tearDown() { -// if (mFactory != null) { -// mFactory.destroy(); -// } - } - private HttpResponseMock createResponse(int status, String data) { return new HttpResponseMock(status, data); } @@ -233,11 +233,15 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("** My segments hit"); - return createResponse(200, IntegrationHelper.emptyMySegments()); - } else if (uri.getPath().contains("/splitChanges")) { + return createResponse(200, mCurrentResponse.get()); + + } else if (uri.getPath().contains("/splitChanges")) { + if (mSplitChangesLatch != null) { + mSplitChangesLatch.countDown(); + } Logger.i("** Split Changes hit"); return createResponse(200, mSplitChange); } else if (uri.getPath().contains("/auth")) { @@ -275,28 +279,15 @@ private String loadSplitChanges() { return Json.toJson(change); } - private void pushMySegmentMessage(String segmentName) { - String message = loadMockedData(MSG_SEGMENT_UPDATE_PAYLOAD); - message = message.replace("[SEGMENT_NAME]", segmentName); - mTimestamp += 100; - message = message.replace(CONTROL_TIMESTAMP_PLACEHOLDER, String.valueOf(mTimestamp)); + private void pushMySegmentMessage() { + String msg = TestingData.segmentsUnboundedNoCompression("1"); + String MSG_SEGMENT_UPDATE_TEMPLATE = "push_msg-largesegment_update.txt"; + BlockingQueue queue = mStreamingData; + String message = loadMockedData(MSG_SEGMENT_UPDATE_TEMPLATE); + message = message.replace("$TIMESTAMP$", String.valueOf(System.currentTimeMillis())); + message = message.replace(TestingHelper.MSG_DATA_FIELD, msg); try { - mStreamingData.put(message + "" + "\n"); - sleep(500); - mPushLatch.countDown(); - Logger.d("Pushed message: " + message); - } catch (InterruptedException e) { - } - } - - private void pushMessage(String fileName) { - String message = loadMockedData(fileName); - mTimestamp += 100; - message = message.replace(CONTROL_TIMESTAMP_PLACEHOLDER, String.valueOf(mTimestamp)); - try { - mStreamingData.put(message + "" + "\n"); - sleep(200); - mPushLatch.countDown(); + queue.put(message + "" + "\n"); Logger.d("Pushed message: " + message); } catch (InterruptedException e) { } @@ -310,6 +301,7 @@ private void pushControl(String controlType) { try { mStreamingData.put(message + "" + "\n"); + Logger.e("Pushed control: " + controlType); Thread.sleep(500); Logger.d("Pushed message: " + message); } catch (InterruptedException ignored) { diff --git a/src/androidTest/java/tests/integration/streaming/ImpressionsCountTest.java b/src/androidTest/java/tests/integration/streaming/ImpressionsCountTest.java index 1b32c3ec0..d816ee747 100644 --- a/src/androidTest/java/tests/integration/streaming/ImpressionsCountTest.java +++ b/src/androidTest/java/tests/integration/streaming/ImpressionsCountTest.java @@ -195,8 +195,8 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { boolean first = true; @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { - return createResponse(200, IntegrationHelper.dummyMySegments()); + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { if (first) { first = false; diff --git a/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2MultiClientTest.java b/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2MultiClientTest.java index 6790672fe..f12b98ca6 100644 --- a/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2MultiClientTest.java +++ b/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2MultiClientTest.java @@ -34,12 +34,14 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; import io.split.android.client.api.Key; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.network.HttpMethod; 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.Json; import io.split.android.client.utils.logger.Logger; import tests.integration.shared.TestingData; import tests.integration.shared.TestingHelper; @@ -157,21 +159,19 @@ mApiKey, new Key(userKey), pushMessage(TestingData.ESCAPED_KEY_LIST_NOTIFICATION_GZIP); l1.await(5, TimeUnit.SECONDS); - MySegmentEntity e = mySegmentsDao.getByUserKey(userKey); - MySegmentEntity e1 = mySegmentsDao.getByUserKey(userKey2); - l1 = new CountDownLatch(1); updateTask.setLatch(l1); pushMessage(TestingData.SEGMENT_REMOVAL_NOTIFICATION); l1.await(5, TimeUnit.SECONDS); - MySegmentEntity mySegmentEntity = getByKey(userKey, mDb); - MySegmentEntity mySegmentEntity2 = getByKey(userKey2, mDb); - Assert.assertTrue(mySegmentEntity.getSegmentList().contains("new_segment_added")); - Assert.assertFalse(mySegmentEntity.getSegmentList().contains("segment1")); + SegmentsChange mySegmentEntity = Json.fromJson(getByKey(userKey, mDb).getSegmentList(), SegmentsChange.class); + SegmentsChange mySegmentEntity2 = Json.fromJson(getByKey(userKey2, mDb).getSegmentList(), SegmentsChange.class); + Assert.assertTrue(mySegmentEntity.getNames().contains("new_segment_added")); + Assert.assertFalse(mySegmentEntity.getNames().contains("segment1")); + Assert.assertEquals(1, mySegmentEntity2.getSegments().size()); + Assert.assertEquals("new_segment_added", mySegmentEntity2.getNames().get(0)); Assert.assertEquals(4, mTelemetryStorage.popUpdatesFromSSE().getMySegments()); - Assert.assertEquals("new_segment_added", mySegmentEntity2.getSegmentList()); } @After @@ -191,11 +191,11 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher(int number) { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments/key2")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "key2")) { mMySegmentsSyncLatch2.countDown(); mMySegmentsUpdateLatch2.countDown(); - return createResponse(200, IntegrationHelper.emptyMySegments()); - } else if (uri.getPath().contains("/mySegments")) { + return createResponse(200, IntegrationHelper.emptyAllSegments()); + } else if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { mMySegmentsHitCount++; Logger.i("** My segments hit: " + mMySegmentsHitCount); mMySegmentsSyncLatch.countDown(); diff --git a/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2Test.java b/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2Test.java index 374ec0e81..9a13d5921 100644 --- a/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2Test.java +++ b/src/androidTest/java/tests/integration/streaming/MySegmentsChangeV2Test.java @@ -150,11 +150,11 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher(int number) { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments/key2")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/key2")) { mMySegmentsSyncLatch2.countDown(); mMySegmentsUpdateLatch2.countDown(); - return createResponse(200, IntegrationHelper.emptyMySegments()); - } else if (uri.getPath().contains("/mySegments")) { + return createResponse(200, IntegrationHelper.emptyAllSegments()); + } else if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { mMySegmentsHitCount++; Logger.i("** My segments hit: " + mMySegmentsHitCount); mMySegmentsSyncLatch.countDown(); diff --git a/src/androidTest/java/tests/integration/streaming/MySegmentsSyncProcessTest.java b/src/androidTest/java/tests/integration/streaming/MySegmentsSyncProcessTest.java index a6bfecd75..63af0388c 100644 --- a/src/androidTest/java/tests/integration/streaming/MySegmentsSyncProcessTest.java +++ b/src/androidTest/java/tests/integration/streaming/MySegmentsSyncProcessTest.java @@ -12,10 +12,12 @@ import java.io.IOException; import java.net.URI; +import java.util.Arrays; 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.AtomicReference; import fake.HttpClientMock; import fake.HttpResponseMock; @@ -28,12 +30,16 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; import io.split.android.client.api.Key; +import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; 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.utils.Json; import io.split.android.client.utils.logger.Logger; +import tests.integration.shared.TestingData; import tests.integration.shared.TestingHelper; public class MySegmentsSyncProcessTest { @@ -47,16 +53,13 @@ public class MySegmentsSyncProcessTest { private CountDownLatch mMySegmentsUpdateLatch; private CountDownLatch mMySegmentsPushLatch; - private final static String MSG_SEGMENT_UPDATE = "push_msg-segment_update.txt"; - private final static String MSG_SEGMENT_UPDATE_PAYLOAD = "push_msg-segment_update_payload.txt"; - private final static String MSG_SEGMENT_UPDATE_EMPTY_PAYLOAD = "push_msg-segment_update_empty_payload.txt"; - private int mMySegmentsHitCount = 0; SplitFactory mFactory; SplitClient mClient; SplitRoomDatabase mSplitRoomDatabase; + private final AtomicReference mCurrentUpdate = new AtomicReference<>(IntegrationHelper.dummyAllSegments()); @Before public void setup() { @@ -108,18 +111,24 @@ public void mySegmentsUpdate() throws IOException, InterruptedException { MySegmentEntity mySegmentEntity = mSplitRoomDatabase.mySegmentDao().getByUserKey(mUserKey.matchingKey()); mClient.on(SplitEvent.SDK_UPDATE, secondUpdateTask); - testMySegmentsPush(MSG_SEGMENT_UPDATE_PAYLOAD); + // MSG_SEGMENT_UPDATE_PAYLOAD //only segment1 + mCurrentUpdate.set(segment1Update()); + testMySegmentsPush(TestingData.largeSegmentsUnboundedNoCompression("1")); secondUpdateLatch.await(5, TimeUnit.SECONDS); MySegmentEntity mySegmentEntityPayload = mSplitRoomDatabase.mySegmentDao().getByUserKey(mUserKey.matchingKey()); mClient.on(SplitEvent.SDK_UPDATE, thirdUpdateTask); - testMySegmentsPush(MSG_SEGMENT_UPDATE_EMPTY_PAYLOAD); + // MSG_SEGMENT_UPDATE_EMPTY_PAYLOAD // empty + mCurrentUpdate.set(IntegrationHelper.emptyAllSegments()); + testMySegmentsPush(TestingData.largeSegmentsUnboundedNoCompression("1")); thirdUpdateLatch.await(5, TimeUnit.SECONDS); MySegmentEntity mySegmentEntityEmptyPayload = mSplitRoomDatabase.mySegmentDao().getByUserKey(mUserKey.matchingKey()); - Assert.assertEquals("segment1,segment2,segment3", mySegmentEntity.getSegmentList()); - Assert.assertEquals("segment1", mySegmentEntityPayload.getSegmentList()); - Assert.assertEquals("", mySegmentEntityEmptyPayload.getSegmentList()); + Assert.assertTrue(mySegmentEntity.getSegmentList().contains("segment1") && mySegmentEntity.getSegmentList().contains("segment2") && mySegmentEntity.getSegmentList().contains("segment3")); + String body = mySegmentEntityPayload.getSegmentList(); + SegmentsChange segmentsChange = Json.fromJson(body, SegmentsChange.class); + Assert.assertEquals(Arrays.asList("segment1"), segmentsChange.getNames()); + Assert.assertEquals("{\"cn\":null,\"k\":[]}", mySegmentEntityEmptyPayload.getSegmentList()); } @Test @@ -177,28 +186,30 @@ public void onPostExecutionView(SplitClient client) { MySegmentEntity client1SegmentEntity = mSplitRoomDatabase.mySegmentDao().getByUserKey(mUserKey.matchingKey()); MySegmentEntity client2SegmentEntity = mSplitRoomDatabase.mySegmentDao().getByUserKey("key2"); - testMySegmentsPush(MSG_SEGMENT_UPDATE_PAYLOAD); + mCurrentUpdate.set(segment1Update()); + testMySegmentsPush(TestingData.largeSegmentsUnboundedNoCompression("1")); client1UpdateLatch.await(5, TimeUnit.SECONDS); MySegmentEntity client1SegmentEntityPayload = mSplitRoomDatabase.mySegmentDao().getByUserKey(mUserKey.matchingKey()); MySegmentEntity client2SegmentEntityPayload = mSplitRoomDatabase.mySegmentDao().getByUserKey("key2"); - testMySegmentsPush(MSG_SEGMENT_UPDATE_EMPTY_PAYLOAD); + mCurrentUpdate.set(IntegrationHelper.emptyAllSegments()); + testMySegmentsPush(TestingData.largeSegmentsUnboundedNoCompression("1")); client1UpdateLatch.await(5, TimeUnit.SECONDS); MySegmentEntity client1SegmentEntityEmptyPayload = mSplitRoomDatabase.mySegmentDao().getByUserKey(mUserKey.matchingKey()); MySegmentEntity client2SegmentEntityEmptyPayload = mSplitRoomDatabase.mySegmentDao().getByUserKey("key2"); - Assert.assertEquals("segment1,segment2,segment3", client1SegmentEntity.getSegmentList()); - Assert.assertEquals("segment1", client1SegmentEntityPayload.getSegmentList()); - Assert.assertEquals("", client1SegmentEntityEmptyPayload.getSegmentList()); + Assert.assertTrue(client1SegmentEntity.getSegmentList().contains("segment1") && client1SegmentEntity.getSegmentList().contains("segment2") && client1SegmentEntity.getSegmentList().contains("segment3")); + Assert.assertEquals("{\"cn\":null,\"k\":[{\"n\":\"segment1\"}]}", client1SegmentEntityPayload.getSegmentList()); + Assert.assertEquals("{\"cn\":null,\"k\":[]}", client1SegmentEntityEmptyPayload.getSegmentList()); - Assert.assertEquals("", client2SegmentEntity.getSegmentList()); - Assert.assertEquals("", client2SegmentEntityPayload.getSegmentList()); - Assert.assertEquals("", client2SegmentEntityEmptyPayload.getSegmentList()); + Assert.assertEquals("{\"cn\":null,\"k\":[]}", client2SegmentEntity.getSegmentList()); + Assert.assertEquals("{\"cn\":null,\"k\":[]}", client2SegmentEntityPayload.getSegmentList()); + Assert.assertEquals("{\"cn\":null,\"k\":[]}", client2SegmentEntityEmptyPayload.getSegmentList()); } private void testMySegmentsUpdate() throws InterruptedException { mMySegmentsUpdateLatch = new CountDownLatch(1); - pushMessage(MSG_SEGMENT_UPDATE); + pushMessage(TestingData.largeSegmentsUnboundedNoCompression("1")); boolean await = mMySegmentsUpdateLatch.await(30, TimeUnit.SECONDS); if (!await) { Assert.fail("MySegments update not received"); @@ -234,21 +245,21 @@ public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { try { Thread.sleep(800); if (uri.getPath().contains("auth")) { - return createResponse(200, IntegrationHelper.streamingEnabledV1Token()); - } else if (uri.getPath().contains("/mySegments/key1")) { + return createResponse(200, IntegrationHelper.streamingEnabledToken()); + } else if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS + "/key1")) { mMySegmentsHitCount++; Logger.i("** My segments hit: " + mMySegmentsHitCount); mMySegmentsSyncLatch.countDown(); if (mMySegmentsHitCount == 3) { mMySegmentsUpdateLatch.countDown(); + + mCurrentUpdate.set(updatedMySegments()); Logger.d("updatedMySegments SEGMENTS"); - return createResponse(200, updatedMySegments()); } - Logger.d("DUMMY SEGMENTS"); - return createResponse(200, IntegrationHelper.dummyMySegments()); - } else if (uri.getPath().contains("/mySegments/key2")) { - return createResponse(200, IntegrationHelper.emptyMySegments()); + return createResponse(200, mCurrentUpdate.get()); + } else if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS + "/key2")) { + return createResponse(200, IntegrationHelper.emptyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { Logger.i("** Split Changes hit"); String data = IntegrationHelper.emptySplitChanges(-1, 1000); @@ -282,28 +293,30 @@ private String loadMockedData(String fileName) { return fileHelper.loadFileContent(mContext, fileName); } - private void pushMessage(String fileName) { + private void pushMessage(String msg) { new Thread(() -> { + String MSG_SEGMENT_UPDATE_TEMPLATE = "push_msg-largesegment_update.txt"; + BlockingQueue queue = mStreamingData; + String message = loadMockedData(MSG_SEGMENT_UPDATE_TEMPLATE); + message = message.replace("$TIMESTAMP$", String.valueOf(System.currentTimeMillis())); + message = message.replace(TestingHelper.MSG_DATA_FIELD, msg); try { - Thread.sleep(500); - String message = loadMockedData(fileName); - message = message.replace("$TIMESTAMP$", String.valueOf(System.currentTimeMillis())); mStreamingData.put(message + "" + "\n"); if (mMySegmentsPushLatch != null) { mMySegmentsPushLatch.countDown(); } Logger.d("Pushed message: " + message); - } catch (InterruptedException e) { - throw new RuntimeException(e); + e.printStackTrace(); } }).start(); } private String updatedMySegments() { - return "{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, " + - " { \"id\":\"id1\", \"name\":\"segment2\"}, " + - "{ \"id\":\"id3\", \"name\":\"segment3\"}]}"; + return Json.toJson(new AllSegmentsChange(Arrays.asList("segment1", "segment2", "segment3"))); } + private String segment1Update() { + return IntegrationHelper.dummySingleSegment("segment1"); + } } diff --git a/src/androidTest/java/tests/integration/streaming/OccupancyBaseTest.java b/src/androidTest/java/tests/integration/streaming/OccupancyBaseTest.java index 560166d67..a25b8af8d 100644 --- a/src/androidTest/java/tests/integration/streaming/OccupancyBaseTest.java +++ b/src/androidTest/java/tests/integration/streaming/OccupancyBaseTest.java @@ -61,10 +61,10 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("** My segments hit"); mMySegmentsHitCount++; - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { Logger.i("** Split Changes hit"); diff --git a/src/androidTest/java/tests/integration/streaming/SdkUpdateStreamingTest.java b/src/androidTest/java/tests/integration/streaming/SdkUpdateStreamingTest.java index ff4262c82..07ac91545 100644 --- a/src/androidTest/java/tests/integration/streaming/SdkUpdateStreamingTest.java +++ b/src/androidTest/java/tests/integration/streaming/SdkUpdateStreamingTest.java @@ -23,16 +23,15 @@ import fake.HttpClientMock; import fake.HttpResponseMock; import fake.HttpResponseMockDispatcher; +import fake.HttpStreamResponseMock; import helper.DatabaseHelper; import helper.FileHelper; import helper.IntegrationHelper; import helper.SplitEventTaskHelper; -import helper.TestingHelper; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; import io.split.android.client.api.Key; -import io.split.android.client.dtos.MySegment; import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; @@ -43,9 +42,8 @@ import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.utils.Json; import io.split.android.client.utils.logger.Logger; -import fake.HttpStreamResponseMock; - -import static java.lang.Thread.sleep; +import tests.integration.shared.TestingData; +import tests.integration.shared.TestingHelper; public class SdkUpdateStreamingTest { Context mContext; @@ -57,7 +55,6 @@ public class SdkUpdateStreamingTest { final static String MSG_SPLIT_UPDATE = "push_msg-split_update.txt"; final static String MSG_SPLIT_KILL = "push_msg-split_kill.txt"; - final static String MSG_SEGMENT_UPDATE_PAYLOAD = "push_msg-segment_update_payload.txt"; CountDownLatch mSplitsPushLatch = null; CountDownLatch mMySegmentsPushLatch = null; @@ -246,11 +243,19 @@ private void testSplitsUpdate() throws IOException, InterruptedException { mSplitsPushLatch = null; } - private void testMySegmentsUpdate() throws IOException, InterruptedException { + private void testMySegmentsUpdate() throws InterruptedException { mMySegmentsPushLatch = new CountDownLatch(1); - pushMessage(MSG_SEGMENT_UPDATE_PAYLOAD); - mMySegmentsPushLatch.await(5, TimeUnit.SECONDS); - mMySegmentsPushLatch = null; + String msg = TestingData.segmentsUnboundedNoCompression("1"); + String MSG_SEGMENT_UPDATE_TEMPLATE = "push_msg-largesegment_update.txt"; + BlockingQueue queue = mStreamingData; + String message = loadMockedData(MSG_SEGMENT_UPDATE_TEMPLATE); + message = message.replace("$TIMESTAMP$", String.valueOf(System.currentTimeMillis())); + message = message.replace(TestingHelper.MSG_DATA_FIELD, msg); + try { + queue.put(message + "" + "\n"); + Logger.d("Pushed message: " + message); + } catch (InterruptedException e) { + } } @After @@ -270,9 +275,10 @@ private HttpResponseMockDispatcher createNoChangesDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + Logger.e("PATH IS " + uri.getPath()); + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("NO CHANGWES MY S"); - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { Logger.i("NO CHANGES changes"); return createResponse(200, IntegrationHelper.emptySplitChanges(99999, 99999)); @@ -301,8 +307,8 @@ private HttpResponseMockDispatcher createSplitChangesDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { - return createResponse(200, IntegrationHelper.dummyMySegments()); + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { mSplitChangesHitCount++; if(mSplitsPushLatch != null) { @@ -333,19 +339,21 @@ private HttpResponseMockDispatcher createMySegmentsChangesDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + Logger.d("*** Memberships hit; " + mMySegmentsHitCount); mMySegmentsHitCount++; int hit = mMySegmentsHitCount; - String json = IntegrationHelper.emptyMySegments(); - if (mMySegmentsHitCount > 2) { - List mySegments = new ArrayList<>(); + String json = IntegrationHelper.emptyAllSegments(); + if (mMySegmentsHitCount > 1) { + StringBuilder mySegments = new StringBuilder(); + mySegments.append("{\"ms\":{\"k\":["); + List segmentList = new ArrayList<>(); for (int i = 0; i <= hit; i++) { - MySegment segment = new MySegment(); - segment.id = "s" + i; - segment.name = "segment" + i; - mySegments.add(segment); + segmentList.add("{\"n\":\"" + "s" + i + "\"}"); } - json = "{\"mySegments\": " + Json.toJson(mySegments) + "}"; + mySegments.append(String.join(",", segmentList)); + mySegments.append("],\"cn\":99999}}"); + json = mySegments.toString(); } if(mMySegmentsPushLatch != null) { mMySegmentsPushLatch.countDown(); diff --git a/src/androidTest/java/tests/integration/streaming/SplitChangeNotificationIntegrationTest.java b/src/androidTest/java/tests/integration/streaming/SplitChangeNotificationIntegrationTest.java index b62385aa2..8b13a657b 100644 --- a/src/androidTest/java/tests/integration/streaming/SplitChangeNotificationIntegrationTest.java +++ b/src/androidTest/java/tests/integration/streaming/SplitChangeNotificationIntegrationTest.java @@ -195,9 +195,9 @@ private HttpResponseMockDispatcher createStreamingResponseDispatcher(final Strin return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("mySegments")) { + if (uri.getPath().contains(IntegrationHelper.ServicePath.MEMBERSHIPS)) { mMySegmentsHitsCountLatch.countDown(); - return createResponse(IntegrationHelper.dummyMySegments()); + return createResponse(IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { mSplitsHitsCountLatch.countDown(); mSplitsHitsCountHit.incrementAndGet(); diff --git a/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java b/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java index 5e1b6c6c6..bf7fea927 100644 --- a/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java +++ b/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java @@ -1,5 +1,8 @@ package tests.integration.streaming; +import static java.lang.Thread.sleep; +import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; + import android.content.Context; import androidx.core.util.Pair; @@ -41,9 +44,6 @@ import io.split.android.client.utils.logger.Logger; import tests.integration.shared.TestingHelper; -import static java.lang.Thread.sleep; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; - public class SplitsKillProcessTest { Context mContext; BlockingQueue mStreamingData; @@ -83,7 +83,7 @@ public void setup() { mSplitRoomDatabase.clearAllTables(); mUserKey = IntegrationHelper.dummyUserKey(); mSplitRoomDatabase.generalInfoDao().update( - new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis() / 1000 - 30)); + new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis() - 30)); loadSplitChanges(); } @@ -168,11 +168,11 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("** My segments hit"); mMySegmentsSyncLatch.countDown(); - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { mSplitChangesHitCount++; @@ -242,6 +242,7 @@ private String getSplitChanges(int hit) { split.changeNumber = CHANGE_NUMBER; split.killed = true; split.defaultTreatment = "off"; + mSplitChange.since = CHANGE_NUMBER; mSplitChange.till = CHANGE_NUMBER; return Json.toJson(mSplitChange); } diff --git a/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java b/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java index 970eb47e1..536a9c7d4 100644 --- a/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java +++ b/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java @@ -1,5 +1,8 @@ package tests.integration.streaming; +import static java.lang.Thread.sleep; +import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; + import android.content.Context; import androidx.core.util.Pair; @@ -40,9 +43,6 @@ import io.split.android.client.utils.logger.Logger; import tests.integration.shared.TestingHelper; -import static java.lang.Thread.sleep; -import static helper.IntegrationHelper.ResponseClosure.getSinceFromUri; - public class SplitsSyncProcessTest { Context mContext; BlockingQueue mStreamingData; @@ -82,7 +82,7 @@ public void setup() { mSplitRoomDatabase = Room.inMemoryDatabaseBuilder(mContext, SplitRoomDatabase.class).build(); mSplitRoomDatabase.clearAllTables(); mSplitRoomDatabase.generalInfoDao().update( - new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis() / 1000 - 30)); + new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis() - 30)); mUserKey = IntegrationHelper.dummyUserKey(); loadSplitChanges(); } @@ -158,11 +158,11 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("** My segments hit"); mMySegmentsSyncLatch.countDown(); - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { mSplitChangesHitCount++; @@ -173,7 +173,7 @@ public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { mSplitsUpdateLatch.countDown(); return createResponse(200, getSplitChanges(mSplitChangesHitCount - 1)); } - String data = IntegrationHelper.emptySplitChanges(-1, CHANGE_NUMBER - 1000); + String data = IntegrationHelper.emptySplitChanges(CHANGE_NUMBER - 1000, CHANGE_NUMBER - 1000); return createResponse(200, data); } else if (uri.getPath().contains("/auth")) { Logger.i("** SSE Auth hit"); @@ -220,6 +220,7 @@ private void loadSplitChanges() { private String getSplitChanges(int hit) { mSplitChange.splits.get(0).changeNumber = CHANGE_NUMBER; + mSplitChange.since = CHANGE_NUMBER; mSplitChange.till = CHANGE_NUMBER; return Json.toJson(mSplitChange); } diff --git a/src/androidTest/java/tests/integration/streaming/SseAuthFail4xxTest.java b/src/androidTest/java/tests/integration/streaming/SseAuthFail4xxTest.java index 0eefda569..de167a6fa 100644 --- a/src/androidTest/java/tests/integration/streaming/SseAuthFail4xxTest.java +++ b/src/androidTest/java/tests/integration/streaming/SseAuthFail4xxTest.java @@ -102,11 +102,11 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("** My segments hit"); mMySegmentsHitsCountLatch.countDown(); mySegmentsHitsCountHit++; - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { Logger.i("** Split Changes hit"); mSplitsHitsCountLatch.countDown(); diff --git a/src/androidTest/java/tests/integration/streaming/SseAuthFail5xxTest.java b/src/androidTest/java/tests/integration/streaming/SseAuthFail5xxTest.java index 44a7585ab..fa567ec2f 100644 --- a/src/androidTest/java/tests/integration/streaming/SseAuthFail5xxTest.java +++ b/src/androidTest/java/tests/integration/streaming/SseAuthFail5xxTest.java @@ -132,14 +132,14 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher(){ @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("** My segments hit: " + mMySegmentsHitsCountHit); if(!mIsStreamingConnected) { mMySegmentsHitsCountHit++; } else { mMySegmentsHitsCountHitAfterSseConn++; } - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { Logger.i("** Split Changes hit: " + mSplitsHitsCountHit); if(!mIsStreamingConnected) { diff --git a/src/androidTest/java/tests/integration/streaming/SseConnectionExpiredTokenTest.java b/src/androidTest/java/tests/integration/streaming/SseConnectionExpiredTokenTest.java index 2cbf236c1..8b826c408 100644 --- a/src/androidTest/java/tests/integration/streaming/SseConnectionExpiredTokenTest.java +++ b/src/androidTest/java/tests/integration/streaming/SseConnectionExpiredTokenTest.java @@ -114,8 +114,8 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { - return createResponse(200, IntegrationHelper.dummyMySegments()); + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { String data = IntegrationHelper.emptySplitChanges(-1, 1000); return createResponse(200, data); diff --git a/src/androidTest/java/tests/integration/streaming/SseConnectionFail5xxTest.java b/src/androidTest/java/tests/integration/streaming/SseConnectionFail5xxTest.java index 32bb2f094..8671d41b2 100644 --- a/src/androidTest/java/tests/integration/streaming/SseConnectionFail5xxTest.java +++ b/src/androidTest/java/tests/integration/streaming/SseConnectionFail5xxTest.java @@ -1,5 +1,7 @@ package tests.integration.streaming; +import static java.lang.Thread.sleep; + import android.content.Context; import androidx.test.platform.app.InstrumentationRegistry; @@ -18,8 +20,8 @@ import fake.HttpClientMock; import fake.HttpResponseMock; import fake.HttpResponseMockDispatcher; -import helper.DatabaseHelper; import fake.HttpStreamResponseMock; +import helper.DatabaseHelper; import helper.IntegrationHelper; import helper.SplitEventTaskHelper; import io.split.android.client.SplitClient; @@ -30,8 +32,6 @@ import io.split.android.client.utils.logger.Logger; import tests.integration.shared.TestingHelper; -import static java.lang.Thread.sleep; - public class SseConnectionFail5xxTest { Context mContext; BlockingQueue mStreamingData; @@ -130,14 +130,14 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("** My segments hit: " + mMySegmentsHitsCountHit); if (!mIsStreamingConnected) { mMySegmentsHitsCountHit++; } else { mMySegmentsHitsCountHitAfterSseConn++; } - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { Logger.i("** Split Changes hit: " + mSplitsHitsCountHit); if (!mIsStreamingConnected) { diff --git a/src/androidTest/java/tests/integration/streaming/StreamingDisabledInConfigTest.java b/src/androidTest/java/tests/integration/streaming/StreamingDisabledInConfigTest.java index 637e7a70c..a4183413f 100644 --- a/src/androidTest/java/tests/integration/streaming/StreamingDisabledInConfigTest.java +++ b/src/androidTest/java/tests/integration/streaming/StreamingDisabledInConfigTest.java @@ -106,11 +106,11 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher(){ @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("** My segments hit"); mMySegmentsHitsCountLatch.countDown(); mySegmentsHitsCountHit++; - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { Logger.i("** Split Changes hit"); mSplitsHitsCountLatch.countDown(); diff --git a/src/androidTest/java/tests/integration/streaming/StreamingDisabledTest.java b/src/androidTest/java/tests/integration/streaming/StreamingDisabledTest.java index c66830c97..7fed4ee16 100644 --- a/src/androidTest/java/tests/integration/streaming/StreamingDisabledTest.java +++ b/src/androidTest/java/tests/integration/streaming/StreamingDisabledTest.java @@ -158,11 +158,11 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher(){ @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("** My segments hit"); mMySegmentsHitsCountLatch.countDown(); mySegmentsHitsCountHit++; - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { Logger.i("** Split Changes hit"); mSplitsHitsCountLatch.countDown(); diff --git a/src/androidTest/java/tests/integration/streaming/StreamingInitializationTest.java b/src/androidTest/java/tests/integration/streaming/StreamingInitializationTest.java index 77a831041..74f88ff47 100644 --- a/src/androidTest/java/tests/integration/streaming/StreamingInitializationTest.java +++ b/src/androidTest/java/tests/integration/streaming/StreamingInitializationTest.java @@ -92,9 +92,9 @@ private HttpResponseMockDispatcher createBasicResponseDispatcher() { return new HttpResponseMockDispatcher(){ @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("** My segments hit"); - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { Logger.i("** Split Changes hit"); String data = IntegrationHelper.emptySplitChanges(-1, 1000); diff --git a/src/androidTest/java/tests/integration/streaming/SyncGuardianIntegrationTest.java b/src/androidTest/java/tests/integration/streaming/SyncGuardianIntegrationTest.java index 41f9109a9..fd51fd8a6 100644 --- a/src/androidTest/java/tests/integration/streaming/SyncGuardianIntegrationTest.java +++ b/src/androidTest/java/tests/integration/streaming/SyncGuardianIntegrationTest.java @@ -290,9 +290,9 @@ private HttpResponseMockDispatcher createStreamingResponseDispatcher(final Strin return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("mySegments")) { + if (uri.getPath().contains(IntegrationHelper.ServicePath.MEMBERSHIPS)) { mMySegmentsHitsCountLatch.countDown(); - return createResponse(IntegrationHelper.dummyMySegments()); + return createResponse(IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { mSplitsHitsCountLatch.countDown(); mSplitsHitsCountHit.incrementAndGet(); diff --git a/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java b/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java index 3415e6c14..50d747591 100644 --- a/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java +++ b/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java @@ -114,12 +114,12 @@ public void evaluationByFlagsInfoIsInPayload() throws InterruptedException { @Override public MockResponse dispatch(RecordedRequest request) { String path = request.getPath(); - if (path.contains("/mySegments")) { - return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); } else if (path.contains("/splitChanges")) { long changeNumber = -1; return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\":" + changeNumber + ", \"till\":" + (changeNumber + 1000) + "}"); + .setBody("{\"splits\":[], \"since\":" + (changeNumber + 1000) + ", \"till\":" + (changeNumber + 1000) + "}"); } else if (path.contains("/events/bulk")) { return new MockResponse().setResponseCode(200); } else if (path.contains("metrics/usage")) { @@ -214,12 +214,12 @@ public void recordAuthRejections() throws InterruptedException { @Override public MockResponse dispatch(RecordedRequest request) { String path = request.getPath(); - if (path.contains("/mySegments")) { - return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); } else if (path.contains("/splitChanges")) { long changeNumber = -1; return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\":" + changeNumber + ", \"till\":" + (changeNumber + 1000) + "}"); + .setBody("{\"splits\":[], \"since\":" + (changeNumber + 1000) + ", \"till\":" + (changeNumber + 1000) + "}"); } else if (path.contains("/events/bulk")) { return new MockResponse().setResponseCode(200); } else if (path.contains("metrics/usage")) { @@ -267,12 +267,12 @@ public void flagSetsAreIncludedInPayload() throws InterruptedException { @Override public MockResponse dispatch(RecordedRequest request) { String path = request.getPath(); - if (path.contains("/mySegments")) { - return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); } else if (path.contains("/splitChanges")) { long changeNumber = -1; return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\":" + changeNumber + ", \"till\":" + (changeNumber + 1000) + "}"); + .setBody("{\"splits\":[], \"since\":" + (changeNumber + 1000) + ", \"till\":" + (changeNumber + 1000) + "}"); } else if (path.contains("/events/bulk")) { return new MockResponse().setResponseCode(200); } else if (path.contains("metrics/usage")) { @@ -367,7 +367,7 @@ private void insertSplitsFromFileIntoDB() { testDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.DATBASE_MIGRATION_STATUS, GeneralInfoEntity.DATBASE_MIGRATION_STATUS_DONE)); testDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 1)); - testDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis() / 1000)); + testDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis())); testDatabase.splitDao().insert(entities); } @@ -389,12 +389,12 @@ private void setupServer() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { String path = request.getPath(); - if (path.contains("/mySegments")) { - return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id1\", \"name\":\"segment2\"}]}"); + if (path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.dummyAllSegments()); } else if (path.contains("/splitChanges")) { long changeNumber = -1; return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\":" + changeNumber + ", \"till\":" + (changeNumber + 1000) + "}"); + .setBody("{\"splits\":[], \"since\":" + (changeNumber + 1000) + ", \"till\":" + (changeNumber + 1000) + "}"); } else if (path.contains("/events/bulk")) { return new MockResponse().setResponseCode(200); } else if (path.contains("/metrics")) { diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt index 4c0d9eb02..c86df4698 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt @@ -226,8 +226,8 @@ class UserConsentModeDebugTest { override fun getResponse(uri: URI, method: HttpMethod, body: String): HttpResponseMock { println(uri.path) - return if (uri.path.contains("/mySegments")) { - HttpResponseMock(200, IntegrationHelper.emptyMySegments()) + return if (uri.path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + HttpResponseMock(200, IntegrationHelper.emptyAllSegments()) } else if (uri.path.contains("/splitChanges")) { if (mChangeHit == 0) { mChangeHit+=1 diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt index 691b21369..eac6083a6 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt @@ -230,8 +230,8 @@ class UserConsentModeNoneTest { override fun getResponse(uri: URI, method: HttpMethod, body: String): HttpResponseMock { println(uri.path) - return if (uri.path.contains("/mySegments")) { - HttpResponseMock(200, IntegrationHelper.emptyMySegments()) + return if (uri.path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + HttpResponseMock(200, IntegrationHelper.emptyAllSegments()) } else if (uri.path.contains("/splitChanges")) { if (mChangeHit == 0) { mChangeHit += 1 diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt index bc864fa4d..686b3436e 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt @@ -238,8 +238,8 @@ class UserConsentModeOptimizedTest { override fun getResponse(uri: URI, method: HttpMethod, body: String): HttpResponseMock { println(uri.path) - return if (uri.path.contains("/mySegments")) { - HttpResponseMock(200, IntegrationHelper.emptyMySegments()) + return if (uri.path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + HttpResponseMock(200, IntegrationHelper.emptyAllSegments()) } else if (uri.path.contains("/splitChanges")) { if (mChangeHit == 0) { mChangeHit+=1 diff --git a/src/androidTest/java/tests/service/ImpressionsRequestTest.java b/src/androidTest/java/tests/service/ImpressionsRequestTest.java index 8b7df88ae..d9124428a 100644 --- a/src/androidTest/java/tests/service/ImpressionsRequestTest.java +++ b/src/androidTest/java/tests/service/ImpressionsRequestTest.java @@ -110,7 +110,7 @@ private Map getMockResponses() { }); responses.put("splitChanges", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptySplitChanges(9999, 9999))); - responses.put("mySegments/key1", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "key1", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); responses.put("v2/auth", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.streamingEnabledToken())); return responses; diff --git a/src/androidTest/java/tests/service/SdkUpdatePollingTest.java b/src/androidTest/java/tests/service/SdkUpdatePollingTest.java index 087cda511..ee20e6bd6 100644 --- a/src/androidTest/java/tests/service/SdkUpdatePollingTest.java +++ b/src/androidTest/java/tests/service/SdkUpdatePollingTest.java @@ -32,7 +32,7 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; import io.split.android.client.api.Key; -import io.split.android.client.dtos.MySegment; +import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; @@ -227,9 +227,9 @@ private HttpResponseMockDispatcher createNoChangesDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { Logger.i("NO CHANGWES MY S"); - return createResponse(200, IntegrationHelper.dummyMySegments()); + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { String json = IntegrationHelper.emptySplitChanges(99999, 99999); Logger.i("NO CHANGES changes: " + json); @@ -259,8 +259,8 @@ private HttpResponseMockDispatcher createSplitChangesDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { - return createResponse(200, IntegrationHelper.dummyMySegments()); + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return createResponse(200, IntegrationHelper.dummyAllSegments()); } else if (uri.getPath().contains("/splitChanges")) { mSplitChangesHitCount++; String json = getChanges(mSplitChangesHitCount); @@ -290,17 +290,14 @@ private HttpResponseMockDispatcher createMySegmentsChangesDispatcher() { return new HttpResponseMockDispatcher() { @Override public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { - if (uri.getPath().contains("/mySegments")) { + if (uri.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { mMySegmentsHitCount++; int hit = mMySegmentsHitCount; - List mySegments = new ArrayList<>(); + List mySegments = new ArrayList<>(); for (int i = 0; i <= hit; i++) { - MySegment segment = new MySegment(); - segment.id = "s" + i; - segment.name = "segment" + i; - mySegments.add(segment); + mySegments.add("segment" + i); } - String json = "{\"mySegments\": " + Json.toJson(mySegments) + "}"; + String json = Json.toJson(new AllSegmentsChange(mySegments)); return createResponse(200, json); } else if (uri.getPath().contains("/splitChanges")) { diff --git a/src/androidTest/java/tests/service/SseJwtTokenParserTest.java b/src/androidTest/java/tests/service/SseJwtTokenParserTest.java index 80e3922dc..ed9bef446 100644 --- a/src/androidTest/java/tests/service/SseJwtTokenParserTest.java +++ b/src/androidTest/java/tests/service/SseJwtTokenParserTest.java @@ -165,4 +165,34 @@ public void nullToken() { Assert.assertNotNull(thrownEx); } + @Test + public void testLargeSegmentsToken() throws InvalidJwtTokenException { + String jwtToken = "eyJhbGciOiJIUzI1NiIsImtpZCI6IjVZOU05US45QnJtR0EiLCJ0eXAiOiJKV1QifQ." + + "ewogICJ4LWFibHktY2FwYWJpbGl0eSI6ICJ7XCJNek01TmpjME9EY3lOZz09X01URXhNemd3Tm" + + "pneF9NVGN3TlRJMk1UTTBNZz09X215U2VnbWVudHNcIjpbXCJzdWJzY3JpYmVcIl0sXCJNek01" + + "TmpjME9EY3lOZz09X01URXhNemd3TmpneF9NVGN3TlRJMk1UTTBNZz09X215bGFyZ2VzZWdtZW" + + "50c1wiOltcInN1YnNjcmliZVwiXSxcIk16TTVOamMwT0RjeU5nPT1fTVRFeE16Z3dOamd4X3Nw" + + "bGl0c1wiOltcInN1YnNjcmliZVwiXSxcImNvbnRyb2xfcHJpXCI6W1wic3Vic2NyaWJlXCIsXC" + + "JjaGFubmVsLW1ldGFkYXRhOnB1Ymxpc2hlcnNcIl0sXCJjb250cm9sX3NlY1wiOltcInN1YnNj" + + "cmliZVwiLFwiY2hhbm5lbC1tZXRhZGF0YTpwdWJsaXNoZXJzXCJdfSIsCiAgIngtYWJseS1jbG" + + "llbnRJZCI6ICJjbGllbnRJZCIsCiAgImV4cCI6IDIyMDg5ODg4MDAsCiAgImlhdCI6IDE1ODc0M" + + "DQzODgKfQ==.LcKAXnkr-CiYVxZ7l38w9i98Y-BMAv9JlGP2i92nVQY"; + + SseJwtParser parser = new SseJwtParser(); + SseJwtToken parsedToken = parser.parse(jwtToken); + List channels = parsedToken.getChannels(); + + Assert.assertEquals(2208988800L, parsedToken.getExpirationTime()); + Assert.assertEquals(jwtToken, parsedToken.getRawJwt()); + Assert.assertEquals("MzM5Njc0ODcyNg==_MTExMzgwNjgx_MTcwNTI2MTM0Mg==_mySegments", + channels.get(0)); + Assert.assertEquals("MzM5Njc0ODcyNg==_MTExMzgwNjgx_MTcwNTI2MTM0Mg==_mylargesegments", + channels.get(1)); + Assert.assertEquals("MzM5Njc0ODcyNg==_MTExMzgwNjgx_splits", + channels.get(2)); + Assert.assertEquals("[?occupancy=metrics.publishers]control_pri", + channels.get(3)); + Assert.assertEquals("[?occupancy=metrics.publishers]control_sec", + channels.get(4)); + } } diff --git a/src/androidTest/java/tests/service/UniqueKeysIntegrationTest.java b/src/androidTest/java/tests/service/UniqueKeysIntegrationTest.java index 8b807b378..244bf685c 100644 --- a/src/androidTest/java/tests/service/UniqueKeysIntegrationTest.java +++ b/src/androidTest/java/tests/service/UniqueKeysIntegrationTest.java @@ -9,7 +9,6 @@ import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import java.io.IOException; @@ -248,14 +247,14 @@ private Map getMockResponses() { }); responses.put("splitChanges", (uri, httpMethod, body) -> { - String splitChange = "{\"splits\":[{\"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\"}]},{\"trafficTypeName\":\"account\",\"name\":\"android_test_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-397942789,\"seed\":1852089605,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733496087,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"android_test\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in segment android_test\"}]}],\"since\":-1,\"till\":1648733409158}"; + String splitChange = "{\"splits\":[{\"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\"}]},{\"trafficTypeName\":\"account\",\"name\":\"android_test_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-397942789,\"seed\":1852089605,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733496087,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"android_test\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in segment android_test\"}]}],\"since\":1648733409158,\"till\":1648733409158}"; return new HttpResponseMock(200, splitChange); }); IntegrationHelper.ResponseClosure mySegmentsResponse = (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyMySegments()); - responses.put("mySegments/key1", mySegmentsResponse); - responses.put("mySegments/key2", mySegmentsResponse); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/key1", mySegmentsResponse); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/key2", mySegmentsResponse); responses.put("v2/auth", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.streamingEnabledToken())); return responses; diff --git a/src/androidTest/java/tests/storage/LoadMySegmentsTaskTest.java b/src/androidTest/java/tests/storage/LoadMySegmentsTaskTest.java index 509c6c5dd..84897a005 100644 --- a/src/androidTest/java/tests/storage/LoadMySegmentsTaskTest.java +++ b/src/androidTest/java/tests/storage/LoadMySegmentsTaskTest.java @@ -16,7 +16,9 @@ import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.mysegments.LoadMySegmentsTask; +import io.split.android.client.service.mysegments.LoadMySegmentsTaskConfig; import io.split.android.client.storage.cipher.SplitCipherFactory; +import io.split.android.client.storage.db.MyLargeSegmentEntity; import io.split.android.client.storage.db.MySegmentEntity; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.storage.mysegments.MySegmentsStorage; @@ -29,9 +31,12 @@ public class LoadMySegmentsTaskTest { SplitRoomDatabase mRoomDb; Context mContext; PersistentMySegmentsStorage mPersistentMySegmentsStorage; + PersistentMySegmentsStorage mPersistentMyLargeSegmentsStorage; MySegmentsStorage mMySegmentsStorage; + MySegmentsStorage mMyLargeSegmentsStorage; final String mUserKey = "userkey-1"; private MySegmentsStorageContainer mMySegmentsStorageContainer; + private MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; @Before public void setUp() { @@ -51,22 +56,43 @@ public void setUp() { entity.setUpdatedAt(System.currentTimeMillis() / 1000); mRoomDb.mySegmentDao().update(entity); - mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage(mRoomDb, SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", false)); + MyLargeSegmentEntity largeEntity = new MyLargeSegmentEntity(); + largeEntity.setUserKey(mUserKey); + largeEntity.setSegmentList("ls1,ls2,ls3"); + largeEntity.setUpdatedAt(System.currentTimeMillis() / 1000); + mRoomDb.myLargeSegmentDao().update(largeEntity); + + largeEntity = new MyLargeSegmentEntity(); + largeEntity.setUserKey("userkey-2"); + largeEntity.setSegmentList("ls10,ls20"); + largeEntity.setUpdatedAt(System.currentTimeMillis() / 1000); + mRoomDb.myLargeSegmentDao().update(largeEntity); + + mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage(SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", false), mRoomDb.mySegmentDao(), MySegmentEntity.creator()); + mPersistentMyLargeSegmentsStorage = new SqLitePersistentMySegmentsStorage(SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", false), mRoomDb.myLargeSegmentDao(), MyLargeSegmentEntity.creator()); mMySegmentsStorageContainer = new MySegmentsStorageContainerImpl(mPersistentMySegmentsStorage); + mMyLargeSegmentsStorageContainer = new MySegmentsStorageContainerImpl(mPersistentMyLargeSegmentsStorage); mMySegmentsStorage = mMySegmentsStorageContainer.getStorageForKey(mUserKey); + mMyLargeSegmentsStorage = mMyLargeSegmentsStorageContainer.getStorageForKey(mUserKey); } @Test public void execute() { - SplitTask task = new LoadMySegmentsTask(mMySegmentsStorage); + SplitTask task = new LoadMySegmentsTask(mMySegmentsStorage, mMyLargeSegmentsStorage, LoadMySegmentsTaskConfig.get()); SplitTaskExecutionInfo result = task.execute(); - Set snapshot = new HashSet(mMySegmentsStorage.getAll()); + Set snapshot = new HashSet<>(mMySegmentsStorage.getAll()); + Set largeSnapshot = new HashSet<>(mMyLargeSegmentsStorage.getAll()); Assert.assertEquals(3, snapshot.size()); Assert.assertTrue(snapshot.contains("s1")); Assert.assertTrue(snapshot.contains("s2")); Assert.assertTrue(snapshot.contains("s3")); + + Assert.assertEquals(3, largeSnapshot.size()); + Assert.assertTrue(largeSnapshot.contains("ls1")); + Assert.assertTrue(largeSnapshot.contains("ls2")); + Assert.assertTrue(largeSnapshot.contains("ls3")); Assert.assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); } } diff --git a/src/androidTest/java/tests/storage/MySegmentsStorageTest.java b/src/androidTest/java/tests/storage/MySegmentsStorageTest.java index ecbd9764f..4cea487ad 100644 --- a/src/androidTest/java/tests/storage/MySegmentsStorageTest.java +++ b/src/androidTest/java/tests/storage/MySegmentsStorageTest.java @@ -10,15 +10,15 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import helper.DatabaseHelper; +import helper.IntegrationHelper; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.storage.cipher.SplitCipherFactory; import io.split.android.client.storage.db.MySegmentEntity; import io.split.android.client.storage.db.SplitRoomDatabase; @@ -45,18 +45,18 @@ public void setUp() { MySegmentEntity entity = new MySegmentEntity(); entity.setUserKey(mUserKey); - entity.setSegmentList("s1,s2,s3"); + entity.setSegmentList("{\"k\":[{\"n\":\"s1\"},{\"n\":\"s2\"},{\"n\":\"s3\"}],\"cn\":-1}"); entity.setUpdatedAt(System.currentTimeMillis() / 1000); mRoomDb.mySegmentDao().update(entity); entity = new MySegmentEntity(); entity.setUserKey("userkey-2"); - entity.setSegmentList("s10,s20"); + entity.setSegmentList("{\"k\":[{\"n\":\"s10\"},{\"n\":\"s20\"}],\"cn\":-1}"); entity.setUpdatedAt(System.currentTimeMillis() / 1000); mRoomDb.mySegmentDao().update(entity); - mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage(mRoomDb, - SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", false)); + mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage( + SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", false), mRoomDb.mySegmentDao(), MySegmentEntity.creator()); mMySegmentsStorageContainer = new MySegmentsStorageContainerImpl(mPersistentMySegmentsStorage); mMySegmentsStorage = mMySegmentsStorageContainer.getStorageForKey(mUserKey); } @@ -69,7 +69,7 @@ public void noLocalLoaded() { } @Test - public void getMySegments() { + public void getSegments() { mMySegmentsStorage.loadLocal(); Set snapshot = new HashSet(mMySegmentsStorage.getAll()); @@ -82,30 +82,34 @@ public void getMySegments() { @Test public void updateSegments() { mMySegmentsStorage.loadLocal(); - mMySegmentsStorage.set(Arrays.asList("a1", "a2", "a3", "a4")); + mMySegmentsStorage.set(SegmentsChange.create(IntegrationHelper.asSet("a1", "a2", "a3", "a4"), 2222222L)); MySegmentsStorage mySegmentsStorage = mMySegmentsStorageContainer.getStorageForKey(mUserKey); mySegmentsStorage.loadLocal(); Set snapshot = new HashSet<>(mMySegmentsStorage.getAll()); + long till = mMySegmentsStorage.getChangeNumber(); Set newSnapshot = new HashSet<>(mySegmentsStorage.getAll()); + long newTill = mySegmentsStorage.getChangeNumber(); assertEquals(4, snapshot.size()); assertTrue(snapshot.contains("a1")); assertTrue(snapshot.contains("a2")); assertTrue(snapshot.contains("a3")); assertTrue(snapshot.contains("a4")); + assertEquals(2222222, till); assertEquals(4, newSnapshot.size()); assertTrue(newSnapshot.contains("a1")); assertTrue(newSnapshot.contains("a2")); assertTrue(newSnapshot.contains("a3")); assertTrue(newSnapshot.contains("a4")); + assertEquals(2222222, newTill); } @Test public void updateEmptyMySegment() { mMySegmentsStorage.loadLocal(); - mMySegmentsStorage.set(new ArrayList<>()); + mMySegmentsStorage.set(SegmentsChange.create(Collections.emptySet(), 11124442L)); MySegmentsStorage mySegmentsStorage = mMySegmentsStorageContainer.getStorageForKey(mUserKey); mySegmentsStorage.loadLocal(); @@ -115,12 +119,13 @@ public void updateEmptyMySegment() { assertEquals(0, snapshot.size()); assertEquals(0, newSnapshot.size()); + assertEquals(11124442, mySegmentsStorage.getChangeNumber()); } @Test public void addNullMySegmentsList() { - mPersistentMySegmentsStorage.set(mUserKey, null); + mPersistentMySegmentsStorage.set(mUserKey, SegmentsChange.create(null, -1)); // till will be ignored mMySegmentsStorage.loadLocal(); MySegmentsStorage mySegmentsStorage = mMySegmentsStorageContainer.getStorageForKey(mUserKey); mySegmentsStorage.loadLocal(); @@ -130,6 +135,7 @@ public void addNullMySegmentsList() { assertEquals(3, snapshot.size()); assertEquals(3, newSnapshot.size()); + assertEquals(-1, mySegmentsStorage.getChangeNumber()); } @Test @@ -144,16 +150,17 @@ public void clear() { Set snapshot = new HashSet<>(mMySegmentsStorage.getAll()); assertEquals(0, snapshot.size()); + assertEquals(-1, mySegmentsStorage.getChangeNumber()); } @Test public void originalValuesCanBeRetrievedWhenStorageIsEncrypted() { - mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage(mRoomDb, - SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", true)); + mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage( + SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", true), mRoomDb.mySegmentDao(), MySegmentEntity.creator()); mMySegmentsStorageContainer = new MySegmentsStorageContainerImpl(mPersistentMySegmentsStorage); mMySegmentsStorage = mMySegmentsStorageContainer.getStorageForKey(mUserKey); - mMySegmentsStorage.set(Arrays.asList("a1", "a2", "a3", "a4")); + mMySegmentsStorage.set(SegmentsChange.create(IntegrationHelper.asSet("a1", "a2", "a3", "a4"), 999820)); MySegmentsStorage mySegmentsStorage = mMySegmentsStorageContainer.getStorageForKey(mUserKey); mySegmentsStorage.loadLocal(); @@ -163,6 +170,7 @@ public void originalValuesCanBeRetrievedWhenStorageIsEncrypted() { assertTrue(all.contains("a3")); assertTrue(all.contains("a4")); assertEquals(4, all.size()); + assertEquals(999820, mySegmentsStorage.getChangeNumber()); } @Test @@ -170,11 +178,11 @@ public void updateToStorageConcurrency() throws InterruptedException { mMySegmentsStorage.loadLocal(); CountDownLatch latch = new CountDownLatch(2); - new Thread(new Runnable() { + Thread thread1 = new Thread(new Runnable() { @Override public void run() { for (int j = 1000; j < 1200; j += 10) { - List segments = new ArrayList<>(); + Set segments = new HashSet<>(); for (int i = 0; i < 10; i++) { segments.add("segment_" + j + "_" + i); @@ -183,34 +191,40 @@ public void run() { Thread.sleep(80); } catch (InterruptedException e) { } - mMySegmentsStorage.set(segments); + mMySegmentsStorage.set(SegmentsChange.create(segments, 112421 + j)); } latch.countDown(); } - }).start(); + }); - new Thread(new Runnable() { + Thread thread2 = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 200; j += 10) { - List segments = new ArrayList<>(); + Set segments = new HashSet<>(); for (int i = 0; i < 10; i++) { segments.add("segment_" + j + "_" + i); } try { Thread.sleep(80); - mMySegmentsStorage.set(segments); + mMySegmentsStorage.set(SegmentsChange.create(segments, 112421 + j)); Thread.sleep(80); } catch (InterruptedException e) { } } latch.countDown(); } - }).start(); + }); + + thread1.start(); + thread2.start(); + thread1.join(); + thread2.join(); + latch.await(40, TimeUnit.SECONDS); - Set l = mMySegmentsStorage.getAll(); assertEquals(10, mMySegmentsStorage.getAll().size()); + assertEquals(112421 + 190, mMySegmentsStorage.getChangeNumber()); } } diff --git a/src/androidTest/java/tests/storage/PersistentMyLargeSegmentStorageTest.java b/src/androidTest/java/tests/storage/PersistentMyLargeSegmentStorageTest.java new file mode 100644 index 000000000..05a3024f6 --- /dev/null +++ b/src/androidTest/java/tests/storage/PersistentMyLargeSegmentStorageTest.java @@ -0,0 +1,116 @@ +package tests.storage; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import helper.DatabaseHelper; +import helper.IntegrationHelper; +import io.split.android.client.dtos.SegmentsChange; +import io.split.android.client.storage.cipher.SplitCipherFactory; +import io.split.android.client.storage.db.MyLargeSegmentEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.mysegments.PersistentMySegmentsStorage; +import io.split.android.client.storage.mysegments.SqLitePersistentMySegmentsStorage; + +public class PersistentMyLargeSegmentStorageTest { + SplitRoomDatabase mRoomDb; + Context mContext; + PersistentMySegmentsStorage mPersistentMySegmentsStorage; + private final String mUserKey = "userkey-1"; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + mRoomDb = DatabaseHelper.getTestDatabase(mContext); + mRoomDb.clearAllTables(); + + MyLargeSegmentEntity entity = new MyLargeSegmentEntity(); + entity.setUserKey(mUserKey); + entity.setSegmentList("{\"k\":[{\"n\":\"s1\"},{\"n\":\"s2\"},{\"n\":\"s3\"}],\"cn\":null}"); + entity.setUpdatedAt(System.currentTimeMillis() / 1000); + mRoomDb.myLargeSegmentDao().update(entity); + + entity = new MyLargeSegmentEntity(); + String mUserKey2 = "userkey-2"; + entity.setUserKey(mUserKey2); + entity.setSegmentList("{\"k\":[{\"n\":\"s10\"},{\"n\":\"s20\"}],\"cn\":-1}"); + entity.setUpdatedAt(System.currentTimeMillis() / 1000); + mRoomDb.myLargeSegmentDao().update(entity); + + mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage( + SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", false), mRoomDb.myLargeSegmentDao(), MyLargeSegmentEntity.creator()); + } + + @Test + public void getMySegments() { + SegmentsChange snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); + + Assert.assertEquals(3, snapshot.getNames().size()); + Assert.assertTrue(snapshot.getNames().contains("s1")); + Assert.assertTrue(snapshot.getNames().contains("s2")); + Assert.assertTrue(snapshot.getNames().contains("s3")); + Assert.assertNull(snapshot.getChangeNumber()); + } + + @Test + public void updateSegments() { + + mPersistentMySegmentsStorage.set(mUserKey, SegmentsChange.create(IntegrationHelper.asSet("a1", "a2", "a3", "a4"), 2002012)); + + SegmentsChange snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); + + Assert.assertEquals(4, snapshot.getNames().size()); + Assert.assertTrue(snapshot.getNames().contains("a1")); + Assert.assertTrue(snapshot.getNames().contains("a2")); + Assert.assertTrue(snapshot.getNames().contains("a3")); + Assert.assertTrue(snapshot.getNames().contains("a4")); + Assert.assertEquals(2002012, snapshot.getChangeNumber().longValue()); + } + + @Test + public void updateSegmentsEncrypted() { + mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage( + SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", true), mRoomDb.myLargeSegmentDao(), MyLargeSegmentEntity.creator()); + + mPersistentMySegmentsStorage.set(mUserKey, SegmentsChange.create(IntegrationHelper.asSet("a1", "a2", "a3", "a4"), 2002012)); + + SegmentsChange snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); + + List mySegments = snapshot.getNames(); + Assert.assertEquals(4, mySegments.size()); + Assert.assertTrue(mySegments.contains("a1")); + Assert.assertTrue(mySegments.contains("a2")); + Assert.assertTrue(mySegments.contains("a3")); + Assert.assertTrue(mySegments.contains("a4")); + Assert.assertEquals(2002012, snapshot.getChangeNumber().longValue()); + } + + @Test + public void updateEmptyMySegment() { + + mPersistentMySegmentsStorage.set(mUserKey, SegmentsChange.create(Collections.emptySet(), 2002012)); + + SegmentsChange snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); + + Assert.assertEquals(0, snapshot.getNames().size()); + Assert.assertEquals(2002012, snapshot.getChangeNumber().longValue()); + } + + @Test + public void addNullMySegmentsList() { + mPersistentMySegmentsStorage.set(mUserKey, SegmentsChange.create(null, 2002012)); + + SegmentsChange snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); + + Assert.assertEquals(3, snapshot.getNames().size()); + Assert.assertNull(snapshot.getChangeNumber()); + } +} diff --git a/src/androidTest/java/tests/storage/PersistentMySegmentStorageTest.java b/src/androidTest/java/tests/storage/PersistentMySegmentStorageTest.java index f620f8022..8a7bd7e64 100644 --- a/src/androidTest/java/tests/storage/PersistentMySegmentStorageTest.java +++ b/src/androidTest/java/tests/storage/PersistentMySegmentStorageTest.java @@ -8,14 +8,11 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; import helper.DatabaseHelper; +import helper.IntegrationHelper; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.storage.cipher.SplitCipherFactory; import io.split.android.client.storage.db.MySegmentEntity; import io.split.android.client.storage.db.SplitRoomDatabase; @@ -37,77 +34,85 @@ public void setUp() { MySegmentEntity entity = new MySegmentEntity(); entity.setUserKey(mUserKey); - entity.setSegmentList("s1,s2,s3"); + entity.setSegmentList("{\"k\":[{\"n\":\"s1\"},{\"n\":\"s2\"},{\"n\":\"s3\"}],\"cn\":null}"); entity.setUpdatedAt(System.currentTimeMillis() / 1000); mRoomDb.mySegmentDao().update(entity); entity = new MySegmentEntity(); String mUserKey2 = "userkey-2"; entity.setUserKey(mUserKey2); - entity.setSegmentList("s10,s20"); + entity.setSegmentList("{\"k\":[{\"n\":\"s10\"},{\"n\":\"s20\"}],\"cn\":-1}"); entity.setUpdatedAt(System.currentTimeMillis() / 1000); mRoomDb.mySegmentDao().update(entity); - mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage(mRoomDb, - SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", false)); + mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage( + SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", false), mRoomDb.mySegmentDao(), MySegmentEntity.creator()); } @Test - public void getMySegments() { - Set snapshot = new HashSet<>(mPersistentMySegmentsStorage.getSnapshot(mUserKey)); - - Assert.assertEquals(3, snapshot.size()); - Assert.assertTrue(snapshot.contains("s1")); - Assert.assertTrue(snapshot.contains("s2")); - Assert.assertTrue(snapshot.contains("s3")); + public void getNames() { + SegmentsChange snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); + + List mySegments = snapshot.getNames(); + Assert.assertEquals(3, mySegments.size()); + Assert.assertTrue(mySegments.contains("s1")); + Assert.assertTrue(mySegments.contains("s2")); + Assert.assertTrue(mySegments.contains("s3")); } @Test public void updateSegments() { - mPersistentMySegmentsStorage.set(mUserKey, Collections.singletonList("a1,a2,a3,a4")); + mPersistentMySegmentsStorage.set(mUserKey, SegmentsChange.create(IntegrationHelper.asSet("a1", "a2", "a3", "a4"), 2002012L)); - Set snapshot = new HashSet<>(mPersistentMySegmentsStorage.getSnapshot(mUserKey)); + SegmentsChange snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); - Assert.assertEquals(4, snapshot.size()); - Assert.assertTrue(snapshot.contains("a1")); - Assert.assertTrue(snapshot.contains("a2")); - Assert.assertTrue(snapshot.contains("a3")); - Assert.assertTrue(snapshot.contains("a4")); + List mySegments = snapshot.getNames(); + Long till = snapshot.getChangeNumber(); + Assert.assertEquals(4, mySegments.size()); + Assert.assertTrue(mySegments.contains("a1")); + Assert.assertTrue(mySegments.contains("a2")); + Assert.assertTrue(mySegments.contains("a3")); + Assert.assertTrue(mySegments.contains("a4")); + Assert.assertEquals(2002012, till.longValue()); } @Test public void updateSegmentsEncrypted() { - mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage(mRoomDb, - SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", true)); + mPersistentMySegmentsStorage = new SqLitePersistentMySegmentsStorage( + SplitCipherFactory.create("abcdefghijlkmnopqrstuvxyz", true), mRoomDb.mySegmentDao(), MySegmentEntity.creator()); - mPersistentMySegmentsStorage.set(mUserKey, Collections.singletonList("a1,a2,a3,a4")); + mPersistentMySegmentsStorage.set(mUserKey, SegmentsChange.create(IntegrationHelper.asSet("a1", "a2", "a3", "a4"), -1L)); - Set snapshot = new HashSet<>(mPersistentMySegmentsStorage.getSnapshot(mUserKey)); + SegmentsChange snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); - Assert.assertEquals(4, snapshot.size()); - Assert.assertTrue(snapshot.contains("a1")); - Assert.assertTrue(snapshot.contains("a2")); - Assert.assertTrue(snapshot.contains("a3")); - Assert.assertTrue(snapshot.contains("a4")); + List mySegments = snapshot.getNames(); + Assert.assertEquals(4, mySegments.size()); + Assert.assertTrue(mySegments.contains("a1")); + Assert.assertTrue(mySegments.contains("a2")); + Assert.assertTrue(mySegments.contains("a3")); + Assert.assertTrue(mySegments.contains("a4")); + Assert.assertEquals(-1, snapshot.getChangeNumber().longValue()); } @Test public void updateEmptyMySegment() { - mPersistentMySegmentsStorage.set(mUserKey, new ArrayList<>()); + mPersistentMySegmentsStorage.set(mUserKey, SegmentsChange.create(IntegrationHelper.asSet(), 22121L)); - List snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); + SegmentsChange snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); - Assert.assertEquals(0, snapshot.size()); + Assert.assertEquals(0, snapshot.getNames().size()); + Assert.assertEquals(22121, snapshot.getChangeNumber().longValue()); } @Test public void addNullMySegmentsList() { - mPersistentMySegmentsStorage.set(mUserKey, null); + mPersistentMySegmentsStorage.set(mUserKey, SegmentsChange.create(null, -1L)); - List snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); + SegmentsChange snapshot = mPersistentMySegmentsStorage.getSnapshot(mUserKey); - Assert.assertEquals(3, snapshot.size()); + Assert.assertEquals(3, snapshot.getNames().size()); + Assert.assertNull(snapshot.getChangeNumber()); } } diff --git a/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java b/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java index 0c7ce2b24..bc9867946 100644 --- a/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java +++ b/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java @@ -1,6 +1,7 @@ package tests.workmanager; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -26,6 +27,7 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedList; import java.util.concurrent.TimeUnit; import io.split.android.client.ServiceEndpoints; @@ -38,6 +40,9 @@ import io.split.android.client.service.workmanager.ImpressionsRecorderWorker; import io.split.android.client.service.workmanager.MySegmentsSyncWorker; import io.split.android.client.service.workmanager.splits.SplitsSyncWorker; +import io.split.android.client.utils.logger.LogPrinter; +import io.split.android.client.utils.logger.Logger; +import io.split.android.client.utils.logger.SplitLogLevel; public class WorkManagerWrapperTest { @@ -51,7 +56,13 @@ public void setUp() throws Exception { when(mWorkManager.getWorkInfosByTagLiveData(any())).thenReturn(mock(LiveData.class)); - SplitClientConfig splitClientConfig = new SplitClientConfig.Builder() + SplitClientConfig splitClientConfig = buildConfig(true); + + mWrapper = getWrapper(splitClientConfig); + } + + private static @NonNull SplitClientConfig buildConfig(boolean useCertificatePinning) { + SplitClientConfig.Builder configBuilder = new SplitClientConfig.Builder() .serviceEndpoints( ServiceEndpoints.builder() .sseAuthServiceEndpoint("https://test.split.io/serviceEndpoint") @@ -64,23 +75,31 @@ public void setUp() throws Exception { .eventsPerPush(526) .impressionsPerPush(256) .backgroundSyncWhenWifiOnly(true) - .backgroundSyncWhenBatteryNotLow(false) - .certificatePinningConfiguration(CertificatePinningConfiguration.builder() - .addPin("events.split.io", "sha256/sDKdggs") - .addPin("sdk.split.io", "sha256/jIUe51") - .addPin("events.split.io", "sha1/jLeisDf") - .build()) - .build(); + .backgroundSyncWhenBatteryNotLow(false); + + if (useCertificatePinning) { + configBuilder.certificatePinningConfiguration(CertificatePinningConfiguration.builder() + .addPin("events.split.io", "sha256/sDKdggs") + .addPin("sdk.split.io", "sha256/jIUe51") + .addPin("events.split.io", "sha1/jLeisDf") + .build()); + } + + SplitClientConfig config = configBuilder.build(); try { - Method method = splitClientConfig.getClass().getDeclaredMethod("enableTelemetry"); + Method method = config.getClass().getDeclaredMethod("enableTelemetry"); method.setAccessible(true); - method.invoke(splitClientConfig); + method.invoke(config); } catch (Exception exception) { exception.printStackTrace(); } - mWrapper = new WorkManagerWrapper( + return config; + } + + private @NonNull WorkManagerWrapper getWrapper(SplitClientConfig splitClientConfig) { + return new WorkManagerWrapper( mWorkManager, splitClientConfig, "api_key", @@ -97,6 +116,7 @@ public void removeWorkCancelsJobs() { verify(mWorkManager).cancelUniqueWork(SplitTaskType.MY_SEGMENTS_SYNC.toString()); verify(mWorkManager).cancelUniqueWork(SplitTaskType.EVENTS_RECORDER.toString()); verify(mWorkManager).cancelUniqueWork(SplitTaskType.IMPRESSIONS_RECORDER.toString()); + verify(mWorkManager).cancelUniqueWork(SplitTaskType.UNIQUE_KEYS_RECORDER_TASK.toString()); } @Test @@ -109,7 +129,7 @@ public void scheduleWorkSchedulesSplitsJob() { .putBoolean("shouldRecordTelemetry", true) .putStringArray("configuredFilterValues", new String[]{"set_1", "set_2"}) .putString("configuredFilterType", SplitFilter.Type.BY_SET.queryStringField()) - .putString("flagsSpec", "1.1") + .putString("flagsSpec", "1.2") .putString("certificatePins", certificatePinsJson()) .build(); @@ -221,6 +241,44 @@ public void scheduleMySegmentsWorkSchedulesJob() { assertWorkSpecMatches(argumentCaptor.getValue().getWorkSpec(), expectedRequest.getWorkSpec()); } + @Test + public void schedulingWithoutCertificatePinning() { + SplitClientConfig splitClientConfig = buildConfig(false); + LinkedList logs = new LinkedList<>(); + mWrapper = getWrapper(splitClientConfig); + Logger.instance().setLevel(SplitLogLevel.ERROR); + Logger.instance().setPrinter(getLogPrinter(logs)); + + mWrapper.scheduleWork(); + + Data inputData = new Data.Builder() + .putLong("splitCacheExpiration", 864000) + .putString("endpoint", "https://test.split.io/api") + .putBoolean("shouldRecordTelemetry", true) + .putStringArray("configuredFilterValues", new String[]{"set_1", "set_2"}) + .putString("configuredFilterType", SplitFilter.Type.BY_SET.queryStringField()) + .putString("flagsSpec", "1.2") + .build(); + + PeriodicWorkRequest expectedRequest = new PeriodicWorkRequest + .Builder(SplitsSyncWorker.class, 5263, TimeUnit.MINUTES) + .setInputData(buildInputData(inputData)) + .setConstraints(buildConstraints()) + .setInitialDelay(15L, TimeUnit.MINUTES) + .build(); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(PeriodicWorkRequest.class); + + verify(mWorkManager).enqueueUniquePeriodicWork( + eq(SplitTaskType.SPLITS_SYNC.toString()), + eq(ExistingPeriodicWorkPolicy.REPLACE), + argumentCaptor.capture() + ); + + assertTrue(logs.isEmpty()); + assertWorkSpecMatches(argumentCaptor.getValue().getWorkSpec(), expectedRequest.getWorkSpec()); + } + private void assertWorkSpecMatches(WorkSpec workSpec, WorkSpec expectedWorkSpec) { assertEquals(expectedWorkSpec.backoffPolicy, workSpec.backoffPolicy); assertEquals(expectedWorkSpec.backoffDelayDuration, workSpec.backoffDelayDuration); @@ -256,4 +314,39 @@ private Constraints buildConstraints() { private static String certificatePinsJson() { return "{\"events.split.io\":[{\"algo\":\"sha256\",\"pin\":[-80,50,-99,-126,11]},{\"algo\":\"sha1\",\"pin\":[-116,-73,-94,-80,55]}],\"sdk.split.io\":[{\"algo\":\"sha256\",\"pin\":[-116,-123,30,-25]}]}"; } + + + private static @NonNull LogPrinter getLogPrinter(LinkedList logs) { + return new LogPrinter() { + @Override + public void v(String tag, String msg, Throwable tr) { + logs.add("V: " + tag + " - " + msg); + } + + @Override + public void d(String tag, String msg, Throwable tr) { + logs.add("D: " + tag + " - " + msg); + } + + @Override + public void i(String tag, String msg, Throwable tr) { + logs.add("I: " + tag + " - " + msg); + } + + @Override + public void w(String tag, String msg, Throwable tr) { + logs.add("W: " + tag + " - " + msg); + } + + @Override + public void e(String tag, String msg, Throwable tr) { + logs.add("E: " + tag + " - " + msg); + } + + @Override + public void wtf(String tag, String msg, Throwable tr) { + logs.add("!: " + tag + " - " + msg); + } + }; + } } diff --git a/src/main/java/io/split/android/client/SplitClientConfig.java b/src/main/java/io/split/android/client/SplitClientConfig.java index 9e62f31a2..15872b80b 100644 --- a/src/main/java/io/split/android/client/SplitClientConfig.java +++ b/src/main/java/io/split/android/client/SplitClientConfig.java @@ -56,8 +56,7 @@ public class SplitClientConfig { private static final int DEFAULT_BACKGROUND_SYNC_PERIOD_MINUTES = 15; private static final long MIN_IMPRESSIONS_DEDUPE_TIME_INTERVAL = TimeUnit.HOURS.toMillis(1); private static final long MAX_IMPRESSIONS_DEDUPE_TIME_INTERVAL = TimeUnit.HOURS.toMillis(24); - - private final static int DEFAULT_MTK_PER_PUSH = 30000; + private static final int DEFAULT_MTK_PER_PUSH = 30000; // Validation settings private static final int MAXIMUM_KEY_LENGTH = 250; diff --git a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java index ab741197f..0956c1788 100644 --- a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java @@ -73,7 +73,7 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, validationLogger, splitTaskExecutor, mStorageContainer.getPersistentAttributesStorage()); - mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer()); + mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer(), mStorageContainer.getMyLargeSegmentsStorageContainer()); mSplitValidator = new SplitValidatorImpl(); SplitsStorage splitsStorage = mStorageContainer.getSplitsStorage(); mTreatmentManagerFactory = new TreatmentManagerFactoryImpl( diff --git a/src/main/java/io/split/android/client/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index c70047fe8..45dacb73a 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -22,27 +22,29 @@ import io.split.android.client.common.CompressionUtilProvider; import io.split.android.client.events.EventsManagerCoordinator; import io.split.android.client.network.HttpClient; +import io.split.android.client.network.SdkTargetPath; import io.split.android.client.network.SplitHttpHeadersBuilder; import io.split.android.client.service.ServiceFactory; import io.split.android.client.service.SplitApiFacade; import io.split.android.client.service.executor.SplitTaskExecutionListener; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskFactory; +import io.split.android.client.service.http.mysegments.MySegmentsFetcherFactory; import io.split.android.client.service.http.mysegments.MySegmentsFetcherFactoryImpl; import io.split.android.client.service.impressions.strategy.ImpressionStrategyConfig; import io.split.android.client.service.impressions.strategy.ImpressionStrategyProvider; import io.split.android.client.service.impressions.strategy.ProcessStrategy; +import io.split.android.client.service.mysegments.AllSegmentsResponseParser; import io.split.android.client.service.sseclient.EventStreamParser; import io.split.android.client.service.sseclient.ReconnectBackoffCounter; import io.split.android.client.service.sseclient.SseJwtParser; import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; -import io.split.android.client.service.sseclient.notifications.MySegmentsPayloadDecoder; import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder; 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.sseclient.notifications.mysegments.MySegmentsNotificationProcessorFactory; -import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessorFactoryImpl; +import io.split.android.client.service.sseclient.notifications.mysegments.MembershipsNotificationProcessorFactory; +import io.split.android.client.service.sseclient.notifications.mysegments.MembershipsNotificationProcessorFactoryImpl; import io.split.android.client.service.sseclient.reactor.MySegmentsUpdateWorkerRegistry; import io.split.android.client.service.sseclient.reactor.SplitUpdatesWorker; import io.split.android.client.service.sseclient.sseclient.BackoffCounterTimer; @@ -161,6 +163,7 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, return new SplitStorageContainer( StorageFactory.getSplitsStorage(splitRoomDatabase, splitCipher), StorageFactory.getMySegmentsStorage(splitRoomDatabase, splitCipher), + StorageFactory.getMyLargeSegmentsStorage(splitRoomDatabase, splitCipher), StorageFactory.getPersistentSplitsStorage(splitRoomDatabase, splitCipher), StorageFactory.getEventsStorage(persistentEventsStorage, isPersistenceEnabled), persistentEventsStorage, @@ -182,7 +185,8 @@ SplitApiFacade buildApiFacade(SplitClientConfig splitClientConfig, ServiceFactory.getSplitsFetcher(httpClient, splitClientConfig.endpoint(), splitsFilterQueryString), new MySegmentsFetcherFactoryImpl(httpClient, - splitClientConfig.endpoint()), + splitClientConfig.endpoint(), new AllSegmentsResponseParser(), + new MySegmentsUriBuilder(splitClientConfig.endpoint())), ServiceFactory.getSseAuthenticationFetcher(httpClient, splitClientConfig.authServiceUrl()), ServiceFactory.getEventsRecorder(httpClient, @@ -298,14 +302,11 @@ public ClientComponentsRegisterImpl getClientComponentsRegister(SplitClientConfi if (config.persistentAttributesEnabled()) { attributesStorage = storageContainer.getPersistentAttributesStorage(); } - MySegmentsSynchronizerFactory mySegmentsSynchronizerFactory = new MySegmentsSynchronizerFactoryImpl( - new RetryBackoffCounterTimerFactory(), - taskExecutor, - config.segmentsRefreshRate()); + MySegmentsSynchronizerFactory mySegmentsSynchronizerFactory = new MySegmentsSynchronizerFactoryImpl(new RetryBackoffCounterTimerFactory(), taskExecutor); - MySegmentsNotificationProcessorFactory mySegmentsNotificationProcessorFactory = null; + MembershipsNotificationProcessorFactory membershipsNotificationProcessorFactory = null; if (config.syncEnabled()) { - mySegmentsNotificationProcessorFactory = new MySegmentsNotificationProcessorFactoryImpl(notificationParser, + membershipsNotificationProcessorFactory = new MembershipsNotificationProcessorFactoryImpl(notificationParser, taskExecutor, mySegmentsV2PayloadDecoder, compressionProvider); @@ -322,7 +323,7 @@ public ClientComponentsRegisterImpl getClientComponentsRegister(SplitClientConfi eventsManagerCoordinator, sseAuthenticator, notificationProcessor, - mySegmentsNotificationProcessorFactory, + membershipsNotificationProcessorFactory, mySegmentsV2PayloadDecoder); } @@ -343,7 +344,7 @@ public StreamingComponents buildStreamingComponents(@NonNull SplitTaskExecutor s NotificationParser notificationParser = new NotificationParser(); NotificationProcessor notificationProcessor = new NotificationProcessor(splitTaskExecutor, splitTaskFactory, - notificationParser, splitsUpdateNotificationQueue, new MySegmentsPayloadDecoder()); + notificationParser, splitsUpdateNotificationQueue); PushManagerEventBroadcaster pushManagerEventBroadcaster = new PushManagerEventBroadcaster(); @@ -469,4 +470,17 @@ private TelemetryStorage getTelemetryStorage(boolean shouldRecordTelemetry, Tele } return StorageFactory.getTelemetryStorage(shouldRecordTelemetry); } + + static class MySegmentsUriBuilder implements MySegmentsFetcherFactory.UriBuilder { + private final String mEndpoint; + + public MySegmentsUriBuilder(String endpoint) { + mEndpoint = endpoint; + } + + @Override + public URI build(String matchingKey) throws URISyntaxException { + return SdkTargetPath.mySegments(mEndpoint, matchingKey); + } + } } diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index a62861e06..da1db63ff 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -11,9 +11,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import io.split.android.android_client.BuildConfig; import io.split.android.client.api.Key; @@ -328,7 +325,7 @@ public void run() { // Initialize default client client(); - SplitParser mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer()); + SplitParser mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer(), mStorageContainer.getMyLargeSegmentsStorageContainer()); mManager = new SplitManagerImpl( mStorageContainer.getSplitsStorage(), new SplitValidatorImpl(), mSplitParser); diff --git a/src/main/java/io/split/android/client/dtos/AllSegmentsChange.java b/src/main/java/io/split/android/client/dtos/AllSegmentsChange.java new file mode 100644 index 000000000..0be63f89d --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/AllSegmentsChange.java @@ -0,0 +1,53 @@ +package io.split.android.client.dtos; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.google.gson.annotations.SerializedName; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class AllSegmentsChange { + + @SerializedName("ms") + private SegmentsChange mMySegmentsChange; + + @SerializedName("ls") + private SegmentsChange mMyLargeSegmentsChange; + + public AllSegmentsChange() { + + } + + // TODO legacy endpoint support during development + @Deprecated + public AllSegmentsChange(List mySegments) { + Set segments = new HashSet<>(); + for (String name : mySegments) { + Segment segment = new Segment(); + segment.setName(name); + segments.add(segment); + } + mMySegmentsChange = new SegmentsChange(segments, null); + } + + @Nullable + public SegmentsChange getSegmentsChange() { + return mMySegmentsChange; + } + + @Nullable + public SegmentsChange getLargeSegmentsChange() { + return mMyLargeSegmentsChange; + } + + @VisibleForTesting + public static AllSegmentsChange create(SegmentsChange mySegmentsChange, SegmentsChange myLargeSegmentsChange) { + AllSegmentsChange allSegmentsChange = new AllSegmentsChange(); + allSegmentsChange.mMySegmentsChange = mySegmentsChange; + allSegmentsChange.mMyLargeSegmentsChange = myLargeSegmentsChange; + return allSegmentsChange; + } +} diff --git a/src/main/java/io/split/android/client/dtos/Matcher.java b/src/main/java/io/split/android/client/dtos/Matcher.java index 8007065b7..69113a58a 100644 --- a/src/main/java/io/split/android/client/dtos/Matcher.java +++ b/src/main/java/io/split/android/client/dtos/Matcher.java @@ -1,19 +1,34 @@ package io.split.android.client.dtos; +import com.google.gson.annotations.SerializedName; + /** * A leaf class representing a matcher. * */ public class Matcher { + @SerializedName("keySelector") public KeySelector keySelector; + @SerializedName("matcherType") public MatcherType matcherType; + @SerializedName("negate") public boolean negate; + @SerializedName("userDefinedSegmentMatcherData") public UserDefinedSegmentMatcherData userDefinedSegmentMatcherData; + @SerializedName("userDefinedLargeSegmentMatcherData") + public UserDefinedLargeSegmentMatcherData userDefinedLargeSegmentMatcherData; + @SerializedName("whitelistMatcherData") public WhitelistMatcherData whitelistMatcherData; + @SerializedName("unaryNumericMatcherData") public UnaryNumericMatcherData unaryNumericMatcherData; + @SerializedName("betweenMatcherData") public BetweenMatcherData betweenMatcherData; + @SerializedName("dependencyMatcherData") public DependencyMatcherData dependencyMatcherData; + @SerializedName("booleanMatcherData") public Boolean booleanMatcherData; + @SerializedName("stringMatcherData") public String stringMatcherData; + @SerializedName("betweenStringMatcherData") public BetweenStringMatcherData betweenStringMatcherData; } diff --git a/src/main/java/io/split/android/client/dtos/MatcherType.java b/src/main/java/io/split/android/client/dtos/MatcherType.java index fafa56204..580e97f2e 100644 --- a/src/main/java/io/split/android/client/dtos/MatcherType.java +++ b/src/main/java/io/split/android/client/dtos/MatcherType.java @@ -7,6 +7,8 @@ public enum MatcherType { ALL_KEYS, @SerializedName("IN_SEGMENT") IN_SEGMENT, + @SerializedName("IN_LARGE_SEGMENT") + IN_LARGE_SEGMENT, @SerializedName("WHITELIST") WHITELIST, diff --git a/src/main/java/io/split/android/client/dtos/MySegment.java b/src/main/java/io/split/android/client/dtos/MySegment.java deleted file mode 100644 index a1132780a..000000000 --- a/src/main/java/io/split/android/client/dtos/MySegment.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.split.android.client.dtos; - -public class MySegment { - public String id; - public String name; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - MySegment mySegment = (MySegment) o; - - return name.equals(mySegment.name); - } - - @Override - public int hashCode() { - return name.hashCode(); - } -} diff --git a/src/main/java/io/split/android/client/dtos/Segment.java b/src/main/java/io/split/android/client/dtos/Segment.java new file mode 100644 index 000000000..dce92693e --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/Segment.java @@ -0,0 +1,17 @@ +package io.split.android.client.dtos; + +import com.google.gson.annotations.SerializedName; + +public class Segment { + + @SerializedName("n") + private String mName; + + public String getName() { + return mName; + } + + void setName(String name) { + mName = name; + } +} diff --git a/src/main/java/io/split/android/client/dtos/SegmentChange.java b/src/main/java/io/split/android/client/dtos/SegmentChange.java deleted file mode 100644 index 5ec6da727..000000000 --- a/src/main/java/io/split/android/client/dtos/SegmentChange.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.split.android.client.dtos; - -import java.util.List; - -public class SegmentChange { - public String id; - public String name; - public List added; - public List removed; - public long since; - public long till; -} diff --git a/src/main/java/io/split/android/client/dtos/SegmentResponse.java b/src/main/java/io/split/android/client/dtos/SegmentResponse.java new file mode 100644 index 000000000..f5c4b2ab8 --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/SegmentResponse.java @@ -0,0 +1,12 @@ +package io.split.android.client.dtos; + +import androidx.annotation.NonNull; + +import java.util.List; + +public interface SegmentResponse { + @NonNull + List getSegments(); + + long getTill(); +} diff --git a/src/main/java/io/split/android/client/dtos/SegmentsChange.java b/src/main/java/io/split/android/client/dtos/SegmentsChange.java new file mode 100644 index 000000000..d01bf5437 --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/SegmentsChange.java @@ -0,0 +1,67 @@ +package io.split.android.client.dtos; + +import androidx.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SegmentsChange { + @SerializedName("k") + private Set mSegments; + + @SerializedName("cn") + private Long mChangeNumber; + + public SegmentsChange(Set segments, Long changeNumber) { + mSegments = segments; + mChangeNumber = changeNumber; + } + + public Set getSegments() { + return mSegments == null ? Collections.emptySet() : mSegments; + } + + @Nullable + public Long getChangeNumber() { + return mChangeNumber; + } + + public List getNames() { + Set segments = new HashSet<>(getSegments()); + List names = new ArrayList<>(segments.size()); + for (Segment segment : segments) { + names.add(segment.getName()); + } + return names; + } + + public static SegmentsChange createEmpty() { + return new SegmentsChange(Collections.emptySet(), null); + } + + @Nullable + public static SegmentsChange create(Set segments, long changeNumber) { + if (segments == null) { + return null; + } + return create(segments, Long.valueOf(changeNumber)); + } + + public static SegmentsChange create(Set segments, @Nullable Long changeNumber) { + if (segments == null) { + return SegmentsChange.createEmpty(); + } + Set segmentSet = new HashSet<>(); + for (String segment : segments) { + Segment segmentObj = new Segment(); + segmentObj.setName(segment); + segmentSet.add(segmentObj); + } + return new SegmentsChange(segmentSet, changeNumber); + } +} diff --git a/src/main/java/io/split/android/client/dtos/UserDefinedLargeSegmentMatcherData.java b/src/main/java/io/split/android/client/dtos/UserDefinedLargeSegmentMatcherData.java new file mode 100644 index 000000000..058e7611c --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/UserDefinedLargeSegmentMatcherData.java @@ -0,0 +1,8 @@ +package io.split.android.client.dtos; + +import com.google.gson.annotations.SerializedName; + +public class UserDefinedLargeSegmentMatcherData { + @SerializedName("largeSegmentName") + public String largeSegmentName; +} diff --git a/src/main/java/io/split/android/client/dtos/UserDefinedSegmentMatcherData.java b/src/main/java/io/split/android/client/dtos/UserDefinedSegmentMatcherData.java index b1f147cee..4044520f7 100644 --- a/src/main/java/io/split/android/client/dtos/UserDefinedSegmentMatcherData.java +++ b/src/main/java/io/split/android/client/dtos/UserDefinedSegmentMatcherData.java @@ -1,5 +1,8 @@ package io.split.android.client.dtos; +import com.google.gson.annotations.SerializedName; + public class UserDefinedSegmentMatcherData { + @SerializedName("segmentName") public String segmentName; } diff --git a/src/main/java/io/split/android/client/events/SplitEventsManager.java b/src/main/java/io/split/android/client/events/SplitEventsManager.java index 80d269d98..f6150157e 100644 --- a/src/main/java/io/split/android/client/events/SplitEventsManager.java +++ b/src/main/java/io/split/android/client/events/SplitEventsManager.java @@ -28,6 +28,10 @@ public class SplitEventsManager extends BaseEventsManager implements ISplitEvent private final SplitTaskExecutor mSplitTaskExecutor; public SplitEventsManager(SplitClientConfig config, SplitTaskExecutor splitTaskExecutor) { + this(splitTaskExecutor, config.blockUntilReady()); + } + + public SplitEventsManager(SplitTaskExecutor splitTaskExecutor, final int blockUntilReady) { super(); mSplitTaskExecutor = splitTaskExecutor; mSubscriptions = new ConcurrentHashMap<>(); @@ -39,8 +43,8 @@ public SplitEventsManager(SplitClientConfig config, SplitTaskExecutor splitTaskE @Override public void run() { try { - if (config.blockUntilReady() > 0) { - Thread.sleep(config.blockUntilReady()); + if (blockUntilReady > 0) { + Thread.sleep(blockUntilReady); notifyInternalEvent(SplitInternalEvent.SDK_READY_TIMEOUT_REACHED); } } catch (InterruptedException e) { @@ -84,6 +88,7 @@ public void notifyInternalEvent(SplitInternalEvent internalEvent) { // These events were added to handle updated event logic in this component // and also to fix some issues when processing queue that made sdk update // fire on init + if ((internalEvent == SplitInternalEvent.SPLITS_FETCHED || internalEvent == SplitInternalEvent.MY_SEGMENTS_FETCHED) && isTriggered(SplitEvent.SDK_READY)) { @@ -129,6 +134,7 @@ protected void triggerEventsWhenAreAvailable() { switch (event) { case SPLITS_UPDATED: case MY_SEGMENTS_UPDATED: + case MY_LARGE_SEGMENTS_UPDATED: if (isTriggered(SplitEvent.SDK_READY)) { trigger(SplitEvent.SDK_UPDATE); return; @@ -182,7 +188,7 @@ private boolean isTriggered(SplitEvent event) { } private void triggerSdkReadyIfNeeded() { - if ((wasTriggered(SplitInternalEvent.MY_SEGMENTS_UPDATED) || wasTriggered(SplitInternalEvent.MY_SEGMENTS_FETCHED)) && + if ((wasTriggered(SplitInternalEvent.MY_SEGMENTS_UPDATED) || wasTriggered(SplitInternalEvent.MY_SEGMENTS_FETCHED) || wasTriggered(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED)) && (wasTriggered(SplitInternalEvent.SPLITS_UPDATED) || wasTriggered(SplitInternalEvent.SPLITS_FETCHED)) && !isTriggered(SplitEvent.SDK_READY)) { trigger(SplitEvent.SDK_READY); @@ -195,15 +201,17 @@ private void trigger(SplitEvent event) { return; // If executionTimes is grater than zero, maximum executions decrease 1 } else if (mExecutionTimes.get(event) > 0) { - if (event != null) { - Logger.d(event.name() + " event triggered"); - } mExecutionTimes.put(event, mExecutionTimes.get(event) - 1); } //If executionTimes is lower than zero, execute it without limitation + if (event != null) { + Logger.d(event.name() + " event triggered"); + } if (mSubscriptions.containsKey(event)) { List toExecute = mSubscriptions.get(event); - for (SplitEventTask task : toExecute) { - executeTask(event, task); + if (toExecute != null) { + for (SplitEventTask task : toExecute) { + executeTask(event, task); + } } } } diff --git a/src/main/java/io/split/android/client/events/SplitInternalEvent.java b/src/main/java/io/split/android/client/events/SplitInternalEvent.java index 665f871f8..eaa25767d 100644 --- a/src/main/java/io/split/android/client/events/SplitInternalEvent.java +++ b/src/main/java/io/split/android/client/events/SplitInternalEvent.java @@ -15,4 +15,5 @@ public enum SplitInternalEvent { SPLIT_KILLED_NOTIFICATION, ATTRIBUTES_LOADED_FROM_STORAGE, ENCRYPTION_MIGRATION_DONE, + MY_LARGE_SEGMENTS_UPDATED, } diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java b/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java index 7c2c89321..25092466b 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java @@ -66,7 +66,7 @@ public LocalhostSplitFactory(String key, Context context, EventsManagerCoordinator eventsManagerCoordinator = new EventsManagerCoordinator(); FileStorage fileStorage = new FileStorage(context.getCacheDir(), ServiceConstants.LOCALHOST_FOLDER); SplitsStorage splitsStorage = new LocalhostSplitsStorage(mLocalhostFileName, context, fileStorage, eventsManagerCoordinator); - SplitParser splitParser = new SplitParser(new LocalhostMySegmentsStorageContainer()); + SplitParser splitParser = new SplitParser(new LocalhostMySegmentsStorageContainer(), new LocalhostMySegmentsStorageContainer()); SplitTaskExecutorImpl taskExecutor = new SplitTaskExecutorImpl(); AttributesManagerFactory attributesManagerFactory = new AttributesManagerFactoryImpl(new AttributesValidatorImpl(), new ValidationMessageLoggerImpl()); diff --git a/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java b/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java index d722b50c6..b0da1451f 100644 --- a/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java +++ b/src/main/java/io/split/android/client/localhost/shared/LocalhostSplitClientContainerImpl.java @@ -56,7 +56,7 @@ public LocalhostSplitClientContainerImpl(LocalhostSplitFactory splitFactory, @Override protected void createNewClient(Key key) { - SplitEventsManager eventsManager = new SplitEventsManager(mConfig, mSplitTaskExecutor); + SplitEventsManager eventsManager = new SplitEventsManager(mSplitTaskExecutor, mConfig.blockUntilReady()); eventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); eventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); eventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); diff --git a/src/main/java/io/split/android/client/network/SdkTargetPath.java b/src/main/java/io/split/android/client/network/SdkTargetPath.java index c977485dc..ff98fd6b9 100644 --- a/src/main/java/io/split/android/client/network/SdkTargetPath.java +++ b/src/main/java/io/split/android/client/network/SdkTargetPath.java @@ -1,5 +1,7 @@ package io.split.android.client.network; +import androidx.annotation.Nullable; + import java.net.URI; import java.net.URISyntaxException; @@ -7,7 +9,7 @@ public class SdkTargetPath { public static final String SPLIT_CHANGES = "/splitChanges"; - public static final String MY_SEGMENTS = "/mySegments"; + public static final String MEMBERSHIPS = "/memberships"; public static final String EVENTS = "/events/bulk"; public static final String IMPRESSIONS = "/testImpressions/bulk"; public static final String IMPRESSIONS_COUNT = "/testImpressions/count"; @@ -21,8 +23,7 @@ public static URI splitChanges(String baseUrl, String queryString) throws URISyn } public static URI mySegments(String baseUrl, String key) throws URISyntaxException { - String encodedKey = key != null ? UrlEscapers.urlPathSegmentEscaper().escape(key) : null; - return buildUrl(baseUrl, MY_SEGMENTS + "/" + encodedKey); + return buildUrl(baseUrl, MEMBERSHIPS + "/" + getUrlEncodedKey(key)); } public static URI events(String baseUrl) throws URISyntaxException { @@ -73,4 +74,9 @@ private static String removeLastChar(String sourceString) { ? sourceString : (sourceString.substring(0, sourceString.length() - 1)); } + + @Nullable + private static String getUrlEncodedKey(String key) { + return key != null ? UrlEscapers.urlPathSegmentEscaper().escape(key) : null; + } } diff --git a/src/main/java/io/split/android/client/service/ServiceConstants.java b/src/main/java/io/split/android/client/service/ServiceConstants.java index 29663e437..097b043b8 100644 --- a/src/main/java/io/split/android/client/service/ServiceConstants.java +++ b/src/main/java/io/split/android/client/service/ServiceConstants.java @@ -59,4 +59,5 @@ public class ServiceConstants { public static final long DEFAULT_OBSERVER_CACHE_EXPIRATION_PERIOD_MS = TimeUnit.HOURS.toMillis(4); public static final String FLAGS_SPEC_PARAM = "s"; public static final long DEFAULT_IMPRESSIONS_DEDUPE_TIME_INTERVAL = 3600L * 1000L; // 1 hour + public static final int ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES = 10; } diff --git a/src/main/java/io/split/android/client/service/ServiceFactory.java b/src/main/java/io/split/android/client/service/ServiceFactory.java index 4c98eb9a8..9af8f5093 100644 --- a/src/main/java/io/split/android/client/service/ServiceFactory.java +++ b/src/main/java/io/split/android/client/service/ServiceFactory.java @@ -5,9 +5,9 @@ import java.net.URISyntaxException; import java.util.List; +import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.dtos.Event; import io.split.android.client.dtos.KeyImpression; -import io.split.android.client.dtos.MySegment; import io.split.android.client.dtos.SplitChange; import io.split.android.client.network.HttpClient; import io.split.android.client.network.SdkTargetPath; @@ -22,7 +22,7 @@ import io.split.android.client.service.impressions.ImpressionsRequestBodySerializer; import io.split.android.client.service.impressions.unique.MTK; import io.split.android.client.service.impressions.unique.MTKRequestBodySerializer; -import io.split.android.client.service.mysegments.MySegmentsResponseParser; +import io.split.android.client.service.mysegments.AllSegmentsResponseParser; import io.split.android.client.service.splits.SplitChangeResponseParser; import io.split.android.client.service.sseauthentication.SseAuthenticationResponseParser; import io.split.android.client.telemetry.TelemetryConfigBodySerializer; @@ -43,14 +43,14 @@ public static HttpFetcher getSplitsFetcher( new SplitChangeResponseParser()); } - public static HttpFetcher> getMySegmentsFetcher( + public static HttpFetcher getMySegmentsFetcher( HttpClient httpClient, String endPoint, String key) throws URISyntaxException { return new HttpFetcherImpl<>(httpClient, SdkTargetPath.mySegments(endPoint, key), - new MySegmentsResponseParser()); + new AllSegmentsResponseParser()); } public static HttpRecorder> getEventsRecorder( diff --git a/src/main/java/io/split/android/client/service/SplitApiFacade.java b/src/main/java/io/split/android/client/service/SplitApiFacade.java index b0b2251dd..bad8763c6 100644 --- a/src/main/java/io/split/android/client/service/SplitApiFacade.java +++ b/src/main/java/io/split/android/client/service/SplitApiFacade.java @@ -6,9 +6,9 @@ import java.util.List; +import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.dtos.Event; import io.split.android.client.dtos.KeyImpression; -import io.split.android.client.dtos.MySegment; import io.split.android.client.dtos.SplitChange; import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpRecorder; @@ -54,7 +54,7 @@ public HttpFetcher getSplitFetcher() { return mSplitFetcher; } - public HttpFetcher> getMySegmentsFetcher(String matchingKey) { + public HttpFetcher getMySegmentsFetcher(String matchingKey) { return mMySegmentsFetcherFactory.getFetcher(matchingKey); } diff --git a/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java b/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java index 75bcb064d..d380af4e7 100644 --- a/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java +++ b/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java @@ -76,7 +76,7 @@ public SplitTaskExecutionInfo execute() { mTelemetryRuntimeProducer.recordSyncError(OperationType.EVENTS, e.getHttpStatus()); - if (HttpStatus.fromCode(e.getHttpStatus()) == HttpStatus.INTERNAL_NON_RETRYABLE) { + if (HttpStatus.isNotRetryable(e.getHttpStatus())) { doNotRetry = true; break; } 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 d05000bf4..2c167cda0 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 @@ -224,6 +224,7 @@ private TelemetryTaskFactory initializeTelemetryTaskFactory(@NonNull SplitClient splitClientConfig, mSplitsStorageContainer.getSplitsStorage(), mSplitsStorageContainer.getMySegmentsStorageContainer(), + mSplitsStorageContainer.getMyLargeSegmentsStorageContainer(), totalFlagSetCount, invalidFlagSetCount); return mTelemetryTaskFactory; diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskType.java b/src/main/java/io/split/android/client/service/executor/SplitTaskType.java index 6eb182489..601b79b7f 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskType.java +++ b/src/main/java/io/split/android/client/service/executor/SplitTaskType.java @@ -2,10 +2,11 @@ public enum SplitTaskType { SPLITS_SYNC, MY_SEGMENTS_SYNC, EVENTS_RECORDER, IMPRESSIONS_RECORDER, - LOAD_LOCAL_SPLITS, LOAD_LOCAL_MY_SYGMENTS, SSE_AUTHENTICATION_TASK, - MY_SEGMENTS_OVERWRITE, SPLIT_KILL, FILTER_SPLITS_CACHE, GENERIC_TASK, + LOAD_LOCAL_SPLITS, LOAD_LOCAL_MY_SEGMENTS, SSE_AUTHENTICATION_TASK, + SPLIT_KILL, FILTER_SPLITS_CACHE, GENERIC_TASK, CLEAN_UP_DATABASE, IMPRESSIONS_COUNT_RECORDER, SAVE_IMPRESSIONS_COUNT, MY_SEGMENTS_UPDATE, LOAD_LOCAL_ATTRIBUTES, TELEMETRY_CONFIG_TASK, TELEMETRY_STATS_TASK, SAVE_UNIQUE_KEYS_TASK, UNIQUE_KEYS_RECORDER_TASK, + MY_LARGE_SEGMENTS_UPDATE, } diff --git a/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java b/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java index 381a9cc02..38c445713 100644 --- a/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java +++ b/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java @@ -49,6 +49,7 @@ public T execute(@NonNull Map params, uriBuilder.addParameter(param.getKey(), value != null ? value.toString() : ""); } URI builtUri = uriBuilder.build(); + HttpResponse response = mClient.request(builtUri, HttpMethod.GET, null, headers).execute(); if (builtUri != null && response != null) { Logger.v("Received from: " + builtUri + " -> " + response.getData()); diff --git a/src/main/java/io/split/android/client/service/http/HttpStatus.java b/src/main/java/io/split/android/client/service/http/HttpStatus.java index 7660e65b3..2ae1c9a86 100644 --- a/src/main/java/io/split/android/client/service/http/HttpStatus.java +++ b/src/main/java/io/split/android/client/service/http/HttpStatus.java @@ -5,6 +5,7 @@ public enum HttpStatus { URI_TOO_LONG(414, "URI Too Long"), + FORBIDDEN(403, "Forbidden"), INTERNAL_NON_RETRYABLE(9009, "Non retryable"); @@ -41,6 +42,11 @@ public static HttpStatus fromCode(Integer code) { public static boolean isNotRetryable(HttpStatus httpStatus) { // these are values that internally indicate that the request should not be retried return httpStatus == HttpStatus.URI_TOO_LONG || + httpStatus == HttpStatus.FORBIDDEN || httpStatus == HttpStatus.INTERNAL_NON_RETRYABLE; } + + public static boolean isNotRetryable(Integer code) { + return isNotRetryable(fromCode(code)); + } } diff --git a/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactory.java b/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactory.java index cf8a4f28e..8f3309879 100644 --- a/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactory.java +++ b/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactory.java @@ -1,11 +1,17 @@ package io.split.android.client.service.http.mysegments; -import java.util.List; +import java.net.URI; +import java.net.URISyntaxException; -import io.split.android.client.dtos.MySegment; +import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.service.http.HttpFetcher; public interface MySegmentsFetcherFactory { - HttpFetcher> getFetcher(String userKey); + HttpFetcher getFetcher(String userKey); + + interface UriBuilder { + + URI build(String matchingKey) throws URISyntaxException; + } } diff --git a/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImpl.java b/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImpl.java index 60ce2d244..6317cc10b 100644 --- a/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImpl.java @@ -6,37 +6,40 @@ import java.net.URI; import java.net.URISyntaxException; -import java.util.List; -import io.split.android.client.dtos.MySegment; +import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.network.HttpClient; -import io.split.android.client.network.SdkTargetPath; import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpFetcherImpl; -import io.split.android.client.service.mysegments.MySegmentsResponseParser; +import io.split.android.client.service.http.HttpResponseParser; import io.split.android.client.utils.logger.Logger; public class MySegmentsFetcherFactoryImpl implements MySegmentsFetcherFactory { private final String mEndpoint; private final HttpClient mHttpClient; - private final MySegmentsResponseParser mMySegmentsResponseParser; + private final HttpResponseParser mMySegmentsResponseParser; + private final UriBuilder mUriBuilder; public MySegmentsFetcherFactoryImpl(@NonNull HttpClient httpClient, - @NonNull String endpoint) { + @NonNull String endpoint, + @NonNull HttpResponseParser responseParser, + @NonNull UriBuilder uriBuilder) { mHttpClient = checkNotNull(httpClient); mEndpoint = checkNotNull(endpoint); - mMySegmentsResponseParser = new MySegmentsResponseParser(); + mMySegmentsResponseParser = checkNotNull(responseParser); + mUriBuilder = uriBuilder; } @Override - public HttpFetcher> getFetcher(String matchingKey) { + public HttpFetcher getFetcher(String matchingKey) { return new HttpFetcherImpl<>(mHttpClient, buildTargetUrl(matchingKey), mMySegmentsResponseParser); } private URI buildTargetUrl(String matchingKey) { try { - return SdkTargetPath.mySegments(mEndpoint, matchingKey); + return mUriBuilder.build(matchingKey); } catch (URISyntaxException e) { Logger.e(e.getMessage()); } diff --git a/src/main/java/io/split/android/client/service/mysegments/AllSegmentsResponseParser.java b/src/main/java/io/split/android/client/service/mysegments/AllSegmentsResponseParser.java new file mode 100644 index 000000000..445d05e5e --- /dev/null +++ b/src/main/java/io/split/android/client/service/mysegments/AllSegmentsResponseParser.java @@ -0,0 +1,22 @@ +package io.split.android.client.service.mysegments; + +import com.google.gson.JsonSyntaxException; + +import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.service.http.HttpResponseParser; +import io.split.android.client.service.http.HttpResponseParserException; +import io.split.android.client.utils.Json; + +public class AllSegmentsResponseParser implements HttpResponseParser { + + @Override + public AllSegmentsChange parse(String responseData) throws HttpResponseParserException { + try { + return Json.fromJson(responseData, AllSegmentsChange.class); + } catch (JsonSyntaxException e) { + throw new HttpResponseParserException("Syntax error parsing my large segments http response: " + e.getLocalizedMessage()); + } catch (Exception e) { + throw new HttpResponseParserException("Unknown error parsing my large segments http response: " + e.getLocalizedMessage()); + } + } +} diff --git a/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTask.java b/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTask.java index 4084898e3..6965a249f 100644 --- a/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTask.java +++ b/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTask.java @@ -1,5 +1,7 @@ package io.split.android.client.service.mysegments; +import static io.split.android.client.utils.Utils.checkNotNull; + import androidx.annotation.NonNull; import io.split.android.client.service.executor.SplitTask; @@ -7,21 +9,25 @@ import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.storage.mysegments.MySegmentsStorage; -import static io.split.android.client.utils.Utils.checkNotNull; - public class LoadMySegmentsTask implements SplitTask { private final MySegmentsStorage mMySegmentsStorage; + private final MySegmentsStorage mMyLargeSegmentsStorage; + private final SplitTaskType mSplitTaskType; - public LoadMySegmentsTask(@NonNull MySegmentsStorage mySegmentsStorage) { - + public LoadMySegmentsTask(@NonNull MySegmentsStorage mySegmentsStorage, + @NonNull MySegmentsStorage myLargeSegmentsStorage, + @NonNull LoadMySegmentsTaskConfig config) { mMySegmentsStorage = checkNotNull(mySegmentsStorage); + mMyLargeSegmentsStorage = checkNotNull(myLargeSegmentsStorage); + mSplitTaskType = config.getTaskType(); } @Override @NonNull public SplitTaskExecutionInfo execute() { mMySegmentsStorage.loadLocal(); - return SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_MY_SYGMENTS); + mMyLargeSegmentsStorage.loadLocal(); + return SplitTaskExecutionInfo.success(mSplitTaskType); } } diff --git a/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java b/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java new file mode 100644 index 000000000..451d58dad --- /dev/null +++ b/src/main/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfig.java @@ -0,0 +1,22 @@ +package io.split.android.client.service.mysegments; + +import io.split.android.client.service.executor.SplitTaskType; + +public class LoadMySegmentsTaskConfig { + + private static final LoadMySegmentsTaskConfig LOAD_MY_SEGMENTS_TASK_CONFIG = new LoadMySegmentsTaskConfig(SplitTaskType.LOAD_LOCAL_MY_SEGMENTS); + + private final SplitTaskType mTaskType; + + private LoadMySegmentsTaskConfig(SplitTaskType taskType) { + mTaskType = taskType; + } + + public SplitTaskType getTaskType() { + return mTaskType; + } + + public static LoadMySegmentsTaskConfig get() { + return LOAD_MY_SEGMENTS_TASK_CONFIG; + } +} diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentUpdateParams.java b/src/main/java/io/split/android/client/service/mysegments/MySegmentUpdateParams.java new file mode 100644 index 000000000..2372cabeb --- /dev/null +++ b/src/main/java/io/split/android/client/service/mysegments/MySegmentUpdateParams.java @@ -0,0 +1,26 @@ +package io.split.android.client.service.mysegments; + +public class MySegmentUpdateParams { + + private final Long mSyncDelay; + private final Long mTargetSegmentsCn; + private final Long mTargetLargeSegmentsCn; + + public MySegmentUpdateParams(Long syncDelay, Long targetSegmentsCn, Long targetLargeSegmentsCn) { + mSyncDelay = syncDelay; + mTargetSegmentsCn = targetSegmentsCn; + mTargetLargeSegmentsCn = targetLargeSegmentsCn; + } + + public Long getSyncDelay() { + return mSyncDelay; + } + + public Long getTargetSegmentsCn() { + return mTargetSegmentsCn; + } + + public Long getTargetLargeSegmentsCn() { + return mTargetLargeSegmentsCn; + } +} diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsOverwriteTask.java b/src/main/java/io/split/android/client/service/mysegments/MySegmentsOverwriteTask.java deleted file mode 100644 index da18f2f42..000000000 --- a/src/main/java/io/split/android/client/service/mysegments/MySegmentsOverwriteTask.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.split.android.client.service.mysegments; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -import java.util.ArrayList; -import java.util.List; - -import io.split.android.client.events.SplitEventsManager; -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.service.synchronizer.MySegmentsChangeChecker; -import io.split.android.client.storage.mysegments.MySegmentsStorage; -import io.split.android.client.utils.logger.Logger; - -import static io.split.android.client.utils.Utils.checkNotNull; - -public class MySegmentsOverwriteTask implements SplitTask { - - private final List mMySegments; - private final MySegmentsStorage mMySegmentsStorage; - private final SplitEventsManager mEventsManager; - private MySegmentsChangeChecker mMySegmentsChangeChecker; - - public MySegmentsOverwriteTask(@NonNull MySegmentsStorage mySegmentsStorage, - List mySegments, - SplitEventsManager eventsManager) { - mMySegmentsStorage = checkNotNull(mySegmentsStorage); - mMySegments = mySegments; - mEventsManager = eventsManager; - mMySegmentsChangeChecker = new MySegmentsChangeChecker(); - } - - @Override - @NonNull - public SplitTaskExecutionInfo execute() { - try { - if (mMySegments == null) { - logError("My segment list could not be null."); - return SplitTaskExecutionInfo.error(SplitTaskType.MY_SEGMENTS_OVERWRITE); - } - List oldSegments = new ArrayList(mMySegmentsStorage.getAll()); - if(mMySegmentsChangeChecker.mySegmentsHaveChanged(oldSegments, mMySegments)) { - mMySegmentsStorage.set(mMySegments); - mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - } - } catch (Exception e) { - logError("Unknown error while overwriting my segments: " + e.getLocalizedMessage()); - return SplitTaskExecutionInfo.error(SplitTaskType.MY_SEGMENTS_OVERWRITE); - } - Logger.d("My Segments have been overwritten"); - return SplitTaskExecutionInfo.success(SplitTaskType.MY_SEGMENTS_OVERWRITE); - } - - private void logError(String message) { - Logger.e("Error while executing my segments overwrite task: " + message); - } - - @VisibleForTesting - public void setChangesChecker(MySegmentsChangeChecker changesChecker) { - mMySegmentsChangeChecker = changesChecker; - } -} diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsResponseParser.java b/src/main/java/io/split/android/client/service/mysegments/MySegmentsResponseParser.java deleted file mode 100644 index 63725f7f0..000000000 --- a/src/main/java/io/split/android/client/service/mysegments/MySegmentsResponseParser.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.split.android.client.service.mysegments; - -import com.google.gson.JsonSyntaxException; -import com.google.gson.reflect.TypeToken; - -import java.lang.reflect.Type; -import java.util.List; -import java.util.Map; - -import io.split.android.client.dtos.MySegment; -import io.split.android.client.service.http.HttpResponseParser; -import io.split.android.client.service.http.HttpResponseParserException; -import io.split.android.client.utils.Json; - -public class MySegmentsResponseParser implements HttpResponseParser> { - - static final private Type MY_SEGMENTS_RESPONSE_TYPE - = new TypeToken>>() { - }.getType(); - - @Override - public List parse(String responseData) throws HttpResponseParserException { - try { - Map> parsedResponse = Json.fromJson(responseData, MY_SEGMENTS_RESPONSE_TYPE); - return parsedResponse.get("mySegments"); - } catch (JsonSyntaxException e) { - throw new HttpResponseParserException("Syntax error parsing my segments http response: " + e.getLocalizedMessage()); - } catch (Exception e) { - throw new HttpResponseParserException("Unknown error parsing my segments http response: " + e.getLocalizedMessage()); - } - } -} diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java b/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java index cb8e9c07f..2158985aa 100644 --- a/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java +++ b/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTask.java @@ -1,52 +1,116 @@ package io.split.android.client.service.mysegments; +import static io.split.android.client.service.ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_WAIT; import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; -import io.split.android.client.dtos.MySegment; +import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.network.SplitHttpHeadersBuilder; +import io.split.android.client.service.ServiceConstants; 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.service.http.HttpFetcher; import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.http.HttpStatus; +import io.split.android.client.service.sseclient.BackoffCounter; +import io.split.android.client.service.sseclient.ReconnectBackoffCounter; import io.split.android.client.service.synchronizer.MySegmentsChangeChecker; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.telemetry.model.OperationType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; +import io.split.android.client.utils.Utils; import io.split.android.client.utils.logger.Logger; public class MySegmentsSyncTask implements SplitTask { - private final HttpFetcher> mMySegmentsFetcher; + private static final String TILL_PARAM = "till"; + + private final HttpFetcher mMySegmentsFetcher; + private final MySegmentsStorage mMySegmentsStorage; - private final boolean mAvoidCache; + private final MySegmentsStorage mMyLargeSegmentsStorage; private final SplitEventsManager mEventsManager; private final MySegmentsChangeChecker mMySegmentsChangeChecker; private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; + private final BackoffCounter mBackoffCounter; + + private final SplitTaskType mTaskType; + private final SplitInternalEvent mUpdateEvent; + private final SplitInternalEvent mFetchedEvent; + private final OperationType mTelemetryOperationType; + + private final boolean mAvoidCache; + @Nullable + private final Long mTargetSegmentsChangeNumber; + @Nullable + private final Long mTargetLargeSegmentsChangeNumber; + private final int mOnDemandFetchBackoffMaxRetries; + + public MySegmentsSyncTask(@NonNull HttpFetcher mySegmentsFetcher, + @NonNull MySegmentsStorage mySegmentsStorage, + @NonNull MySegmentsStorage myLargeSegmentsStorage, + boolean avoidCache, + SplitEventsManager eventsManager, + @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, + @NonNull MySegmentsSyncTaskConfig config, + @Nullable Long targetSegmentsChangeNumber, + @Nullable Long targetLargeSegmentsChangeNumber) { + this(mySegmentsFetcher, + mySegmentsStorage, + myLargeSegmentsStorage, + avoidCache, + eventsManager, + new MySegmentsChangeChecker(), + telemetryRuntimeProducer, + config, + targetSegmentsChangeNumber, + targetLargeSegmentsChangeNumber, + new ReconnectBackoffCounter(1, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT), + ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + } - public MySegmentsSyncTask(@NonNull HttpFetcher> mySegmentsFetcher, + @VisibleForTesting + public MySegmentsSyncTask(@NonNull HttpFetcher mySegmentsFetcher, @NonNull MySegmentsStorage mySegmentsStorage, + @NonNull MySegmentsStorage myLargeSegmentsStorage, boolean avoidCache, SplitEventsManager eventsManager, - @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { + MySegmentsChangeChecker mySegmentsChangeChecker, + @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, + @NonNull MySegmentsSyncTaskConfig config, + @Nullable Long targetSegmentsChangeNumber, + @Nullable Long targetLargeSegmentsChangeNumber, + BackoffCounter backoffCounter, + int onDemandFetchBackoffMaxRetries) { mMySegmentsFetcher = checkNotNull(mySegmentsFetcher); mMySegmentsStorage = checkNotNull(mySegmentsStorage); + mMyLargeSegmentsStorage = checkNotNull(myLargeSegmentsStorage); mAvoidCache = avoidCache; mEventsManager = eventsManager; - mMySegmentsChangeChecker = new MySegmentsChangeChecker(); + mMySegmentsChangeChecker = mySegmentsChangeChecker; mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + mTaskType = config.getTaskType(); + mUpdateEvent = config.getUpdateEvent(); + mFetchedEvent = config.getFetchedEvent(); + mTelemetryOperationType = config.getTelemetryOperationType(); + mTargetSegmentsChangeNumber = targetSegmentsChangeNumber; + mTargetLargeSegmentsChangeNumber = targetLargeSegmentsChangeNumber; + mBackoffCounter = backoffCounter; + mOnDemandFetchBackoffMaxRetries = onDemandFetchBackoffMaxRetries; } @Override @@ -55,46 +119,138 @@ public SplitTaskExecutionInfo execute() { long startTime = System.currentTimeMillis(); long latency = 0; try { - List segments = mMySegmentsFetcher.execute(new HashMap<>(), getHeaders()); + // if target change number is outdated, we don't need to fetch + if (targetChangeNumberIsOutdated()) { + Logger.v("Target CN is outdated. Skipping membership fetch"); + return SplitTaskExecutionInfo.success(mTaskType); + } + + fetch(mOnDemandFetchBackoffMaxRetries); long now = System.currentTimeMillis(); latency = now - startTime; - List oldSegments = new ArrayList<>(mMySegmentsStorage.getAll()); - List mySegments = getNameList(segments); - mMySegmentsStorage.set(mySegments); - mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.MY_SEGMENT, now); - fireMySegmentsUpdatedIfNeeded(oldSegments, mySegments); + mTelemetryRuntimeProducer.recordSuccessfulSync(mTelemetryOperationType, now); } catch (HttpFetcherException e) { - logError("Network error while retrieving my segments: " + e.getLocalizedMessage()); - mTelemetryRuntimeProducer.recordSyncError(OperationType.MY_SEGMENT, e.getHttpStatus()); + logError("Network error while retrieving memberships: " + e.getLocalizedMessage()); + mTelemetryRuntimeProducer.recordSyncError(mTelemetryOperationType, e.getHttpStatus()); if (HttpStatus.isNotRetryable(HttpStatus.fromCode(e.getHttpStatus()))) { - return SplitTaskExecutionInfo.error(SplitTaskType.MY_SEGMENTS_SYNC, + return SplitTaskExecutionInfo.error(mTaskType, Collections.singletonMap(SplitTaskExecutionInfo.DO_NOT_RETRY, true)); } - return SplitTaskExecutionInfo.error(SplitTaskType.MY_SEGMENTS_SYNC); + return SplitTaskExecutionInfo.error(mTaskType); } catch (Exception e) { - logError("Unknown error while retrieving my segments: " + e.getLocalizedMessage()); - return SplitTaskExecutionInfo.error(SplitTaskType.MY_SEGMENTS_SYNC); + logError("Unknown error while retrieving memberships: " + e.getLocalizedMessage()); + return SplitTaskExecutionInfo.error(mTaskType); } finally { - mTelemetryRuntimeProducer.recordSyncLatency(OperationType.MY_SEGMENT, latency); + mTelemetryRuntimeProducer.recordSyncLatency(mTelemetryOperationType, latency); } Logger.d("My Segments have been updated"); - return SplitTaskExecutionInfo.success(SplitTaskType.MY_SEGMENTS_SYNC); + return SplitTaskExecutionInfo.success(mTaskType); } - private void logError(String message) { - Logger.e("Error while executing my segments sync task: " + message); + private boolean targetChangeNumberIsOutdated() { + // In case both targets are present, both CN in storage should be newer for the targets to be considered outdated + if (mTargetSegmentsChangeNumber != null && mTargetLargeSegmentsChangeNumber != null) { + return isTargetOutdated(mTargetSegmentsChangeNumber, mMySegmentsStorage.getChangeNumber()) && + isTargetOutdated(mTargetLargeSegmentsChangeNumber, mMyLargeSegmentsStorage.getChangeNumber()); + } + + // If only LS target is set, there's no need to check MS storage CN + if (mTargetLargeSegmentsChangeNumber != null) { + return isTargetOutdated(mTargetLargeSegmentsChangeNumber, mMyLargeSegmentsStorage.getChangeNumber()); + } + + // If only MS target is set, there's no need to check LS storage CN + if (mTargetSegmentsChangeNumber != null) { + return isTargetOutdated(mTargetSegmentsChangeNumber, mMySegmentsStorage.getChangeNumber()); + } + + // If no targets are set, consider it not outdated + return false; + } + + private boolean isTargetOutdated(@Nullable Long targetChangeNumber, long storageChangeNumber) { + long target = Utils.getOrDefault(targetChangeNumber, -1L); + return target < storageChangeNumber; } - private List getNameList(List mySegments) { - List nameList = new ArrayList(); - for (MySegment segment : mySegments) { - nameList.add(segment.name); + private void fetch(int initialRetries) throws HttpFetcherException, InterruptedException { + int remainingRetries = initialRetries; + mBackoffCounter.resetCounter(); + while (remainingRetries > 0) { + AllSegmentsChange response = mMySegmentsFetcher.execute(getParams(false), getHeaders()); + if (response == null) { + throw new HttpFetcherException("", "Response is null"); + } + + if (isStaleResponse(response)) { + Logger.d("Retrying memberships fetch due to change number mismatch"); + long waitMillis = TimeUnit.SECONDS.toMillis(mBackoffCounter.getNextRetryTime()); + Thread.sleep(waitMillis); + remainingRetries--; + } else { + updateStorage(response); + return; + } } - return nameList; + + AllSegmentsChange response = mMySegmentsFetcher.execute(getParams(true), getHeaders()); + if (response == null) { + throw new HttpFetcherException("", "Response is null"); + } + + updateStorage(response); + } + + private Map getParams(boolean addTill) { + Map params = new HashMap<>(); + if (addTill) { + params.put(TILL_PARAM, Math.max( + Utils.getOrDefault(mTargetSegmentsChangeNumber, -1L), + Utils.getOrDefault(mTargetLargeSegmentsChangeNumber, -1L))); + } + + return params; + } + + private boolean isStaleResponse(@NonNull AllSegmentsChange response) { + boolean segmentsTargetMatched = targetMatched(mTargetSegmentsChangeNumber, response.getSegmentsChange()); + boolean largeSegmentsTargetMatched = targetMatched(mTargetLargeSegmentsChangeNumber, response.getLargeSegmentsChange()); + + return !segmentsTargetMatched || !largeSegmentsTargetMatched; + } + + private boolean targetMatched(@Nullable Long targetChangeNumber, SegmentsChange change) { + Long target = Utils.getOrDefault(targetChangeNumber, -1L); + return target == -1 || + change == null || + change.getChangeNumber() == null || + change.getChangeNumber() != null && target <= change.getChangeNumber(); + } + + private void updateStorage(AllSegmentsChange response) { + UpdateSegmentsResult segmentsResult = updateSegments(response.getSegmentsChange(), mMySegmentsStorage); + UpdateSegmentsResult largeSegmentsResult = updateSegments(response.getLargeSegmentsChange(), mMyLargeSegmentsStorage); + fireMySegmentsUpdatedIfNeeded(segmentsResult, largeSegmentsResult); + } + + @NonNull + private static UpdateSegmentsResult updateSegments(SegmentsChange segmentsChange, MySegmentsStorage storage) { + List oldSegments = new ArrayList<>(); + List mySegments = new ArrayList<>(); + if (segmentsChange != null) { + oldSegments = new ArrayList<>(storage.getAll()); + mySegments = segmentsChange.getNames(); + storage.set(segmentsChange); + } + return new UpdateSegmentsResult(oldSegments, mySegments); + } + + private void logError(String message) { + Logger.e("Error while executing memberships sync task: " + message); } private @Nullable Map getHeaders() { @@ -104,18 +260,43 @@ private List getNameList(List mySegments) { return null; } - private void fireMySegmentsUpdatedIfNeeded(List oldSegments, List newSegments) { + private void fireMySegmentsUpdatedIfNeeded(UpdateSegmentsResult segmentsResult, UpdateSegmentsResult largeSegmentsResult) { if (mEventsManager == null) { return; } - mEventsManager.notifyInternalEvent(getInternalEvent(oldSegments, newSegments)); + + // MY_SEGMENTS_UPDATED event when segments have changed + boolean segmentsHaveChanged = mMySegmentsChangeChecker.mySegmentsHaveChanged(segmentsResult.oldSegments, segmentsResult.newSegments); + boolean largeSegmentsHaveChanged = mMySegmentsChangeChecker.mySegmentsHaveChanged(largeSegmentsResult.oldSegments, largeSegmentsResult.newSegments); + + if (segmentsHaveChanged) { + Logger.v("New segments: " + segmentsResult.newSegments); + } + + if (largeSegmentsHaveChanged) { + Logger.v("New large segments: " + largeSegmentsResult.newSegments); + } + + if (segmentsHaveChanged) { + mEventsManager.notifyInternalEvent(mUpdateEvent); + } else { + // MY_LARGE_SEGMENTS_UPDATED event when large segments have changed + if (largeSegmentsHaveChanged) { + mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + } else { + // otherwise, MY_SEGMENTS_FETCHED event + mEventsManager.notifyInternalEvent(mFetchedEvent); + } + } } - private SplitInternalEvent getInternalEvent(List oldSegments, List newSegments) { - boolean haveChanged = mMySegmentsChangeChecker.mySegmentsHaveChanged(oldSegments, newSegments); - if (haveChanged) { - return SplitInternalEvent.MY_SEGMENTS_UPDATED; + private static class UpdateSegmentsResult { + public final List oldSegments; + public final List newSegments; + + private UpdateSegmentsResult(List oldSegments, List newSegments) { + this.oldSegments = oldSegments; + this.newSegments = newSegments; } - return SplitInternalEvent.MY_SEGMENTS_FETCHED; } } diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java b/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java new file mode 100644 index 000000000..210fb2d4e --- /dev/null +++ b/src/main/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfig.java @@ -0,0 +1,51 @@ +package io.split.android.client.service.mysegments; + +import androidx.annotation.NonNull; + +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.telemetry.model.OperationType; + +public class MySegmentsSyncTaskConfig { + + private static final MySegmentsSyncTaskConfig MY_SEGMENTS_TASK_CONFIG = new MySegmentsSyncTaskConfig( + SplitTaskType.MY_SEGMENTS_SYNC, + SplitInternalEvent.MY_SEGMENTS_UPDATED, + SplitInternalEvent.MY_SEGMENTS_FETCHED, + OperationType.MY_SEGMENT); + private final SplitTaskType mTaskType; + private final SplitInternalEvent mUpdateEvent; + private final SplitInternalEvent mFetchedEvent; + private final OperationType mTelemetryOperationType; + + private MySegmentsSyncTaskConfig(@NonNull SplitTaskType taskType, + @NonNull SplitInternalEvent updateEvent, + @NonNull SplitInternalEvent fetchedEvent, + @NonNull OperationType telemetryOperationType) { + mTaskType = taskType; + mUpdateEvent = updateEvent; + mFetchedEvent = fetchedEvent; + mTelemetryOperationType = telemetryOperationType; + } + + SplitTaskType getTaskType() { + return mTaskType; + } + + SplitInternalEvent getUpdateEvent() { + return mUpdateEvent; + } + + SplitInternalEvent getFetchedEvent() { + return mFetchedEvent; + } + + OperationType getTelemetryOperationType() { + return mTelemetryOperationType; + } + + @NonNull + public static MySegmentsSyncTaskConfig get() { + return MY_SEGMENTS_TASK_CONFIG; + } +} diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactory.java b/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactory.java index 72576bc0f..37ab55373 100644 --- a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactory.java +++ b/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactory.java @@ -1,14 +1,14 @@ package io.split.android.client.service.mysegments; -import java.util.List; +import java.util.Set; public interface MySegmentsTaskFactory { - MySegmentsSyncTask createMySegmentsSyncTask(boolean avoidCache); + MySegmentsSyncTask createMySegmentsSyncTask(boolean avoidCache, Long targetSegmentsCn, Long targetLargeSegmentsCn); LoadMySegmentsTask createLoadMySegmentsTask(); - MySegmentsOverwriteTask createMySegmentsOverwriteTask(List segments); + MySegmentsUpdateTask createMySegmentsUpdateTask(boolean add, Set segmentNames, Long changeNumber); - MySegmentsUpdateTask createMySegmentsUpdateTask(boolean add, String segmentName); + MySegmentsUpdateTask createMyLargeSegmentsUpdateTask(boolean add, Set segmentNames, Long changeNumber); } diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfiguration.java b/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfiguration.java index 94884b4c4..9cf4a9147 100644 --- a/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfiguration.java +++ b/src/main/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfiguration.java @@ -4,39 +4,91 @@ import androidx.annotation.NonNull; -import java.util.List; - -import io.split.android.client.dtos.MySegment; +import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.storage.mysegments.MySegmentsStorage; public class MySegmentsTaskFactoryConfiguration { - private final HttpFetcher> mHttpFetcher; - private final MySegmentsStorage mStorage; + private final HttpFetcher mHttpFetcher; + private final MySegmentsStorage mMySegmentsStorage; private final SplitEventsManager mEventsManager; + private final MySegmentsSyncTaskConfig mMySegmentsSyncTaskConfig; + private final MySegmentsUpdateTaskConfig mMySegmentsUpdateTaskConfig; + private final MySegmentsUpdateTaskConfig mMyLargeSegmentsUpdateTaskConfig; + private final LoadMySegmentsTaskConfig mLoadMySegmentsTaskConfig; + private final MySegmentsStorage mMyLargeSegmentsStorage; - public MySegmentsTaskFactoryConfiguration(@NonNull HttpFetcher> httpFetcher, - @NonNull MySegmentsStorage storage, - @NonNull SplitEventsManager eventsManager) { + private MySegmentsTaskFactoryConfiguration(@NonNull HttpFetcher httpFetcher, + @NonNull MySegmentsStorage storage, + @NonNull MySegmentsStorage myLargeSegmentsStorage, + @NonNull SplitEventsManager eventsManager, + @NonNull MySegmentsSyncTaskConfig mySegmentsSyncTaskConfig, + @NonNull MySegmentsUpdateTaskConfig mySegmentsUpdateTaskConfig, + @NonNull MySegmentsUpdateTaskConfig myLargeSegmentsUpdateTaskConfig, + @NonNull LoadMySegmentsTaskConfig loadMySegmentsTaskConfig) { mHttpFetcher = checkNotNull(httpFetcher); - mStorage = checkNotNull(storage); + mMySegmentsStorage = checkNotNull(storage); + mMyLargeSegmentsStorage = checkNotNull(myLargeSegmentsStorage); mEventsManager = checkNotNull(eventsManager); + mMySegmentsSyncTaskConfig = checkNotNull(mySegmentsSyncTaskConfig); + mMySegmentsUpdateTaskConfig = checkNotNull(mySegmentsUpdateTaskConfig); + mMyLargeSegmentsUpdateTaskConfig = checkNotNull(myLargeSegmentsUpdateTaskConfig); + mLoadMySegmentsTaskConfig = checkNotNull(loadMySegmentsTaskConfig); } @NonNull - public HttpFetcher> getHttpFetcher() { + public HttpFetcher getHttpFetcher() { return mHttpFetcher; } @NonNull - public MySegmentsStorage getStorage() { - return mStorage; + public MySegmentsStorage getMySegmentsStorage() { + return mMySegmentsStorage; + } + + @NonNull + public MySegmentsStorage getMyLargeSegmentsStorage() { + return mMyLargeSegmentsStorage; } @NonNull public SplitEventsManager getEventsManager() { return mEventsManager; } + + @NonNull + public MySegmentsSyncTaskConfig getMySegmentsSyncTaskConfig() { + return mMySegmentsSyncTaskConfig; + } + + @NonNull + public MySegmentsUpdateTaskConfig getMySegmentsUpdateTaskConfig() { + return mMySegmentsUpdateTaskConfig; + } + + @NonNull + public MySegmentsUpdateTaskConfig getMyLargeSegmentsUpdateTaskConfig() { + return mMyLargeSegmentsUpdateTaskConfig; + } + + @NonNull + public LoadMySegmentsTaskConfig getLoadMySegmentsTaskConfig() { + return mLoadMySegmentsTaskConfig; + } + + public static MySegmentsTaskFactoryConfiguration get(@NonNull HttpFetcher httpFetcher, + @NonNull MySegmentsStorage mySegmentsStorage, + @NonNull MySegmentsStorage myLargeSegmentsStorage, + @NonNull SplitEventsManager eventsManager) { + return new MySegmentsTaskFactoryConfiguration(httpFetcher, + mySegmentsStorage, + myLargeSegmentsStorage, + eventsManager, + MySegmentsSyncTaskConfig.get(), + MySegmentsUpdateTaskConfig.getForMySegments(), + MySegmentsUpdateTaskConfig.getForMyLargeSegments(), + LoadMySegmentsTaskConfig.get()); + } } 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 934139cf0..93615ccd8 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 @@ -4,7 +4,7 @@ import androidx.annotation.NonNull; -import java.util.List; +import java.util.Set; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; @@ -20,26 +20,30 @@ public MySegmentsTaskFactoryImpl(@NonNull MySegmentsTaskFactoryConfiguration con } @Override - public MySegmentsSyncTask createMySegmentsSyncTask(boolean avoidCache) { + public MySegmentsSyncTask createMySegmentsSyncTask(boolean avoidCache, Long targetSegmentsCn, Long targetLargeSegmentsCn) { return new MySegmentsSyncTask(mConfiguration.getHttpFetcher(), - mConfiguration.getStorage(), + mConfiguration.getMySegmentsStorage(), + mConfiguration.getMyLargeSegmentsStorage(), avoidCache, mConfiguration.getEventsManager(), - mTelemetryRuntimeProducer); + mTelemetryRuntimeProducer, + mConfiguration.getMySegmentsSyncTaskConfig(), + targetSegmentsCn, + targetLargeSegmentsCn); } @Override public LoadMySegmentsTask createLoadMySegmentsTask() { - return new LoadMySegmentsTask(mConfiguration.getStorage()); + return new LoadMySegmentsTask(mConfiguration.getMySegmentsStorage(), mConfiguration.getMyLargeSegmentsStorage(), mConfiguration.getLoadMySegmentsTaskConfig()); } @Override - public MySegmentsOverwriteTask createMySegmentsOverwriteTask(List segments) { - return new MySegmentsOverwriteTask(mConfiguration.getStorage(), segments, mConfiguration.getEventsManager()); + public MySegmentsUpdateTask createMySegmentsUpdateTask(boolean add, Set segmentNames, Long changeNumber) { + return new MySegmentsUpdateTask(mConfiguration.getMySegmentsStorage(), add, segmentNames, changeNumber, mConfiguration.getEventsManager(), mTelemetryRuntimeProducer, mConfiguration.getMySegmentsUpdateTaskConfig()); } @Override - public MySegmentsUpdateTask createMySegmentsUpdateTask(boolean add, String segmentName) { - return new MySegmentsUpdateTask(mConfiguration.getStorage(), add, segmentName, mConfiguration.getEventsManager(), mTelemetryRuntimeProducer); + public MySegmentsUpdateTask createMyLargeSegmentsUpdateTask(boolean add, Set segmentNames, Long changeNumber) { + return new MySegmentsUpdateTask(mConfiguration.getMyLargeSegmentsStorage(), add, segmentNames, changeNumber, mConfiguration.getEventsManager(), mTelemetryRuntimeProducer, mConfiguration.getMyLargeSegmentsUpdateTaskConfig()); } } 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 3f7cbc3a1..3f8dc1260 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 @@ -1,10 +1,12 @@ package io.split.android.client.service.mysegments; +import static io.split.android.client.utils.Utils.checkNotNull; + import androidx.annotation.NonNull; -import java.util.ArrayList; import java.util.Set; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.service.executor.SplitTask; @@ -15,26 +17,34 @@ import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; import io.split.android.client.utils.logger.Logger; -import static io.split.android.client.utils.Utils.checkNotNull; - public class MySegmentsUpdateTask implements SplitTask { - private final String mSegmentName; + private final Set mSegmentNames; + private final Long mChangeNumber; private final MySegmentsStorage mMySegmentsStorage; private final SplitEventsManager mEventsManager; private final boolean mIsAddOperation; private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; + private final SplitTaskType mTaskType; + private final SplitInternalEvent mUpdateEvent; + private final UpdatesFromSSEEnum mTelemetrySSEKey; public MySegmentsUpdateTask(@NonNull MySegmentsStorage mySegmentsStorage, boolean add, - @NonNull String segmentName, + @NonNull Set segmentName, + Long changeNumber, @NonNull SplitEventsManager eventsManager, - @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { + @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, + @NonNull MySegmentsUpdateTaskConfig config) { mMySegmentsStorage = checkNotNull(mySegmentsStorage); - mSegmentName = checkNotNull(segmentName); + mSegmentNames = checkNotNull(segmentName); + mChangeNumber = changeNumber == null ? -1 : changeNumber; mIsAddOperation = add; mEventsManager = checkNotNull(eventsManager); mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + mTaskType = config.getTaskType(); + mUpdateEvent = config.getUpdateEvent(); + mTelemetrySSEKey = config.getTelemetrySSEKey(); } @Override @@ -49,40 +59,51 @@ public SplitTaskExecutionInfo execute() { private SplitTaskExecutionInfo add() { try { Set segments = mMySegmentsStorage.getAll(); - if (!segments.contains(mSegmentName)) { - segments.add(mSegmentName); + boolean updateAndNotify = false; + for (String segment : mSegmentNames) { + if (!segments.contains(segment)) { + updateAndNotify = true; + segments.add(segment); + } + } + + if (updateAndNotify) { updateAndNotify(segments); } - mTelemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.MY_SEGMENTS); + mTelemetryRuntimeProducer.recordUpdatesFromSSE(mTelemetrySSEKey); } catch (Exception e) { - logError("Unknown error while adding segment " + mSegmentName + ": " + e.getLocalizedMessage()); - return SplitTaskExecutionInfo.error(SplitTaskType.MY_SEGMENTS_UPDATE); + logError("Unknown error while adding segment " + getSegmentNames() + ": " + e.getLocalizedMessage()); + return SplitTaskExecutionInfo.error(mTaskType); } - Logger.d("My Segments have been updated. Added " + mSegmentName); - return SplitTaskExecutionInfo.success(SplitTaskType.MY_SEGMENTS_UPDATE); + Logger.d("My Segments have been updated. Added " + getSegmentNames()); + return SplitTaskExecutionInfo.success(mTaskType); } public SplitTaskExecutionInfo remove() { try { Set segments = mMySegmentsStorage.getAll(); - if(segments.remove(mSegmentName)) { + if (segments.removeAll(mSegmentNames)) { updateAndNotify(segments); } - mTelemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.MY_SEGMENTS); + mTelemetryRuntimeProducer.recordUpdatesFromSSE(mTelemetrySSEKey); } catch (Exception e) { - logError("Unknown error while removing segment " + mSegmentName + ": " + e.getLocalizedMessage()); - return SplitTaskExecutionInfo.error(SplitTaskType.MY_SEGMENTS_UPDATE); + logError("Unknown error while removing segment " + getSegmentNames() + ": " + e.getLocalizedMessage()); + return SplitTaskExecutionInfo.error(mTaskType); } - Logger.d("My Segments have been updated. Removed " + mSegmentName); - return SplitTaskExecutionInfo.success(SplitTaskType.MY_SEGMENTS_UPDATE); + Logger.d("My Segments have been updated. Removed " + getSegmentNames()); + return SplitTaskExecutionInfo.success(mTaskType); } private void updateAndNotify(Set segments) { - mMySegmentsStorage.set(new ArrayList<>(segments)); - mEventsManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + mMySegmentsStorage.set(SegmentsChange.create(segments, mChangeNumber)); + mEventsManager.notifyInternalEvent(mUpdateEvent); } private void logError(String message) { Logger.e("Error while executing my segments removal task: " + message); } + + private String getSegmentNames() { + return String.join(",", mSegmentNames); + } } diff --git a/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java b/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java new file mode 100644 index 000000000..5a3caf515 --- /dev/null +++ b/src/main/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfig.java @@ -0,0 +1,51 @@ +package io.split.android.client.service.mysegments; + +import androidx.annotation.NonNull; + +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum; + +public class MySegmentsUpdateTaskConfig { + + private static final MySegmentsUpdateTaskConfig MY_SEGMENTS_UPDATE_TASK_CONFIG = new MySegmentsUpdateTaskConfig(SplitTaskType.MY_SEGMENTS_UPDATE, + SplitInternalEvent.MY_SEGMENTS_UPDATED, + UpdatesFromSSEEnum.MY_SEGMENTS); + private static final MySegmentsUpdateTaskConfig MY_LARGE_SEGMENTS_UPDATE_TASK_CONFIG = new MySegmentsUpdateTaskConfig(SplitTaskType.MY_LARGE_SEGMENTS_UPDATE, + SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED, + UpdatesFromSSEEnum.MY_LARGE_SEGMENTS); + + private final SplitTaskType mTaskType; + private final SplitInternalEvent mUpdateEvent; + private final UpdatesFromSSEEnum mTelemetrySSEKey; + + private MySegmentsUpdateTaskConfig(@NonNull SplitTaskType taskType, + @NonNull SplitInternalEvent updateEvent, + @NonNull UpdatesFromSSEEnum telemetrySSEKey) { + mTaskType = taskType; + mUpdateEvent = updateEvent; + mTelemetrySSEKey = telemetrySSEKey; + } + + public SplitTaskType getTaskType() { + return mTaskType; + } + + public SplitInternalEvent getUpdateEvent() { + return mUpdateEvent; + } + + public UpdatesFromSSEEnum getTelemetrySSEKey() { + return mTelemetrySSEKey; + } + + @NonNull + public static MySegmentsUpdateTaskConfig getForMySegments() { + return MY_SEGMENTS_UPDATE_TASK_CONFIG; + } + + @NonNull + public static MySegmentsUpdateTaskConfig getForMyLargeSegments() { + return MY_LARGE_SEGMENTS_UPDATE_TASK_CONFIG; + } +} 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 6ca3a5c05..2c4fe4545 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 @@ -74,7 +74,7 @@ private ProcessedSplitChange buildProcessedSplitChange(List featureFlags, processStrategy.process(activeFeatureFlags, archivedFeatureFlags, featureFlag); } - return new ProcessedSplitChange(activeFeatureFlags, archivedFeatureFlags, changeNumber, System.currentTimeMillis() / 100); + return new ProcessedSplitChange(activeFeatureFlags, archivedFeatureFlags, changeNumber, System.currentTimeMillis()); } private FeatureFlagProcessStrategy getProcessStrategy(SplitFilter splitFilter) { 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 9cf2b8500..f65adb969 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 @@ -31,7 +31,6 @@ public class SplitsSyncHelper { private static final String SINCE_PARAM = "since"; private static final String TILL_PARAM = "till"; - private static final int ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES = 10; private static final int ON_DEMAND_FETCH_BACKOFF_MAX_WAIT = ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_WAIT; private final HttpFetcher mSplitFetcher; @@ -69,20 +68,20 @@ public SplitsSyncHelper(@NonNull HttpFetcher splitFetcher, mFlagsSpec = flagsSpec; } - public SplitTaskExecutionInfo sync(long till) { - return sync(till, false, true, false); + public SplitTaskExecutionInfo sync(long till, int onDemandFetchBackoffMaxRetries) { + return sync(till, false, true, false, onDemandFetchBackoffMaxRetries); } - public SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolean resetChangeNumber) { - return sync(till, clearBeforeUpdate, false, resetChangeNumber); + public SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) { + return sync(till, clearBeforeUpdate, false, resetChangeNumber, onDemandFetchBackoffMaxRetries); } - private SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolean avoidCache, boolean resetChangeNumber) { + private SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolean avoidCache, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) { try { - boolean successfulSync = attemptSplitSync(till, clearBeforeUpdate, avoidCache, false, resetChangeNumber); + boolean successfulSync = attemptSplitSync(till, clearBeforeUpdate, avoidCache, false, resetChangeNumber, onDemandFetchBackoffMaxRetries); if (!successfulSync) { - attemptSplitSync(till, clearBeforeUpdate, avoidCache, true, resetChangeNumber); + attemptSplitSync(till, clearBeforeUpdate, avoidCache, true, resetChangeNumber, onDemandFetchBackoffMaxRetries); } } catch (HttpFetcherException e) { logError("Network error while fetching feature flags" + e.getLocalizedMessage()); @@ -113,10 +112,11 @@ private SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolea * @param clearBeforeUpdate whether to clear splits storage before updating it * @param avoidCache whether to send no-cache header to api * @param withCdnBypass whether to add additional query param to bypass CDN + * @param onDemandFetchBackoffMaxRetries max backoff retries for CDN bypass * @return whether sync finished successfully */ - private boolean attemptSplitSync(long till, boolean clearBeforeUpdate, boolean avoidCache, boolean withCdnBypass, boolean resetChangeNumber) throws Exception { - int remainingAttempts = ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES; + private boolean attemptSplitSync(long till, boolean clearBeforeUpdate, boolean avoidCache, boolean withCdnBypass, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) throws Exception { + int remainingAttempts = onDemandFetchBackoffMaxRetries; mBackoffCounter.resetCounter(); while (true) { remainingAttempts--; @@ -145,10 +145,11 @@ private boolean attemptSplitSync(long till, boolean clearBeforeUpdate, boolean a private long fetchUntil(long till, boolean clearBeforeUpdate, boolean avoidCache, boolean withCdnByPass, boolean resetChangeNumber) throws Exception { boolean shouldClearBeforeUpdate = clearBeforeUpdate; + long newTill = till; while (true) { long changeNumber = (resetChangeNumber) ? -1 : mSplitsStorage.getTill(); resetChangeNumber = false; - if (till < changeNumber) { + if (newTill < changeNumber) { return changeNumber; } @@ -156,6 +157,7 @@ private long fetchUntil(long till, boolean clearBeforeUpdate, boolean avoidCache updateStorage(shouldClearBeforeUpdate, splitChange); shouldClearBeforeUpdate = false; + newTill = splitChange.till; if (splitChange.till == splitChange.since) { return splitChange.till; } @@ -184,14 +186,14 @@ private void updateStorage(boolean clearBeforeUpdate, SplitChange splitChange) { } public boolean cacheHasExpired(long storedChangeNumber, long updateTimestamp, long cacheExpirationInSeconds) { - long elapsed = now() - updateTimestamp; + long elapsed = now() - TimeUnit.MILLISECONDS.toSeconds(updateTimestamp); return storedChangeNumber > -1 && updateTimestamp > 0 && (elapsed > cacheExpirationInSeconds); } private long now() { - return System.currentTimeMillis() / 1000; + return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()); } private void logError(String message) { diff --git a/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java b/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java index f3ed17489..1fe6d6cec 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java @@ -7,6 +7,7 @@ import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionStatus; @@ -27,6 +28,7 @@ public class SplitsSyncTask implements SplitTask { private final ISplitEventsManager mEventsManager; // Should only be null on background sync private final SplitsChangeChecker mChangeChecker; private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; + private final int mOnDemandFetchBackoffMaxRetries; public static SplitsSyncTask build(@NonNull SplitsSyncHelper splitsSyncHelper, @NonNull SplitsStorage splitsStorage, @@ -35,7 +37,7 @@ public static SplitsSyncTask build(@NonNull SplitsSyncHelper splitsSyncHelper, String splitsFilterQueryString, @NonNull ISplitEventsManager eventsManager, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { - return new SplitsSyncTask(splitsSyncHelper, splitsStorage, checkCacheExpiration, cacheExpirationInSeconds, splitsFilterQueryString, telemetryRuntimeProducer, eventsManager); + return new SplitsSyncTask(splitsSyncHelper, splitsStorage, checkCacheExpiration, cacheExpirationInSeconds, splitsFilterQueryString, telemetryRuntimeProducer, eventsManager, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); } public static SplitTask buildForBackground(@NonNull SplitsSyncHelper splitsSyncHelper, @@ -44,7 +46,7 @@ public static SplitTask buildForBackground(@NonNull SplitsSyncHelper splitsSyncH long cacheExpirationInSeconds, String splitsFilterQueryString, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { - return new SplitsSyncTask(splitsSyncHelper, splitsStorage, checkCacheExpiration, cacheExpirationInSeconds, splitsFilterQueryString, telemetryRuntimeProducer, null); + return new SplitsSyncTask(splitsSyncHelper, splitsStorage, checkCacheExpiration, cacheExpirationInSeconds, splitsFilterQueryString, telemetryRuntimeProducer, null, 1); } private SplitsSyncTask(@NonNull SplitsSyncHelper splitsSyncHelper, @@ -53,7 +55,8 @@ private SplitsSyncTask(@NonNull SplitsSyncHelper splitsSyncHelper, long cacheExpirationInSeconds, String splitsFilterQueryString, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, - @Nullable ISplitEventsManager eventsManager) { + @Nullable ISplitEventsManager eventsManager, + int onDemandFetchBackoffMaxRetries) { mSplitsStorage = checkNotNull(splitsStorage); mSplitsSyncHelper = checkNotNull(splitsSyncHelper); @@ -63,6 +66,7 @@ private SplitsSyncTask(@NonNull SplitsSyncHelper splitsSyncHelper, mEventsManager = eventsManager; mChangeChecker = new SplitsChangeChecker(); mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + mOnDemandFetchBackoffMaxRetries = onDemandFetchBackoffMaxRetries; } @Override @@ -84,7 +88,7 @@ public SplitTaskExecutionInfo execute() { long startTime = System.currentTimeMillis(); SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(storedChangeNumber, splitsFilterHasChanged || shouldClearExpiredCache, - splitsFilterHasChanged); + splitsFilterHasChanged || shouldClearExpiredCache, mOnDemandFetchBackoffMaxRetries); mTelemetryRuntimeProducer.recordSyncLatency(OperationType.SPLITS, System.currentTimeMillis() - startTime); if (result.getStatus() == SplitTaskExecutionStatus.SUCCESS) { diff --git a/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java b/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java index 9b0b76313..1deca7b5c 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java @@ -7,6 +7,7 @@ import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionStatus; @@ -50,7 +51,7 @@ public SplitTaskExecutionInfo execute() { return SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC); } - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(mChangeNumber); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(mChangeNumber, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); if (result.getStatus() == SplitTaskExecutionStatus.SUCCESS) { SplitInternalEvent event = SplitInternalEvent.SPLITS_FETCHED; if (mChangeChecker.splitsHaveChanged(storedChangeNumber, mSplitsStorage.getTill())) { diff --git a/src/main/java/io/split/android/client/service/sseclient/SseJwtParser.java b/src/main/java/io/split/android/client/service/sseclient/SseJwtParser.java index 5994429ce..c2ec1ec16 100644 --- a/src/main/java/io/split/android/client/service/sseclient/SseJwtParser.java +++ b/src/main/java/io/split/android/client/service/sseclient/SseJwtParser.java @@ -16,13 +16,13 @@ public class SseJwtParser { - private final static String PUBLISHERS_CHANNEL_METADATA = "channel-metadata:publishers"; - private final static String PUBLISHERS_CHANNEL_PREFIX = "[?occupancy=metrics.publishers]"; + private static final String PUBLISHERS_CHANNEL_METADATA = "channel-metadata:publishers"; + private static final String PUBLISHERS_CHANNEL_PREFIX = "[?occupancy=metrics.publishers]"; - final static Type ALL_TOKEN_TYPE = new TypeToken>() { + static final Type ALL_TOKEN_TYPE = new TypeToken>() { }.getType(); - final static Type CHANNEL_TYPE = new TypeToken>>() { + private static final Type CHANNEL_TYPE = new TypeToken>>() { }.getType(); public SseJwtToken parse(String rawToken) throws InvalidJwtTokenException { @@ -64,7 +64,7 @@ public SseJwtToken parse(String rawToken) throws InvalidJwtTokenException { Logger.e("Error parsing SSE authentication JWT json " + e.getLocalizedMessage()); throw new InvalidJwtTokenException(); } catch (Exception e) { - Logger.e("Unknonwn error while parsing SSE authentication JWT: " + e.getLocalizedMessage()); + Logger.e("Unknown error while parsing SSE authentication JWT: " + e.getLocalizedMessage()); throw new InvalidJwtTokenException(); } diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/ControlNotification.java b/src/main/java/io/split/android/client/service/sseclient/notifications/ControlNotification.java index 2f12877ea..a93d87878 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/ControlNotification.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/ControlNotification.java @@ -1,11 +1,21 @@ package io.split.android.client.service.sseclient.notifications; +import com.google.gson.annotations.SerializedName; + public class ControlNotification extends IncomingNotification { public enum ControlType { - STREAMING_RESUMED, STREAMING_DISABLED, STREAMING_PAUSED, STREAMING_RESET + @SerializedName("STREAMING_RESUMED") + STREAMING_RESUMED, + @SerializedName("STREAMING_DISABLED") + STREAMING_DISABLED, + @SerializedName("STREAMING_PAUSED") + STREAMING_PAUSED, + @SerializedName("STREAMING_RESET") + STREAMING_RESET } @SuppressWarnings("unused") + @SerializedName("controlType") private ControlType controlType; public ControlType getControlType() { diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/HashingAlgorithm.java b/src/main/java/io/split/android/client/service/sseclient/notifications/HashingAlgorithm.java new file mode 100644 index 000000000..1b8049233 --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/HashingAlgorithm.java @@ -0,0 +1,12 @@ +package io.split.android.client.service.sseclient.notifications; + +import com.google.gson.annotations.SerializedName; + +public enum HashingAlgorithm { + @SerializedName("0") + NONE, + @SerializedName("1") + MURMUR3_32, + @SerializedName("2") + MURMUR3_64 +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotificationType.java b/src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotificationType.java index 92fe06b84..adf6a1d99 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotificationType.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/IncomingNotificationType.java @@ -1,6 +1,9 @@ package io.split.android.client.service.sseclient.notifications; +import com.google.gson.annotations.SerializedName; + public class IncomingNotificationType { + @SerializedName(value = "type") protected NotificationType type; public NotificationType getType() { diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java b/src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java new file mode 100644 index 000000000..20cfea311 --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/MembershipNotification.java @@ -0,0 +1,71 @@ +package io.split.android.client.service.sseclient.notifications; + +import androidx.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +import java.util.Set; + +import io.split.android.client.common.CompressionType; + +public class MembershipNotification extends IncomingNotification { + + @SerializedName("cn") + private Long changeNumber; + @SerializedName("n") + private Set names; + + @SerializedName("c") + private CompressionType compression; + @SerializedName("u") + private MySegmentUpdateStrategy updateStrategy; + @SerializedName("d") + private String data; + + @SerializedName("i") + private Long updateIntervalMs; + @SerializedName("h") + private HashingAlgorithm hashingAlgorithm; + @SerializedName("s") + private Integer algorithmSeed; + + @Nullable + public Long getChangeNumber() { + return changeNumber; + } + + @Nullable + public Set getNames() { + return names; + } + + @Nullable + public CompressionType getCompression() { + return compression; + } + + @Nullable + public MySegmentUpdateStrategy getUpdateStrategy() { + return updateStrategy; + } + + @Nullable + public String getData() { + return data; + } + + @Nullable + public Long getUpdateIntervalMs() { + return updateIntervalMs; + } + + @Nullable + public HashingAlgorithm getHashingAlgorithm() { + return hashingAlgorithm; + } + + @Nullable + public Integer getAlgorithmSeed() { + return algorithmSeed; + } +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentChangeNotification.java b/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentChangeNotification.java deleted file mode 100644 index 1a3cd29dc..000000000 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentChangeNotification.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.split.android.client.service.sseclient.notifications; - -import java.util.List; - -public class MySegmentChangeNotification extends IncomingNotification { - private long changeNumber; - private boolean includesPayload; - private List segmentList; - - public long getChangeNumber() { - return changeNumber; - } - - public boolean isIncludesPayload() { - return includesPayload; - } - - public List getSegmentList() { - return segmentList; - } -} 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 deleted file mode 100644 index d760a6148..000000000 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentChangeV2Notification.java +++ /dev/null @@ -1,52 +0,0 @@ -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 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; - - @Nullable - public Long getChangeNumber() { - return changeNumber; - } - - @Nullable - public String getSegmentName() { - return segmentName; - } - - @Nullable - public CompressionType getCompression() { - return compression; - } - - @Nullable - public MySegmentUpdateStrategy getUpdateStrategy() { - return updateStrategy; - } - - @Nullable - public String getData() { - return data; - } -} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsPayloadDecoder.java b/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsPayloadDecoder.java deleted file mode 100644 index 541398ccb..000000000 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/MySegmentsPayloadDecoder.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.split.android.client.service.sseclient.notifications; - -import android.util.Base64; - -import androidx.annotation.NonNull; - -import io.split.android.client.utils.logger.Logger; -import io.split.android.client.utils.MurmurHash3; -import io.split.android.client.utils.StringHelper; - -public class MySegmentsPayloadDecoder { - - /** - * @param matchingKey plain matching key. - * @return Base64 encoding of {@param matchingKey} murmur3_32 hash. - */ - @NonNull - public String hashUserKeyForMySegmentsV1(String matchingKey) { - try { - long murmurHash = MurmurHash3.murmurhash3_x86_32(matchingKey, 0, matchingKey.length(), 0); - byte[] murmurHashStringBytes = String.valueOf(murmurHash).getBytes(StringHelper.defaultCharset()); - - return Base64.encodeToString(murmurHashStringBytes, Base64.NO_WRAP); - } catch (Exception exception) { - Logger.e("An error occurred when encoding matching key"); - - return ""; - } - } -} 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 97f63d2d5..3a4dbb8bc 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 @@ -55,14 +55,6 @@ public SplitKillNotification parseSplitKill(String jsonData) throws JsonSyntaxEx return Json.fromJson(jsonData, SplitKillNotification.class); } - public MySegmentChangeNotification parseMySegmentUpdate(String jsonData) throws JsonSyntaxException { - return Json.fromJson(jsonData, MySegmentChangeNotification.class); - } - - public MySegmentChangeV2Notification parseMySegmentUpdateV2(String jsonData) throws JsonSyntaxException { - return Json.fromJson(jsonData, MySegmentChangeV2Notification.class); - } - public OccupancyNotification parseOccupancy(String jsonData) throws JsonSyntaxException { return Json.fromJson(jsonData, OccupancyNotification.class); } @@ -94,4 +86,14 @@ public String extractUserKeyHashFromChannel(String channel) { return null; } + + @Nullable + public MembershipNotification parseMembershipNotification(String jsonData) { + try { + return Json.fromJson(jsonData, MembershipNotification.class); + } catch (Exception e) { + Logger.w("Failed to parse membership notification"); + return null; + } + } } 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 f4b67bec7..6eef5c597 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 @@ -3,10 +3,10 @@ import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.gson.JsonSyntaxException; -import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -14,7 +14,7 @@ import io.split.android.client.dtos.Split; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskFactory; -import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessor; +import io.split.android.client.service.sseclient.notifications.memberships.MembershipsNotificationProcessor; import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessorRegistry; import io.split.android.client.utils.logger.Logger; @@ -24,21 +24,18 @@ public class NotificationProcessor implements MySegmentsNotificationProcessorReg private final SplitTaskExecutor mSplitTaskExecutor; private final SplitTaskFactory mSplitTaskFactory; private final BlockingQueue mSplitsUpdateNotificationsQueue; - private final ConcurrentMap mMySegmentsNotificationProcessors; - private final MySegmentsPayloadDecoder mMySegmentsPayloadDecoder; + private final ConcurrentMap mMembershipsNotificationProcessors; public NotificationProcessor( @NonNull SplitTaskExecutor splitTaskExecutor, @NonNull SplitTaskFactory splitTaskFactory, @NonNull NotificationParser notificationParser, - @NonNull BlockingQueue splitsUpdateNotificationsQueue, - @NonNull MySegmentsPayloadDecoder mySegmentsPayloadDecoder) { + @NonNull BlockingQueue splitsUpdateNotificationsQueue) { mSplitTaskExecutor = checkNotNull(splitTaskExecutor); mSplitTaskFactory = checkNotNull(splitTaskFactory); mNotificationParser = checkNotNull(notificationParser); mSplitsUpdateNotificationsQueue = checkNotNull(splitsUpdateNotificationsQueue); - mMySegmentsPayloadDecoder = checkNotNull(mySegmentsPayloadDecoder); - mMySegmentsNotificationProcessors = new ConcurrentHashMap<>(); + mMembershipsNotificationProcessors = new ConcurrentHashMap<>(); } public void process(IncomingNotification incomingNotification) { @@ -51,12 +48,9 @@ public void process(IncomingNotification incomingNotification) { case SPLIT_KILL: processSplitKill(mNotificationParser.parseSplitKill(notificationJson)); break; - case MY_SEGMENTS_UPDATE: - processMySegmentUpdate(mNotificationParser.parseMySegmentUpdate(notificationJson), - mNotificationParser.extractUserKeyHashFromChannel(incomingNotification.getChannel())); - break; - case MY_SEGMENTS_UPDATE_V2: - processMySegmentUpdateV2(mNotificationParser.parseMySegmentUpdateV2(notificationJson)); + case MEMBERSHIPS_MS_UPDATE: + case MEMBERSHIPS_LS_UPDATE: + processMembershipsUpdate(mNotificationParser.parseMembershipNotification(notificationJson)); break; default: Logger.e("Unknown notification arrived: " + notificationJson); @@ -71,13 +65,13 @@ public void process(IncomingNotification incomingNotification) { } @Override - public void registerMySegmentsProcessor(String matchingKey, MySegmentsNotificationProcessor processor) { - mMySegmentsNotificationProcessors.put(matchingKey, processor); + public void registerMembershipsNotificationProcessor(String matchingKey, MembershipsNotificationProcessor processor) { + mMembershipsNotificationProcessors.put(matchingKey, processor); } @Override - public void unregisterMySegmentsProcessor(String matchingKey) { - mMySegmentsNotificationProcessors.remove(matchingKey); + public void unregisterMembershipsProcessor(String matchingKey) { + mMembershipsNotificationProcessors.remove(matchingKey); } private void processSplitUpdate(SplitsChangeNotification notification) { @@ -94,23 +88,9 @@ private void processSplitKill(SplitKillNotification notification) { mSplitsUpdateNotificationsQueue.offer(new SplitsChangeNotification(split.changeNumber)); } - private void processMySegmentUpdate(MySegmentChangeNotification notification, String hashedUserKey) { - for (Map.Entry processor : mMySegmentsNotificationProcessors.entrySet()) { - String encodedProcessorKey = mMySegmentsPayloadDecoder.hashUserKeyForMySegmentsV1(processor.getKey()); - - if (encodedProcessorKey == null) { - continue; - } - - if (encodedProcessorKey.equals(hashedUserKey)) { - processor.getValue().processMySegmentsUpdate(notification); - } - } - } - - private void processMySegmentUpdateV2(MySegmentChangeV2Notification notification) { - for (MySegmentsNotificationProcessor processor : mMySegmentsNotificationProcessors.values()) { - processor.processMySegmentsUpdateV2(notification); + private void processMembershipsUpdate(@Nullable MembershipNotification notification) { + for (MembershipsNotificationProcessor processor : mMembershipsNotificationProcessors.values()) { + processor.process(notification); } } } diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java index 8a18b89c4..fb38d5859 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java @@ -1,5 +1,21 @@ package io.split.android.client.service.sseclient.notifications; +import com.google.gson.annotations.SerializedName; + public enum NotificationType { - SPLIT_UPDATE, MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, SPLIT_KILL, CONTROL, OCCUPANCY, ERROR + @SerializedName("SPLIT_UPDATE") + SPLIT_UPDATE, + @SerializedName("SPLIT_KILL") + SPLIT_KILL, + @SerializedName("CONTROL") + CONTROL, + @SerializedName("OCCUPANCY") + OCCUPANCY, + @SerializedName("ERROR") + ERROR, + + @SerializedName("MEMBERSHIPS_LS_UPDATE") + MEMBERSHIPS_LS_UPDATE, + @SerializedName("MEMBERSHIPS_MS_UPDATE") + MEMBERSHIPS_MS_UPDATE, } diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessor.java b/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessor.java new file mode 100644 index 000000000..f0ddc93af --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessor.java @@ -0,0 +1,10 @@ +package io.split.android.client.service.sseclient.notifications.memberships; + +import androidx.annotation.Nullable; + +import io.split.android.client.service.sseclient.notifications.MembershipNotification; + +public interface MembershipsNotificationProcessor { + + void process(@Nullable MembershipNotification notification); +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java b/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java new file mode 100644 index 000000000..a9eb549c3 --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/memberships/MembershipsNotificationProcessorImpl.java @@ -0,0 +1,148 @@ +package io.split.android.client.service.sseclient.notifications.memberships; + +import androidx.annotation.Nullable; + +import java.util.Set; +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.SplitTaskExecutor; +import io.split.android.client.service.mysegments.MySegmentUpdateParams; +import io.split.android.client.service.mysegments.MySegmentsUpdateTask; +import io.split.android.client.service.sseclient.notifications.KeyList; +import io.split.android.client.service.sseclient.notifications.MembershipNotification; +import io.split.android.client.service.sseclient.notifications.MySegmentUpdateStrategy; +import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder; +import io.split.android.client.service.sseclient.notifications.NotificationParser; +import io.split.android.client.service.sseclient.notifications.NotificationType; +import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessorConfiguration; +import io.split.android.client.service.sseclient.notifications.mysegments.SyncDelayCalculator; +import io.split.android.client.utils.logger.Logger; + +public class MembershipsNotificationProcessorImpl implements MembershipsNotificationProcessor { + + private final NotificationParser mNotificationParser; + private final SplitTaskExecutor mSplitTaskExecutor; + private final MySegmentsV2PayloadDecoder mMySegmentsPayloadDecoder; + private final CompressionUtilProvider mCompressionProvider; + private final MySegmentsNotificationProcessorConfiguration mConfiguration; + private final SyncDelayCalculator mSyncDelayCalculator; + + public MembershipsNotificationProcessorImpl(NotificationParser notificationParser, + SplitTaskExecutor splitTaskExecutor, + MySegmentsV2PayloadDecoder mySegmentsPayloadDecoder, + CompressionUtilProvider compressionProvider, + MySegmentsNotificationProcessorConfiguration configuration, + SyncDelayCalculator syncDelayCalculator) { + mNotificationParser = notificationParser; + mSplitTaskExecutor = splitTaskExecutor; + mMySegmentsPayloadDecoder = mySegmentsPayloadDecoder; + mCompressionProvider = compressionProvider; + mConfiguration = configuration; + mSyncDelayCalculator = syncDelayCalculator; + } + + @Override + public void process(@Nullable MembershipNotification notification) { + if (notification == null) { + notifyMySegmentRefreshNeeded(mConfiguration.getNotificationsQueue(), 0L, null, null); + } else { + long syncDelay = mSyncDelayCalculator.calculateSyncDelay(mConfiguration.getUserKey(), + notification.getUpdateIntervalMs(), + notification.getAlgorithmSeed(), + notification.getUpdateStrategy(), + notification.getHashingAlgorithm()); + + processUpdate(notification.getType(), + notification.getUpdateStrategy(), + notification.getData(), + notification.getCompression(), + notification.getNames(), + notification.getChangeNumber(), + mConfiguration.getNotificationsQueue(), + syncDelay); + } + } + + private void processUpdate(NotificationType notificationType, MySegmentUpdateStrategy updateStrategy, String data, CompressionType compression, Set names, Long changeNumber, BlockingQueue notificationsQueue, long syncDelay) { + try { + switch (updateStrategy) { + case UNBOUNDED_FETCH_REQUEST: + Logger.d("Received Unbounded membership fetch request"); + notifyMySegmentRefreshNeeded(notificationsQueue, syncDelay, notificationType, changeNumber); + break; + case BOUNDED_FETCH_REQUEST: + Logger.d("Received Bounded membership fetch request"); + byte[] keyMap = mMySegmentsPayloadDecoder.decodeAsBytes(data, + mCompressionProvider.get(compression)); + executeBoundedFetch(keyMap, syncDelay, notificationType, changeNumber); + break; + case KEY_LIST: + Logger.d("Received KeyList membership fetch request"); + updateSegments(notificationType, mMySegmentsPayloadDecoder.decodeAsString(data, + mCompressionProvider.get(compression)), + names, changeNumber); + break; + case SEGMENT_REMOVAL: + Logger.d("Received membership removal request"); + removeSegment(notificationType, names, changeNumber); + break; + default: + notifyMySegmentRefreshNeeded(notificationsQueue, syncDelay, notificationType, changeNumber); + Logger.w("Unknown membership change notification type: " + updateStrategy); + break; + } + } catch (Exception e) { + Logger.e("Executing unbounded fetch because an error has occurred processing my "+(notificationType == NotificationType.MEMBERSHIPS_LS_UPDATE ? "large" : "")+" segment notification: " + e.getLocalizedMessage()); + notifyMySegmentRefreshNeeded(notificationsQueue, syncDelay, notificationType, changeNumber); + } + } + + private void notifyMySegmentRefreshNeeded(BlockingQueue notificationsQueue, long syncDelay, NotificationType notificationType, Long changeNumber) { + Long targetSegmentsCn = (notificationType == NotificationType.MEMBERSHIPS_MS_UPDATE) ? changeNumber : null; + Long targetLargeSegmentsCn = (notificationType == NotificationType.MEMBERSHIPS_LS_UPDATE) ? changeNumber : null; + + //noinspection ResultOfMethodCallIgnored + notificationsQueue.offer(new MySegmentUpdateParams(syncDelay, targetSegmentsCn, targetLargeSegmentsCn)); + } + + private void removeSegment(NotificationType notificationType, Set segmentNames, Long changeNumber) { + // Shouldn't be null, some defensive code here + if (segmentNames == null) { + return; + } + MySegmentsUpdateTask task = (notificationType == NotificationType.MEMBERSHIPS_LS_UPDATE) ? + mConfiguration.getMySegmentsTaskFactory().createMyLargeSegmentsUpdateTask(false, segmentNames, changeNumber) : + mConfiguration.getMySegmentsTaskFactory().createMySegmentsUpdateTask(false, segmentNames, changeNumber); + mSplitTaskExecutor.submit(task, null); + } + + private void executeBoundedFetch(byte[] keyMap, long syncDelay, NotificationType notificationType, Long changeNumber) { + int index = mMySegmentsPayloadDecoder.computeKeyIndex(mConfiguration.getHashedUserKey(), keyMap.length); + if (mMySegmentsPayloadDecoder.isKeyInBitmap(keyMap, index)) { + Logger.d("Executing Bounded membership fetch request"); + notifyMySegmentRefreshNeeded(mConfiguration.getNotificationsQueue(), syncDelay, notificationType, changeNumber); + } + } + + private void updateSegments(NotificationType notificationType, String keyListString, Set segmentNames, Long changeNumber) { + // Shouldn't be null, some defensive code here + if (segmentNames == null) { + return; + } + KeyList keyList = mNotificationParser.parseKeyList(keyListString); + KeyList.Action action = mMySegmentsPayloadDecoder.getKeyListAction(keyList, mConfiguration.getHashedUserKey()); + boolean actionIsAdd = action != KeyList.Action.REMOVE; + + if (action == KeyList.Action.NONE) { + return; + } + + boolean largeSegmentsUpdate = notificationType == NotificationType.MEMBERSHIPS_LS_UPDATE; + Logger.d("Executing KeyList my "+ (largeSegmentsUpdate ? "large " : "") +"segment fetch request: Adding = " + actionIsAdd); + MySegmentsUpdateTask task = largeSegmentsUpdate ? mConfiguration.getMySegmentsTaskFactory().createMyLargeSegmentsUpdateTask(actionIsAdd, segmentNames, changeNumber) : + mConfiguration.getMySegmentsTaskFactory().createMySegmentsUpdateTask(actionIsAdd, segmentNames, changeNumber); + mSplitTaskExecutor.submit(task, null); + } +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactory.java b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactory.java new file mode 100644 index 000000000..5eb78b8eb --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactory.java @@ -0,0 +1,8 @@ +package io.split.android.client.service.sseclient.notifications.mysegments; + +import io.split.android.client.service.sseclient.notifications.memberships.MembershipsNotificationProcessor; + +public interface MembershipsNotificationProcessorFactory { + + MembershipsNotificationProcessor getProcessor(MySegmentsNotificationProcessorConfiguration configuration); +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java new file mode 100644 index 000000000..898ea21d9 --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MembershipsNotificationProcessorFactoryImpl.java @@ -0,0 +1,35 @@ +package io.split.android.client.service.sseclient.notifications.mysegments; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; + +import io.split.android.client.common.CompressionUtilProvider; +import io.split.android.client.service.executor.SplitTaskExecutor; +import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder; +import io.split.android.client.service.sseclient.notifications.NotificationParser; +import io.split.android.client.service.sseclient.notifications.memberships.MembershipsNotificationProcessor; +import io.split.android.client.service.sseclient.notifications.memberships.MembershipsNotificationProcessorImpl; + +public class MembershipsNotificationProcessorFactoryImpl implements MembershipsNotificationProcessorFactory { + + private final NotificationParser mNotificationParser; + private final SplitTaskExecutor mSplitTaskExecutor; + private final MySegmentsV2PayloadDecoder mMySegmentsPayloadDecoder; + private final CompressionUtilProvider mCompressionProvider; + + public MembershipsNotificationProcessorFactoryImpl(@NonNull NotificationParser notificationParser, + @NonNull SplitTaskExecutor splitTaskExecutor, + @NonNull MySegmentsV2PayloadDecoder mySegmentsPayloadDecoder, + @NonNull CompressionUtilProvider compressionProvider) { + mNotificationParser = checkNotNull(notificationParser); + mSplitTaskExecutor = checkNotNull(splitTaskExecutor); + mMySegmentsPayloadDecoder = checkNotNull(mySegmentsPayloadDecoder); + mCompressionProvider = checkNotNull(compressionProvider); + } + + @Override + public MembershipsNotificationProcessor getProcessor(MySegmentsNotificationProcessorConfiguration configuration) { + return new MembershipsNotificationProcessorImpl(mNotificationParser, mSplitTaskExecutor, mMySegmentsPayloadDecoder, mCompressionProvider, configuration, new SyncDelayCalculatorImpl()); + } +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessor.java b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessor.java deleted file mode 100644 index 7a3ab01b6..000000000 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessor.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.split.android.client.service.sseclient.notifications.mysegments; - -import io.split.android.client.service.sseclient.notifications.MySegmentChangeNotification; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeV2Notification; - -public interface MySegmentsNotificationProcessor { - - void processMySegmentsUpdate(MySegmentChangeNotification notification); - - void processMySegmentsUpdateV2(MySegmentChangeV2Notification notification); -} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorConfiguration.java b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorConfiguration.java index 19af487ba..2ac0fa4a7 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorConfiguration.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorConfiguration.java @@ -1,33 +1,43 @@ package io.split.android.client.service.sseclient.notifications.mysegments; +import static io.split.android.client.utils.Utils.checkNotNull; + import androidx.annotation.NonNull; import java.math.BigInteger; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import io.split.android.client.service.mysegments.MySegmentUpdateParams; import io.split.android.client.service.mysegments.MySegmentsTaskFactory; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeNotification; public class MySegmentsNotificationProcessorConfiguration { private final MySegmentsTaskFactory mMySegmentsTaskFactory; - private final BlockingQueue mMySegmentUpdateNotificationsQueue; + private final BlockingQueue mNotificationsQueue; + private final String mUserKey; private final BigInteger mHashedUserKey; public MySegmentsNotificationProcessorConfiguration(@NonNull MySegmentsTaskFactory mySegmentsTaskFactory, - @NonNull BlockingQueue mySegmentUpdateNotificationsQueue, + @NonNull LinkedBlockingDeque mySegmentUpdateNotificationsQueue, + @NonNull String userKey, @NonNull BigInteger hashedUserKey) { - mMySegmentsTaskFactory = mySegmentsTaskFactory; - mMySegmentUpdateNotificationsQueue = mySegmentUpdateNotificationsQueue; - mHashedUserKey = hashedUserKey; + mMySegmentsTaskFactory = checkNotNull(mySegmentsTaskFactory); + mNotificationsQueue = checkNotNull(mySegmentUpdateNotificationsQueue); + mUserKey = checkNotNull(userKey); + mHashedUserKey = checkNotNull(hashedUserKey); } public MySegmentsTaskFactory getMySegmentsTaskFactory() { return mMySegmentsTaskFactory; } - public BlockingQueue getMySegmentUpdateNotificationsQueue() { - return mMySegmentUpdateNotificationsQueue; + public BlockingQueue getNotificationsQueue() { + return mNotificationsQueue; + } + + public String getUserKey() { + return mUserKey; } public BigInteger getHashedUserKey() { diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorFactory.java b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorFactory.java deleted file mode 100644 index 44dc5c4c6..000000000 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorFactory.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.split.android.client.service.sseclient.notifications.mysegments; - -public interface MySegmentsNotificationProcessorFactory { - - MySegmentsNotificationProcessor getProcessor(MySegmentsNotificationProcessorConfiguration configuration); -} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorFactoryImpl.java b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorFactoryImpl.java deleted file mode 100644 index 34d2e4a76..000000000 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorFactoryImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.split.android.client.service.sseclient.notifications.mysegments; - -import static io.split.android.client.utils.Utils.checkNotNull; - -import androidx.annotation.NonNull; - -import io.split.android.client.common.CompressionUtilProvider; -import io.split.android.client.service.executor.SplitTaskExecutor; -import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder; -import io.split.android.client.service.sseclient.notifications.NotificationParser; - -public class MySegmentsNotificationProcessorFactoryImpl implements MySegmentsNotificationProcessorFactory { - - private final NotificationParser mNotificationParser; - private final SplitTaskExecutor mSplitTaskExecutor; - private final MySegmentsV2PayloadDecoder mMySegmentsPayloadDecoder; - private final CompressionUtilProvider mCompressionProvider; - - public MySegmentsNotificationProcessorFactoryImpl(@NonNull NotificationParser notificationParser, - @NonNull SplitTaskExecutor splitTaskExecutor, - @NonNull MySegmentsV2PayloadDecoder mySegmentsPayloadDecoder, - @NonNull CompressionUtilProvider compressionProvider) { - mNotificationParser = checkNotNull(notificationParser); - mSplitTaskExecutor = checkNotNull(splitTaskExecutor); - mMySegmentsPayloadDecoder = checkNotNull(mySegmentsPayloadDecoder); - mCompressionProvider = checkNotNull(compressionProvider); - } - - @Override - public MySegmentsNotificationProcessor getProcessor(@NonNull MySegmentsNotificationProcessorConfiguration configuration) { - return new MySegmentsNotificationProcessorImpl(mNotificationParser, - mSplitTaskExecutor, - mMySegmentsPayloadDecoder, - mCompressionProvider, - checkNotNull(configuration)); - } -} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImpl.java b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImpl.java deleted file mode 100644 index 8ee1215dc..000000000 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImpl.java +++ /dev/null @@ -1,122 +0,0 @@ -package io.split.android.client.service.sseclient.notifications.mysegments; - -import static io.split.android.client.utils.Utils.checkNotNull; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.List; - -import io.split.android.client.common.CompressionUtilProvider; -import io.split.android.client.service.executor.SplitTaskExecutor; -import io.split.android.client.service.mysegments.MySegmentsOverwriteTask; -import io.split.android.client.service.mysegments.MySegmentsUpdateTask; -import io.split.android.client.service.sseclient.notifications.KeyList; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeNotification; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeV2Notification; -import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder; -import io.split.android.client.service.sseclient.notifications.NotificationParser; -import io.split.android.client.utils.logger.Logger; - -public class MySegmentsNotificationProcessorImpl implements MySegmentsNotificationProcessor { - - private final NotificationParser mNotificationParser; - private final SplitTaskExecutor mSplitTaskExecutor; - private final MySegmentsV2PayloadDecoder mMySegmentsPayloadDecoder; - private final CompressionUtilProvider mCompressionProvider; - private final MySegmentsNotificationProcessorConfiguration mConfiguration; - - public MySegmentsNotificationProcessorImpl(@NonNull NotificationParser notificationParser, - @NonNull SplitTaskExecutor splitTaskExecutor, - @NonNull MySegmentsV2PayloadDecoder mySegmentsPayloadDecoder, - @NonNull CompressionUtilProvider compressionProvider, - @NonNull MySegmentsNotificationProcessorConfiguration configuration) { - mNotificationParser = checkNotNull(notificationParser); - mSplitTaskExecutor = checkNotNull(splitTaskExecutor); - mMySegmentsPayloadDecoder = checkNotNull(mySegmentsPayloadDecoder); - mCompressionProvider = checkNotNull(compressionProvider); - mConfiguration = checkNotNull(configuration); - } - - @Override - public void processMySegmentsUpdate(MySegmentChangeNotification notification) { - if (!notification.isIncludesPayload()) { - mConfiguration.getMySegmentUpdateNotificationsQueue().offer(notification); - } else { - List segmentList = notification.getSegmentList() != null ? notification.getSegmentList() : new ArrayList<>(); - MySegmentsOverwriteTask task = mConfiguration.getMySegmentsTaskFactory().createMySegmentsOverwriteTask(segmentList); - mSplitTaskExecutor.submit(task, null); - } - } - - @Override - public void processMySegmentsUpdateV2(MySegmentChangeV2Notification notification) { - try { - switch (notification.getUpdateStrategy()) { - case UNBOUNDED_FETCH_REQUEST: - Logger.d("Received Unbounded my segment fetch request"); - notifyMySegmentRefreshNeeded(); - break; - case BOUNDED_FETCH_REQUEST: - Logger.d("Received Bounded my segment fetch request"); - byte[] keyMap = mMySegmentsPayloadDecoder.decodeAsBytes(notification.getData(), - mCompressionProvider.get(notification.getCompression())); - executeBoundedFetch(keyMap); - break; - case KEY_LIST: - Logger.d("Received KeyList my segment fetch request"); - updateSegments(mMySegmentsPayloadDecoder.decodeAsString(notification.getData(), - mCompressionProvider.get(notification.getCompression())), - notification.getSegmentName()); - break; - case SEGMENT_REMOVAL: - Logger.d("Received Segment removal request"); - removeSegment(notification.getSegmentName()); - break; - default: - Logger.i("Unknown my segment change v2 notification type: " + notification.getUpdateStrategy()); - } - } catch (Exception e) { - Logger.e("Executing unbounded fetch because an error has occurred processing my segmentV2 notification: " + e.getLocalizedMessage()); - notifyMySegmentRefreshNeeded(); - } - } - - private void notifyMySegmentRefreshNeeded() { - mConfiguration.getMySegmentUpdateNotificationsQueue().offer(new MySegmentChangeNotification()); - } - - private void removeSegment(String segmentName) { - // Shouldn't be null, some defensive code here - if (segmentName == null) { - return; - } - MySegmentsUpdateTask task = mConfiguration.getMySegmentsTaskFactory().createMySegmentsUpdateTask(false, segmentName); - mSplitTaskExecutor.submit(task, null); - } - - private void executeBoundedFetch(byte[] keyMap) { - int index = mMySegmentsPayloadDecoder.computeKeyIndex(mConfiguration.getHashedUserKey(), keyMap.length); - if (mMySegmentsPayloadDecoder.isKeyInBitmap(keyMap, index)) { - Logger.d("Executing Unbounded my segment fetch request"); - notifyMySegmentRefreshNeeded(); - } - } - - private void updateSegments(String keyListString, String segmentName) { - // Shouldn't be null, some defensive code here - if (segmentName == null) { - return; - } - KeyList keyList = mNotificationParser.parseKeyList(keyListString); - KeyList.Action action = mMySegmentsPayloadDecoder.getKeyListAction(keyList, mConfiguration.getHashedUserKey()); - boolean actionIsAdd = action != KeyList.Action.REMOVE; - - if (action == KeyList.Action.NONE) { - return; - } - Logger.d("Executing KeyList my segment fetch request: Adding = " + actionIsAdd); - MySegmentsUpdateTask task = mConfiguration.getMySegmentsTaskFactory().createMySegmentsUpdateTask(actionIsAdd, segmentName); - mSplitTaskExecutor.submit(task, null); - } -} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorRegistry.java b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorRegistry.java index d04b145a5..19d5f2759 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorRegistry.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorRegistry.java @@ -1,8 +1,10 @@ package io.split.android.client.service.sseclient.notifications.mysegments; +import io.split.android.client.service.sseclient.notifications.memberships.MembershipsNotificationProcessor; + public interface MySegmentsNotificationProcessorRegistry { - void registerMySegmentsProcessor(String matchingKey, MySegmentsNotificationProcessor processor); + void registerMembershipsNotificationProcessor(String matchingKey, MembershipsNotificationProcessor processor); - void unregisterMySegmentsProcessor(String matchingKey); + void unregisterMembershipsProcessor(String matchingKey); } diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculator.java b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculator.java new file mode 100644 index 000000000..4b78f2a26 --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculator.java @@ -0,0 +1,8 @@ +package io.split.android.client.service.sseclient.notifications.mysegments; + +import io.split.android.client.service.sseclient.notifications.HashingAlgorithm; +import io.split.android.client.service.sseclient.notifications.MySegmentUpdateStrategy; + +public interface SyncDelayCalculator { + long calculateSyncDelay(String key, Long updateIntervalMs, Integer algorithmSeed, MySegmentUpdateStrategy updateStrategy, HashingAlgorithm hashingAlgorithm); +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorImpl.java b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorImpl.java new file mode 100644 index 000000000..6ba6aedc7 --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorImpl.java @@ -0,0 +1,31 @@ +package io.split.android.client.service.sseclient.notifications.mysegments; + +import java.util.concurrent.TimeUnit; + +import io.split.android.client.service.sseclient.notifications.HashingAlgorithm; +import io.split.android.client.service.sseclient.notifications.MySegmentUpdateStrategy; +import io.split.android.client.utils.MurmurHash3; + +public class SyncDelayCalculatorImpl implements SyncDelayCalculator { + + public static final long DEFAULT_SYNC_INTERVAL_MS = TimeUnit.SECONDS.toMillis(60); + + @Override + public long calculateSyncDelay(String key, Long updateIntervalMs, Integer algorithmSeed, MySegmentUpdateStrategy updateStrategy, HashingAlgorithm hashingAlgorithm) { + boolean fetchNotification = updateStrategy == MySegmentUpdateStrategy.UNBOUNDED_FETCH_REQUEST || + updateStrategy == MySegmentUpdateStrategy.BOUNDED_FETCH_REQUEST; + if (!fetchNotification || hashingAlgorithm == HashingAlgorithm.NONE) { + return 0L; + } + + if (updateIntervalMs == null || updateIntervalMs <= 0) { + updateIntervalMs = DEFAULT_SYNC_INTERVAL_MS; + } + + if (algorithmSeed == null) { + algorithmSeed = 0; + } + + return MurmurHash3.murmurhash3_x86_32(key, 0, key.length(), algorithmSeed) % updateIntervalMs; + } +} diff --git a/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorker.java b/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorker.java index a56dfc208..3381e6d60 100644 --- a/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorker.java +++ b/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorker.java @@ -6,7 +6,7 @@ import java.util.concurrent.BlockingQueue; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeNotification; +import io.split.android.client.service.mysegments.MySegmentUpdateParams; import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizer; import io.split.android.client.utils.logger.Logger; @@ -16,11 +16,11 @@ public class MySegmentsUpdateWorker extends UpdateWorker { private final MySegmentsSynchronizer mSynchronizer; - private final BlockingQueue mNotificationsQueue; + private final BlockingQueue mNotificationsQueue; public MySegmentsUpdateWorker( @NonNull MySegmentsSynchronizer synchronizer, - @NonNull BlockingQueue notificationsQueue) { + @NonNull BlockingQueue notificationsQueue) { super(); mSynchronizer = checkNotNull(synchronizer); mNotificationsQueue = checkNotNull(notificationsQueue); @@ -29,8 +29,8 @@ public MySegmentsUpdateWorker( @Override protected void onWaitForNotificationLoop() throws InterruptedException { try { - mNotificationsQueue.take(); - mSynchronizer.forceMySegmentsSync(); + MySegmentUpdateParams params = mNotificationsQueue.take(); + mSynchronizer.forceMySegmentsSync(params); Logger.d("A new notification to update segments has been received. " + "Enqueuing polling task."); } catch (InterruptedException e) { diff --git a/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImpl.java b/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImpl.java index f6c5afb51..d2d03334c 100644 --- a/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImpl.java +++ b/src/main/java/io/split/android/client/service/sseclient/reactor/MySegmentsUpdateWorkerRegistryImpl.java @@ -14,9 +14,7 @@ public class MySegmentsUpdateWorkerRegistryImpl implements MySegmentsUpdateWorke @Override public synchronized void registerMySegmentsUpdateWorker(String matchingKey, MySegmentsUpdateWorker mySegmentsUpdateWorker) { mMySegmentUpdateWorkers.put(matchingKey, mySegmentsUpdateWorker); - if (mStarted.get()) { - mySegmentsUpdateWorker.start(); - } + startIfNeeded(mySegmentsUpdateWorker); } @Override @@ -49,4 +47,10 @@ public void stop() { } } } + + private void startIfNeeded(MySegmentsUpdateWorker mySegmentsUpdateWorker) { + if (mStarted.get()) { + mySegmentsUpdateWorker.start(); + } + } } diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java index 128af8dcb..eeba8744d 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/NotificationManagerKeeper.java @@ -79,9 +79,10 @@ public void handleControlNotification(ControlNotification notification) { case STREAMING_RESET: mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_RESET)); - + break; default: - Logger.e("Unknown message received" + notification.getControlType()); + Logger.e("Unknown message received " + notification.getControlType()); + break; } } catch (JsonSyntaxException e) { Logger.e("Could not parse control notification: " 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 6aa7594fc..5217889b2 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 @@ -210,11 +210,11 @@ public void run() { recordSuccessfulSyncAndTokenRefreshes(token); long delay = authResult.getSseConnectionDelay(); + mBroadcasterChannel.pushMessage(new DelayStatusEvent(delay)); // 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()) { diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java index 91293155f..79c14f699 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimer.java @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; @@ -13,6 +14,7 @@ import io.split.android.client.service.sseclient.BackoffCounter; import io.split.android.client.utils.logger.Logger; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class RetryBackoffCounterTimer implements SplitTaskExecutionListener { @@ -22,10 +24,11 @@ public class RetryBackoffCounterTimer implements SplitTaskExecutionListener { private final SplitTaskExecutor mTaskExecutor; private final BackoffCounter mBackoffCounter; private final int mRetryAttemptsLimit; - private final AtomicInteger mCurrentAttempts = new AtomicInteger(0); + private final AtomicInteger mCurrentAttempts; + private Long mInitialDelayInSeconds; private SplitTask mTask; private SplitTaskExecutionListener mListener; - private String mTaskId; + private volatile String mTaskId; /** * Creates an instance which retries tasks indefinitely, using the strategy defined by backoffCounter. @@ -35,9 +38,7 @@ public class RetryBackoffCounterTimer implements SplitTaskExecutionListener { */ public RetryBackoffCounterTimer(@NonNull SplitTaskExecutor taskExecutor, @NonNull BackoffCounter backoffCounter) { - mTaskExecutor = checkNotNull(taskExecutor); - mBackoffCounter = checkNotNull(backoffCounter); - mRetryAttemptsLimit = DEFAULT_MAX_ATTEMPTS; + this(taskExecutor, backoffCounter, DEFAULT_MAX_ATTEMPTS); } /** @@ -50,20 +51,31 @@ public RetryBackoffCounterTimer(@NonNull SplitTaskExecutor taskExecutor, public RetryBackoffCounterTimer(@NonNull SplitTaskExecutor taskExecutor, @NonNull BackoffCounter backoffCounter, int retryAttemptsLimit) { + mCurrentAttempts = new AtomicInteger(0); mTaskExecutor = checkNotNull(taskExecutor); mBackoffCounter = checkNotNull(backoffCounter); mRetryAttemptsLimit = retryAttemptsLimit; } synchronized public void setTask(@NonNull SplitTask task, @Nullable SplitTaskExecutionListener listener) { - mTask = checkNotNull(task); - mListener = listener; + setTask(task, ServiceConstants.NO_INITIAL_DELAY, listener); } synchronized public void setTask(@NonNull SplitTask task) { setTask(task, null); } + synchronized public void setTask(@NonNull SplitTask task, @Nullable Long initialDelayInMillis, @Nullable SplitTaskExecutionListener listener) { + mTask = checkNotNull(task); + mListener = listener; + if (initialDelayInMillis != null) { + mInitialDelayInSeconds = TimeUnit.MILLISECONDS.toSeconds(initialDelayInMillis); + } else { + mInitialDelayInSeconds = 0L; + } + mCurrentAttempts.set(0); + } + synchronized public void stop() { if (mTask == null) { return; @@ -78,7 +90,7 @@ synchronized public void start() { } mBackoffCounter.resetCounter(); mCurrentAttempts.incrementAndGet(); - mTaskId = mTaskExecutor.schedule(mTask, 0L, this); + mTaskId = mTaskExecutor.schedule(mTask, mInitialDelayInSeconds, this); } synchronized private void schedule() { @@ -94,15 +106,20 @@ synchronized private void schedule() { @Override public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { mTaskId = null; - if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR && - (taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY) == null || - Boolean.FALSE.equals(taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)))) { - - if (mRetryAttemptsLimit == DEFAULT_MAX_ATTEMPTS || mCurrentAttempts.get() < mRetryAttemptsLimit) { - schedule(); + if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { + if (taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY) == null || + Boolean.FALSE.equals(taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY))) { + + if (mRetryAttemptsLimit == DEFAULT_MAX_ATTEMPTS || mCurrentAttempts.get() < mRetryAttemptsLimit) { + schedule(); + } + + return; + } else if (Boolean.TRUE.equals(taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY))) { + if (mListener != null) { + mListener.taskExecuted(taskInfo); + } } - - return; } mBackoffCounter.resetCounter(); diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java index 2f943995c..dd2f47071 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java @@ -1,5 +1,7 @@ package io.split.android.client.service.sseclient.sseclient; +import static io.split.android.client.utils.Utils.checkNotNull; + import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -22,8 +24,6 @@ import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; import io.split.android.client.utils.logger.Logger; -import static io.split.android.client.utils.Utils.checkNotNull; - public class SseHandler { private final PushManagerEventBroadcaster mBroadcasterChannel; @@ -85,14 +85,14 @@ public void handleIncomingMessage(Map values) { break; case SPLIT_KILL: case SPLIT_UPDATE: - case MY_SEGMENTS_UPDATE: - case MY_SEGMENTS_UPDATE_V2: + case MEMBERSHIPS_MS_UPDATE: + case MEMBERSHIPS_LS_UPDATE: if (mNotificationManagerKeeper.isStreamingActive()) { mNotificationProcessor.process(incomingNotification); } break; default: - Logger.w("SSE Handler: Unknown notification"); + Logger.w("SSE Handler: Unknown notification: " + incomingNotification.getType()); } } } diff --git a/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java b/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java index e4d2fc4c8..16fb4cb73 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java +++ b/src/main/java/io/split/android/client/service/synchronizer/MySegmentsChangeChecker.java @@ -1,15 +1,12 @@ package io.split.android.client.service.synchronizer; -import java.util.ArrayList; import java.util.Collections; import java.util.List; public class MySegmentsChangeChecker { - public boolean mySegmentsHaveChanged(List oldSegments, List newSegments) { - List oldValues = new ArrayList<>(oldSegments); - List newValues = new ArrayList<>(newSegments); - Collections.sort(oldValues); - Collections.sort(newValues); - return !oldValues.equals(newValues); + public boolean mySegmentsHaveChanged(final List oldSegments, final List newSegments) { + Collections.sort(oldSegments); + Collections.sort(newSegments); + return !oldSegments.equals(newSegments); } } diff --git a/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java b/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java index 6f8e614a4..27adf8d38 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java +++ b/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java @@ -18,8 +18,6 @@ public interface Synchronizer extends SplitLifecycleAware { void synchronizeMySegments(); - void forceMySegmentsSync(); - void startPeriodicFetching(); void stopPeriodicFetching(); 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 e0b318585..5f069bee9 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 @@ -35,7 +35,8 @@ import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; import io.split.android.client.utils.logger.Logger; -public class SynchronizerImpl implements Synchronizer, SplitTaskExecutionListener, MySegmentsSynchronizerRegistry, AttributesSynchronizerRegistry { +public class SynchronizerImpl implements Synchronizer, SplitTaskExecutionListener, + MySegmentsSynchronizerRegistry, AttributesSynchronizerRegistry { private final SplitTaskExecutor mTaskExecutor; private final SplitTaskExecutor mSingleThreadTaskExecutor; @@ -166,11 +167,6 @@ public void synchronizeMySegments() { mMySegmentsSynchronizerRegistry.synchronizeMySegments(); } - @Override - public void forceMySegmentsSync() { - mMySegmentsSynchronizerRegistry.forceMySegmentsSync(); - } - @Override synchronized public void startPeriodicFetching() { mFeatureFlagsSynchronizer.startPeriodicFetching(); 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 b5cc20991..c342f1cbc 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 @@ -18,12 +18,14 @@ import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import io.split.android.android_client.BuildConfig; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFilter; +import io.split.android.client.network.CertificatePin; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; @@ -33,10 +35,10 @@ import io.split.android.client.service.workmanager.EventsRecorderWorker; import io.split.android.client.service.workmanager.ImpressionsRecorderWorker; import io.split.android.client.service.workmanager.MySegmentsSyncWorker; +import io.split.android.client.service.workmanager.UniqueKeysRecorderWorker; import io.split.android.client.service.workmanager.splits.SplitsSyncWorker; import io.split.android.client.utils.Json; import io.split.android.client.utils.logger.Logger; -import io.split.android.client.service.workmanager.UniqueKeysRecorderWorker; public class WorkManagerWrapper implements MySegmentsWorkManagerWrapper { final private WorkManager mWorkManager; @@ -145,11 +147,14 @@ private Data buildInputData(Data customData) { dataBuilder.putString(ServiceConstants.WORKER_PARAM_DATABASE_NAME, mDatabaseName); dataBuilder.putString(ServiceConstants.WORKER_PARAM_API_KEY, mApiKey); dataBuilder.putBoolean(ServiceConstants.WORKER_PARAM_ENCRYPTION_ENABLED, mSplitClientConfig.encryptionEnabled()); - try { - String pinsJson = Json.toJson(mSplitClientConfig.certificatePinningConfiguration().getPins()); - dataBuilder.putString(ServiceConstants.WORKER_PARAM_CERTIFICATE_PINS, pinsJson); - } catch (Exception e) { - Logger.e("Error converting pins to JSON for BG sync", e.getLocalizedMessage()); + if (mSplitClientConfig.certificatePinningConfiguration() != null) { + try { + Map> pins = mSplitClientConfig.certificatePinningConfiguration().getPins(); + String pinsJson = Json.toJson(pins); + dataBuilder.putString(ServiceConstants.WORKER_PARAM_CERTIFICATE_PINS, pinsJson); + } catch (Exception e) { + Logger.e("Error converting pins to JSON for BG sync", e.getLocalizedMessage()); + } } if (customData != null) { diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizer.java b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizer.java index 4c8d4c17c..c656190c1 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizer.java +++ b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizer.java @@ -1,12 +1,14 @@ package io.split.android.client.service.synchronizer.mysegments; +import io.split.android.client.service.mysegments.MySegmentUpdateParams; + public interface MySegmentsSynchronizer { void loadMySegmentsFromCache(); void synchronizeMySegments(); - void forceMySegmentsSync(); + void forceMySegmentsSync(MySegmentUpdateParams params); void destroy(); diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactory.java b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactory.java index 31e9b63f3..240916f40 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactory.java +++ b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactory.java @@ -1,9 +1,10 @@ package io.split.android.client.service.synchronizer.mysegments; import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.service.mysegments.MySegmentsTaskFactory; public interface MySegmentsSynchronizerFactory { - MySegmentsSynchronizer getSynchronizer(MySegmentsTaskFactory mySegmentsTaskFactory, SplitEventsManager splitEventsManager); + MySegmentsSynchronizer getSynchronizer(MySegmentsTaskFactory mySegmentsTaskFactory, SplitEventsManager splitEventsManager, SplitInternalEvent loadedFromStorageInternalEvent, int segmentsRefreshRate); } diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactoryImpl.java b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactoryImpl.java index 09f1aaaeb..12ec91a8d 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerFactoryImpl.java @@ -6,6 +6,7 @@ import io.split.android.client.RetryBackoffCounterTimerFactory; import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.mysegments.MySegmentsTaskFactory; @@ -15,22 +16,20 @@ public class MySegmentsSynchronizerFactoryImpl implements MySegmentsSynchronizer private final RetryBackoffCounterTimerFactory mRetryBackoffCounterTimerFactory; private final SplitTaskExecutor mSplitTaskExecutor; - private final int mSegmentsRefreshRate; public MySegmentsSynchronizerFactoryImpl(@NonNull RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory, - @NonNull SplitTaskExecutor splitTaskExecutor, - int segmentsRefreshRate) { + @NonNull SplitTaskExecutor splitTaskExecutor) { mRetryBackoffCounterTimerFactory = checkNotNull(retryBackoffCounterTimerFactory); mSplitTaskExecutor = checkNotNull(splitTaskExecutor); - mSegmentsRefreshRate = segmentsRefreshRate; } @Override - public MySegmentsSynchronizer getSynchronizer(MySegmentsTaskFactory mySegmentsTaskFactory, SplitEventsManager splitEventsManager) { + public MySegmentsSynchronizer getSynchronizer(MySegmentsTaskFactory mySegmentsTaskFactory, SplitEventsManager splitEventsManager, SplitInternalEvent loadedFromStorageInternalEvent, int segmentsRefreshRate) { return new MySegmentsSynchronizerImpl(mRetryBackoffCounterTimerFactory.create(mSplitTaskExecutor, BACKOFF_BASE), mSplitTaskExecutor, splitEventsManager, mySegmentsTaskFactory, - mSegmentsRefreshRate); + segmentsRefreshRate, + loadedFromStorageInternalEvent); } } diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImpl.java index f743940d2..999ff04e2 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImpl.java @@ -12,6 +12,8 @@ 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.mysegments.MySegmentUpdateParams; +import io.split.android.client.service.mysegments.MySegmentsSyncTask; import io.split.android.client.service.mysegments.MySegmentsTaskFactory; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; import io.split.android.client.service.synchronizer.LoadLocalDataListener; @@ -26,21 +28,24 @@ public class MySegmentsSynchronizerImpl implements MySegmentsSynchronizer { private final SplitTaskExecutionListener mMySegmentsSyncListener; private final AtomicBoolean mIsSynchronizing = new AtomicBoolean(true); private String mMySegmentsFetcherTaskId; + private final AtomicBoolean mIsDelayedFetchScheduled = new AtomicBoolean(false); public MySegmentsSynchronizerImpl(@NonNull RetryBackoffCounterTimer retryBackoffCounterTimer, @NonNull SplitTaskExecutor taskExecutor, @NonNull SplitEventsManager eventsManager, @NonNull MySegmentsTaskFactory mySegmentsTaskFactory, - int segmentsRefreshRate) { + int segmentsRefreshRate, + @NonNull SplitInternalEvent loadedFromStorageInternalEvent) { mTaskExecutor = checkNotNull(taskExecutor); mMySegmentsSyncRetryTimer = checkNotNull(retryBackoffCounterTimer); mSplitTaskFactory = checkNotNull(mySegmentsTaskFactory); mSegmentsRefreshRate = segmentsRefreshRate; mLoadLocalMySegmentsListener = new LoadLocalDataListener( - eventsManager, SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); + eventsManager, loadedFromStorageInternalEvent); mMySegmentsSyncListener = new SplitTaskExecutionListener() { @Override public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { + mIsDelayedFetchScheduled.set(false); if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { if (Boolean.TRUE.equals(taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY))) { mIsSynchronizing.compareAndSet(true, false); @@ -59,15 +64,17 @@ public void loadMySegmentsFromCache() { @Override public void synchronizeMySegments() { if (mIsSynchronizing.get()) { - mMySegmentsSyncRetryTimer.setTask(mSplitTaskFactory.createMySegmentsSyncTask(false), mMySegmentsSyncListener); + mMySegmentsSyncRetryTimer.stop(); + mMySegmentsSyncRetryTimer.setTask(mSplitTaskFactory.createMySegmentsSyncTask(false, null, null), mMySegmentsSyncListener); mMySegmentsSyncRetryTimer.start(); } } @Override - public void forceMySegmentsSync() { - if (mIsSynchronizing.get()) { - mMySegmentsSyncRetryTimer.setTask(mSplitTaskFactory.createMySegmentsSyncTask(true), mMySegmentsSyncListener); + public void forceMySegmentsSync(MySegmentUpdateParams params) { + if (mIsSynchronizing.get() && mIsDelayedFetchScheduled.compareAndSet(false, true)) { + mMySegmentsSyncRetryTimer.stop(); + mMySegmentsSyncRetryTimer.setTask(getForcedSegmentsSyncTask(params.getTargetSegmentsCn(), params.getTargetLargeSegmentsCn()), params.getSyncDelay(), mMySegmentsSyncListener); mMySegmentsSyncRetryTimer.start(); } } @@ -85,7 +92,7 @@ public void scheduleSegmentsSyncTask() { } mMySegmentsFetcherTaskId = mTaskExecutor.schedule( - mSplitTaskFactory.createMySegmentsSyncTask(false), + getMySegmentsSyncTask(), mSegmentsRefreshRate, mSegmentsRefreshRate, mMySegmentsSyncListener); @@ -105,4 +112,12 @@ public void submitMySegmentsLoadingTask() { private void submitMySegmentsLoadingTask(SplitTaskExecutionListener executionListener) { mTaskExecutor.submit(mSplitTaskFactory.createLoadMySegmentsTask(), executionListener); } + + private MySegmentsSyncTask getMySegmentsSyncTask() { + return mSplitTaskFactory.createMySegmentsSyncTask(false, null, null); + } + + private MySegmentsSyncTask getForcedSegmentsSyncTask(Long targetSegmentsCn, Long targetLargeSegmentsCn) { + return mSplitTaskFactory.createMySegmentsSyncTask(true, targetSegmentsCn, targetLargeSegmentsCn); + } } 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 097b95e53..5aba4ce82 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 @@ -1,10 +1,15 @@ package io.split.android.client.service.synchronizer.mysegments; +import androidx.core.util.Consumer; + import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; -public class MySegmentsSynchronizerRegistryImpl implements MySegmentsSynchronizerRegistry, MySegmentsSynchronizer { +import io.split.android.client.service.mysegments.MySegmentUpdateParams; + +public class MySegmentsSynchronizerRegistryImpl implements MySegmentsSynchronizerRegistry, + MySegmentsSynchronizer { private final AtomicBoolean mLoadedFromCache = new AtomicBoolean(false); private final AtomicBoolean mSynchronizedSegments = new AtomicBoolean(false); @@ -15,17 +20,7 @@ public class MySegmentsSynchronizerRegistryImpl implements MySegmentsSynchronize @Override public synchronized void registerMySegmentsSynchronizer(String userKey, MySegmentsSynchronizer mySegmentsSynchronizer) { mMySegmentsSynchronizers.put(userKey, mySegmentsSynchronizer); - if (mLoadedFromCache.get()) { - mySegmentsSynchronizer.loadMySegmentsFromCache(); - } - - if (mSynchronizedSegments.get()) { - mySegmentsSynchronizer.synchronizeMySegments(); - } - - if (mScheduledSegmentsSyncTask.get()) { - mySegmentsSynchronizer.scheduleSegmentsSyncTask(); - } + triggerPendingActions(mySegmentsSynchronizer); } @Override @@ -35,64 +30,71 @@ public synchronized void unregisterMySegmentsSynchronizer(String userKey) { mySegmentsSynchronizer.stopPeriodicFetching(); mySegmentsSynchronizer.destroy(); } + mMySegmentsSynchronizers.remove(userKey); } @Override public synchronized void loadMySegmentsFromCache() { - for (MySegmentsSynchronizer mySegmentsSynchronizer : mMySegmentsSynchronizers.values()) { - mySegmentsSynchronizer.loadMySegmentsFromCache(); - } + executeForAll(MySegmentsSynchronizer::loadMySegmentsFromCache); mLoadedFromCache.set(true); } @Override public void synchronizeMySegments() { - for (MySegmentsSynchronizer mySegmentsSynchronizer : mMySegmentsSynchronizers.values()) { - mySegmentsSynchronizer.synchronizeMySegments(); - } + executeForAll(MySegmentsSynchronizer::synchronizeMySegments); mSynchronizedSegments.set(true); } @Override - public void forceMySegmentsSync() { - for (MySegmentsSynchronizer mySegmentsSynchronizer : mMySegmentsSynchronizers.values()) { - mySegmentsSynchronizer.forceMySegmentsSync(); - } + public void forceMySegmentsSync(MySegmentUpdateParams params) { + executeForAll(mySegmentsSynchronizer -> mySegmentsSynchronizer.forceMySegmentsSync(params)); } @Override public synchronized void destroy() { - for (MySegmentsSynchronizer mySegmentsSynchronizer : mMySegmentsSynchronizers.values()) { - mySegmentsSynchronizer.destroy(); - } + executeForAll(MySegmentsSynchronizer::destroy); } @Override public synchronized void scheduleSegmentsSyncTask() { - for (MySegmentsSynchronizer mySegmentsSynchronizer : mMySegmentsSynchronizers.values()) { - mySegmentsSynchronizer.scheduleSegmentsSyncTask(); - } + executeForAll(MySegmentsSynchronizer::scheduleSegmentsSyncTask); mScheduledSegmentsSyncTask.set(true); } @Override public void submitMySegmentsLoadingTask() { - for (MySegmentsSynchronizer mySegmentsSynchronizer : mMySegmentsSynchronizers.values()) { - mySegmentsSynchronizer.submitMySegmentsLoadingTask(); - } + executeForAll(MySegmentsSynchronizer::submitMySegmentsLoadingTask); } @Override public synchronized void stopPeriodicFetching() { - for (MySegmentsSynchronizer mySegmentsSynchronizer : mMySegmentsSynchronizers.values()) { - mySegmentsSynchronizer.stopPeriodicFetching(); - } + executeForAll(MySegmentsSynchronizer::stopPeriodicFetching); mScheduledSegmentsSyncTask.set(false); mStoppedPeriodicFetching.set(true); } + + private void triggerPendingActions(MySegmentsSynchronizer mySegmentsSynchronizer) { + if (mLoadedFromCache.get()) { + mySegmentsSynchronizer.loadMySegmentsFromCache(); + } + + if (mSynchronizedSegments.get()) { + mySegmentsSynchronizer.synchronizeMySegments(); + } + + if (mScheduledSegmentsSyncTask.get()) { + mySegmentsSynchronizer.scheduleSegmentsSyncTask(); + } + } + + private void executeForAll(Consumer consumer) { + for (MySegmentsSynchronizer mySegmentsSynchronizer : mMySegmentsSynchronizers.values()) { + consumer.accept(mySegmentsSynchronizer); + } + } } diff --git a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsWorkManagerWrapper.java b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsWorkManagerWrapper.java index 6d1c056e2..67dee11e3 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsWorkManagerWrapper.java +++ b/src/main/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsWorkManagerWrapper.java @@ -3,6 +3,7 @@ import java.util.Set; public interface MySegmentsWorkManagerWrapper { + void scheduleMySegmentsWork(Set keys); void removeWork(); diff --git a/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java b/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java index e28af28d9..72c1b9c32 100644 --- a/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/telemetry/TelemetryTaskFactoryImpl.java @@ -29,12 +29,13 @@ public TelemetryTaskFactoryImpl(@NonNull HttpRecorder telemetryConfigRec @NonNull SplitClientConfig splitClientConfig, @NonNull SplitsStorage splitsStorage, @NonNull MySegmentsStorageContainer mySegmentsStorageContainer, + MySegmentsStorageContainer myLargeSegmentsStorageContainer, int flagSetCount, int invalidFlagSetCount) { mTelemetryConfigRecorder = telemetryConfigRecorder; mTelemetryConfigProvider = new TelemetryConfigProviderImpl(telemetryStorage, splitClientConfig, flagSetCount, invalidFlagSetCount); mTelemetryStatsRecorder = telemetryStatsRecorder; - mTelemetryStatsProvider = new TelemetryStatsProviderImpl(telemetryStorage, splitsStorage, mySegmentsStorageContainer); + mTelemetryStatsProvider = new TelemetryStatsProviderImpl(telemetryStorage, splitsStorage, mySegmentsStorageContainer, myLargeSegmentsStorageContainer); mTelemetryRuntimeProducer = telemetryStorage; } diff --git a/src/main/java/io/split/android/client/service/workmanager/BaseSegmentsSyncWorker.java b/src/main/java/io/split/android/client/service/workmanager/BaseSegmentsSyncWorker.java new file mode 100644 index 000000000..ce3014a62 --- /dev/null +++ b/src/main/java/io/split/android/client/service/workmanager/BaseSegmentsSyncWorker.java @@ -0,0 +1,68 @@ +package io.split.android.client.service.workmanager; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.work.WorkerParameters; + +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import io.split.android.client.network.HttpClient; +import io.split.android.client.service.ServiceConstants; +import io.split.android.client.service.mysegments.MySegmentsBulkSyncTask; +import io.split.android.client.service.mysegments.MySegmentsSyncTask; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.logger.Logger; + +abstract class BaseSegmentsSyncWorker extends SplitWorker { + + BaseSegmentsSyncWorker(@NonNull Context context, + @NonNull WorkerParameters workerParams) { + + super(context, workerParams); + String[] keys = + workerParams.getInputData().getStringArray(ServiceConstants.WORKER_PARAM_KEY); + String apiKey = workerParams.getInputData().getString(ServiceConstants.WORKER_PARAM_API_KEY); + boolean isEncryptionEnabled = workerParams.getInputData().getBoolean(ServiceConstants.WORKER_PARAM_ENCRYPTION_ENABLED, + false); + boolean shouldRecordTelemetry = workerParams.getInputData().getBoolean(ServiceConstants.SHOULD_RECORD_TELEMETRY, false); + try { + if (keys == null) { + Logger.e("Error scheduling segments sync worker: Keys are null"); + return; + } + + mSplitTask = new MySegmentsBulkSyncTask(Collections.unmodifiableSet(getIndividualMySegmentsSyncTasks(keys, + shouldRecordTelemetry, + getHttpClient(), + getEndPoint(), + getDatabase(), + apiKey, + isEncryptionEnabled))); + + } catch (URISyntaxException e) { + Logger.e("Error creating Split worker: " + e.getMessage()); + } + } + + private Set getIndividualMySegmentsSyncTasks(String[] keys, + boolean shouldRecordTelemetry, + HttpClient httpClient, + String endPoint, + SplitRoomDatabase database, + String apiKey, + boolean isEncryptionEnabled) throws URISyntaxException { + Set mySegmentsSyncTasks = new HashSet<>(); + for (String key : keys) { + mySegmentsSyncTasks.add( + getTask(shouldRecordTelemetry, httpClient, endPoint, database, apiKey, isEncryptionEnabled, key)); + } + + return mySegmentsSyncTasks; + } + + protected abstract @NonNull MySegmentsSyncTask getTask(boolean shouldRecordTelemetry, HttpClient httpClient, String endPoint, SplitRoomDatabase database, String apiKey, boolean isEncryptionEnabled, String key) throws URISyntaxException; +} diff --git a/src/main/java/io/split/android/client/service/workmanager/MySegmentsSyncWorker.java b/src/main/java/io/split/android/client/service/workmanager/MySegmentsSyncWorker.java index f3f84f02a..a55814375 100644 --- a/src/main/java/io/split/android/client/service/workmanager/MySegmentsSyncWorker.java +++ b/src/main/java/io/split/android/client/service/workmanager/MySegmentsSyncWorker.java @@ -6,70 +6,35 @@ import androidx.work.WorkerParameters; import java.net.URISyntaxException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; import io.split.android.client.network.HttpClient; -import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.ServiceFactory; -import io.split.android.client.service.mysegments.MySegmentsBulkSyncTask; import io.split.android.client.service.mysegments.MySegmentsSyncTask; +import io.split.android.client.service.mysegments.MySegmentsSyncTaskConfig; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.storage.db.StorageFactory; -import io.split.android.client.utils.logger.Logger; -public class MySegmentsSyncWorker extends SplitWorker { +public class MySegmentsSyncWorker extends BaseSegmentsSyncWorker { public MySegmentsSyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); - String[] keys = - workerParams.getInputData().getStringArray(ServiceConstants.WORKER_PARAM_KEY); - String apiKey = workerParams.getInputData().getString(ServiceConstants.WORKER_PARAM_API_KEY); - boolean isEncryptionEnabled = workerParams.getInputData().getBoolean(ServiceConstants.WORKER_PARAM_ENCRYPTION_ENABLED, - false); - boolean shouldRecordTelemetry = workerParams.getInputData().getBoolean(ServiceConstants.SHOULD_RECORD_TELEMETRY, false); - try { - if (keys == null) { - Logger.e("Error scheduling segments sync worker: Keys are null"); - return; - } - - mSplitTask = new MySegmentsBulkSyncTask(Collections.unmodifiableSet(getIndividualMySegmentsSyncTasks(keys, - shouldRecordTelemetry, - getHttpClient(), - getEndPoint(), - getDatabase(), - apiKey, - isEncryptionEnabled))); - - } catch (URISyntaxException e) { - Logger.e("Error creating Split worker: " + e.getMessage()); - } } - private static Set getIndividualMySegmentsSyncTasks(String[] keys, - boolean shouldRecordTelemetry, - HttpClient httpClient, - String endPoint, - SplitRoomDatabase database, - String apiKey, - boolean isEncryptionEnabled) throws URISyntaxException { - Set mySegmentsSyncTasks = new HashSet<>(); - for (String key : keys) { - mySegmentsSyncTasks.add( - new MySegmentsSyncTask( - ServiceFactory.getMySegmentsFetcher(httpClient, - endPoint, key), - StorageFactory.getMySegmentsStorageForWorker(database, apiKey, isEncryptionEnabled).getStorageForKey(key), - false, - null, - StorageFactory.getTelemetryStorage(shouldRecordTelemetry)) - ); - } - - return mySegmentsSyncTasks; + @NonNull + @Override + protected MySegmentsSyncTask getTask(boolean shouldRecordTelemetry, HttpClient httpClient, String endPoint, SplitRoomDatabase database, String apiKey, boolean isEncryptionEnabled, String key) throws URISyntaxException { + return new MySegmentsSyncTask( + ServiceFactory.getMySegmentsFetcher(httpClient, + endPoint, key), + StorageFactory.getMySegmentsStorageForWorker(database, apiKey, isEncryptionEnabled).getStorageForKey(key), + StorageFactory.getMyLargeSegmentsStorageForWorker(database, apiKey, isEncryptionEnabled).getStorageForKey(key), + false, + null, + StorageFactory.getTelemetryStorage(shouldRecordTelemetry), + MySegmentsSyncTaskConfig.get(), + null, + null); } } diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java b/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java index 26519f20b..d1fa25c7a 100644 --- a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java +++ b/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java @@ -1,7 +1,5 @@ package io.split.android.client.service.workmanager.splits; -import androidx.annotation.NonNull; - import java.net.URISyntaxException; import io.split.android.client.service.executor.SplitTask; diff --git a/src/main/java/io/split/android/client/shared/ClientComponentsRegister.java b/src/main/java/io/split/android/client/shared/ClientComponentsRegister.java index 935aac005..8411c009e 100644 --- a/src/main/java/io/split/android/client/shared/ClientComponentsRegister.java +++ b/src/main/java/io/split/android/client/shared/ClientComponentsRegister.java @@ -5,7 +5,7 @@ import io.split.android.client.service.mysegments.MySegmentsTaskFactory; public interface ClientComponentsRegister { - void registerComponents(Key key, MySegmentsTaskFactory mySegmentsTaskFactory, SplitEventsManager eventsManager); + void registerComponents(Key key, SplitEventsManager eventsManager, MySegmentsTaskFactory mySegmentsTaskFactory); void unregisterComponentsForKey(Key key); } diff --git a/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java b/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java index dfe56b4a8..de1d53414 100644 --- a/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java +++ b/src/main/java/io/split/android/client/shared/ClientComponentsRegisterImpl.java @@ -11,13 +11,14 @@ import io.split.android.client.api.Key; import io.split.android.client.events.EventsManagerRegistry; import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.service.attributes.AttributeTaskFactoryImpl; +import io.split.android.client.service.mysegments.MySegmentUpdateParams; import io.split.android.client.service.mysegments.MySegmentsTaskFactory; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeNotification; import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder; -import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessor; +import io.split.android.client.service.sseclient.notifications.memberships.MembershipsNotificationProcessor; import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessorConfiguration; -import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessorFactory; +import io.split.android.client.service.sseclient.notifications.mysegments.MembershipsNotificationProcessorFactory; import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessorRegistry; import io.split.android.client.service.sseclient.reactor.MySegmentsUpdateWorker; import io.split.android.client.service.sseclient.reactor.MySegmentsUpdateWorkerRegistry; @@ -28,13 +29,13 @@ import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizer; import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerFactory; import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerRegistry; -import io.split.android.client.storage.common.SplitStorageContainer; import io.split.android.client.storage.attributes.AttributesStorage; +import io.split.android.client.storage.common.SplitStorageContainer; public class ClientComponentsRegisterImpl implements ClientComponentsRegister { private final MySegmentsSynchronizerFactory mMySegmentsSynchronizerFactory; - private final MySegmentsNotificationProcessorFactory mMySegmentsNotificationProcessorFactory; + private final MembershipsNotificationProcessorFactory mMembershipsNotificationProcessorFactory; private final SplitStorageContainer mStorageContainer; private final AttributesSynchronizerFactory mAttributesSynchronizerFactory; private final AttributesSynchronizerRegistry mAttributesSynchronizerRegistry; @@ -56,7 +57,7 @@ public ClientComponentsRegisterImpl(@NonNull SplitClientConfig splitConfig, @NonNull EventsManagerRegistry eventsManagerRegistry, @Nullable SseAuthenticator sseAuthenticator, @Nullable MySegmentsNotificationProcessorRegistry mySegmentsNotificationProcessorRegistry, - @Nullable MySegmentsNotificationProcessorFactory mySegmentsNotificationProcessorFactory, + @Nullable MembershipsNotificationProcessorFactory membershipsNotificationProcessorFactory, @Nullable MySegmentsV2PayloadDecoder mySegmentsV2PayloadDecoder) { mSplitConfig = splitConfig; mMySegmentsSynchronizerFactory = checkNotNull(mySegmentsSynchronizerFactory); @@ -70,20 +71,23 @@ public ClientComponentsRegisterImpl(@NonNull SplitClientConfig splitConfig, mMySegmentsNotificationProcessorRegistry = mySegmentsNotificationProcessorRegistry; mMySegmentsUpdateWorkerRegistry = mySegmentsUpdateWorkerRegistry; mSseAuthenticator = sseAuthenticator; - mMySegmentsNotificationProcessorFactory = mySegmentsNotificationProcessorFactory; + mMembershipsNotificationProcessorFactory = membershipsNotificationProcessorFactory; mMySegmentsV2PayloadDecoder = mySegmentsV2PayloadDecoder; } @Override - public void registerComponents(Key key, MySegmentsTaskFactory mySegmentsTaskFactory, SplitEventsManager eventsManager) { + public void registerComponents(Key key, SplitEventsManager eventsManager, MySegmentsTaskFactory mySegmentsTaskFactory) { registerEventsManager(key, eventsManager); - MySegmentsSynchronizer mySegmentsSynchronizer = mMySegmentsSynchronizerFactory.getSynchronizer(mySegmentsTaskFactory, eventsManager); + + MySegmentsSynchronizer mySegmentsSynchronizer = mMySegmentsSynchronizerFactory.getSynchronizer(mySegmentsTaskFactory, eventsManager, SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE, mSplitConfig.segmentsRefreshRate()); registerMySegmentsSynchronizer(key, mySegmentsSynchronizer); + registerAttributesSynchronizer(key, eventsManager); + if (isSyncEnabled()) { registerKeyInSeeAuthenticator(key); - LinkedBlockingDeque mySegmentsNotificationQueue = new LinkedBlockingDeque<>(); - registerMySegmentsNotificationProcessor(key, mySegmentsTaskFactory, mySegmentsNotificationQueue); + LinkedBlockingDeque mySegmentsNotificationQueue = new LinkedBlockingDeque<>(); + registerMembershipsNotificationProcessor(key, mySegmentsTaskFactory, mySegmentsNotificationQueue); registerMySegmentsUpdateWorker(key, mySegmentsSynchronizer, mySegmentsNotificationQueue); } } @@ -97,7 +101,7 @@ public void unregisterComponentsForKey(Key key) { if (isSyncEnabled()) { mSseAuthenticator.unregisterKey(key.matchingKey()); mMySegmentsUpdateWorkerRegistry.unregisterMySegmentsUpdateWorker(key.matchingKey()); - mMySegmentsNotificationProcessorRegistry.unregisterMySegmentsProcessor(key.matchingKey()); + mMySegmentsNotificationProcessorRegistry.unregisterMembershipsProcessor(key.matchingKey()); } } @@ -116,7 +120,7 @@ private void registerMySegmentsSynchronizer(Key key, MySegmentsSynchronizer mySe mySegmentsSynchronizer); } - private void registerMySegmentsUpdateWorker(Key key, MySegmentsSynchronizer mySegmentsSynchronizer, LinkedBlockingDeque notificationsQueue) { + private void registerMySegmentsUpdateWorker(Key key, MySegmentsSynchronizer mySegmentsSynchronizer, LinkedBlockingDeque notificationsQueue) { mMySegmentsUpdateWorkerRegistry.registerMySegmentsUpdateWorker(key.matchingKey(), new MySegmentsUpdateWorker(mySegmentsSynchronizer, notificationsQueue)); } @@ -129,19 +133,18 @@ private void registerKeyInSeeAuthenticator(Key key) { mSseAuthenticator.registerKey(key.matchingKey()); } - private void registerMySegmentsNotificationProcessor(Key key, MySegmentsTaskFactory mySegmentsTaskFactory, LinkedBlockingDeque notificationsQueue) { - MySegmentsNotificationProcessor processor = getMySegmentsNotificationProcessor(key, mySegmentsTaskFactory, notificationsQueue); - mMySegmentsNotificationProcessorRegistry.registerMySegmentsProcessor(key.matchingKey(), processor); + private void registerMembershipsNotificationProcessor(Key key, MySegmentsTaskFactory mySegmentsTaskFactory, LinkedBlockingDeque notificationsQueue) { + MembershipsNotificationProcessor processor = getMembershipsNotificationProcessor(key, mySegmentsTaskFactory, notificationsQueue); + mMySegmentsNotificationProcessorRegistry.registerMembershipsNotificationProcessor(key.matchingKey(), processor); } - private MySegmentsNotificationProcessor getMySegmentsNotificationProcessor(Key key, MySegmentsTaskFactory mySegmentsTaskFactory, LinkedBlockingDeque mySegmentUpdateNotificationsQueue) { - return mMySegmentsNotificationProcessorFactory.getProcessor( + private MembershipsNotificationProcessor getMembershipsNotificationProcessor(Key key, MySegmentsTaskFactory mySegmentsTaskFactory, LinkedBlockingDeque mySegmentUpdateNotificationsQueue) { + return mMembershipsNotificationProcessorFactory.getProcessor( new MySegmentsNotificationProcessorConfiguration( mySegmentsTaskFactory, mySegmentUpdateNotificationsQueue, - mMySegmentsV2PayloadDecoder.hashKey(key.matchingKey()) - ) - ); + key.matchingKey(), + mMySegmentsV2PayloadDecoder.hashKey(key.matchingKey()))); } private boolean isSyncEnabled() { diff --git a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java index 64535d7bb..9d50f645d 100644 --- a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java +++ b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java @@ -147,7 +147,7 @@ public void createNewClient(Key key) { SplitClient client = mSplitClientFactory.getClient(key, mySegmentsTaskFactory, eventsManager, mDefaultMatchingKey.equals(key.matchingKey())); trackNewClient(key, client); - mClientComponentsRegister.registerComponents(key, mySegmentsTaskFactory, eventsManager); + mClientComponentsRegister.registerComponents(key, eventsManager, mySegmentsTaskFactory); if (mConfig.syncEnabled() && mStreamingEnabled) { connectToStreaming(); @@ -159,14 +159,14 @@ public void createNewClient(Key key) { } } + @NonNull private MySegmentsTaskFactory getMySegmentsTaskFactory(Key key, SplitEventsManager eventsManager) { return mMySegmentsTaskFactoryProvider.getFactory( - new MySegmentsTaskFactoryConfiguration( + MySegmentsTaskFactoryConfiguration.get( mSplitApiFacade.getMySegmentsFetcher(key.matchingKey()), mStorageContainer.getMySegmentsStorage(key.matchingKey()), - eventsManager - ) - ); + mStorageContainer.getMyLargeSegmentsStorage(key.matchingKey()), + eventsManager)); } private void connectToStreaming() { diff --git a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java b/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java index 0aa6c88f0..adb47d63a 100644 --- a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java +++ b/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java @@ -13,8 +13,12 @@ import io.split.android.client.storage.db.ImpressionEntity; import io.split.android.client.storage.db.ImpressionsCountDao; import io.split.android.client.storage.db.ImpressionsCountEntity; +import io.split.android.client.storage.db.MyLargeSegmentDao; +import io.split.android.client.storage.db.MyLargeSegmentEntity; import io.split.android.client.storage.db.MySegmentDao; import io.split.android.client.storage.db.MySegmentEntity; +import io.split.android.client.storage.db.SegmentDao; +import io.split.android.client.storage.db.SegmentEntity; import io.split.android.client.storage.db.SplitDao; import io.split.android.client.storage.db.SplitEntity; import io.split.android.client.storage.db.SplitRoomDatabase; @@ -47,6 +51,7 @@ public SplitTaskExecutionInfo execute() { public void run() { updateSplits(mSplitDatabase.splitDao()); updateSegments(mSplitDatabase.mySegmentDao()); + updateLargeSegments(mSplitDatabase.myLargeSegmentDao()); updateImpressions(mSplitDatabase.impressionDao()); updateEvents(mSplitDatabase.eventDao()); updateImpressionsCount(mSplitDatabase.impressionsCountDao()); @@ -140,7 +145,17 @@ private void updateImpressions(ImpressionDao impressionDao) { private void updateSegments(MySegmentDao mySegmentDao) { List items = mySegmentDao.getAll(); - for (MySegmentEntity item : items) { + updateSegments(mySegmentDao, items); + } + + private void updateLargeSegments(MyLargeSegmentDao myLargeSegmentDao) { + List items = myLargeSegmentDao.getAll(); + + updateSegments(myLargeSegmentDao, items); + } + + private void updateSegments(SegmentDao mySegmentDao, List items) { + for (SegmentEntity item : items) { String userKey = item.getUserKey(); String fromUserKey = mFromCipher.decrypt(userKey); String fromBody = mFromCipher.decrypt(item.getSegmentList()); @@ -151,7 +166,7 @@ private void updateSegments(MySegmentDao mySegmentDao) { if (toUserKey != null && toBody != null) { mySegmentDao.update(userKey, toUserKey, toBody); } else { - Logger.e("Error applying cipher to my segment"); + Logger.e("Error applying cipher to my " + (item instanceof MyLargeSegmentEntity ? "large" : "") + " segment"); } } } diff --git a/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java b/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java index 837ff6390..af0f62f98 100644 --- a/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java +++ b/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java @@ -24,6 +24,7 @@ public class SplitStorageContainer { private final SplitsStorage mSplitStorage; private final MySegmentsStorageContainer mMySegmentsStorageContainer; + private final MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; private final PersistentSplitsStorage mPersistentSplitsStorage; private final PersistentEventsStorage mPersistentEventsStorage; private final EventsStorage mEventsStorage; @@ -38,6 +39,7 @@ public class SplitStorageContainer { public SplitStorageContainer(@NonNull SplitsStorage splitStorage, @NonNull MySegmentsStorageContainer mySegmentsStorageContainer, + @NonNull MySegmentsStorageContainer myLargeSegmentsStorageContainer, @NonNull PersistentSplitsStorage persistentSplitsStorage, @NonNull EventsStorage eventsStorage, @NonNull PersistentEventsStorage persistentEventsStorage, @@ -52,6 +54,7 @@ public SplitStorageContainer(@NonNull SplitsStorage splitStorage, mSplitStorage = checkNotNull(splitStorage); mMySegmentsStorageContainer = checkNotNull(mySegmentsStorageContainer); + mMyLargeSegmentsStorageContainer = checkNotNull(myLargeSegmentsStorageContainer); mPersistentSplitsStorage = checkNotNull(persistentSplitsStorage); mEventsStorage = checkNotNull(eventsStorage); mPersistentEventsStorage = checkNotNull(persistentEventsStorage); @@ -73,10 +76,18 @@ public MySegmentsStorageContainer getMySegmentsStorageContainer() { return mMySegmentsStorageContainer; } + public MySegmentsStorageContainer getMyLargeSegmentsStorageContainer() { + return mMyLargeSegmentsStorageContainer; + } + public MySegmentsStorage getMySegmentsStorage(String matchingKey) { return mMySegmentsStorageContainer.getStorageForKey(matchingKey); } + public MySegmentsStorage getMyLargeSegmentsStorage(String matchingKey) { + return mMyLargeSegmentsStorageContainer.getStorageForKey(matchingKey); + } + public PersistentSplitsStorage getPersistentSplitsStorage() { return mPersistentSplitsStorage; } diff --git a/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java b/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java new file mode 100644 index 000000000..c770c753a --- /dev/null +++ b/src/main/java/io/split/android/client/storage/db/MyLargeSegmentDao.java @@ -0,0 +1,30 @@ +package io.split.android.client.storage.db; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import java.util.List; + +@Dao +public interface MyLargeSegmentDao extends SegmentDao { + + String TABLE_NAME = "my_large_segments"; + + @Override + @Insert(onConflict = OnConflictStrategy.REPLACE) + void update(MyLargeSegmentEntity mySegment); + + @Override + @Query("UPDATE " + TABLE_NAME + " SET user_key = :userKey, segment_list = :segmentList WHERE user_key = :formerUserKey") + void update(String formerUserKey, String userKey, String segmentList); + + @Override + @Query("SELECT user_key, segment_list, updated_at FROM " + TABLE_NAME + " WHERE user_key = :userKey") + MyLargeSegmentEntity getByUserKey(String userKey); + + @Override + @Query("SELECT user_key, segment_list, updated_at FROM " + TABLE_NAME) + List getAll(); +} diff --git a/src/main/java/io/split/android/client/storage/db/MyLargeSegmentEntity.java b/src/main/java/io/split/android/client/storage/db/MyLargeSegmentEntity.java new file mode 100644 index 000000000..1a7cd76fc --- /dev/null +++ b/src/main/java/io/split/android/client/storage/db/MyLargeSegmentEntity.java @@ -0,0 +1,23 @@ +package io.split.android.client.storage.db; + +import androidx.room.Entity; + +@Entity(tableName = "my_large_segments") +public class MyLargeSegmentEntity extends SegmentEntity { + + public static final Creator CREATOR = new Creator() { + @Override + public MyLargeSegmentEntity createEntity(String userKey, String segmentList, long updatedAt) { + MyLargeSegmentEntity entity = new MyLargeSegmentEntity(); + entity.setUserKey(userKey); + entity.setSegmentList(segmentList); + entity.setUpdatedAt(updatedAt); + + return entity; + } + }; + + public static Creator creator() { + return CREATOR; + } +} diff --git a/src/main/java/io/split/android/client/storage/db/MySegmentDao.java b/src/main/java/io/split/android/client/storage/db/MySegmentDao.java index a43ab3cd9..b4c6ef5d7 100644 --- a/src/main/java/io/split/android/client/storage/db/MySegmentDao.java +++ b/src/main/java/io/split/android/client/storage/db/MySegmentDao.java @@ -8,17 +8,23 @@ import java.util.List; @Dao -public interface MySegmentDao { +public interface MySegmentDao extends SegmentDao { + String TABLE_NAME = "my_segments"; + + @Override @Insert(onConflict = OnConflictStrategy.REPLACE) void update(MySegmentEntity mySegment); - @Query("UPDATE my_segments SET user_key = :userKey, segment_list = :segmentList WHERE user_key = :formerUserKey") + @Override + @Query("UPDATE " + TABLE_NAME + " SET user_key = :userKey, segment_list = :segmentList WHERE user_key = :formerUserKey") void update(String formerUserKey, String userKey, String segmentList); - @Query("SELECT user_key, segment_list, updated_at FROM my_segments WHERE user_key = :userKey") + @Override + @Query("SELECT user_key, segment_list, updated_at FROM " + TABLE_NAME + " WHERE user_key = :userKey") MySegmentEntity getByUserKey(String userKey); - @Query("SELECT user_key, segment_list, updated_at FROM my_segments") + @Override + @Query("SELECT user_key, segment_list, updated_at FROM " + TABLE_NAME) List getAll(); } diff --git a/src/main/java/io/split/android/client/storage/db/MySegmentEntity.java b/src/main/java/io/split/android/client/storage/db/MySegmentEntity.java index 13d24581c..8c93e2dc2 100644 --- a/src/main/java/io/split/android/client/storage/db/MySegmentEntity.java +++ b/src/main/java/io/split/android/client/storage/db/MySegmentEntity.java @@ -1,48 +1,23 @@ package io.split.android.client.storage.db; -import androidx.annotation.NonNull; -import androidx.room.ColumnInfo; import androidx.room.Entity; -import androidx.room.PrimaryKey; @Entity(tableName = "my_segments") -public class MySegmentEntity { - - @PrimaryKey() - @NonNull - @ColumnInfo(name = "user_key") - private String userKey; - - @NonNull - @ColumnInfo(name = "segment_list") - private String segmentList; - - @ColumnInfo(name = "updated_at") - private long updatedAt; - - @NonNull - public String getUserKey() { - return userKey; - } - - public void setUserKey(String userKey) { - this.userKey = userKey; - } - - @NonNull - public String getSegmentList() { - return segmentList; - } - - public void setSegmentList(@NonNull String segmentList) { - this.segmentList = segmentList; - } - - public long getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(long updatedAt) { - this.updatedAt = updatedAt; +public class MySegmentEntity extends SegmentEntity { + + private final static Creator CREATOR = new Creator() { + @Override + public MySegmentEntity createEntity(String userKey, String segmentList, long updatedAt) { + MySegmentEntity entity = new MySegmentEntity(); + entity.setUserKey(userKey); + entity.setSegmentList(segmentList); + entity.setUpdatedAt(updatedAt); + + return entity; + } + }; + + public static Creator creator() { + return CREATOR; } } diff --git a/src/main/java/io/split/android/client/storage/db/SegmentDao.java b/src/main/java/io/split/android/client/storage/db/SegmentDao.java new file mode 100644 index 000000000..6f6e45a66 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/db/SegmentDao.java @@ -0,0 +1,14 @@ +package io.split.android.client.storage.db; + +import java.util.List; + +public interface SegmentDao { + + void update(T mySegment); + + void update(String formerUserKey, String userKey, String segmentList); + + T getByUserKey(String userKey); + + List getAll(); +} diff --git a/src/main/java/io/split/android/client/storage/db/SegmentEntity.java b/src/main/java/io/split/android/client/storage/db/SegmentEntity.java new file mode 100644 index 000000000..f379ec631 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/db/SegmentEntity.java @@ -0,0 +1,57 @@ +package io.split.android.client.storage.db; + +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.PrimaryKey; + +/** + * Base class for segment entities. This entity does not correspond to any actual SQL table + */ +public abstract class SegmentEntity { + + @PrimaryKey() + @NonNull + @ColumnInfo(name = "user_key") + private String userKey; + + @NonNull + @ColumnInfo(name = "segment_list") + private String segmentList; + + @ColumnInfo(name = "updated_at") + private long updatedAt; + + @NonNull + public String getUserKey() { + return userKey; + } + + public void setUserKey(String userKey) { + this.userKey = userKey; + } + + @NonNull + public String getSegmentList() { + return segmentList; + } + + public void setSegmentList(@NonNull String segmentList) { + this.segmentList = segmentList; + } + + public long getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(long updatedAt) { + this.updatedAt = updatedAt; + } + + public static Creator creator() { + return null; + } + + public interface Creator { + T createEntity(String userKey, String segmentList, long updatedAt); + } +} diff --git a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java b/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java index 8124e5ea3..273208e52 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java +++ b/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java @@ -23,14 +23,17 @@ entities = { MySegmentEntity.class, SplitEntity.class, EventEntity.class, ImpressionEntity.class, GeneralInfoEntity.class, ImpressionsCountEntity.class, - AttributesEntity.class, UniqueKeyEntity.class, ImpressionsObserverCacheEntity.class + AttributesEntity.class, UniqueKeyEntity.class, ImpressionsObserverCacheEntity.class, + MyLargeSegmentEntity.class }, - version = 5 + version = 6 ) public abstract class SplitRoomDatabase extends RoomDatabase { public abstract MySegmentDao mySegmentDao(); + public abstract MyLargeSegmentDao myLargeSegmentDao(); + public abstract SplitDao splitDao(); public abstract EventDao eventDao(); diff --git a/src/main/java/io/split/android/client/storage/db/StorageFactory.java b/src/main/java/io/split/android/client/storage/db/StorageFactory.java index ad9bf63f0..f4d6b9be1 100644 --- a/src/main/java/io/split/android/client/storage/db/StorageFactory.java +++ b/src/main/java/io/split/android/client/storage/db/StorageFactory.java @@ -55,6 +55,14 @@ public static MySegmentsStorageContainer getMySegmentsStorageForWorker(SplitRoom return getMySegmentsStorageContainer(splitRoomDatabase, SplitCipherFactory.create(apiKey, encryptionEnabled)); } + public static MySegmentsStorageContainer getMyLargeSegmentsStorage(SplitRoomDatabase splitRoomDatabase, SplitCipher splitCipher) { + return getMyLargeSegmentsStorageContainer(splitRoomDatabase, splitCipher); + } + + public static MySegmentsStorageContainer getMyLargeSegmentsStorageForWorker(SplitRoomDatabase splitRoomDatabase, String apiKey, boolean encryptionEnabled) { + return getMyLargeSegmentsStorageContainer(splitRoomDatabase, SplitCipherFactory.create(apiKey, encryptionEnabled)); + } + public static EventsStorage getEventsStorage(PersistentEventsStorage persistentEventsStorage, boolean isPersistenceEnabled) { return new EventsStorage(persistentEventsStorage, isPersistenceEnabled); @@ -124,7 +132,11 @@ public static TelemetryStorage getTelemetryStorage(boolean shouldRecordTelemetry } private static MySegmentsStorageContainer getMySegmentsStorageContainer(SplitRoomDatabase splitRoomDatabase, SplitCipher splitCipher) { - return new MySegmentsStorageContainerImpl(new SqLitePersistentMySegmentsStorage(splitRoomDatabase, splitCipher)); + return new MySegmentsStorageContainerImpl(new SqLitePersistentMySegmentsStorage<>(splitCipher, splitRoomDatabase.mySegmentDao(), MySegmentEntity.creator())); + } + + private static MySegmentsStorageContainer getMyLargeSegmentsStorageContainer(SplitRoomDatabase splitRoomDatabase, SplitCipher splitCipher) { + return new MySegmentsStorageContainerImpl(new SqLitePersistentMySegmentsStorage<>(splitCipher, splitRoomDatabase.myLargeSegmentDao(), MyLargeSegmentEntity.creator())); } private static AttributesStorageContainer getAttributesStorageContainerInstance() { diff --git a/src/main/java/io/split/android/client/storage/mysegments/EmptyMySegmentsStorage.java b/src/main/java/io/split/android/client/storage/mysegments/EmptyMySegmentsStorage.java index 584bad5e6..39a65bf89 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/EmptyMySegmentsStorage.java +++ b/src/main/java/io/split/android/client/storage/mysegments/EmptyMySegmentsStorage.java @@ -1,11 +1,10 @@ package io.split.android.client.storage.mysegments; -import androidx.annotation.NonNull; - import java.util.HashSet; -import java.util.List; import java.util.Set; +import io.split.android.client.dtos.SegmentsChange; + public class EmptyMySegmentsStorage implements MySegmentsStorage{ @Override public void loadLocal() { @@ -17,7 +16,12 @@ public Set getAll() { } @Override - public void set(@NonNull List mySegments) { + public void set(SegmentsChange segmentsChange) { + } + + @Override + public long getChangeNumber() { + return -1; } @Override diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java index 3b05da6c3..351a82d79 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java +++ b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java @@ -1,16 +1,20 @@ package io.split.android.client.storage.mysegments; -import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; -import java.util.List; import java.util.Set; +import io.split.android.client.dtos.SegmentsChange; + public interface MySegmentsStorage { void loadLocal(); Set getAll(); - void set(@NonNull List mySegments); + void set(SegmentsChange segmentsChange); + + long getChangeNumber(); + @VisibleForTesting void clear(); } diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java index 7688fdda3..4478a9a81 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java +++ b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageImpl.java @@ -3,28 +3,38 @@ import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; +import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import io.split.android.client.dtos.Segment; +import io.split.android.client.dtos.SegmentsChange; class MySegmentsStorageImpl implements MySegmentsStorage { + public static final int DEFAULT_CHANGE_NUMBER = -1; private final String mMatchingKey; private final PersistentMySegmentsStorage mPersistentStorage; private final Set mInMemoryMySegments; + private final AtomicLong mTill; public MySegmentsStorageImpl(@NonNull String matchingKey, @NonNull PersistentMySegmentsStorage persistentStorage) { mPersistentStorage = checkNotNull(persistentStorage); mMatchingKey = checkNotNull(matchingKey); mInMemoryMySegments = Collections.newSetFromMap(new ConcurrentHashMap<>()); + mTill = new AtomicLong(DEFAULT_CHANGE_NUMBER); } @Override public void loadLocal() { - mInMemoryMySegments.addAll(mPersistentStorage.getSnapshot(mMatchingKey)); + SegmentsChange snapshot = mPersistentStorage.getSnapshot(mMatchingKey); + mInMemoryMySegments.addAll(toNames(snapshot.getSegments())); + mTill.set(getOrDefault(snapshot.getChangeNumber())); } @Override @@ -33,18 +43,45 @@ public Set getAll() { } @Override - public void set(@NonNull List mySegments) { - if (mySegments == null) { + public void set(@NonNull SegmentsChange segmentsChange) { + if (segmentsChange == null) { return; } mInMemoryMySegments.clear(); - mInMemoryMySegments.addAll(mySegments); - mPersistentStorage.set(mMatchingKey, mySegments); + mInMemoryMySegments.addAll(toNames(segmentsChange.getSegments())); + mTill.set(getOrDefault(segmentsChange.getChangeNumber())); + mPersistentStorage.set(mMatchingKey, segmentsChange); + } + + @Override + public long getChangeNumber() { + return mTill.get(); } @Override + @VisibleForTesting public void clear() { mInMemoryMySegments.clear(); - mPersistentStorage.set(mMatchingKey, new ArrayList<>()); + mTill.set(DEFAULT_CHANGE_NUMBER); + mPersistentStorage.set(mMatchingKey, SegmentsChange.createEmpty()); + } + + @NonNull + private static Set toNames(Set segments) { + if (segments == null) { + return Collections.emptySet(); + } + + Set names = new HashSet<>(); + for (Segment segment : segments) { + names.add(segment.getName()); + } + + return names; + } + + @NonNull + private static Long getOrDefault(@Nullable Long changeNumber) { + return changeNumber == null ? DEFAULT_CHANGE_NUMBER : changeNumber; } } diff --git a/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java b/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java index 633b62d04..03ad60ae8 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java +++ b/src/main/java/io/split/android/client/storage/mysegments/PersistentMySegmentsStorage.java @@ -1,14 +1,12 @@ package io.split.android.client.storage.mysegments; -import androidx.annotation.NonNull; - -import java.util.List; +import io.split.android.client.dtos.SegmentsChange; public interface PersistentMySegmentsStorage { - void set(String userKey, @NonNull List mySegments); + void set(String userKey, SegmentsChange segmentsChange); - List getSnapshot(String userKey); + SegmentsChange getSnapshot(String userKey); void close(); } diff --git a/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java b/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java index 80d5c80b6..8d2da826d 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java +++ b/src/main/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorage.java @@ -1,72 +1,79 @@ package io.split.android.client.storage.mysegments; +import static io.split.android.client.dtos.SegmentsChange.createEmpty; import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; -import java.util.ArrayList; +import com.google.gson.JsonParseException; + import java.util.Arrays; -import java.util.List; +import java.util.HashSet; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.storage.cipher.SplitCipher; -import io.split.android.client.storage.db.MySegmentEntity; -import io.split.android.client.storage.db.SplitRoomDatabase; -import io.split.android.client.utils.StringHelper; +import io.split.android.client.storage.db.SegmentDao; +import io.split.android.client.storage.db.SegmentEntity; +import io.split.android.client.utils.Json; import io.split.android.client.utils.Utils; import io.split.android.client.utils.logger.Logger; -public class SqLitePersistentMySegmentsStorage implements PersistentMySegmentsStorage { +public class SqLitePersistentMySegmentsStorage implements PersistentMySegmentsStorage { - private final SplitRoomDatabase mDatabase; - private final StringHelper mStringHelper; + private final SegmentDao mDao; private final SplitCipher mSplitCipher; + private final SegmentEntity.Creator mCreator; - public SqLitePersistentMySegmentsStorage(@NonNull SplitRoomDatabase database, - @NonNull SplitCipher splitCipher) { - mDatabase = checkNotNull(database); - mStringHelper = new StringHelper(); + public SqLitePersistentMySegmentsStorage(@NonNull SplitCipher splitCipher, SegmentDao mySegmentDao, SegmentEntity.Creator creator) { + mDao = mySegmentDao; mSplitCipher = checkNotNull(splitCipher); + mCreator = checkNotNull(creator); } @Override - public void set(String userKey, @NonNull List mySegments) { - if (mySegments == null) { + public void set(String userKey, SegmentsChange segmentsChange) { + if (segmentsChange == null || segmentsChange.getSegments() == null) { return; } String encryptedUserKey = mSplitCipher.encrypt(userKey); - String encryptedSegmentList = mSplitCipher.encrypt(mStringHelper.join(",", mySegments)); - if (encryptedUserKey == null || encryptedSegmentList == null) { + String dto = Json.toJson(segmentsChange); + String encryptedDto = mSplitCipher.encrypt(dto); + if (encryptedUserKey == null || encryptedDto == null) { Logger.e("Error encrypting my segments"); return; } - MySegmentEntity entity = new MySegmentEntity(); - entity.setUserKey(encryptedUserKey); - entity.setSegmentList(encryptedSegmentList); - entity.setUpdatedAt(System.currentTimeMillis() / 1000); - mDatabase.mySegmentDao().update(entity); + T entity = mCreator.createEntity(encryptedUserKey, encryptedDto, System.currentTimeMillis() / 1000); + mDao.update(entity); } @Override - public List getSnapshot(String userKey) { + public SegmentsChange getSnapshot(String userKey) { String encryptedUserKey = mSplitCipher.encrypt(userKey); - return getMySegmentsFromEntity(mDatabase.mySegmentDao().getByUserKey(encryptedUserKey)); + return getMySegmentsFromEntity(mDao.getByUserKey(encryptedUserKey)); } @Override public void close() { } - private List getMySegmentsFromEntity(MySegmentEntity entity) { + private SegmentsChange getMySegmentsFromEntity(SegmentEntity entity) { if (entity == null || Utils.isNullOrEmpty(entity.getSegmentList())) { - return new ArrayList<>(); + return createEmpty(); + } + + String storedJson = mSplitCipher.decrypt(entity.getSegmentList()); + if (storedJson == null) { + return createEmpty(); } - String segmentList = mSplitCipher.decrypt(entity.getSegmentList()); - if (segmentList == null) { - return new ArrayList<>(); + try { + return Json.fromJson(storedJson, SegmentsChange.class); + } catch (JsonParseException | NullPointerException ex) { + Logger.v("Parsing of segments DTO failed, returning as legacy"); + String[] segments = storedJson.split(","); + + return SegmentsChange.create(new HashSet<>(Arrays.asList(segments)), null); } - String[] segments = segmentList.split(","); - return Arrays.asList(segments); } } 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 bf6849271..5c24ab0cd 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 @@ -66,6 +66,12 @@ public class Config { @SerializedName("fsI") private int flagSetsInvalid; + @SerializedName("lsE") + private boolean largeSegmentsEnabled; + + @SerializedName("wls") + private boolean waitForLargeSegments; + public int getOperationMode() { return operationMode; } @@ -217,4 +223,20 @@ public int getFlagSetsInvalid() { public void setFlagSetsInvalid(int flagSetsInvalid) { this.flagSetsInvalid = flagSetsInvalid; } + + public boolean largeSegmentsEnabled() { + return largeSegmentsEnabled; + } + + public void setLargeSegmentsEnabled(boolean largeSegmentsEnabled) { + this.largeSegmentsEnabled = largeSegmentsEnabled; + } + + public boolean getWaitForLargeSegments() { + return waitForLargeSegments; + } + + public void setWaitForLargeSegments(boolean waitForLargeSegments) { + this.waitForLargeSegments = waitForLargeSegments; + } } diff --git a/src/main/java/io/split/android/client/telemetry/model/HttpErrors.java b/src/main/java/io/split/android/client/telemetry/model/HttpErrors.java index 75352efaa..da32619ca 100644 --- a/src/main/java/io/split/android/client/telemetry/model/HttpErrors.java +++ b/src/main/java/io/split/android/client/telemetry/model/HttpErrors.java @@ -12,6 +12,9 @@ public class HttpErrors { @SerializedName("ms") private Map mySegmentSyncErrs; + @SerializedName("mls") + private Map myLargeSegmentsSyncErrs; + @SerializedName("im") private Map impressionSyncErrs; @@ -43,6 +46,14 @@ public void setMySegmentSyncErrs(Map mySegmentSyncErrs) { this.mySegmentSyncErrs = mySegmentSyncErrs; } + public Map getMyLargeSegmentsSyncErrs() { + return myLargeSegmentsSyncErrs; + } + + public void setMyLargeSegmentsSyncErrs(Map myLargeSegmentsSyncErrs) { + this.myLargeSegmentsSyncErrs = myLargeSegmentsSyncErrs; + } + public Map getImpressionSyncErrs() { return impressionSyncErrs; } diff --git a/src/main/java/io/split/android/client/telemetry/model/HttpLatencies.java b/src/main/java/io/split/android/client/telemetry/model/HttpLatencies.java index 22f80c6c2..d58e647e5 100644 --- a/src/main/java/io/split/android/client/telemetry/model/HttpLatencies.java +++ b/src/main/java/io/split/android/client/telemetry/model/HttpLatencies.java @@ -12,6 +12,9 @@ public class HttpLatencies { @SerializedName("ms") private List mySegments; + @SerializedName("mls") + private List myLargeSegments; + @SerializedName("im") private List impressions; @@ -43,6 +46,14 @@ public void setMySegments(List mySegments) { this.mySegments = mySegments; } + public List getMyLargeSegments() { + return myLargeSegments; + } + + public void setMyLargeSegments(List myLargeSegments) { + this.myLargeSegments = myLargeSegments; + } + public List getImpressions() { return impressions; } diff --git a/src/main/java/io/split/android/client/telemetry/model/LastSync.java b/src/main/java/io/split/android/client/telemetry/model/LastSync.java index 0f4dc5df3..e0e65653b 100644 --- a/src/main/java/io/split/android/client/telemetry/model/LastSync.java +++ b/src/main/java/io/split/android/client/telemetry/model/LastSync.java @@ -5,25 +5,28 @@ public class LastSync { @SerializedName("sp") - private long lastSplitSync; + private Long lastSplitSync; @SerializedName("ms") - private long lastMySegmentSync; + private Long lastMySegmentSync; + + @SerializedName("mls") + private Long lastMyLargeSegmentSync; @SerializedName("im") - private long lastImpressionSync; + private Long lastImpressionSync; @SerializedName("ic") - private long lastImpressionCountSync; + private Long lastImpressionCountSync; @SerializedName("ev") - private long lastEventSync; + private Long lastEventSync; @SerializedName("te") - private long lastTelemetrySync; + private Long lastTelemetrySync; @SerializedName("to") - private long lastTokenRefresh; + private Long lastTokenRefresh; public long getLastSplitSync() { return lastSplitSync; @@ -41,6 +44,14 @@ public void setLastMySegmentSync(long lastMySegmentSync) { this.lastMySegmentSync = lastMySegmentSync; } + public long getLastMyLargeSegmentSync() { + return lastMyLargeSegmentSync; + } + + public void setLastMyLargeSegmentSync(long lastMyLargeSegmentSync) { + this.lastMyLargeSegmentSync = lastMyLargeSegmentSync; + } + public long getLastImpressionSync() { return lastImpressionSync; } diff --git a/src/main/java/io/split/android/client/telemetry/model/OperationType.java b/src/main/java/io/split/android/client/telemetry/model/OperationType.java index edc8df96a..abd691e31 100644 --- a/src/main/java/io/split/android/client/telemetry/model/OperationType.java +++ b/src/main/java/io/split/android/client/telemetry/model/OperationType.java @@ -7,5 +7,6 @@ public enum OperationType { EVENTS, TELEMETRY, TOKEN, - MY_SEGMENT + MY_SEGMENT, + MY_LARGE_SEGMENT, } diff --git a/src/main/java/io/split/android/client/telemetry/model/RefreshRates.java b/src/main/java/io/split/android/client/telemetry/model/RefreshRates.java index 6588f2f3b..b5492a828 100644 --- a/src/main/java/io/split/android/client/telemetry/model/RefreshRates.java +++ b/src/main/java/io/split/android/client/telemetry/model/RefreshRates.java @@ -10,6 +10,9 @@ public class RefreshRates { @SerializedName("ms") private long mySegments; + @SerializedName("mls") + private long myLargeSegments; + @SerializedName("im") private long impressions; @@ -35,6 +38,14 @@ public void setMySegments(long mySegments) { this.mySegments = mySegments; } + public long getMyLargeSegments() { + return myLargeSegments; + } + + public void setMyLargeSegments(long myLargeSegments) { + this.myLargeSegments = myLargeSegments; + } + public long getImpressions() { return impressions; } 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 7b1a169e4..06438619a 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 @@ -46,6 +46,9 @@ public class Stats { @SerializedName("seC") private long segmentCount; + @SerializedName("lsC") + private long largeSegmentCount; + @SerializedName("skC") private final long segmentKeyCount = 1; @@ -115,6 +118,10 @@ public void setSegmentCount(long segmentCount) { this.segmentCount = segmentCount; } + public void setLargeSegmentCount(long largeSegmentCount) { + this.largeSegmentCount = largeSegmentCount; + } + public void setSessionLengthMs(long sessionLengthMs) { this.sessionLengthMs = sessionLengthMs; } @@ -207,6 +214,11 @@ public long getSegmentCount() { return segmentCount; } + @VisibleForTesting + public long getLargeSegmentCount() { + return largeSegmentCount; + } + @VisibleForTesting public long getSessionLengthMs() { return sessionLengthMs; 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 index 48b30c9a5..24504a0c9 100644 --- a/src/main/java/io/split/android/client/telemetry/model/UpdatesFromSSE.java +++ b/src/main/java/io/split/android/client/telemetry/model/UpdatesFromSSE.java @@ -10,9 +10,13 @@ public class UpdatesFromSSE { @SerializedName("ms") private long mMySegments; - public UpdatesFromSSE(long splits, long mySegments) { + @SerializedName("mls") + private long mMyLargeSegments; + + public UpdatesFromSSE(long splits, long mySegments, long myLargeSegments) { mSplits = splits; mMySegments = mySegments; + mMyLargeSegments = myLargeSegments; } public long getSplits() { @@ -22,4 +26,8 @@ public long getSplits() { public long getMySegments() { return mMySegments; } + + public long getMyLargeSegments() { + return mMyLargeSegments; + } } 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 index 1e4a1acaf..add69333d 100644 --- 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 @@ -4,4 +4,5 @@ public enum UpdatesFromSSEEnum { SPLITS, MY_SEGMENTS, + MY_LARGE_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 2c824efa2..6ee8507a6 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 @@ -181,6 +181,7 @@ public LastSync getLastSynchronization() { lastSync.setLastEventSync(lastSynchronizationData.get(OperationType.EVENTS).get()); lastSync.setLastSplitSync(lastSynchronizationData.get(OperationType.SPLITS).get()); lastSync.setLastMySegmentSync(lastSynchronizationData.get(OperationType.MY_SEGMENT).get()); + lastSync.setLastMyLargeSegmentSync(lastSynchronizationData.get(OperationType.MY_LARGE_SEGMENT).get()); lastSync.setLastTelemetrySync(lastSynchronizationData.get(OperationType.TELEMETRY).get()); lastSync.setLastImpressionSync(lastSynchronizationData.get(OperationType.IMPRESSIONS).get()); lastSync.setLastImpressionCountSync(lastSynchronizationData.get(OperationType.IMPRESSIONS_COUNT).get()); @@ -199,6 +200,7 @@ public HttpErrors popHttpErrors() { errors.setImpressionSyncErrs(httpErrors.get(OperationType.IMPRESSIONS)); errors.setSplitSyncErrs(httpErrors.get(OperationType.SPLITS)); errors.setMySegmentSyncErrs(httpErrors.get(OperationType.MY_SEGMENT)); + errors.setMyLargeSegmentsSyncErrs(httpErrors.get(OperationType.MY_LARGE_SEGMENT)); errors.setTokenGetErrs(httpErrors.get(OperationType.TOKEN)); initializeHttpErrors(); @@ -215,6 +217,7 @@ public HttpLatencies popHttpLatencies() { latencies.setEvents(popLatencies(OperationType.EVENTS)); latencies.setSplits(popLatencies(OperationType.SPLITS)); latencies.setMySegments(popLatencies(OperationType.MY_SEGMENT)); + latencies.setMyLargeSegments(popLatencies(OperationType.MY_LARGE_SEGMENT)); latencies.setToken(popLatencies(OperationType.TOKEN)); latencies.setImpressions(popLatencies(OperationType.IMPRESSIONS)); latencies.setImpressionsCount(popLatencies(OperationType.IMPRESSIONS_COUNT)); @@ -263,6 +266,7 @@ public UpdatesFromSSE popUpdatesFromSSE() { synchronized (updatesFromSSELock) { long sCount = 0L; long mCount = 0L; + long lCount = 0L; AtomicLong splits = updatesFromSSE.get(UpdatesFromSSEEnum.SPLITS); if (splits != null) { @@ -274,7 +278,12 @@ public UpdatesFromSSE popUpdatesFromSSE() { mCount = mySegments.getAndSet(0L); } - return new UpdatesFromSSE(sCount, mCount); + AtomicLong myLargeSegments = updatesFromSSE.get(UpdatesFromSSEEnum.MY_LARGE_SEGMENTS); + if (myLargeSegments != null) { + lCount = myLargeSegments.getAndSet(0L); + } + + return new UpdatesFromSSE(sCount, mCount, lCount); } } @@ -421,6 +430,7 @@ private void initializeLastSynchronizationData() { lastSynchronizationData.put(OperationType.TELEMETRY, new AtomicLong()); lastSynchronizationData.put(OperationType.EVENTS, new AtomicLong()); lastSynchronizationData.put(OperationType.MY_SEGMENT, new AtomicLong()); + lastSynchronizationData.put(OperationType.MY_LARGE_SEGMENT, new AtomicLong()); lastSynchronizationData.put(OperationType.SPLITS, new AtomicLong()); lastSynchronizationData.put(OperationType.TOKEN, new AtomicLong()); } @@ -430,6 +440,7 @@ private void initializeHttpErrors() { httpErrors.put(OperationType.SPLITS, new ConcurrentHashMap<>()); httpErrors.put(OperationType.TELEMETRY, new ConcurrentHashMap<>()); httpErrors.put(OperationType.MY_SEGMENT, new ConcurrentHashMap<>()); + httpErrors.put(OperationType.MY_LARGE_SEGMENT, new ConcurrentHashMap<>()); httpErrors.put(OperationType.IMPRESSIONS_COUNT, new ConcurrentHashMap<>()); httpErrors.put(OperationType.IMPRESSIONS, new ConcurrentHashMap<>()); httpErrors.put(OperationType.TOKEN, new ConcurrentHashMap<>()); @@ -441,6 +452,7 @@ private void initializeHttpLatencies() { httpLatencies.put(OperationType.TELEMETRY, new BinarySearchLatencyTracker()); httpLatencies.put(OperationType.IMPRESSIONS_COUNT, new BinarySearchLatencyTracker()); httpLatencies.put(OperationType.MY_SEGMENT, new BinarySearchLatencyTracker()); + httpLatencies.put(OperationType.MY_LARGE_SEGMENT, new BinarySearchLatencyTracker()); httpLatencies.put(OperationType.SPLITS, new BinarySearchLatencyTracker()); httpLatencies.put(OperationType.TOKEN, new BinarySearchLatencyTracker()); } @@ -453,6 +465,7 @@ private void initializePushCounters() { private void initializeUpdatesFromSSE() { updatesFromSSE.put(UpdatesFromSSEEnum.SPLITS, new AtomicLong()); updatesFromSSE.put(UpdatesFromSSEEnum.MY_SEGMENTS, new AtomicLong()); + updatesFromSSE.put(UpdatesFromSSEEnum.MY_LARGE_SEGMENTS, new AtomicLong()); } private List popLatencies(OperationType operationType) { 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 595cb0b17..839d88f0d 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 @@ -8,6 +8,8 @@ import static io.split.android.client.ServiceEndpoints.EndpointValidator.streamingEndpointIsOverridden; import static io.split.android.client.ServiceEndpoints.EndpointValidator.telemetryEndpointIsOverridden; +import android.os.Build; + import androidx.annotation.NonNull; import io.split.android.client.SplitClientConfig; @@ -15,6 +17,7 @@ import io.split.android.client.telemetry.model.Config; import io.split.android.client.telemetry.model.RefreshRates; import io.split.android.client.telemetry.model.UrlOverrides; +import io.split.android.client.utils.logger.Logger; public class TelemetryConfigProviderImpl implements TelemetryConfigProvider { @@ -36,6 +39,7 @@ public TelemetryConfigProviderImpl(@NonNull TelemetryStorageConsumer telemetryCo @Override public Config getConfigTelemetry() { Config config = new Config(); + addDefaultTags(mSplitClientConfig); config.setStreamingEnabled(mSplitClientConfig.streamingEnabled()); config.setRefreshRates(buildRefreshRates(mSplitClientConfig)); config.setTags(mTelemetryConsumer.popTags()); @@ -84,4 +88,20 @@ private UrlOverrides buildUrlOverrides(SplitClientConfig splitClientConfig) { return urlOverrides; } + + private void addDefaultTags(SplitClientConfig mSplitClientConfig) { + try { + TelemetryRuntimeProducer producer = (TelemetryRuntimeProducer) mTelemetryConsumer; + int sdkInt = Build.VERSION.SDK_INT; + if (sdkInt > 0) { + producer.addTag("av:" + sdkInt); + } + + if (mSplitClientConfig.synchronizeInBackground()) { + producer.addTag("bgr:" + mSplitClientConfig.backgroundSyncPeriod()); + } + } catch (ClassCastException ex) { + Logger.d("Telemetry storage is not a producer"); + } + } } 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 35783b09a..a7f2a2d1b 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 @@ -2,6 +2,8 @@ import static io.split.android.client.utils.Utils.checkNotNull; +import android.os.Build; + import androidx.annotation.NonNull; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; @@ -9,21 +11,25 @@ import io.split.android.client.telemetry.model.EventsDataRecordsEnum; import io.split.android.client.telemetry.model.ImpressionsDataType; import io.split.android.client.telemetry.model.Stats; +import io.split.android.client.utils.logger.Logger; public class TelemetryStatsProviderImpl implements TelemetryStatsProvider { private final TelemetryStorageConsumer mTelemetryStorageConsumer; private final SplitsStorage mSplitsStorage; private final MySegmentsStorageContainer mMySegmentsStorageContainer; + private final MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; private volatile Stats pendingStats = null; private final Object mLock = new Object(); public TelemetryStatsProviderImpl(@NonNull TelemetryStorageConsumer telemetryStorageConsumer, @NonNull SplitsStorage splitsStorage, - @NonNull MySegmentsStorageContainer mySegmentsStorage) { + @NonNull MySegmentsStorageContainer mySegmentsStorage, + @NonNull MySegmentsStorageContainer myLargeSegmentsStorage) { mTelemetryStorageConsumer = checkNotNull(telemetryStorageConsumer); mSplitsStorage = checkNotNull(splitsStorage); mMySegmentsStorageContainer = checkNotNull(mySegmentsStorage); + mMyLargeSegmentsStorageContainer = myLargeSegmentsStorage; } @Override @@ -46,12 +52,16 @@ public void clearStats() { private Stats buildStats() { Stats stats = new Stats(); + addDefaultTags(); stats.setStreamingEvents(mTelemetryStorageConsumer.popStreamingEvents()); stats.setSplitCount(mSplitsStorage.getAll().size()); stats.setTags(mTelemetryStorageConsumer.popTags()); stats.setMethodLatencies(mTelemetryStorageConsumer.popLatencies()); stats.setSegmentCount(mMySegmentsStorageContainer.getUniqueAmount()); + if (mMyLargeSegmentsStorageContainer != null) { + stats.setLargeSegmentCount(mMyLargeSegmentsStorageContainer.getUniqueAmount()); + } stats.setSessionLengthMs(mTelemetryStorageConsumer.getSessionLength()); stats.setLastSynchronizations(mTelemetryStorageConsumer.getLastSynchronization()); stats.setImpressionsDropped(mTelemetryStorageConsumer.getImpressionsStats(ImpressionsDataType.IMPRESSIONS_DROPPED)); @@ -68,4 +78,16 @@ private Stats buildStats() { return stats; } + + private void addDefaultTags() { + try { + TelemetryRuntimeProducer producer = (TelemetryRuntimeProducer) mTelemetryStorageConsumer; + int sdkInt = Build.VERSION.SDK_INT; + if (sdkInt > 0) { + producer.addTag("av:" + sdkInt); + } + } catch (ClassCastException ex) { + Logger.d("Telemetry storage is not a producer"); + } + } } diff --git a/src/main/java/io/split/android/client/utils/StringHelper.java b/src/main/java/io/split/android/client/utils/StringHelper.java index c63e00d6c..5fbfc39f7 100644 --- a/src/main/java/io/split/android/client/utils/StringHelper.java +++ b/src/main/java/io/split/android/client/utils/StringHelper.java @@ -50,4 +50,5 @@ public String join(String connector, Iterable values) { return string.toString(); } + } diff --git a/src/main/java/io/split/android/client/utils/Utils.java b/src/main/java/io/split/android/client/utils/Utils.java index 415cb0170..8341d776c 100644 --- a/src/main/java/io/split/android/client/utils/Utils.java +++ b/src/main/java/io/split/android/client/utils/Utils.java @@ -1,5 +1,6 @@ package io.split.android.client.utils; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; @@ -105,4 +106,9 @@ public static String repeat(String str, int count) { } return builder.toString(); } + + @NonNull + public static T getOrDefault(@Nullable T value, @NonNull T defaultValue) { + return value != null ? value : defaultValue; + } } diff --git a/src/main/java/io/split/android/engine/experiments/SplitParser.java b/src/main/java/io/split/android/engine/experiments/SplitParser.java index 598473a6d..2763ecca7 100644 --- a/src/main/java/io/split/android/engine/experiments/SplitParser.java +++ b/src/main/java/io/split/android/engine/experiments/SplitParser.java @@ -49,20 +49,24 @@ public class SplitParser { public static final int CONDITIONS_UPPER_LIMIT = 50; private final MySegmentsStorageContainer mMySegmentsStorageContainer; + private final MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; private final DefaultConditionsProvider mDefaultConditionsProvider; - public SplitParser(@NonNull MySegmentsStorageContainer mySegmentsStorageContainer) { - this(mySegmentsStorageContainer, new DefaultConditionsProvider()); + public SplitParser(@NonNull MySegmentsStorageContainer mySegmentsStorageContainer, @Nullable MySegmentsStorageContainer myLargeSegmentsStorageContainer) { + this(mySegmentsStorageContainer, myLargeSegmentsStorageContainer, new DefaultConditionsProvider()); } @VisibleForTesting - static SplitParser get(MySegmentsStorageContainer mySegmentsStorageContainer) { - return new SplitParser(mySegmentsStorageContainer); + static SplitParser get(MySegmentsStorageContainer mySegmentsStorageContainer, MySegmentsStorageContainer myLargeSegmentsStorageContainer) { + return new SplitParser(mySegmentsStorageContainer, myLargeSegmentsStorageContainer); } @VisibleForTesting - SplitParser(@NonNull MySegmentsStorageContainer mySegmentsStorageContainer, @NonNull DefaultConditionsProvider defaultConditionsProvider) { + SplitParser(@NonNull MySegmentsStorageContainer mySegmentsStorageContainer, + @Nullable MySegmentsStorageContainer myLargeSegmentsStorageContainer, + @NonNull DefaultConditionsProvider defaultConditionsProvider) { mMySegmentsStorageContainer = checkNotNull(mySegmentsStorageContainer); + mMyLargeSegmentsStorageContainer = myLargeSegmentsStorageContainer; mDefaultConditionsProvider = checkNotNull(defaultConditionsProvider); } @@ -154,6 +158,10 @@ private AttributeMatcher toMatcher(Matcher matcher, String matchingKey) throws U checkNotNull(matcher.userDefinedSegmentMatcherData); delegate = new MySegmentsMatcher((matchingKey != null) ? mMySegmentsStorageContainer.getStorageForKey(matchingKey) : new EmptyMySegmentsStorage(), matcher.userDefinedSegmentMatcherData.segmentName); break; + case IN_LARGE_SEGMENT: + checkNotNull(matcher.userDefinedLargeSegmentMatcherData); + delegate = new MySegmentsMatcher((matchingKey != null) ? mMyLargeSegmentsStorageContainer.getStorageForKey(matchingKey) : new EmptyMySegmentsStorage(), matcher.userDefinedLargeSegmentMatcherData.largeSegmentName); + break; case WHITELIST: checkNotNull(matcher.whitelistMatcherData); delegate = new WhitelistMatcher(matcher.whitelistMatcherData.whitelist); diff --git a/src/sharedTest/java/helper/TestingData.java b/src/sharedTest/java/helper/TestingData.java index 3ba5476fd..1a8da42df 100644 --- a/src/sharedTest/java/helper/TestingData.java +++ b/src/sharedTest/java/helper/TestingData.java @@ -1,26 +1,26 @@ package helper; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeV2Notification; +import io.split.android.client.service.sseclient.notifications.MembershipNotification; import io.split.android.client.utils.Json; public class TestingData { public final static String UNBOUNDED_NOTIFICATION = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + "\\\"u\\\": 0," + "\\\"c\\\": 0," + "\\\"d\\\": \\\"\\\"," + - "\\\"segmentName\\\": \\\"pepe\\\"," + - "\\\"changeNumber\\\": 28" + + "\\\"n\\\": [\\\"pepe\\\"]," + + "\\\"cn\\\": 28" + "}"; public final static String SEGMENT_REMOVAL_NOTIFICATION = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + "\\\"u\\\": 3," + "\\\"c\\\": 0," + "\\\"d\\\": \\\"\\\"," + - "\\\"segmentName\\\": \\\"segment1\\\"," + - "\\\"changeNumber\\\": 28" + + "\\\"n\\\": [\\\"segment1\\\"]," + + "\\\"cn\\\": 28" + "}"; /** @@ -39,28 +39,28 @@ public class TestingData { 8492584437244343049 11382796718859679607 11383137936375052427 17699699514337596928 17001541343685934583 8355202062888946034] */ public final static String BOUNDED_NOTIFICATION_GZIP = "{" + - "\"type\": \"MY_SEGMENTS_UPDATE_V2\"," + + "\"type\": \"MEMBERSHIPS_MS_UPDATE\"," + "\"u\": 1," + "\"c\": 1," + "\"d\": \"H4sIAAAAAAAA/2IYBfgAx0A7YBTgB4wD7YABAAID7QC6g5EYy8MEMA20A+gMFAbaAYMZDPXqlGWgHTAKRsEoGAWjgCzQQFjJkKqiiPAPAQAIAAD//5L7VQwAEAAA\"" + "}"; public final static String ESCAPED_BOUNDED_NOTIFICATION_GZIP = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + "\\\"u\\\": 1," + "\\\"c\\\": 1," + "\\\"d\\\": \\\"H4sIAAAAAAAA/2IYBfgAx0A7YBTgB4wD7YABAAID7QC6g5EYy8MEMA20A+gMFAbaAYMZDPXqlGWgHTAKRsEoGAWjgCzQQFjJkKqiiPAPAQAIAAD//5L7VQwAEAAA\\\"" + "}"; public final static String BOUNDED_NOTIFICATION_ZLIB = "{" + - "\"type\": \"MY_SEGMENTS_UPDATE_V2\"," + + "\"type\": \"MEMBERSHIPS_MS_UPDATE\"," + "\"u\": 1," + "\"c\": 2," + "\"d\": \"eJxiGAX4AMdAO2AU4AeMA+2AAQACA+0AuoORGMvDBDANtAPoDBQG2gGDGQz16pRloB0wCkbBKBgFo4As0EBYyZCqoojwDwEACAAA//+W/QFR\"" + "}"; public final static String ESCAPED_BOUNDED_NOTIFICATION_ZLIB = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + "\\\"u\\\": 1," + "\\\"c\\\": 2," + "\\\"d\\\": \\\"eJxiGAX4AMdAO2AU4AeMA+2AAQACA+0AuoORGMvDBDANtAPoDBQG2gGDGQz16pRloB0wCkbBKBgFo4As0EBYyZCqoojwDwEACAAA//+W/QFR\\\"" + @@ -68,7 +68,7 @@ public class TestingData { public final static String ESCAPED_BOUNDED_NOTIFICATION_MALFORMED = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + "\\\"u\\\": 1," + "\\\"c\\\": 1," + "\\\"d\\\": \\\"H4sIAAAAAAAAg5EYy8MEMA20A+//5L7VQwAEAAA\\\"" + @@ -80,49 +80,49 @@ public class TestingData { * = a: [key1, key2] , r: [key3, key4] */ public final static String KEY_LIST_NOTIFICATION_GZIP = "{" + - "\"type\": \"MY_SEGMENTS_UPDATE_V2\"," + + "\"type\": \"MEMBERSHIPS_MS_UPDATE\"," + "\"u\": 2," + "\"c\": 1," + "\"d\": \"H4sIAAAAAAAA/wTAsRHDUAgD0F2ofwEIkPAqPhdZIW0uu/v97GPXHU004ULuMGrYR6XUbIjlXULPPse+dt1yhJibBODjrTmj3GJ4emduuDDP/w0AAP//18WLsl0AAAA=\"" + "}"; public final static String ESCAPED_KEY_LIST_NOTIFICATION_GZIP = "{" + - "\\\"type\\\": \\\"MY_SEGMENTS_UPDATE_V2\\\"," + - "\\\"segmentName\\\": \\\"new_segment_added\\\"," + + "\\\"type\\\": \\\"MEMBERSHIPS_MS_UPDATE\\\"," + + "\\\"n\\\": [\\\"new_segment_added\\\"]," + "\\\"u\\\": 2," + "\\\"c\\\": 1," + "\\\"d\\\": \\\"H4sIAAAAAAAA/wTAsRHDUAgD0F2ofwEIkPAqPhdZIW0uu/v97GPXHU004ULuMGrYR6XUbIjlXULPPse+dt1yhJibBODjrTmj3GJ4emduuDDP/w0AAP//18WLsl0AAAA=\\\"" + "}"; public final static String BOUNDED_NOTIFICATION_ZLIB_2 = "{" + - "\"changeNumber\": 1629754722111, " + - "\"type\": \"MY_SEGMENTS_UPDATE_V2\"," + + "\"cn\": 1629754722111, " + + "\"type\": \"MEMBERSHIPS_MS_UPDATE\"," + "\"u\": 1," + "\"c\": 2," + "\"d\": \"eJzMVk3OhDAIVdNFl9/22zVzEo8yR5mjT6LGsRTKg2LiW8yPUnjQB+2kIwM2ThTIKtVU1oknFcRzufz+YGYM/phnHW8sdPvs9EzXW2I+HFzhNyTNgCD/PpW9xpGiHD0Bw1U5HLSS644FbGZgoPovmjpmX5wAzhIxJyN7IAnFQWX1htj+LUl6ZQRV3umMqYG1LCrOJGLPV8+IidBQZFt6sOUA6CqsX5iEFY2gqufs2mfqRtsVWytRnO+iYMN7xIBqJhDqAydV+HidkGOGEJYvk4fhe/8iIukphG/XfFcfVxnMVcALCOF77qL/EU7ODepxlLST6qxFLYRdOyW8EBY4BqVjObnm3V5ZMkZIKf++8+hM7zM1Kd3aFqVZeSHzDQAA//+QUQ3a\"" + "}"; // c: 2 -// changeNumber: 1629754722111 +// cn: 1629754722111 // d: "eJzMVk3OhDAIVdNFl9/22zVzEo8yR5mjT6LGsRTKg2LiW8yPUnjQB+2kIwM2ThTIKtVU1oknFcRzufz+YGYM/phnHW8sdPvs9EzXW2I+HFzhNyTNgCD/PpW9xpGiHD0Bw1U5HLSS644FbGZgoPovmjpmX5wAzhIxJyN7IAnFQWX1htj+LUl6ZQRV3umMqYG1LCrOJGLPV8+IidBQZFt6sOUA6CqsX5iEFY2gqufs2mfqRtsVWytRnO+iYMN7xIBqJhDqAydV+HidkGOGEJYvk4fhe/8iIukphG/XfFcfVxnMVcALCOF77qL/EU7ODepxlLST6qxFLYRdOyW8EBY4BqVjObnm3V5ZMkZIKf++8+hM7zM1Kd3aFqVZeSHzDQAA//+QUQ3a" -// segmentName: "" -// type: "MY_SEGMENTS_UPDATE_V2" +// n: "" +// type: "MEMBERSHIPS_MS_UPDATE" // u: 1 public final static String DECOMPRESSED_KEY_LIST_PAYLOAD_GZIP = "{\"a\":[1573573083296714675,8482869187405483569],\"r\":[8031872927333060586,6829471020522910836]}"; public static String encodedKeyListPayloadGzip() { - return (Json.fromJson(KEY_LIST_NOTIFICATION_GZIP, MySegmentChangeV2Notification.class)).getData(); + return (Json.fromJson(KEY_LIST_NOTIFICATION_GZIP, MembershipNotification.class)).getData(); } public static String encodedBoundedPayloadZlib() { - return (Json.fromJson(BOUNDED_NOTIFICATION_ZLIB, MySegmentChangeV2Notification.class)).getData(); + return (Json.fromJson(BOUNDED_NOTIFICATION_ZLIB, MembershipNotification.class)).getData(); } public static String encodedBoundedPayloadZlib2() { - return (Json.fromJson(BOUNDED_NOTIFICATION_ZLIB_2, MySegmentChangeV2Notification.class)).getData(); + return (Json.fromJson(BOUNDED_NOTIFICATION_ZLIB_2, MembershipNotification.class)).getData(); } public static String encodedBoundedPayloadGzip() { - return (Json.fromJson(BOUNDED_NOTIFICATION_GZIP, MySegmentChangeV2Notification.class)).getData(); + return (Json.fromJson(BOUNDED_NOTIFICATION_GZIP, MembershipNotification.class)).getData(); } } diff --git a/src/test/java/io/split/android/client/MySegmentsUriBuildersTest.java b/src/test/java/io/split/android/client/MySegmentsUriBuildersTest.java new file mode 100644 index 000000000..ab0237d42 --- /dev/null +++ b/src/test/java/io/split/android/client/MySegmentsUriBuildersTest.java @@ -0,0 +1,25 @@ +package io.split.android.client; + +import static org.mockito.Mockito.mockStatic; + +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.net.URISyntaxException; + +import io.split.android.client.network.SdkTargetPath; + +public class MySegmentsUriBuildersTest { + + @Test + public void mySegmentsUriBuilderUsesSdkTargetPath() throws URISyntaxException { + try (MockedStatic mockedStatic = mockStatic(SdkTargetPath.class)) { + + SplitFactoryHelper.MySegmentsUriBuilder builder = new SplitFactoryHelper.MySegmentsUriBuilder( + "https://sdk.split.io/api/"); + builder.build("some_key"); + + mockedStatic.verify(() -> SdkTargetPath.mySegments("https://sdk.split.io/api/", "some_key")); + } + } +} diff --git a/src/test/java/io/split/android/client/SplitClientImplBaseTest.java b/src/test/java/io/split/android/client/SplitClientImplBaseTest.java index f5edff592..7b8076a18 100644 --- a/src/test/java/io/split/android/client/SplitClientImplBaseTest.java +++ b/src/test/java/io/split/android/client/SplitClientImplBaseTest.java @@ -30,6 +30,8 @@ public abstract class SplitClientImplBaseTest { @Mock protected MySegmentsStorageContainer mySegmentsStorageContainer; @Mock + protected MySegmentsStorageContainer myLargeSegmentsStorageContainer; + @Mock protected MySegmentsStorage mySegmentsStorage; @Mock protected ImpressionListener impressionListener; @@ -56,7 +58,7 @@ public void setUp() { container, clientContainer, new Key("test_key"), - new SplitParser(mySegmentsStorageContainer), + new SplitParser(mySegmentsStorageContainer, myLargeSegmentsStorageContainer), impressionListener, splitClientConfig, new SplitEventsManager(splitClientConfig, new SplitTaskExecutorStub()), diff --git a/src/test/java/io/split/android/client/SplitManagerImplTest.java b/src/test/java/io/split/android/client/SplitManagerImplTest.java index c2b903e58..07adcfc84 100644 --- a/src/test/java/io/split/android/client/SplitManagerImplTest.java +++ b/src/test/java/io/split/android/client/SplitManagerImplTest.java @@ -45,6 +45,8 @@ public class SplitManagerImplTest { @Mock MySegmentsStorageContainer mMySegmentsStorageContainer; @Mock + MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; + @Mock SplitManager mSplitManager; @Before @@ -52,7 +54,7 @@ public void setup() { MockitoAnnotations.openMocks(this); SplitValidator validator = new SplitValidatorImpl(); when(mMySegmentsStorageContainer.getStorageForKey("")).thenReturn(mMySegmentsStorage); - SplitParser parser = new SplitParser(mMySegmentsStorageContainer); + SplitParser parser = new SplitParser(mMySegmentsStorageContainer, mMyLargeSegmentsStorageContainer); mSplitManager = new SplitManagerImpl(mSplitsStorage, validator, parser); } diff --git a/src/test/java/io/split/android/client/TreatmentManagerTest.java b/src/test/java/io/split/android/client/TreatmentManagerTest.java index 371e92a36..a13f3e287 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerTest.java @@ -70,12 +70,13 @@ public void loadSplitsFromFile() { if (evaluator == null) { FileHelper fileHelper = new FileHelper(); MySegmentsStorageContainer mySegmentsStorageContainer = mock(MySegmentsStorageContainer.class); + MySegmentsStorageContainer myLargeSegmentsStorageContainer = mock(MySegmentsStorageContainer.class); MySegmentsStorage mySegmentsStorage = mock(MySegmentsStorage.class); SplitsStorage splitsStorage = mock(SplitsStorage.class); Set mySegments = new HashSet(Arrays.asList("s1", "s2", "test_copy")); List splits = fileHelper.loadAndParseSplitChangeFile("split_changes_1.json"); - SplitParser splitParser = new SplitParser(mySegmentsStorageContainer); + SplitParser splitParser = new SplitParser(mySegmentsStorageContainer, myLargeSegmentsStorageContainer); Map splitsMap = splitsMap(splits); when(splitsStorage.getAll()).thenReturn(splitsMap); diff --git a/src/test/java/io/split/android/client/events/EventsManagerTest.java b/src/test/java/io/split/android/client/events/EventsManagerTest.java index 8a530296a..c9cca29c6 100644 --- a/src/test/java/io/split/android/client/events/EventsManagerTest.java +++ b/src/test/java/io/split/android/client/events/EventsManagerTest.java @@ -1,11 +1,13 @@ package io.split.android.client.events; -import org.junit.Assert; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @@ -13,17 +15,11 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import helper.TestingHelper; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.events.executors.SplitEventExecutorResources; import io.split.android.fake.SplitTaskExecutorStub; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.when; - public class EventsManagerTest { @Mock @@ -46,33 +42,16 @@ public void eventOnReady() { eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); - + eventManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 10000; - long intervalExecutionTime = 1000; - - while(!shouldStop) { - try { - Thread.currentThread().sleep(intervalExecutionTime); - } catch (InterruptedException e) { - e.printStackTrace(); - Assert.fail(); - } - - maxExecutionTime -= intervalExecutionTime; + long intervalExecutionTime = 200; - if (System.currentTimeMillis() > maxExecutionTime) { - shouldStop = true; - } - - if (eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY)) { - shouldStop = true; - } - } + execute(shouldStop, intervalExecutionTime, maxExecutionTime, eventManager, SplitEvent.SDK_READY); - assertThat(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY), is(equalTo(true))); - assertThat(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT), is(equalTo(false))); + assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY)); + assertFalse(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)); } @Test @@ -82,29 +61,12 @@ public void eventOnReadyTimedOut() { boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 10000; - long intervalExecutionTime = 1000; + long intervalExecutionTime = 200; - while(!shouldStop) { - try { - Thread.currentThread().sleep(intervalExecutionTime); - } catch (InterruptedException e) { - e.printStackTrace(); - Assert.fail(); - } - - maxExecutionTime -= intervalExecutionTime; + execute(shouldStop, intervalExecutionTime, maxExecutionTime, eventManager, SplitEvent.SDK_READY_TIMED_OUT); - if (System.currentTimeMillis() > maxExecutionTime) { - shouldStop = true; - } - - if (eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)) { - shouldStop = true; - } - } - - assertThat(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY), is(equalTo(false))); - assertThat(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT), is(equalTo(true))); + assertFalse(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY)); + assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)); } @Test @@ -114,59 +76,26 @@ public void eventOnReadyAndOnReadyTimedOut() { boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 10000; - long intervalExecutionTime = 1000; + long intervalExecutionTime = 200; - while(!shouldStop) { - try { - Thread.currentThread().sleep(intervalExecutionTime); - } catch (InterruptedException e) { - e.printStackTrace(); - Assert.fail(); - } - - maxExecutionTime -= intervalExecutionTime; - - if (System.currentTimeMillis() > maxExecutionTime) { - shouldStop = true; - } - - if (eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)) { - shouldStop = true; - } - } + execute(shouldStop, intervalExecutionTime, maxExecutionTime, eventManager, SplitEvent.SDK_READY_TIMED_OUT); //At this line timeout has been reached - assertThat(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT), is(equalTo(true))); + assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)); //But if after timeout event, the Splits and MySegments are ready, SDK_READY should be triggered eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + eventManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); shouldStop = false; maxExecutionTime = System.currentTimeMillis() + 10000; - intervalExecutionTime = 1000; - - while(!shouldStop) { - try { - Thread.currentThread().sleep(intervalExecutionTime); - } catch (InterruptedException e) { - e.printStackTrace(); - Assert.fail(); - } - - maxExecutionTime -= intervalExecutionTime; - - if (System.currentTimeMillis() > maxExecutionTime) { - shouldStop = true; - } + intervalExecutionTime = 200; - if (eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY)) { - shouldStop = true; - } - } + execute(shouldStop, intervalExecutionTime, maxExecutionTime, eventManager, SplitEvent.SDK_READY); - assertThat(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY), is(equalTo(true))); - assertThat(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT), is(equalTo(true))); + assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY)); + assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)); } @Test @@ -176,7 +105,7 @@ public void eventOnReadyFromCacheSplitsFirst() { eventList.add(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); eventList.add(SplitInternalEvent.ATTRIBUTES_LOADED_FROM_STORAGE); eventList.add(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); - eventOnReadyFromCache(eventList); + eventOnReadyFromCache(eventList, SplitClientConfig.builder().build()); } @Test @@ -186,7 +115,7 @@ public void eventOnReadyFromCacheMySegmentsFirst() { eventList.add(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); eventList.add(SplitInternalEvent.ATTRIBUTES_LOADED_FROM_STORAGE); eventList.add(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); - eventOnReadyFromCache(eventList); + eventOnReadyFromCache(eventList, SplitClientConfig.builder().build()); } @Test @@ -196,7 +125,7 @@ public void eventOnReadyFromCacheAttributesFirst() { eventList.add(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); eventList.add(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); eventList.add(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); - eventOnReadyFromCache(eventList); + eventOnReadyFromCache(eventList, SplitClientConfig.builder().build()); } @Test @@ -206,25 +135,114 @@ public void eventEncryptionMigrationDoneFirst() { eventList.add(SplitInternalEvent.ATTRIBUTES_LOADED_FROM_STORAGE); eventList.add(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); eventList.add(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); - eventOnReadyFromCache(eventList); + eventOnReadyFromCache(eventList, SplitClientConfig.builder().build()); + } + + @Test + public void eventOnReadyFromCacheMyLargeSegmentsFirst() { + List eventList = new ArrayList<>(); + eventList.add(SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); + eventList.add(SplitInternalEvent.SPLITS_LOADED_FROM_STORAGE); + eventList.add(SplitInternalEvent.ATTRIBUTES_LOADED_FROM_STORAGE); + eventList.add(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); + eventOnReadyFromCache(eventList, SplitClientConfig.builder() + .build()); + } + + @Test + public void sdkUpdateWithFeatureFlags() throws InterruptedException { + sdkUpdateTest(SplitInternalEvent.SPLITS_UPDATED, false); } - public void eventOnReadyFromCache(List eventList) { + @Test + public void sdkUpdateWithMySegments() throws InterruptedException { + sdkUpdateTest(SplitInternalEvent.MY_SEGMENTS_UPDATED, false); + } + + @Test + public void sdkUpdateWithLargeSegmentsAndConfigEnabledEmitsEvent() throws InterruptedException { + sdkUpdateTest(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED, false); + } + + @Test + public void sdkUpdateWithLargeSegmentsAndConfigEnabledAndWaitForLargeSegmentsFalseEmitsEvent() throws InterruptedException { + sdkUpdateTest(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED, false); + } + + @Test + public void sdkReadyWithSplitsAndUpdatedLargeSegments() { SplitClientConfig cfg = SplitClientConfig.builder().build(); SplitEventsManager eventManager = new SplitEventsManager(cfg, new SplitTaskExecutorStub()); - for(SplitInternalEvent event : eventList) { + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); + eventManager.notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + + boolean shouldStop = false; + long maxExecutionTime = System.currentTimeMillis() + 10000; + long intervalExecutionTime = 200; + + execute(shouldStop, intervalExecutionTime, maxExecutionTime, eventManager, SplitEvent.SDK_READY); + + assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY)); + assertFalse(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_TIMED_OUT)); + } + + private static void sdkUpdateTest(SplitInternalEvent eventToCheck, boolean negate) throws InterruptedException { + SplitEventsManager eventManager = new SplitEventsManager(SplitClientConfig.builder() + .build(), new SplitTaskExecutorStub()); + + CountDownLatch updateLatch = new CountDownLatch(1); + CountDownLatch readyLatch = new CountDownLatch(1); + eventManager.register(SplitEvent.SDK_UPDATE, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + updateLatch.countDown(); + } + }); + eventManager.register(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + readyLatch.countDown(); + } + }); + + eventManager.notifyInternalEvent(SplitInternalEvent.SPLITS_FETCHED); + eventManager.notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); + boolean readyAwait = readyLatch.await(3, TimeUnit.SECONDS); + + eventManager.notifyInternalEvent(eventToCheck); + boolean updateAwait = updateLatch.await(3, TimeUnit.SECONDS); + + assertTrue(readyAwait); + if (!negate) { + assertTrue(updateAwait); + } else { + assertFalse(updateAwait); + } + } + + private void eventOnReadyFromCache(List eventList, SplitClientConfig config) { + + SplitEventsManager eventManager = new SplitEventsManager(config, new SplitTaskExecutorStub()); + + for (SplitInternalEvent event : eventList) { eventManager.notifyInternalEvent(event); } boolean shouldStop = false; long maxExecutionTime = System.currentTimeMillis() + 10000; - long intervalExecutionTime = 1000; + long intervalExecutionTime = 200; + + execute(shouldStop, intervalExecutionTime, maxExecutionTime, eventManager, SplitEvent.SDK_READY_FROM_CACHE); - while(!shouldStop) { + assertTrue(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE)); + } + + private static void execute(boolean shouldStop, long intervalExecutionTime, long maxExecutionTime, SplitEventsManager eventManager, SplitEvent event) { + while (!shouldStop) { try { - Thread.currentThread().sleep(intervalExecutionTime); + Thread.sleep(intervalExecutionTime); } catch (InterruptedException e) { e.printStackTrace(); Assert.fail(); @@ -236,11 +254,9 @@ public void eventOnReadyFromCache(List eventList) { shouldStop = true; } - if (eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE)) { + if (eventManager.eventAlreadyTriggered(event)) { shouldStop = true; } } - - assertThat(eventManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE), is(equalTo(true))); } } diff --git a/src/test/java/io/split/android/client/network/HttpClientTest.java b/src/test/java/io/split/android/client/network/HttpClientTest.java index c655b92a3..b036ec5b8 100644 --- a/src/test/java/io/split/android/client/network/HttpClientTest.java +++ b/src/test/java/io/split/android/client/network/HttpClientTest.java @@ -30,12 +30,11 @@ import java.net.URL; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.dtos.Event; -import io.split.android.client.dtos.MySegment; import io.split.android.client.dtos.SplitChange; import io.split.android.client.dtos.TestImpressions; import io.split.android.client.utils.Json; @@ -143,13 +142,11 @@ public void severalRequest() throws Exception { Assert.assertEquals("this is split test", dummyResp.getData()); // Assert my segments - List mySegments = parseMySegments(mySegmentsResp.getData()); + List mySegments = parseMySegments(mySegmentsResp.getData()).getSegmentsChange().getNames(); Assert.assertEquals(200, mySegmentsResp.getHttpStatus()); Assert.assertEquals(2, mySegments.size()); - Assert.assertEquals("id1", mySegments.get(0).id); - Assert.assertEquals("groupa", mySegments.get(0).name); - Assert.assertEquals("id2", mySegments.get(1).id); - Assert.assertEquals("groupb", mySegments.get(1).name); + Assert.assertTrue(mySegments.contains("groupa")); + Assert.assertTrue(mySegments.contains("groupb")); // Assert split changes SplitChange splitChange = Json.fromJson(splitChangeResp.getData(), SplitChange.class); @@ -157,7 +154,7 @@ public void severalRequest() throws Exception { assertTrue(splitChangeResp.isSuccess()); Assert.assertEquals(-1, splitChange.since); Assert.assertEquals(1506703262916L, splitChange.till); - Assert.assertEquals(30, splitChange.splits.size()); + Assert.assertEquals(31, splitChange.splits.size()); // Assert empty response Assert.assertEquals(200, emptyResp.getHttpStatus()); @@ -426,7 +423,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio case "/test2/": - return new MockResponse().setResponseCode(200).setBody("{\"mySegments\":[{\"id\":\"id1\", \"name\":\"groupa\"}, {\"id\":\"id2\", \"name\":\"groupb\"}]}"); + return new MockResponse().setResponseCode(200).setBody("{\"ms\":{\"k\":[{\"n\":\"groupa\"},{\"n\":\"groupb\"}],\"cn\":999999},\"ls\":{\"k\":[],\"cn\":999999}}"); case "/test3/": return new MockResponse().setResponseCode(200).setBody(splitChangesResponse); @@ -462,12 +459,8 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio .build(); } - private List parseMySegments(String json) { - Type mapType = new TypeToken>>() { - }.getType(); - - Map> mySegmentsMap = Json.fromJson(json, mapType); - return mySegmentsMap.get("mySegments"); + private AllSegmentsChange parseMySegments(String json) { + return Json.fromJson(json, AllSegmentsChange.class); } private List parseTrackEvents(String json) { diff --git a/src/test/java/io/split/android/client/network/SdkTargetPathTest.java b/src/test/java/io/split/android/client/network/SdkTargetPathTest.java index 8c44403a3..8f8c34657 100644 --- a/src/test/java/io/split/android/client/network/SdkTargetPathTest.java +++ b/src/test/java/io/split/android/client/network/SdkTargetPathTest.java @@ -13,21 +13,21 @@ public class SdkTargetPathTest { public void userKeyWithSpaces() throws URISyntaxException { URI uri = SdkTargetPath.mySegments("https://split.io", "CABM, CCIB Marketing"); - assertEquals("/mySegments/CABM,%20CCIB%20Marketing", uri.getRawPath()); + assertEquals(SdkTargetPath.MEMBERSHIPS+"/CABM,%20CCIB%20Marketing", uri.getRawPath()); } @Test public void userKeyWithSlash() throws URISyntaxException { URI uri = SdkTargetPath.mySegments("https://split.io", "user/key"); - assertEquals("/mySegments/user%2Fkey", uri.getRawPath()); + assertEquals(SdkTargetPath.MEMBERSHIPS+"/user%2Fkey", uri.getRawPath()); } @Test public void userKeyWithSpecialChars() throws URISyntaxException { URI uri = SdkTargetPath.mySegments("https://split.io", "grüneStraße"); - assertEquals("/mySegments/gr%C3%BCneStra%C3%9Fe", uri.getRawPath()); + assertEquals(SdkTargetPath.MEMBERSHIPS+"/gr%C3%BCneStra%C3%9Fe", uri.getRawPath()); } @Test diff --git a/src/test/java/io/split/android/client/service/HttpFetcherTest.java b/src/test/java/io/split/android/client/service/HttpFetcherTest.java index 4b1259a3b..36c85ffe0 100644 --- a/src/test/java/io/split/android/client/service/HttpFetcherTest.java +++ b/src/test/java/io/split/android/client/service/HttpFetcherTest.java @@ -18,13 +18,14 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; -import io.split.android.client.dtos.MySegment; +import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.dtos.SplitChange; import io.split.android.client.network.HttpClient; import io.split.android.client.network.HttpException; @@ -38,21 +39,21 @@ import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.http.HttpFetcherImpl; import io.split.android.client.service.http.HttpResponseParser; -import io.split.android.client.service.mysegments.MySegmentsResponseParser; +import io.split.android.client.service.mysegments.AllSegmentsResponseParser; import io.split.android.client.service.splits.SplitChangeResponseParser; public class HttpFetcherTest { private final static String TEST_URL = "http://testurl.com"; private final static String SPLIT_CHANGES_TEST_URL = TEST_URL + SdkTargetPath.SPLIT_CHANGES; - private final static String MY_SEGMENTS_TEST_URL = TEST_URL + SdkTargetPath.MY_SEGMENTS; + private final static String MY_SEGMENTS_TEST_URL = TEST_URL + SdkTargetPath.MEMBERSHIPS; - HttpClient mClientMock; - URI mUrl; - URI mSplitChangesUrl; - URI mMySegmentsUrl; - HttpResponseParser mSplitChangeResponseParser = new SplitChangeResponseParser(); - HttpResponseParser> mMySegmentsResponseParser = new MySegmentsResponseParser(); + private HttpClient mClientMock; + private URI mUrl; + private URI mSplitChangesUrl; + private URI mMySegmentsUrl; + private final HttpResponseParser mSplitChangeResponseParser = new SplitChangeResponseParser(); + private final HttpResponseParser mMySegmentsResponseParser = new AllSegmentsResponseParser(); @Before public void setup() throws URISyntaxException { @@ -193,15 +194,16 @@ public void testSuccessfulMySegmentsFetch() throws URISyntaxException, HttpExcep when(request.execute()).thenReturn(response); when(mClientMock.request(mMySegmentsUrl, HttpMethod.GET, null, null)).thenReturn(request); - HttpFetcher> fetcher = new HttpFetcherImpl<>(mClientMock, mMySegmentsUrl, mMySegmentsResponseParser); - List mySegments = null; + HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mMySegmentsUrl, mMySegmentsResponseParser); + List mySegments = null; try { - mySegments = fetcher.execute(new HashMap<>(), null); + SegmentsChange segmentsChange = fetcher.execute(new HashMap<>(), null).getSegmentsChange(); + mySegments = segmentsChange.getNames(); } catch (HttpFetcherException e) { exceptionWasThrown = true; } - Set mySegmentsSet = mySegments.stream().map(mySegment -> mySegment.name).collect(Collectors.toSet()); + Set mySegmentsSet = new HashSet<>(mySegments); Assert.assertFalse(exceptionWasThrown); Assert.assertEquals(2, mySegments.size()); @@ -222,10 +224,10 @@ public void tesNonEmptyHeadersMySegmentsFetch() throws URISyntaxException, HttpE when(request.execute()).thenReturn(response); when(mClientMock.request(mMySegmentsUrl, HttpMethod.GET, null, headers)).thenReturn(request); - HttpFetcher> fetcher = new HttpFetcherImpl<>(mClientMock, mMySegmentsUrl, mMySegmentsResponseParser); + HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mMySegmentsUrl, mMySegmentsResponseParser); try { - List mySegments = fetcher.execute(new HashMap<>(), headers); + List mySegments = fetcher.execute(new HashMap<>(), headers).getSegmentsChange().getNames(); } catch (HttpFetcherException e) { exceptionWasThrown = true; } @@ -246,10 +248,10 @@ public void testHandleParserExceptionFetch() throws URISyntaxException, HttpExce when(mClientMock.request(mMySegmentsUrl, HttpMethod.GET)).thenReturn(request); - HttpFetcher> fetcher = new HttpFetcherImpl<>(mClientMock, mMySegmentsUrl, mMySegmentsResponseParser); - List mySegments = null; + HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mMySegmentsUrl, mMySegmentsResponseParser); + List mySegments = null; try { - mySegments = fetcher.execute(new HashMap<>(), null); + mySegments = fetcher.execute(new HashMap<>(), null).getSegmentsChange().getNames(); } catch (HttpFetcherException e) { exceptionWasThrown = true; } @@ -281,7 +283,7 @@ public void paramOrderWithoutTillIsCorrect() throws HttpException, HttpFetcherEx params.put("since", "-1"); params.put("sets", "flag_set1,flagset2"); - fetcher.execute(params, null); + fetcher.execute(params, null); verifyQuery("s=1.1&since=-1&sets=flag_set1,flagset2"); } @@ -365,6 +367,6 @@ private String dummySplitChangeResponse() { } private String dummyMySegmentsResponse() { - return "{\"mySegments\":[{ \"id\":\"id1\", \"name\":\"segment1\"}, { \"id\":\"id2\", \"name\":\"segment2\"}]}"; + return "{\"ms\":{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}],\"cn\":99999},\"ls\":{\"k\":[{\"n\":\"large-segment1\"},{\"n\":\"large-segment2\"},{\"n\":\"large-segment3\"}],\"cn\":9999999999999}}"; } } diff --git a/src/test/java/io/split/android/client/service/MySegmentsOverwriteTaskTest.java b/src/test/java/io/split/android/client/service/MySegmentsOverwriteTaskTest.java deleted file mode 100644 index 4e799857e..000000000 --- a/src/test/java/io/split/android/client/service/MySegmentsOverwriteTaskTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.split.android.client.service; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.List; - -import io.split.android.client.events.SplitEventsManager; -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.http.HttpFetcherException; -import io.split.android.client.service.mysegments.MySegmentsOverwriteTask; -import io.split.android.client.storage.mysegments.MySegmentsStorage; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -public class MySegmentsOverwriteTaskTest { - - @Mock - MySegmentsStorage mySegmentsStorage; - - @Mock - SplitEventsManager mEventsManager; - - MySegmentsOverwriteTask mTask; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void correctExecution() throws HttpFetcherException { - List segments = dummySegments(); - mTask = new MySegmentsOverwriteTask(mySegmentsStorage, segments, mEventsManager); - SplitTaskExecutionInfo result = mTask.execute(); - - verify(mySegmentsStorage, times(1)).set(any()); - - Assert.assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); - Assert.assertEquals(SplitTaskType.MY_SEGMENTS_OVERWRITE, result.getTaskType()); - } - - @Test - public void storageException() { - List segments = dummySegments(); - mTask = new MySegmentsOverwriteTask(mySegmentsStorage, segments, mEventsManager); - doThrow(NullPointerException.class).when(mySegmentsStorage).set(segments); - - SplitTaskExecutionInfo result = mTask.execute(); - - Assert.assertEquals(SplitTaskExecutionStatus.ERROR, result.getStatus()); - } - - @Test - public void nullParameter() { - - mTask = new MySegmentsOverwriteTask(mySegmentsStorage, null, mEventsManager); - - SplitTaskExecutionInfo result = mTask.execute(); - - Assert.assertEquals(SplitTaskExecutionStatus.ERROR, result.getStatus()); - } - - @After - public void tearDown() { - reset(mySegmentsStorage); - } - - private List dummySegments() { - List segments = new ArrayList<>(); - for (int i = 0; i < 3; i++) { - segments.add("segment" + i); - } - return segments; - } -} diff --git a/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java b/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java index cce259185..696c844f0 100644 --- a/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java +++ b/src/test/java/io/split/android/client/service/MySegmentsSyncTaskTest.java @@ -5,11 +5,14 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.longThat; import static org.mockito.Mockito.doThrow; +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 androidx.annotation.NonNull; + import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -17,21 +20,27 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.List; +import java.util.HashSet; import java.util.Map; +import java.util.Set; -import io.split.android.client.dtos.MySegment; +import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.network.SplitHttpHeadersBuilder; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.mysegments.MySegmentsSyncTask; +import io.split.android.client.service.mysegments.MySegmentsSyncTaskConfig; +import io.split.android.client.service.sseclient.BackoffCounter; +import io.split.android.client.service.synchronizer.MySegmentsChangeChecker; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.telemetry.model.OperationType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; @@ -41,15 +50,19 @@ public class MySegmentsSyncTaskTest { Map noParams = Collections.emptyMap(); @Mock - HttpFetcher> mMySegmentsFetcher; + HttpFetcher mMySegmentsFetcher; @Mock MySegmentsStorage mySegmentsStorage; @Mock + MySegmentsStorage myLargeSegmentsStorage; + @Mock SplitEventsManager mEventsManager; @Mock TelemetryRuntimeProducer mTelemetryRuntimeProducer; + @Mock + MySegmentsChangeChecker mMySegmentsChangeChecker; - List mMySegments = null; + AllSegmentsChange mMySegments = null; MySegmentsSyncTask mTask; private AutoCloseable mAutoCloseable; @@ -57,7 +70,8 @@ public class MySegmentsSyncTaskTest { @Before public void setup() { mAutoCloseable = MockitoAnnotations.openMocks(this); - mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, false, mEventsManager, mTelemetryRuntimeProducer); + when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(true); + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null); loadMySegments(); } @@ -72,7 +86,7 @@ public void tearDown() { @Test public void correctExecution() throws HttpFetcherException { - when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); + when(mMySegmentsFetcher.execute(noParams, null)).thenAnswer((Answer) invocation -> mMySegments); mTask.execute(); @@ -88,8 +102,8 @@ public void correctExecution() throws HttpFetcherException { public void correctExecutionNoCache() throws HttpFetcherException { Map headers = new HashMap<>(); headers.put(SplitHttpHeadersBuilder.CACHE_CONTROL_HEADER, SplitHttpHeadersBuilder.CACHE_CONTROL_NO_CACHE); - mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, true, null, mTelemetryRuntimeProducer); - when(mMySegmentsFetcher.execute(noParams, headers)).thenReturn(mMySegments); + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, true, null, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null); + when(mMySegmentsFetcher.execute(noParams, headers)).thenAnswer((Answer) invocation -> mMySegments); mTask.execute(); @@ -111,7 +125,7 @@ public void fetcherExceptionRetryOff() throws HttpFetcherException { @Test public void fetcherOtherExceptionRetryOn() throws HttpFetcherException { - mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, false, mEventsManager, mTelemetryRuntimeProducer); + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null); when(mMySegmentsFetcher.execute(noParams, null)).thenThrow(IllegalStateException.class); mTask.execute(); @@ -122,7 +136,7 @@ public void fetcherOtherExceptionRetryOn() throws HttpFetcherException { @Test public void storageException() throws HttpFetcherException { - when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); + when(mMySegmentsFetcher.execute(noParams, null)).thenAnswer((Answer) invocation -> mMySegments); doThrow(NullPointerException.class).when(mySegmentsStorage).set(any()); mTask.execute(); @@ -142,7 +156,7 @@ public void errorIsTrackedInTelemetry() throws HttpFetcherException { @Test public void latencyIsTrackedInTelemetry() throws HttpFetcherException { - when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); + when(mMySegmentsFetcher.execute(noParams, null)).thenAnswer((Answer) invocation -> mMySegments); mTask.execute(); @@ -151,7 +165,7 @@ public void latencyIsTrackedInTelemetry() throws HttpFetcherException { @Test public void successIsTrackedInTelemetry() throws HttpFetcherException { - when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); + when(mMySegmentsFetcher.execute(noParams, null)).thenAnswer((Answer) invocation -> mMySegments); mTask.execute(); @@ -180,7 +194,7 @@ public void nullStatusCodeReturnsNullDoNotRetry() throws HttpFetcherException { @Test public void successfulCallToExecuteReturnsNullDoNotRetry() throws HttpFetcherException { - when(mMySegmentsFetcher.execute(any(), any())).thenReturn(mMySegments); + when(mMySegmentsFetcher.execute(any(), any())).thenAnswer((Answer) invocation -> mMySegments); SplitTaskExecutionInfo result = mTask.execute(); @@ -188,15 +202,198 @@ public void successfulCallToExecuteReturnsNullDoNotRetry() throws HttpFetcherExc Assert.assertEquals(result.getStatus(), SplitTaskExecutionStatus.SUCCESS); } + @Test + public void addTillParameterToRequestWhenResponseCnDoesNotMatchTargetAndRetryLimitIsReached() throws HttpFetcherException { + long targetLargeSegmentsChangeNumber = 4L; + BackoffCounter backoffCounter = mock(BackoffCounter.class); + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, targetLargeSegmentsChangeNumber, + backoffCounter, 2); + when(mMySegmentsFetcher.execute(noParams, null)) + .thenReturn(createChange(1L)); + when(mMySegmentsFetcher.execute(Collections.singletonMap("till", 4L), null)) + .thenReturn(createChange(4L)); + + SplitTaskExecutionInfo result = mTask.execute(); + + verify(backoffCounter).resetCounter(); + verify(mMySegmentsFetcher, times(2)).execute(noParams, null); + verify(mMySegmentsFetcher, times(1)).execute(Collections.singletonMap("till", 4L), null); + verify(backoffCounter, times(2)).getNextRetryTime(); + } + + @Test + public void fetchedEventIsEmittedWhenNoChangesInSegments() throws HttpFetcherException { + when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(false); + when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_FETCHED); + } + + @Test + public void updatedEventIsEmittedWhenChangesInSegments() throws HttpFetcherException { + when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(true); + when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(mMySegments); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + } + + @Test + public void largeSegmentsUpdatedEventIsEmittedWhenChangesInLargeSegmentsAndNotInSegments() throws HttpFetcherException { + when(mMySegmentsChangeChecker.mySegmentsHaveChanged(any(), any())).thenReturn(false); + when(mMySegmentsChangeChecker.mySegmentsHaveChanged(Collections.emptyList(), Collections.singletonList("largesegment0"))).thenReturn(true); + when(mMySegmentsFetcher.execute(noParams, null)).thenReturn(createChange(1L)); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, null, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED); + } + + @Test + public void largeSegmentsTargetIsUsedForCdnBypassWhenSegmentsChangeNumberIsNotSet() throws HttpFetcherException { + long targetLargeSegmentsChangeNumber = 4L; + when(mMySegmentsFetcher.execute(noParams, null)) + .thenReturn(createChange(null, 1L)); + when(mMySegmentsFetcher.execute(Collections.singletonMap("till", 4L), null)) + .thenReturn(createChange(null, 4L)); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, targetLargeSegmentsChangeNumber, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mMySegmentsFetcher, times(2)).execute(any(), any()); + verify(mMySegmentsFetcher).execute(noParams, null); + verify(mMySegmentsFetcher).execute(Collections.singletonMap("till", 4L), null); + } + + @Test + public void segmentsTargetIsUsedForCdnBypassWhenLargeSegmentsChangeNumberIsNotSet() throws HttpFetcherException { + long targetSegmentsChangeNumber = 5L; + when(mMySegmentsFetcher.execute(noParams, null)) + .thenReturn(createChange(2L, null)); + when(mMySegmentsFetcher.execute(Collections.singletonMap("till", 5L), null)) + .thenReturn(createChange(5L, null)); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), targetSegmentsChangeNumber, null, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mMySegmentsFetcher, times(2)).execute(any(), any()); + verify(mMySegmentsFetcher).execute(noParams, null); + verify(mMySegmentsFetcher).execute(Collections.singletonMap("till", 5L), null); + } + + @Test + public void largeSegmentsTargetIsUsedForCdnBypassWhenSegmentsChangeNumberIsSmaller() throws HttpFetcherException { + long targetLargeSegmentsChangeNumber = 4L; + when(mMySegmentsFetcher.execute(noParams, null)) + .thenReturn(createChange(2L, 1L)); + when(mMySegmentsFetcher.execute(Collections.singletonMap("till", 4L), null)) + .thenReturn(createChange(2L, 4L)); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, targetLargeSegmentsChangeNumber, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mMySegmentsFetcher, times(2)).execute(any(), any()); + verify(mMySegmentsFetcher).execute(noParams, null); + verify(mMySegmentsFetcher).execute(Collections.singletonMap("till", 4L), null); + } + + @Test + public void segmentsTargetIsUsedForCdnBypassWhenLargeSegmentsChangeNumberIsSmaller() throws HttpFetcherException { + long targetSegmentsChangeNumber = 5L; + when(mMySegmentsFetcher.execute(noParams, null)) + .thenReturn(createChange(2L, 1L)); + when(mMySegmentsFetcher.execute(Collections.singletonMap("till", 5L), null)) + .thenReturn(createChange(5L, 1L)); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), targetSegmentsChangeNumber, null, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mMySegmentsFetcher, times(2)).execute(any(), any()); + verify(mMySegmentsFetcher).execute(noParams, null); + verify(mMySegmentsFetcher).execute(Collections.singletonMap("till", 5L), null); + } + + @Test + public void noFetchWhenSegmentsChangeNumberInStorageIsNewerThanTarget() throws HttpFetcherException { + long targetSegmentsChangeNumber = 5L; + when(mySegmentsStorage.getChangeNumber()).thenReturn(6L); + when(myLargeSegmentsStorage.getChangeNumber()).thenReturn(5L); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), targetSegmentsChangeNumber, null, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mMySegmentsFetcher, never()).execute(any(), any()); + } + + @Test + public void noFetchWhenLargeSegmentsChangeNumberIsNewerThanTarget() throws HttpFetcherException { + long targetLargeSegmentsChangeNumber = 4L; + when(mySegmentsStorage.getChangeNumber()).thenReturn(3L); + when(myLargeSegmentsStorage.getChangeNumber()).thenReturn(5L); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), null, targetLargeSegmentsChangeNumber, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mMySegmentsFetcher, never()).execute(any(), any()); + } + + @Test + public void fetchWhenSegmentsChangeNumberInStorageIsNewerThanTargetAndLargeSegmentsChangeNumberIsOlder() throws HttpFetcherException { + long targetSegmentsChangeNumber = 5L; + long targetLargeSegmentsChangeNumber = 4L; + when(mySegmentsStorage.getChangeNumber()).thenReturn(6L); + when(myLargeSegmentsStorage.getChangeNumber()).thenReturn(3L); + when(mMySegmentsFetcher.execute(noParams, null)) + .thenReturn(createChange(6L, 4L)); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), targetSegmentsChangeNumber, targetLargeSegmentsChangeNumber, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mMySegmentsFetcher).execute(any(), any()); + verify(mMySegmentsFetcher).execute(noParams, null); + } + + @Test + public void fetchIsPerformedWhenLargeSegmentsChangeNumberInStorageIsNewerThanTargetAndSegmentsChangeNumberIsOlder() throws HttpFetcherException { + long targetSegmentsChangeNumber = 5L; + long targetLargeSegmentsChangeNumber = 4L; + when(mySegmentsStorage.getChangeNumber()).thenReturn(3L); + when(myLargeSegmentsStorage.getChangeNumber()).thenReturn(6L); + when(mMySegmentsFetcher.execute(noParams, null)) + .thenReturn(createChange(5L, 6L)); + + mTask = new MySegmentsSyncTask(mMySegmentsFetcher, mySegmentsStorage, myLargeSegmentsStorage, false, mEventsManager, mMySegmentsChangeChecker, mTelemetryRuntimeProducer, MySegmentsSyncTaskConfig.get(), targetSegmentsChangeNumber, targetLargeSegmentsChangeNumber, mock(BackoffCounter.class), 1); + mTask.execute(); + + verify(mMySegmentsFetcher).execute(any(), any()); + verify(mMySegmentsFetcher).execute(noParams, null); + } + + @NonNull + private static AllSegmentsChange createChange(Long msChangeNumber, Long lsChangeNumber) { + return AllSegmentsChange.create( + SegmentsChange.create(new HashSet<>(Collections.singletonList("segment0")), msChangeNumber), + SegmentsChange.create(new HashSet<>(Collections.singletonList("largesegment0")), lsChangeNumber)); + } + + @NonNull + private static AllSegmentsChange createChange(long lsChangeNumber) { + return createChange(null, lsChangeNumber); + } + private void loadMySegments() { if (mMySegments == null) { - mMySegments = new ArrayList<>(); + Set segments = new HashSet<>(); for (int i = 0; i < 5; i++) { - MySegment s = new MySegment(); - s.id = "id_" + i; - s.name = "segment_" + i; - mMySegments.add(s); + segments.add("segment_" + i); } + mMySegments = AllSegmentsChange.create(SegmentsChange.create(segments, null), SegmentsChange.createEmpty()); } } } 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 9acb9614a..3e4972765 100644 --- a/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java +++ b/src/test/java/io/split/android/client/service/MySegmentsUpdateTaskTest.java @@ -1,5 +1,14 @@ package io.split.android.client.service; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +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; + import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -8,31 +17,25 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.events.SplitEventsManager; +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.executor.SplitTaskType; import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.mysegments.MySegmentsUpdateTask; +import io.split.android.client.service.mysegments.MySegmentsUpdateTaskConfig; 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; -import static org.mockito.Mockito.never; -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 { @Mock @@ -61,15 +64,15 @@ public void setup() { @Test public void correctExecution() throws HttpFetcherException { - mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, mSegmentToRemove, mEventsManager, mTelemetryRuntimeProducer); + mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, Collections.singleton(mSegmentToRemove), 25L, mEventsManager, mTelemetryRuntimeProducer, MySegmentsUpdateTaskConfig.getForMySegments()); - ArgumentCaptor> segmentsCaptor = ArgumentCaptor.forClass(List.class); + ArgumentCaptor segmentsCaptor = ArgumentCaptor.forClass(SegmentsChange.class); SplitTaskExecutionInfo result = mTask.execute(); verify(mySegmentsStorage, times(1)).set(segmentsCaptor.capture()); - Assert.assertTrue(isSegmentRemoved(segmentsCaptor.getValue(), mSegmentToRemove)); - Assert.assertFalse(isSegmentRemoved(segmentsCaptor.getValue(), mCustomerSegment)); + Assert.assertTrue(isSegmentRemoved(segmentsCaptor.getValue().getNames(), mSegmentToRemove)); + Assert.assertFalse(isSegmentRemoved(segmentsCaptor.getValue().getNames(), mCustomerSegment)); Assert.assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); Assert.assertEquals(SplitTaskType.MY_SEGMENTS_UPDATE, result.getTaskType()); } @@ -77,7 +80,7 @@ public void correctExecution() throws HttpFetcherException { @Test public void correctExecutionToEraseNotInSegments() throws HttpFetcherException { String otherSegment = "OtherSegment"; - mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, otherSegment, mEventsManager, mTelemetryRuntimeProducer); + mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, Collections.singleton(otherSegment), 25L, mEventsManager, mTelemetryRuntimeProducer, MySegmentsUpdateTaskConfig.getForMySegments()); ArgumentCaptor> segmentsCaptor = ArgumentCaptor.forClass(List.class); SplitTaskExecutionInfo result = mTask.execute(); @@ -90,7 +93,7 @@ public void correctExecutionToEraseNotInSegments() throws HttpFetcherException { @Test public void storageException() { - mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, mSegmentToRemove, mEventsManager, mTelemetryRuntimeProducer); + mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, Collections.singleton(mSegmentToRemove), 25L, mEventsManager, mTelemetryRuntimeProducer, MySegmentsUpdateTaskConfig.getForMySegments()); doThrow(NullPointerException.class).when(mySegmentsStorage).set(any()); SplitTaskExecutionInfo result = mTask.execute(); @@ -101,7 +104,7 @@ public void storageException() { @Test public void successfulAddOperationIsRecordedInTelemetry() { - mTask = new MySegmentsUpdateTask(mySegmentsStorage, true, mSegmentToRemove, mEventsManager, mTelemetryRuntimeProducer); + mTask = new MySegmentsUpdateTask(mySegmentsStorage, true, Collections.singleton(mSegmentToRemove), 25L, mEventsManager, mTelemetryRuntimeProducer, MySegmentsUpdateTaskConfig.getForMySegments()); mTask.execute(); @@ -110,13 +113,90 @@ public void successfulAddOperationIsRecordedInTelemetry() { @Test public void successfulRemoveOperationIsRecordedInTelemetry() { - mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, mSegmentToRemove, mEventsManager, mTelemetryRuntimeProducer); + mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, Collections.singleton(mSegmentToRemove), 25L, mEventsManager, mTelemetryRuntimeProducer, MySegmentsUpdateTaskConfig.getForMySegments()); mTask.execute(); verify(mTelemetryRuntimeProducer).recordUpdatesFromSSE(UpdatesFromSSEEnum.MY_SEGMENTS); } + @Test + public void addOperationWithSegmentsAlreadyInStorage() { + Set oldSegments = new HashSet<>(); + oldSegments.add(mCustomerSegment); + oldSegments.add(mSegmentToRemove); + + when(mySegmentsStorage.getAll()).thenReturn(oldSegments); + + mTask = new MySegmentsUpdateTask(mySegmentsStorage, true, oldSegments, 25L, mEventsManager, mTelemetryRuntimeProducer, MySegmentsUpdateTaskConfig.getForMySegments()); + + SplitTaskExecutionInfo result = mTask.execute(); + + verify(mySegmentsStorage, never()).set(any()); + verify(mEventsManager, never()).notifyInternalEvent(any()); + } + + @Test + public void addOperationWithOneSegmentAlreadyInStorage() { + Set oldSegments = new HashSet<>(); + oldSegments.add(mCustomerSegment); + + when(mySegmentsStorage.getAll()).thenReturn(oldSegments); + + mTask = new MySegmentsUpdateTask(mySegmentsStorage, true, new HashSet<>(Arrays.asList(mCustomerSegment, mSegmentToRemove)), 25L, mEventsManager, mTelemetryRuntimeProducer, MySegmentsUpdateTaskConfig.getForMySegments()); + + ArgumentCaptor segmentsCaptor = ArgumentCaptor.forClass(SegmentsChange.class); + + SplitTaskExecutionInfo result = mTask.execute(); + + verify(mySegmentsStorage, times(1)).set(segmentsCaptor.capture()); + Assert.assertTrue(segmentsCaptor.getValue().getNames().contains(mSegmentToRemove)); + Assert.assertTrue(segmentsCaptor.getValue().getNames().contains(mCustomerSegment)); + Assert.assertEquals(2, segmentsCaptor.getValue().getNames().size()); + Assert.assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); + Assert.assertEquals(SplitTaskType.MY_SEGMENTS_UPDATE, result.getTaskType()); + } + + @Test + public void removeOperationRemovesOnlyNotifiedSegments() { + Set oldSegments = new HashSet<>(); + oldSegments.add(mCustomerSegment); + oldSegments.add(mSegmentToRemove); + oldSegments.add("extra_segment"); + + when(mySegmentsStorage.getAll()).thenReturn(oldSegments); + + mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, new HashSet<>(Arrays.asList(mSegmentToRemove, "extra_segment")), 25L, mEventsManager, mTelemetryRuntimeProducer, MySegmentsUpdateTaskConfig.getForMySegments()); + + ArgumentCaptor segmentsCaptor = ArgumentCaptor.forClass(SegmentsChange.class); + + SplitTaskExecutionInfo result = mTask.execute(); + + verify(mySegmentsStorage, times(1)).set(segmentsCaptor.capture()); + SegmentsChange captorValue = segmentsCaptor.getValue(); + Assert.assertFalse(captorValue.getNames().contains(mSegmentToRemove)); + Assert.assertFalse(captorValue.getNames().contains("extra_segment")); + Assert.assertTrue(captorValue.getNames().contains(mCustomerSegment)); + Assert.assertEquals(1, captorValue.getNames().size()); + Assert.assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); + verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.MY_SEGMENTS_UPDATED); + } + + @Test + public void removeOperationDoesNotNotifyWhenNothingWasRemoved() { + Set oldSegments = new HashSet<>(); + oldSegments.add(mCustomerSegment); + oldSegments.add(mSegmentToRemove); + + when(mySegmentsStorage.getAll()).thenReturn(oldSegments); + + mTask = new MySegmentsUpdateTask(mySegmentsStorage, false, Collections.singleton("extra_segment"), 25L, mEventsManager, mTelemetryRuntimeProducer, MySegmentsUpdateTaskConfig.getForMySegments()); + + SplitTaskExecutionInfo result = mTask.execute(); + + verify(mEventsManager, never()).notifyInternalEvent(any()); + } + @After public void tearDown() { reset(mySegmentsStorage); diff --git a/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java b/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java index 7692180d7..9c354f744 100644 --- a/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java +++ b/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java @@ -55,7 +55,7 @@ public void setup() { mSplitsSyncHelper = mock(SplitsSyncHelper.class); mEventsManager = mock(SplitEventsManager.class); - when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean())).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLIT_KILL)); + when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLIT_KILL)); loadSplitChanges(); } @@ -74,7 +74,7 @@ public void correctExecution() throws HttpFetcherException { mTask.execute(); - verify(mSplitsSyncHelper, times(1)).sync(-1, false, false); + verify(mSplitsSyncHelper, times(1)).sync(-1, false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); } @Test @@ -106,7 +106,7 @@ public void cleanOldCacheEnabled() throws HttpFetcherException { mTask.execute(); - verify(mSplitsSyncHelper, times(1)).sync(100L, true, false); + verify(mSplitsSyncHelper, times(1)).sync(100L, true, true, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); } @Test @@ -127,7 +127,7 @@ public void cleanSplitsWhenQueryStringHasChanged() throws HttpFetcherException { mTask.execute(); - verify(mSplitsSyncHelper, times(1)).sync(-1, true, true); + verify(mSplitsSyncHelper, times(1)).sync(-1, true, true, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsStorage, times(1)).updateSplitsFilterQueryString(otherQs); } @@ -145,7 +145,7 @@ public void noClearSplitsWhenQueryStringHasNotChanged() throws HttpFetcherExcept mTask.execute(); - verify(mSplitsSyncHelper, times(1)).sync(100L, false, false); + verify(mSplitsSyncHelper, times(1)).sync(100L, false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsStorage, never()).updateSplitsFilterQueryString(anyString()); } @@ -160,7 +160,7 @@ public void splitUpdatedNotified() throws HttpFetcherException { when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean())).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); mTask.execute(); @@ -178,7 +178,7 @@ public void splitFetchdNotified() throws HttpFetcherException { when(mSplitsStorage.getTill()).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean())).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); mTask.execute(); @@ -192,7 +192,7 @@ public void syncIsTrackedInTelemetry() { when(mSplitsStorage.getTill()).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean())).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); mTask.execute(); diff --git a/src/test/java/io/split/android/client/service/SplitTaskExecutorTest.java b/src/test/java/io/split/android/client/service/SplitTaskExecutorTest.java index 90157d367..341150e1c 100644 --- a/src/test/java/io/split/android/client/service/SplitTaskExecutorTest.java +++ b/src/test/java/io/split/android/client/service/SplitTaskExecutorTest.java @@ -24,6 +24,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskBatchItem; @@ -130,7 +131,7 @@ public void simpleScheduled() throws InterruptedException { latch.await(10, TimeUnit.SECONDS); Assert.assertTrue(task.taskHasBeenCalled); - Assert.assertEquals(4, task.callCount); + Assert.assertEquals(4, task.callCount.get()); } @Test @@ -152,7 +153,7 @@ public void multipleScheduled() throws InterruptedException { for (int i = 0; i < taskCount; i++) { Assert.assertTrue(taskList.get(i).taskHasBeenCalled); } - Assert.assertTrue(4 < taskList.get(0).callCount); + Assert.assertTrue(4 < taskList.get(0).callCount.get()); } @Test @@ -192,8 +193,8 @@ public void pauseScheduled() throws InterruptedException { mTaskExecutor.schedule( task1, 6L, 20, mock(SplitTaskExecutionListener.class)); latch.await(5, TimeUnit.SECONDS); - int countBeforePause = task.callCount; - int count1BeforePause = task1.callCount; + int countBeforePause = task.callCount.get(); + int count1BeforePause = task1.callCount.get(); mTaskExecutor.pause(); @@ -201,13 +202,13 @@ public void pauseScheduled() throws InterruptedException { // then resumes task executor // and wait for task 1 latch sleep(3); - int countAfterPause = task.callCount; - int count1AfterPause = task1.callCount; + int countAfterPause = task.callCount.get(); + int count1AfterPause = task1.callCount.get(); mTaskExecutor.resume(); latch1.await(5, TimeUnit.SECONDS); - int count1AfterResume = task1.callCount; + int count1AfterResume = task1.callCount.get(); Assert.assertEquals(3, countBeforePause); Assert.assertEquals(0, count1BeforePause); @@ -250,7 +251,7 @@ public void exceptionInScheduled() throws InterruptedException { latch.await(10, TimeUnit.SECONDS); Assert.assertTrue(task.taskHasBeenCalled); - Assert.assertEquals(4, task.callCount); + Assert.assertEquals(4, task.callCount.get()); } @Test @@ -278,7 +279,7 @@ public void stopStartedTask() throws InterruptedException { assertTrue(task.taskHasBeenCalled); assertTrue(testListener.taskExecutedCalled); - assertEquals(2, task.callCount); + assertEquals(2, task.callCount.get()); } @Test @@ -293,7 +294,7 @@ public void scheduleDelayedTask() throws InterruptedException { listenerLatch.await(2L, TimeUnit.SECONDS); assertTrue(task.taskHasBeenCalled); assertTrue(testListener.taskExecutedCalled); - assertEquals(1, task.callCount); + assertEquals(1, task.callCount.get()); } @Test @@ -314,7 +315,7 @@ public void schedulingAfterShutdown() throws InterruptedException { assertTrue(task.taskHasBeenCalled); assertTrue(testListener.taskExecutedCalled); - assertEquals(2, task.callCount); + assertEquals(2, task.callCount.get()); assertFalse(newTask.taskHasBeenCalled); } @@ -338,13 +339,13 @@ static class TestTask implements SplitTask { } public boolean shouldThrowException = false; - public int callCount = 0; + public AtomicInteger callCount = new AtomicInteger(0); public boolean taskHasBeenCalled = false; @NonNull @Override public SplitTaskExecutionInfo execute() { - callCount++; + callCount.incrementAndGet(); taskHasBeenCalled = true; latch.countDown(); if (shouldThrowException) { diff --git a/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java b/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java index 84e47742b..134db2981 100644 --- a/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java +++ b/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java @@ -3,33 +3,23 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.mockito.Mock; import org.mockito.Mockito; -import java.util.HashMap; -import java.util.Map; - import io.split.android.client.dtos.SplitChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskType; -import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpFetcherException; -import io.split.android.client.service.splits.SplitChangeProcessor; import io.split.android.client.service.splits.SplitsSyncHelper; -import io.split.android.client.service.splits.SplitsSyncTask; import io.split.android.client.service.splits.SplitsUpdateTask; -import io.split.android.client.storage.splits.ProcessedSplitChange; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.helpers.FileHelper; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.doThrow; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -50,8 +40,8 @@ public void setup() { mSplitsSyncHelper = Mockito.mock(SplitsSyncHelper.class); mEventsManager = Mockito.mock(SplitEventsManager.class); mTask = new SplitsUpdateTask(mSplitsSyncHelper, mSplitsStorage, mChangeNumber, mEventsManager); - when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean())).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); - when(mSplitsSyncHelper.sync(anyLong())).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); + when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); + when(mSplitsSyncHelper.sync(anyLong(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); loadSplitChanges(); } @@ -61,7 +51,7 @@ public void correctExecution() throws HttpFetcherException { mTask.execute(); - verify(mSplitsSyncHelper).sync(mChangeNumber); + verify(mSplitsSyncHelper).sync(mChangeNumber, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); } @Test @@ -70,7 +60,7 @@ public void storedChangeNumBigger() throws HttpFetcherException { mTask.execute(); - verify(mSplitsSyncHelper, never()).sync(anyLong(), anyBoolean(), anyBoolean()); + verify(mSplitsSyncHelper, never()).sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); } @After diff --git a/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java b/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java index b99575481..1b2d9e104 100644 --- a/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java +++ b/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java @@ -94,7 +94,7 @@ public void correctSyncExecution() throws HttpFetcherException { when(mSplitsFetcher.execute(mSecondFetchParams, null)).thenReturn(secondSplitChange); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, false, false); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher, times(1)).execute(mDefaultParams, null); verify(mSplitsStorage, times(1)).update(any()); @@ -116,7 +116,7 @@ public void correctSyncExecutionNoCache() throws HttpFetcherException { when(mSplitsFetcher.execute(mSecondFetchParams, null)).thenReturn(secondSplitChange); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher, times(1)).execute(mDefaultParams, headers); verify(mSplitsStorage, times(1)).update(any()); @@ -131,7 +131,7 @@ public void fetcherSyncException() throws HttpFetcherException { .thenThrow(HttpFetcherException.class); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, true, false); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher, times(1)).execute(mDefaultParams, null); verify(mSplitsStorage, never()).update(any()); @@ -146,7 +146,7 @@ public void storageException() throws HttpFetcherException { doThrow(NullPointerException.class).when(mSplitsStorage).update(any(ProcessedSplitChange.class)); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, true, false); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher, times(1)).execute(mDefaultParams, null); verify(mSplitsStorage, times(1)).update(any()); @@ -164,7 +164,7 @@ public void shouldClearStorageAfterFetch() throws HttpFetcherException { when(mSplitsFetcher.execute(mSecondFetchParams, null)).thenReturn(secondSplitChange); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, true, false); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher, times(1)).execute(mDefaultParams, null); verify(mSplitsStorage, times(1)).update(any()); @@ -194,7 +194,7 @@ public void cacheNotExpired() throws HttpFetcherException { // only when cache expired long cacheExpInSeconds = 10000; - long updateTimestamp = System.currentTimeMillis() / 1000 - cacheExpInSeconds + 1000; + long updateTimestamp = System.currentTimeMillis() - cacheExpInSeconds * 1000 + 1000; boolean expired = mSplitsSyncHelper.cacheHasExpired(100, updateTimestamp, cacheExpInSeconds); Assert.assertFalse(expired); @@ -220,7 +220,7 @@ public void errorIsRecordedInTelemetry() throws HttpFetcherException { .thenThrow(new HttpFetcherException("error", "error", 500)); when(mSplitsStorage.getTill()).thenReturn(-1L); - mSplitsSyncHelper.sync(-1, true, false); + mSplitsSyncHelper.sync(-1, true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mTelemetryRuntimeProducer).recordSyncError(OperationType.SPLITS, 500); } @@ -240,7 +240,7 @@ public void performSplitsFetchUntilSinceEqualsTill() throws HttpFetcherException when(mSplitsFetcher.execute(eq(secondParams), any())).thenReturn(secondSplitChange); when(mSplitsFetcher.execute(eq(thirdParams), any())).thenReturn(thirdSplitChange); - mSplitsSyncHelper.sync(3); + mSplitsSyncHelper.sync(3, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsStorage, times(3)).getTill(); verify(mSplitsFetcher).execute(eq(firstParams), any()); @@ -260,7 +260,7 @@ public void performSplitFetchUntilStoredChangeNumberIsGreaterThanRequested() thr when(mSplitsFetcher.execute(eq(firstParams), any())).thenReturn(firstSplitChange); when(mSplitsFetcher.execute(eq(secondParams), any())).thenReturn(secondSplitChange); - mSplitsSyncHelper.sync(3); + mSplitsSyncHelper.sync(3, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsStorage, times(3)).getTill(); verify(mSplitsFetcher).execute(eq(firstParams), any()); @@ -271,7 +271,7 @@ public void performSplitFetchUntilStoredChangeNumberIsGreaterThanRequested() thr public void syncWithClearBeforeUpdateOnlyClearsStorageOnce() { when(mSplitsStorage.getTill()).thenReturn(-1L, 2L, 4L); - mSplitsSyncHelper.sync(3, true, false); + mSplitsSyncHelper.sync(3, true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsStorage).clear(); } @@ -280,7 +280,7 @@ public void syncWithClearBeforeUpdateOnlyClearsStorageOnce() { public void syncWithoutClearBeforeUpdateDoesNotClearStorage() { when(mSplitsStorage.getTill()).thenReturn(-1L, 2L, 4L); - mSplitsSyncHelper.sync(3, false, false); + mSplitsSyncHelper.sync(3, false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsStorage, never()).clear(); } @@ -304,7 +304,7 @@ public void cdnIsBypassedWhenNeeded() throws HttpFetcherException { getSplitChange(4, 4) ); - mSplitsSyncHelper.sync(4); + mSplitsSyncHelper.sync(4, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); Map headers = new HashMap<>(); headers.put("Cache-Control", "no-cache"); @@ -332,7 +332,7 @@ public void backoffIsAppliedWhenRetryingSplits() throws HttpFetcherException { getSplitChange(4, 4) ); - mSplitsSyncHelper.sync(4); + mSplitsSyncHelper.sync(4, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mBackoffCounter).resetCounter(); verify(mBackoffCounter, times(2)).getNextRetryTime(); @@ -340,7 +340,7 @@ public void backoffIsAppliedWhenRetryingSplits() throws HttpFetcherException { @Test public void replaceTillWhenFilterHasChanged() throws HttpFetcherException { - mSplitsSyncHelper.sync(14829471, true, true); + mSplitsSyncHelper.sync(14829471, true, true, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); Map params = new HashMap<>(); params.put("s", "1.1"); @@ -355,7 +355,7 @@ public void returnTaskInfoToDoNotRetryWhenHttpFetcherExceptionStatusCodeIs414() .thenThrow(new HttpFetcherException("error", "error", 414)); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); assertEquals(true, result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); } @@ -366,7 +366,7 @@ public void doNotRetryFlagIsNullWhenFetcherExceptionStatusCodeIsNot414() throws .thenThrow(new HttpFetcherException("error", "error", 500)); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); assertNull(result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); } @@ -377,7 +377,7 @@ public void returnTaskInfoToDoNotRetryWhenHttpFetcherExceptionStatusCodeIs9009() .thenThrow(new HttpFetcherException("error", "error", 9009)); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); assertEquals(true, result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); } @@ -388,14 +388,14 @@ public void doNotRetryFlagIsNullWhenFetcherExceptionStatusCodeIsNot9009() throws .thenThrow(new HttpFetcherException("error", "error", 500)); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); assertNull(result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); } @Test public void defaultQueryParamOrderIsCorrect() throws HttpFetcherException { - mSplitsSyncHelper.sync(100); + mSplitsSyncHelper.sync(100, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher).execute(argThat(new ArgumentMatcher>() { @Override 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 6a932d6a9..c0c32eabb 100644 --- a/src/test/java/io/split/android/client/service/SynchronizerTest.java +++ b/src/test/java/io/split/android/client/service/SynchronizerTest.java @@ -41,7 +41,6 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.dtos.Event; import io.split.android.client.dtos.KeyImpression; -import io.split.android.client.dtos.MySegment; import io.split.android.client.dtos.SplitChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.impressions.Impression; @@ -163,7 +162,7 @@ public void setup(SplitClientConfig splitClientConfig, ImpressionManagerConfig.M mTaskExecutor = taskExecutor; mSingleThreadedTaskExecutor = spy(new SplitTaskExecutorStub()); HttpFetcher splitsFetcher = Mockito.mock(HttpFetcher.class); - HttpFetcher> mySegmentsFetcher = Mockito.mock(HttpFetcher.class); + HttpFetcher mySegmentsFetcher = Mockito.mock(HttpFetcher.class); HttpRecorder> eventsRecorder = Mockito.mock(HttpRecorder.class); HttpRecorder> impressionsRecorder = Mockito.mock(HttpRecorder.class); @@ -182,7 +181,7 @@ public void setup(SplitClientConfig splitClientConfig, ImpressionManagerConfig.M when(mSplitStorageContainer.getImpressionsStorage()).thenReturn(mImpressionsStorage); when(mTaskFactory.createSplitsSyncTask(anyBoolean())).thenReturn(Mockito.mock(SplitsSyncTask.class)); - when(mMySegmentsTaskFactory.createMySegmentsSyncTask(anyBoolean())).thenReturn(Mockito.mock(MySegmentsSyncTask.class)); + when(mMySegmentsTaskFactory.createMySegmentsSyncTask(anyBoolean(), anyLong(), anyLong())).thenReturn(Mockito.mock(MySegmentsSyncTask.class)); when(mTaskFactory.createImpressionsRecorderTask()).thenReturn(Mockito.mock(ImpressionsRecorderTask.class)); when(mTaskFactory.createEventsRecorderTask()).thenReturn(Mockito.mock(EventsRecorderTask.class)); when(mTaskFactory.createLoadSplitsTask()).thenReturn(Mockito.mock(LoadSplitsTask.class)); @@ -247,6 +246,20 @@ public void splitExecutorSchedule() { mSynchronizer.unregisterMySegmentsSynchronizer("userKey"); } + @Test + public void startPeriodicRecordingSchedulesLargeSegmentsSyncTask() { + SplitClientConfig config = SplitClientConfig.builder() + .eventsQueueSize(10) + .userConsent(UserConsent.GRANTED) + .impressionsQueueSize(3) + .build(); + setup(config); + + mSynchronizer.startPeriodicFetching(); + + verify(mMySegmentsSynchronizerRegistry).scheduleSegmentsSyncTask(); + } + @Test public void workManagerSchedule() throws InterruptedException { SplitClientConfig config = SplitClientConfig.builder() @@ -515,7 +528,7 @@ public void loadLocalData() { setup(config); List list = new ArrayList<>(); - list.add(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_MY_SYGMENTS)); + list.add(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_MY_SEGMENTS)); list.add(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); list.add(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_ATTRIBUTES)); SplitTaskExecutor executor = new SplitTaskExecutorSub(list); @@ -528,7 +541,7 @@ public void loadLocalData() { mFeatureFlagsSynchronizer, mSplitStorageContainer.getEventsStorage()); LoadMySegmentsTask loadMySegmentsTask = mock(LoadMySegmentsTask.class); - when(loadMySegmentsTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_MY_SYGMENTS)); + when(loadMySegmentsTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_MY_SEGMENTS)); when(mMySegmentsTaskFactory.createLoadMySegmentsTask()).thenReturn(loadMySegmentsTask); ((MySegmentsSynchronizerRegistry) mSynchronizer).registerMySegmentsSynchronizer("", mMySegmentsSynchronizer); @@ -608,15 +621,6 @@ public void synchronizeMySegmentsDelegatesToRegistry() { verify(mMySegmentsSynchronizerRegistry).synchronizeMySegments(); } - @Test - public void forceMySegmentsSyncDelegatesToRegistry() { - setup(SplitClientConfig.builder().synchronizeInBackground(false).build()); - - mSynchronizer.forceMySegmentsSync(); - - verify(mMySegmentsSynchronizerRegistry).forceMySegmentsSync(); - } - @Test public void destroyDelegatesToRegisteredSyncs() { setup(SplitClientConfig.builder().synchronizeInBackground(false).build()); @@ -685,8 +689,8 @@ public void beingNotifiedOfSplitsSyncTaskTriggersSplitsLoad() { } @Test - public void beginNotifiedOfMySegmentsSyncTriggersMySegmentsLoad() { - setup(SplitClientConfig.builder().persistentAttributesEnabled(false).build()); + public void beingNotifiedOfMySegmentsSyncTriggersMySegmentsLoad() { + setup(SplitClientConfig.builder().build()); mSynchronizer.taskExecuted(SplitTaskExecutionInfo.success(SplitTaskType.MY_SEGMENTS_SYNC)); @@ -767,6 +771,24 @@ public void reschedulingEventsTaskCancelsPreviousWhenCallingSequentially() { verify(mTaskExecutor, times(1)).stopTask("task-id"); } + @Test + public void registerMySegmentsSynchronizerDelegatesToRegistry() { + setup(SplitClientConfig.builder().synchronizeInBackground(false).build()); + + mSynchronizer.registerMySegmentsSynchronizer("userKey", mMySegmentsSynchronizer); + + verify(mMySegmentsSynchronizerRegistry).registerMySegmentsSynchronizer("userKey", mMySegmentsSynchronizer); + } + + @Test + public void synchronizeSplitsDelegatesToFeatureFlagsSynchronizer() { + setup(SplitClientConfig.builder().synchronizeInBackground(false).build()); + + mSynchronizer.synchronizeSplits(); + + verify(mFeatureFlagsSynchronizer).synchronize(); + } + @After public void tearDown() { } diff --git a/src/test/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImplTest.java b/src/test/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImplTest.java new file mode 100644 index 000000000..5ccc083bd --- /dev/null +++ b/src/test/java/io/split/android/client/service/http/mysegments/MySegmentsFetcherFactoryImplTest.java @@ -0,0 +1,40 @@ +package io.split.android.client.service.http.mysegments; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +import java.net.URI; +import java.net.URISyntaxException; + +import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.network.HttpClient; +import io.split.android.client.service.http.HttpFetcher; +import io.split.android.client.service.http.HttpResponseParser; + +public class MySegmentsFetcherFactoryImplTest { + + @Test + public void getFetcherCallsUriBuilderImplementation() throws URISyntaxException { + MySegmentsFetcherFactory.UriBuilder uriBuilder = mock(MySegmentsFetcherFactory.UriBuilder.class); + when(uriBuilder.build("matchingKey")).thenReturn(mock(URI.class)); + HttpFetcher fetcher = new MySegmentsFetcherFactoryImpl(mock(HttpClient.class), + "endpoint", + mock(HttpResponseParser.class), + uriBuilder).getFetcher("matchingKey"); + + verify(uriBuilder).build("matchingKey"); + } + + @Test + public void getFetcherDoesNotCrashWhenUriBuilderFails() throws URISyntaxException { + MySegmentsFetcherFactory.UriBuilder uriBuilder = mock(MySegmentsFetcherFactory.UriBuilder.class); + when(uriBuilder.build("matchingKey")).thenThrow(new URISyntaxException("test", "test")); + HttpFetcher fetcher = new MySegmentsFetcherFactoryImpl(mock(HttpClient.class), + "endpoint", + mock(HttpResponseParser.class), + uriBuilder).getFetcher("matchingKey"); + } +} diff --git a/src/test/java/io/split/android/client/service/mysegments/AllSegmentsResponseParserTest.java b/src/test/java/io/split/android/client/service/mysegments/AllSegmentsResponseParserTest.java new file mode 100644 index 000000000..dd3067e48 --- /dev/null +++ b/src/test/java/io/split/android/client/service/mysegments/AllSegmentsResponseParserTest.java @@ -0,0 +1,38 @@ +package io.split.android.client.service.mysegments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.service.http.HttpResponseParserException; + +public class AllSegmentsResponseParserTest { + + private AllSegmentsResponseParser mParser; + + @Before + public void setUp() { + mParser = new AllSegmentsResponseParser(); + } + + @Test + public void parse() throws HttpResponseParserException { + String response = "{\"ms\":{\"k\":[{\"n\":\"segment1\"},{\"n\":\"segment2\"}], \"cn\":null},\"ls\":{\"k\":[{\"n\":\"large-segment1\"},{\"n\":\"large-segment2\"},{\"n\":\"large-segment3\"}], \"cn\":2000}}"; + + AllSegmentsChange parsed = mParser.parse(response); + + assertTrue(parsed.getSegmentsChange().getNames().contains("segment1")); + assertTrue(parsed.getSegmentsChange().getNames().contains("segment2")); + assertTrue(parsed.getLargeSegmentsChange().getNames().contains("large-segment1")); + assertTrue(parsed.getLargeSegmentsChange().getNames().contains("large-segment2")); + assertTrue(parsed.getLargeSegmentsChange().getNames().contains("large-segment3")); + assertEquals(2, parsed.getSegmentsChange().getSegments().size()); + assertEquals(3, parsed.getLargeSegmentsChange().getSegments().size()); + assertNull(parsed.getSegmentsChange().getChangeNumber()); + assertEquals(Long.valueOf(2000), parsed.getLargeSegmentsChange().getChangeNumber()); + } +} diff --git a/src/test/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfigTest.java b/src/test/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfigTest.java new file mode 100644 index 000000000..9973189e6 --- /dev/null +++ b/src/test/java/io/split/android/client/service/mysegments/LoadMySegmentsTaskConfigTest.java @@ -0,0 +1,17 @@ +package io.split.android.client.service.mysegments; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.split.android.client.service.executor.SplitTaskType; + +public class LoadMySegmentsTaskConfigTest { + + @Test + public void getForMySegments() { + LoadMySegmentsTaskConfig config = LoadMySegmentsTaskConfig.get(); + + assertEquals(config.getTaskType(), SplitTaskType.LOAD_LOCAL_MY_SEGMENTS); + } +} diff --git a/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java b/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java new file mode 100644 index 000000000..07f4fa6a0 --- /dev/null +++ b/src/test/java/io/split/android/client/service/mysegments/MySegmentsSyncTaskConfigTest.java @@ -0,0 +1,22 @@ +package io.split.android.client.service.mysegments; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.telemetry.model.OperationType; + +public class MySegmentsSyncTaskConfigTest { + + @Test + public void getForMySegments() { + MySegmentsSyncTaskConfig config = MySegmentsSyncTaskConfig.get(); + + assertEquals(config.getTaskType(), SplitTaskType.MY_SEGMENTS_SYNC); + assertEquals(config.getUpdateEvent(), SplitInternalEvent.MY_SEGMENTS_UPDATED); + assertEquals(config.getFetchedEvent(), SplitInternalEvent.MY_SEGMENTS_FETCHED); + assertEquals(config.getTelemetryOperationType(), OperationType.MY_SEGMENT); + } +} diff --git a/src/test/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfigurationTest.java b/src/test/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfigurationTest.java new file mode 100644 index 000000000..765714214 --- /dev/null +++ b/src/test/java/io/split/android/client/service/mysegments/MySegmentsTaskFactoryConfigurationTest.java @@ -0,0 +1,45 @@ +package io.split.android.client.service.mysegments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; + +import org.junit.Before; +import org.junit.Test; + +import io.split.android.client.dtos.AllSegmentsChange; +import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.service.http.HttpFetcher; +import io.split.android.client.storage.mysegments.MySegmentsStorage; + +public class MySegmentsTaskFactoryConfigurationTest { + + private HttpFetcher mHttpFetcher; + private MySegmentsStorage mMySegmentsStorage; + private MySegmentsStorage mMyLargeSegmentsStorage; + private SplitEventsManager mEventsManager; + + @Before + public void setUp() { + mHttpFetcher = mock(HttpFetcher.class); + mMySegmentsStorage = mock(MySegmentsStorage.class); + mMyLargeSegmentsStorage = mock(MySegmentsStorage.class); + mEventsManager = mock(SplitEventsManager.class); + } + + @Test + public void getForMySegments() { + MySegmentsTaskFactoryConfiguration config = MySegmentsTaskFactoryConfiguration.get( + mHttpFetcher, + mMySegmentsStorage, + mMyLargeSegmentsStorage, + mEventsManager); + + assertSame(mHttpFetcher, config.getHttpFetcher()); + assertSame(mMySegmentsStorage, config.getMySegmentsStorage()); + assertSame(mEventsManager, config.getEventsManager()); + assertEquals(MySegmentsSyncTaskConfig.get(), config.getMySegmentsSyncTaskConfig()); + assertEquals(MySegmentsUpdateTaskConfig.getForMySegments(), config.getMySegmentsUpdateTaskConfig()); + assertEquals(LoadMySegmentsTaskConfig.get(), config.getLoadMySegmentsTaskConfig()); + } +} diff --git a/src/test/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfigTest.java b/src/test/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfigTest.java new file mode 100644 index 000000000..ea7f26181 --- /dev/null +++ b/src/test/java/io/split/android/client/service/mysegments/MySegmentsUpdateTaskConfigTest.java @@ -0,0 +1,21 @@ +package io.split.android.client.service.mysegments; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.telemetry.model.streaming.UpdatesFromSSEEnum; + +public class MySegmentsUpdateTaskConfigTest { + + @Test + public void getForMySegments() { + MySegmentsUpdateTaskConfig config = MySegmentsUpdateTaskConfig.getForMySegments(); + + assertEquals(config.getTaskType(), SplitTaskType.MY_SEGMENTS_UPDATE); + assertEquals(config.getUpdateEvent(), SplitInternalEvent.MY_SEGMENTS_UPDATED); + assertEquals(config.getTelemetrySSEKey(), UpdatesFromSSEEnum.MY_SEGMENTS); + } +} diff --git a/src/test/java/io/split/android/client/service/sseclient/MySegmentsUpdateWorkerTest.java b/src/test/java/io/split/android/client/service/sseclient/MySegmentsUpdateWorkerTest.java index a3547bad5..f1cd8ac0c 100644 --- a/src/test/java/io/split/android/client/service/sseclient/MySegmentsUpdateWorkerTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/MySegmentsUpdateWorkerTest.java @@ -1,5 +1,6 @@ package io.split.android.client.service.sseclient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -12,7 +13,7 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeNotification; +import io.split.android.client.service.mysegments.MySegmentUpdateParams; import io.split.android.client.service.sseclient.reactor.MySegmentsUpdateWorker; import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizer; @@ -23,7 +24,7 @@ public class MySegmentsUpdateWorkerTest { @Mock MySegmentsSynchronizer mSynchronizer; - BlockingQueue mNotificationQueue; + BlockingQueue mNotificationQueue; @Before public void setup() { @@ -36,20 +37,27 @@ public void setup() { @Test public void mySegmentsUpdateReceived() throws InterruptedException { - mNotificationQueue.offer(new MySegmentChangeNotification()); - mNotificationQueue.offer(new MySegmentChangeNotification()); - mNotificationQueue.offer(new MySegmentChangeNotification()); - mNotificationQueue.offer(new MySegmentChangeNotification()); - - Thread.sleep(1000); - - verify(mSynchronizer, times(4)).forceMySegmentsSync(); + MySegmentUpdateParams params = mock(MySegmentUpdateParams.class); + MySegmentUpdateParams params2 = mock(MySegmentUpdateParams.class); + MySegmentUpdateParams params3 = mock(MySegmentUpdateParams.class); + MySegmentUpdateParams params4 = mock(MySegmentUpdateParams.class); + mNotificationQueue.offer(params); + mNotificationQueue.offer(params2); + mNotificationQueue.offer(params3); + mNotificationQueue.offer(params4); + + Thread.sleep(200); + + verify(mSynchronizer, times(1)).forceMySegmentsSync(params); + verify(mSynchronizer, times(1)).forceMySegmentsSync(params2); + verify(mSynchronizer, times(1)).forceMySegmentsSync(params3); + verify(mSynchronizer, times(1)).forceMySegmentsSync(params4); } @Test public void stopped() throws InterruptedException { mWorker.stop(); - mNotificationQueue.offer(new MySegmentChangeNotification()); + mNotificationQueue.offer(mock(MySegmentUpdateParams.class)); Thread.sleep(1000); diff --git a/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java b/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java index 9a80a13aa..5e6adb906 100644 --- a/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java @@ -1,15 +1,22 @@ package io.split.android.client.service.sseclient; +import static org.junit.Assert.assertEquals; +import static io.split.android.client.service.sseclient.notifications.NotificationType.MEMBERSHIPS_LS_UPDATE; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import io.split.android.client.common.CompressionType; import io.split.android.client.service.sseclient.notifications.ControlNotification; +import io.split.android.client.service.sseclient.notifications.HashingAlgorithm; import io.split.android.client.service.sseclient.notifications.IncomingNotification; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeNotification; +import io.split.android.client.service.sseclient.notifications.MembershipNotification; +import io.split.android.client.service.sseclient.notifications.MySegmentUpdateStrategy; import io.split.android.client.service.sseclient.notifications.NotificationParser; import io.split.android.client.service.sseclient.notifications.NotificationType; import io.split.android.client.service.sseclient.notifications.OccupancyNotification; @@ -25,17 +32,13 @@ public class NotificationParserTest { "{\"id\":\"VSEQrcq9D8:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:MjU4MzkwNDA2NA==\",\"timestamp\":1584554772719,\"encoding\":\"json\",\"channel\":\"MzM5Njc0ODcyNg==_MTExMzgwNjgx_splits\",\n" + "\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1584554772108}\"}"; - private final static String MY_SEGMENT_UDATE_NOTIFICATION = "{\"id\":\"x2dE2TEiJL:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:OTc5Nzc4NDYz\",\"timestamp\":1584647533288,\"encoding\":\"json\",\"channel\":\"MzM5Njc0ODcyNg==_MTExMzgwNjgx_MTcwNTI2MTM0Mg==_mySegments\",\"data\":\"{\\\"type\\\":\\\"MY_SEGMENTS_UPDATE\\\",\\\"changeNumber\\\":1584647532812,\\\"includesPayload\\\":false}\"}"; - private final static String SPLIT_KILL_NOTIFICATION = "{\"id\":\"-OT-rGuSwz:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:NDIxNjU0NTUyNw==\",\"timestamp\":1584647606489,\"encoding\":\"json\",\"channel\":\"MzM5Njc0ODcyNg==_MTExMzgwNjgx_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_KILL\\\",\\\"changeNumber\\\":1584647606125,\\\"defaultTreatment\\\":\\\"off\\\",\\\"splitName\\\":\\\"dep_split\\\"}\"}"; - private final static String MY_SEGMENT_UDATE_INLINE_NOTIFICATION = "{\"id\":\"x2dE2TEiJL:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:OTc5Nzc4NDYz\",\"timestamp\":1584647533288,\"encoding\":\"json\",\"channel\":\"MzM5Njc0ODcyNg==_MTExMzgwNjgx_MTcwNTI2MTM0Mg==_mySegments\",\"data\":\"{\\\"type\\\":\\\"MY_SEGMENTS_UPDATE\\\",\\\"changeNumber\\\":1584647532812,\\\"includesPayload\\\":true,\\\"segmentList\\\":[\\\"segment1\\\", \\\"segment2\\\"]}\"}"; - private final static String OCCUPANCY = "{\"id\":\"x2dE2TEiJL:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:OTc5Nzc4NDYz\",\"timestamp\":1584647533288,\"encoding\":\"json\",\"channel\":\"control_pri\",\"data\":\"{\\\"metrics\\\": {\\\"publishers\\\":1}}\"}"; private final static String CONTROL = "{\"id\":\"x2dE2TEiJL:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:OTc5Nzc4NDYz\",\"timestamp\":1584647533288,\"encoding\":\"json\",\"channel\":\"control_pri\",\"data\":\"{\\\"type\\\":\\\"CONTROL\\\",\\\"controlType\\\":\\\"STREAMING_RESUMED\\\"}\"}"; - private final static String ERROR = "{\"id\":\"null\",\"name\":\"error\",\"comment\":\"[no comments]\",\"data\":\"{\\\"message\\\":\\\"Invalid token; capability must be a string\\\",\\\"code\\\":40144,\\\"statusCode\\\":400,\\\"href\\\":\\\"https://help.ably.io/error/40144\\\"}\"}"; + private static final String MY_LARGE_SEGMENTS_UPDATE = "{\"id\": \"diSrQttrC9:0:0\",\"clientId\": \"pri:MjcyNDE2NDUxMA==\",\"timestamp\": 1702507131100,\"encoding\": \"json\",\"channel\": \"NzM2MDI5Mzc0_MTc1MTYwODQxMQ==_memberships\",\"data\": \"{\\\"type\\\":\\\"MEMBERSHIPS_LS_UPDATE\\\",\\\"cn\\\":1702507130121,\\\"n\\\":[\\\"android_test\\\"],\\\"c\\\":2,\\\"u\\\":2,\\\"d\\\":\\\"eJwEwLsRwzAMA9BdWKsg+IFBraJTkRXS5rK7388+tg+KdC8+jq4eBBQLFcUnO8FAAC36gndOSEyFqJFP32Vf2+f+3wAAAP//hUQQ9A==\\\",\\\"i\\\":100,\\\"h\\\":1,\\\"s\\\":325}\"}"; @Before public void setup() { @@ -47,8 +50,8 @@ public void processSplitUpdate() { IncomingNotification incoming = mParser.parseIncoming(SPLIT_UPDATE_NOTIFICATION); SplitsChangeNotification splitUpdate = mParser.parseSplitUpdate(incoming.getJsonData()); - Assert.assertEquals(NotificationType.SPLIT_UPDATE, incoming.getType()); - Assert.assertEquals(1584554772108L, splitUpdate.getChangeNumber()); + assertEquals(NotificationType.SPLIT_UPDATE, incoming.getType()); + assertEquals(1584554772108L, splitUpdate.getChangeNumber()); } @Test @@ -56,32 +59,9 @@ public void processSplitKill() { IncomingNotification incoming = mParser.parseIncoming(SPLIT_KILL_NOTIFICATION); SplitKillNotification splitKill = mParser.parseSplitKill(incoming.getJsonData()); - Assert.assertEquals(NotificationType.SPLIT_KILL, incoming.getType()); - Assert.assertEquals("dep_split", splitKill.getSplitName()); - Assert.assertEquals("off", splitKill.getDefaultTreatment()); - } - - @Test - public void processMySegmentUpdate() { - IncomingNotification incoming = mParser.parseIncoming(MY_SEGMENT_UDATE_NOTIFICATION); - MySegmentChangeNotification mySegmentUpdate = mParser.parseMySegmentUpdate(incoming.getJsonData()); - - Assert.assertEquals(NotificationType.MY_SEGMENTS_UPDATE, incoming.getType()); - Assert.assertEquals(1584647532812L, mySegmentUpdate.getChangeNumber()); - Assert.assertFalse(mySegmentUpdate.isIncludesPayload()); - } - - @Test - public void processMySegmentUpdateInline() { - IncomingNotification incoming = mParser.parseIncoming(MY_SEGMENT_UDATE_INLINE_NOTIFICATION); - MySegmentChangeNotification mySegmentUpdate = mParser.parseMySegmentUpdate(incoming.getJsonData()); - - Assert.assertEquals(NotificationType.MY_SEGMENTS_UPDATE, incoming.getType()); - Assert.assertEquals(1584647532812L, mySegmentUpdate.getChangeNumber()); - Assert.assertTrue(mySegmentUpdate.isIncludesPayload()); - Assert.assertEquals(2, mySegmentUpdate.getSegmentList().size()); - Assert.assertEquals("segment1", mySegmentUpdate.getSegmentList().get(0)); - Assert.assertEquals("segment2", mySegmentUpdate.getSegmentList().get(1)); + assertEquals(NotificationType.SPLIT_KILL, incoming.getType()); + assertEquals("dep_split", splitKill.getSplitName()); + assertEquals("off", splitKill.getDefaultTreatment()); } @Test @@ -90,8 +70,8 @@ public void processOccupancy() { OccupancyNotification notification = mParser.parseOccupancy(incoming.getJsonData()); - Assert.assertEquals(NotificationType.OCCUPANCY, notification.getType()); - Assert.assertEquals(1, notification.getMetrics().getPublishers()); + assertEquals(NotificationType.OCCUPANCY, notification.getType()); + assertEquals(1, notification.getMetrics().getPublishers()); } @Test @@ -99,8 +79,8 @@ public void processControl() { IncomingNotification incoming = mParser.parseIncoming(CONTROL); ControlNotification notification = mParser.parseControl(incoming.getJsonData()); - Assert.assertEquals(NotificationType.CONTROL, notification.getType()); - Assert.assertEquals(ControlNotification.ControlType.STREAMING_RESUMED, notification.getControlType()); + assertEquals(NotificationType.CONTROL, notification.getType()); + assertEquals(ControlNotification.ControlType.STREAMING_RESUMED, notification.getControlType()); } @Test @@ -110,9 +90,9 @@ public void parseErrorMessage() { StreamingError errorMsg = mParser.parseError(data); - Assert.assertEquals("Token expired", errorMsg.getMessage()); - Assert.assertEquals(40142, errorMsg.getCode()); - Assert.assertEquals(401, errorMsg.getStatusCode()); + assertEquals("Token expired", errorMsg.getMessage()); + assertEquals(40142, errorMsg.getCode()); + assertEquals(401, errorMsg.getStatusCode()); } @Test @@ -153,4 +133,29 @@ public void NoCrashIfisNullEventError() { Assert.assertFalse(isError); } + + @Test + public void parseMyLargeSegmentsIncomingNotification() { + IncomingNotification incoming = mParser.parseIncoming(MY_LARGE_SEGMENTS_UPDATE); + + assertEquals(MEMBERSHIPS_LS_UPDATE, incoming.getType()); + assertEquals("{\"type\":\"MEMBERSHIPS_LS_UPDATE\",\"cn\":1702507130121,\"n\":[\"android_test\"],\"c\":2,\"u\":2,\"d\":\"eJwEwLsRwzAMA9BdWKsg+IFBraJTkRXS5rK7388+tg+KdC8+jq4eBBQLFcUnO8FAAC36gndOSEyFqJFP32Vf2+f+3wAAAP//hUQQ9A==\",\"i\":100,\"h\":1,\"s\":325}", incoming.getJsonData()); + assertEquals("NzM2MDI5Mzc0_MTc1MTYwODQxMQ==_memberships", incoming.getChannel()); + assertEquals(1702507131100L, incoming.getTimestamp()); + } + + @Test + public void parseMyLargeSegmentsNotificationData() { + IncomingNotification incomingNotification = mParser.parseIncoming(MY_LARGE_SEGMENTS_UPDATE); + MembershipNotification notification = mParser.parseMembershipNotification(incomingNotification.getJsonData()); + + assertEquals("eJwEwLsRwzAMA9BdWKsg+IFBraJTkRXS5rK7388+tg+KdC8+jq4eBBQLFcUnO8FAAC36gndOSEyFqJFP32Vf2+f+3wAAAP//hUQQ9A==", notification.getData()); + assertEquals((Long) 1702507130121L, notification.getChangeNumber()); + assertEquals(Collections.singleton("android_test"), notification.getNames()); + assertEquals(CompressionType.ZLIB, notification.getCompression()); + assertEquals(MySegmentUpdateStrategy.KEY_LIST, notification.getUpdateStrategy()); + assertEquals((Long) 100L, notification.getUpdateIntervalMs()); + assertEquals((Integer) 325, notification.getAlgorithmSeed()); + assertEquals(HashingAlgorithm.MURMUR3_32, notification.getHashingAlgorithm()); + } } diff --git a/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java b/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java index 1f05cc94d..863d7805e 100644 --- a/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java @@ -21,15 +21,13 @@ import io.split.android.client.service.executor.SplitTaskFactory; import io.split.android.client.service.splits.SplitKillTask; import io.split.android.client.service.sseclient.notifications.IncomingNotification; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeNotification; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeV2Notification; -import io.split.android.client.service.sseclient.notifications.MySegmentsPayloadDecoder; +import io.split.android.client.service.sseclient.notifications.MembershipNotification; 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.NotificationType; import io.split.android.client.service.sseclient.notifications.SplitKillNotification; import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; -import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessor; +import io.split.android.client.service.sseclient.notifications.memberships.MembershipsNotificationProcessor; public class NotificationProcessorTest { @@ -43,8 +41,6 @@ public class NotificationProcessorTest { private BlockingQueue mSplitsChangeQueue; @Mock private IncomingNotification mIncomingNotification; - @Mock - private MySegmentsPayloadDecoder mMySegmentsPayloadDecoder; private NotificationProcessor mNotificationProcessor; @Before @@ -57,7 +53,7 @@ public void setup() { mNotificationProcessor = new NotificationProcessor(mSplitTaskExecutor, mSplitTaskFactory, mNotificationParser, - mSplitsChangeQueue, mMySegmentsPayloadDecoder); + mSplitsChangeQueue); } @Test @@ -92,40 +88,20 @@ public void splitKillNotification() { verify(mSplitTaskExecutor, times(1)).submit(any(), isNull()); } - @Test - public void notificationProcessorDelegatesRegisteredProcessorDependingOnKey() { - MySegmentsNotificationProcessor mySegmentsNotificationProcessor = mock(MySegmentsNotificationProcessor.class); - MySegmentsNotificationProcessor mySegmentsNotificationProcessor2 = mock(MySegmentsNotificationProcessor.class); - MySegmentChangeNotification mySegmentChangeNotification = mock(MySegmentChangeNotification.class); - when(mNotificationParser.extractUserKeyHashFromChannel("a_b_MjAwNjI0Nzg3NQ==_mySegments")).thenReturn("MjAwNjI0Nzg3NQ=="); - when(mIncomingNotification.getChannel()).thenReturn("a_b_MjAwNjI0Nzg3NQ==_mySegments"); - when(mMySegmentsPayloadDecoder.hashUserKeyForMySegmentsV1("user_key")).thenReturn("MjAwNjI0Nzg3NQ=="); - - when(mySegmentChangeNotification.getJsonData()).thenReturn("{}"); - when(mIncomingNotification.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE); - when(mNotificationParser.parseMySegmentUpdate(anyString())).thenReturn(mySegmentChangeNotification); - - mNotificationProcessor.registerMySegmentsProcessor("user_key", mySegmentsNotificationProcessor); - mNotificationProcessor.registerMySegmentsProcessor("button_key", mySegmentsNotificationProcessor2); - mNotificationProcessor.process(mIncomingNotification); - - verify(mySegmentsNotificationProcessor).processMySegmentsUpdate(mySegmentChangeNotification); - } - @Test public void notificationProcessorDelegatesMySegmentsNotificationsV2ToRegisteredProcessors() { - MySegmentsNotificationProcessor mySegmentsNotificationProcessor = mock(MySegmentsNotificationProcessor.class); - MySegmentsNotificationProcessor mySegmentsNotificationProcessor2 = mock(MySegmentsNotificationProcessor.class); - MySegmentChangeV2Notification mySegmentChangeNotification = mock(MySegmentChangeV2Notification.class); + MembershipsNotificationProcessor mySegmentsNotificationProcessor = mock(MembershipsNotificationProcessor.class); + MembershipsNotificationProcessor mySegmentsNotificationProcessor2 = mock(MembershipsNotificationProcessor.class); + MembershipNotification mySegmentChangeNotification = mock(MembershipNotification.class); when(mySegmentChangeNotification.getJsonData()).thenReturn("{}"); - when(mIncomingNotification.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); - when(mNotificationParser.parseMySegmentUpdateV2(anyString())).thenReturn(mySegmentChangeNotification); + when(mIncomingNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); + when(mNotificationParser.parseMembershipNotification(anyString())).thenReturn(mySegmentChangeNotification); - mNotificationProcessor.registerMySegmentsProcessor("1", mySegmentsNotificationProcessor); - mNotificationProcessor.registerMySegmentsProcessor("2", mySegmentsNotificationProcessor2); + mNotificationProcessor.registerMembershipsNotificationProcessor("1", mySegmentsNotificationProcessor); + mNotificationProcessor.registerMembershipsNotificationProcessor("2", mySegmentsNotificationProcessor2); mNotificationProcessor.process(mIncomingNotification); - verify(mySegmentsNotificationProcessor).processMySegmentsUpdateV2(mySegmentChangeNotification); + verify(mySegmentsNotificationProcessor).process(mySegmentChangeNotification); } } diff --git a/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java b/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java index 049784277..f6bec9eda 100644 --- a/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java @@ -23,8 +23,7 @@ import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent.EventType; import io.split.android.client.service.sseclient.notifications.ControlNotification; import io.split.android.client.service.sseclient.notifications.IncomingNotification; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeNotification; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeV2Notification; +import io.split.android.client.service.sseclient.notifications.MembershipNotification; 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.NotificationType; @@ -100,14 +99,14 @@ public void incomingSplitKill() { } @Test - public void incomingMySegmentsUpdate() { + public void incomingMembershipUpdate() { IncomingNotification incomingNotification = - new IncomingNotification(NotificationType.MY_SEGMENTS_UPDATE, "", "", 100); - MySegmentChangeNotification notification = new MySegmentChangeNotification(); + new IncomingNotification(NotificationType.MEMBERSHIPS_MS_UPDATE, "", "", 100); + MembershipNotification notification = new MembershipNotification(); when(mNotificationParser.parseIncoming(anyString())).thenReturn(incomingNotification); - when(mNotificationParser.parseMySegmentUpdate(anyString())).thenReturn(notification); + when(mNotificationParser.parseMembershipNotification(anyString())).thenReturn(notification); when(mManagerKeeper.isStreamingActive()).thenReturn(true); mSseHandler.handleIncomingMessage(buildMessage("{}")); @@ -116,14 +115,14 @@ public void incomingMySegmentsUpdate() { } @Test - public void incomingMySegmentsUpdateV2() { + public void incomingLargeMembershipUpdate() { IncomingNotification incomingNotification = - new IncomingNotification(NotificationType.MY_SEGMENTS_UPDATE_V2, "", "", 100); - MySegmentChangeV2Notification notification = new MySegmentChangeV2Notification(); + new IncomingNotification(NotificationType.MEMBERSHIPS_LS_UPDATE, "", "", 100); + MembershipNotification notification = new MembershipNotification(); when(mNotificationParser.parseIncoming(anyString())).thenReturn(incomingNotification); - when(mNotificationParser.parseMySegmentUpdateV2(anyString())).thenReturn(notification); + when(mNotificationParser.parseMembershipNotification(anyString())).thenReturn(notification); when(mManagerKeeper.isStreamingActive()).thenReturn(true); mSseHandler.handleIncomingMessage(buildMessage("{}")); @@ -135,11 +134,11 @@ public void incomingMySegmentsUpdateV2() { public void streamingPaused() { IncomingNotification incomingNotification = - new IncomingNotification(NotificationType.MY_SEGMENTS_UPDATE, "", "", 100); - MySegmentChangeNotification notification = new MySegmentChangeNotification(); + new IncomingNotification(NotificationType.MEMBERSHIPS_LS_UPDATE, "", "", 100); + MembershipNotification notification = new MembershipNotification(); when(mNotificationParser.parseIncoming(anyString())).thenReturn(incomingNotification); - when(mNotificationParser.parseMySegmentUpdate(anyString())).thenReturn(notification); + when(mNotificationParser.parseMembershipNotification(anyString())).thenReturn(notification); when(mManagerKeeper.isStreamingActive()).thenReturn(false); mSseHandler.handleIncomingMessage(buildMessage("{}")); 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 1fb9ff16b..df6499bd5 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 @@ -269,6 +269,20 @@ public void successfulSyncEventUpdatesLastSyncInGuardian() { verify(mSyncGuardian).updateLastSyncTimestamp(); } + @Test + public void startCallsLoadMySegmentsFromCache() { + mSyncManager.start(); + + verify(mSynchronizer).loadMySegmentsFromCache(); + } + + @Test + public void startCallsSynchronizeMySegments() { + mSyncManager.start(); + + verify(mSynchronizer).synchronizeMySegments(); + } + private void testStartUserConsentNotGranted(UserConsent userConsent) { when(mConfig.userConsent()).thenReturn(userConsent); mSyncManager.start(); diff --git a/src/test/java/io/split/android/client/service/sseclient/notifications/MySegmentsPayloadDecoderTest.java b/src/test/java/io/split/android/client/service/sseclient/notifications/MySegmentsPayloadDecoderTest.java deleted file mode 100644 index 73217298c..000000000 --- a/src/test/java/io/split/android/client/service/sseclient/notifications/MySegmentsPayloadDecoderTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.split.android.client.service.sseclient.notifications; - -import static org.junit.Assert.assertEquals; - -import org.junit.Before; -import org.junit.Test; - -public class MySegmentsPayloadDecoderTest { - - private MySegmentsPayloadDecoder mMySegmentsPayloadDecoder; - - @Before - public void setUp() { - mMySegmentsPayloadDecoder = new MySegmentsPayloadDecoder(); - } - - @Test - public void encodingOfUserKeyWorksAsExpected() { - String expectedResult = "MjAwNjI0Nzg3NQ=="; - - assertEquals(expectedResult, mMySegmentsPayloadDecoder.hashUserKeyForMySegmentsV1("user_key")); - } -} diff --git a/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImplTest.java b/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImplTest.java index 3d239a31b..f1df86439 100644 --- a/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImplTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/MySegmentsNotificationProcessorImplTest.java @@ -3,8 +3,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.isNull; +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; @@ -18,28 +21,28 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; +import java.util.Collections; +import java.util.Set; import java.util.concurrent.BlockingQueue; import io.split.android.client.common.CompressionUtilProvider; import io.split.android.client.exceptions.MySegmentsParsingException; import io.split.android.client.service.executor.SplitTaskExecutor; -import io.split.android.client.service.mysegments.MySegmentsOverwriteTask; +import io.split.android.client.service.mysegments.MySegmentUpdateParams; import io.split.android.client.service.mysegments.MySegmentsSyncTask; import io.split.android.client.service.mysegments.MySegmentsTaskFactory; import io.split.android.client.service.mysegments.MySegmentsUpdateTask; +import io.split.android.client.service.sseclient.notifications.HashingAlgorithm; import io.split.android.client.service.sseclient.notifications.KeyList; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeNotification; -import io.split.android.client.service.sseclient.notifications.MySegmentChangeV2Notification; +import io.split.android.client.service.sseclient.notifications.MembershipNotification; import io.split.android.client.service.sseclient.notifications.MySegmentUpdateStrategy; import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder; import io.split.android.client.service.sseclient.notifications.NotificationParser; import io.split.android.client.service.sseclient.notifications.NotificationType; +import io.split.android.client.service.sseclient.notifications.memberships.MembershipsNotificationProcessorImpl; import io.split.android.client.utils.CompressionUtil; public class MySegmentsNotificationProcessorImplTest { @@ -58,15 +61,17 @@ public class MySegmentsNotificationProcessorImplTest { private CompressionUtil mCompressionUtil; private final BigInteger mHashedUserKey = new BigInteger("11288179738259047283"); @Mock - private BlockingQueue mMySegmentChangeQueue; + private BlockingQueue mMySegmentChangeQueue; @Mock - private MySegmentChangeNotification mIncomingNotification; + private MembershipNotification mMsMembershipNotification; @Mock - private MySegmentChangeV2Notification mIncomingNotificationV2; + private MembershipNotification mLsMembershipNotification; @Mock private MySegmentsNotificationProcessorConfiguration mConfiguration; + @Mock + private SyncDelayCalculator mSyncCalculator; - private MySegmentsNotificationProcessorImpl mNotificationProcessor; + private MembershipsNotificationProcessorImpl mNotificationProcessor; @Before public void setUp() { @@ -74,85 +79,36 @@ public void setUp() { when(mCompressionUtilProvider.get(any())).thenReturn(mCompressionUtil); when(mMySegmentsPayloadDecoder.hashKey(anyString())).thenReturn(mHashedUserKey); - when(mIncomingNotification.getJsonData()).thenReturn("{}"); - when(mIncomingNotificationV2.getJsonData()).thenReturn("{}"); - when(mSplitTaskFactory.createMySegmentsUpdateTask(anyBoolean(), anyString())) - .thenReturn(Mockito.mock(MySegmentsUpdateTask.class)); - when(mSplitTaskFactory.createMySegmentsOverwriteTask(any())) - .thenReturn(Mockito.mock(MySegmentsOverwriteTask.class)); - when(mSplitTaskFactory.createMySegmentsSyncTask(anyBoolean())) - .thenReturn(Mockito.mock(MySegmentsSyncTask.class)); + when(mMsMembershipNotification.getJsonData()).thenReturn("{}"); + when(mLsMembershipNotification.getJsonData()).thenReturn("{}"); + when(mSplitTaskFactory.createMySegmentsUpdateTask(anyBoolean(), anySet(), anyLong())) + .thenReturn(mock(MySegmentsUpdateTask.class)); + when(mSplitTaskFactory.createMySegmentsSyncTask(anyBoolean(), anyLong(), anyLong())) + .thenReturn(mock(MySegmentsSyncTask.class)); + when(mConfiguration.getUserKey()).thenReturn("key"); when(mConfiguration.getHashedUserKey()).thenReturn(mHashedUserKey); when(mConfiguration.getMySegmentsTaskFactory()).thenReturn(mSplitTaskFactory); - when(mConfiguration.getMySegmentUpdateNotificationsQueue()).thenReturn(mMySegmentChangeQueue); - mNotificationProcessor = new MySegmentsNotificationProcessorImpl( + when(mConfiguration.getNotificationsQueue()).thenReturn(mMySegmentChangeQueue); + mNotificationProcessor = new MembershipsNotificationProcessorImpl( mNotificationParser, mSplitTaskExecutor, mMySegmentsPayloadDecoder, mCompressionUtilProvider, - mConfiguration + mConfiguration, + mSyncCalculator ); } - @Test - public void mySegmentsUpdateWithSegmentListNotification() { - List segments = new ArrayList<>(); - segments.add("s1"); - when(mIncomingNotification.isIncludesPayload()).thenReturn(true); - when(mIncomingNotification.getSegmentList()).thenReturn(segments); - when(mIncomingNotification.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE); - when(mNotificationParser.parseMySegmentUpdate(anyString())).thenReturn(mIncomingNotification); - - mNotificationProcessor.processMySegmentsUpdate(mIncomingNotification); - - verify(mSplitTaskFactory, times(1)).createMySegmentsOverwriteTask(any()); - verify(mSplitTaskExecutor, times(1)).submit(any(), isNull()); - } - - @Test - public void mySegmentsUpdateWithNullSegmentListNotification() { - List segments = null; - when(mIncomingNotification.isIncludesPayload()).thenReturn(true); - when(mIncomingNotification.getSegmentList()).thenReturn(segments); - when(mIncomingNotification.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE); - when(mNotificationParser.parseMySegmentUpdate(anyString())).thenReturn(mIncomingNotification); - - mNotificationProcessor.processMySegmentsUpdate(mIncomingNotification); - - verify(mSplitTaskFactory, times(1)).createMySegmentsOverwriteTask(any()); - verify(mSplitTaskExecutor, times(1)).submit(any(), isNull()); - } - - @Test - public void mySegmentsUpdateNoSegmentListNotification() { - - MySegmentChangeNotification mySegmentChangeNotification - = Mockito.mock(MySegmentChangeNotification.class); - when(mySegmentChangeNotification.isIncludesPayload()).thenReturn(false); - when(mIncomingNotification.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE); - when(mySegmentChangeNotification.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE); - when(mNotificationParser.parseMySegmentUpdate(anyString())).thenReturn(mySegmentChangeNotification); - - mNotificationProcessor.processMySegmentsUpdate(mIncomingNotification); - - verify(mSplitTaskFactory, never()).createMySegmentsOverwriteTask(any()); - ArgumentCaptor messageCaptor = - ArgumentCaptor.forClass(MySegmentChangeNotification.class); - verify(mMySegmentChangeQueue, times(1)).offer(messageCaptor.capture()); - Assert.assertEquals(NotificationType.MY_SEGMENTS_UPDATE, messageCaptor.getValue().getType()); - } - @Test public void mySegmentsUpdateV2UnboundedNotification() { - MySegmentChangeV2Notification mySegmentChangeNotification - = Mockito.mock(MySegmentChangeV2Notification.class); + MembershipNotification mySegmentChangeNotification + = mock(MembershipNotification.class); when(mySegmentChangeNotification.getUpdateStrategy()).thenReturn(MySegmentUpdateStrategy.UNBOUNDED_FETCH_REQUEST); - when(mIncomingNotificationV2.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); - when(mySegmentChangeNotification.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); - when(mNotificationParser.parseMySegmentUpdateV2(anyString())).thenReturn(mySegmentChangeNotification); + when(mySegmentChangeNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); + when(mNotificationParser.parseMembershipNotification(anyString())).thenReturn(mySegmentChangeNotification); - mNotificationProcessor.processMySegmentsUpdateV2(mIncomingNotificationV2); + mNotificationProcessor.process(mySegmentChangeNotification); verify(mMySegmentChangeQueue, times(1)).offer(any()); } @@ -161,19 +117,19 @@ public void mySegmentsUpdateV2UnboundedNotification() { public void mySegmentsUpdateV2RemovalNotification() { String segmentName = "ToRemove"; - when(mIncomingNotificationV2.getUpdateStrategy()).thenReturn(MySegmentUpdateStrategy.SEGMENT_REMOVAL); - when(mIncomingNotificationV2.getSegmentName()).thenReturn(segmentName); - when(mIncomingNotificationV2.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); - when(mIncomingNotificationV2.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); - when(mNotificationParser.parseMySegmentUpdateV2(anyString())).thenReturn(mIncomingNotificationV2); + when(mMsMembershipNotification.getUpdateStrategy()).thenReturn(MySegmentUpdateStrategy.SEGMENT_REMOVAL); + when(mMsMembershipNotification.getNames()).thenReturn(Collections.singleton(segmentName)); + when(mMsMembershipNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); + when(mMsMembershipNotification.getChangeNumber()).thenReturn(25L); + when(mNotificationParser.parseMembershipNotification(anyString())).thenReturn(mMsMembershipNotification); - mNotificationProcessor.processMySegmentsUpdateV2(mIncomingNotificationV2); + mNotificationProcessor.process(mMsMembershipNotification); - ArgumentCaptor messageCaptor = - ArgumentCaptor.forClass(String.class); - verify(mSplitTaskFactory, times(1)).createMySegmentsUpdateTask(anyBoolean(), messageCaptor.capture()); + ArgumentCaptor messageCaptor = + ArgumentCaptor.forClass(Set.class); + verify(mSplitTaskFactory, times(1)).createMySegmentsUpdateTask(anyBoolean(), messageCaptor.capture(), eq(25L)); - Assert.assertEquals(segmentName, messageCaptor.getValue()); + Assert.assertEquals(Collections.singleton(segmentName), messageCaptor.getValue()); } @Test @@ -185,26 +141,26 @@ public void mySegmentsUpdateV2BoundedNotificationFetch() { @Test public void mySegmentsUpdateV2BoundedNotificationNoFetch() { mySegmentsUpdateV2BoundedNotification(false); - verify(mSplitTaskFactory, never()).createMySegmentsSyncTask(anyBoolean()); + verify(mSplitTaskFactory, never()).createMySegmentsSyncTask(anyBoolean(), anyLong(), anyLong()); } public void mySegmentsUpdateV2BoundedNotification(boolean hasToFetch) { - MySegmentChangeV2Notification mySegmentChangeNotification - = Mockito.mock(MySegmentChangeV2Notification.class); + MembershipNotification mySegmentChangeNotification + = mock(MembershipNotification.class); when(mySegmentChangeNotification.getUpdateStrategy()).thenReturn(MySegmentUpdateStrategy.BOUNDED_FETCH_REQUEST); when(mySegmentChangeNotification.getData()).thenReturn("dummy"); - when(mIncomingNotificationV2.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); - when(mySegmentChangeNotification.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); + when(mMsMembershipNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); + when(mySegmentChangeNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); try { when(mMySegmentsPayloadDecoder.decodeAsBytes(anyString(), any())).thenReturn(new byte[]{}); } catch (MySegmentsParsingException e) { } when(mMySegmentsPayloadDecoder.computeKeyIndex(any(), anyInt())).thenReturn(1); when(mMySegmentsPayloadDecoder.isKeyInBitmap(any(), anyInt())).thenReturn(hasToFetch); - when(mNotificationParser.parseMySegmentUpdateV2(anyString())).thenReturn(mySegmentChangeNotification); + when(mNotificationParser.parseMembershipNotification(anyString())).thenReturn(mySegmentChangeNotification); - mNotificationProcessor.processMySegmentsUpdateV2(mIncomingNotificationV2); + mNotificationProcessor.process(mySegmentChangeNotification); } @@ -212,34 +168,34 @@ public void mySegmentsUpdateV2BoundedNotification(boolean hasToFetch) { public void mySegmentsUpdateV2KeyListNotificationAdd() { String segment = "TheSegment"; mySegmentsUpdateV2KeyListNotification(segment, ADD); - verify(mSplitTaskFactory, times(1)).createMySegmentsUpdateTask(true, segment); + verify(mSplitTaskFactory, times(1)).createMySegmentsUpdateTask(true, Collections.singleton(segment), 123456L); } @Test public void mySegmentsUpdateV2KeyListNotificationRemove() { String segment = "TheSegment"; mySegmentsUpdateV2KeyListNotification(segment, REMOVE); - verify(mSplitTaskFactory, times(1)).createMySegmentsUpdateTask(false, segment); + verify(mSplitTaskFactory, times(1)).createMySegmentsUpdateTask(false, Collections.singleton(segment), 123456L); } @Test public void mySegmentsUpdateV2KeyListNotificationNone() { mySegmentsUpdateV2KeyListNotification("", NONE); - verify(mSplitTaskFactory, never()).createMySegmentsUpdateTask(anyBoolean(), anyString()); + verify(mSplitTaskFactory, never()).createMySegmentsUpdateTask(anyBoolean(), anySet(), anyLong()); } @Test public void mySegmentsUpdateV2KeyListNotificationErrorFallback() throws MySegmentsParsingException { - MySegmentChangeV2Notification mySegmentChangeNotification - = Mockito.mock(MySegmentChangeV2Notification.class); + MembershipNotification mySegmentChangeNotification + = mock(MembershipNotification.class); when(mySegmentChangeNotification.getUpdateStrategy()).thenReturn(MySegmentUpdateStrategy.KEY_LIST); - when(mySegmentChangeNotification.getSegmentName()).thenReturn("s1"); - when(mIncomingNotificationV2.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); - when(mySegmentChangeNotification.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); + when(mySegmentChangeNotification.getNames()).thenReturn(Collections.singleton("s1")); + when(mMsMembershipNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); + when(mySegmentChangeNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); when(mMySegmentsPayloadDecoder.decodeAsString(anyString(), any())).thenThrow(MySegmentsParsingException.class); - mNotificationProcessor.processMySegmentsUpdateV2(mIncomingNotificationV2); + mNotificationProcessor.process(mMsMembershipNotification); verify(mMySegmentsPayloadDecoder, never()).getKeyListAction(any(), any()); verify(mMySegmentChangeQueue, times(1)).offer(any()); @@ -248,34 +204,49 @@ public void mySegmentsUpdateV2KeyListNotificationErrorFallback() throws MySegmen @Test public void mySegmentsUpdateV2BoundedNotificationErrorFallback() throws MySegmentsParsingException { - MySegmentChangeV2Notification mySegmentChangeNotification - = Mockito.mock(MySegmentChangeV2Notification.class); + MembershipNotification mySegmentChangeNotification + = mock(MembershipNotification.class); when(mySegmentChangeNotification.getUpdateStrategy()).thenReturn(MySegmentUpdateStrategy.BOUNDED_FETCH_REQUEST); - when(mySegmentChangeNotification.getSegmentName()).thenReturn("s1"); - when(mIncomingNotificationV2.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); - when(mySegmentChangeNotification.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); + when(mySegmentChangeNotification.getNames()).thenReturn(Collections.singleton("s1")); + when(mMsMembershipNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); + when(mySegmentChangeNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); when(mMySegmentsPayloadDecoder.decodeAsBytes(anyString(), any())).thenThrow(MySegmentsParsingException.class); - mNotificationProcessor.processMySegmentsUpdateV2(mIncomingNotificationV2); + mNotificationProcessor.process(mMsMembershipNotification); verify(mMySegmentsPayloadDecoder, never()).isKeyInBitmap(any(), anyInt()); verify(mMySegmentChangeQueue, times(1)).offer(any()); } + @Test + public void delayIsCalculatedWithCalculator() { + MembershipNotification notification = mock(MembershipNotification.class); + when(notification.getUpdateIntervalMs()).thenReturn(1000L); + when(notification.getAlgorithmSeed()).thenReturn(1234); + when(notification.getUpdateStrategy()).thenReturn(MySegmentUpdateStrategy.UNBOUNDED_FETCH_REQUEST); + when(notification.getHashingAlgorithm()).thenReturn(HashingAlgorithm.MURMUR3_32); + when(mSyncCalculator.calculateSyncDelay("key", 1000L, 1234, MySegmentUpdateStrategy.UNBOUNDED_FETCH_REQUEST, HashingAlgorithm.MURMUR3_32)).thenReturn(25L); + + mNotificationProcessor.process(notification); + + verify(mSyncCalculator).calculateSyncDelay("key", 1000L, 1234, MySegmentUpdateStrategy.UNBOUNDED_FETCH_REQUEST, HashingAlgorithm.MURMUR3_32); + } + private void mySegmentsUpdateV2KeyListNotification(String segmentName, KeyList.Action action) { - when(mIncomingNotificationV2.getUpdateStrategy()).thenReturn(MySegmentUpdateStrategy.KEY_LIST); - when(mIncomingNotificationV2.getSegmentName()).thenReturn(segmentName); - when(mIncomingNotificationV2.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); - when(mIncomingNotificationV2.getType()).thenReturn(NotificationType.MY_SEGMENTS_UPDATE_V2); + when(mMsMembershipNotification.getUpdateStrategy()).thenReturn(MySegmentUpdateStrategy.KEY_LIST); + when(mMsMembershipNotification.getNames()).thenReturn(Collections.singleton(segmentName)); + when(mMsMembershipNotification.getChangeNumber()).thenReturn(123456L); + when(mMsMembershipNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); + when(mMsMembershipNotification.getType()).thenReturn(NotificationType.MEMBERSHIPS_MS_UPDATE); try { when(mMySegmentsPayloadDecoder.decodeAsString(anyString(), any())).thenReturn(""); } catch (MySegmentsParsingException e) { e.printStackTrace(); } when(mMySegmentsPayloadDecoder.getKeyListAction(any(), any())).thenReturn(action); - when(mNotificationParser.parseMySegmentUpdateV2(anyString())).thenReturn(mIncomingNotificationV2); + when(mNotificationParser.parseMembershipNotification(anyString())).thenReturn(mMsMembershipNotification); - mNotificationProcessor.processMySegmentsUpdateV2(mIncomingNotificationV2); + mNotificationProcessor.process(mMsMembershipNotification); } } diff --git a/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorTest.java b/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorTest.java new file mode 100644 index 000000000..405caa988 --- /dev/null +++ b/src/test/java/io/split/android/client/service/sseclient/notifications/mysegments/SyncDelayCalculatorTest.java @@ -0,0 +1,81 @@ +package io.split.android.client.service.sseclient.notifications.mysegments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.Random; + +import io.split.android.client.service.sseclient.notifications.HashingAlgorithm; +import io.split.android.client.service.sseclient.notifications.MySegmentUpdateStrategy; + +public class SyncDelayCalculatorTest { + + @Test + public void delayIsNotLowerThan0() { + SyncDelayCalculator calculator = new SyncDelayCalculatorImpl(); + long delay = calculator.calculateSyncDelay(getRandomString(), 600L, 1525, MySegmentUpdateStrategy.UNBOUNDED_FETCH_REQUEST, HashingAlgorithm.MURMUR3_32); + assertTrue(delay >= 0); + } + + @Test + public void delayIsNotHigherThanUpdateInterval() { + SyncDelayCalculator calculator = new SyncDelayCalculatorImpl(); + long delay = calculator.calculateSyncDelay(getRandomString(), 600L, 24515, MySegmentUpdateStrategy.BOUNDED_FETCH_REQUEST, HashingAlgorithm.MURMUR3_32); + assertTrue(delay <= 600L); + } + + @Test + public void delayIsZeroWhenUpdateStrategyIsKeyList() { + SyncDelayCalculator calculator = new SyncDelayCalculatorImpl(); + long delay = calculator.calculateSyncDelay(getRandomString(), 600L, 24515, MySegmentUpdateStrategy.KEY_LIST, HashingAlgorithm.MURMUR3_32); + assertEquals(0, delay); + } + + @Test + public void delayIsZeroWhenUpdateStrategyIsSegmentRemoval() { + SyncDelayCalculator calculator = new SyncDelayCalculatorImpl(); + long delay = calculator.calculateSyncDelay(getRandomString(), 600L, 24515, MySegmentUpdateStrategy.SEGMENT_REMOVAL, HashingAlgorithm.MURMUR3_32); + assertEquals(0, delay); + } + + @Test + public void delayIsZeroWhenHashingAlgorithmIsNone() { + SyncDelayCalculator calculator = new SyncDelayCalculatorImpl(); + long delay = calculator.calculateSyncDelay(getRandomString(), 600L, 24515, MySegmentUpdateStrategy.BOUNDED_FETCH_REQUEST, HashingAlgorithm.NONE); + assertEquals(0, delay); + } + + @Test + public void delayIsLessThanSixtyMsWhenUpdateIntervalIsNull() { + SyncDelayCalculator calculator = new SyncDelayCalculatorImpl(); + long delay = calculator.calculateSyncDelay(getRandomString(), null, 24515, MySegmentUpdateStrategy.BOUNDED_FETCH_REQUEST, HashingAlgorithm.MURMUR3_32); + assertTrue(delay <= 60000); + } + + @Test + public void delayIsLessThanSixtyMsWhenUpdateIntervalIsZero() { + SyncDelayCalculator calculator = new SyncDelayCalculatorImpl(); + long delay = calculator.calculateSyncDelay(getRandomString(), 0L, 24515, MySegmentUpdateStrategy.BOUNDED_FETCH_REQUEST, HashingAlgorithm.MURMUR3_32); + assertTrue(delay <= 60000); + } + + @Test + public void delayIsLessThanSixtyMsWhenUpdateIntervalIsNegative() { + SyncDelayCalculator calculator = new SyncDelayCalculatorImpl(); + long delay = calculator.calculateSyncDelay(getRandomString(), -1L, 24515, MySegmentUpdateStrategy.BOUNDED_FETCH_REQUEST, HashingAlgorithm.MURMUR3_32); + assertTrue(delay <= 60000); + } + + @Test + public void delayIsCalculatedWithNullSeed() { + SyncDelayCalculator calculator = new SyncDelayCalculatorImpl(); + long delay = calculator.calculateSyncDelay(getRandomString(), 1L, null, MySegmentUpdateStrategy.BOUNDED_FETCH_REQUEST, HashingAlgorithm.MURMUR3_32); + assertTrue(delay >= 0 && delay <= 1); + } + + private String getRandomString() { + return String.valueOf(new Random().nextInt()); + } +} diff --git a/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java b/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java index d0d07fbaf..36f847b39 100644 --- a/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/sseclient/RetryBackoffCounterTimerTest.java @@ -7,6 +7,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static java.lang.Boolean.TRUE; + import androidx.annotation.NonNull; import org.junit.After; @@ -167,6 +169,22 @@ public void nonRetryableErrorTaskNotifiesListenerWithErrorStatus() { counterTimer.start(); verify(mockListener).taskExecuted(argThat(taskInfo -> taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR && - taskInfo.getTaskType() == SplitTaskType.SPLITS_SYNC)); + taskInfo.getTaskType() == SplitTaskType.SPLITS_SYNC && TRUE.equals(taskInfo.getBoolValue("DO_NOT_RETRY")))); + } + + @Test + public void testWithListenerAndInitialDelay() throws InterruptedException { + SplitTaskExecutionListener mockListener = mock(SplitTaskExecutionListener.class); + counterTimer = new RetryBackoffCounterTimer(taskExecutor, backoffCounter, 2); + when(taskExecutor.schedule(mockTask, 5, counterTimer)).then(invocation -> { + counterTimer.taskExecuted(SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC, Collections.singletonMap("DO_NOT_RETRY", true))); + return "100"; + }); + + counterTimer.setTask(mockTask, 5000L, mockListener); + + counterTimer.start(); + + verify(taskExecutor).schedule(mockTask, 5L, counterTimer); } } diff --git a/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImplTest.java b/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImplTest.java index f2d116e96..2c87b101a 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImplTest.java +++ b/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerImplTest.java @@ -23,15 +23,16 @@ import java.util.concurrent.TimeUnit; import io.split.android.client.events.SplitEventsManager; +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.SplitTaskExecutionListener; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.mysegments.LoadMySegmentsTask; +import io.split.android.client.service.mysegments.MySegmentUpdateParams; import io.split.android.client.service.mysegments.MySegmentsSyncTask; import io.split.android.client.service.mysegments.MySegmentsTaskFactory; -import io.split.android.client.service.splits.SplitsSyncTask; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; public class MySegmentsSynchronizerImplTest { @@ -54,7 +55,7 @@ public void setUp() { mSplitTaskExecutor, mSplitEventsManager, mMySegmentsTaskFactory, - SEGMENTS_REFRESH_RATE); + SEGMENTS_REFRESH_RATE, SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE); } @Test @@ -70,7 +71,7 @@ public void loadMySegmentsFromCacheSubmitsTasksToTaskExecutor() { @Test public void synchronizeMySegmentsStartsSegmentsSyncTask() { MySegmentsSyncTask mockTask = mock(MySegmentsSyncTask.class); - when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false)).thenReturn(mockTask); + when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false, null, null)).thenReturn(mockTask); mMySegmentsSynchronizer.synchronizeMySegments(); @@ -81,18 +82,35 @@ public void synchronizeMySegmentsStartsSegmentsSyncTask() { @Test public void forceMySegmentsSyncLaunchesSegmentsSyncTaskAvoidingCache() { MySegmentsSyncTask mockTask = mock(MySegmentsSyncTask.class); - when(mMySegmentsTaskFactory.createMySegmentsSyncTask(true)).thenReturn(mockTask); + when(mMySegmentsTaskFactory.createMySegmentsSyncTask(true, null, 10L)).thenReturn(mockTask); - mMySegmentsSynchronizer.forceMySegmentsSync(); + mMySegmentsSynchronizer.forceMySegmentsSync(new MySegmentUpdateParams(1L, null, 10L)); - verify(mRetryBackoffCounterTimer).setTask(eq(mockTask), any()); + verify(mRetryBackoffCounterTimer).setTask(eq(mockTask), eq(1L), any()); verify(mRetryBackoffCounterTimer).start(); } + @Test + public void forceMySegmentsSyncDoesNotStopPreviouslyRunningTask() { + MySegmentsSyncTask mockTask = mock(MySegmentsSyncTask.class); + MySegmentsSyncTask mockTask2 = mock(MySegmentsSyncTask.class); + when(mMySegmentsTaskFactory.createMySegmentsSyncTask(true, null, null)) + .thenReturn(mockTask) + .thenReturn(mockTask2); + + mMySegmentsSynchronizer.forceMySegmentsSync(new MySegmentUpdateParams(1000L, null, null)); + mMySegmentsSynchronizer.forceMySegmentsSync(new MySegmentUpdateParams(0L, null, null)); + + verify(mRetryBackoffCounterTimer).setTask(eq(mockTask), eq(1000L), any()); + verify(mRetryBackoffCounterTimer, times(0)).setTask(eq(mockTask2), eq(0L), any()); + verify(mRetryBackoffCounterTimer, times(1)).stop(); + verify(mRetryBackoffCounterTimer, times(1)).start(); + } + @Test public void scheduleSegmentsSyncTaskSchedulesSyncTaskInTaskExecutor() { MySegmentsSyncTask mockTask = mock(MySegmentsSyncTask.class); - when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false)).thenReturn(mockTask); + when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false, null, null)).thenReturn(mockTask); when(mSplitTaskExecutor.schedule(eq(mockTask), eq(1L), eq(1L), notNull())).thenReturn("TaskID"); mMySegmentsSynchronizer.scheduleSegmentsSyncTask(); @@ -103,7 +121,7 @@ public void scheduleSegmentsSyncTaskSchedulesSyncTaskInTaskExecutor() { @Test public void stopPeriodicFetchingCallsStopTaskOnExecutor() { MySegmentsSyncTask mockTask = mock(MySegmentsSyncTask.class); - when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false)).thenReturn(mockTask); + when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false, null, null)).thenReturn(mockTask); when(mSplitTaskExecutor.schedule(eq(mockTask), eq(1L), eq(1L), notNull())).thenReturn("TaskID"); mMySegmentsSynchronizer.scheduleSegmentsSyncTask(); @@ -118,11 +136,11 @@ public void stopPeriodicFetchingCallsStopTaskOnExecutor() { public void startPeriodicFetchingCancelsPreviousTaskIfExecutedSequentially() { MySegmentsSyncTask mockTask = mock(MySegmentsSyncTask.class); MySegmentsSyncTask mockTask2 = mock(MySegmentsSyncTask.class); - when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false)) + when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false, null, null)) .thenReturn(mockTask) .thenReturn(mockTask2); - when(mSplitTaskExecutor.schedule(eq(mockTask), eq(1L), eq(1L), notNull())).thenReturn("TaskID"); - when(mSplitTaskExecutor.schedule(eq(mockTask2), eq(1L), eq(1L), notNull())).thenReturn("TaskID2"); + when(mSplitTaskExecutor.schedule(eq(mockTask), anyLong(), anyLong(), notNull())).thenReturn("TaskID"); + when(mSplitTaskExecutor.schedule(eq(mockTask2), anyLong(), anyLong(), notNull())).thenReturn("TaskID2"); mMySegmentsSynchronizer.scheduleSegmentsSyncTask(); mMySegmentsSynchronizer.scheduleSegmentsSyncTask(); @@ -130,13 +148,12 @@ public void startPeriodicFetchingCancelsPreviousTaskIfExecutedSequentially() { verify(mSplitTaskExecutor).schedule(eq(mockTask), eq(1L), eq(1L), notNull()); verify(mSplitTaskExecutor).stopTask("TaskID"); verify(mSplitTaskExecutor).schedule(eq(mockTask2), eq(1L), eq(1L), notNull()); - verify(mSplitTaskExecutor, times(0)).stopTask("TaskID2"); } @Test public void syncTaskIsStoppedWhenTaskResultIsDoNotRetry() throws InterruptedException { MySegmentsSyncTask mockTask = mock(MySegmentsSyncTask.class); - when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false)).thenReturn(mockTask); + when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false, null, null)).thenReturn(mockTask); when(mockTask.execute()).thenReturn(SplitTaskExecutionInfo.error( SplitTaskType.MY_SEGMENTS_SYNC, Collections.singletonMap(SplitTaskExecutionInfo.DO_NOT_RETRY, true))); @@ -170,7 +187,7 @@ public String answer(InvocationOnMock invocation) { @Test public void syncTaskIsNotRestartedWhenTaskResultIsDoNotRetry() throws InterruptedException { MySegmentsSyncTask mockTask = mock(MySegmentsSyncTask.class); - when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false)).thenReturn(mockTask); + when(mMySegmentsTaskFactory.createMySegmentsSyncTask(false, null, null)).thenReturn(mockTask); when(mockTask.execute()).thenReturn(SplitTaskExecutionInfo.error( SplitTaskType.MY_SEGMENTS_SYNC, Collections.singletonMap(SplitTaskExecutionInfo.DO_NOT_RETRY, true))); diff --git a/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java b/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java index cc82f0810..9052de36a 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java +++ b/src/test/java/io/split/android/client/service/synchronizer/mysegments/MySegmentsSynchronizerRegistryImplTest.java @@ -6,6 +6,8 @@ import org.junit.Before; import org.junit.Test; +import io.split.android.client.service.mysegments.MySegmentUpdateParams; + public class MySegmentsSynchronizerRegistryImplTest { private MySegmentsSynchronizerRegistryImpl mRegistry; @@ -38,11 +40,12 @@ public void synchronizeMySegmentsGetCalledInEveryRegisteredSync() { @Test public void forceMySegmentsSyncGetCalledInEveryRegisteredSync() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); + MySegmentUpdateParams params = new MySegmentUpdateParams(4L, 1L, 2L); mRegistry.registerMySegmentsSynchronizer("key", syncMock); - mRegistry.forceMySegmentsSync(); + mRegistry.forceMySegmentsSync(params); - verify(syncMock).forceMySegmentsSync(); + verify(syncMock).forceMySegmentsSync(params); } @Test @@ -100,35 +103,44 @@ public void unregisterStopsTasksBeforeRemovingSync() { public void callLoadSegmentsFromCacheForNewlyRegisteredSyncIfNecessary() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); MySegmentsSynchronizer syncMock2 = mock(MySegmentsSynchronizer.class); + MySegmentsSynchronizer syncMock3 = mock(MySegmentsSynchronizer.class); mRegistry.registerMySegmentsSynchronizer("key", syncMock); mRegistry.loadMySegmentsFromCache(); mRegistry.registerMySegmentsSynchronizer("new_key", syncMock2); + mRegistry.registerMySegmentsSynchronizer("new_key", syncMock3); verify(syncMock2).loadMySegmentsFromCache(); + verify(syncMock3).loadMySegmentsFromCache(); } @Test public void callSynchronizeMySegmentsForNewlyRegisteredSyncIfNecessary() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); MySegmentsSynchronizer syncMock2 = mock(MySegmentsSynchronizer.class); + MySegmentsSynchronizer syncMock3 = mock(MySegmentsSynchronizer.class); mRegistry.registerMySegmentsSynchronizer("key", syncMock); mRegistry.synchronizeMySegments(); mRegistry.registerMySegmentsSynchronizer("new_key", syncMock2); + mRegistry.registerMySegmentsSynchronizer("new_key", syncMock3); verify(syncMock2).synchronizeMySegments(); + verify(syncMock3).synchronizeMySegments(); } @Test public void callScheduleSegmentsSyncTaskForNewlyRegisteredSyncIfNecessary() { MySegmentsSynchronizer syncMock = mock(MySegmentsSynchronizer.class); MySegmentsSynchronizer syncMock2 = mock(MySegmentsSynchronizer.class); + MySegmentsSynchronizer syncMock3 = mock(MySegmentsSynchronizer.class); mRegistry.registerMySegmentsSynchronizer("key", syncMock); mRegistry.scheduleSegmentsSyncTask(); mRegistry.registerMySegmentsSynchronizer("new_key", syncMock2); + mRegistry.registerMySegmentsSynchronizer("new_key", syncMock3); verify(syncMock2).scheduleSegmentsSyncTask(); + verify(syncMock3).scheduleSegmentsSyncTask(); } } diff --git a/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java b/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java index a3f6c3fd9..fac2fbd93 100644 --- a/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java +++ b/src/test/java/io/split/android/client/shared/ClientComponentsRegisterImplTest.java @@ -5,18 +5,23 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import androidx.annotation.NonNull; + import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.math.BigInteger; + import io.split.android.client.SplitClientConfig; import io.split.android.client.api.Key; import io.split.android.client.events.EventsManagerRegistry; import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; import io.split.android.client.service.mysegments.MySegmentsTaskFactory; import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder; -import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessorFactory; +import io.split.android.client.service.sseclient.notifications.mysegments.MembershipsNotificationProcessorFactory; import io.split.android.client.service.sseclient.notifications.mysegments.MySegmentsNotificationProcessorRegistry; import io.split.android.client.service.sseclient.reactor.MySegmentsUpdateWorkerRegistry; import io.split.android.client.service.sseclient.sseclient.SseAuthenticator; @@ -48,7 +53,7 @@ public class ClientComponentsRegisterImplTest { @Mock private MySegmentsNotificationProcessorRegistry mMySegmentsNotificationProcessorRegistry; @Mock - private MySegmentsNotificationProcessorFactory mMySegmentsNotificationProcessorFactory; + private MembershipsNotificationProcessorFactory mMembershipsNotificationProcessorFactory; @Mock private MySegmentsV2PayloadDecoder mMySegmentsV2PayloadDecoder; @@ -67,56 +72,45 @@ public class ClientComponentsRegisterImplTest { public void setUp() { MockitoAnnotations.openMocks(this); - when(mMySegmentsSynchronizerFactory.getSynchronizer(mMySegmentsTaskFactory, mSplitEventsManager)) + when(mMySegmentsV2PayloadDecoder.hashKey("matching_key")).thenReturn(BigInteger.valueOf(123)); + + when(mMySegmentsSynchronizerFactory.getSynchronizer(mMySegmentsTaskFactory, mSplitEventsManager, SplitInternalEvent.MY_SEGMENTS_LOADED_FROM_STORAGE, 1800)) .thenReturn(mMySegmentsSynchronizer); - register = new ClientComponentsRegisterImpl( - new SplitClientConfig.Builder().build(), - mMySegmentsSynchronizerFactory, - mStorageContainer, - mAttributesSynchronizerFactory, - mAttributesSynchronizerRegistry, - mMySegmentsSynchronizerRegistry, - mMySegmentsUpdateWorkerRegistry, - mEventsManagerRegistry, - mSseAuthenticator, - mMySegmentsNotificationProcessorRegistry, - mMySegmentsNotificationProcessorFactory, - mMySegmentsV2PayloadDecoder - ); + register = getRegister(); } @Test public void attributesSynchronizerIsRegistered() { - register.registerComponents(mMatchingKey, mMySegmentsTaskFactory, mSplitEventsManager); + register.registerComponents(mMatchingKey, mSplitEventsManager, mMySegmentsTaskFactory); verify(mAttributesSynchronizerRegistry).registerAttributesSynchronizer(eq("matching_key"), any()); } @Test public void mySegmentsSynchronizerIsRegistered() { - register.registerComponents(mMatchingKey, mMySegmentsTaskFactory, mSplitEventsManager); + register.registerComponents(mMatchingKey, mSplitEventsManager, mMySegmentsTaskFactory); verify(mMySegmentsSynchronizerRegistry).registerMySegmentsSynchronizer("matching_key", mMySegmentsSynchronizer); } @Test public void mySegmentsUpdateWorkerIsRegistered() { - register.registerComponents(mMatchingKey, mMySegmentsTaskFactory, mSplitEventsManager); + register.registerComponents(mMatchingKey, mSplitEventsManager, mMySegmentsTaskFactory); verify(mMySegmentsUpdateWorkerRegistry).registerMySegmentsUpdateWorker(eq("matching_key"), any()); } @Test public void mySegmentsNotificationProcessorIsRegistered() { - register.registerComponents(mMatchingKey, mMySegmentsTaskFactory, mSplitEventsManager); + register.registerComponents(mMatchingKey, mSplitEventsManager, mMySegmentsTaskFactory); - verify(mMySegmentsNotificationProcessorRegistry).registerMySegmentsProcessor(eq("matching_key"), any()); + verify(mMySegmentsNotificationProcessorRegistry).registerMembershipsNotificationProcessor(eq("matching_key"), any()); } @Test public void eventsManagerIsRegistered() { - register.registerComponents(mMatchingKey, mMySegmentsTaskFactory, mSplitEventsManager); + register.registerComponents(mMatchingKey, mSplitEventsManager, mMySegmentsTaskFactory); verify(mEventsManagerRegistry).registerEventsManager(mMatchingKey, mSplitEventsManager); } @@ -128,7 +122,25 @@ public void componentsAreCorrectlyUnregistered() { verify(mAttributesSynchronizerRegistry).unregisterAttributesSynchronizer("matching_key"); verify(mMySegmentsSynchronizerRegistry).unregisterMySegmentsSynchronizer("matching_key"); verify(mMySegmentsUpdateWorkerRegistry).unregisterMySegmentsUpdateWorker("matching_key"); - verify(mMySegmentsNotificationProcessorRegistry).unregisterMySegmentsProcessor("matching_key"); + verify(mMySegmentsNotificationProcessorRegistry).unregisterMembershipsProcessor("matching_key"); verify(mEventsManagerRegistry).unregisterEventsManager(mMatchingKey); } + + @NonNull + private ClientComponentsRegisterImpl getRegister() { + return new ClientComponentsRegisterImpl( + new SplitClientConfig.Builder().build(), + mMySegmentsSynchronizerFactory, + mStorageContainer, + mAttributesSynchronizerFactory, + mAttributesSynchronizerRegistry, + mMySegmentsSynchronizerRegistry, + mMySegmentsUpdateWorkerRegistry, + mEventsManagerRegistry, + mSseAuthenticator, + mMySegmentsNotificationProcessorRegistry, + mMembershipsNotificationProcessorFactory, + mMySegmentsV2PayloadDecoder + ); + } } diff --git a/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java b/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java index b91c660bd..e33e50b1f 100644 --- a/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java +++ b/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java @@ -77,6 +77,7 @@ public void setUp() { MockitoAnnotations.openMocks(this); when(mSplitApiFacade.getMySegmentsFetcher(any())).thenReturn(mock(HttpFetcher.class)); when(mStorageContainer.getMySegmentsStorage(any())).thenReturn(mock(MySegmentsStorage.class)); + when(mStorageContainer.getMyLargeSegmentsStorage(any())).thenReturn(mock(MySegmentsStorage.class)); mClientContainer = getSplitClientContainer(mDefaultMatchingKey, true); } diff --git a/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt b/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt index 33a654687..6d5f2b77c 100644 --- a/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt +++ b/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt @@ -3,18 +3,33 @@ package io.split.android.client.storage.cipher import io.split.android.client.service.executor.SplitTaskExecutionStatus import io.split.android.client.service.executor.SplitTaskType import io.split.android.client.storage.db.EventDao +import io.split.android.client.storage.db.EventEntity import io.split.android.client.storage.db.ImpressionDao +import io.split.android.client.storage.db.ImpressionEntity import io.split.android.client.storage.db.ImpressionsCountDao +import io.split.android.client.storage.db.ImpressionsCountEntity +import io.split.android.client.storage.db.MyLargeSegmentDao +import io.split.android.client.storage.db.MyLargeSegmentEntity import io.split.android.client.storage.db.MySegmentDao +import io.split.android.client.storage.db.MySegmentEntity +import io.split.android.client.storage.db.SegmentDao +import io.split.android.client.storage.db.SegmentEntity import io.split.android.client.storage.db.SplitDao +import io.split.android.client.storage.db.SplitEntity import io.split.android.client.storage.db.SplitRoomDatabase import io.split.android.client.storage.db.attributes.AttributesDao +import io.split.android.client.storage.db.attributes.AttributesEntity +import io.split.android.client.storage.db.impressions.unique.UniqueKeyEntity import io.split.android.client.storage.db.impressions.unique.UniqueKeysDao import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import org.mockito.ArgumentMatcher +import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyString +import org.mockito.ArgumentMatchers.argThat import org.mockito.Mock +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @@ -23,48 +38,296 @@ class ApplyCipherTaskTest { @Mock private lateinit var splitDatabase: SplitRoomDatabase + @Mock private lateinit var fromCipher: SplitCipher + @Mock private lateinit var toCipher: SplitCipher @Mock private lateinit var splitDao: SplitDao + @Mock private lateinit var mySegmentDao: MySegmentDao + + @Mock + private lateinit var myLargeSegmentDao: MyLargeSegmentDao + @Mock private lateinit var impressionDao: ImpressionDao + @Mock private lateinit var eventDao: EventDao + @Mock private lateinit var impressionsCountDao: ImpressionsCountDao + @Mock private lateinit var uniqueKeysDao: UniqueKeysDao + @Mock private lateinit var attributesDao: AttributesDao + private lateinit var applyCipherTask: ApplyCipherTask + @Before fun setup() { MockitoAnnotations.openMocks(this) - } - - @Test - fun testExecute() { `when`(splitDatabase.splitDao()).thenReturn(splitDao) `when`(splitDatabase.mySegmentDao()).thenReturn(mySegmentDao) + `when`(splitDatabase.myLargeSegmentDao()).thenReturn(myLargeSegmentDao) `when`(splitDatabase.impressionDao()).thenReturn(impressionDao) `when`(splitDatabase.eventDao()).thenReturn(eventDao) `when`(splitDatabase.impressionsCountDao()).thenReturn(impressionsCountDao) `when`(splitDatabase.uniqueKeysDao()).thenReturn(uniqueKeysDao) `when`(splitDatabase.attributesDao()).thenReturn(attributesDao) - `when`(fromCipher.decrypt(anyString())).thenAnswer { invocation -> invocation.arguments[0] } - `when`(toCipher.encrypt(anyString())).thenAnswer { invocation -> invocation.arguments[0] } + `when`(fromCipher.decrypt(anyString())).thenAnswer { invocation -> "decrypted_${invocation.arguments[0]}" } + `when`(toCipher.encrypt(anyString())).thenAnswer { invocation -> "encrypted_${invocation.arguments[0]}" } + `when`(splitDatabase.runInTransaction(any())).thenAnswer { invocation -> (invocation.arguments[0] as Runnable).run() } + + applyCipherTask = ApplyCipherTask(splitDatabase, fromCipher, toCipher) + } - val applyCipherTask = ApplyCipherTask(splitDatabase, fromCipher, toCipher) + @Test + fun testExecute() { val result = applyCipherTask.execute() assertEquals(SplitTaskType.GENERIC_TASK, result.taskType) assertEquals(SplitTaskExecutionStatus.SUCCESS, result.status) } + + @Test + fun `flags are migrated`() { + `when`(splitDao.all).thenReturn( + listOf( + SplitEntity().apply { name = "name1"; body = "body1" }, + SplitEntity().apply { name = "name2"; body = "body2" }, + ) + ) + + applyCipherTask.execute() + + verify(splitDao).all + verify(fromCipher).decrypt("name1") + verify(fromCipher).decrypt("body1") + verify(toCipher).encrypt("decrypted_name1") + verify(toCipher).encrypt("decrypted_body1") + verify(splitDao).update("name1", "encrypted_decrypted_name1", "encrypted_decrypted_body1") + + verify(fromCipher).decrypt("name2") + verify(fromCipher).decrypt("body2") + verify(toCipher).encrypt("decrypted_name2") + verify(toCipher).encrypt("decrypted_body2") + verify(splitDao).update("name2", "encrypted_decrypted_name2", "encrypted_decrypted_body2") + } + + @Test + fun `segments are migrated`() { + `when`(mySegmentDao.all).thenReturn( + listOf( + MySegmentEntity.creator().createEntity("userKey1", "segment1,segment2", 999999), + MySegmentEntity.creator().createEntity("userKey2", "segment3,segment4", 999999), + ) + ) + + applyCipherTask.execute() + + verify(mySegmentDao).all + verify(fromCipher).decrypt("userKey1") + verify(fromCipher).decrypt("segment1,segment2") + verify(toCipher).encrypt("decrypted_userKey1") + verify(toCipher).encrypt("decrypted_segment1,segment2") + verify(mySegmentDao).update( + "userKey1", + "encrypted_decrypted_userKey1", + "encrypted_decrypted_segment1,segment2" + ) + + verify(fromCipher).decrypt("userKey2") + verify(fromCipher).decrypt("segment3,segment4") + verify(toCipher).encrypt("decrypted_userKey2") + verify(toCipher).encrypt("decrypted_segment3,segment4") + verify(mySegmentDao).update( + "userKey2", + "encrypted_decrypted_userKey2", + "encrypted_decrypted_segment3,segment4" + ) + } + + @Test + fun `large segments are migrated`() { + `when`(myLargeSegmentDao.all).thenReturn( + listOf( + MyLargeSegmentEntity.creator() + .createEntity("userKey1", "segment1,segment2", 999999), + MyLargeSegmentEntity.creator() + .createEntity("userKey2", "segment3,segment4", 999999), + ) + ) + + applyCipherTask.execute() + + verify(myLargeSegmentDao).all + verify(fromCipher).decrypt("userKey1") + verify(fromCipher).decrypt("segment1,segment2") + verify(toCipher).encrypt("decrypted_userKey1") + verify(toCipher).encrypt("decrypted_segment1,segment2") + verify(myLargeSegmentDao).update( + "userKey1", + "encrypted_decrypted_userKey1", + "encrypted_decrypted_segment1,segment2" + ) + + verify(fromCipher).decrypt("userKey2") + verify(fromCipher).decrypt("segment3,segment4") + verify(toCipher).encrypt("decrypted_userKey2") + verify(toCipher).encrypt("decrypted_segment3,segment4") + verify(myLargeSegmentDao).update( + "userKey2", + "encrypted_decrypted_userKey2", + "encrypted_decrypted_segment3,segment4" + ) + } + + @Test + fun `impressions are migrated`() { + `when`(impressionDao.all).thenReturn( + listOf( + ImpressionEntity().apply { id = 1; testName = "test1"; body = "body1" }, + ImpressionEntity().apply { id = 2; testName = "test2"; body = "body2" }, + ) + ) + + applyCipherTask.execute() + + verify(impressionDao).all + verify(fromCipher, times(0)).decrypt("1") + verify(fromCipher).decrypt("test1") + verify(fromCipher).decrypt("body1") + verify(toCipher).encrypt("decrypted_test1") + verify(toCipher).encrypt("decrypted_body1") + verify(impressionDao).insert(argThat(ArgumentMatcher { entity -> + entity.id == 1L && entity.testName == "encrypted_decrypted_test1" && entity.body == "encrypted_decrypted_body1" + })) + + verify(fromCipher, times(0)).decrypt("2") + verify(fromCipher).decrypt("test2") + verify(fromCipher).decrypt("body2") + verify(toCipher).encrypt("decrypted_test2") + verify(toCipher).encrypt("decrypted_body2") + verify(impressionDao).insert(argThat(ArgumentMatcher { entity -> + entity.id == 2L && entity.testName == "encrypted_decrypted_test2" && entity.body == "encrypted_decrypted_body2" + })) + } + + @Test + fun `events are migrated`() { + `when`(eventDao.all).thenReturn( + listOf( + EventEntity().apply { id = 1; body = "body1"; createdAt = 999991 }, + EventEntity().apply { id = 2; body = "body2"; createdAt = 999992 }, + ) + ) + + applyCipherTask.execute() + + verify(eventDao).all + verify(fromCipher, times(0)).decrypt("1") + verify(fromCipher).decrypt("body1") + verify(toCipher).encrypt("decrypted_body1") + verify(eventDao).insert(argThat(ArgumentMatcher { entity -> + entity.id == 1.toLong() && entity.body == "encrypted_decrypted_body1" && entity.createdAt == 999991L + })) + + verify(fromCipher, times(0)).decrypt("2") + verify(fromCipher).decrypt("body2") + verify(toCipher).encrypt("decrypted_body2") + verify(eventDao).insert(argThat(ArgumentMatcher { entity -> + entity.id == 2.toLong() && entity.body == "encrypted_decrypted_body2" && entity.createdAt == 999992L + })) + } + + @Test + fun `impressions count are migrated`() { + `when`(impressionsCountDao.all).thenReturn( + listOf( + ImpressionsCountEntity().apply { id = 1; body = "body1"; createdAt = 999991 }, + ImpressionsCountEntity().apply { id = 2; body = "body2"; createdAt = 999992 }, + ) + ) + + applyCipherTask.execute() + + verify(impressionsCountDao).all + verify(fromCipher, times(0)).decrypt("1") + verify(fromCipher).decrypt("body1") + verify(toCipher).encrypt("decrypted_body1") + verify(impressionsCountDao).insert(argThat(ArgumentMatcher { entity -> + entity.id == 1.toLong() && entity.body == "encrypted_decrypted_body1" && entity.createdAt == 999991L + })) + + verify(fromCipher, times(0)).decrypt("2") + verify(fromCipher).decrypt("body2") + verify(toCipher).encrypt("decrypted_body2") + verify(impressionsCountDao).insert(argThat(ArgumentMatcher { entity -> + entity.id == 2.toLong() && entity.body == "encrypted_decrypted_body2" && entity.createdAt == 999992L + })) + } + + @Test + fun `unique keys are migrated`() { + `when`(uniqueKeysDao.all).thenReturn( + listOf( + UniqueKeyEntity().apply { id = 1; userKey = "key1"; featureList = "feature1,feature2"; createdAt = 999991 }, + UniqueKeyEntity().apply { id = 2; userKey = "key2"; featureList = "feature3,feature4"; createdAt = 999992 }, + ) + ) + + applyCipherTask.execute() + + verify(uniqueKeysDao).all + verify(fromCipher, times(0)).decrypt("1") + verify(fromCipher).decrypt("key1") + verify(fromCipher).decrypt("feature1,feature2") + verify(toCipher).encrypt("decrypted_key1") + verify(toCipher).encrypt("decrypted_feature1,feature2") + verify(uniqueKeysDao).insert(argThat(ArgumentMatcher { entity -> + entity.id == 1.toLong() && entity.userKey == "encrypted_decrypted_key1" && entity.featureList == "encrypted_decrypted_feature1,feature2" && entity.createdAt == 999991L + })) + + verify(fromCipher, times(0)).decrypt("2") + verify(fromCipher).decrypt("key2") + verify(fromCipher).decrypt("feature3,feature4") + verify(toCipher).encrypt("decrypted_key2") + verify(toCipher).encrypt("decrypted_feature3,feature4") + verify(uniqueKeysDao).insert(argThat(ArgumentMatcher { entity -> + entity.id == 2.toLong() && entity.userKey == "encrypted_decrypted_key2" && entity.featureList == "encrypted_decrypted_feature3,feature4" && entity.createdAt == 999992L + })) + } + + @Test + fun `attributes are migrated`() { + `when`(attributesDao.all).thenReturn( + listOf( + AttributesEntity().apply { userKey = "key1"; attributes = "{\"attr1\":\"val1\",\"attr2\":\"val2\"}"; updatedAt = 999991 }, + AttributesEntity().apply { userKey = "key2"; attributes = "{\"attr3\":\"val3\",\"attr4\":\"val4\"}"; updatedAt = 999992 }, + )) + + applyCipherTask.execute() + + verify(attributesDao).all + verify(fromCipher).decrypt("key1") + verify(fromCipher).decrypt("{\"attr1\":\"val1\",\"attr2\":\"val2\"}") + verify(toCipher).encrypt("decrypted_key1") + verify(toCipher).encrypt("decrypted_{\"attr1\":\"val1\",\"attr2\":\"val2\"}") + verify(attributesDao).update("key1", "encrypted_decrypted_key1", "encrypted_decrypted_{\"attr1\":\"val1\",\"attr2\":\"val2\"}") + + verify(fromCipher).decrypt("key2") + verify(fromCipher).decrypt("{\"attr3\":\"val3\",\"attr4\":\"val4\"}") + verify(toCipher).encrypt("decrypted_key2") + verify(toCipher).encrypt("decrypted_{\"attr3\":\"val3\",\"attr4\":\"val4\"}") + verify(attributesDao).update("key2", "encrypted_decrypted_key2", "encrypted_decrypted_{\"attr3\":\"val3\",\"attr4\":\"val4\"}") + } } diff --git a/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java b/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java index 521374426..d3a049f28 100644 --- a/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java +++ b/src/test/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImplTest.java @@ -10,6 +10,9 @@ import org.mockito.MockitoAnnotations; import java.util.Arrays; +import java.util.HashSet; + +import io.split.android.client.dtos.SegmentsChange; public class MySegmentsStorageContainerImplTest { @@ -55,8 +58,8 @@ public void getUniqueAmountReturnsUniqueSegmentCount() { MySegmentsStorage storageForKey = mContainer.getStorageForKey(userKey); MySegmentsStorage storageForKey2 = mContainer.getStorageForKey(userKey2); - storageForKey.set(Arrays.asList("s1", "s2")); - storageForKey2.set(Arrays.asList("s2", "s4", "s6")); + storageForKey.set(SegmentsChange.create(new HashSet<>(Arrays.asList("s1", "s2")), -1L)); + storageForKey2.set(SegmentsChange.create(new HashSet<>(Arrays.asList("s2", "s4", "s6")), -1L)); long distinctAmount = mContainer.getUniqueAmount(); diff --git a/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java b/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java new file mode 100644 index 000000000..1e594ee48 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMyLargeSegmentsStorageTest.java @@ -0,0 +1,113 @@ +package io.split.android.client.storage.mysegments; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import io.split.android.client.dtos.SegmentsChange; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.MyLargeSegmentDao; +import io.split.android.client.storage.db.MyLargeSegmentEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; + +public class SqLitePersistentMyLargeSegmentsStorageTest { + + @Mock + private SplitRoomDatabase mDatabase; + @Mock + private SplitCipher mSplitCipher; + @Mock + private MyLargeSegmentDao mDao; + private SqLitePersistentMySegmentsStorage mStorage; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + when(mDatabase.myLargeSegmentDao()).thenReturn(mDao); + mStorage = new SqLitePersistentMySegmentsStorage<>(mSplitCipher, mDatabase.myLargeSegmentDao(), MyLargeSegmentEntity.creator()); + } + + @Test + public void encryptedValuesAreStoredWithDao() { + String userKey = "user_key"; + Set segments = new HashSet<>(Arrays.asList("segment1", "segment2", "segment3")); + String encryptedSegments = "encrypted_segments"; + + when(mSplitCipher.encrypt(anyString())).thenReturn(encryptedSegments); + + mStorage.set(userKey, SegmentsChange.create(segments, 2415L)); + + verify(mSplitCipher).encrypt(argThat(new ArgumentMatcher() { + @Override + public boolean matches(String argument) { + if (argument.contains("{")) { + SegmentsChange segmentsChange = Json.fromJson(argument, SegmentsChange.class); + return segmentsChange.getSegments().size() == 3 && + segmentsChange.getNames().containsAll(Arrays.asList("segment1", "segment2", "segment3")) && + segmentsChange.getChangeNumber() == 2415; + } else { + return false; + } + } + })); + verify(mDao).update(any(MyLargeSegmentEntity.class)); + } + + @Test + public void noUpdatesAreMadeWhenEncryptionResultIsNull() { + String userKey = "user_key"; + Set segments = new HashSet<>(Arrays.asList("segment1", "segment2", "segment3")); + + when(mSplitCipher.encrypt(anyString())).thenReturn(null); + + mStorage.set(userKey, SegmentsChange.create(segments, 2415L)); + + verify(mSplitCipher).encrypt(argThat(new ArgumentMatcher() { + @Override + public boolean matches(String argument) { + if (argument.contains("{")) { + SegmentsChange segmentsChange = Json.fromJson(argument, SegmentsChange.class); + return segmentsChange.getSegments().size() == 3 && + segmentsChange.getNames().containsAll(Arrays.asList("segment1", "segment2", "segment3")) && + segmentsChange.getChangeNumber() == 2415; + } else { + return false; + } + } + })); + verifyNoInteractions(mDao); + } + + @Test + public void getSnapshotReturnsDecryptedValues() { + String userKey = "user_key"; + String encryptedSegments = "encrypted_segments"; + String decryptedSegments = "segment1,segment2,segment3"; + MyLargeSegmentEntity entity = new MyLargeSegmentEntity(); + entity.setUserKey(userKey); + entity.setSegmentList(encryptedSegments); + + when(mDao.getByUserKey("encrypted_user_key")).thenReturn(entity); + when(mSplitCipher.encrypt(userKey)).thenReturn("encrypted_user_key"); + when(mSplitCipher.decrypt(encryptedSegments)).thenReturn(decryptedSegments); + + SegmentsChange result = mStorage.getSnapshot(userKey); + + assertTrue(result.getNames().containsAll(Arrays.asList("segment1", "segment2", "segment3"))); + } +} diff --git a/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java b/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java index 0e142b650..112b5e4cc 100644 --- a/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java +++ b/src/test/java/io/split/android/client/storage/mysegments/SqLitePersistentMySegmentsStorageTest.java @@ -1,24 +1,29 @@ package io.split.android.client.storage.mysegments; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Arrays; -import java.util.List; +import java.util.HashSet; +import java.util.Set; +import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.storage.cipher.SplitCipher; 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.utils.Json; public class SqLitePersistentMySegmentsStorageTest { @@ -28,39 +33,63 @@ public class SqLitePersistentMySegmentsStorageTest { private SplitCipher mSplitCipher; @Mock private MySegmentDao mDao; - private SqLitePersistentMySegmentsStorage mStorage; + private SqLitePersistentMySegmentsStorage mStorage; @Before public void setup() { MockitoAnnotations.openMocks(this); when(mDatabase.mySegmentDao()).thenReturn(mDao); - mStorage = new SqLitePersistentMySegmentsStorage(mDatabase, mSplitCipher); + mStorage = new SqLitePersistentMySegmentsStorage<>(mSplitCipher, mDatabase.mySegmentDao(), MySegmentEntity.creator()); } @Test public void encryptedValuesAreStoredWithDao() { String userKey = "user_key"; - List segments = Arrays.asList("segment1", "segment2", "segment3"); + Set segments = new HashSet<>(Arrays.asList("segment1", "segment2", "segment3")); String encryptedSegments = "encrypted_segments"; when(mSplitCipher.encrypt(anyString())).thenReturn(encryptedSegments); - mStorage.set(userKey, segments); - - verify(mSplitCipher).encrypt("segment1,segment2,segment3"); + mStorage.set(userKey, SegmentsChange.create(segments, -1L)); + + verify(mSplitCipher).encrypt(argThat(new ArgumentMatcher() { + @Override + public boolean matches(String argument) { + if (argument.contains("{")) { + SegmentsChange segmentsChange = Json.fromJson(argument, SegmentsChange.class); + return segmentsChange.getSegments().size() == 3 && + segmentsChange.getNames().containsAll(Arrays.asList("segment1", "segment2", "segment3")) && + segmentsChange.getChangeNumber() == -1; + } else { + return false; + } + } + })); verify(mDao).update(any(MySegmentEntity.class)); } @Test public void noUpdatesAreMadeWhenEncryptionResultIsNull() { String userKey = "user_key"; - List segments = Arrays.asList("segment1", "segment2", "segment3"); + Set segments = new HashSet<>(Arrays.asList("segment1", "segment2", "segment3")); when(mSplitCipher.encrypt(anyString())).thenReturn(null); - mStorage.set(userKey, segments); - - verify(mSplitCipher).encrypt("segment1,segment2,segment3"); + mStorage.set(userKey, SegmentsChange.create(segments, -1L)); + + verify(mSplitCipher).encrypt(argThat(new ArgumentMatcher() { + @Override + public boolean matches(String argument) { + if (argument.contains("{")) { + SegmentsChange segmentsChange = Json.fromJson(argument, SegmentsChange.class); + return segmentsChange.getSegments().size() == 3 && + segmentsChange.getNames().containsAll(Arrays.asList("segment1", "segment2", "segment3")) && + segmentsChange.getChangeNumber() == -1; + } else { + return false; + } + } + })); verifyNoInteractions(mDao); } @@ -77,8 +106,8 @@ public void getSnapshotReturnsDecryptedValues() { when(mSplitCipher.encrypt(userKey)).thenReturn("encrypted_user_key"); when(mSplitCipher.decrypt(encryptedSegments)).thenReturn(decryptedSegments); - List result = mStorage.getSnapshot(userKey); + SegmentsChange result = mStorage.getSnapshot(userKey); - assertEquals(Arrays.asList("segment1", "segment2", "segment3"), result); + assertTrue(result.getNames().containsAll(Arrays.asList("segment1", "segment2", "segment3"))); } } 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 f15584ed5..9c2677c38 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\"],\"fsT\":4,\"fsI\":2}"; + final String expectedJson = "{\"oM\":0,\"st\":\"memory\",\"sE\":true,\"rR\":{\"sp\":4000,\"ms\":5000,\"mls\":6000,\"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\"],\"fsT\":4,\"fsI\":2,\"lsE\":true,\"wls\":true}"; 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\"],\"fsT\":0,\"fsI\":0}"; + 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\"],\"fsT\":0,\"fsI\":0,\"lsE\":false,\"wls\":false}"; final String serializedConfig = telemetryConfigBodySerializer.serialize(buildMockConfigWithNulls()); assertEquals(expectedJson, serializedConfig); @@ -48,6 +48,7 @@ private Config buildMockConfig() { refreshRates.setImpressions(3000); refreshRates.setSplits(4000); refreshRates.setMySegments(5000); + refreshRates.setMyLargeSegments(6000); UrlOverrides urlOverrides = new UrlOverrides(); urlOverrides.setTelemetry(true); @@ -73,6 +74,8 @@ private Config buildMockConfig() { config.setIntegrations(Arrays.asList("integration1", "integration2")); config.setFlagSetsTotal(4); config.setFlagSetsInvalid(2); + config.setLargeSegmentsEnabled(true); + config.setWaitForLargeSegments(true); return config; } 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 1a6ec1cd4..2ca9d13ee 100644 --- a/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java +++ b/src/test/java/io/split/android/client/telemetry/TelemetryStatsBodySerializerTest.java @@ -30,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],\"tf\":[1,0,0,0],\"tfs\":[2,0,0,0],\"tcf\":[3,0,0,0],\"tcfs\":[4,0,0,0],\"tr\":[0,0,1,0]},\"mE\":{\"t\":2,\"ts\":3,\"tc\":5,\"tcs\":4,\"tf\":10,\"tfs\":20,\"tcf\":30,\"tcfs\":40,\"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); + assertEquals("{\"lS\":{\"sp\":1000,\"ms\":2000,\"mls\":425,\"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],\"tf\":[1,0,0,0],\"tfs\":[2,0,0,0],\"tcf\":[3,0,0,0],\"tcfs\":[4,0,0,0],\"tr\":[0,0,1,0]},\"mE\":{\"t\":2,\"ts\":3,\"tc\":5,\"tcs\":4,\"tf\":10,\"tfs\":20,\"tcf\":30,\"tcfs\":40,\"tr\":1},\"hE\":{},\"hL\":{\"sp\":[0,0,3,0],\"ms\":[0,0,5,0],\"mls\":[0,0,6,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,\"lsC\":25,\"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,\"mls\":3}}", serializedStats); } private Stats getMockStats() { @@ -46,6 +46,7 @@ private Stats getMockStats() { httpLatencies.setSplits(Arrays.asList(0L, 0L, 3L, 0L)); httpLatencies.setImpressionsCount(Arrays.asList(0L, 0L, 4L, 0L)); httpLatencies.setMySegments(Arrays.asList(0L, 0L, 5L, 0L)); + httpLatencies.setMyLargeSegments(Arrays.asList(0L, 0L, 6L, 0L)); httpLatencies.setToken(Arrays.asList(0L, 0L, 6L, 0L)); MethodLatencies methodLatencies = new MethodLatencies(); @@ -80,6 +81,7 @@ private Stats getMockStats() { LastSync lastSynchronizations = new LastSync(); lastSynchronizations.setLastSplitSync(1000); lastSynchronizations.setLastMySegmentSync(2000); + lastSynchronizations.setLastMyLargeSegmentSync(425); lastSynchronizations.setLastImpressionSync(3000); lastSynchronizations.setLastImpressionCountSync(4000); lastSynchronizations.setLastEventSync(5000); @@ -87,6 +89,7 @@ private Stats getMockStats() { lastSynchronizations.setLastTokenRefresh(7000); stats.setLastSynchronizations(lastSynchronizations); stats.setSegmentCount(4); + stats.setLargeSegmentCount(25); stats.setMethodExceptions(methodExceptions); stats.setMethodLatencies(methodLatencies); stats.setSessionLengthMs(2000); @@ -94,7 +97,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)); + stats.setUpdatesFromSSE(new UpdatesFromSSE(4L, 8L, 3L)); 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 4227802af..ee6001ad3 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 @@ -288,6 +288,7 @@ public void lastSyncDataBuildsCorrectly() { telemetryStorage.recordSuccessfulSync(OperationType.MY_SEGMENT, 5000); telemetryStorage.recordSuccessfulSync(OperationType.SPLITS, 6000); telemetryStorage.recordSuccessfulSync(OperationType.TOKEN, 7000); + telemetryStorage.recordSuccessfulSync(OperationType.MY_LARGE_SEGMENT, 8000); LastSync lastSync = telemetryStorage.getLastSynchronization(); @@ -296,6 +297,7 @@ public void lastSyncDataBuildsCorrectly() { assertEquals(3000, lastSync.getLastImpressionSync()); assertEquals(4000, lastSync.getLastImpressionCountSync()); assertEquals(5000, lastSync.getLastMySegmentSync()); + assertEquals(8000, lastSync.getLastMyLargeSegmentSync()); assertEquals(6000, lastSync.getLastSplitSync()); assertEquals(7000, lastSync.getLastTokenRefresh()); } @@ -324,6 +326,7 @@ public void popHttpErrorsBuildObjectCorrectly() { telemetryStorage.recordSyncError(OperationType.SPLITS, 404); telemetryStorage.recordSyncError(OperationType.SPLITS, 500); telemetryStorage.recordSyncError(OperationType.TOKEN, 401); + telemetryStorage.recordSyncError(OperationType.MY_LARGE_SEGMENT, 401); HttpErrors httpErrors = telemetryStorage.popHttpErrors(); @@ -344,6 +347,7 @@ public void popHttpErrorsBuildObjectCorrectly() { assertEquals(expectedEventMap, httpErrors.getImpressionSyncErrs()); assertEquals(expectedEventMap, httpErrors.getTelemetrySyncErrs()); assertEquals(expectedEventMap, httpErrors.getMySegmentSyncErrs()); + assertEquals(expectedEventMap, httpErrors.getMyLargeSegmentsSyncErrs()); assertEquals(expectedSplitSyncMap, httpErrors.getSplitSyncErrs()); assertEquals(expectedEventMap, httpErrors.getTokenGetErrs()); } @@ -355,6 +359,7 @@ public void popHttpErrorsReinitializesMap() { telemetryStorage.recordSyncError(OperationType.IMPRESSIONS, 401); telemetryStorage.recordSyncError(OperationType.TELEMETRY, 401); telemetryStorage.recordSyncError(OperationType.MY_SEGMENT, 401); + telemetryStorage.recordSyncError(OperationType.MY_LARGE_SEGMENT, 401); telemetryStorage.recordSyncError(OperationType.SPLITS, 401); telemetryStorage.recordSyncError(OperationType.TOKEN, 401); @@ -366,6 +371,7 @@ public void popHttpErrorsReinitializesMap() { assertTrue(httpErrors.getImpressionSyncErrs().isEmpty()); assertTrue(httpErrors.getTelemetrySyncErrs().isEmpty()); assertTrue(httpErrors.getMySegmentSyncErrs().isEmpty()); + assertTrue(httpErrors.getMyLargeSegmentsSyncErrs().isEmpty()); assertTrue(httpErrors.getSplitSyncErrs().isEmpty()); assertTrue(httpErrors.getTokenGetErrs().isEmpty()); } @@ -380,6 +386,7 @@ public void popHttpLatenciesBuildsObjectCorrectly() { telemetryStorage.recordSyncLatency(OperationType.IMPRESSIONS, 200); telemetryStorage.recordSyncLatency(OperationType.IMPRESSIONS_COUNT, 10); telemetryStorage.recordSyncLatency(OperationType.MY_SEGMENT, 2000); + telemetryStorage.recordSyncLatency(OperationType.MY_LARGE_SEGMENT, 200); telemetryStorage.recordSyncLatency(OperationType.TOKEN, 2000); HttpLatencies httpLatencies = telemetryStorage.popHttpLatencies(); @@ -396,6 +403,7 @@ public void popHttpLatenciesBuildsObjectCorrectly() { assertEquals(1, (long) httpLatencies.getSplits().get(14)); assertEquals(1, (long) httpLatencies.getSplits().get(22)); assertEquals(1, (long) httpLatencies.getMySegments().get(19)); + assertEquals(1, (long) httpLatencies.getMyLargeSegments().get(14)); assertEquals(1, (long) httpLatencies.getImpressions().get(14)); assertEquals(1, (long) httpLatencies.getImpressionsCount().get(6)); assertEquals(1, (long) httpLatencies.getEvents().get(15)); 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 1776d6c6a..ab15ad5e0 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 @@ -39,12 +39,14 @@ public class TelemetryStatsProviderImplTest { private SplitsStorage splitsStorage; @Mock private MySegmentsStorageContainer mySegmentsStorageContainer; + @Mock + private MySegmentsStorageContainer myLargeSegmentsStorageContainer; private TelemetryStatsProvider telemetryStatsProvider; @Before public void setUp() { MockitoAnnotations.openMocks(this); - telemetryStatsProvider = new TelemetryStatsProviderImpl(telemetryStorageConsumer, splitsStorage, mySegmentsStorageContainer); + telemetryStatsProvider = new TelemetryStatsProviderImpl(telemetryStorageConsumer, splitsStorage, mySegmentsStorageContainer, myLargeSegmentsStorageContainer); } @Test @@ -64,6 +66,7 @@ public void clearRemovesExistingStatsFromProvider() { public void statsAreCorrectlyBuilt() { long mySegmentsUniqueCount = 3; + long myLargeSegmentsUniqueCount = 152516; int splitsCount = 40; List streamingEvents = Arrays.asList( @@ -89,6 +92,7 @@ public void statsAreCorrectlyBuilt() { long eventsDropped = 21L; long sseSplits = 2L; long sseMySegments = 4L; + long sseMyLargeSegments = 5L; Map splits = new HashMap<>(); for (int i = 0; i < splitsCount; i++) { @@ -97,6 +101,7 @@ public void statsAreCorrectlyBuilt() { when(splitsStorage.getAll()).thenReturn(splits); when(mySegmentsStorageContainer.getUniqueAmount()).thenReturn(mySegmentsUniqueCount); + when(myLargeSegmentsStorageContainer.getUniqueAmount()).thenReturn(myLargeSegmentsUniqueCount); when(telemetryStorageConsumer.popStreamingEvents()).thenReturn(streamingEvents); when(telemetryStorageConsumer.popTags()).thenReturn(tags); when(telemetryStorageConsumer.popLatencies()).thenReturn(methodLatencies); @@ -112,7 +117,7 @@ public void statsAreCorrectlyBuilt() { 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)); + when(telemetryStorageConsumer.popUpdatesFromSSE()).thenReturn(new UpdatesFromSSE(sseSplits, sseMySegments, sseMyLargeSegments)); Stats stats = telemetryStatsProvider.getTelemetryStats(); assertEquals(streamingEvents, stats.getStreamingEvents()); @@ -120,6 +125,7 @@ public void statsAreCorrectlyBuilt() { assertEquals(tags, stats.getTags()); assertEquals(methodLatencies, stats.getMethodLatencies()); assertEquals(mySegmentsUniqueCount, stats.getSegmentCount()); + assertEquals(myLargeSegmentsUniqueCount, stats.getLargeSegmentCount()); assertEquals(sessionLength, stats.getSessionLengthMs()); assertEquals(lastSync, stats.getLastSynchronizations()); assertEquals(impDropped, stats.getImpressionsDropped()); diff --git a/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java b/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java index 63a14768d..73f254230 100644 --- a/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java +++ b/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java @@ -34,7 +34,7 @@ public class SplitClientImplFactory { public static SplitClientImpl get(Key key, SplitsStorage splitsStorage) { SplitClientConfig cfg = SplitClientConfig.builder().build(); SplitEventsManager eventsManager = new SplitEventsManager(cfg, new SplitTaskExecutorStub()); - SplitParser splitParser = new SplitParser(mock(MySegmentsStorageContainer.class)); + SplitParser splitParser = new SplitParser(mock(MySegmentsStorageContainer.class), mock(MySegmentsStorageContainer.class)); TelemetryStorage telemetryStorage = mock(TelemetryStorage.class); TreatmentManagerFactory treatmentManagerFactory = new TreatmentManagerFactoryImpl( new KeyValidatorImpl(), new SplitValidatorImpl(), new ImpressionListener.NoopImpressionListener(), @@ -61,7 +61,7 @@ false, new AttributesMergerImpl(), telemetryStorage, splitParser, } public static SplitClientImpl get(Key key, ImpressionListener impressionListener) { - SplitParser splitParser = new SplitParser(mock(MySegmentsStorageContainer.class)); + SplitParser splitParser = new SplitParser(mock(MySegmentsStorageContainer.class), mock(MySegmentsStorageContainer.class)); SplitClientConfig cfg = SplitClientConfig.builder().build(); return new SplitClientImpl( mock(SplitFactory.class), @@ -79,7 +79,7 @@ public static SplitClientImpl get(Key key, ImpressionListener impressionListener } public static SplitClientImpl get(Key key, SplitEventsManager eventsManager) { - SplitParser splitParser = new SplitParser(mock(MySegmentsStorageContainer.class)); + SplitParser splitParser = new SplitParser(mock(MySegmentsStorageContainer.class), mock(MySegmentsStorageContainer.class)); return new SplitClientImpl( mock(SplitFactory.class), mock(SplitClientContainer.class), diff --git a/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java b/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java index af7ae6ef9..77d185093 100644 --- a/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java +++ b/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java @@ -7,6 +7,8 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.util.Arrays; import java.util.HashMap; @@ -32,25 +34,39 @@ public class EvaluatorTest { private Evaluator evaluator; @Before - public void loadSplitsFromFile(){ - if(evaluator == null) { + public void loadSplitsFromFile() { + if (evaluator == null) { FileHelper fileHelper = new FileHelper(); MySegmentsStorage mySegmentsStorage = mock(MySegmentsStorage.class); + MySegmentsStorage myLargeSegmentsStorage = mock(MySegmentsStorage.class); MySegmentsStorageContainer mySegmentsStorageContainer = mock(MySegmentsStorageContainer.class); + MySegmentsStorageContainer myLargeSegmentsStorageContainer = mock(MySegmentsStorageContainer.class); SplitsStorage splitsStorage = mock(SplitsStorage.class); Set mySegments = new HashSet<>(Arrays.asList("s1", "s2", "test_copy")); + Set myLargeSegments = new HashSet<>(Arrays.asList("segment1")); List splits = fileHelper.loadAndParseSplitChangeFile("split_changes_1.json"); - SplitParser splitParser = new SplitParser(mySegmentsStorageContainer); + SplitParser splitParser = new SplitParser(mySegmentsStorageContainer, myLargeSegmentsStorageContainer); Map splitsMap = splitsMap(splits); when(splitsStorage.getAll()).thenReturn(splitsMap); + when(splitsStorage.get(any())).thenAnswer(new Answer() { + @Override + public Split answer(InvocationOnMock invocation) throws Throwable { + return splitsMap.get(invocation.getArgument(0)); + } + }); + + when(splitsStorage.get("FACUNDO_TEST")).thenReturn(splitsMap.get("FACUNDO_TEST")); when(splitsStorage.get("a_new_split_2")).thenReturn(splitsMap.get("a_new_split_2")); when(splitsStorage.get("Test")).thenReturn(splitsMap.get("Test")); + when(splitsStorage.get("ls_split")).thenReturn(splitsMap.get("ls_split")); when(mySegmentsStorageContainer.getStorageForKey(any())).thenReturn(mySegmentsStorage); + when(myLargeSegmentsStorageContainer.getStorageForKey("anyKey")).thenReturn(myLargeSegmentsStorage); when(mySegmentsStorage.getAll()).thenReturn(mySegments); + when(myLargeSegmentsStorage.getAll()).thenReturn(myLargeSegments); evaluator = new EvaluatorImpl(splitsStorage, splitParser); } @@ -99,6 +115,16 @@ public void testInSegmentTestKey() { Assert.assertEquals("whitelisted segment", result.getLabel()); } + @Test + public void testInLargeSegmentKey() { + String matchingKey = "anyKey"; + String splitName = "ls_split"; + EvaluationResult result = evaluator.getTreatment(matchingKey, matchingKey, splitName, null); + Assert.assertNotNull(result); + Assert.assertEquals("on", result.getTreatment()); + Assert.assertEquals("whitelisted segment", result.getLabel()); + } + @Test public void testKilledSplit() { String matchingKey = "anyKey"; @@ -174,7 +200,7 @@ public void changeNumberExceptionReturnsResultWithExceptionLabelAndChangeNumber( private Map splitsMap(List splits) { Map splitsMap = new HashMap<>(); - for(Split split : splits) { + for (Split split : splits) { splitsMap.put(split.name, split); } return splitsMap; diff --git a/src/test/java/io/split/android/engine/experiments/SplitParserTest.java b/src/test/java/io/split/android/engine/experiments/SplitParserTest.java index 6028439b6..e6c1d6ca6 100644 --- a/src/test/java/io/split/android/engine/experiments/SplitParserTest.java +++ b/src/test/java/io/split/android/engine/experiments/SplitParserTest.java @@ -4,8 +4,12 @@ import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import androidx.annotation.NonNull; + import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -27,6 +31,7 @@ import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.Status; +import io.split.android.client.dtos.UserDefinedLargeSegmentMatcherData; import io.split.android.client.dtos.WhitelistMatcherData; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; @@ -56,6 +61,8 @@ public class SplitParserTest { MySegmentsStorage mMySegmentsStorage; @Mock MySegmentsStorageContainer mMySegmentsStorageContainer; + @Mock + MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; @Before public void setup() { @@ -65,7 +72,7 @@ public void setup() { @Test public void less_than_or_equal_to() { - SplitParser parser = SplitParser.get(mMySegmentsStorageContainer); + SplitParser parser = createParser(); Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.LESS_THAN_OR_EQUAL_TO, DataType.NUMBER, 10L, false); @@ -95,7 +102,7 @@ public void less_than_or_equal_to() { @Test public void equal_to() { - SplitParser parser = SplitParser.get(mMySegmentsStorageContainer); + SplitParser parser = createParser(); Matcher ageLessThan10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, DataType.NUMBER, 10L, true); @@ -122,7 +129,7 @@ public void equal_to() { @Test public void equal_to_negative_number() { - SplitParser parser = new SplitParser(mMySegmentsStorageContainer); + SplitParser parser = createParser(); Matcher equalToNegative10 = ConditionsTestUtil.numericMatcher("user", "age", MatcherType.EQUAL_TO, DataType.NUMBER, -10L, false); @@ -146,10 +153,15 @@ public void equal_to_negative_number() { assertThat(actual, is(equalTo(expected))); } + @NonNull + private SplitParser createParser() { + return new SplitParser(mMySegmentsStorageContainer, mMyLargeSegmentsStorageContainer); + } + @Test public void between() { - SplitParser parser = new SplitParser(mMySegmentsStorageContainer); + SplitParser parser = createParser(); Matcher ageBetween10And11 = ConditionsTestUtil.betweenMatcher("user", "age", @@ -327,7 +339,7 @@ public void equalToSemverParsing() { condition.matcherGroup.matchers = Collections.singletonList(matcher); Split split = makeSplit("test1", Collections.singletonList(condition)); - SplitParser parser = new SplitParser(mMySegmentsStorageContainer); + SplitParser parser = createParser(); ParsedSplit parsedSplit = parser.parse(split); assertEquals("test1", parsedSplit.feature()); @@ -354,7 +366,7 @@ public void greaterThanOrEqualToSemverParsing() { condition.matcherGroup.matchers = Collections.singletonList(matcher); Split split = makeSplit("test1", Collections.singletonList(condition)); - SplitParser parser = new SplitParser(mMySegmentsStorageContainer); + SplitParser parser = createParser(); ParsedSplit parsedSplit = parser.parse(split); assertEquals("test1", parsedSplit.feature()); @@ -381,7 +393,7 @@ public void lessThanOrEqualToSemverParsing() { condition.matcherGroup.matchers = Collections.singletonList(matcher); Split split = makeSplit("test1", Collections.singletonList(condition)); - SplitParser parser = new SplitParser(mMySegmentsStorageContainer); + SplitParser parser = createParser(); ParsedSplit parsedSplit = parser.parse(split); assertEquals("test1", parsedSplit.feature()); @@ -411,7 +423,7 @@ public void betweenSemverParsing() { condition.matcherGroup.matchers = Collections.singletonList(matcher); Split split = makeSplit("test1", Collections.singletonList(condition)); - SplitParser parser = new SplitParser(mMySegmentsStorageContainer); + SplitParser parser = createParser(); ParsedSplit parsedSplit = parser.parse(split); assertEquals("test1", parsedSplit.feature()); @@ -440,7 +452,7 @@ public void inListSemverParsing() { condition.matcherGroup.matchers = Collections.singletonList(matcher); Split split = makeSplit("test1", Collections.singletonList(condition)); - SplitParser parser = new SplitParser(mMySegmentsStorageContainer); + SplitParser parser = createParser(); ParsedSplit parsedSplit = parser.parse(split); assertEquals("test1", parsedSplit.feature()); @@ -452,9 +464,40 @@ public void inListSemverParsing() { assertEquals(2, parsedCondition.partitions().size()); } + @Test + public void inLargeSegmentMatcherParsingTest() { + Condition condition = new Condition(); + condition.conditionType = ConditionType.ROLLOUT; + condition.label = "new label"; + condition.partitions = Arrays.asList( + ConditionsTestUtil.partition("on", 50), + ConditionsTestUtil.partition("0ff", 50)); + Matcher matcher = new Matcher(); + matcher.matcherType = MatcherType.IN_LARGE_SEGMENT; + UserDefinedLargeSegmentMatcherData userDefinedLargeSegmentMatcherData = new UserDefinedLargeSegmentMatcherData(); + userDefinedLargeSegmentMatcherData.largeSegmentName = "segment1"; + matcher.userDefinedLargeSegmentMatcherData = userDefinedLargeSegmentMatcherData; + condition.matcherGroup = new MatcherGroup(); + condition.matcherGroup.matchers = Collections.singletonList(matcher); + Split split = makeSplit("test1", Collections.singletonList(condition)); + + SplitParser parser = createParser(); + + ParsedSplit parsedSplit = parser.parse(split, "matching_key"); + assertEquals("test1", parsedSplit.feature()); + assertEquals("off", parsedSplit.defaultTreatment()); + assertEquals(1, parsedSplit.parsedConditions().size()); + ParsedCondition parsedCondition = parsedSplit.parsedConditions().get(0); + assertEquals("new label", parsedCondition.label()); + assertEquals(ConditionType.ROLLOUT, parsedCondition.conditionType()); + assertEquals(2, parsedCondition.partitions().size()); + verify(mMyLargeSegmentsStorageContainer).getStorageForKey("matching_key"); + verify(mMySegmentsStorageContainer, never()).getStorageForKey("matching_key"); + } + private void set_matcher_test(Condition c, io.split.android.engine.matchers.Matcher m) { - SplitParser parser = new SplitParser(mMySegmentsStorageContainer); + SplitParser parser = createParser(); List partitions = Arrays.asList(ConditionsTestUtil.partition("on", 100)); diff --git a/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java b/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java index 4472debb3..4a972866d 100644 --- a/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java +++ b/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java @@ -24,6 +24,8 @@ public class UnsupportedMatcherSplitParserTest { @Mock private MySegmentsStorageContainer mMySegmentsStorageContainer; @Mock + private MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; + @Mock private DefaultConditionsProvider mDefaultConditionsProvider; private final Split mTestFlag = Json.fromJson("{\"changeNumber\":1709843458770,\"trafficTypeName\":\"user\",\"name\":\"feature_flag_for_test\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1364119282,\"seed\":-605938843,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"algo\":2,\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"WRONG_MATCHER_TYPE\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"dependencyMatcherData\":null,\"booleanMatcherData\":null,\"stringMatcherData\":\"123123\"}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"wrong matcher type\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"sem\"},\"matcherType\":\"MATCHES_STRING\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"dependencyMatcherData\":null,\"booleanMatcherData\":null,\"stringMatcherData\":\"1.2.3\"}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"sem matches 1.2.3\"}],\"configurations\":{},\"sets\":[]}", Split.class); private AutoCloseable mAutoCloseable; @@ -35,7 +37,7 @@ public void setUp() { when(mMySegmentsStorageContainer.getStorageForKey("")).thenReturn(mMySegmentsStorage); when(mMySegmentsStorage.getAll()).thenReturn(Collections.emptySet()); when(mDefaultConditionsProvider.getDefaultConditions()).thenReturn(Collections.emptyList()); - mSplitParser = new SplitParser(mMySegmentsStorageContainer, mDefaultConditionsProvider); + mSplitParser = new SplitParser(mMySegmentsStorageContainer, mMyLargeSegmentsStorageContainer, mDefaultConditionsProvider); } @After diff --git a/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java b/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java index a197b0cee..21cacf357 100644 --- a/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java +++ b/src/test/java/io/split/android/fake/SplitTaskExecutorStub.java @@ -58,6 +58,6 @@ public void stop() { @Override public void submitOnMainThread(SplitTask splitTask) { - + splitTask.execute(); } } diff --git a/src/test/resources/split_changes_1.json b/src/test/resources/split_changes_1.json index 838cfd0c2..7d72dbbcf 100644 --- a/src/test/resources/split_changes_1.json +++ b/src/test/resources/split_changes_1.json @@ -2514,6 +2514,52 @@ } ] }, + { + "trafficTypeName":"account", + "name":"ls_split", + "trafficAllocation":100, + "trafficAllocationSeed":-285565213, + "seed":-1992295819, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1506703262916, + "algo":2, + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"IN_LARGE_SEGMENT", + "negate":false, + "userDefinedSegmentMatcherData":{ + "segmentName":"segment1" + }, + "userDefinedLargeSegmentMatcherData":{ + "largeSegmentName":"segment1" + }, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted segment" + } + ] + }, { "trafficTypeName":"user", "name":"broken_split", From 05ba399a86fd6fe8d1485979e3e3b77e33ebaecd Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 1 Nov 2024 09:22:10 -0300 Subject: [PATCH 2/3] Version 5.0.0-rc1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e7a7ca5ec..5a8e915aa 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '5.0.0-alpha.1' + splitVersion = '5.0.0-rc1' } android { From fbff30a6fd971d52b2f71ec829d7adf5e74b03b1 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Fri, 1 Nov 2024 17:41:54 -0300 Subject: [PATCH 3/3] Version 5.0.0 --- CHANGES.txt | 4 ++++ build.gradle | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 276f35747..4a034b66a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +5.0.0 (Nov 1, 2024) +- Added support for targeting rules based on large segments. +- BREAKING: Dropped support for Split Proxy below version 5.9.0. The SDK now requires Split Proxy 5.9.0 or above. + 4.2.2 (Sep 25, 2024) - Fixed issue in updating archived feature flags when using encryption. diff --git a/build.gradle b/build.gradle index 5a8e915aa..0ebef0b78 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '5.0.0-rc1' + splitVersion = '5.0.0' } android {