diff --git a/Split.xcodeproj/project.pbxproj b/Split.xcodeproj/project.pbxproj index 7068190d0..8271df2ee 100644 --- a/Split.xcodeproj/project.pbxproj +++ b/Split.xcodeproj/project.pbxproj @@ -353,9 +353,10 @@ 59FB7C35220329B900ECC96A /* SplitFactoryBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C34220329B900ECC96A /* SplitFactoryBuilderTests.swift */; }; 59FB7C3C2203795F00ECC96A /* LocalhostSplitsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C3B2203795F00ECC96A /* LocalhostSplitsParser.swift */; }; 59FB7C3E22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7C3D22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift */; }; + 5B0162682E4A9C7A0009D3B7 /* SplitEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0162672E4A9C5D0009D3B7 /* SplitEventsTests.swift */; }; + 5B26B0D72E4F70A00025AAB7 /* StorageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B343EAC2E26E937006BEBE7 /* StorageHelper.swift */; }; 5B279CF92E340FC600B73A36 /* splitschanges_no_segments.json in Resources */ = {isa = PBXBuildFile; fileRef = 5B279CF82E340FB900B73A36 /* splitschanges_no_segments.json */; }; 5B343EAD2E26E93B006BEBE7 /* StorageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B343EAC2E26E937006BEBE7 /* StorageHelper.swift */; }; - 5B343EAE2E26E93B006BEBE7 /* StorageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B343EAC2E26E937006BEBE7 /* StorageHelper.swift */; }; 5B48D8172DEA2CED00351925 /* PrerequisitesMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF52DF52DE0B60300FEDAFE /* PrerequisitesMatcher.swift */; }; 5B91B8392DDE4A3B000510F0 /* SplitDTOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B91B8382DDE4A30000510F0 /* SplitDTOTests.swift */; }; 5BF52DF72DE0B60700FEDAFE /* PrerequisitesMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BF52DF52DE0B60300FEDAFE /* PrerequisitesMatcher.swift */; }; @@ -1560,6 +1561,7 @@ 59FB7C34220329B900ECC96A /* SplitFactoryBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitFactoryBuilderTests.swift; sourceTree = ""; }; 59FB7C3B2203795F00ECC96A /* LocalhostSplitsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalhostSplitsParser.swift; sourceTree = ""; }; 59FB7C3D22037B9400ECC96A /* SpaceDelimitedLocalhostSplitsParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceDelimitedLocalhostSplitsParser.swift; sourceTree = ""; }; + 5B0162672E4A9C5D0009D3B7 /* SplitEventsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitEventsTests.swift; sourceTree = ""; }; 5B279CF82E340FB900B73A36 /* splitschanges_no_segments.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = splitschanges_no_segments.json; sourceTree = ""; }; 5B343EAC2E26E937006BEBE7 /* StorageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageHelper.swift; sourceTree = ""; }; 5B91B8382DDE4A30000510F0 /* SplitDTOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitDTOTests.swift; sourceTree = ""; }; @@ -2893,6 +2895,7 @@ isa = PBXGroup; children = ( C539CAE52D947D2A0050C732 /* Common */, + 5B0162672E4A9C5D0009D3B7 /* SplitEventsTests.swift */, 5B91B8382DDE4A30000510F0 /* SplitDTOTests.swift */, C53EDFCC2DD4E10A000DCDBC /* SplitsSyncHelperWithProxyHandlerTests.swift */, C53EDFCA2DD3E257000DCDBC /* OutdatedSplitProxyHandlerTests.swift */, @@ -4249,7 +4252,7 @@ 59F4AA9B24FE93E300A1C69A /* NotificationManagerKeeper.swift in Sources */, 95C1600D27D28CF4008562E3 /* PersistentAttributesStorage.swift in Sources */, 95726075262F548500350CCA /* SplitBgSynchronizer.swift in Sources */, - 5B343EAE2E26E93B006BEBE7 /* StorageHelper.swift in Sources */, + 5B26B0D72E4F70A00025AAB7 /* StorageHelper.swift in Sources */, 95C1600B27D28CB8008562E3 /* OneKeyPersistentAttributesStorage.swift in Sources */, 5BF52DF72DE0B60700FEDAFE /* PrerequisitesMatcher.swift in Sources */, 9519A91127D6935700278AEC /* ByKeyAttributesStorage.swift in Sources */, @@ -4438,6 +4441,7 @@ 95B1801E2763BF7E002DC9DF /* TelemetryStatsRecorderWorkerTests.swift in Sources */, 95F3F002258D3EE700084AF8 /* HttpEventsRecorderStub.swift in Sources */, 59D84BE3221734F5003DA248 /* LocalhostSplitClientTests.swift in Sources */, + 5B0162682E4A9C7A0009D3B7 /* SplitEventsTests.swift in Sources */, 59B2043924F5667A0092F2E9 /* SseNotificationProcessorTest.swift in Sources */, 592C6AC6211B718E002D120C /* SplitEventsManagerTest.swift in Sources */, 95C7569D2696457500696148 /* NotificationHelperStub.swift in Sources */, diff --git a/Split/Api/DefaultSplitClient.swift b/Split/Api/DefaultSplitClient.swift index bbf3aa8a7..6ce325d21 100644 --- a/Split/Api/DefaultSplitClient.swift +++ b/Split/Api/DefaultSplitClient.swift @@ -88,16 +88,37 @@ extension DefaultSplitClient { task.event = event on(event: event, executeTask: task) } - - private func on(event: SplitEvent, executeTask task: SplitEventTask) { - if event != .sdkReadyFromCache, - eventsManager.eventAlreadyTriggered(event: event) { - Logger.w("A handler was added for \(event.toString()) on the SDK, " + - "which has already fired and won’t be emitted again. The callback won’t be executed.") + + private func on(event: SplitEvent, executeTask task: SplitEventActionTask) { + if event != .sdkReadyFromCache, eventsManager.eventAlreadyTriggered(event: event) { + Logger.w("A handler was added for \(event.toString()) on the SDK, which has already fired and won’t be emitted again. The callback won’t be executed.") return } eventsManager.register(event: event, task: task) } + + // MARK: Listeners with Metadata + public func on(event: SplitEvent, executeWithMetadata action: @escaping SplitActionWithMetadata) { + on(event: event, runInBackground: true, queue: nil, executeWithMetadata: action) + } + + public func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) { + on(event: event, runInBackground: runInBackground, queue: nil, executeWithMetadata: executeWithMetadata) + } + + public func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue? = nil, executeWithMetadata action: @escaping SplitActionWithMetadata) { + on(event: event, runInBackground: runInBackground, queue: queue, action: action) + } + + public func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) { + on(event: event, runInBackground: true, queue: queue, action: action) + } + + private func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue? = nil, action: @escaping SplitActionWithMetadata) { + guard let factory = clientManager?.splitFactory else { return } + let task = SplitEventActionTask(action: action, event: event, runInBackground: runInBackground, factory: factory, queue: queue) + on(event: event, executeTask: task) + } } // MARK: Treatment / Evaluation diff --git a/Split/Api/FailHelpers.swift b/Split/Api/FailHelpers.swift index 6f90ba7b7..0c2bf13cf 100644 --- a/Split/Api/FailHelpers.swift +++ b/Split/Api/FailHelpers.swift @@ -64,6 +64,18 @@ class FailedClient: SplitClient { func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) { } + + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } func track(trafficType: String, eventType: String) -> Bool { return false diff --git a/Split/Api/LocalhostSplitClient.swift b/Split/Api/LocalhostSplitClient.swift index 2aaacca6b..eb15e21fe 100644 --- a/Split/Api/LocalhostSplitClient.swift +++ b/Split/Api/LocalhostSplitClient.swift @@ -146,6 +146,18 @@ public final class LocalhostSplitClient: NSObject, SplitClient { eventsManager.register(event: event, task: task) } } + + public func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + public func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + public func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } public func track(trafficType: String, eventType: String) -> Bool { return true diff --git a/Split/Api/SplitClient.swift b/Split/Api/SplitClient.swift index ff4df2005..7c87983d3 100644 --- a/Split/Api/SplitClient.swift +++ b/Split/Api/SplitClient.swift @@ -9,6 +9,7 @@ import Foundation public typealias SplitAction = () -> Void +public typealias SplitActionWithMetadata = (EventMetadata) -> Void @objc public protocol SplitClient { @@ -33,9 +34,15 @@ public typealias SplitAction = () -> Void @objc(getTreatmentsWithConfigForSplits:attributes:evaluationOptions:) func getTreatmentsWithConfig(splits: [String], attributes: [String: Any]?, evaluationOptions: EvaluationOptions?) -> [String: SplitResult] + + // MARK: Events Listeners func on(event: SplitEvent, execute action: @escaping SplitAction) func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) + + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) + func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) + func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) // MARK: Track feature func track(trafficType: String, eventType: String) -> Bool diff --git a/Split/Common/Structs/BlockingQueue.swift b/Split/Common/Structs/BlockingQueue.swift index c8b750940..4fb01e5d9 100644 --- a/Split/Common/Structs/BlockingQueue.swift +++ b/Split/Common/Structs/BlockingQueue.swift @@ -73,18 +73,23 @@ class GenericBlockingQueue { // Protocol to allow mocking protocol InternalEventBlockingQueue { - func add(_ item: SplitInternalEvent) - func take() throws -> SplitInternalEvent + func add(_ item: SplitInternalEventWithMetadata) + func take() throws -> SplitInternalEventWithMetadata func stop() } class DefaultInternalEventBlockingQueue: InternalEventBlockingQueue { - let blockingQueue = GenericBlockingQueue() - func add(_ item: SplitInternalEvent) { + let blockingQueue = GenericBlockingQueue() + + func add(_ item: SplitInternalEventWithMetadata) { blockingQueue.add(item) } + + func add(_ item: SplitInternalEvent) { + blockingQueue.add(SplitInternalEventWithMetadata(item, metadata: nil)) + } - func take() throws -> SplitInternalEvent { + func take() throws -> SplitInternalEventWithMetadata { let value = try blockingQueue.take() return value } diff --git a/Split/Events/EventsManagerCoordinator.swift b/Split/Events/EventsManagerCoordinator.swift index 3ddf4fe93..c25213105 100644 --- a/Split/Events/EventsManagerCoordinator.swift +++ b/Split/Events/EventsManagerCoordinator.swift @@ -14,24 +14,30 @@ protocol SplitEventsManagerCoordinator: SplitEventsManager { } class MainSplitEventsManager: SplitEventsManagerCoordinator { + private var defaultManager: SplitEventsManager? private var managers = [Key: SplitEventsManager]() private var triggered = Set() private let queue = DispatchQueue(label: "split-event-manager-coordinator") private let eventsToHandle: Set = Set( [.splitsLoadedFromCache, - .splitsUpdated, - .splitKilledNotification] + .splitsUpdated, + .splitKilledNotification, + .sdkError] ) - + func notifyInternalEvent(_ event: SplitInternalEvent) { - if !eventsToHandle.contains(event) { + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { + if !eventsToHandle.contains(event.type) { return } queue.async { [weak self] in guard let self = self else { return } - self.triggered.insert(event) + self.triggered.insert(event.type) self.managers.forEach { _, manager in manager.notifyInternalEvent(event) } @@ -77,4 +83,8 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator { } func register(event: SplitEvent, task: SplitEventTask) {} + + func register(event: SplitEventWithMetadata, task: any SplitEventTask) { + /* Intentionally unimplemented */ + } } diff --git a/Split/Events/SplitEvent.swift b/Split/Events/SplitEvent.swift index d2561e2d9..0b2a7872d 100644 --- a/Split/Events/SplitEvent.swift +++ b/Split/Events/SplitEvent.swift @@ -7,22 +7,35 @@ import Foundation +@objcMembers public class SplitEventWithMetadata: NSObject { + let type: SplitEvent + let metadata: EventMetadata? + + @objc public init(_ type: SplitEvent, metadata: EventMetadata? = nil) { + self.type = type + self.metadata = metadata + } +} + @objc public enum SplitEvent: Int { case sdkReady case sdkReadyTimedOut case sdkReadyFromCache case sdkUpdated + case sdkError public func toString() -> String { switch self { - case .sdkReady: - return "SDK_READY" - case .sdkUpdated: - return "SDK_UPDATE" - case .sdkReadyTimedOut: - return "SDK_READY_TIMED_OUT" - case .sdkReadyFromCache: - return "SDK_READY_FROM_CACHE" + case .sdkReady: + return "SDK_READY" + case .sdkUpdated: + return "SDK_UPDATE" + case .sdkReadyTimedOut: + return "SDK_READY_TIMED_OUT" + case .sdkReadyFromCache: + return "SDK_READY_FROM_CACHE" + case .sdkError: + return "SDK_ERROR" } } } diff --git a/Split/Events/SplitEventActionTask.swift b/Split/Events/SplitEventActionTask.swift index be1368d04..3d879e83e 100644 --- a/Split/Events/SplitEventActionTask.swift +++ b/Split/Events/SplitEventActionTask.swift @@ -10,30 +10,39 @@ import Foundation class SplitEventActionTask: SplitEventTask { private var eventHandler: SplitAction? + private var eventHandlerWithMetadata: SplitActionWithMetadata? private var queue: DispatchQueue? + var event: SplitEvent var runInBackground: Bool = false var factory: SplitFactory - init(action: @escaping SplitAction, - event: SplitEvent, - runInBackground: Bool = false, - factory: SplitFactory, - queue: DispatchQueue? = nil) { - - self.eventHandler = action - self.event = event - self.runInBackground = runInBackground - self.queue = queue - self.factory = factory + init(action: @escaping SplitActionWithMetadata, event: SplitEvent, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) { + self.eventHandlerWithMetadata = action + self.event = event + self.runInBackground = runInBackground + self.queue = queue + self.factory = factory } + + init(action: @escaping SplitAction, event: SplitEvent, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) { + self.eventHandler = action + self.event = event + self.runInBackground = runInBackground + self.queue = queue + self.factory = factory + } func takeQueue() -> DispatchQueue? { defer { queue = nil } return queue } - func run() { + func run(_ metadata: EventMetadata?) { eventHandler?() + + if let metadata = metadata { + eventHandlerWithMetadata?(metadata) + } } } diff --git a/Split/Events/SplitEventTask.swift b/Split/Events/SplitEventTask.swift index 1655e2b25..147bc8441 100644 --- a/Split/Events/SplitEventTask.swift +++ b/Split/Events/SplitEventTask.swift @@ -11,5 +11,5 @@ protocol SplitEventTask { var event: SplitEvent { get } var runInBackground: Bool { get } func takeQueue() -> DispatchQueue? - func run() + func run(_ metadata: EventMetadata?) } diff --git a/Split/Events/SplitEventsManager.swift b/Split/Events/SplitEventsManager.swift index b66564a2e..3bca15aaa 100644 --- a/Split/Events/SplitEventsManager.swift +++ b/Split/Events/SplitEventsManager.swift @@ -11,19 +11,21 @@ import Foundation protocol SplitEventsManager: AnyObject { func register(event: SplitEvent, task: SplitEventTask) func notifyInternalEvent(_ event: SplitInternalEvent) + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) func start() func stop() func eventAlreadyTriggered(event: SplitEvent) -> Bool } class DefaultSplitEventsManager: SplitEventsManager { + private let readingRefreshTime: Int private var sdkReadyTimeStart: Int64 private var subscriptions = [SplitEvent: [SplitEventTask]]() private var executionTimes: [String: Int] - private var triggered: [SplitInternalEvent] + private var triggered: [SplitInternalEventWithMetadata] private let processQueue: DispatchQueue private let dataAccessQueue: DispatchQueue private var isStarted: Bool @@ -35,29 +37,34 @@ class DefaultSplitEventsManager: SplitEventsManager { self.isStarted = false self.sdkReadyTimeStart = Date().unixTimestampInMiliseconds() self.readingRefreshTime = 300 - self.triggered = [SplitInternalEvent]() + self.triggered = [SplitInternalEventWithMetadata]() self.eventsQueue = DefaultInternalEventBlockingQueue() self.executionTimes = [String: Int]() registerMaxAllowedExecutionTimesPerEvent() + // SDK Timeout Event if config.sdkReadyTimeOut > 0 { let readyTimedoutQueue = DispatchQueue(label: "split-event-timedout") readyTimedoutQueue.asyncAfter(deadline: .now() + .milliseconds(config.sdkReadyTimeOut)) { [weak self] in guard let self = self else { return } - self.notifyInternalEvent(SplitInternalEvent.sdkReadyTimeoutReached) + self.notifyInternalEvent(.sdkReadyTimeoutReached) } } } - func notifyInternalEvent(_ event: SplitInternalEvent) { + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { processQueue.async { [weak self] in if let self = self { - Logger.v("Event \(event) notified") + Logger.v("Event \(event.type) notified") self.eventsQueue.add(event) } } } - + + func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) + } + func register(event: SplitEvent, task: SplitEventTask) { let eventName = event.toString() processQueue.async { [weak self] in @@ -115,6 +122,7 @@ class DefaultSplitEventsManager: SplitEventsManager { executionTimes = [ SplitEvent.sdkReady.toString(): 1, SplitEvent.sdkUpdated.toString(): -1, + SplitEvent.sdkError.toString(): -1, SplitEvent.sdkReadyFromCache.toString(): 1, SplitEvent.sdkReadyTimedOut.toString(): 1] } @@ -127,7 +135,7 @@ class DefaultSplitEventsManager: SplitEventsManager { return isRunning } - private func takeEvent() -> SplitInternalEvent? { + private func takeEvent() -> SplitInternalEventWithMetadata? { do { return try eventsQueue.take() } catch BlockingQueueError.hasBeenStopped { @@ -145,38 +153,40 @@ class DefaultSplitEventsManager: SplitEventsManager { guard let event = takeEvent() else { continue } - self.triggered.append(event) - switch event { - case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated: - if isTriggered(external: .sdkReady) { - trigger(event: .sdkUpdated) - continue - } - self.triggerSdkReadyIfNeeded() - - case .mySegmentsLoadedFromCache, .myLargeSegmentsLoadedFromCache, - .splitsLoadedFromCache, .attributesLoadedFromCache: - Logger.v("Event \(event) triggered") - if isTriggered(internal: .splitsLoadedFromCache), - isTriggered(internal: .mySegmentsLoadedFromCache), - isTriggered(internal: .myLargeSegmentsLoadedFromCache), - isTriggered(internal: .attributesLoadedFromCache) { - trigger(event: SplitEvent.sdkReadyFromCache) - } - case .splitKilledNotification: - if isTriggered(external: .sdkReady) { - trigger(event: .sdkUpdated) - continue - } - case .sdkReadyTimeoutReached: - if !isTriggered(external: .sdkReady) { - trigger(event: SplitEvent.sdkReadyTimedOut) - } + triggered.append(event) + switch event.type { + case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated: + if isTriggered(external: .sdkReady) { + trigger(event: .sdkUpdated) + continue + } + triggerSdkReadyIfNeeded() + + case .mySegmentsLoadedFromCache, .myLargeSegmentsLoadedFromCache, + .splitsLoadedFromCache, .attributesLoadedFromCache: + Logger.v("Event \(event) triggered") + if isTriggered(internal: .splitsLoadedFromCache), + isTriggered(internal: .mySegmentsLoadedFromCache), + isTriggered(internal: .myLargeSegmentsLoadedFromCache), + isTriggered(internal: .attributesLoadedFromCache) { + trigger(event: .sdkReadyFromCache) + } + case .splitKilledNotification: + if isTriggered(external: .sdkReady) { + trigger(event: .sdkUpdated) + } + case .sdkReadyTimeoutReached: + if !isTriggered(external: .sdkReady) { + trigger(event: .sdkReadyTimedOut) + } + case .sdkError: + let eventWithMetadata = SplitEventWithMetadata(.sdkError, metadata: event.metadata) + trigger(event: eventWithMetadata) } } } - // MARK: Helper functions. + // MARK: Helper functions func isTriggered(external event: SplitEvent) -> Bool { var triggered = false dataAccessQueue.sync { @@ -200,9 +210,13 @@ class DefaultSplitEventsManager: SplitEventsManager { self.trigger(event: .sdkReady) } } - + private func trigger(event: SplitEvent) { - let eventName = event.toString() + trigger(event: SplitEventWithMetadata(event, metadata: nil)) + } + + private func trigger(event: SplitEventWithMetadata) { + let eventName = event.type.toString() // If executionTimes is zero, maximum executions has been reached if executionTimes(for: eventName) == 0 { @@ -216,37 +230,47 @@ class DefaultSplitEventsManager: SplitEventsManager { Logger.d("Triggering SDK event \(eventName)") // If executionTimes is lower than zero, execute it without limitation - if let subscriptions = getSubscriptions(for: event) { + if let subscriptions = getSubscriptions(for: event.type) { for task in subscriptions { executeTask(event: event, task: task) } } } - + private func executeTask(event: SplitEvent, task: SplitEventTask) { + executeTask(event: SplitEventWithMetadata(event, metadata: nil), task: task) + } + + private func executeTask(event: SplitEventWithMetadata, task: SplitEventTask) { let eventName = task.event.toString() + // RUN IN BG & RETURN if task.runInBackground { TimeChecker.logInterval("Previous to run \(eventName) in Background") let queue = task.takeQueue() ?? DispatchQueue.general queue.async { TimeChecker.logInterval("Running \(eventName) in Background queue \(queue)") - task.run() + task.run(event.metadata) } return } + // OR RUN ON MAIN DispatchQueue.main.async { TimeChecker.logInterval("Running event on main: \(eventName)") // UI Updates - task.run() + task.run(event.metadata) } } - + + private func isTriggered(internal event: SplitInternalEventWithMetadata) -> Bool { + return triggered.filter { $0.type == event.type }.count > 0 + } + private func isTriggered(internal event: SplitInternalEvent) -> Bool { - return triggered.filter { $0 == event }.count > 0 + return isTriggered(internal: SplitInternalEventWithMetadata(event, metadata: nil)) } // MARK: Safe Data Access diff --git a/Split/Events/SplitInternalEvent.swift b/Split/Events/SplitInternalEvent.swift index 4c9521204..a26d4ccb6 100644 --- a/Split/Events/SplitInternalEvent.swift +++ b/Split/Events/SplitInternalEvent.swift @@ -7,6 +7,42 @@ import Foundation +// All events (internal & external) support metadata. +// Internal errors are propagated to the customer as events "(.sdkError)". The error info will travel as the event metadata. +struct SplitInternalEventWithMetadata { + let type: SplitInternalEvent + let metadata: EventMetadata? + + init(_ type: SplitInternalEvent, metadata: EventMetadata? = nil) { + self.type = type + self.metadata = metadata + } +} + +@objc public class EventMetadata: NSObject { + var type: EventMetadataType + var data: [String] = [] + + init(type: EventMetadataType, data: [String] = []) { + self.type = type + self.data = data + } +} + +enum EventMetadataType: Int { + case featureFlagsSyncError + case segmentsSyncError + + public func toString() -> String { + switch self { + case .featureFlagsSyncError: + return "FEATURE_FLAGS_SYNC_ERROR" + case .segmentsSyncError: + return "SEGMENTS_SYNC_ERROR" + } + } +} + enum SplitInternalEvent { case mySegmentsUpdated case myLargeSegmentsUpdated @@ -17,4 +53,5 @@ enum SplitInternalEvent { case attributesLoadedFromCache case sdkReadyTimeoutReached case splitKilledNotification + case sdkError } diff --git a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift index c2693d298..95a95d82c 100644 --- a/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift @@ -1,12 +1,10 @@ -// // PeriodicSplitsSyncWorker.swift // Split // // Created by Javier Avrudsky on 26-Sep-2020 -// -// import Foundation + protocol PeriodicTimer { func trigger() func stop() @@ -35,15 +33,13 @@ class DefaultPeriodicTimer: PeriodicTimer { func trigger() { if !isRunning.getAndSet(true) { - fetchTimer.schedule(deadline: .now() + .seconds(deadLineInSecs), - repeating: .seconds(intervalInSecs)) -// fetchTimer.resume() + fetchTimer.schedule(deadline: .now() + .seconds(deadLineInSecs), repeating: .seconds(intervalInSecs)) + // fetchTimer.resume() } } func stop() { - // Not suspending the timer to avoid crashes - isRunning.set(false) + isRunning.set(false) // Not suspending the timer to avoid crashes } func destroy() { @@ -61,8 +57,8 @@ class DefaultPeriodicTimer: PeriodicTimer { } protocol PeriodicSyncWorker { - // typealias SyncCompletion = (Bool) -> Void - // var completion: SyncCompletion? { get set } + // typealias SyncCompletion = (Bool) -> Void + // var completion: SyncCompletion? { get set } func start() func pause() func resume() @@ -77,17 +73,17 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker { private let eventsManager: SplitEventsManager private var isPaused: Atomic = Atomic(false) - init(timer: PeriodicTimer, - eventsManager: SplitEventsManager) { + init(timer: PeriodicTimer, eventsManager: SplitEventsManager) { self.eventsManager = eventsManager self.fetchTimer = timer + self.fetchTimer.handler { [weak self] in - guard let self = self else { - return - } - if self.isPaused.value { - return + guard let self = self else { return } + + if !self.isPaused.value { + self.isPaused.set(true) } + self.fetchQueue.async { self.fetchFromRemote() } @@ -130,13 +126,17 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker { Logger.i("Fetch from remote not implemented") } - func notifyUpdate(_ events: [SplitInternalEvent]) { - events.forEach { - eventsManager.notifyInternalEvent($0) - } + func notifyUpdate(_ event: SplitInternalEvent) { + let withMetadata = SplitInternalEventWithMetadata(event, metadata: nil) + notifyUpdate(withMetadata) + } + + func notifyUpdate(_ event: SplitInternalEventWithMetadata) { + eventsManager.notifyInternalEvent(event) } } +// MARK: Sync Splits (Targeting Rules) class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { private let splitFetcher: HttpSplitFetcher @@ -146,49 +146,44 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker { private let ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor private let syncHelper: SplitsSyncHelper - init(splitFetcher: HttpSplitFetcher, - splitsStorage: SplitsStorage, - generalInfoStorage: GeneralInfoStorage, - ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, - splitChangeProcessor: SplitChangeProcessor, - ruleBasedSegmentsChangeProcessor: RuleBasedSegmentChangeProcessor, - timer: PeriodicTimer, - eventsManager: SplitEventsManager, - splitConfig: SplitClientConfig) { - + init(splitFetcher: HttpSplitFetcher, splitsStorage: SplitsStorage, generalInfoStorage: GeneralInfoStorage, 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, - generalInfoStorage: generalInfoStorage, - splitConfig: splitConfig) - super.init(timer: timer, - eventsManager: eventsManager) + self.syncHelper = SplitsSyncHelper(splitFetcher: splitFetcher, splitsStorage: splitsStorage, ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, ruleBasedSegmentsChangeProcessor: ruleBasedSegmentsChangeProcessor, generalInfoStorage: generalInfoStorage, splitConfig: splitConfig) + + super.init(timer: timer, eventsManager: eventsManager) } override func fetchFromRemote() { - // Polling should be done once sdk ready is fired in initial sync - if !isSdkReadyFired() { - return - } + if !isSdkReadyFired() { return } // Polling should be done once sdk ready is fired in initial sync let changeNumber = splitsStorage.changeNumber let rbChangeNumber: Int64 = ruleBasedSegmentsStorage.changeNumber + + // 1. Try to Sync guard let result = try? syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber) else { + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) + notifyUpdate(event) return } + + // 2. Process Result if result.success, result.featureFlagsUpdated || result.rbsUpdated { - notifyUpdate([.splitsUpdated]) + // Success + notifyUpdate(.splitsUpdated) + } else if !result.success { + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) + notifyUpdate(event) } } } +// MARK: Sync Segments class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { private let mySegmentsStorage: ByKeyMySegmentsStorage @@ -196,20 +191,13 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { private let telemetryProducer: TelemetryRuntimeProducer? private let syncHelper: SegmentsSyncHelper - init(mySegmentsStorage: ByKeyMySegmentsStorage, - myLargeSegmentsStorage: ByKeyMySegmentsStorage, - telemetryProducer: TelemetryRuntimeProducer?, - timer: PeriodicTimer, - eventsManager: SplitEventsManager, - syncHelper: SegmentsSyncHelper) { - + init(mySegmentsStorage: ByKeyMySegmentsStorage, myLargeSegmentsStorage: ByKeyMySegmentsStorage, telemetryProducer: TelemetryRuntimeProducer?, timer: PeriodicTimer, eventsManager: SplitEventsManager, syncHelper: SegmentsSyncHelper) { self.mySegmentsStorage = mySegmentsStorage self.myLargeSegmentsStorage = myLargeSegmentsStorage self.telemetryProducer = telemetryProducer self.syncHelper = syncHelper - super.init(timer: timer, - eventsManager: eventsManager) + super.init(timer: timer, eventsManager: eventsManager) } override func fetchFromRemote() { @@ -220,17 +208,25 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker { } do { - let result = try syncHelper.sync(msTill: mySegmentsStorage.changeNumber, - mlsTill: myLargeSegmentsStorage.changeNumber, - headers: nil) + // 1. Try to sync + let result = try syncHelper.sync(msTill: mySegmentsStorage.changeNumber, mlsTill: myLargeSegmentsStorage.changeNumber, headers: nil) + + // 2. Process result if result.success { + // Success if result.msUpdated || result.mlsUpdated { - // For now is not necessary specify which entity was updated - notifyUpdate([.mySegmentsUpdated]) + notifyUpdate(.mySegmentsUpdated) } + } else { + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) + notifyUpdate(event) } } catch { + // Fail Logger.e("Problem fetching segments: %@", error.localizedDescription) + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) + notifyUpdate(event) } } } diff --git a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift index 36973b66b..1a543f403 100644 --- a/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift @@ -1,10 +1,7 @@ -// // RetryableSegmentsSyncWorker.swift // Split // // Created by Javier Avrudsky on 15-Sep-2020 -// -// import Foundation @@ -21,41 +18,38 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker { var changeChecker: MySegmentsChangesChecker - init(telemetryProducer: TelemetryRuntimeProducer?, - eventsManager: SplitEventsManager, - reconnectBackoffCounter: ReconnectBackoffCounter, - avoidCache: Bool, - changeNumbers: SegmentsChangeNumber, - syncHelper: SegmentsSyncHelper) { - + init(telemetryProducer: TelemetryRuntimeProducer?, eventsManager: SplitEventsManager, reconnectBackoffCounter: ReconnectBackoffCounter, avoidCache: Bool, changeNumbers: SegmentsChangeNumber, syncHelper: SegmentsSyncHelper) { self.telemetryProducer = telemetryProducer self.changeChecker = DefaultMySegmentsChangesChecker() self.avoidCache = avoidCache self.changeNumbers = changeNumbers self.syncHelper = syncHelper - super.init(eventsManager: eventsManager, - reconnectBackoffCounter: reconnectBackoffCounter) + super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter) } override func fetchFromRemote() throws -> Bool { do { - let result = try syncHelper.sync(msTill: changeNumbers.msChangeNumber, - mlsTill: changeNumbers.mlsChangeNumber, - headers: getHeaders()) + let result = try syncHelper.sync(msTill: changeNumbers.msChangeNumber, mlsTill: changeNumbers.mlsChangeNumber, headers: getHeaders()) + if result.success { - if !isSdkReadyTriggered() { - // Notifying both to trigger SDK Ready - notifyUpdate([.mySegmentsUpdated]) - notifyUpdate([.myLargeSegmentsUpdated]) - } else if result.msUpdated || result.mlsUpdated { - // For now is not necessary specify which entity was updated - notifyUpdate([.mySegmentsUpdated]) + if !isSdkReadyTriggered() { // Notifying both to trigger SDK Ready + notifyUpdate(.mySegmentsUpdated) + notifyUpdate(.myLargeSegmentsUpdated) + } else if result.msUpdated || result.mlsUpdated { + notifyUpdate(.mySegmentsUpdated) } return true + } else { + // Fail + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) + notifyUpdate(event) } } catch { + // Fail Logger.e("Error while fetching segments in method: \(error.localizedDescription)") + let event = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .segmentsSyncError, data: [])) + notifyUpdate(event) errorHandler?(error) } return false @@ -63,25 +57,10 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker { } private func getHeaders() -> [String: String]? { - return avoidCache ? ServiceConstants.controlNoCacheHeader : nil + avoidCache ? ServiceConstants.controlNoCacheHeader : nil } } -struct SegmentsSyncResult { - let success: Bool - let msChangeNumber: Int64 - let mlsChangeNumber: Int64 - let msUpdated: Bool - let mlsUpdated: Bool -} - -protocol SegmentsSyncHelper { - func sync(msTill: Int64, - mlsTill: Int64, - headers: HttpHeaders?) throws -> SegmentsSyncResult - -} - class DefaultSegmentsSyncHelper: SegmentsSyncHelper { struct FetchResult { let msTill: Int64 @@ -89,33 +68,19 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { let msUpdated: Bool let mlsUdated: Bool } - + private let segmentsFetcher: HttpMySegmentsFetcher private let mySegmentsStorage: ByKeyMySegmentsStorage private let myLargeSegmentsStorage: ByKeyMySegmentsStorage private let splitConfig: SplitClientConfig private let userKey: String private let changeChecker: MySegmentsChangesChecker - - private var maxAttempts: Int { - return splitConfig.cdnByPassMaxAttempts - } - - private var backoffTimeBaseInSecs: Int { - return splitConfig.cdnBackoffTimeBaseInSecs - } - - private var backoffTimeMaxInSecs: Int { - return splitConfig.cdnBackoffTimeMaxInSecs - } - - init(userKey: String, - segmentsFetcher: HttpMySegmentsFetcher, - mySegmentsStorage: ByKeyMySegmentsStorage, - myLargeSegmentsStorage: ByKeyMySegmentsStorage, - changeChecker: MySegmentsChangesChecker, - splitConfig: SplitClientConfig) { - + + private var maxAttempts: Int { splitConfig.cdnByPassMaxAttempts } + private var backoffTimeBaseInSecs: Int { splitConfig.cdnBackoffTimeBaseInSecs } + private var backoffTimeMaxInSecs: Int { splitConfig.cdnBackoffTimeMaxInSecs } + + init(userKey: String, segmentsFetcher: HttpMySegmentsFetcher, mySegmentsStorage: ByKeyMySegmentsStorage, myLargeSegmentsStorage: ByKeyMySegmentsStorage, changeChecker: MySegmentsChangesChecker, splitConfig: SplitClientConfig) { self.userKey = userKey self.segmentsFetcher = segmentsFetcher self.mySegmentsStorage = mySegmentsStorage @@ -123,43 +88,33 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { self.splitConfig = splitConfig self.changeChecker = changeChecker } - - func sync(msTill: Int64 = -1, - mlsTill: Int64 = -1, - headers: HttpHeaders? = nil) throws -> SegmentsSyncResult { + + // MARK: Entry Point + func sync(msTill: Int64 = -1, mlsTill: Int64 = -1, headers: HttpHeaders? = nil) throws -> SegmentsSyncResult { do { - let res = try tryToSync(msTill: msTill, - mlsTill: mlsTill, - headers: headers) - - if res.success { - return res + let result = try tryToSync(msTill: msTill, mlsTill: mlsTill, headers: headers) + + if result.success { + return result + } else { + return try tryToSync(msTill: result.msChangeNumber, mlsTill: result.mlsChangeNumber, headers: headers, useTillParam: true) } - - return try tryToSync(msTill: res.msChangeNumber, - mlsTill: res.mlsChangeNumber, - headers: headers, - useTillParam: true) } catch let error { Logger.e("Problem fetching segments %@", error.localizedDescription) throw error } } - - private func tryToSync(msTill: Int64, - mlsTill: Int64, - headers: HttpHeaders? = nil, - useTillParam: Bool = false) throws -> SegmentsSyncResult { - - let backoffCounter = DefaultReconnectBackoffCounter(backoffBase: backoffTimeBaseInSecs, - maxTimeLimit: backoffTimeMaxInSecs) + + // MARK: Retry Logic w/ Backoff + private func tryToSync(msTill: Int64, mlsTill: Int64, headers: HttpHeaders? = nil, useTillParam: Bool = false) throws -> SegmentsSyncResult { + let backoffCounter = DefaultReconnectBackoffCounter(backoffBase: backoffTimeBaseInSecs, maxTimeLimit: backoffTimeMaxInSecs) var attemptCount = 0 let goalTill = SegmentsChangeNumber(msChangeNumber: msTill, mlsChangeNumber: mlsTill) let till = useTillParam ? goalTill.max() : nil + while attemptCount < maxAttempts { - let result = try fetchUntil(till: till, - headers: headers) - + let result = try fetchUntil(till: till, headers: headers) + if goalReached(goalTill: goalTill, result: result) { return SegmentsSyncResult(success: true, msChangeNumber: result.msTill, @@ -167,67 +122,54 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { msUpdated: result.msUpdated, mlsUpdated: result.mlsUdated) } + + // Backoff attemptCount+=1 if attemptCount < maxAttempts { Thread.sleep(forTimeInterval: backoffCounter.getNextRetryTime()) } } - return SegmentsSyncResult(success: false, - msChangeNumber: -1, - mlsChangeNumber: -1, - msUpdated: false, - mlsUpdated: false) + + return SegmentsSyncResult(success: false, msChangeNumber: -1, mlsChangeNumber: -1, msUpdated: false, mlsUpdated: false) } - - private func fetchUntil(till: Int64?, - headers: HttpHeaders? = nil) throws -> FetchResult { - - let oldChange = SegmentChange(segments: mySegmentsStorage.getAll().asArray(), - changeNumber: mySegmentsStorage.changeNumber) - - let oldLargeChange = SegmentChange(segments: myLargeSegmentsStorage.getAll().asArray(), - changeNumber: myLargeSegmentsStorage.changeNumber) - - var prevChange = AllSegmentsChange(mySegmentsChange: oldChange, - myLargeSegmentsChange: oldLargeChange) + + // MARK: Sync loop (check if outdated, and re-sync until latest version) + private func fetchUntil(till: Int64?, headers: HttpHeaders? = nil) throws -> FetchResult { + + let oldChange = SegmentChange(segments: mySegmentsStorage.getAll().asArray(), changeNumber: mySegmentsStorage.changeNumber) + let oldLargeChange = SegmentChange(segments: myLargeSegmentsStorage.getAll().asArray(), changeNumber: myLargeSegmentsStorage.changeNumber) + var prevChange = AllSegmentsChange(mySegmentsChange: oldChange, myLargeSegmentsChange: oldLargeChange) + while true { - guard let change = try segmentsFetcher.execute(userKey: userKey, - till: till, - headers: headers) else { + guard let change = try segmentsFetcher.execute(userKey: userKey, till: till, headers: headers) else { throw HttpError.unknown(code: -1, message: "Segment result is null") } - - let mySegmentsChange = change.mySegmentsChange - let myLargeSegmentsChange = change.myLargeSegmentsChange - + if !isOutdated(change, prevChange) { - let msChanged = changeChecker.mySegmentsHaveChanged(old: oldChange, - new: mySegmentsChange) - let mlsChanged = changeChecker.mySegmentsHaveChanged(old: oldLargeChange, - new: myLargeSegmentsChange) + let msChanged = changeChecker.mySegmentsHaveChanged(old: oldChange, new: change.mySegmentsChange) + let mlsChanged = changeChecker.mySegmentsHaveChanged(old: oldLargeChange, new: change.myLargeSegmentsChange) Logger.d("Checking my segments update") - checkAndUpdate(isChanged: msChanged, change: mySegmentsChange, storage: mySegmentsStorage) + checkAndUpdate(isChanged: msChanged, change: change.mySegmentsChange, storage: mySegmentsStorage) Logger.d("Checking my large segments update") - checkAndUpdate(isChanged: mlsChanged, change: myLargeSegmentsChange, storage: myLargeSegmentsStorage) - - return FetchResult(msTill: mySegmentsChange.unwrappedChangeNumber, - mlsTill: myLargeSegmentsChange.unwrappedChangeNumber, + checkAndUpdate(isChanged: mlsChanged, change: change.myLargeSegmentsChange, storage: myLargeSegmentsStorage) + + return FetchResult(msTill: change.mySegmentsChange.unwrappedChangeNumber, + mlsTill: change.myLargeSegmentsChange.unwrappedChangeNumber, msUpdated: msChanged, mlsUdated: mlsChanged) } prevChange = change } } +} - private func isOutdated(_ change: AllSegmentsChange, - _ prevChange: AllSegmentsChange?) -> Bool { - - guard let prevChange = prevChange else { - return true - } +// MARK: Segments Sync Helpers +extension DefaultSegmentsSyncHelper { + private func isOutdated(_ change: AllSegmentsChange, _ prevChange: AllSegmentsChange?) -> Bool { + guard let prevChange = prevChange else { return true } return change.changeNumbers.msChangeNumber < prevChange.changeNumbers.msChangeNumber || - change.changeNumbers.mlsChangeNumber < prevChange.changeNumbers.mlsChangeNumber + change.changeNumbers.mlsChangeNumber < prevChange.changeNumbers.mlsChangeNumber } private func checkAndUpdate(isChanged: Bool, change: SegmentChange, storage: ByKeyMySegmentsStorage) { @@ -239,6 +181,18 @@ class DefaultSegmentsSyncHelper: SegmentsSyncHelper { } private func goalReached(goalTill: SegmentsChangeNumber, result: FetchResult) -> Bool { - return (result.msTill >= goalTill.msChangeNumber && result.mlsTill >= goalTill.mlsChangeNumber) + (result.msTill >= goalTill.msChangeNumber && result.mlsTill >= goalTill.mlsChangeNumber) } } + +struct SegmentsSyncResult { + let success: Bool + let msChangeNumber: Int64 + let mlsChangeNumber: Int64 + let msUpdated: Bool + let mlsUpdated: Bool +} + +protocol SegmentsSyncHelper { + func sync(msTill: Int64, mlsTill: Int64, headers: HttpHeaders?) throws -> SegmentsSyncResult +} diff --git a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift index 28c0a6f9c..b9d700cc9 100644 --- a/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift +++ b/Split/FetcherEngine/Refresh/RetryableSyncWorker.swift @@ -1,10 +1,7 @@ -// // RetryableSyncWorker.swift // Split // // Created by Javier Avrudsky on 15-Sep-2020 -// -// import Foundation @@ -32,9 +29,7 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { private var isRunning: Atomic = Atomic(false) private let syncQueue = DispatchQueue.general - init(eventsManager: SplitEventsManager, - reconnectBackoffCounter: ReconnectBackoffCounter) { - + init(eventsManager: SplitEventsManager, reconnectBackoffCounter: ReconnectBackoffCounter) { self.eventsManager = eventsManager self.reconnectBackoffCounter = reconnectBackoffCounter } @@ -42,11 +37,13 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { func start() { syncQueue.async { [weak self] in guard let self = self else { return } - if self.isRunning.value { - return + + if !self.isRunning.value { + self.isRunning.set(true) } - self.isRunning.set(true) + self.reconnectBackoffCounter.resetCounter() + do { try self.fetchFromRemoteLoop() } catch { @@ -65,6 +62,7 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { private func fetchFromRemoteLoop() throws { var success = false + while isRunning.value, !success { success = try fetchFromRemote() if !success { @@ -73,16 +71,18 @@ class BaseRetryableSyncWorker: RetryableSyncWorker { ThreadUtils.delay(seconds: retryTimeInSeconds) } } - self.isRunning.set(false) + isRunning.set(false) if let handler = completion { handler(success) } } - func notifyUpdate(_ events: [SplitInternalEvent]) { - events.forEach { - eventsManager.notifyInternalEvent($0) - } + func notifyUpdate(_ event: SplitInternalEvent) { + notifyUpdate(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + func notifyUpdate(_ event: SplitInternalEventWithMetadata) { + eventsManager.notifyInternalEvent(event) } func isSdkReadyTriggered() -> Bool { @@ -109,47 +109,43 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker { private let ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor private let syncHelper: SplitsSyncHelper - init(splitFetcher: HttpSplitFetcher, - splitsStorage: SplitsStorage, - generalInfoStorage: GeneralInfoStorage, - ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, - splitChangeProcessor: SplitChangeProcessor, - ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor, - eventsManager: SplitEventsManager, - reconnectBackoffCounter: ReconnectBackoffCounter, - splitConfig: SplitClientConfig) { - + init(splitFetcher: HttpSplitFetcher, splitsStorage: SplitsStorage, generalInfoStorage: GeneralInfoStorage, 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, - generalInfoStorage: generalInfoStorage, - splitConfig: splitConfig) - super.init(eventsManager: eventsManager, - reconnectBackoffCounter: reconnectBackoffCounter) + self.syncHelper = SplitsSyncHelper(splitFetcher: splitFetcher, splitsStorage: splitsStorage, ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, splitChangeProcessor: splitChangeProcessor, ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, generalInfoStorage: generalInfoStorage,splitConfig: splitConfig) + + super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter) } override func fetchFromRemote() throws -> Bool { do { let changeNumber = splitsStorage.changeNumber let rbChangeNumber = ruleBasedSegmentsStorage.changeNumber + + // 1. Try to Sync let result = try syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber, clearBeforeUpdate: false) + + // 2. Process Result if result.success { - if !isSdkReadyTriggered() || - result.featureFlagsUpdated { - notifyUpdate([.splitsUpdated]) + // Success + if !isSdkReadyTriggered() || result.featureFlagsUpdated { + notifyUpdate(.splitsUpdated) } resetBackoffCounter() return true + } else { + // Fail + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) + notifyUpdate(eventWithMetadata) } } catch { + // Fail Logger.e("Error while fetching splits in method: \(error.localizedDescription)") + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) + notifyUpdate(eventWithMetadata) errorHandler?(error) } return false @@ -167,17 +163,7 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { private let syncHelper: SplitsSyncHelper var changeChecker: SplitsChangesChecker - init(splitsFetcher: HttpSplitFetcher, - splitsStorage: SplitsStorage, - ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, - generalInfoStorage: GeneralInfoStorage, - splitChangeProcessor: SplitChangeProcessor, - ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor, - changeNumber: SplitsUpdateChangeNumber, - eventsManager: SplitEventsManager, - reconnectBackoffCounter: ReconnectBackoffCounter, - splitConfig: SplitClientConfig) { - + init(splitsFetcher: HttpSplitFetcher, splitsStorage: SplitsStorage, ruleBasedSegmentsStorage: RuleBasedSegmentsStorage, generalInfoStorage: GeneralInfoStorage, splitChangeProcessor: SplitChangeProcessor, ruleBasedSegmentChangeProcessor: RuleBasedSegmentChangeProcessor, changeNumber: SplitsUpdateChangeNumber, eventsManager: SplitEventsManager, reconnectBackoffCounter: ReconnectBackoffCounter, splitConfig: SplitClientConfig) { self.splitsFetcher = splitsFetcher self.splitsStorage = splitsStorage self.ruleBasedSegmentsStorage = ruleBasedSegmentsStorage @@ -185,46 +171,41 @@ class RetryableSplitsUpdateWorker: BaseRetryableSyncWorker { self.ruleBasedSegmentChangeProcessor = ruleBasedSegmentChangeProcessor self.changeNumber = changeNumber self.changeChecker = DefaultSplitsChangesChecker() - - self.syncHelper = SplitsSyncHelper(splitFetcher: splitsFetcher, - splitsStorage: splitsStorage, - ruleBasedSegmentsStorage: ruleBasedSegmentsStorage, - splitChangeProcessor: splitChangeProcessor, - ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor, - generalInfoStorage: generalInfoStorage, - splitConfig: splitConfig) - super.init(eventsManager: eventsManager, - reconnectBackoffCounter: reconnectBackoffCounter) + self.syncHelper = SplitsSyncHelper(splitFetcher: splitsFetcher, splitsStorage: splitsStorage, ruleBasedSegmentsStorage: ruleBasedSegmentsStorage,splitChangeProcessor: splitChangeProcessor, ruleBasedSegmentsChangeProcessor: ruleBasedSegmentChangeProcessor,generalInfoStorage: generalInfoStorage, splitConfig: splitConfig) + + super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter) } override func fetchFromRemote() throws -> Bool { + let storedChangeNumber = splitsStorage.changeNumber let flagsChangeNumber = changeNumber.flags - if let flagsChangeNumber, flagsChangeNumber <= storedChangeNumber { - return true - } + if let flagsChangeNumber, flagsChangeNumber <= storedChangeNumber { return true } let storedRbChangeNumber: Int64 = ruleBasedSegmentsStorage.changeNumber let rbsChangeNumber = changeNumber.rbs - if let rbsChangeNumber, rbsChangeNumber <= storedRbChangeNumber { - return true - } + if let rbsChangeNumber, rbsChangeNumber <= storedRbChangeNumber { return true } do { - let result = try syncHelper.sync(since: storedChangeNumber, - rbSince: storedRbChangeNumber, - till: flagsChangeNumber ?? rbsChangeNumber, - clearBeforeUpdate: false, - headers: ServiceConstants.controlNoCacheHeader) + // 1. Try to sync + let result = try syncHelper.sync(since: storedChangeNumber, rbSince: storedRbChangeNumber, till: flagsChangeNumber ?? rbsChangeNumber, clearBeforeUpdate: false, headers: ServiceConstants.controlNoCacheHeader) + + // 2. Process result if result.success { - if result.featureFlagsUpdated { - notifyUpdate([.splitsUpdated]) - } + // Success + if result.featureFlagsUpdated { notifyUpdate(.splitsUpdated) } resetBackoffCounter() return true + } else { + // Fail + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) + notifyUpdate(eventWithMetadata) } } catch { + // Fail Logger.e("Error while fetching splits in method \(#function): \(error.localizedDescription)") + let eventWithMetadata = SplitInternalEventWithMetadata(.sdkError, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) + notifyUpdate(eventWithMetadata) errorHandler?(error) } Logger.d("Feature flag changes are not updated yet") diff --git a/Split/Network/Streaming/SyncUpdateWorker.swift b/Split/Network/Streaming/SyncUpdateWorker.swift index 5aede0758..6b148d204 100644 --- a/Split/Network/Streaming/SyncUpdateWorker.swift +++ b/Split/Network/Streaming/SyncUpdateWorker.swift @@ -129,9 +129,9 @@ class SplitsUpdateWorker: UpdateWorker { previousChangeNumber: Int64, changeNumber: Int64) -> Bool { do { - let split = try self.payloadDecoder.decode( + let split = try payloadDecoder.decode( payload: payload, - compressionUtil: self.decomProvider.decompressor(for: compressionType)) + compressionUtil: decomProvider.decompressor(for: compressionType)) if !allRuleBasedSegmentsExist(in: split) { return false @@ -143,11 +143,11 @@ class SplitsUpdateWorker: UpdateWorker { Logger.v("Split update received: \(change)") - if self.splitsStorage.update(splitChange: self.splitChangeProcessor.process(change)) { - self.synchronizer.notifyFeatureFlagsUpdated() + if self.splitsStorage.update(splitChange: splitChangeProcessor.process(change)) { + synchronizer.notifyFeatureFlagsUpdated() } - self.telemetryProducer?.recordUpdatesFromSse(type: .splits) + telemetryProducer?.recordUpdatesFromSse(type: .splits) return true } catch { Logger.e("Error decoding feature flags payload from notification: \(error)") @@ -161,9 +161,9 @@ class SplitsUpdateWorker: UpdateWorker { previousChangeNumber: Int64, changeNumber: Int64) -> Bool { do { - let rbs = try self.ruleBasedSegmentsPayloadDecoder.decode( + let rbs = try ruleBasedSegmentsPayloadDecoder.decode( payload: payload, - compressionUtil: self.decomProvider.decompressor(for: compressionType)) + compressionUtil: decomProvider.decompressor(for: compressionType)) let change = RuleBasedSegmentChange(segments: [rbs], since: previousChangeNumber, @@ -173,13 +173,13 @@ class SplitsUpdateWorker: UpdateWorker { let processedChange = ruleBasedSegmentsChangeProcessor.process(change) - if self.ruleBasedSegmentsStorage.update(toAdd: processedChange.toAdd, + if ruleBasedSegmentsStorage.update(toAdd: processedChange.toAdd, toRemove: processedChange.toRemove, changeNumber: processedChange.changeNumber) { - self.synchronizer.notifyFeatureFlagsUpdated() + synchronizer.notifyFeatureFlagsUpdated() } - self.telemetryProducer?.recordUpdatesFromSse(type: .splits) + telemetryProducer?.recordUpdatesFromSse(type: .splits) return true } catch { Logger.e("Error decoding rule based segments payload from notification: \(error)") diff --git a/SplitTests/Collections/BlockingQueueTest.swift b/SplitTests/Collections/BlockingQueueTest.swift index 03ad78675..7e6b97b69 100644 --- a/SplitTests/Collections/BlockingQueueTest.swift +++ b/SplitTests/Collections/BlockingQueueTest.swift @@ -25,7 +25,7 @@ class BlockingQueueTest: XCTestCase { while true { do { let event = try queue.take() - local.append(event) + local.append(event.type) if local.count == 4 { endExp.fulfill() } @@ -34,9 +34,9 @@ class BlockingQueueTest: XCTestCase { } } globalQ.asyncAfter(deadline: .now() + 1) { - queue.add(SplitInternalEvent.mySegmentsLoadedFromCache) + queue.add(.mySegmentsLoadedFromCache) globalQ.asyncAfter(deadline: .now() + 1) { - queue.add(SplitInternalEvent.splitsLoadedFromCache) + queue.add(.splitsLoadedFromCache) } } @@ -60,7 +60,7 @@ class BlockingQueueTest: XCTestCase { while true { do { let event = try queue.take() - local.append(event) + local.append(event.type) } catch BlockingQueueError.noElementAvailable { continue } catch { @@ -107,8 +107,8 @@ class BlockingQueueTest: XCTestCase { for _ in 0..<50000 { do { let event = try queue.take() - local.append(event) - print("Took: \(event)") + local.append(event.type) + print("Took: \(event.type)") } catch { } } @@ -119,8 +119,8 @@ class BlockingQueueTest: XCTestCase { for _ in 0..<50000 { do { let event = try queue.take() - local.append(event) - print("Took QA1: \(event)") + local.append(event.type) + print("Took QA1: \(event.type)") } catch { print("\n\n\nERROR!!!!: \(error) \n\n\n") } @@ -131,9 +131,9 @@ class BlockingQueueTest: XCTestCase { for _ in 0..<50000 { do { let event = try queue.take() - local.append(event) + local.append(event.type) Thread.sleep(forTimeInterval: 0.3) - print("Took QA2: \(event)") + print("Took QA2: \(event.type)") } catch { } } @@ -144,8 +144,8 @@ class BlockingQueueTest: XCTestCase { do { Thread.sleep(forTimeInterval: 0.5) let event = try queue.take() - local.append(event) - print("Took QA3: \(event)") + local.append(event.type) + print("Took QA3: \(event.type)") } catch { } } @@ -153,7 +153,7 @@ class BlockingQueueTest: XCTestCase { qu1.async { for _ in 1..<100000 { - queue.add(SplitInternalEvent.splitsUpdated) + queue.add(.splitsUpdated) print("qu1 add") Thread.sleep(forTimeInterval: 0.2) } @@ -162,7 +162,7 @@ class BlockingQueueTest: XCTestCase { qu2.async { for _ in 1..<10000 { print("qu2 add") - queue.add(SplitInternalEvent.sdkReadyTimeoutReached) + queue.add(.sdkReadyTimeoutReached) Thread.sleep(forTimeInterval: 0.5) } } @@ -170,7 +170,7 @@ class BlockingQueueTest: XCTestCase { qu3.async { for _ in 1..<10000 { print("qu3 add") - queue.add(SplitInternalEvent.splitsUpdated) + queue.add(.splitsUpdated) Thread.sleep(forTimeInterval: 0.8) } } @@ -178,7 +178,7 @@ class BlockingQueueTest: XCTestCase { qu4.async { for _ in 1..<10000 { print("qu4 add") - queue.add(SplitInternalEvent.mySegmentsUpdated) + queue.add(.mySegmentsUpdated) sleep(1) } } diff --git a/SplitTests/Fake/InternalSplitClientStub.swift b/SplitTests/Fake/InternalSplitClientStub.swift index 5b792fb3e..26fc89caa 100644 --- a/SplitTests/Fake/InternalSplitClientStub.swift +++ b/SplitTests/Fake/InternalSplitClientStub.swift @@ -103,6 +103,18 @@ class InternalSplitClientStub: InternalSplitClient { func on(event: SplitEvent, execute action: @escaping SplitAction) { } + + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } func track(trafficType: String, eventType: String) -> Bool { return true diff --git a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift index 94cbb8991..cd32813fd 100644 --- a/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift +++ b/SplitTests/Fake/Service/SplitEventsManagerCoordinatorStub.swift @@ -25,10 +25,18 @@ class SplitEventsManagerCoordinatorStub: SplitEventsManagerCoordinator { func register(event: SplitEvent, task: SplitEventTask) { } - - var notifiedEvents = Set() + + func register(event: SplitEventWithMetadata, task: SplitEventTask) { + + } + func notifyInternalEvent(_ event: SplitInternalEvent) { - notifiedEvents.insert(IntegrationHelper.describeEvent(event)) + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + var notifiedEvents: [SplitInternalEventWithMetadata] = [] + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { + notifiedEvents.append(event) } var startCalled = false diff --git a/SplitTests/Fake/SplitClientStub.swift b/SplitTests/Fake/SplitClientStub.swift index a19acdcb7..2cb2b2492 100644 --- a/SplitTests/Fake/SplitClientStub.swift +++ b/SplitTests/Fake/SplitClientStub.swift @@ -94,6 +94,18 @@ class SplitClientStub: SplitClient { func on(event: SplitEvent, runInBackground: Bool, queue: DispatchQueue?, execute action: @escaping SplitAction) { } + + func on(event: SplitEvent, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, runInBackground: Bool, executeWithMetadata: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } + + func on(event: SplitEvent, queue: DispatchQueue, action: @escaping SplitActionWithMetadata) { + /* Intentionally unimplemented */ + } func track(trafficType: String, eventType: String) -> Bool { return true diff --git a/SplitTests/Fake/SplitEventsManagerMock.swift b/SplitTests/Fake/SplitEventsManagerMock.swift index 1132148df..a82e250fa 100644 --- a/SplitTests/Fake/SplitEventsManagerMock.swift +++ b/SplitTests/Fake/SplitEventsManagerMock.swift @@ -32,28 +32,36 @@ class SplitEventsManagerMock: SplitEventsManager { var isSdkReadyChecked = false - func notifyInternalEvent(_ event:SplitInternalEvent) { - switch event { - case .mySegmentsUpdated: - isSegmentsReadyFired = true - case .splitsUpdated: - isSplitsReadyFired = true - isSplitUpdatedTriggered = true - if let exp = readyExp { - exp.fulfill() - } - case .sdkReadyTimeoutReached: - isSdkTimeoutFired = true - if let exp = timeoutExp { - exp.fulfill() - } - default: - print("\(event)") + func notifyInternalEvent(_ event: SplitInternalEvent) { + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { + switch event.type { + case .mySegmentsUpdated: + isSegmentsReadyFired = true + case .splitsUpdated: + isSplitsReadyFired = true + isSplitUpdatedTriggered = true + if let exp = readyExp { + exp.fulfill() + } + case .sdkReadyTimeoutReached: + isSdkTimeoutFired = true + if let exp = timeoutExp { + exp.fulfill() + } + default: + print("\(event)") } } - var registeredEvents = [SplitEvent: SplitEventTask]() func register(event: SplitEvent, task: SplitEventTask) { + register(event: SplitEventWithMetadata(event, metadata: nil), task: task) + } + + var registeredEvents = [SplitEventWithMetadata: SplitEventTask]() + func register(event: SplitEventWithMetadata, task: SplitEventTask) { registeredEvents[event] = task } diff --git a/SplitTests/Fake/SplitEventsManagerStub.swift b/SplitTests/Fake/SplitEventsManagerStub.swift index 1d7685dc3..44dc2a4ab 100644 --- a/SplitTests/Fake/SplitEventsManagerStub.swift +++ b/SplitTests/Fake/SplitEventsManagerStub.swift @@ -20,27 +20,35 @@ class SplitEventsManagerStub: SplitEventsManager { var stopCalled = false func notifyInternalEvent(_ event: SplitInternalEvent) { - switch event { - case .mySegmentsLoadedFromCache: - mySegmentsLoadedEventFiredCount+=1 - if let exp = mySegmentsLoadedEventExp { - exp.fulfill() - } - case .splitsLoadedFromCache: - splitsLoadedEventFiredCount+=1 - - case .splitKilledNotification: - splitsKilledEventFiredCount+=1 - - case .splitsUpdated: - splitsUpdatedEventFiredCount+=1 - default: - print("internal event fired: \(event)") + notifyInternalEvent(SplitInternalEventWithMetadata(event, metadata: nil)) + } + + func notifyInternalEvent(_ event: SplitInternalEventWithMetadata) { + switch event.type { + case .mySegmentsLoadedFromCache: + mySegmentsLoadedEventFiredCount+=1 + if let exp = mySegmentsLoadedEventExp { + exp.fulfill() + } + case .splitsLoadedFromCache: + splitsLoadedEventFiredCount+=1 + + case .splitKilledNotification: + splitsKilledEventFiredCount+=1 + + case .splitsUpdated: + splitsUpdatedEventFiredCount+=1 + default: + print("internal event fired: \(event)") } } - var registeredEvents = [SplitEvent: SplitEventTask]() func register(event: SplitEvent, task: SplitEventTask) { + register(event: SplitEventWithMetadata(event, metadata: nil), task: task) + } + + var registeredEvents = [SplitEventWithMetadata: SplitEventTask]() + func register(event: SplitEventWithMetadata, task: SplitEventTask) { registeredEvents[event] = task } diff --git a/SplitTests/Helpers/IntegrationHelper.swift b/SplitTests/Helpers/IntegrationHelper.swift index b75db36cf..6f34a5564 100644 --- a/SplitTests/Helpers/IntegrationHelper.swift +++ b/SplitTests/Helpers/IntegrationHelper.swift @@ -209,24 +209,26 @@ class IntegrationHelper { static func describeEvent(_ event: SplitInternalEvent) -> String { switch event { - case .mySegmentsUpdated: - return "mySegmentsUpdated" - case .splitsUpdated: - return "splitsUpdated" - case .mySegmentsLoadedFromCache: - return "mySegmentsLoadedFromCache" - case .splitsLoadedFromCache: - return "splitsLoadedFromCache" - case .attributesLoadedFromCache: - return "attributesLoadedFromCache" - case .sdkReadyTimeoutReached: - return "sdkReadyTimeoutReached" - case .splitKilledNotification: - return "splitKilledNotification" - case .myLargeSegmentsUpdated: - return "myLargeSegmentsUpdated" - case .myLargeSegmentsLoadedFromCache: - return "myLargeSegmentsLoadedFromCache" + case .mySegmentsUpdated: + return "mySegmentsUpdated" + case .splitsUpdated: + return "splitsUpdated" + case .mySegmentsLoadedFromCache: + return "mySegmentsLoadedFromCache" + case .splitsLoadedFromCache: + return "splitsLoadedFromCache" + case .attributesLoadedFromCache: + return "attributesLoadedFromCache" + case .sdkReadyTimeoutReached: + return "sdkReadyTimeoutReached" + case .splitKilledNotification: + return "splitKilledNotification" + case .myLargeSegmentsUpdated: + return "myLargeSegmentsUpdated" + case .myLargeSegmentsLoadedFromCache: + return "myLargeSegmentsLoadedFromCache" + case .sdkError: + return "sdkError" } } diff --git a/SplitTests/Init/SplitClientTests.swift b/SplitTests/Init/SplitClientTests.swift index 25c6b3260..4a831906f 100644 --- a/SplitTests/Init/SplitClientTests.swift +++ b/SplitTests/Init/SplitClientTests.swift @@ -45,7 +45,7 @@ class SplitClientTests: XCTestCase { } for event in events { - guard let task = eventsManager.registeredEvents[event] else { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { XCTAssertTrue(false) continue } @@ -61,7 +61,7 @@ class SplitClientTests: XCTestCase { } for event in events { - guard let task = eventsManager.registeredEvents[event] else { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { XCTAssertTrue(false) continue } @@ -70,6 +70,42 @@ class SplitClientTests: XCTestCase { XCTAssertNil(task.takeQueue()) } } + + func testOnBgWithMetadata() { + for event in events { + client.on(event: event, executeWithMetadata: { metadata in + print("Metadata: \(metadata.data)") + }) + } + + for event in events { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { + XCTAssertTrue(false) + continue + } + + XCTAssertEqual(true, task.runInBackground) + XCTAssertNil(task.takeQueue()) + } + } + + func testOnMainWithMetadata() { + for event in events { + client.on(event: event, runInBackground: false) { metadata in + print("Metadata: \(metadata.data)") + } + } + + for event in events { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { + XCTAssertTrue(false) + continue + } + + XCTAssertEqual(false, task.runInBackground) + XCTAssertNil(task.takeQueue()) + } + } func testOnQueue() { for event in events { @@ -77,7 +113,7 @@ class SplitClientTests: XCTestCase { } for event in events { - guard let task = eventsManager.registeredEvents[event] else { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { XCTAssertTrue(false) continue } @@ -86,6 +122,21 @@ class SplitClientTests: XCTestCase { XCTAssertNotNil(task.takeQueue()) } } + + func testOnQueueWithMetadata() { + for event in events { + client.on(event: event, queue: DispatchQueue(label: "queuemetadata1"), action: { _ in print("exec")}) + } + + for event in events { + guard let task = eventsManager.registeredEvents.first(where: { $0.key.type == event })?.value else { + XCTAssertTrue(false) + continue + } + + XCTAssertNotNil(task.takeQueue()) + } + } func testGetTreatmentWithEvaluationOptions() { testEvaluationOptionsPassedCorrectly( diff --git a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift index 63169787f..f6723f9d2 100644 --- a/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift +++ b/SplitTests/Integration/Sync/SplitChangesServerErrorTest.swift @@ -24,24 +24,222 @@ class SplitChangesServerErrorTest: XCTestCase { ] var serverUrl = "localhost" - let impExp = XCTestExpectation(description: "impressions") - var impHit: [ImpressionsTest]? + // Client config var httpClient: HttpClient! var streamingBinding: TestStreamResponseBinding? - + var splitConfig: SplitClientConfig? + var apiKey = "99049fd8653247c5ea42bc3c1ae2c6a42bc3_f" + var key: Key = Key(matchingKey: "CUSTOMER_ID", bucketingKey: nil) + let builder = DefaultSplitFactoryBuilder() + override func setUp() { + splitConfig = SplitClientConfig() + splitConfig!.streamingEnabled = false + splitConfig!.featuresRefreshRate = 3 + splitConfig!.impressionRefreshRate = kNeverRefreshRate + splitConfig!.sdkReadyTimeOut = 60000 + splitConfig!.trafficType = "client" + splitConfig!.streamingEnabled = false + splitConfig!.serviceEndpoints = ServiceEndpoints.builder().set(sdkEndpoint: serverUrl).set(eventsEndpoint: serverUrl).build() + _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesServerErrorTest")) + + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: buildTestDispatcher(), streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + } + + // MARK: Getting changes from server and test treatments and change number + func testChangesError() throws { + var treatments = [String]() + let sdkReady = XCTestExpectation(description: "SDK READY Expectation") + + _ = builder.setHttpClient(httpClient) + var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() + + let client = factory!.client + + var sdkReadyFired = false + + client.on(event: SplitEvent.sdkReady) { + sdkReadyFired = true + sdkReady.fulfill() + } + + wait(for: [sdkReady], timeout: 10) + + for i in 0..<4 { + wait(for: [spExp[i]], timeout: 40) + treatments.append(client.getTreatment("test_feature")) + } + + XCTAssertTrue(sdkReadyFired) + + XCTAssertEqual("on_0", treatments[0]) + XCTAssertEqual("on_0", treatments[1]) + XCTAssertEqual("on_0", treatments[2]) + XCTAssertEqual("off_1", treatments[3]) + + cleanup(client, &factory) + } + + // MARK: Getting segments from server and getting a server error + func testResponseSegmentsSyncError() throws { + + // Networking setup + let dispatcher: HttpClientTestDispatcher = { request in + if request.isSplitEndpoint() { + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_1"))) // OK Splits + } + if request.isMySegmentsEndpoint() { + return TestDispatcherResponse(code: 500) // Error for Segments + } + return TestDispatcherResponse(code: 200) + } + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + + // Client config + _ = builder.setHttpClient(httpClient) + var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() + let client = factory!.client + + let sdkError = XCTestExpectation(description: "SDK ERROR Expectation") + var errorType: EventMetadataType? + + // Listener + client.on(event: .sdkError) { error in + errorType = error.type + sdkError.fulfill() + } + + // Test + wait(for: [sdkError], timeout: 5) + XCTAssertEqual(errorType, .segmentsSyncError) + + cleanup(client, &factory) + } + + // MARK: Getting Flags from server and getting a server error + func testResponseFlagsSyncError() throws { + + // Networking setup + let dispatcher: HttpClientTestDispatcher = { request in + if request.isMySegmentsEndpoint() { + return TestDispatcherResponse(code: 200, data: Data(self.updatedSegments(index: 4).utf8)) // OK Segments + } + return TestDispatcherResponse(code: 500) // Error for Splits + } + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + + // Client config + _ = builder.setHttpClient(httpClient) + var factory: SplitFactory? = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() + let client = factory!.client + + let sdkError = XCTestExpectation(description: "SDK ERROR Expectation") + var errorType: EventMetadataType? + + // Listener + client.on(event: .sdkError) { error in + errorType = error.type + sdkError.fulfill() + } + + // Test + wait(for: [sdkError], timeout: 5) + XCTAssertEqual(errorType, .featureFlagsSyncError) + + cleanup(client, &factory) + } + + // MARK: Getting malformed flags from server + func testResponseFlagsParseError() throws { + + // Networking setup + let dispatcher: HttpClientTestDispatcher = { request in + if request.isMySegmentsEndpoint() { + return TestDispatcherResponse(code: 200, data: Data(self.updatedSegments(index: 4).utf8)) // OK for Segments + } + if request.isSplitEndpoint() { + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("matchers"))) // OK for Splits, but bad JSON + } + return TestDispatcherResponse(code: 200) + } + let session = HttpSessionMock() + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) + httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + + // Client config + _ = builder.setHttpClient(httpClient) + var factory: SplitFactory? = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() + let client = factory!.client + + let sdkError = XCTestExpectation(description: "SDK ERROR Expectation") + var errorType: EventMetadataType? + + // Listener + client.on(event: .sdkError) { error in + errorType = error.type + sdkError.fulfill() + } + + // Test + wait(for: [sdkError], timeout: 5) + XCTAssertEqual(errorType, .featureFlagsSyncError) + + cleanup(client, &factory) + } + + // MARK: Getting malformed segments from server + func testResponseSegmentsParseError() throws { + + // Networking setup + let dispatcher: HttpClientTestDispatcher = { request in + if request.isMySegmentsEndpoint() { + return TestDispatcherResponse(code: 200, data: Data("".utf8)) // OK for Segments, but bad JSON + } + if request.isSplitEndpoint() { + return TestDispatcherResponse(code: 200, data: try? Json.encodeToJsonData(self.loadSplitsChangeFile("splitchanges_int_test"))) // OK Splits + } + return TestDispatcherResponse(code: 500) + } let session = HttpSessionMock() - let reqManager = HttpRequestManagerTestDispatcher(dispatcher: buildTestDispatcher(), - streamingHandler: buildStreamingHandler()) + let reqManager = HttpRequestManagerTestDispatcher(dispatcher: dispatcher, streamingHandler: buildStreamingHandler()) httpClient = DefaultHttpClient(session: session, requestManager: reqManager) + + // Client config + _ = builder.setHttpClient(httpClient) + var factory: SplitFactory? = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig!).build() + let client = factory!.client + + let sdkError = XCTestExpectation(description: "SDK ERROR Expectation") + var errorType: EventMetadataType? + + // Listener + client.on(event: .sdkError) { error in + errorType = error.type + sdkError.fulfill() + } + + // Test + wait(for: [sdkError], timeout: 5) + XCTAssertEqual(errorType, .segmentsSyncError) + + cleanup(client, &factory) } +} +// MARK: Test Helpers +extension SplitChangesServerErrorTest { + private func buildTestDispatcher() -> HttpClientTestDispatcher { - - let respData = responseSplitChanges() + let respData = responseSplitChanges("splitchanges_int_test") var responses = [TestDispatcherResponse]() responses.append(TestDispatcherResponse(code: 200, data: Data(try! Json.encodeToJson( TargetingRulesChange(featureFlags: respData[0], ruleBasedSegments: RuleBasedSegmentChange(segments: [], since: -1, till: -1))).utf8))) @@ -49,8 +247,7 @@ class SplitChangesServerErrorTest: XCTestCase { responses.append(TestDispatcherResponse(code: 500)) responses.append(TestDispatcherResponse(code: 200, data: Data(try! Json.encodeToJson( TargetingRulesChange(featureFlags: respData[1], ruleBasedSegments: RuleBasedSegmentChange(segments: [], since: -1, till: -1))).utf8))) - - + return { request in if request.isSplitEndpoint() { let index = self.reqChangesIndex @@ -93,68 +290,11 @@ class SplitChangesServerErrorTest: XCTestCase { } } - // MARK: Test - /// Getting changes from server and test treatments and change number - func testChangesError() throws { - let apiKey = "99049fd8653247c5ea42bc3c1ae2c6a42bc3_f" - let matchingKey = "CUSTOMER_ID" - let trafficType = "client" - var treatments = [String]() - - let sdkReady = XCTestExpectation(description: "SDK READY Expectation") - - let splitConfig: SplitClientConfig = SplitClientConfig() - splitConfig.streamingEnabled = false - splitConfig.featuresRefreshRate = 3 - splitConfig.impressionRefreshRate = kNeverRefreshRate - splitConfig.sdkReadyTimeOut = 60000 - splitConfig.trafficType = trafficType - splitConfig.streamingEnabled = false - splitConfig.serviceEndpoints = ServiceEndpoints.builder() - .set(sdkEndpoint: serverUrl).set(eventsEndpoint: serverUrl).build() - - let key: Key = Key(matchingKey: matchingKey, bucketingKey: nil) - let builder = DefaultSplitFactoryBuilder() - _ = builder.setTestDatabase(TestingHelper.createTestDatabase(name: "SplitChangesServerErrorTest")) - _ = builder.setHttpClient(httpClient) - var factory = builder.setApiKey(apiKey).setKey(key).setConfig(splitConfig).build() - - let client = factory!.client - - var sdkReadyFired = false - - client.on(event: SplitEvent.sdkReady) { - sdkReadyFired = true - sdkReady.fulfill() - } - - wait(for: [sdkReady], timeout: 10) - - for i in 0..<4 { - wait(for: [spExp[i]], timeout: 40) - treatments.append(client.getTreatment("test_feature")) - } - - XCTAssertTrue(sdkReadyFired) - - XCTAssertEqual("on_0", treatments[0]) - XCTAssertEqual("on_0", treatments[1]) - XCTAssertEqual("on_0", treatments[2]) - XCTAssertEqual("off_1", treatments[3]) - - let semaphore = DispatchSemaphore(value: 0) - client.destroy(completion: { - _ = semaphore.signal() - }) - semaphore.wait() - factory = nil - } - - private func responseSplitChanges() -> [SplitChange] { + private func responseSplitChanges(_ filename: String) -> [SplitChange] { var changes = [SplitChange]() for i in 0..<2 { - let c = loadSplitsChangeFile()! + let c = loadSplitsChangeFile(filename)! var prevChangeNumber = c.since c.since = prevChangeNumber + kChangeNbInterval c.till = c.since @@ -172,13 +312,36 @@ class SplitChangesServerErrorTest: XCTestCase { } return changes } + + private func updatedSegments(index: Int) -> String { + var resp = [String]() + let cn = 5 + for i in (1.. SplitChange? { - return FileHelper.loadSplitChangeFile(sourceClass: self, fileName: "splitchanges_int_test") + private func loadSplitsChangeFile(_ filename: String) -> SplitChange? { + FileHelper.loadSplitChangeFile(sourceClass: self, fileName: filename) } private func buildImpressionsFromJson(content: String) throws -> [ImpressionsTest] { - return try Json.decodeFrom(json: content, to: [ImpressionsTest].self) + try Json.decodeFrom(json: content, to: [ImpressionsTest].self) + } + + private func cleanup(_ client: SplitClient, _ factory: inout SplitFactory?) { + let semaphore = DispatchSemaphore(value: 0) + client.destroy(completion: { + _ = semaphore.signal() + }) + semaphore.wait() + factory = nil } } diff --git a/SplitTests/SplitEventsManagerTest.swift b/SplitTests/SplitEventsManagerTest.swift index 11d8603ae..8efc0267a 100644 --- a/SplitTests/SplitEventsManagerTest.swift +++ b/SplitTests/SplitEventsManagerTest.swift @@ -296,6 +296,26 @@ class SplitEventsManagerTest: XCTestCase { eventManager.stop() } + func testEventWithMetadata() { + + let taskExp = XCTestExpectation() + let data = ["TEST_DATA_123456"] + + // Build Task + let metadata = EventMetadata(type: .featureFlagsSyncError, data: data) + + let handler: SplitActionWithMetadata = { handlerMetadata in + XCTAssertEqual(metadata.type, handlerMetadata.type) + XCTAssertEqual(metadata.data, data) + taskExp.fulfill() + } + let task = SplitEventActionTask(action: handler, event: .sdkReady, runInBackground: false, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey), queue: nil) + + // Run & test + task.run(metadata) + wait(for: [taskExp], timeout: 1) + } + // MARK: Helpers func currentTimestamp() -> Int { return Int(Date().unixTimestamp()) @@ -306,29 +326,22 @@ class SplitEventsManagerTest: XCTestCase { } } -class TestTask: SplitEventTask { - - var event: SplitEvent = .sdkReady +class TestTask: SplitEventActionTask { - var runInBackground: Bool = false - - var queue: DispatchQueue? - var taskTriggered = false let label: String var exp: XCTestExpectation? - init(exp: XCTestExpectation?, label: String = "") { + + init(exp: XCTestExpectation?, label: String = "", action: SplitActionWithMetadata? = nil, metadata: EventMetadata? = nil) { self.exp = exp self.label = label + super.init(action: action ?? { _ in }, event: .sdkReady, factory: SplitFactoryStub(apiKey: IntegrationHelper.dummyApiKey)) } - - func takeQueue() -> DispatchQueue? { - return nil - } - - func run() { + + override func run(_ metadata: EventMetadata?) { print("run: \(self.label)") taskTriggered = true + super.run(metadata) if let exp = self.exp { exp.fulfill() } diff --git a/SplitTests/SplitEventsTests.swift b/SplitTests/SplitEventsTests.swift new file mode 100644 index 000000000..c575d7162 --- /dev/null +++ b/SplitTests/SplitEventsTests.swift @@ -0,0 +1,13 @@ +// Created by Martin Cardozo on 11/08/2025 + +import XCTest +@testable import Split + +class SplitEventsTests: XCTestCase { + func testInternalEventsWithMetadataErrorType() { + var event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .featureFlagsSyncError, data: [])) + XCTAssertEqual(event.metadata!.type.toString(), "FEATURE_FLAGS_SYNC_ERROR") + event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: EventMetadata(type: .segmentsSyncError, data: [])) + XCTAssertEqual(event.metadata!.type.toString(), "SEGMENTS_SYNC_ERROR") + } +}