diff --git a/Split.xcodeproj/project.pbxproj b/Split.xcodeproj/project.pbxproj index 7d603744..f371a5a1 100644 --- a/Split.xcodeproj/project.pbxproj +++ b/Split.xcodeproj/project.pbxproj @@ -1093,6 +1093,8 @@ C539CAD72D8B5AD00050C732 /* TargetingRulesChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = C539CAD32D8B5AD00050C732 /* TargetingRulesChange.swift */; }; C539CAD92D8B5AF00050C732 /* ProcessedRuleBasedSegmentChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = C539CAD82D8B5AF00050C732 /* ProcessedRuleBasedSegmentChange.swift */; }; C539CADA2D8B5AF00050C732 /* ProcessedRuleBasedSegmentChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = C539CAD82D8B5AF00050C732 /* ProcessedRuleBasedSegmentChange.swift */; }; + C53F3C472DCB956900655753 /* SplitsSyncHelperTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53F3C462DCB956900655753 /* SplitsSyncHelperTest.swift */; }; + C53F3C4F2DCD112400655753 /* RuleBasedSegmentChangeProcessorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53F3C4E2DCD110700655753 /* RuleBasedSegmentChangeProcessorStub.swift */; }; C58F33732BDAC4AC00D66549 /* split_unsupported_matcher.json in Resources */ = {isa = PBXBuildFile; fileRef = C58F33722BDAC4AC00D66549 /* split_unsupported_matcher.json */; }; C5977BFF2BF27375003E293A /* Semver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5977BFE2BF27375003E293A /* Semver.swift */; }; C5977C012BF27390003E293A /* SemverTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5977C002BF27390003E293A /* SemverTest.swift */; }; @@ -1954,6 +1956,8 @@ C539CAD22D8B5AD00050C732 /* RuleBasedSegmentChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleBasedSegmentChange.swift; sourceTree = ""; }; C539CAD32D8B5AD00050C732 /* TargetingRulesChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetingRulesChange.swift; sourceTree = ""; }; C539CAD82D8B5AF00050C732 /* ProcessedRuleBasedSegmentChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedRuleBasedSegmentChange.swift; sourceTree = ""; }; + C53F3C462DCB956900655753 /* SplitsSyncHelperTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitsSyncHelperTest.swift; sourceTree = ""; }; + C53F3C4E2DCD110700655753 /* RuleBasedSegmentChangeProcessorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleBasedSegmentChangeProcessorStub.swift; sourceTree = ""; }; C58F33722BDAC4AC00D66549 /* split_unsupported_matcher.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = split_unsupported_matcher.json; sourceTree = ""; }; C5977BFE2BF27375003E293A /* Semver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Semver.swift; sourceTree = ""; }; C5977C002BF27390003E293A /* SemverTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SemverTest.swift; sourceTree = ""; }; @@ -3252,6 +3256,7 @@ 954F9C73257961AA00140B81 /* Service */ = { isa = PBXGroup; children = ( + C53F3C4E2DCD110700655753 /* RuleBasedSegmentChangeProcessorStub.swift */, 954F9C74257961C100140B81 /* HttpSplitFetcherStub.swift */, 95C7569C2696457500696148 /* NotificationHelperStub.swift */, 95F7BC262C45C12700C5F2E4 /* TlsPinCheckerMock.swift */, @@ -3358,6 +3363,7 @@ 956A7E2D297ED7600080D53C /* SplitsEncoderTest.swift */, 956A7E30297EE5A60080D53C /* ComputeProcessCountTest.swift */, 9572BA802AC4A4B100C10FC1 /* SplitChangeProcessorTests.swift */, + C53F3C462DCB956900655753 /* SplitsSyncHelperTest.swift */, ); path = Splits; sourceTree = ""; @@ -4525,6 +4531,7 @@ 9577A84B2683CBDC00D92AE1 /* ImpressionsCountRecorderWorkerTests.swift in Sources */, 5932260F24AAB27900496D8B /* TestUtils.swift in Sources */, 95F3F006258D49B600084AF8 /* PersistentEventsStorageStub.swift in Sources */, + C53F3C4F2DCD112400655753 /* RuleBasedSegmentChangeProcessorStub.swift in Sources */, 5959C477227B89820064F968 /* FactoryMonitorTest.swift in Sources */, 592C6ABE211B718E002D120C /* ArrayBlockingQueueTest.swift in Sources */, 5919017D24A28E1D005BD12A /* HttpRequestListTest.swift in Sources */, @@ -4664,6 +4671,7 @@ C5977C572BF53D60003E293A /* UnsupportedMatcherIntegrationTest.swift in Sources */, 95B1800527626C70002DC9DF /* TelemetryConfigRecorderWorkerTests.swift in Sources */, 95ABF4C6293522D6006ED016 /* EventsTrackerTest.swift in Sources */, + C53F3C472DCB956900655753 /* SplitsSyncHelperTest.swift in Sources */, 95ABF4C829352425006ED016 /* EventValidatorStub.swift in Sources */, 59F4AAC42513EFF800A1C69A /* PeriodicMySegmentsSyncWorkerTest.swift in Sources */, 31AA21DE01A000A7C30630F2 /* SseClientTest.swift in Sources */, diff --git a/Split/Api/SplitDatabaseHelper.swift b/Split/Api/SplitDatabaseHelper.swift index a7a03e64..953272bc 100644 --- a/Split/Api/SplitDatabaseHelper.swift +++ b/Split/Api/SplitDatabaseHelper.swift @@ -169,6 +169,10 @@ struct SplitDatabaseHelper { return DefaultPersistentSplitsStorage(database: database) } + static func openPersistentRuleBasedSegmentsStorage(database: SplitDatabase, generalInfoStorage: GeneralInfoStorage) -> PersistentRuleBasedSegmentsStorage { + return DefaultPersistentRuleBasedSegmentsStorage(database: database, generalInfoStorage: generalInfoStorage) + } + static func openSplitsStorage(database: SplitDatabase, flagSetsCache: FlagSetsCache) -> SplitsStorage { return DefaultSplitsStorage(persistentSplitsStorage: openPersistentSplitsStorage(database: database), diff --git a/Split/Common/Utils/Spec.swift b/Split/Common/Utils/Spec.swift index 758cdb06..615a52e3 100644 --- a/Split/Common/Utils/Spec.swift +++ b/Split/Common/Utils/Spec.swift @@ -9,5 +9,5 @@ import Foundation class Spec { - static var flagsSpec = "1.2" + static var flagsSpec = "1.3" } diff --git a/Split/FetcherEngine/HttpSplitFetcher.swift b/Split/FetcherEngine/HttpSplitFetcher.swift index 1774ebf8..8dfcac43 100644 --- a/Split/FetcherEngine/HttpSplitFetcher.swift +++ b/Split/FetcherEngine/HttpSplitFetcher.swift @@ -7,8 +7,7 @@ import Foundation protocol HttpSplitFetcher { - func execute(since: Int64, till: Int64?, headers: HttpHeaders?) throws -> SplitChange - func executeForTargetingRules(since: Int64, till: Int64?, headers: HttpHeaders?) throws -> TargetingRulesChange + func execute(since: Int64, rbSince: Int64?, till: Int64?, headers: HttpHeaders?) throws -> TargetingRulesChange } class DefaultHttpSplitFetcher: HttpSplitFetcher { @@ -22,19 +21,14 @@ class DefaultHttpSplitFetcher: HttpSplitFetcher { self.syncHelper = syncHelper } - func execute(since: Int64, till: Int64?, headers: HttpHeaders? = nil) throws -> SplitChange { - let targetingRulesChange = try executeForTargetingRules(since: since, till: till, headers: headers) - return targetingRulesChange.featureFlags - } - - func executeForTargetingRules(since: Int64, till: Int64?, headers: HttpHeaders? = nil) throws -> TargetingRulesChange { + func execute(since: Int64, rbSince: Int64?, till: Int64?, headers: HttpHeaders? = nil) throws -> TargetingRulesChange { Logger.d("Fetching targeting rules definitions") try syncHelper.checkEndpointReachability(restClient: restClient, resource: resource) let semaphore = DispatchSemaphore(value: 0) var requestResult: DataResult? let startTime = Date.nowMillis() - restClient.getSplitChanges(since: since, till: till, headers: headers) { result in + restClient.getSplitChanges(since: since, rbSince: rbSince, till: till, headers: headers) { result in TimeChecker.logInterval("Time to fetch targeting rules", startTime: startTime) requestResult = result semaphore.signal() diff --git a/Split/FetcherEngine/Refresh/BackgroundSyncWorker.swift b/Split/FetcherEngine/Refresh/BackgroundSyncWorker.swift index a7c11396..49c1f51a 100644 --- a/Split/FetcherEngine/Refresh/BackgroundSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/BackgroundSyncWorker.swift @@ -45,31 +45,36 @@ class BackgroundMySegmentsSyncWorker: BackgroundSyncWorker { class BackgroundSplitsSyncWorker: BackgroundSyncWorker { private let splitFetcher: HttpSplitFetcher - private let splitsStorage: BackgroundSyncSplitsStorage private let persistenSplitsStorage: PersistentSplitsStorage + private let persistentRuleBasedSegmentsStorage: PersistentRuleBasedSegmentsStorage private let splitChangeProcessor: SplitChangeProcessor private let cacheExpiration: Int64 private let syncHelper: SplitsSyncHelper init(splitFetcher: HttpSplitFetcher, persistentSplitsStorage: PersistentSplitsStorage, + persistentRuleBasedSegmentsStorage: PersistentRuleBasedSegmentsStorage, splitChangeProcessor: SplitChangeProcessor, + ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor, cacheExpiration: Int64, splitConfig: SplitClientConfig) { self.persistenSplitsStorage = persistentSplitsStorage + self.persistentRuleBasedSegmentsStorage = persistentRuleBasedSegmentsStorage self.splitFetcher = splitFetcher - self.splitsStorage = BackgroundSyncSplitsStorage(persistentSplitsStorage: persistentSplitsStorage) self.splitChangeProcessor = splitChangeProcessor self.cacheExpiration = cacheExpiration self.syncHelper = SplitsSyncHelper(splitFetcher: splitFetcher, - splitsStorage: splitsStorage, + splitsStorage: BackgroundSyncSplitsStorage(persistentSplitsStorage: persistentSplitsStorage), + ruleBasedSegmentsStorage: DefaultRuleBasedSegmentsStorage(persistentStorage: persistentRuleBasedSegmentsStorage), splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentsChangeProcessor, splitConfig: splitConfig) } func execute() { - var changeNumber = persistenSplitsStorage.getChangeNumber() - _ = try? syncHelper.sync(since: changeNumber, clearBeforeUpdate: false) + let changeNumber = persistenSplitsStorage.getChangeNumber() + let rbChangeNumber = persistentRuleBasedSegmentsStorage.getChangeNumber() + _ = try? syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber, clearBeforeUpdate: false) } } diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index 66065d93..68216241 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -141,22 +141,30 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { private let splitFetcher: HttpSplitFetcher private let splitsStorage: SplitsStorage + private let ruleBasedSegmentsStorage: RuleBasedSegmentsStorage private let splitChangeProcessor: SplitChangeProcessor + private let ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor private let syncHelper: SplitsSyncHelper init(splitFetcher: HttpSplitFetcher, splitsStorage: SplitsStorage, + ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, splitChangeProcessor: SplitChangeProcessor, + ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor, timer: PeriodicTimer, eventsManager: SplitEventsManager, splitConfig: SplitClientConfig) { self.splitFetcher = splitFetcher self.splitsStorage = splitsStorage + self.ruleBasedSegmentsStorage = ruleBasedSegmentsStorage self.splitChangeProcessor = splitChangeProcessor + self.ruleBasedSegmentsChangeProcessor = ruleBasedSegmentsChangeProcessor self.syncHelper = SplitsSyncHelper(splitFetcher: splitFetcher, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentsChangeProcessor, splitConfig: splitConfig) super.init(timer: timer, eventsManager: eventsManager) @@ -168,10 +176,12 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { return } - guard let result = try? syncHelper.sync(since: splitsStorage.changeNumber) else { + let changeNumber = splitsStorage.changeNumber + let rbChangeNumber: Int64 = ruleBasedSegmentsStorage.changeNumber + guard let result = try? syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber) else { return } - if result.success, result.featureFlagsUpdated { + if result.success, result.featureFlagsUpdated || result.rbsUpdated { notifyUpdate([.splitsUpdated]) } } diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index ce0c7b6e..c20d10a7 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -104,22 +104,30 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { private let splitFetcher: HttpSplitFetcher private let splitsStorage: SplitsStorage + private let ruleBasedSegmentsStorage: RuleBasedSegmentsStorage private let splitChangeProcessor: SplitChangeProcessor + private let ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor private let syncHelper: SplitsSyncHelper init(splitFetcher: HttpSplitFetcher, splitsStorage: SplitsStorage, + ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, splitChangeProcessor: SplitChangeProcessor, + ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor, eventsManager: SplitEventsManager, reconnectBackoffCounter: ReconnectBackoffCounter, splitConfig: SplitClientConfig) { self.splitFetcher = splitFetcher self.splitsStorage = splitsStorage + self.ruleBasedSegmentsStorage = ruleBasedSegmentsStorage self.splitChangeProcessor = splitChangeProcessor + self.ruleBasedSegmentChangeProcessor = ruleBasedSegmentChangeProcessor self.syncHelper = SplitsSyncHelper(splitFetcher: splitFetcher, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, splitConfig: splitConfig) super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter) @@ -127,7 +135,9 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { override func fetchFromRemote() throws -> Bool { do { - let result = try syncHelper.sync(since: splitsStorage.changeNumber, clearBeforeUpdate: false) + let changeNumber = splitsStorage.changeNumber + let rbChangeNumber = ruleBasedSegmentsStorage.changeNumber + let result = try syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber, clearBeforeUpdate: false) if result.success { if !isSdkReadyTriggered() || result.featureFlagsUpdated { @@ -148,28 +158,36 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { private let splitsFetcher: HttpSplitFetcher private let splitsStorage: SplitsStorage + private let ruleBasedSegmentsStorage: RuleBasedSegmentsStorage private let splitChangeProcessor: SplitChangeProcessor - private let changeNumber: Int64 + private let ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor + private let changeNumber: SplitsUpdateChangeNumber private let syncHelper: SplitsSyncHelper var changeChecker: SplitsChangesChecker init(splitsFetcher: HttpSplitFetcher, splitsStorage: SplitsStorage, + ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, splitChangeProcessor: SplitChangeProcessor, - changeNumber: Int64, + ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor, + changeNumber: SplitsUpdateChangeNumber, eventsManager: SplitEventsManager, reconnectBackoffCounter: ReconnectBackoffCounter, splitConfig: SplitClientConfig) { self.splitsFetcher = splitsFetcher self.splitsStorage = splitsStorage + self.ruleBasedSegmentsStorage = ruleBasedSegmentsStorage self.splitChangeProcessor = splitChangeProcessor + self.ruleBasedSegmentChangeProcessor = ruleBasedSegmentChangeProcessor self.changeNumber = changeNumber self.changeChecker = DefaultSplitsChangesChecker() self.syncHelper = SplitsSyncHelper(splitFetcher: splitsFetcher, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, splitConfig: splitConfig) super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter) @@ -177,13 +195,21 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { override func fetchFromRemote() throws -> Bool { let storedChangeNumber = splitsStorage.changeNumber - if changeNumber <= storedChangeNumber { + let flagsChangeNumber = changeNumber.flags + if let flagsChangeNumber, flagsChangeNumber <= storedChangeNumber { + return true + } + + let storedRbChangeNumber: Int64 = ruleBasedSegmentsStorage.changeNumber + let rbsChangeNumber = changeNumber.rbs + if let rbsChangeNumber, rbsChangeNumber <= storedRbChangeNumber { return true } do { let result = try syncHelper.sync(since: storedChangeNumber, - till: changeNumber, + rbSince: storedRbChangeNumber, + till: flagsChangeNumber ?? rbsChangeNumber, clearBeforeUpdate: false, headers: ServiceConstants.controlNoCacheHeader) if result.success { diff --git a/Split/FetcherEngine/Refresh/SplitBgSynchronizer.swift b/Split/FetcherEngine/Refresh/SplitBgSynchronizer.swift index 7617c2d3..5f21f0c1 100644 --- a/Split/FetcherEngine/Refresh/SplitBgSynchronizer.swift +++ b/Split/FetcherEngine/Refresh/SplitBgSynchronizer.swift @@ -165,6 +165,8 @@ struct BackgroundSyncExecutor { } let splitsStorage = SplitDatabaseHelper.openPersistentSplitsStorage(database: splitDatabase) let generalInfoStorage = SplitDatabaseHelper.openGeneralInfoStorage(database: splitDatabase) + let persistentRuleBasedSegmentsStorage = SplitDatabaseHelper.openPersistentRuleBasedSegmentsStorage(database: splitDatabase, + generalInfoStorage: generalInfoStorage) let endpoints = serviceEndpoints ?? ServiceEndpoints.builder().build() let endpointFactory = EndpointFactory(serviceEndpoints: endpoints, apiKey: apiKey, @@ -189,9 +191,12 @@ struct BackgroundSyncExecutor { let bySetsFilter = splitsStorage.getBySetsFilter() let cacheExpiration = Int64(ServiceConstants.cacheExpirationInSeconds) let changeProcessor = DefaultSplitChangeProcessor(filterBySet: bySetsFilter) + let ruleBasedSegmentChangeProcessor = DefaultRuleBasedSegmentChangeProcessor() self.splitsSyncWorker = BackgroundSplitsSyncWorker(splitFetcher: splitsFetcher, persistentSplitsStorage: splitsStorage, + persistentRuleBasedSegmentsStorage: persistentRuleBasedSegmentsStorage, splitChangeProcessor: changeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, cacheExpiration: cacheExpiration, splitConfig: SplitClientConfig()) diff --git a/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift b/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift index dde8934d..b17ded67 100644 --- a/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift +++ b/Split/FetcherEngine/Refresh/SplitsSyncHelper.swift @@ -1,5 +1,5 @@ // -// RetryableSyncWorker.swift +// SplitsSyncHelper.swift // Split // // Created by Javier Avrudsky on 15-Sep-2020 @@ -11,19 +11,25 @@ import Foundation struct SyncResult { let success: Bool let changeNumber: Int64 + let rbChangeNumber: Int64? let featureFlagsUpdated: Bool + let rbsUpdated: Bool } class SplitsSyncHelper { struct FetchResult { let till: Int64 + let rbTill: Int64? let featureFlagsUpdated: Bool + let rbsUpdated: Bool } private let splitFetcher: HttpSplitFetcher private let splitsStorage: SyncSplitsStorage + private let ruleBasedSegmentsStorage: RuleBasedSegmentsStorage private let splitChangeProcessor: SplitChangeProcessor + private let ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor private let splitConfig: SplitClientConfig private var maxAttempts: Int { @@ -40,21 +46,27 @@ class SplitsSyncHelper { init(splitFetcher: HttpSplitFetcher, splitsStorage: SyncSplitsStorage, + ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, splitChangeProcessor: SplitChangeProcessor, + ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor, splitConfig: SplitClientConfig) { self.splitFetcher = splitFetcher self.splitsStorage = splitsStorage + self.ruleBasedSegmentsStorage = ruleBasedSegmentsStorage self.splitChangeProcessor = splitChangeProcessor + self.ruleBasedSegmentsChangeProcessor = ruleBasedSegmentsChangeProcessor self.splitConfig = splitConfig } func sync(since: Int64, + rbSince: Int64, till: Int64? = nil, clearBeforeUpdate: Bool = false, headers: HttpHeaders? = nil) throws -> SyncResult { do { let res = try tryToSync(since: since, + rbSince: rbSince, till: till, clearBeforeUpdate: clearBeforeUpdate, headers: headers) @@ -64,6 +76,7 @@ class SplitsSyncHelper { } return try tryToSync(since: res.changeNumber, + rbSince: res.rbChangeNumber, till: res.changeNumber, clearBeforeUpdate: clearBeforeUpdate && res.changeNumber == since, headers: headers, @@ -75,7 +88,9 @@ class SplitsSyncHelper { } func tryToSync(since: Int64, + rbSince: Int64? = nil, till: Int64? = nil, + rbTill: Int64? = nil, clearBeforeUpdate: Bool = false, headers: HttpHeaders? = nil, useTillParam: Bool = false) throws -> SyncResult { @@ -83,28 +98,39 @@ class SplitsSyncHelper { let backoffCounter = DefaultReconnectBackoffCounter(backoffBase: backoffTimeBaseInSecs, maxTimeLimit: backoffTimeMaxInSecs) var nextSince = since + var nextRbSince: Int64? = rbSince var attemptCount = 0 let goalTill = till ?? -10 + let goalRbTill = rbTill ?? -10 while attemptCount < maxAttempts { let result = try fetchUntil(since: nextSince, + rbSince: nextRbSince, till: useTillParam ? till : nil, clearBeforeUpdate: clearBeforeUpdate, headers: headers) nextSince = result.till + nextRbSince = result.rbTill ?? -1 - if nextSince >= goalTill { + if nextSince >= goalTill, nextRbSince ?? -1 >= goalRbTill { return SyncResult(success: true, changeNumber: nextSince, - featureFlagsUpdated: result.featureFlagsUpdated) + rbChangeNumber: nextRbSince, + featureFlagsUpdated: result.featureFlagsUpdated, + rbsUpdated: result.rbsUpdated) } Thread.sleep(forTimeInterval: backoffCounter.getNextRetryTime()) attemptCount+=1 } - return SyncResult(success: false, changeNumber: nextSince, featureFlagsUpdated: false) + return SyncResult(success: false, + changeNumber: nextSince, + rbChangeNumber: nextRbSince, + featureFlagsUpdated: false, + rbsUpdated: false) } func fetchUntil(since: Int64, + rbSince: Int64?, till: Int64? = nil, clearBeforeUpdate: Bool = false, headers: HttpHeaders? = nil) throws -> FetchResult { @@ -112,28 +138,45 @@ class SplitsSyncHelper { var clearCache = clearBeforeUpdate var firstFetch = true var nextSince = since + var nextRbSince = rbSince var featureFlagsUpdated = false + var rbsUpdated = false while true { clearCache = clearCache && firstFetch - let splitChange = try self.splitFetcher.execute(since: nextSince, + let targetingRulesChange = try self.splitFetcher.execute(since: nextSince, + rbSince: nextRbSince, till: till, headers: headers) - let newSince = splitChange.since - let newTill = splitChange.till + let flagsChange = targetingRulesChange.featureFlags + let newSince = flagsChange.since + let newTill = flagsChange.till + + let rbsChange = targetingRulesChange.ruleBasedSegments + let newRbSince = rbsChange.since + let newRbTill = rbsChange.till if clearCache { splitsStorage.clear() + ruleBasedSegmentsStorage.clear() } firstFetch = false - if splitsStorage.update(splitChange: splitChangeProcessor.process(splitChange)) { + if splitsStorage.update(splitChange: splitChangeProcessor.process(targetingRulesChange.featureFlags)) { featureFlagsUpdated = true } + + let processedChange = ruleBasedSegmentsChangeProcessor.process(targetingRulesChange.ruleBasedSegments) + if ruleBasedSegmentsStorage.update(toAdd: processedChange.toAdd, toRemove: processedChange.toRemove, changeNumber: processedChange.changeNumber) { + rbsUpdated = true + } + Logger.i("Feature flag definitions have been updated") // Line below commented temporary for debug purposes // Logger.v(splitChange.description) - if newSince == newTill, newTill >= since { - return FetchResult(till: newTill, featureFlagsUpdated: featureFlagsUpdated) + let rbSince = rbSince ?? -1 + if newSince == newTill, newTill >= since, newRbSince == newRbTill, newRbTill >= rbSince { + return FetchResult(till: newTill, rbTill: newRbTill, featureFlagsUpdated: featureFlagsUpdated, rbsUpdated: rbsUpdated) } nextSince = newTill + nextRbSince = newRbTill } } } diff --git a/Split/Initialization/SplitComponentFactory.swift b/Split/Initialization/SplitComponentFactory.swift index 20681f10..e0f65928 100644 --- a/Split/Initialization/SplitComponentFactory.swift +++ b/Split/Initialization/SplitComponentFactory.swift @@ -341,6 +341,7 @@ extension SplitComponentFactory { apiFacade: try getSplitApiFacade(), storageContainer: try getSplitStorageContainer(), splitChangeProcessor: DefaultSplitChangeProcessor(filterBySet: splitClientConfig.bySetsFilter()), + ruleBasedSegmentChangeProcessor: DefaultRuleBasedSegmentChangeProcessor(), eventsManager: getSplitEventsManagerCoordinator()) catalog.add(component: component) return component diff --git a/Split/Localhost/LocalhostSynchronizer.swift b/Split/Localhost/LocalhostSynchronizer.swift index 7bc59ea2..0081bf27 100644 --- a/Split/Localhost/LocalhostSynchronizer.swift +++ b/Split/Localhost/LocalhostSynchronizer.swift @@ -31,7 +31,7 @@ class LocalhostSynchronizer: FeatureFlagsSynchronizer { featureFlagsDataSource.start() } - func synchronize(changeNumber: Int64) { + func synchronize(changeNumber: Int64?, rbsChangeNumber: Int64?) { } func startPeriodicSync() { diff --git a/Split/Matchers/InRuleBasedSegmentMatcher.swift b/Split/Matchers/InRuleBasedSegmentMatcher.swift index a5c5b21d..1560c188 100644 --- a/Split/Matchers/InRuleBasedSegmentMatcher.swift +++ b/Split/Matchers/InRuleBasedSegmentMatcher.swift @@ -27,7 +27,7 @@ class InRuleBasedSegmentMatcher: BaseMatcher, MatcherProtocol { } if isExcluded(segment: segment, matchingKey: values.matchingKey, - mySegmentsStorage: context?.mySegmentsStorage) { + values: values, context: context) { return false } @@ -36,7 +36,7 @@ class InRuleBasedSegmentMatcher: BaseMatcher, MatcherProtocol { /// returns true if the matchingKey or any of the segments is excluded private func isExcluded(segment: RuleBasedSegment, matchingKey: String, - mySegmentsStorage: MySegmentsStorage?) -> Bool { + values: EvalValues, context: EvalContext?) -> Bool { // no excluded property guard let excluded = segment.excluded else { return false @@ -50,8 +50,28 @@ class InRuleBasedSegmentMatcher: BaseMatcher, MatcherProtocol { // check excluded segments if let excludedSegments = excluded.segments { for segment in excludedSegments { - if mySegmentsStorage?.getAll(forKey: matchingKey).contains(segment) ?? false { - return true + guard let name = segment.name else { + continue + } + + if segment.isStandard() { + if context?.mySegmentsStorage?.getAll(forKey: matchingKey).contains(name) ?? false { + return true + } + } + + if segment.isRuleBased() { + let matcherData = UserDefinedSegmentMatcherData() + matcherData.segmentName = name + if InRuleBasedSegmentMatcher(data: matcherData).evaluate(values: values, context: context) { + return true + } + } + + if segment.isLarge() { + if context?.myLargeSegmentsStorage?.getAll(forKey: matchingKey).contains(name) ?? false { + return true + } } } } diff --git a/Split/Models/SplitModel/RuleBasedSegmentChange.swift b/Split/Models/SplitModel/RuleBasedSegmentChange.swift index 2a15c3f9..d4212592 100644 --- a/Split/Models/SplitModel/RuleBasedSegmentChange.swift +++ b/Split/Models/SplitModel/RuleBasedSegmentChange.swift @@ -24,6 +24,10 @@ import Foundation self.since = since self.till = till } + + static func empty() -> RuleBasedSegmentChange { + return RuleBasedSegmentChange(segments: [], since: -1, till: -1) + } } extension RuleBasedSegmentChange { diff --git a/Split/Models/SplitModel/TargetingRulesChange.swift b/Split/Models/SplitModel/TargetingRulesChange.swift index 4a847ac7..2dec80be 100644 --- a/Split/Models/SplitModel/TargetingRulesChange.swift +++ b/Split/Models/SplitModel/TargetingRulesChange.swift @@ -22,6 +22,10 @@ import Foundation self.ruleBasedSegments = ruleBasedSegments super.init() } + + convenience init(featureFlags: SplitChange) { + self.init(featureFlags: featureFlags, ruleBasedSegments: RuleBasedSegmentChange.empty()) + } } extension TargetingRulesChange { diff --git a/Split/Network/RestClient/RestClient+SplitChanges.swift b/Split/Network/RestClient/RestClient+SplitChanges.swift index 55f73df6..c55ab8cd 100644 --- a/Split/Network/RestClient/RestClient+SplitChanges.swift +++ b/Split/Network/RestClient/RestClient+SplitChanges.swift @@ -10,6 +10,7 @@ import Foundation protocol RestClientSplitChanges: RestClient { func getSplitChanges(since: Int64, + rbSince: Int64?, till: Int64?, headers: HttpHeaders?, completion: @escaping (DataResult) -> Void) @@ -17,17 +18,19 @@ protocol RestClientSplitChanges: RestClient { extension DefaultRestClient: RestClientSplitChanges { func getSplitChanges(since: Int64, + rbSince: Int64?, till: Int64?, headers: HttpHeaders?, completion: @escaping (DataResult) -> Void) { self.execute( endpoint: endpointFactory.splitChangesEndpoint, - parameters: buildParameters(since: since, till: till), + parameters: buildParameters(since: since, rbSince: rbSince, till: till), headers: headers, completion: completion) } private func buildParameters(since: Int64, + rbSince: Int64?, till: Int64?) -> HttpParameters { var parameters: [HttpParameter] = [] if !Spec.flagsSpec.isEmpty() { @@ -35,6 +38,9 @@ extension DefaultRestClient: RestClientSplitChanges { } parameters.append(HttpParameter(key: "since", value: since)) + if let rbSince = rbSince { + parameters.append(HttpParameter(key: "rbSince", value: rbSince)) + } parameters.append(HttpParameter(key: "sets")) parameters.append(HttpParameter(key: "names")) parameters.append(HttpParameter(key: "prefixes")) diff --git a/Split/Network/Streaming/DefaultSseNotificationParser.swift b/Split/Network/Streaming/DefaultSseNotificationParser.swift index 6dfe37d9..c5c0bea3 100644 --- a/Split/Network/Streaming/DefaultSseNotificationParser.swift +++ b/Split/Network/Streaming/DefaultSseNotificationParser.swift @@ -12,7 +12,7 @@ protocol SseNotificationParser { func parseIncoming(jsonString: String) -> IncomingNotification? - func parseSplitUpdate(jsonString: String) throws -> SplitsUpdateNotification + func parseTargetingRuleNotification(jsonString: String, type: NotificationType) throws -> TargetingRuleUpdateNotification func parseSplitKill(jsonString: String) throws -> SplitKillNotification @@ -52,8 +52,10 @@ class DefaultSseNotificationParser: SseNotificationParser { return nil } - func parseSplitUpdate(jsonString: String) throws -> SplitsUpdateNotification { - return try Json.decodeFrom(json: jsonString, to: SplitsUpdateNotification.self) + func parseTargetingRuleNotification(jsonString: String, type: NotificationType) throws -> TargetingRuleUpdateNotification { + var notification = try Json.decodeFrom(json: jsonString, to: TargetingRuleUpdateNotification.self) + notification.entityType = type + return notification } func parseSplitKill(jsonString: String) throws -> SplitKillNotification { diff --git a/Split/Network/Streaming/FeatureFlagsPayloadDecoder.swift b/Split/Network/Streaming/FeatureFlagsPayloadDecoder.swift index 66ac3f1f..b28aa880 100644 --- a/Split/Network/Streaming/FeatureFlagsPayloadDecoder.swift +++ b/Split/Network/Streaming/FeatureFlagsPayloadDecoder.swift @@ -8,15 +8,23 @@ import Foundation -protocol FeatureFlagsPayloadDecoder { - func decode(payload: String, compressionUtil: CompressionUtil) throws -> Split +protocol TargetingRulePayloadDecoder { + associatedtype DecodedType + func decode(payload: String, compressionUtil: CompressionUtil) throws -> DecodedType } -struct DefaultFeatureFlagsPayloadDecoder: FeatureFlagsPayloadDecoder { +class DefaultTargetingRulePayloadDecoder: TargetingRulePayloadDecoder { + typealias DecodedType = T - func decode(payload: String, compressionUtil: CompressionUtil) throws -> Split { + private let type: T.Type + + init(type: T.Type) { + self.type = type + } + + func decode(payload: String, compressionUtil: CompressionUtil) throws -> T { let json = try decodeAsBytes(payload: payload, compressionUtil: compressionUtil).stringRepresentation - return try Json.decodeFrom(json: json, to: Split.self) + return try Json.decodeFrom(json: json, to: type) } private func decodeAsBytes(payload: String, compressionUtil: CompressionUtil) throws -> Data { @@ -27,3 +35,6 @@ struct DefaultFeatureFlagsPayloadDecoder: FeatureFlagsPayloadDecoder { return descomp } } + +typealias DefaultFeatureFlagsPayloadDecoder = DefaultTargetingRulePayloadDecoder +typealias DefaultRuleBasedSegmentsPayloadDecoder = DefaultTargetingRulePayloadDecoder diff --git a/Split/Network/Streaming/RetryableSplitsUpdateWorkerFactory.swift b/Split/Network/Streaming/RetryableSplitsUpdateWorkerFactory.swift index f392cb35..03e1f1ef 100644 --- a/Split/Network/Streaming/RetryableSplitsUpdateWorkerFactory.swift +++ b/Split/Network/Streaming/RetryableSplitsUpdateWorkerFactory.swift @@ -25,7 +25,7 @@ protocol SyncWorkerFactory { func createPeriodicSplitsSyncWorker() -> PeriodicSyncWorker - func createRetryableSplitsUpdateWorker(changeNumber: Int64, + func createRetryableSplitsUpdateWorker(changeNumber: SplitsUpdateChangeNumber, reconnectBackoffCounter: ReconnectBackoffCounter ) -> RetryableSyncWorker @@ -52,12 +52,23 @@ protocol SyncWorkerFactory { func createPeriodicTelemetryStatsRecorderWorker() -> PeriodicRecorderWorker? } +struct SplitsUpdateChangeNumber: Hashable { + let flags: Int64? + let rbs: Int64? + + init(flags: Int64?, rbs: Int64?) { + self.flags = flags + self.rbs = rbs + } +} + class DefaultSyncWorkerFactory: SyncWorkerFactory { private let storageContainer: SplitStorageContainer private let apiFacade: SplitApiFacade private let splitConfig: SplitClientConfig private let splitChangeProcessor: SplitChangeProcessor + private let ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor private let userKey: String private let eventsManager: SplitEventsManager private let splitsFilterQueryString: String @@ -71,6 +82,7 @@ class DefaultSyncWorkerFactory: SyncWorkerFactory { apiFacade: SplitApiFacade, storageContainer: SplitStorageContainer, splitChangeProcessor: SplitChangeProcessor, + ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor, eventsManager: SplitEventsManager) { self.userKey = userKey @@ -80,6 +92,7 @@ class DefaultSyncWorkerFactory: SyncWorkerFactory { self.apiFacade = apiFacade self.storageContainer = storageContainer self.splitChangeProcessor = splitChangeProcessor + self.ruleBasedSegmentChangeProcessor = ruleBasedSegmentChangeProcessor self.eventsManager = eventsManager self.telemetryProducer = storageContainer.telemetryStorage } @@ -88,18 +101,23 @@ class DefaultSyncWorkerFactory: SyncWorkerFactory { let backoffCounter = DefaultReconnectBackoffCounter(backoffBase: splitConfig.generalRetryBackoffBase) return RetryableSplitsSyncWorker(splitFetcher: apiFacade.splitsFetcher, splitsStorage: storageContainer.splitsStorage, + ruleBasedSegmentsStorage: storageContainer.ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentChangeProcessor: ruleBasedSegmentChangeProcessor, eventsManager: eventsManager, reconnectBackoffCounter: backoffCounter, splitConfig: splitConfig) } - func createRetryableSplitsUpdateWorker(changeNumber: Int64, + func createRetryableSplitsUpdateWorker(changeNumber: SplitsUpdateChangeNumber, reconnectBackoffCounter: ReconnectBackoffCounter) -> RetryableSyncWorker { return RetryableSplitsUpdateWorker(splitsFetcher: apiFacade.splitsFetcher, splitsStorage: storageContainer.splitsStorage, + ruleBasedSegmentsStorage: storageContainer.ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, - changeNumber: changeNumber, eventsManager: eventsManager, + ruleBasedSegmentChangeProcessor: ruleBasedSegmentChangeProcessor, + changeNumber: changeNumber, + eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter, splitConfig: splitConfig) } @@ -108,7 +126,9 @@ class DefaultSyncWorkerFactory: SyncWorkerFactory { return PeriodicSplitsSyncWorker( splitFetcher: apiFacade.splitsFetcher, splitsStorage: storageContainer.splitsStorage, + ruleBasedSegmentsStorage: storageContainer.ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, timer: DefaultPeriodicTimer(interval: splitConfig.featuresRefreshRate), eventsManager: eventsManager, splitConfig: splitConfig) } diff --git a/Split/Network/Streaming/SseNotificationProcessor.swift b/Split/Network/Streaming/SseNotificationProcessor.swift index fdfcb5bc..4f0240e8 100644 --- a/Split/Network/Streaming/SseNotificationProcessor.swift +++ b/Split/Network/Streaming/SseNotificationProcessor.swift @@ -37,7 +37,9 @@ class DefaultSseNotificationProcessor: SseNotificationProcessor { Logger.d("Received notification \(notification.type)") switch notification.type { case .splitUpdate: - processSplitsUpdate(notification) + processTargetingRuleUpdate(notification) + case .ruleBasedSegmentUpdate: + processTargetingRuleUpdate(notification) case .mySegmentsUpdate: processSegmentsUpdate(notification, updateWorker: mySegmentsUpdateWorker) case .myLargeSegmentsUpdate: @@ -49,13 +51,13 @@ class DefaultSseNotificationProcessor: SseNotificationProcessor { } } - private func processSplitsUpdate(_ notification: IncomingNotification) { + private func processTargetingRuleUpdate(_ notification: IncomingNotification) { if let jsonData = notification.jsonData { do { try splitsUpdateWorker.process(notification: - sseNotificationParser.parseSplitUpdate(jsonString: jsonData)) + sseNotificationParser.parseTargetingRuleNotification(jsonString: jsonData, type: notification.type)) } catch { - Logger.e("Error while parsing feature flags update notification: \(error.localizedDescription)") + Logger.e("Error while parsing targeting rule update notification: \(error.localizedDescription)") } } } diff --git a/Split/Network/Streaming/SseNotifications.swift b/Split/Network/Streaming/SseNotifications.swift index 19b6522c..a9a380fa 100644 --- a/Split/Network/Streaming/SseNotifications.swift +++ b/Split/Network/Streaming/SseNotifications.swift @@ -22,6 +22,7 @@ struct RawNotification: Decodable { /// Notification types of any type enum NotificationType: Decodable { case splitUpdate + case ruleBasedSegmentUpdate case mySegmentsUpdate case myLargeSegmentsUpdate case splitKill @@ -39,6 +40,8 @@ enum NotificationType: Decodable { switch string.lowercased() { case "split_update": return NotificationType.splitUpdate + case "rb_segment_update": + return NotificationType.ruleBasedSegmentUpdate case "memberships_ms_update": return NotificationType.mySegmentsUpdate case "memberships_ls_update": @@ -237,9 +240,13 @@ struct SplitKillNotification: NotificationTypeField { } /// indicates feature flag changes -struct SplitsUpdateNotification: NotificationTypeField { +struct TargetingRuleUpdateNotification: NotificationTypeField { + var entityType: NotificationType? var type: NotificationType { - return .splitUpdate + guard let notificationType = entityType else { + return .unknown + } + return notificationType } let changeNumber: Int64 let previousChangeNumber: Int64? @@ -264,6 +271,8 @@ struct SplitsUpdateNotification: NotificationTypeField { } } +typealias SplitsUpdateNotification = TargetingRuleUpdateNotification // TODO: Temporary alias to be removed in follow-up PR + /// Indicates a notification related to occupancy struct OccupancyNotification: NotificationTypeField { private let kControlPriToken = "control_pri" diff --git a/Split/Network/Streaming/SyncUpdateWorker.swift b/Split/Network/Streaming/SyncUpdateWorker.swift index 0a87fbd6..5aede075 100644 --- a/Split/Network/Streaming/SyncUpdateWorker.swift +++ b/Split/Network/Streaming/SyncUpdateWorker.swift @@ -24,60 +24,183 @@ class UpdateWorker { } } -class SplitsUpdateWorker: UpdateWorker { +class SplitsUpdateWorker: UpdateWorker { private let synchronizer: Synchronizer private let splitsStorage: SplitsStorage + private let ruleBasedSegmentsStorage: RuleBasedSegmentsStorage private let splitChangeProcessor: SplitChangeProcessor - private let payloadDecoder: FeatureFlagsPayloadDecoder + private let ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor + private let payloadDecoder: DefaultFeatureFlagsPayloadDecoder + private let ruleBasedSegmentsPayloadDecoder: DefaultRuleBasedSegmentsPayloadDecoder private let telemetryProducer: TelemetryRuntimeProducer? var decomProvider: CompressionProvider = DefaultDecompressionProvider() init(synchronizer: Synchronizer, splitsStorage: SplitsStorage, + ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, splitChangeProcessor: SplitChangeProcessor, - featureFlagsPayloadDecoder: FeatureFlagsPayloadDecoder, + ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor, + featureFlagsPayloadDecoder: DefaultFeatureFlagsPayloadDecoder, + ruleBasedSegmentsPayloadDecoder: DefaultRuleBasedSegmentsPayloadDecoder, telemetryProducer: TelemetryRuntimeProducer?) { self.synchronizer = synchronizer self.splitsStorage = splitsStorage + self.ruleBasedSegmentsStorage = ruleBasedSegmentsStorage self.splitChangeProcessor = splitChangeProcessor + self.ruleBasedSegmentsChangeProcessor = ruleBasedSegmentsChangeProcessor self.payloadDecoder = featureFlagsPayloadDecoder + self.ruleBasedSegmentsPayloadDecoder = ruleBasedSegmentsPayloadDecoder self.telemetryProducer = telemetryProducer super.init(queueName: "SplitsUpdateWorker") } - override func process(notification: SplitsUpdateNotification) throws { + override func process(notification: TargetingRuleUpdateNotification) throws { processQueue.async { [weak self] in guard let self = self else { return } - let storedChangeNumber = self.splitsStorage.changeNumber + let storedChangeNumber = getChangeNumber(notification.type) if storedChangeNumber >= notification.changeNumber { return } if let previousChangeNumber = notification.previousChangeNumber, - previousChangeNumber == storedChangeNumber { - if let payload = notification.definition, let compressionType = notification.compressionType { - do { - let split = try self.payloadDecoder.decode( - payload: payload, - compressionUtil: self.decomProvider.decompressor(for: compressionType)) - let change = SplitChange(splits: [split], - since: previousChangeNumber, - till: notification.changeNumber) - Logger.v("Split update received: \(change)") - if self.splitsStorage.update(splitChange: self.splitChangeProcessor.process(change)) { - self.synchronizer.notifyFeatureFlagsUpdated() - } - self.telemetryProducer?.recordUpdatesFromSse(type: .splits) - return - - } catch { - Logger.e("Error decoding feature flags payload from notification: \(error)") - } + previousChangeNumber == storedChangeNumber, + let payload = notification.definition, + let compressionType = notification.compressionType { + + if processTargetingRuleUpdate(notification: notification, + payload: payload, + compressionType: compressionType, + previousChangeNumber: previousChangeNumber) { + return } } - self.synchronizer.synchronizeSplits(changeNumber: notification.changeNumber) + // If processing failed or there's no payload + fetchChanges(notificationType: notification.type, changeNumber: notification.changeNumber) + } + } + + private func getChangeNumber(_ notificationType: NotificationType) -> Int64 { + if notificationType == .splitUpdate { + return splitsStorage.changeNumber + } else if notificationType == .ruleBasedSegmentUpdate { + return ruleBasedSegmentsStorage.changeNumber + } else { + return -1 + } + } + + private func fetchChanges(notificationType: NotificationType, changeNumber: Int64) { + if notificationType == .ruleBasedSegmentUpdate { + synchronizer.synchronizeRuleBasedSegments(changeNumber: changeNumber) + } else { + synchronizer.synchronizeSplits(changeNumber: changeNumber) + } + } + + /// Process a targeting rule update notification and return true if successful + private func processTargetingRuleUpdate(notification: TargetingRuleUpdateNotification, + payload: String, + compressionType: CompressionType, + previousChangeNumber: Int64) -> Bool { + + switch notification.type { + case .splitUpdate: + return processSplitUpdate(payload: payload, + compressionType: compressionType, + previousChangeNumber: previousChangeNumber, + changeNumber: notification.changeNumber) + + case .ruleBasedSegmentUpdate: + return processRuleBasedSegmentUpdate(payload: payload, + compressionType: compressionType, + previousChangeNumber: previousChangeNumber, + changeNumber: notification.changeNumber) + + default: + return false + } + } + + /// Process a split update notification + private func processSplitUpdate(payload: String, + compressionType: CompressionType, + previousChangeNumber: Int64, + changeNumber: Int64) -> Bool { + do { + let split = try self.payloadDecoder.decode( + payload: payload, + compressionUtil: self.decomProvider.decompressor(for: compressionType)) + + if !allRuleBasedSegmentsExist(in: split) { + return false + } + + let change = SplitChange(splits: [split], + since: previousChangeNumber, + till: changeNumber) + + Logger.v("Split update received: \(change)") + + if self.splitsStorage.update(splitChange: self.splitChangeProcessor.process(change)) { + self.synchronizer.notifyFeatureFlagsUpdated() + } + + self.telemetryProducer?.recordUpdatesFromSse(type: .splits) + return true + } catch { + Logger.e("Error decoding feature flags payload from notification: \(error)") + return false + } + } + + /// Process a rule-based segment update notification + private func processRuleBasedSegmentUpdate(payload: String, + compressionType: CompressionType, + previousChangeNumber: Int64, + changeNumber: Int64) -> Bool { + do { + let rbs = try self.ruleBasedSegmentsPayloadDecoder.decode( + payload: payload, + compressionUtil: self.decomProvider.decompressor(for: compressionType)) + + let change = RuleBasedSegmentChange(segments: [rbs], + since: previousChangeNumber, + till: changeNumber) + + Logger.v("RBS update received: \(change)") + + let processedChange = ruleBasedSegmentsChangeProcessor.process(change) + + if self.ruleBasedSegmentsStorage.update(toAdd: processedChange.toAdd, + toRemove: processedChange.toRemove, + changeNumber: processedChange.changeNumber) { + self.synchronizer.notifyFeatureFlagsUpdated() + } + + self.telemetryProducer?.recordUpdatesFromSse(type: .splits) + return true + } catch { + Logger.e("Error decoding rule based segments payload from notification: \(error)") + return false + } + } + + /// Checks if the split contains a rule-based segment matcher whose segment does not exist in storage + private func allRuleBasedSegmentsExist(in split: Split) -> Bool { + guard let conditions = split.conditions else { return true } + let segmentNames = conditions + .compactMap { $0.matcherGroup?.matchers } + .flatMap { $0 } + .filter { $0.matcherType == .inRuleBasedSegment } + .compactMap { $0.userDefinedSegmentMatcherData?.segmentName } + + guard !segmentNames.isEmpty else { + return true + } + return segmentNames.allSatisfy { + self.ruleBasedSegmentsStorage.get(segmentName: $0) != nil } } } diff --git a/Split/Network/Sync/FeatureFlagsSynchronizer.swift b/Split/Network/Sync/FeatureFlagsSynchronizer.swift index 7139017e..5434c865 100644 --- a/Split/Network/Sync/FeatureFlagsSynchronizer.swift +++ b/Split/Network/Sync/FeatureFlagsSynchronizer.swift @@ -11,7 +11,7 @@ import Foundation protocol FeatureFlagsSynchronizer { func load() func synchronize() - func synchronize(changeNumber: Int64) + func synchronize(changeNumber: Int64?, rbsChangeNumber: Int64?) func startPeriodicSync() func stopPeriodicSync() func notifyKilled() @@ -27,7 +27,7 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { private var splitsSyncWorker: RetryableSyncWorker! private let splitsFilterQueryString: String private let flagsSpec: String - private let syncTaskByChangeNumberCatalog: ConcurrentDictionary + private let syncTaskByChangeNumberCatalog: ConcurrentDictionary private let syncWorkerFactory: SyncWorkerFactory private let splitConfig: SplitClientConfig private let splitEventsManager: SplitEventsManager @@ -39,8 +39,8 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { storageContainer: SplitStorageContainer, syncWorkerFactory: SyncWorkerFactory, broadcasterChannel: SyncEventBroadcaster, - syncTaskByChangeNumberCatalog: ConcurrentDictionary - = ConcurrentDictionary(), + syncTaskByChangeNumberCatalog: ConcurrentDictionary + = ConcurrentDictionary(), splitsFilterQueryString: String, flagsSpec: String, splitEventsManager: SplitEventsManager) { @@ -100,25 +100,31 @@ class DefaultFeatureFlagsSynchronizer: FeatureFlagsSynchronizer { splitsSyncWorker.start() } - func synchronize(changeNumber: Int64) { + func synchronize(changeNumber: Int64? = nil, rbsChangeNumber: Int64? = nil) { if isDestroyed.value || !splitConfig.syncEnabled { return } - if changeNumber <= storageContainer.splitsStorage.changeNumber { + if let changeNumber, changeNumber <= storageContainer.splitsStorage.changeNumber { return } - if syncTaskByChangeNumberCatalog.value(forKey: changeNumber) == nil { + if let rbsChangeNumber, rbsChangeNumber <= storageContainer.ruleBasedSegmentsStorage.changeNumber { + return + } + + let changeNumberConfig = SplitsUpdateChangeNumber(flags: changeNumber, rbs: rbsChangeNumber) + + if syncTaskByChangeNumberCatalog.value(forKey: changeNumberConfig) == nil { let reconnectBackoff = DefaultReconnectBackoffCounter(backoffBase: splitConfig.generalRetryBackoffBase) - var worker = syncWorkerFactory.createRetryableSplitsUpdateWorker(changeNumber: changeNumber, + var worker = syncWorkerFactory.createRetryableSplitsUpdateWorker(changeNumber: changeNumberConfig, reconnectBackoffCounter: reconnectBackoff) - syncTaskByChangeNumberCatalog.setValue(worker, forKey: changeNumber) + syncTaskByChangeNumberCatalog.setValue(worker, forKey: changeNumberConfig) worker.start() worker.completion = {[weak self] success in if let self = self, success { self.broadcasterChannel.push(event: .syncExecuted) - self.syncTaskByChangeNumberCatalog.removeValue(forKey: changeNumber) + self.syncTaskByChangeNumberCatalog.removeValue(forKey: changeNumberConfig) } } } diff --git a/Split/Network/Sync/SyncCommons.swift b/Split/Network/Sync/SyncCommons.swift index 2dafbba7..fb3313ae 100644 --- a/Split/Network/Sync/SyncCommons.swift +++ b/Split/Network/Sync/SyncCommons.swift @@ -24,8 +24,8 @@ struct SplitStorageContainer { let persistentHashedImpressionsStorage: PersistentHashedImpressionsStorage let hashedImpressionsStorage: HashedImpressionsStorage let generalInfoStorage: GeneralInfoStorage - let ruleBasedSegmentsStorage: RuleBasedSegmentsStorage? - let persistentRuleBasedSegmentsStorage: PersistentRuleBasedSegmentsStorage? + let ruleBasedSegmentsStorage: RuleBasedSegmentsStorage + let persistentRuleBasedSegmentsStorage: PersistentRuleBasedSegmentsStorage } protocol ImpressionLogger { diff --git a/Split/Network/Sync/SyncManagerBuilder.swift b/Split/Network/Sync/SyncManagerBuilder.swift index ea4bf70a..0c33b452 100644 --- a/Split/Network/Sync/SyncManagerBuilder.swift +++ b/Split/Network/Sync/SyncManagerBuilder.swift @@ -128,13 +128,16 @@ class SyncManagerBuilder { telemetryProducer: storageContainer.telemetryStorage, resource: .myLargeSegments) - return DefaultSseNotificationProcessor( + return DefaultSseNotificationProcessor( notificationParser: DefaultSseNotificationParser(), splitsUpdateWorker: SplitsUpdateWorker( synchronizer: synchronizer, splitsStorage: storageContainer.splitsStorage, + ruleBasedSegmentsStorage: storageContainer.ruleBasedSegmentsStorage, splitChangeProcessor: DefaultSplitChangeProcessor(filterBySet: splitConfig?.bySetsFilter()), - featureFlagsPayloadDecoder: DefaultFeatureFlagsPayloadDecoder(), + ruleBasedSegmentsChangeProcessor: DefaultRuleBasedSegmentChangeProcessor(), + featureFlagsPayloadDecoder: DefaultFeatureFlagsPayloadDecoder(type: Split.self), + ruleBasedSegmentsPayloadDecoder: DefaultRuleBasedSegmentsPayloadDecoder(type: RuleBasedSegment.self), telemetryProducer: storageContainer.telemetryStorage), splitKillWorker: SplitKillWorker(synchronizer: synchronizer, splitsStorage: storageContainer.splitsStorage), diff --git a/Split/Network/Sync/Synchronizer.swift b/Split/Network/Sync/Synchronizer.swift index 4a8320fc..d3155a19 100644 --- a/Split/Network/Sync/Synchronizer.swift +++ b/Split/Network/Sync/Synchronizer.swift @@ -19,6 +19,7 @@ protocol Synchronizer: ImpressionLogger { func loadAttributesFromCache(forKey key: String) func syncAll() func synchronizeSplits(changeNumber: Int64) + func synchronizeRuleBasedSegments(changeNumber: Int64) func synchronizeMySegments(forKey key: String) func synchronizeTelemetryConfig() func forceMySegmentsSync(forKey key: String, changeNumbers: SegmentsChangeNumber, delay: Int64) @@ -123,7 +124,13 @@ class DefaultSynchronizer: Synchronizer { func synchronizeSplits(changeNumber: Int64) { runIfSyncEnabled { - self.featureFlagsSynchronizer.synchronize(changeNumber: changeNumber) + self.featureFlagsSynchronizer.synchronize(changeNumber: changeNumber, rbsChangeNumber: nil) + } + } + + func synchronizeRuleBasedSegments(changeNumber: Int64) { + runIfSyncEnabled { + self.featureFlagsSynchronizer.synchronize(changeNumber: nil, rbsChangeNumber: changeNumber) } } diff --git a/Split/Storage/RuleBasedSegments/PersistentRuleBasedSegmentsStorage.swift b/Split/Storage/RuleBasedSegments/PersistentRuleBasedSegmentsStorage.swift index d3f0ee1f..5facb725 100644 --- a/Split/Storage/RuleBasedSegments/PersistentRuleBasedSegmentsStorage.swift +++ b/Split/Storage/RuleBasedSegments/PersistentRuleBasedSegmentsStorage.swift @@ -12,6 +12,7 @@ protocol PersistentRuleBasedSegmentsStorage { func getSnapshot() -> RuleBasedSegmentsSnapshot func update(toAdd: Set, toRemove: Set, changeNumber: Int64) func clear() + func getChangeNumber() -> Int64 } class DefaultPersistentRuleBasedSegmentsStorage: PersistentRuleBasedSegmentsStorage { @@ -50,4 +51,8 @@ class DefaultPersistentRuleBasedSegmentsStorage: PersistentRuleBasedSegmentsStor generalInfoStorage.setRuleBasedSegmentsChangeNumber(changeNumber: -1) ruleBasedSegmentDao.deleteAll() } + + func getChangeNumber() -> Int64 { + return generalInfoStorage.getRuleBasedSegmentsChangeNumber() + } } diff --git a/Split/Storage/RuleBasedSegments/RuleBasedSegment.swift b/Split/Storage/RuleBasedSegments/RuleBasedSegment.swift index 35238258..50e01485 100644 --- a/Split/Storage/RuleBasedSegments/RuleBasedSegment.swift +++ b/Split/Storage/RuleBasedSegments/RuleBasedSegment.swift @@ -8,9 +8,94 @@ import Foundation +// MARK: - SegmentType Enum +@objc enum SegmentType: Int, Codable { + case standard + case ruleBased + case large + case unknown + + static func enumFromString(string: String) -> SegmentType? { + switch string.lowercased() { + case "standard": + return .standard + case "rule-based": + return .ruleBased + case "large": + return .large + default: + return nil + } + } + + public init(from decoder: Decoder) throws { + let stringValue = try? decoder.singleValueContainer().decode(String.self) + self = SegmentType.enumFromString(string: stringValue ?? "") ?? .unknown + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .standard: + try container.encode("standard") + case .ruleBased: + try container.encode("rule-based") + case .large: + try container.encode("large") + case .unknown: + try container.encodeNil() + } + } +} + +class ExcludedSegment: NSObject, Codable { + var name: String? + var type: SegmentType? + + enum CodingKeys: String, CodingKey { + case name + case type + } + + override init() { + super.init() + } + + init(name: String? = nil, type: SegmentType? = nil) { + self.name = name + self.type = type + super.init() + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decodeIfPresent(SegmentType.self, forKey: .type) + name = try container.decodeIfPresent(String.self, forKey: .name) + super.init() + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(type, forKey: .type) + try container.encodeIfPresent(name, forKey: .name) + } + + func isStandard() -> Bool { + return type == .standard + } + + func isRuleBased() -> Bool { + return type == .ruleBased + } + + func isLarge() -> Bool { + return type == .large + } +} + class Excluded: NSObject, Codable { var keys: Set? - var segments: Set? + var segments: Set? enum CodingKeys: String, CodingKey { case keys @@ -21,7 +106,7 @@ class Excluded: NSObject, Codable { super.init() } - init(keys: Set? = nil, segments: Set? = nil) { + init(keys: Set? = nil, segments: Set? = nil) { self.keys = keys self.segments = segments super.init() @@ -30,7 +115,7 @@ class Excluded: NSObject, Codable { required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) keys = try container.decodeIfPresent(Set.self, forKey: .keys) - segments = try container.decodeIfPresent(Set.self, forKey: .segments) + segments = try container.decodeIfPresent(Set.self, forKey: .segments) super.init() } diff --git a/SplitTests/Fake/Network/RestClientStub.swift b/SplitTests/Fake/Network/RestClientStub.swift index 4d767011..c48f9c0e 100644 --- a/SplitTests/Fake/Network/RestClientStub.swift +++ b/SplitTests/Fake/Network/RestClientStub.swift @@ -11,8 +11,8 @@ import XCTest protocol RestClientTest { func update(segments: [AllSegmentsChange]?) - func update(change: SplitChange?) - func update(changes: [SplitChange]) + func update(change: TargetingRulesChange?) + func update(changes: [TargetingRulesChange]) func update(response: SseAuthenticationResponse?) func updateFailedSseAuth(error: Error) } @@ -21,7 +21,7 @@ class RestClientStub: SplitApiRestClient { private var sseAuthResult: DataResult? private var segments: [AllSegmentsChange]? private var largeSegments: [SegmentChange]? - private var splitChanges: [SplitChange] = [] + private var splitChanges: [TargetingRulesChange] = [] private var sendTrackEventsCount = 0 private var sendImpressionsCount = 0 private var sendImpressionsCountCount = 0 @@ -55,29 +55,24 @@ extension RestClientStub: RestClient { } extension RestClientStub: RestClientSplitChanges { - func getSplitChanges(since: Int64, till: Int64?, headers: HttpHeaders?, completion: @escaping (DataResult) -> Void) { + func getSplitChanges(since: Int64, rbSince: Int64?, till: Int64?, headers: HttpHeaders?, completion: @escaping (DataResult) -> Void) { if splitChanges.count == 0 { completion(DataResult.success(value: nil)) return } + + let rbSince = rbSince ?? -1 let hit = splitChangeHitIndex splitChangeHitIndex += 1 if hit <= splitChanges.count - 1 { let splitChange = splitChanges[hit] - let targetingRulesChange = TargetingRulesChange( - featureFlags: splitChange, - ruleBasedSegments: RuleBasedSegmentChange(segments: [], since: -1, till: -1) - ) + let targetingRulesChange = splitChange completion(DataResult.success(value: targetingRulesChange)) return } - + let splitChange = splitChanges[splitChanges.count - 1] - let targetingRulesChange = TargetingRulesChange( - featureFlags: splitChange, - ruleBasedSegments: RuleBasedSegmentChange(segments: [], since: -1, till: -1) - ) - completion(DataResult.success(value: targetingRulesChange)) + completion(DataResult.success(value: splitChange)) } } @@ -147,7 +142,7 @@ extension RestClientStub: RestClientUniqueKeys { } extension RestClientStub: RestClientTest { - func update(changes: [SplitChange]) { + func update(changes: [TargetingRulesChange]) { self.splitChanges = changes } @@ -159,7 +154,7 @@ extension RestClientStub: RestClientTest { self.largeSegments = largeSegments } - func update(change: SplitChange?) { + func update(change: TargetingRulesChange?) { if let change = change { self.splitChanges.append(change) diff --git a/SplitTests/Fake/Service/FeatureFlagsPayloadDecoderMock.swift b/SplitTests/Fake/Service/FeatureFlagsPayloadDecoderMock.swift index 34afef5b..7f1e11b2 100644 --- a/SplitTests/Fake/Service/FeatureFlagsPayloadDecoderMock.swift +++ b/SplitTests/Fake/Service/FeatureFlagsPayloadDecoderMock.swift @@ -9,9 +9,22 @@ import Foundation @testable import Split -class FeatureFlagsPayloadDecoderMock: FeatureFlagsPayloadDecoder { +class FeatureFlagsPayloadDecoderMock: DefaultTargetingRulePayloadDecoder { let helper = SplitHelper() - func decode(payload: String, compressionUtil: CompressionUtil) throws -> Split { + override func decode(payload: String, compressionUtil: CompressionUtil) throws -> Split { return helper.createDefaultSplit(named: "dummy_split") } } + +class RuleBasedSegmentsPayloadDecoderMock: DefaultTargetingRulePayloadDecoder { + let helper = SplitHelper() + override func decode(payload: String, compressionUtil: CompressionUtil) throws -> RuleBasedSegment { + let rbs = RuleBasedSegment() + rbs.name = "dummy_rbs" + rbs.isParsed = true + rbs.trafficTypeName = "custom" + rbs.status = .active + rbs.json = "" + return rbs + } +} diff --git a/SplitTests/Fake/Service/HttpSplitFetcherStub.swift b/SplitTests/Fake/Service/HttpSplitFetcherStub.swift index 8d2eb422..ca8eb312 100644 --- a/SplitTests/Fake/Service/HttpSplitFetcherStub.swift +++ b/SplitTests/Fake/Service/HttpSplitFetcherStub.swift @@ -10,12 +10,16 @@ import Foundation @testable import Split class HttpSplitFetcherStub: HttpSplitFetcher { - var splitChanges = [SplitChange?]() + var splitChanges = [TargetingRulesChange?]() var httpError: HttpError? var hitIndex = 0 var fetchCallCount: Int = 0 + var params: [String: Int64?] = [:] - func execute(since: Int64, till: Int64?, headers: HttpHeaders?) throws -> SplitChange { + func execute(since: Int64, rbSince: Int64? = nil, till: Int64?, headers: HttpHeaders?) throws -> TargetingRulesChange { + params = ["since": since, + "rbSince": rbSince, + "till": till] fetchCallCount+=1 if let e = httpError { throw e @@ -26,6 +30,7 @@ class HttpSplitFetcherStub: HttpSplitFetcher { throw GenericError.unknown(message: "null feature flag changes") } + let rbSince = rbSince ?? -1 if splitChanges.count > hit { if let change = splitChanges[hit] { return change @@ -40,15 +45,4 @@ class HttpSplitFetcherStub: HttpSplitFetcher { throw GenericError.unknown(message: "null split changes") } } - - func executeForTargetingRules(since: Int64, till: Int64?, headers: HttpHeaders?) throws -> TargetingRulesChange { - // Reuse the existing execute method to get the SplitChange - let splitChange = try execute(since: since, till: till, headers: headers) - - // Create a TargetingRulesChange with the SplitChange as feature flags and empty rule-based segments - return TargetingRulesChange( - featureFlags: splitChange, - ruleBasedSegments: RuleBasedSegmentChange(segments: [], since: -1, till: -1) - ) - } } diff --git a/SplitTests/Fake/Service/RuleBasedSegmentChangeProcessorStub.swift b/SplitTests/Fake/Service/RuleBasedSegmentChangeProcessorStub.swift new file mode 100644 index 00000000..bfec09c6 --- /dev/null +++ b/SplitTests/Fake/Service/RuleBasedSegmentChangeProcessorStub.swift @@ -0,0 +1,19 @@ +// +// RuleBasedSegmentChangeProcessorStub.swift +// Split +// +// Copyright © 2025 Split. All rights reserved. +// + +import Foundation +@testable import Split + +class RuleBasedSegmentChangeProcessorStub: RuleBasedSegmentChangeProcessor { + var processedRuleBasedSegmentsChange: ProcessedRuleBasedSegmentChange = ProcessedRuleBasedSegmentChange(activeSegments: [], archivedSegments: [], changeNumber: -1, updateTimestamp: -1) + + var segmentChange: RuleBasedSegmentChange? + func process(_ segmentChange: RuleBasedSegmentChange) -> ProcessedRuleBasedSegmentChange { + self.segmentChange = segmentChange + return processedRuleBasedSegmentsChange + } +} diff --git a/SplitTests/Fake/Storage/PersistentRuleBasedSegmentsStorageStub.swift b/SplitTests/Fake/Storage/PersistentRuleBasedSegmentsStorageStub.swift index ca888fe7..0ffeee90 100644 --- a/SplitTests/Fake/Storage/PersistentRuleBasedSegmentsStorageStub.swift +++ b/SplitTests/Fake/Storage/PersistentRuleBasedSegmentsStorageStub.swift @@ -13,6 +13,7 @@ class PersistentRuleBasedSegmentsStorageStub: PersistentRuleBasedSegmentsStorage var updateCalled = false var clearCalled = false + var changeNumberCalled = false var lastAddedSegments: Set? var lastRemovedSegments: Set? @@ -53,4 +54,9 @@ class PersistentRuleBasedSegmentsStorageStub: PersistentRuleBasedSegmentsStorage delegate?.clear() } + + func getChangeNumber() -> Int64 { + changeNumberCalled = true + return delegate?.getChangeNumber() ?? -1 + } } diff --git a/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift b/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift index 6695c3cf..8e01f5f8 100644 --- a/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift +++ b/SplitTests/Fake/Streaming/FeatureFlagsSynchronizerStub.swift @@ -20,7 +20,7 @@ class FeatureFlagsSynchronizerStub: FeatureFlagsSynchronizer { } var synchronizeNumberCalled = false - func synchronize(changeNumber: Int64) { + func synchronize(changeNumber: Int64?, rbsChangeNumber: Int64?) { synchronizeNumberCalled = true } diff --git a/SplitTests/Fake/Streaming/SseNotificationParserStub.swift b/SplitTests/Fake/Streaming/SseNotificationParserStub.swift index eca0c186..249b97b6 100644 --- a/SplitTests/Fake/Streaming/SseNotificationParserStub.swift +++ b/SplitTests/Fake/Streaming/SseNotificationParserStub.swift @@ -13,7 +13,8 @@ class SseNotificationParserStub: SseNotificationParser { var incomingNotification: IncomingNotification? - var splitsUpdateNotification: SplitsUpdateNotification? + var splitsUpdateNotification: TargetingRuleUpdateNotification? + var ruleBasedSegmentUpdateNotification: TargetingRuleUpdateNotification? var splitKillNotification: SplitKillNotification? var membershipsUpdateNotification: MembershipsUpdateNotification? var occupancyNotification: OccupancyNotification? @@ -25,8 +26,12 @@ class SseNotificationParserStub: SseNotificationParser { return incomingNotification } - func parseSplitUpdate(jsonString: String) throws -> SplitsUpdateNotification { - return splitsUpdateNotification! + func parseTargetingRuleNotification(jsonString: String, type: NotificationType) throws -> TargetingRuleUpdateNotification { + if type == .splitUpdate { + return splitsUpdateNotification! + } else { + return ruleBasedSegmentUpdateNotification! + } } func parseSplitKill(jsonString: String) throws -> SplitKillNotification { diff --git a/SplitTests/Fake/Streaming/SynchronizerSpy.swift b/SplitTests/Fake/Streaming/SynchronizerSpy.swift index da1b1011..fb6e5935 100644 --- a/SplitTests/Fake/Streaming/SynchronizerSpy.swift +++ b/SplitTests/Fake/Streaming/SynchronizerSpy.swift @@ -20,6 +20,7 @@ class SynchronizerSpy: Synchronizer { var syncAllCalled = false var synchronizeSplitsCalled = false var synchronizeSplitsChangeNumberCalled = false + var synchronizeRuleBasedSegmentsCalled = false var synchronizeMySegmentsCalled = false var synchronizeMyLargeSegmentsCalled = false var forceMySegmentsCalledCount = 0 @@ -36,6 +37,7 @@ class SynchronizerSpy: Synchronizer { var syncSplitsExp: XCTestExpectation? var syncSplitsChangeNumberExp: XCTestExpectation? + var syncRuleBasedSegmentsExp: XCTestExpectation? var startPeriodicFetchingExp: XCTestExpectation? var stopPeriodicFetchingExp: XCTestExpectation? @@ -177,6 +179,14 @@ class SynchronizerSpy: Synchronizer { } } + func synchronizeRuleBasedSegments(changeNumber: Int64) { + synchronizeRuleBasedSegmentsCalled = true + splitSynchronizer.synchronizeRuleBasedSegments(changeNumber: changeNumber) + if let exp = syncRuleBasedSegmentsExp { + exp.fulfill() + } + } + func pause() { splitSynchronizer.pause() } diff --git a/SplitTests/Fake/Streaming/SynchronizerStub.swift b/SplitTests/Fake/Streaming/SynchronizerStub.swift index f5bfb64a..4bcdba66 100644 --- a/SplitTests/Fake/Streaming/SynchronizerStub.swift +++ b/SplitTests/Fake/Streaming/SynchronizerStub.swift @@ -27,6 +27,7 @@ class SynchronizerStub: Synchronizer { var syncAllCalled = false var synchronizeSplitsCalled = false var synchronizeSplitsChangeNumberCalled = false + var synchronizeRuleBasedSegmentsCalled = false var synchronizeMySegmentsCalled = false var synchronizeMyLargeSegmentsCalled = false var forceMySegmentsSyncCalled = false @@ -46,6 +47,7 @@ class SynchronizerStub: Synchronizer { var syncSplitsExp: XCTestExpectation? var syncSplitsChangeNumberExp: XCTestExpectation? + var syncRuleBasedSegmentsExp: XCTestExpectation? var syncMySegmentsExp: XCTestExpectation? var syncMyLargeSegmentsExp: XCTestExpectation? var forceMySegmentsSyncExp = [String: XCTestExpectation]() @@ -201,6 +203,13 @@ class SynchronizerStub: Synchronizer { } } + func synchronizeRuleBasedSegments(changeNumber: Int64) { + synchronizeRuleBasedSegmentsCalled = true + if let exp = syncRuleBasedSegmentsExp { + exp.fulfill() + } + } + func synchronizeMySegments() { synchronizeMySegmentsCalled = true if let exp = syncMySegmentsExp { diff --git a/SplitTests/Fake/Streaming/UpdateWorkerMock.swift b/SplitTests/Fake/Streaming/UpdateWorkerMock.swift index fb864f98..94774466 100644 --- a/SplitTests/Fake/Streaming/UpdateWorkerMock.swift +++ b/SplitTests/Fake/Streaming/UpdateWorkerMock.swift @@ -12,7 +12,7 @@ import Foundation class SplitsUpdateWorkerMock: SplitsUpdateWorker { var processCalled = false var throwException = false - override func process(notification: SplitsUpdateNotification) throws { + override func process(notification: TargetingRuleUpdateNotification) throws { if throwException { throw GenericError.unknown(message: "") } diff --git a/SplitTests/Fake/SyncWorkerFactoryStub.swift b/SplitTests/Fake/SyncWorkerFactoryStub.swift index a9769500..71987914 100644 --- a/SplitTests/Fake/SyncWorkerFactoryStub.swift +++ b/SplitTests/Fake/SyncWorkerFactoryStub.swift @@ -30,7 +30,7 @@ class SyncWorkerFactoryStub: SyncWorkerFactory { private var retryableWorkerIndex = -1 var retryableSplitsUpdateWorkers: [RetryableSyncWorker] = [RetryableSyncWorkerStub()] - func createRetryableSplitsUpdateWorker(changeNumber: Int64, + func createRetryableSplitsUpdateWorker(changeNumber: SplitsUpdateChangeNumber, reconnectBackoffCounter: ReconnectBackoffCounter ) -> RetryableSyncWorker { diff --git a/SplitTests/Helpers/TestSplitFactory.swift b/SplitTests/Helpers/TestSplitFactory.swift index 947c84e4..b6d945fc 100644 --- a/SplitTests/Helpers/TestSplitFactory.swift +++ b/SplitTests/Helpers/TestSplitFactory.swift @@ -122,6 +122,7 @@ class TestSplitFactory: SplitFactory { apiFacade: apiFacade, storageContainer: storageContainer, splitChangeProcessor: DefaultSplitChangeProcessor(filterBySet: nil), + ruleBasedSegmentChangeProcessor: DefaultRuleBasedSegmentChangeProcessor(), eventsManager: eventsManager) let impressionsTracker = DefaultImpressionsTracker(splitConfig: splitConfig, diff --git a/SplitTests/Helpers/TestingData.swift b/SplitTests/Helpers/TestingData.swift index bc15b7df..8fbe93ec 100644 --- a/SplitTests/Helpers/TestingData.swift +++ b/SplitTests/Helpers/TestingData.swift @@ -273,12 +273,12 @@ struct TestingData { return "" } - static func updateSplitsNotificationZlib() -> SplitsUpdateNotification { - return try! Json.decodeFrom(json: kUpdateSplitsNotificationZlib, to: SplitsUpdateNotification.self) + static func updateSplitsNotificationZlib() -> TargetingRuleUpdateNotification { + return try! Json.decodeFrom(json: kUpdateSplitsNotificationZlib, to: TargetingRuleUpdateNotification.self) } - static func updateSplitsNotificationGzip() -> SplitsUpdateNotification { - return try! Json.decodeFrom(json: kUpdateSplitsNotificationGzip, to: SplitsUpdateNotification.self) + static func updateSplitsNotificationGzip() -> TargetingRuleUpdateNotification { + return try! Json.decodeFrom(json: kUpdateSplitsNotificationGzip, to: TargetingRuleUpdateNotification.self) } static func membershipsNotificationUnboundedMessage(type: NotificationType, cn: Int64? = nil, delay: Int = 100) -> String { diff --git a/SplitTests/HttpClient/HttpClientTest.swift b/SplitTests/HttpClient/HttpClientTest.swift index b6490c1a..567074fb 100644 --- a/SplitTests/HttpClient/HttpClientTest.swift +++ b/SplitTests/HttpClient/HttpClientTest.swift @@ -60,7 +60,11 @@ class HttpClientTest: XCTestCase { var splitsChange: SplitChange? = nil let dummyChanges = Data(IntegrationHelper.emptySplitChanges(since: 1, till: 2).utf8) let expectation = XCTestExpectation(description: "complete req") - _ = try httpClient.sendRequest(endpoint: factory.splitChangesEndpoint, parameters: HttpParameters([HttpParameter(key: "s", value: "2.2"), HttpParameter(key: "since", value: 100)])).getResponse(completionHandler: { response in + _ = try httpClient.sendRequest(endpoint: factory.splitChangesEndpoint, + parameters: HttpParameters([ + HttpParameter(key: "s", value: "2.2"), + HttpParameter(key: "since", value: 100), + HttpParameter(key: "rbSince", value: 120)])).getResponse(completionHandler: { response in splitsChange = try? response.result.value?.decode(TargetingRulesChange.self)?.featureFlags expectation.fulfill() }, errorHandler: { error in }) @@ -74,7 +78,7 @@ class HttpClientTest: XCTestCase { XCTAssertEqual(1, splitsChange?.since) XCTAssertEqual(2, splitsChange?.till) XCTAssertFalse(requestManager.request.method.isUpload) - XCTAssertEqual("https://sdk.split.io/api/splitChanges?s=2.2&since=100", requestManager.request.url.absoluteString) + XCTAssertEqual("https://sdk.split.io/api/splitChanges?s=2.2&since=100&rbSince=120", requestManager.request.url.absoluteString) } func testDataRequestError() throws { diff --git a/SplitTests/Integration/Api/FlagSetsIntegrationTest.swift b/SplitTests/Integration/Api/FlagSetsIntegrationTest.swift index 94a0b137..7c2b513d 100644 --- a/SplitTests/Integration/Api/FlagSetsIntegrationTest.swift +++ b/SplitTests/Integration/Api/FlagSetsIntegrationTest.swift @@ -166,7 +166,7 @@ class FlagSetsIntegrationTests: XCTestCase { try bodyTest(syncConfig: syncConfig) - XCTAssertEqual("since=-1&sets=set_2,set_3,set_ww,set_x", querystring) + XCTAssertEqual("since=-1&rbSince=-1&sets=set_2,set_3,set_ww,set_x", querystring) } func testInitialQuerystringWithSpec() throws { @@ -178,7 +178,7 @@ class FlagSetsIntegrationTests: XCTestCase { try bodyTest(syncConfig: syncConfig) - XCTAssertEqual("s=1.1&since=-1&sets=set_2,set_3,set_ww,set_x", querystring) + XCTAssertEqual("s=1.1&since=-1&rbSince=-1&sets=set_2,set_3,set_ww,set_x", querystring) } func testTotalAndInvalidFlagSetsTelemetry() throws { @@ -193,7 +193,7 @@ class FlagSetsIntegrationTests: XCTestCase { wait(for: [telemetryConfigExp!], timeout: 3) - XCTAssertEqual("since=-1&sets=a,c,d", querystring) + XCTAssertEqual("since=-1&rbSince=-1&sets=a,c,d", querystring) XCTAssertEqual(7, telemetryConfigSent?.flagSetsTotal ?? -1) XCTAssertEqual(4, telemetryConfigSent?.flagSetsInvalid ?? -1) } @@ -223,7 +223,7 @@ class FlagSetsIntegrationTests: XCTestCase { destroyTest(client: client) - XCTAssertEqual("since=-1&sets=c,nset1,nset2,set1,set10,set2,set20", querystring) + XCTAssertEqual("since=-1&rbSince=-1&sets=c,nset1,nset2,set1,set10,set2,set20", querystring) XCTAssertEqual(expLatencies, telemetryStatsSent?.methodLatencies?.treatmentsByFlagSet ?? []) XCTAssertEqual(expLatencies, telemetryStatsSent?.methodLatencies?.treatmentsByFlagSets ?? []) XCTAssertEqual(expLatencies, telemetryStatsSent?.methodLatencies?.treatmentsWithConfigByFlagSet ?? []) diff --git a/SplitTests/Integration/Sync/CdnByPassTest.swift b/SplitTests/Integration/Sync/CdnByPassTest.swift index 6b413855..9a73557f 100644 --- a/SplitTests/Integration/Sync/CdnByPassTest.swift +++ b/SplitTests/Integration/Sync/CdnByPassTest.swift @@ -53,12 +53,12 @@ class CdnByPassTest: XCTestCase { func testInit() { Spec.flagsSpec = "1.1" - performTest(expectedRequestUrl: "https://sdk.split.io/api/splitChanges?s=1.1&since=1200&sets=c,nset1,nset2&till=1200") + performTest(expectedRequestUrl: "https://sdk.split.io/api/splitChanges?s=1.1&since=1200&rbSince=-1&sets=c,nset1,nset2&till=1200") } func testInitWithoutSpec() { Spec.flagsSpec = "" - performTest(expectedRequestUrl: "https://sdk.split.io/api/splitChanges?since=1200&sets=c,nset1,nset2&till=1200") + performTest(expectedRequestUrl: "https://sdk.split.io/api/splitChanges?since=1200&rbSince=-1&sets=c,nset1,nset2&till=1200") } private func performTest(expectedRequestUrl: String) { diff --git a/SplitTests/Integration/Sync/FetchSpecificSplitsTest.swift b/SplitTests/Integration/Sync/FetchSpecificSplitsTest.swift index 33ad1b2e..6cbac79e 100644 --- a/SplitTests/Integration/Sync/FetchSpecificSplitsTest.swift +++ b/SplitTests/Integration/Sync/FetchSpecificSplitsTest.swift @@ -18,7 +18,7 @@ class FetchSpecificSplitsTest: XCTestCase { var httpClient: HttpClient! var streamingBinding: TestStreamResponseBinding? - var splitChange: SplitChange? + var splitChange: TargetingRulesChange? var serverUrl = "localhost" var splitsRequestUrl = "localhost" var lastChangeNumber = 0 @@ -39,21 +39,21 @@ class FetchSpecificSplitsTest: XCTestCase { .addSplitFilter(SplitFilter.byName(["s1", "s2", "s3"])) .addSplitFilter(SplitFilter.byPrefix(["s1", "s2", "s3"])) .build() - urlQueryStringTest(syncConfig: syncConfig, expectedResult: "/splitChanges?s=\(Spec.flagsSpec)&since=-1&names=s1,s2,s3&prefixes=s1,s2,s3") + urlQueryStringTest(syncConfig: syncConfig, expectedResult: "/splitChanges?s=\(Spec.flagsSpec)&since=-1&rbSince=-1&names=s1,s2,s3&prefixes=s1,s2,s3") } func testByNamesFilter() { let syncConfig = SyncConfig.builder() .addSplitFilter(SplitFilter.byName(["s1", "s2", "s3"])) .build() - urlQueryStringTest(syncConfig: syncConfig, expectedResult: "/splitChanges?s=\(Spec.flagsSpec)&since=-1&names=s1,s2,s3") + urlQueryStringTest(syncConfig: syncConfig, expectedResult: "/splitChanges?s=\(Spec.flagsSpec)&since=-1&rbSince=-1&names=s1,s2,s3") } func testByPrefixFilter() { let syncConfig = SyncConfig.builder() .addSplitFilter(SplitFilter.byPrefix(["s1", "s2", "s3"])) .build() - urlQueryStringTest(syncConfig: syncConfig, expectedResult: "/splitChanges?s=\(Spec.flagsSpec)&since=-1&prefixes=s1,s2,s3") + urlQueryStringTest(syncConfig: syncConfig, expectedResult: "/splitChanges?s=\(Spec.flagsSpec)&since=-1&rbSince=-1&prefixes=s1,s2,s3") } @@ -102,14 +102,14 @@ class FetchSpecificSplitsTest: XCTestCase { factory = nil } - private func loadSplitsChangeFile() -> SplitChange? { + private func loadSplitsChangeFile() -> TargetingRulesChange? { return loadSplitChangeFile(name: "splitchanges_1") } - private func loadSplitChangeFile(name fileName: String) -> SplitChange? { + private func loadSplitChangeFile(name fileName: String) -> TargetingRulesChange? { if let file = FileHelper.readDataFromFile(sourceClass: self, name: fileName, type: "json"), - let change = try? Json.decodeFrom(json: file, to: SplitChange.self) { - self.lastChangeNumber = Int(change.till) + let change: TargetingRulesChange = try? Json.decodeFrom(json: file, to: TargetingRulesChange.self) { + self.lastChangeNumber = Int(change.featureFlags.till) return change } return nil @@ -119,7 +119,6 @@ class FetchSpecificSplitsTest: XCTestCase { private func buildTestDispatcher() -> HttpClientTestDispatcher { return { request in if request.isSplitEndpoint() { - //self.splitsRequestUrl = String(request.url.absoluteString.suffix(request.url.absoluteString.count - 17)) self.splitsRequestUrl = String(request.url.absoluteString) let since = self.lastChangeNumber return TestDispatcherResponse(code: 200, data: Data(IntegrationHelper.emptySplitChanges(since: since, till: since).utf8)) diff --git a/SplitTests/Matcher/InRuleBasedSegmentMatcherTest.swift b/SplitTests/Matcher/InRuleBasedSegmentMatcherTest.swift index 646d4e7c..ed6e9bc2 100644 --- a/SplitTests/Matcher/InRuleBasedSegmentMatcherTest.swift +++ b/SplitTests/Matcher/InRuleBasedSegmentMatcherTest.swift @@ -76,7 +76,7 @@ class InRuleBasedSegmentMatcherTest: XCTestCase { func testEvaluateWithKeyInExcludedSegments() { let segment = createSegment(name: "segment1") segment.excluded = Excluded() - segment.excluded?.segments = ["excluded_segment"] + segment.excluded?.segments = [ExcludedSegment(name: "excluded_segment", type: .standard)] ruleBasedSegmentsStorage.segments["segment1"] = segment mySegmentsStorage.segments["key1"] = Set(["excluded_segment"]) diff --git a/SplitTests/Service/Splits/HttpSplitFetcherTests.swift b/SplitTests/Service/Splits/HttpSplitFetcherTests.swift index 458b9be8..1df27e10 100644 --- a/SplitTests/Service/Splits/HttpSplitFetcherTests.swift +++ b/SplitTests/Service/Splits/HttpSplitFetcherTests.swift @@ -12,7 +12,7 @@ import XCTest @testable import Split class HttpSplitFetcherTests: XCTestCase { - + var restClient: RestClientStub! var fetcher: HttpSplitFetcher! var telemetryProducer: TelemetryStorageStub! @@ -23,31 +23,31 @@ class HttpSplitFetcherTests: XCTestCase { fetcher = DefaultHttpSplitFetcher(restClient: restClient, syncHelper: DefaultSyncHelper(telemetryProducer: telemetryProducer)) } - + func testServerNoReachable() { restClient.isServerAvailable = false var isError = false do { - let _ = try fetcher.execute(since: 1, till: nil, headers: nil) + let _ = try fetcher.execute(since: 1, rbSince: 2, till: nil, headers: nil) } catch { isError = true } XCTAssertTrue(isError) } - + func testSuccessFullFetch() throws { restClient.isServerAvailable = true restClient.update(changes: [newChange(since: 1, till: 2), newChange(since: 2, till: 2)]) - let c = try fetcher.execute(since: 1, till: nil, headers: nil) + let c = try fetcher.execute(since: 1, rbSince: 2, till: nil, headers: nil).featureFlags XCTAssertEqual(1, c.since) XCTAssertEqual(2, c.till) XCTAssertEqual(0, c.splits.count) } - - func newChange(since: Int64, till: Int64, splits: [Split] = []) -> SplitChange { - return SplitChange(splits: [], since: since, till: till) + + func newChange(since: Int64, till: Int64, splits: [Split] = []) -> TargetingRulesChange { + return TargetingRulesChange(featureFlags: SplitChange(splits: [], since: since, till: till), ruleBasedSegments: RuleBasedSegmentChange(segments: [], since: -1, till: -1)) } override func tearDown() { diff --git a/SplitTests/Service/Splits/PeriodicSplitsSyncWorkerTest.swift b/SplitTests/Service/Splits/PeriodicSplitsSyncWorkerTest.swift index 217cb12a..47ac8976 100644 --- a/SplitTests/Service/Splits/PeriodicSplitsSyncWorkerTest.swift +++ b/SplitTests/Service/Splits/PeriodicSplitsSyncWorkerTest.swift @@ -15,15 +15,19 @@ class PeriodicSplitsSyncWorkerTest: XCTestCase { var splitFetcher: HttpSplitFetcherStub! var splitsStorage: SplitsStorageStub! + var ruleBasedSegmentsStorage: RuleBasedSegmentsStorageStub! var eventsManager: SplitEventsManagerMock! var backoffCounter: ReconnectBackoffCounterStub! var splitsSyncWorker: PeriodicSplitsSyncWorker! var splitChangeProcessor: SplitChangeProcessorStub! + var ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessorStub! override func setUp() { splitFetcher = HttpSplitFetcherStub() splitsStorage = SplitsStorageStub() + ruleBasedSegmentsStorage = RuleBasedSegmentsStorageStub() splitChangeProcessor = SplitChangeProcessorStub() + ruleBasedSegmentChangeProcessor = RuleBasedSegmentChangeProcessorStub() eventsManager = SplitEventsManagerMock() backoffCounter = ReconnectBackoffCounterStub() eventsManager.isSplitsReadyFired = false @@ -35,7 +39,9 @@ class PeriodicSplitsSyncWorkerTest: XCTestCase { let timer = PeriodicTimerStub() splitsSyncWorker = PeriodicSplitsSyncWorker(splitFetcher: splitFetcher, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, timer: timer, eventsManager: eventsManager, splitConfig: SplitClientConfig()) @@ -56,7 +62,9 @@ class PeriodicSplitsSyncWorkerTest: XCTestCase { let timer = PeriodicTimerStub() splitsSyncWorker = PeriodicSplitsSyncWorker(splitFetcher: splitFetcher, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, timer: timer, eventsManager: eventsManager, splitConfig: SplitClientConfig()) diff --git a/SplitTests/Service/Splits/SplitsBgSyncWorkerTest.swift b/SplitTests/Service/Splits/SplitsBgSyncWorkerTest.swift index f2be6e33..a9c8d088 100644 --- a/SplitTests/Service/Splits/SplitsBgSyncWorkerTest.swift +++ b/SplitTests/Service/Splits/SplitsBgSyncWorkerTest.swift @@ -15,27 +15,33 @@ class SplitsBgSyncWorkerTest: XCTestCase { var splitFetcher: HttpSplitFetcherStub! var splitStorage: PersistentSplitsStorageStub! + var persistentRuleBasedSegmentsStorage: PersistentRuleBasedSegmentsStorageStub! var splitChangeProcessor: SplitChangeProcessorStub! + var ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessorStub! var splitsSyncWorker: BackgroundSyncWorker! override func setUp() { splitFetcher = HttpSplitFetcherStub() splitStorage = PersistentSplitsStorageStub() + persistentRuleBasedSegmentsStorage = PersistentRuleBasedSegmentsStorageStub() splitStorage.changeNumber = 100 let _ = SplitChange(splits: [], since: splitStorage.changeNumber, till: splitStorage.changeNumber) splitChangeProcessor = SplitChangeProcessorStub() + ruleBasedSegmentChangeProcessor = RuleBasedSegmentChangeProcessorStub() } func testFetchSuccess() { // Cache expiration timestamp set to 0 (no clearing cache) splitsSyncWorker = BackgroundSplitsSyncWorker(splitFetcher: splitFetcher, persistentSplitsStorage: splitStorage, + persistentRuleBasedSegmentsStorage: persistentRuleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, cacheExpiration: 100, splitConfig: SplitClientConfig()) let change = SplitChange(splits: [], since: 200, till: 200) - splitFetcher.splitChanges = [change] + splitFetcher.splitChanges = [TargetingRulesChange(featureFlags: change)] splitsSyncWorker.execute() @@ -47,7 +53,9 @@ class SplitsBgSyncWorkerTest: XCTestCase { // Cache expiration timestamp set to 0 (no clearing cache) splitsSyncWorker = BackgroundSplitsSyncWorker(splitFetcher: splitFetcher, persistentSplitsStorage: splitStorage, + persistentRuleBasedSegmentsStorage: persistentRuleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, cacheExpiration: 100, splitConfig: SplitClientConfig()) @@ -64,13 +72,15 @@ class SplitsBgSyncWorkerTest: XCTestCase { let expiration = 1000 splitsSyncWorker = BackgroundSplitsSyncWorker(splitFetcher: splitFetcher, persistentSplitsStorage: splitStorage, + persistentRuleBasedSegmentsStorage: persistentRuleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, cacheExpiration: 2000, splitConfig: SplitClientConfig()) let change = SplitChange(splits: [], since: 200, till: 200) splitStorage.updateTimestamp = Int64(Date().timeIntervalSince1970) - Int64(expiration / 2) // Non Expired cache - splitFetcher.splitChanges = [change] + splitFetcher.splitChanges = [TargetingRulesChange(featureFlags: change)] splitsSyncWorker.execute() XCTAssertFalse(splitStorage.clearCalled) diff --git a/SplitTests/Service/Splits/SplitsSyncHelperTest.swift b/SplitTests/Service/Splits/SplitsSyncHelperTest.swift new file mode 100644 index 00000000..ccace843 --- /dev/null +++ b/SplitTests/Service/Splits/SplitsSyncHelperTest.swift @@ -0,0 +1,37 @@ +// +// SplitsSyncHelperTest.swift +// SplitTests +// +// Copyright © 2025 Split. All rights reserved. +// + +import XCTest +@testable import Split + +final class SplitsSyncHelperTest: XCTestCase { + + private var syncHelper: SplitsSyncHelper! + private var splitFetcher: HttpSplitFetcherStub! + + override func setUp() { + splitFetcher = HttpSplitFetcherStub() + syncHelper = SplitsSyncHelper(splitFetcher: splitFetcher, + splitsStorage: SplitsStorageStub(), + ruleBasedSegmentsStorage: RuleBasedSegmentsStorageStub(), + splitChangeProcessor: SplitChangeProcessorStub(), + ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessorStub(), + splitConfig: SplitClientConfig()) + } + + func testRbSinceParamIsSentToFetcher() { + do { + _ = try syncHelper.sync(since: 120, rbSince: 130) + } catch { + // ignore; we only care about param values + } + + XCTAssertEqual(120, splitFetcher.params["since"], "since is not 120") + XCTAssertEqual(130, splitFetcher.params["rbSince"], "rbSince is not 130") + XCTAssertEqual(nil, splitFetcher.params["til"], "till is not nil") + } +} diff --git a/SplitTests/Service/Splits/SplitsSyncWorkerTest.swift b/SplitTests/Service/Splits/SplitsSyncWorkerTest.swift index c815a882..336a8727 100644 --- a/SplitTests/Service/Splits/SplitsSyncWorkerTest.swift +++ b/SplitTests/Service/Splits/SplitsSyncWorkerTest.swift @@ -15,7 +15,9 @@ class SplitsSyncWorkerTest: XCTestCase { var splitFetcher: HttpSplitFetcherStub! var splitStorage: SplitsStorageStub! + var ruleBasedSegmentsStorage: RuleBasedSegmentsStorageStub! var splitChangeProcessor: SplitChangeProcessorStub! + var ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessorStub! var eventsManager: SplitEventsManagerMock! var backoffCounter: ReconnectBackoffCounterStub! var splitsSyncWorker: RetryableSplitsSyncWorker! @@ -23,9 +25,11 @@ class SplitsSyncWorkerTest: XCTestCase { override func setUp() { splitFetcher = HttpSplitFetcherStub() splitStorage = SplitsStorageStub() + ruleBasedSegmentsStorage = RuleBasedSegmentsStorageStub() splitStorage.changeNumber = 100 let _ = SplitChange(splits: [], since: splitStorage.changeNumber, till: splitStorage.changeNumber) splitChangeProcessor = SplitChangeProcessorStub() + ruleBasedSegmentChangeProcessor = RuleBasedSegmentChangeProcessorStub() eventsManager = SplitEventsManagerMock() backoffCounter = ReconnectBackoffCounterStub() eventsManager.isSplitsReadyFired = false @@ -35,14 +39,16 @@ class SplitsSyncWorkerTest: XCTestCase { // Cache expiration timestamp set to 0 (no clearing cache) splitsSyncWorker = RetryableSplitsSyncWorker(splitFetcher: splitFetcher, splitsStorage: splitStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentChangeProcessor: ruleBasedSegmentChangeProcessor, eventsManager: eventsManager, reconnectBackoffCounter: backoffCounter, splitConfig: SplitClientConfig()) var resultIsSuccess = false let change = SplitChange(splits: [], since: 200, till: 200) - splitFetcher.splitChanges = [change] + splitFetcher.splitChanges = [TargetingRulesChange(featureFlags: change)] let exp = XCTestExpectation(description: "exp") splitsSyncWorker.completion = { success in resultIsSuccess = success @@ -61,13 +67,15 @@ class SplitsSyncWorkerTest: XCTestCase { func testRetryAndSuccess() { splitsSyncWorker = RetryableSplitsSyncWorker(splitFetcher: splitFetcher, splitsStorage: splitStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentChangeProcessor: ruleBasedSegmentChangeProcessor, eventsManager: eventsManager, reconnectBackoffCounter: backoffCounter, splitConfig: SplitClientConfig()) let change = SplitChange(splits: [], since: 200, till: 200) - splitFetcher.splitChanges = [nil, nil, change] + splitFetcher.splitChanges = [nil, nil, TargetingRulesChange(featureFlags: change)] var resultIsSuccess = false let exp = XCTestExpectation(description: "exp") splitsSyncWorker.completion = { success in @@ -86,7 +94,9 @@ class SplitsSyncWorkerTest: XCTestCase { func testStopNoSuccess() { splitsSyncWorker = RetryableSplitsSyncWorker(splitFetcher: splitFetcher, splitsStorage: splitStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentChangeProcessor: ruleBasedSegmentChangeProcessor, eventsManager: eventsManager, reconnectBackoffCounter: backoffCounter, splitConfig: SplitClientConfig()) @@ -115,14 +125,16 @@ class SplitsSyncWorkerTest: XCTestCase { splitFetcher.httpError = .uriTooLong splitsSyncWorker = RetryableSplitsSyncWorker(splitFetcher: splitFetcher, splitsStorage: splitStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, + ruleBasedSegmentChangeProcessor: ruleBasedSegmentChangeProcessor, eventsManager: eventsManager, reconnectBackoffCounter: backoffCounter, splitConfig: SplitClientConfig()) let change = SplitChange(splits: [], since: 200, till: 200) splitStorage.updateTimestamp = Int64(Int(Date().timeIntervalSince1970) - expiration / 2) // Non Expired cache - splitFetcher.splitChanges = [change] + splitFetcher.splitChanges = [TargetingRulesChange(featureFlags: change)] splitStorage.splitsFilterQueryString = "&q=1" var resultIsSuccess = false let exp = XCTestExpectation(description: "exp") diff --git a/SplitTests/Service/Splits/SplitsUpdateWorkerTest.swift b/SplitTests/Service/Splits/SplitsUpdateWorkerTest.swift index 441d4cef..b5deb3dd 100644 --- a/SplitTests/Service/Splits/SplitsUpdateWorkerTest.swift +++ b/SplitTests/Service/Splits/SplitsUpdateWorkerTest.swift @@ -15,14 +15,18 @@ class SplitsUpdateWorkerTest: XCTestCase { var splitFetcher: HttpSplitFetcherStub! var splitsStorage: SplitsStorageStub! + var ruleBasedSegmentsStorage: RuleBasedSegmentsStorageStub! var eventsManager: SplitEventsManagerMock! var backoffCounter: ReconnectBackoffCounterStub! var splitsUpdateWorker: RetryableSplitsUpdateWorker! override func setUp() { splitFetcher = HttpSplitFetcherStub() - splitFetcher.splitChanges = [SplitChange(splits: [], since: 102, till: 102)] + splitFetcher.splitChanges = [ + TargetingRulesChange(featureFlags: SplitChange(splits: [], since: 102, till: 102)) + ] splitsStorage = SplitsStorageStub() + ruleBasedSegmentsStorage = RuleBasedSegmentsStorageStub() _ = splitsStorage.update(splitChange: ProcessedSplitChange(activeSplits: [Split](), archivedSplits: [], changeNumber: 100, @@ -36,8 +40,10 @@ class SplitsUpdateWorkerTest: XCTestCase { // Cache expiration timestamp set to 0 (no clearing cache) splitsUpdateWorker = RetryableSplitsUpdateWorker(splitsFetcher: splitFetcher, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: DefaultSplitChangeProcessor(filterBySet: nil), - changeNumber: 101, + ruleBasedSegmentChangeProcessor: DefaultRuleBasedSegmentChangeProcessor(), + changeNumber: SplitsUpdateChangeNumber(flags: 101, rbs: nil), eventsManager: eventsManager, reconnectBackoffCounter: backoffCounter, splitConfig: SplitClientConfig()) @@ -60,14 +66,16 @@ class SplitsUpdateWorkerTest: XCTestCase { func testRetryAndSuccess() { splitsUpdateWorker = RetryableSplitsUpdateWorker(splitsFetcher: splitFetcher, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: DefaultSplitChangeProcessor(filterBySet: nil), - changeNumber: 200, + ruleBasedSegmentChangeProcessor: DefaultRuleBasedSegmentChangeProcessor(), + changeNumber: SplitsUpdateChangeNumber(flags: 200, rbs: nil), eventsManager: eventsManager, reconnectBackoffCounter: backoffCounter, splitConfig: SplitClientConfig()) let change = SplitChange(splits: [], since: 200, till: 200) - splitFetcher.splitChanges = [nil, nil, change] + splitFetcher.splitChanges = [nil, nil, TargetingRulesChange(featureFlags: change)] var resultIsSuccess = false let exp = XCTestExpectation(description: "exp") splitsUpdateWorker.completion = { success in @@ -86,8 +94,10 @@ class SplitsUpdateWorkerTest: XCTestCase { func testStopNoSuccess() { splitsUpdateWorker = RetryableSplitsUpdateWorker(splitsFetcher: splitFetcher, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: DefaultSplitChangeProcessor(filterBySet: nil), - changeNumber: 200, + ruleBasedSegmentChangeProcessor: DefaultRuleBasedSegmentChangeProcessor(), + changeNumber: SplitsUpdateChangeNumber(flags: 200, rbs: nil), eventsManager: eventsManager, reconnectBackoffCounter: backoffCounter, splitConfig: SplitClientConfig()) @@ -114,14 +124,16 @@ class SplitsUpdateWorkerTest: XCTestCase { splitsUpdateWorker = RetryableSplitsUpdateWorker(splitsFetcher: splitFetcher, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: DefaultSplitChangeProcessor(filterBySet: nil), - changeNumber: 99, + ruleBasedSegmentChangeProcessor: DefaultRuleBasedSegmentChangeProcessor(), + changeNumber: SplitsUpdateChangeNumber(flags: 99, rbs: nil), eventsManager: eventsManager, reconnectBackoffCounter: backoffCounter, splitConfig: SplitClientConfig()) let change = SplitChange(splits: [], since: 100, till: 100) - splitFetcher.splitChanges = [change] + splitFetcher.splitChanges = [TargetingRulesChange(featureFlags: change)] var resultIsSuccess = false let exp = XCTestExpectation(description: "exp") splitsUpdateWorker.completion = { success in diff --git a/SplitTests/Storage/RuleBasedSegmentDaoTest.swift b/SplitTests/Storage/RuleBasedSegmentDaoTest.swift index 188e8bc4..3cbc5aae 100644 --- a/SplitTests/Storage/RuleBasedSegmentDaoTest.swift +++ b/SplitTests/Storage/RuleBasedSegmentDaoTest.swift @@ -220,7 +220,9 @@ class RuleBasedSegmentDaoTest: XCTestCase { let excluded = Excluded() excluded.keys = Set(["key1", "key2"]) - excluded.segments = Set(["segment1", "segment2"]) + excluded.segments = Set([ + ExcludedSegment(name: "segment1", type: .standard), + ExcludedSegment(name: "segment2", type: .standard)]) segment.excluded = excluded return segment diff --git a/SplitTests/Storage/RuleBasedSegmentStorageTest.swift b/SplitTests/Storage/RuleBasedSegmentStorageTest.swift index 752a1cbc..497e1545 100644 --- a/SplitTests/Storage/RuleBasedSegmentStorageTest.swift +++ b/SplitTests/Storage/RuleBasedSegmentStorageTest.swift @@ -138,7 +138,7 @@ class RuleBasedSegmentStorageTest: XCTestCase { unparsedSegment.isParsed = false // Update storage with the unparsed segment - ruleBasedSegmentsStorage.update( + _ = ruleBasedSegmentsStorage.update( toAdd: Set([unparsedSegment]), toRemove: Set(), changeNumber: 456 @@ -313,6 +313,10 @@ private class MockPersistentRuleBasedSegmentsStorage: PersistentRuleBasedSegment // No-op for the mock } + func getChangeNumber() -> Int64 { + return snapshotChangeNumber + } + func updateSnapshotData(segments: [RuleBasedSegment], changeNumber: Int64) { self.segments = segments self.snapshotChangeNumber = changeNumber diff --git a/SplitTests/Streaming/FeatureFlagsPayloadDecoderTest.swift b/SplitTests/Streaming/FeatureFlagsPayloadDecoderTest.swift index 5c705bdc..2177aa2e 100644 --- a/SplitTests/Streaming/FeatureFlagsPayloadDecoderTest.swift +++ b/SplitTests/Streaming/FeatureFlagsPayloadDecoderTest.swift @@ -13,7 +13,7 @@ import XCTest class FeatureFlagsPayloadDecoderTest: XCTestCase { - let decoder: FeatureFlagsPayloadDecoder = DefaultFeatureFlagsPayloadDecoder() + let decoder: DefaultFeatureFlagsPayloadDecoder = DefaultFeatureFlagsPayloadDecoder(type: Split.self) func testDecodeZlib() throws { diff --git a/SplitTests/Streaming/FeatureFlagsSynchronizerTest.swift b/SplitTests/Streaming/FeatureFlagsSynchronizerTest.swift index 3e51ec3d..efcfece3 100644 --- a/SplitTests/Streaming/FeatureFlagsSynchronizerTest.swift +++ b/SplitTests/Streaming/FeatureFlagsSynchronizerTest.swift @@ -21,7 +21,7 @@ class FeatureFlagsSynchronizerTest: XCTestCase { var splitsStorage: SplitsStorageStub! - var updateWorkerCatalog = ConcurrentDictionary() + var updateWorkerCatalog = ConcurrentDictionary() var syncWorkerFactory: SyncWorkerFactoryStub! var eventsManager: SplitEventsManagerStub! var splitConfig: SplitClientConfig! @@ -49,7 +49,7 @@ class FeatureFlagsSynchronizerTest: XCTestCase { broadcasterChannel = SyncEventBroadcasterStub() generalInfoStorage = GeneralInfoStorageMock() telemetryStorageStub = TelemetryStorageStub() - splitsStorage.update(splitChange: ProcessedSplitChange(activeSplits: [], archivedSplits: [], + _ = splitsStorage.update(splitChange: ProcessedSplitChange(activeSplits: [], archivedSplits: [], changeNumber: 100, updateTimestamp: 100)) generalInfoStorage.setFlagSpec(flagsSpec: "1.2") @@ -133,7 +133,7 @@ class FeatureFlagsSynchronizerTest: XCTestCase { func testLoadSplitsNoClearedOnLoad() { // Splits filter doesn't vary so splits don't gonna be removed // loaded splits > 0, ready from cache should be fired - splitsStorage.update(splitChange: ProcessedSplitChange(activeSplits: [TestingHelper.createSplit(name: "new_pepe")], + _ = splitsStorage.update(splitChange: ProcessedSplitChange(activeSplits: [TestingHelper.createSplit(name: "new_pepe")], archivedSplits: [], changeNumber: 100, updateTimestamp: 100)) generalInfoStorage.setSplitsFilterQueryString(filterQueryString: "") synchronizer.load() @@ -265,8 +265,8 @@ class FeatureFlagsSynchronizerTest: XCTestCase { let sw2 = RetryableSyncWorkerStub() syncWorkerFactory.retryableSplitsUpdateWorkers = [sw1, sw2] - synchronizer.synchronize(changeNumber: 101) - synchronizer.synchronize(changeNumber: 102) + synchronizer.synchronize(changeNumber: 101, rbsChangeNumber: nil) + synchronizer.synchronize(changeNumber: 102, rbsChangeNumber: nil) let initialSyncCount = updateWorkerCatalog.count sw1.completion?(true) @@ -299,7 +299,7 @@ class FeatureFlagsSynchronizerTest: XCTestCase { func testUpdateSplitsSingleModeEnabled() { synchronizer = buildSynchronizer(syncEnabled: false) - synchronizer.synchronize(changeNumber: -1) + synchronizer.synchronize(changeNumber: -1, rbsChangeNumber: nil) let syncExecCount = broadcasterChannel.pushedEvents.filter { $0 == .syncExecuted }.count XCTAssertFalse(splitsSyncWorker.startCalled) @@ -319,8 +319,8 @@ class FeatureFlagsSynchronizerTest: XCTestCase { let sw2 = RetryableSyncWorkerStub() syncWorkerFactory.retryableSplitsUpdateWorkers = [sw1, sw2] - synchronizer.synchronize(changeNumber: 101) - synchronizer.synchronize(changeNumber: 102) + synchronizer.synchronize(changeNumber: 101, rbsChangeNumber: nil) + synchronizer.synchronize(changeNumber: 102, rbsChangeNumber: nil) synchronizer.destroy() diff --git a/SplitTests/Streaming/NotificationParserTest.swift b/SplitTests/Streaming/NotificationParserTest.swift index 50854157..183fb8c0 100644 --- a/SplitTests/Streaming/NotificationParserTest.swift +++ b/SplitTests/Streaming/NotificationParserTest.swift @@ -44,7 +44,7 @@ class NotificationParserTest: XCTestCase { func testProcessSplitUpdate() throws { let incoming = notificationParser.parseIncoming(jsonString: splitsChangeNotificationMessage) - let splitUpdate = try notificationParser.parseSplitUpdate(jsonString: incoming!.jsonData!) + let splitUpdate = try notificationParser.parseTargetingRuleNotification(jsonString: incoming!.jsonData!, type: .splitUpdate) XCTAssertEqual(NotificationType.splitUpdate, incoming?.type) XCTAssertEqual(1584554772108, splitUpdate.changeNumber) @@ -52,7 +52,7 @@ class NotificationParserTest: XCTestCase { func testProcessSplitUpdateWithPayload() throws { let incoming = notificationParser.parseIncoming(jsonString: splitsChangeNotificationMessageWithPayload) - let splitUpdate = try notificationParser.parseSplitUpdate(jsonString: incoming!.jsonData!) + let splitUpdate = try notificationParser.parseTargetingRuleNotification(jsonString: incoming!.jsonData!, type: .splitUpdate) XCTAssertEqual(NotificationType.splitUpdate, incoming?.type) XCTAssertEqual(1584554772108, splitUpdate.changeNumber) diff --git a/SplitTests/Streaming/SseHandlerTest.swift b/SplitTests/Streaming/SseHandlerTest.swift index 4808bd21..315c09c9 100644 --- a/SplitTests/Streaming/SseHandlerTest.swift +++ b/SplitTests/Streaming/SseHandlerTest.swift @@ -37,7 +37,7 @@ class SseHandlerTest: XCTestCase { func testIncomingSplitUpdate() { notificationParser.incomingNotification = IncomingNotification(type: .splitUpdate, jsonData: "dummy") - notificationParser.splitsUpdateNotification = SplitsUpdateNotification(changeNumber: -1 , + notificationParser.splitsUpdateNotification = TargetingRuleUpdateNotification(changeNumber: -1 , previousChangeNumber: nil, definition: nil, compressionType: nil) diff --git a/SplitTests/Streaming/SseNotificationProcessorTest.swift b/SplitTests/Streaming/SseNotificationProcessorTest.swift index c77080f1..f1f7af99 100644 --- a/SplitTests/Streaming/SseNotificationProcessorTest.swift +++ b/SplitTests/Streaming/SseNotificationProcessorTest.swift @@ -25,6 +25,7 @@ class SseNotificationProcessorTest: XCTestCase { override func setUp() { let synchronizer = SynchronizerStub() let splitsStorage = SplitsStorageStub() + let ruleBasedSegmentsStorage = RuleBasedSegmentsStorageStub() payloadDecoderMock = SegmentsPayloadDecoderMock() _ = splitsStorage.update(splitChange: ProcessedSplitChange(activeSplits: [], archivedSplits: [], @@ -35,8 +36,11 @@ class SseNotificationProcessorTest: XCTestCase { sseNotificationParser = SseNotificationParserStub() splitsUpdateWorker = SplitsUpdateWorkerMock(synchronizer: synchronizer, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: SplitChangeProcessorStub(), - featureFlagsPayloadDecoder: FeatureFlagsPayloadDecoderMock(), + ruleBasedSegmentsChangeProcessor: DefaultRuleBasedSegmentChangeProcessor(), + featureFlagsPayloadDecoder: FeatureFlagsPayloadDecoderMock(type: Split.self), + ruleBasedSegmentsPayloadDecoder: RuleBasedSegmentsPayloadDecoderMock(type: RuleBasedSegment.self), telemetryProducer: TelemetryStorageStub()) mySegmentsUpdateWorker = SegmentsUpdateWorkerMock(synchronizer: MySegmentsSynchronizerWrapper(synchronizer: synchronizer), @@ -61,7 +65,7 @@ class SseNotificationProcessorTest: XCTestCase { } func testProcessSplitUpdate() { - sseNotificationParser.splitsUpdateNotification = SplitsUpdateNotification(changeNumber: -1) + sseNotificationParser.splitsUpdateNotification = TargetingRuleUpdateNotification(changeNumber: -1) let notification = IncomingNotification(type: .splitUpdate, channel: nil, jsonData: "", @@ -72,7 +76,7 @@ class SseNotificationProcessorTest: XCTestCase { } func testProcessSplitUpdateNullJson() { - sseNotificationParser.splitsUpdateNotification = SplitsUpdateNotification(changeNumber: -1) + sseNotificationParser.splitsUpdateNotification = TargetingRuleUpdateNotification(changeNumber: -1) let notification = IncomingNotification(type: .splitUpdate, channel: nil, jsonData: nil, @@ -84,7 +88,7 @@ class SseNotificationProcessorTest: XCTestCase { func testProcessSplitUpdateException() { splitsUpdateWorker.throwException = true - sseNotificationParser.splitsUpdateNotification = SplitsUpdateNotification(changeNumber: -1) + sseNotificationParser.splitsUpdateNotification = TargetingRuleUpdateNotification(changeNumber: -1) let notification = IncomingNotification(type: .splitUpdate, channel: nil, jsonData: nil, diff --git a/SplitTests/Streaming/SyncUpdateWorkerTest.swift b/SplitTests/Streaming/SyncUpdateWorkerTest.swift index 370e691e..e903b1c9 100644 --- a/SplitTests/Streaming/SyncUpdateWorkerTest.swift +++ b/SplitTests/Streaming/SyncUpdateWorkerTest.swift @@ -20,15 +20,19 @@ class SyncUpdateWorkerTest: XCTestCase { var synchronizer: SynchronizerStub! var splitsStorage: SplitsStorageStub! + var ruleBasedSegmentsStorage: RuleBasedSegmentsStorageStub! let userKey = IntegrationHelper.dummyUserKey var userKeyHash: String = "" var telemetryProducer: TelemetryStorageStub! var splitChangeProcessor: SplitChangeProcessorStub! + var ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessorStub! override func setUp() { userKeyHash = DefaultMySegmentsPayloadDecoder().hash(userKey: userKey) synchronizer = SynchronizerStub() splitsStorage = SplitsStorageStub() + ruleBasedSegmentsStorage = RuleBasedSegmentsStorageStub() + ruleBasedSegmentChangeProcessor = RuleBasedSegmentChangeProcessorStub() telemetryProducer = TelemetryStorageStub() _ = splitsStorage.update(splitChange: ProcessedSplitChange(activeSplits: [TestingHelper.createSplit(name: "split1")], archivedSplits: [], @@ -37,8 +41,11 @@ class SyncUpdateWorkerTest: XCTestCase { splitChangeProcessor = SplitChangeProcessorStub() splitsUpdateWorker = SplitsUpdateWorker(synchronizer: synchronizer, splitsStorage: splitsStorage, + ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, - featureFlagsPayloadDecoder: FeatureFlagsPayloadDecoderMock(), + ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, + featureFlagsPayloadDecoder: FeatureFlagsPayloadDecoderMock(type: Split.self), + ruleBasedSegmentsPayloadDecoder: RuleBasedSegmentsPayloadDecoderMock(type: RuleBasedSegment.self), telemetryProducer: telemetryProducer) splitKillWorker = SplitKillWorker(synchronizer: synchronizer, splitsStorage: splitsStorage) @@ -46,7 +53,7 @@ class SyncUpdateWorkerTest: XCTestCase { func testSplitUpdateWorkerNoPayload() throws { splitsStorage.changeNumber = 10 - let notification = SplitsUpdateNotification(changeNumber: 100) + let notification = TargetingRuleUpdateNotification(changeNumber: 100) let exp = XCTestExpectation(description: "exp") synchronizer.syncSplitsChangeNumberExp = exp @@ -61,10 +68,12 @@ class SyncUpdateWorkerTest: XCTestCase { let exp = XCTestExpectation() telemetryProducer.recordUpdatesFromSseExp = exp splitsStorage.changeNumber = 10 - let notification = SplitsUpdateNotification(changeNumber: 100, + var notification = TargetingRuleUpdateNotification( + changeNumber: 100, previousChangeNumber: 10, definition: "fake_definition", compressionType: .gzip) + notification.entityType = .splitUpdate try splitsUpdateWorker.process(notification: notification) @@ -82,10 +91,11 @@ class SyncUpdateWorkerTest: XCTestCase { splitsStorage.changeNumber = 1000 splitsStorage.updateSplitChangeCalled = false - let notification = SplitsUpdateNotification(changeNumber: 100, + var notification = TargetingRuleUpdateNotification(changeNumber: 100, previousChangeNumber: 10, definition: "fake_definition", compressionType: .gzip) + notification.entityType = .splitUpdate try splitsUpdateWorker.process(notification: notification)