diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index a0753591..0b991c07 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -540,6 +540,7 @@ 6E424D5326324C4D0081004A /* OptimizelyUserContext+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E86CEA124FDC836005DAFED /* OptimizelyUserContext+ObjC.swift */; }; 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC6DD4024ABF89B0017D296 /* OptimizelyUserContext.swift */; }; 6E424D7626324DBD0081004A /* AtomicArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424D7526324DBD0081004A /* AtomicArrayTests.swift */; }; + 6E474C8D263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E474C8C263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift */; }; 6E593FB625BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */; }; 6E5AB69323F6130D007A82B1 /* OptimizelyClientTests_Init_Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */; }; 6E5AB69423F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */; }; @@ -1329,7 +1330,6 @@ 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */; }; 6E9B115922C5486E00C22D81 /* BatchEventBuilderTests_Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */; }; 6E9B115A22C5486E00C22D81 /* DecisionServiceTests_Others.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */; }; - 6E9B115B22C5486E00C22D81 /* DecisionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199522C5211100B2B157 /* DecisionServiceTests.swift */; }; 6E9B115C22C5486E00C22D81 /* DatafileHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */; }; 6E9B115D22C5486E00C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199722C5211100B2B157 /* BatchEventBuilderTests_EventTags.swift */; }; 6E9B115E22C5486E00C22D81 /* DataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199822C5211100B2B157 /* DataStoreTests.swift */; }; @@ -1354,7 +1354,6 @@ 6E9B117222C5487100C22D81 /* EventDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */; }; 6E9B117322C5487100C22D81 /* BatchEventBuilderTests_Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */; }; 6E9B117422C5487100C22D81 /* DecisionServiceTests_Others.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */; }; - 6E9B117522C5487100C22D81 /* DecisionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199522C5211100B2B157 /* DecisionServiceTests.swift */; }; 6E9B117622C5487100C22D81 /* DatafileHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */; }; 6E9B117722C5487100C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199722C5211100B2B157 /* BatchEventBuilderTests_EventTags.swift */; }; 6E9B117822C5487100C22D81 /* DataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199822C5211100B2B157 /* DataStoreTests.swift */; }; @@ -1838,6 +1837,7 @@ 6E424BFB263228FD0081004A /* AtomicDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicDictionary.swift; sourceTree = ""; }; 6E424C3C263249620081004A /* OptimizelyTests-MultiClients-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-MultiClients-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6E424D7526324DBD0081004A /* AtomicArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicArrayTests.swift; sourceTree = ""; }; + 6E474C8C263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileServiceTests_MultiClients.swift; sourceTree = ""; }; 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Decide.swift; sourceTree = ""; }; 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Sync.swift; sourceTree = ""; }; 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Async.swift; sourceTree = ""; }; @@ -1958,7 +1958,6 @@ 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDispatcherTests.swift; sourceTree = ""; }; 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchEventBuilderTests_Attributes.swift; sourceTree = ""; }; 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests_Others.swift; sourceTree = ""; }; - 6E75199522C5211100B2B157 /* DecisionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests.swift; sourceTree = ""; }; 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatafileHandlerTests.swift; sourceTree = ""; }; 6E75199722C5211100B2B157 /* BatchEventBuilderTests_EventTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchEventBuilderTests_EventTags.swift; sourceTree = ""; }; 6E75199822C5211100B2B157 /* DataStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreTests.swift; sourceTree = ""; }; @@ -2230,6 +2229,7 @@ 6E424D7526324DBD0081004A /* AtomicArrayTests.swift */, 6E2D5DAD26338CA00002077F /* AtomicDictionaryTests.swift */, 6E5D120C2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift */, + 6E474C8C263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift */, ); path = "OptimizelyTests-MultiClients"; sourceTree = ""; @@ -2517,7 +2517,6 @@ 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */, 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */, 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */, - 6E75199522C5211100B2B157 /* DecisionServiceTests.swift */, 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */, 6E75199722C5211100B2B157 /* BatchEventBuilderTests_EventTags.swift */, 6E75199822C5211100B2B157 /* DataStoreTests.swift */, @@ -3707,6 +3706,7 @@ 6E424D0526324B620081004A /* ConditionHolder.swift in Sources */, 6E424D5226324C4D0081004A /* OptimizelyClient+Decide.swift in Sources */, 6E424D0626324B620081004A /* UserAttribute.swift in Sources */, + 6E474C8D263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift in Sources */, 6E8A3D4A2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E424D0726324B620081004A /* Event.swift in Sources */, 6E424D0826324B620081004A /* ProjectConfig.swift in Sources */, @@ -4125,7 +4125,6 @@ 6E9B116E22C5487100C22D81 /* LoggerTests.swift in Sources */, 6E75180D22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E75178722C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, - 6E9B117522C5487100C22D81 /* DecisionServiceTests.swift in Sources */, 6E75179F22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7516BB22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E424C08263228FD0081004A /* AtomicDictionary.swift in Sources */, @@ -4348,7 +4347,6 @@ 6E9B115422C5486E00C22D81 /* LoggerTests.swift in Sources */, 6E7518DF22C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75172D22C520D400B2B157 /* Constants.swift in Sources */, - 6E9B115B22C5486E00C22D81 /* DecisionServiceTests.swift in Sources */, 6E75172122C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E75186722C520D400B2B157 /* Rollout.swift in Sources */, 6E424C01263228FD0081004A /* AtomicDictionary.swift in Sources */, diff --git a/Sources/Extensions/OptimizelyClient+Extension.swift b/Sources/Extensions/OptimizelyClient+Extension.swift index 2541937c..5ae9b58f 100644 --- a/Sources/Extensions/OptimizelyClient+Extension.swift +++ b/Sources/Extensions/OptimizelyClient+Extension.swift @@ -23,19 +23,17 @@ extension OptimizelyClient { datafileHandler: OPTDatafileHandler, decisionService: OPTDecisionService, notificationCenter: OPTNotificationCenter) { - // bind it as a non-singleton. so, we will create an instance anytime injected. - // we don't associate the logger with a sdkKey at this time because not all components are sdkKey specific. - let binder: Binder = Binder(service: OPTLogger.self, factory: type(of: logger).init) + // Register my logger service. Bind it as a non-singleton. So, we will create an instance anytime injected. + // we don't associate the logger with a sdkKey at this time because not all components are sdkKey specific. + HandlerRegistryService.shared.registerBinding(binder: Binder(service: OPTLogger.self, factory: type(of: logger).init)) - // Register my logger service. - HandlerRegistryService.shared.registerBinding(binder: binder) - - // this is bound a reusable singleton. so, if we re-initalize, we will keep this. + // This is bound a reusable singleton. so, if we re-initalize, we will keep this. HandlerRegistryService.shared.registerBinding(binder: Binder(sdkKey: sdkKey, service: OPTNotificationCenter.self, strategy: .reUse, isSingleton: true, inst: notificationCenter)) - // the decision service is also a singleton that will reCreate on re-initalize + + // The decision service is also a singleton that will reCreate on re-initalize HandlerRegistryService.shared.registerBinding(binder: Binder(sdkKey: sdkKey, service: OPTDecisionService.self, strategy: .reUse, isSingleton: true, inst: decisionService)) - // An event dispatcher. We use a singleton and use the same Event dispatcher for all + // An event dispatcher. We use a singleton and use the same Event dispatcher for all // projects. If you change the event dispatcher, you can potentially lose data if you // don't use the same backingstore. HandlerRegistryService.shared.registerBinding(binder: Binder(sdkKey: sdkKey, service: OPTEventDispatcher.self, strategy: .reUse, isSingleton: true, inst: eventDispatcher)) diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 39b7c047..3c5415c3 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -28,6 +28,9 @@ class DefaultDecisionService: OPTDecisionService { let userProfileService: OPTUserProfileService lazy var logger = OPTLoggerFactory.getLogger() + // user-profile-service read-modify-write lock for supporting multiple clients + static let upsRMWLock = DispatchQueue(label: "ups-rmw") + init(userProfileService: OPTUserProfileService) { self.bucketer = DefaultBucketer() self.userProfileService = userProfileService @@ -388,17 +391,19 @@ extension DefaultDecisionService { func saveProfile(userId: String, experimentId: String, variationId: String) { - var profile = userProfileService.lookup(userId: userId) ?? OPTUserProfileService.UPProfile() - - var bucketMap = profile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() - bucketMap[experimentId] = [UserProfileKeys.kVariationId: variationId] - - profile[UserProfileKeys.kBucketMap] = bucketMap - profile[UserProfileKeys.kUserId] = userId - - userProfileService.save(userProfile: profile) - - logger.i(.savedVariationInUserProfile(variationId, experimentId, userId)) + DefaultDecisionService.upsRMWLock.sync { + var profile = self.userProfileService.lookup(userId: userId) ?? OPTUserProfileService.UPProfile() + + var bucketMap = profile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() + bucketMap[experimentId] = [UserProfileKeys.kVariationId: variationId] + + profile[UserProfileKeys.kBucketMap] = bucketMap + profile[UserProfileKeys.kUserId] = userId + + self.userProfileService.save(userProfile: profile) + + self.logger.i(.savedVariationInUserProfile(variationId, experimentId, userId)) + } } } diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index eb6431e5..a77b254d 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -51,17 +51,17 @@ open class OptimizelyClient: NSObject { return HandlerRegistryService.shared.injectEventDispatcher(sdkKey: self.sdkKey) } + public var datafileHandler: OPTDatafileHandler? { + return HandlerRegistryService.shared.injectDatafileHandler(sdkKey: self.sdkKey) + } + lazy var currentDatafileHandler = datafileHandler + // MARK: - Default Services var decisionService: OPTDecisionService { return HandlerRegistryService.shared.injectDecisionService(sdkKey: self.sdkKey)! } - public var datafileHandler: OPTDatafileHandler? { - return HandlerRegistryService.shared.injectDatafileHandler(sdkKey: self.sdkKey) - } - lazy var currentDatafileHandler = datafileHandler - public var notificationCenter: OPTNotificationCenter? { return HandlerRegistryService.shared.injectNotificationCenter(sdkKey: self.sdkKey) } diff --git a/Sources/Utils/HandlerRegistryService.swift b/Sources/Utils/HandlerRegistryService.swift index 6356f8dd..90aba289 100644 --- a/Sources/Utils/HandlerRegistryService.swift +++ b/Sources/Utils/HandlerRegistryService.swift @@ -26,15 +26,12 @@ class HandlerRegistryService { } var binders: AtomicProperty<[ServiceKey: BinderProtocol]> = { - var binders = AtomicProperty<[ServiceKey: BinderProtocol]>() binders.property = [ServiceKey: BinderProtocol]() return binders }() - private init() { - - } + private init() {} func registerBinding(binder: BinderProtocol) { let sk = ServiceKey(service: "\(type(of: binder.service))", sdkKey: binder.sdkKey) @@ -105,8 +102,7 @@ protocol BinderProtocol { var strategy: ReInitializeStrategy { get } var service: Any { get } var isSingleton: Bool { get } - var factory:()->Any? { get } - // var configure:(_ inst:Any?)->Any? { get } + var factory: () -> Any? { get } var instance: Any? { get set } } @@ -114,8 +110,7 @@ struct Binder: BinderProtocol { var sdkKey: String? var service: Any var strategy: ReInitializeStrategy = .reCreate - var factory: (() -> Any?) = { ()->Any? in { return nil as Any? }} - // var configure: ((Any?) -> Any?) = { (_)->Any? in { return nil as Any? }} + var factory: () -> Any? = { return nil as Any? } var isSingleton = false var inst: T? diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests.swift deleted file mode 100644 index 0efeb524..00000000 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright 2019, 2021, Optimizely, Inc. and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest - -class DecisionServiceTests: XCTestCase { - - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() { - let json = "{\"accountId\":\"2360254204\",\"anonymizeIP\":true,\"botFiltering\":true,\"projectId\":\"3918735994\",\"revision\":\"1480511547\",\"version\":\"4\",\"audiences\":[{\"id\":\"3468206642\",\"name\":\"Gryffindors\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"value\":\"Gryffindor\"}]]]},{\"id\":\"3988293898\",\"name\":\"Slytherins\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"value\":\"Slytherin\"}]]]},{\"id\":\"4194404272\",\"name\":\"english_citizens\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"value\":\"English\"}]]]},{\"id\":\"2196265320\",\"name\":\"audience_with_missing_value\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"value\":\"English\"},{\"name\":\"nationality\",\"type\":\"custom_attribute\"}]]]}],\"typedAudiences\":[{\"id\":\"3468206643\",\"name\":\"BOOL\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"booleanKey\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":true}]]]},{\"id\":\"3468206646\",\"name\":\"INTEXACT\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"integerKey\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":1}]]]},{\"id\":\"3468206644\",\"name\":\"INT\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"integerKey\",\"type\":\"custom_attribute\",\"match\":\"gt\",\"value\":1}]]]},{\"id\":\"3468206645\",\"name\":\"DOUBLE\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"doubleKey\",\"type\":\"custom_attribute\",\"match\":\"lt\",\"value\":100}]]]},{\"id\":\"3468206642\",\"name\":\"Gryffindors\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":\"Gryffindor\"}]]]},{\"id\":\"3988293898\",\"name\":\"Slytherins\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"match\":\"substring\",\"value\":\"Slytherin\"}]]]},{\"id\":\"4194404272\",\"name\":\"english_citizens\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":\"English\"}]]]},{\"id\":\"2196265320\",\"name\":\"audience_with_missing_value\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"value\":\"English\"},{\"name\":\"nationality\",\"type\":\"custom_attribute\"}]]]}],\"attributes\":[{\"id\":\"553339214\",\"key\":\"house\"},{\"id\":\"58339410\",\"key\":\"nationality\"},{\"id\":\"583394100\",\"key\":\"$opt_test\"},{\"id\":\"323434545\",\"key\":\"booleanKey\"},{\"id\":\"616727838\",\"key\":\"integerKey\"},{\"id\":\"808797686\",\"key\":\"doubleKey\"},{\"id\":\"808797686\",\"key\":\"\"}],\"events\":[{\"id\":\"3785620495\",\"key\":\"basic_event\",\"experimentIds\":[\"1323241596\",\"2738374745\",\"3042640549\",\"3262035800\",\"3072915611\"]},{\"id\":\"3195631717\",\"key\":\"event_with_paused_experiment\",\"experimentIds\":[\"2667098701\"]},{\"id\":\"1987018666\",\"key\":\"event_with_launched_experiments_only\",\"experimentIds\":[\"3072915611\"]}],\"experiments\":[{\"id\":\"1323241596\",\"key\":\"basic_experiment\",\"layerId\":\"1630555626\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767502\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458314\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767502\",\"endOfRange\":5000},{\"entityId\":\"3433458314\",\"endOfRange\":10000}],\"audienceIds\":[],\"forcedVariations\":{\"Harry Potter\":\"A\",\"Tom Riddle\":\"B\"}},{\"id\":\"1323241597\",\"key\":\"typed_audience_experiment\",\"layerId\":\"1630555627\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767503\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458315\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767503\",\"endOfRange\":5000},{\"entityId\":\"3433458315\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206643\",\"3468206644\",\"3468206646\",\"3468206645\"],\"audienceConditions\":[\"or\",\"3468206643\",\"3468206644\",\"3468206646\",\"3468206645\"],\"forcedVariations\":{}},{\"id\":\"1323241598\",\"key\":\"typed_audience_experiment_with_and\",\"layerId\":\"1630555628\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767504\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458316\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767504\",\"endOfRange\":5000},{\"entityId\":\"3433458316\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206643\",\"3468206644\",\"3468206645\"],\"audienceConditions\":[\"and\",\"3468206643\",\"3468206644\",\"3468206645\"],\"forcedVariations\":{}},{\"id\":\"1323241599\",\"key\":\"typed_audience_experiment_leaf_condition\",\"layerId\":\"1630555629\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767505\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458317\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767505\",\"endOfRange\":5000},{\"entityId\":\"3433458317\",\"endOfRange\":10000}],\"audienceIds\":[],\"audienceConditions\":\"3468206643\",\"forcedVariations\":{}},{\"id\":\"3262035800\",\"key\":\"multivariate_experiment\",\"layerId\":\"3262035800\",\"status\":\"Running\",\"variations\":[{\"id\":\"1880281238\",\"key\":\"Fred\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"F\"},{\"id\":\"4052219963\",\"value\":\"red\"}]},{\"id\":\"3631049532\",\"key\":\"Feorge\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"F\"},{\"id\":\"4052219963\",\"value\":\"eorge\"}]},{\"id\":\"4204375027\",\"key\":\"Gred\",\"featureEnabled\":false,\"variables\":[{\"id\":\"675244127\",\"value\":\"G\"},{\"id\":\"4052219963\",\"value\":\"red\"}]},{\"id\":\"2099211198\",\"key\":\"George\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"G\"},{\"id\":\"4052219963\",\"value\":\"eorge\"}]}],\"trafficAllocation\":[{\"entityId\":\"1880281238\",\"endOfRange\":2500},{\"entityId\":\"3631049532\",\"endOfRange\":5000},{\"entityId\":\"4204375027\",\"endOfRange\":7500},{\"entityId\":\"2099211198\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{\"Fred\":\"Fred\",\"Feorge\":\"Feorge\",\"Gred\":\"Gred\",\"George\":\"George\"}},{\"id\":\"2201520193\",\"key\":\"double_single_variable_feature_experiment\",\"layerId\":\"1278722008\",\"status\":\"Running\",\"variations\":[{\"id\":\"1505457580\",\"key\":\"pi_variation\",\"featureEnabled\":true,\"variables\":[{\"id\":\"4111654444\",\"value\":\"3.14\"}]},{\"id\":\"119616179\",\"key\":\"euler_variation\",\"variables\":[{\"id\":\"4111654444\",\"value\":\"2.718\"}]}],\"trafficAllocation\":[{\"entityId\":\"1505457580\",\"endOfRange\":4000},{\"entityId\":\"119616179\",\"endOfRange\":8000}],\"audienceIds\":[\"3988293898\"],\"forcedVariations\":{}},{\"id\":\"2667098701\",\"key\":\"paused_experiment\",\"layerId\":\"3949273892\",\"status\":\"Paused\",\"variations\":[{\"id\":\"391535909\",\"key\":\"Control\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"391535909\",\"endOfRange\":10000}],\"audienceIds\":[],\"forcedVariations\":{\"Harry Potter\":\"Control\"}},{\"id\":\"3072915611\",\"key\":\"launched_experiment\",\"layerId\":\"3587821424\",\"status\":\"Launched\",\"variations\":[{\"id\":\"1647582435\",\"key\":\"launch_control\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1647582435\",\"endOfRange\":8000}],\"audienceIds\":[],\"forcedVariations\":{}},{\"id\":\"748215081\",\"key\":\"experiment_with_malformed_audience\",\"layerId\":\"1238149537\",\"status\":\"Running\",\"variations\":[{\"id\":\"535538389\",\"key\":\"var1\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"535538389\",\"endOfRange\":10000}],\"audienceIds\":[\"2196265320\"],\"forcedVariations\":{}}],\"groups\":[{\"id\":\"1015968292\",\"policy\":\"random\",\"experiments\":[{\"id\":\"2738374745\",\"key\":\"first_grouped_experiment\",\"layerId\":\"3301900159\",\"status\":\"Running\",\"variations\":[{\"id\":\"2377378132\",\"key\":\"A\",\"variables\":[]},{\"id\":\"1179171250\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"2377378132\",\"endOfRange\":5000},{\"entityId\":\"1179171250\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{\"Harry Potter\":\"A\",\"Tom Riddle\":\"B\"}},{\"id\":\"3042640549\",\"key\":\"second_grouped_experiment\",\"layerId\":\"2625300442\",\"status\":\"Running\",\"variations\":[{\"id\":\"1558539439\",\"key\":\"A\",\"variables\":[]},{\"id\":\"2142748370\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1558539439\",\"endOfRange\":5000},{\"entityId\":\"2142748370\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{\"Hermione Granger\":\"A\",\"Ronald Weasley\":\"B\"}}],\"trafficAllocation\":[{\"entityId\":\"2738374745\",\"endOfRange\":4000},{\"entityId\":\"3042640549\",\"endOfRange\":8000}]},{\"id\":\"2606208781\",\"policy\":\"random\",\"experiments\":[{\"id\":\"4138322202\",\"key\":\"mutex_group_2_experiment_1\",\"layerId\":\"3755588495\",\"status\":\"Running\",\"variations\":[{\"id\":\"1394671166\",\"key\":\"mutex_group_2_experiment_1_variation_1\",\"featureEnabled\":true,\"variables\":[{\"id\":\"2059187672\",\"value\":\"mutex_group_2_experiment_1_variation_1\"}]}],\"audienceIds\":[],\"forcedVariations\":{},\"trafficAllocation\":[{\"entityId\":\"1394671166\",\"endOfRange\":10000}]},{\"id\":\"1786133852\",\"key\":\"mutex_group_2_experiment_2\",\"layerId\":\"3818002538\",\"status\":\"Running\",\"variations\":[{\"id\":\"1619235542\",\"key\":\"mutex_group_2_experiment_2_variation_2\",\"featureEnabled\":true,\"variables\":[{\"id\":\"2059187672\",\"value\":\"mutex_group_2_experiment_2_variation_2\"}]}],\"trafficAllocation\":[{\"entityId\":\"1619235542\",\"endOfRange\":10000}],\"audienceIds\":[],\"forcedVariations\":{}}],\"trafficAllocation\":[{\"entityId\":\"4138322202\",\"endOfRange\":5000},{\"entityId\":\"1786133852\",\"endOfRange\":10000}]}],\"featureFlags\":[{\"id\":\"4195505407\",\"key\":\"boolean_feature\",\"rolloutId\":\"\",\"experimentIds\":[],\"variables\":[]},{\"id\":\"3926744821\",\"key\":\"double_single_variable_feature\",\"rolloutId\":\"\",\"experimentIds\":[\"2201520193\"],\"variables\":[{\"id\":\"4111654444\",\"key\":\"double_variable\",\"type\":\"double\",\"defaultValue\":\"14.99\"}]},{\"id\":\"3281420120\",\"key\":\"integer_single_variable_feature\",\"rolloutId\":\"2048875663\",\"experimentIds\":[],\"variables\":[{\"id\":\"593964691\",\"key\":\"integer_variable\",\"type\":\"integer\",\"defaultValue\":\"7\"}]},{\"id\":\"2591051011\",\"key\":\"boolean_single_variable_feature\",\"rolloutId\":\"\",\"experimentIds\":[],\"variables\":[{\"id\":\"3974680341\",\"key\":\"boolean_variable\",\"type\":\"boolean\",\"defaultValue\":\"true\"}]},{\"id\":\"2079378557\",\"key\":\"string_single_variable_feature\",\"rolloutId\":\"1058508303\",\"experimentIds\":[],\"variables\":[{\"id\":\"2077511132\",\"key\":\"string_variable\",\"type\":\"string\",\"defaultValue\":\"wingardium leviosa\"}]},{\"id\":\"3263342226\",\"key\":\"multi_variate_feature\",\"rolloutId\":\"813411034\",\"experimentIds\":[\"3262035800\"],\"variables\":[{\"id\":\"675244127\",\"key\":\"first_letter\",\"type\":\"string\",\"defaultValue\":\"H\"},{\"id\":\"4052219963\",\"key\":\"rest_of_name\",\"type\":\"string\",\"defaultValue\":\"arry\"}]},{\"id\":\"3263342226\",\"key\":\"mutex_group_feature\",\"rolloutId\":\"\",\"experimentIds\":[\"4138322202\",\"1786133852\"],\"variables\":[{\"id\":\"2059187672\",\"key\":\"correlating_variation_name\",\"type\":\"string\",\"defaultValue\":null}]}],\"rollouts\":[{\"id\":\"1058508303\",\"experiments\":[{\"id\":\"1785077004\",\"key\":\"1785077004\",\"status\":\"Running\",\"layerId\":\"1058508303\",\"audienceIds\":[],\"forcedVariations\":{},\"variations\":[{\"id\":\"1566407342\",\"key\":\"1566407342\",\"featureEnabled\":true,\"variables\":[{\"id\":\"2077511132\",\"value\":\"lumos\"}]}],\"trafficAllocation\":[{\"entityId\":\"1566407342\",\"endOfRange\":5000}]}]},{\"id\":\"813411034\",\"experiments\":[{\"id\":\"3421010877\",\"key\":\"3421010877\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{},\"variations\":[{\"id\":\"521740985\",\"key\":\"521740985\",\"variables\":[{\"id\":\"675244127\",\"value\":\"G\"},{\"id\":\"4052219963\",\"value\":\"odric\"}]}],\"trafficAllocation\":[{\"entityId\":\"521740985\",\"endOfRange\":5000}]},{\"id\":\"600050626\",\"key\":\"600050626\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[\"3988293898\"],\"forcedVariations\":{},\"variations\":[{\"id\":\"180042646\",\"key\":\"180042646\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"S\"},{\"id\":\"4052219963\",\"value\":\"alazar\"}]}],\"trafficAllocation\":[{\"entityId\":\"180042646\",\"endOfRange\":5000}]},{\"id\":\"2637642575\",\"key\":\"2637642575\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[\"4194404272\"],\"forcedVariations\":{},\"variations\":[{\"id\":\"2346257680\",\"key\":\"2346257680\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"D\"},{\"id\":\"4052219963\",\"value\":\"udley\"}]}],\"trafficAllocation\":[{\"entityId\":\"2346257680\",\"endOfRange\":5000}]},{\"id\":\"828245624\",\"key\":\"828245624\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[],\"forcedVariations\":{},\"variations\":[{\"id\":\"3137445031\",\"key\":\"3137445031\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"M\"},{\"id\":\"4052219963\",\"value\":\"uggle\"}]}],\"trafficAllocation\":[{\"entityId\":\"3137445031\",\"endOfRange\":5000}]}]},{\"id\":\"2048875663\",\"experiments\":[{\"id\":\"3794675122\",\"key\":\"3794675122\",\"status\":\"Running\",\"layerId\":\"2048875663\",\"audienceIds\":[],\"forcedVariations\":{},\"variations\":[{\"id\":\"589640735\",\"key\":\"589640735\",\"featureEnabled\":true,\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"589640735\",\"endOfRange\":10000}]}]}],\"variables\":[]}" - - guard let config = try? ProjectConfig(datafile: json) else { - return - } - - let decisionService = DefaultDecisionService(userProfileService: DefaultUserProfileService()) - - let experiment = config.project.experiments.filter({$0.key == "typed_audience_experiment"}).first - let attr = ["integerKey": 1, "doubleKey": 99.0, "booleanKey": true, "nationality": "English"] as [String: Any] - let variation = decisionService.getVariation(config: config, experiment: experiment!, userId: "1234", attributes: attr) - - XCTAssertNotNil(variation) - - XCTAssertNotNil(config) - - } - -} diff --git a/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift index cca47aac..b483e391 100644 --- a/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift @@ -25,7 +25,8 @@ class DatafileHandlerTests_MultiClients: XCTestCase { override func setUp() { OTUtils.bindLoggerForTest(.info) OTUtils.createDocumentDirectoryIfNotAvailable() - + OTUtils.clearAllTestStorage(including: testSdkKeyBasename) + makeSdkKeys(10) } diff --git a/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift new file mode 100644 index 00000000..7fdc6a31 --- /dev/null +++ b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift @@ -0,0 +1,149 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class UserProfileServiceTests_MultiClients: XCTestCase { + + let profiles: [[String: Any]] = [ + [ + "user_id": "0", + "experiment_bucket_map": [ + "1234": [ + "variation_id": "5678" + ] + ] + ], + [ + "user_id": "1", + "experiment_bucket_map": [ + "12345": [ + "variation_id": "56789" + ] + ] + ] + ] + + override func setUpWithError() throws { + OTUtils.bindLoggerForTest(.error) + OTUtils.clearAllUPS() + } + + override func tearDownWithError() throws { + OTUtils.clearAllUPS() + } + + func testConcurrentAccess() { + let ups = DefaultUserProfileService() + let userId1 = "0" + let userId2 = "1" + + let result = OTUtils.runConcurrent(count: 100) { item in + (0..<100).forEach{ _ in + ups.save(userProfile: self.profiles[0]) + ups.save(userProfile: self.profiles[1]) + + let up1 = ups.lookup(userId: userId1)! + let up2 = ups.lookup(userId: userId2)! + + XCTAssertEqual(up1["user_id"] as? String, userId1) + let exp1 = up1["experiment_bucket_map"] as! [String: [String: String]] + XCTAssertEqual(exp1["1234"]!["variation_id"], "5678") + + XCTAssertEqual(up2["user_id"] as? String, userId2) + let exp2 = up2["experiment_bucket_map"] as! [String: [String: String]] + XCTAssertEqual(exp2["12345"]!["variation_id"], "56789") + } + } + + XCTAssertTrue(result, "Concurrent tasks timed out") + } + + func testConcurrentUpdateFromDecisionService() { + // this test is validating thread-safety of read-modify-write UPS operations from decision-service + + let ups = DefaultUserProfileService() // shared instance + let decisionService = DefaultDecisionService(userProfileService: ups) // shared instance + let numThreads = 4 + let numUsers = 2 + let numEventsPerThread = 10 + + let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 10) { thIdx in + for userIdx in 0.. String? { @@ -184,6 +193,10 @@ class OTUtils { ups.save(userProfile: profile) } + static func clearAllUPS() { + UserDefaults.standard.removeObject(forKey: "user-profile-service") + } + // MARK: - events static func makeEventForDispatch(url: String? = nil, event: BatchEvent? = nil) -> EventForDispatch { @@ -206,50 +219,24 @@ class OTUtils { anonymizeIP: true, enrichDecisions: true) } - - // MARK: - files - static func saveAFile(name: String, data: Data) -> URL? { - let ds = DataStoreFile(storeName: name, async: false) - ds.saveItem(forKey: name, value: data) - - return ds.url + static func clearAllEventQueues() { + removeAllFiles(including: "OPTEventQueue", in: .documentDirectory) + removeAllFiles(including: "OPTEventQueue", in: .cachesDirectory) } - static func removeAFile(name: String) -> URL? { - let ds = DataStoreFile(storeName: name, async: false) - ds.removeItem(forKey: name) - - return ds.url - } + // MARK: - datafiles static func createDatafileCache(sdkKey: String, contents: String? = nil) { let data = (contents ?? "datafile-for-\(sdkKey)").data(using: .utf8)! _ = saveAFile(name: sdkKey, data: data) } - static func clearAllTestStorage(including: String) { - removeAllFiles(including: including) - removeAllUserDefaults(including: including) - } - - static func clearAllEventQueues() { - removeAllFiles(including: "OPTEventQueue") - } - - static func removeAllFiles(including: String) { + static func clearAllDataFiles(including: String) { removeAllFiles(including: including, in: .documentDirectory) removeAllFiles(including: including, in: .cachesDirectory) } - static func removeAllUserDefaults(including: String) { - let allKeys = UserDefaults.standard.dictionaryRepresentation().keys - allKeys.filter{ $0.contains(including) }.forEach{ itemKey in - UserDefaults.standard.removeObject(forKey: itemKey) - //print("[OTUtils] removed UserDefaults: '\(itemKey)'") - } - } - static func removeAllFiles(including: String, in directory: FileManager.SearchPathDirectory) { if let docUrl = FileManager.default.urls(for: directory, in: .userDomainMask).first { if let names = try? FileManager.default.contentsOfDirectory(atPath: docUrl.path) { @@ -268,6 +255,26 @@ class OTUtils { } } + static func clearAllLastModifiedData() { + removeAllUserDefaults(including: "LastModified") + } + + // MARK: - files + + static func saveAFile(name: String, data: Data) -> URL? { + let ds = DataStoreFile(storeName: name, async: false) + ds.saveItem(forKey: name, value: data) + + return ds.url + } + + static func removeAFile(name: String) -> URL? { + let ds = DataStoreFile(storeName: name, async: false) + ds.removeItem(forKey: name) + + return ds.url + } + static func createDocumentDirectoryIfNotAvailable() { // documentDirectory may not exist for simulator unit test (iOS11+). create it if not found. @@ -288,6 +295,17 @@ class OTUtils { } } + // MARK: - UserDefaults + + static func removeAllUserDefaults(including: String) { + let allKeys = UserDefaults.standard.dictionaryRepresentation().keys + allKeys.filter{ $0.contains(including) }.forEach{ itemKey in + UserDefaults.standard.removeObject(forKey: itemKey) + //print("[OTUtils] removed UserDefaults: '\(itemKey)'") + } + UserDefaults.standard.synchronize() + } + // MARK: - concurrency static func runConcurrent(for items: [String],