Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(handler-registry-service): add support for multiple clients #406

Merged
merged 28 commits into from
May 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions OptimizelySwiftSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,8 @@
6ECB60D4234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */; };
6ECB60D5234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */; };
6ECB60D7234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60D6234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m */; };
6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */; };
6EE5918E264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE5918D264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift */; };
6EF41A332522BE1900EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */; };
6EF41A422522BE2100EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */; };
6EF8DE0624B8DA58008B9488 /* decide_datafile.json in Resources */ = {isa = PBXBuildFile; fileRef = 6EF8DE0524B8DA58008B9488 /* decide_datafile.json */; };
Expand Down Expand Up @@ -2026,6 +2028,8 @@
6ECB60C5234D329500016D41 /* OptimizelyClientTests_OptimizelyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_OptimizelyConfig.swift; sourceTree = "<group>"; };
6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OptimizelyConfig+ObjC.swift"; sourceTree = "<group>"; };
6ECB60D6234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OptimizelyClientTests_OptimizelyConfig_Objc.m; sourceTree = "<group>"; };
6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests_MultiClients.swift; sourceTree = "<group>"; };
6EE5918D264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandlerRegistryServiceTests_MultiClients.swift; sourceTree = "<group>"; };
6EF8DE0524B8DA58008B9488 /* decide_datafile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_datafile.json; sourceTree = "<group>"; };
6EF8DE0A24BD1BB1008B9488 /* OptimizelyDecision.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyDecision.swift; sourceTree = "<group>"; };
6EF8DE0B24BD1BB2008B9488 /* OptimizelyDecideOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyDecideOption.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2230,6 +2234,8 @@
6E2D5DAD26338CA00002077F /* AtomicDictionaryTests.swift */,
6E5D120C2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift */,
6E474C8C263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift */,
6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */,
6EE5918D264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift */,
);
path = "OptimizelyTests-MultiClients";
sourceTree = "<group>";
Expand Down Expand Up @@ -3687,6 +3693,7 @@
6E424CF426324B620081004A /* DecisionInfo.swift in Sources */,
6E5D120D2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift in Sources */,
6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */,
6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */,
6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */,
6E424CF626324B620081004A /* DefaultNotificationCenter.swift in Sources */,
6E424CF726324B620081004A /* DefaultDecisionService.swift in Sources */,
Expand Down Expand Up @@ -3738,6 +3745,7 @@
6E424CD326324B270081004A /* OptimizelyError.swift in Sources */,
6E424D5326324C4D0081004A /* OptimizelyUserContext+ObjC.swift in Sources */,
6E424CD426324B270081004A /* OptimizelyLogLevel.swift in Sources */,
6EE5918E264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift in Sources */,
6E424CD526324B270081004A /* OptimizelyClient.swift in Sources */,
6E424CD626324B270081004A /* OptimizelyClient+ObjC.swift in Sources */,
6E424CD726324B270081004A /* OptimizelyResult.swift in Sources */,
Expand Down
50 changes: 25 additions & 25 deletions Sources/Utils/HandlerRegistryService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,37 @@
import Foundation

class HandlerRegistryService {

static let shared = HandlerRegistryService()

struct ServiceKey: Hashable {
var service: String
var sdkKey: String?
}

var binders: AtomicProperty<[ServiceKey: BinderProtocol]> = {
var binders = AtomicProperty<[ServiceKey: BinderProtocol]>()
binders.property = [ServiceKey: BinderProtocol]()
return binders
}()
var binders = AtomicProperty(property: [ServiceKey: BinderProtocol]())

private init() {}

func registerBinding(binder: BinderProtocol) {
let sk = ServiceKey(service: "\(type(of: binder.service))", sdkKey: binder.sdkKey)
if binders.property?[sk] != nil {
} else {
binders.property?[sk] = binder
binders.performAtomic{ prop in
if prop[sk] == nil {
prop[sk] = binder
}
}
}

func injectComponent(service: Any, sdkKey: String? = nil, isReintialize: Bool=false) -> Any? {
var result: Any?
// first look up global. Then look up if there is a local.
let skLocal = ServiceKey(service: "\(type(of: service))", sdkKey: sdkKey)
let skGlobal = ServiceKey(service: "\(type(of: service))", sdkKey: nil)

let binderToUse = binders.property?[skLocal] ?? binders.property?[skGlobal]
// service key is shared for all sdkKeys when sdkKey is nil
let sk = ServiceKey(service: "\(type(of: service))", sdkKey: sdkKey)

let binderToUse = binders.property?[sk]

func updateBinder(b: BinderProtocol) {
if binders.property?[skLocal] != nil {
binders.property?[skLocal] = b
} else {
binders.property?[skGlobal] = b
binders.performAtomic{ prop in
prop[sk] = b
}
}

Expand Down Expand Up @@ -110,7 +104,7 @@ struct Binder<T>: BinderProtocol {
var sdkKey: String?
var service: Any
var strategy: ReInitializeStrategy = .reCreate
var factory: () -> Any? = { return nil as Any? }
var factory: () -> Any?
var isSingleton = false
var inst: T?

Expand All @@ -125,33 +119,39 @@ struct Binder<T>: BinderProtocol {
}
}

init(sdkKey: String? = nil, service: Any, strategy: ReInitializeStrategy = .reCreate, factory: @escaping (() -> Any?) = { ()->Any? in { return nil as Any? }}, isSingleton: Bool = false, inst: T? = nil) {
init(sdkKey: String? = nil,
service: Any,
strategy: ReInitializeStrategy = .reCreate,
factory: (() -> Any?)? = nil,
isSingleton: Bool = false,
inst: T? = nil) {

self.sdkKey = sdkKey
self.service = service
self.strategy = strategy
self.factory = factory
self.factory = factory ?? { return nil as Any? }
self.isSingleton = isSingleton
self.inst = inst
}
}

extension HandlerRegistryService {
func injectLogger(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTLogger? {
func injectLogger(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTLogger? {
return injectComponent(service: OPTLogger.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTLogger?
}

func injectNotificationCenter(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTNotificationCenter? {
func injectNotificationCenter(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTNotificationCenter? {
return injectComponent(service: OPTNotificationCenter.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTNotificationCenter?
}
func injectDecisionService(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTDecisionService? {
func injectDecisionService(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTDecisionService? {
return injectComponent(service: OPTDecisionService.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTDecisionService?
}

func injectEventDispatcher(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTEventDispatcher? {
func injectEventDispatcher(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTEventDispatcher? {
return injectComponent(service: OPTEventDispatcher.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTEventDispatcher?
}

func injectDatafileHandler(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTDatafileHandler? {
func injectDatafileHandler(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTDatafileHandler? {
return injectComponent(service: OPTDatafileHandler.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTDatafileHandler?
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//
// 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 HandlerRegistryServiceTests_MultiClients: XCTestCase {

override func setUpWithError() throws {
}

override func tearDownWithError() throws {
}

func testConcurrentAccess_Singleton() {
// this type used for all handlers except for logger

let numThreads = 10
let numEventsPerThread = 100

let registry = HandlerRegistryService.shared

let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 300) { thIdx in
for idx in 0..<numEventsPerThread {
let sdkKey = String(thIdx * numEventsPerThread + idx)
let strategy: ReInitializeStrategy = .reUse
let isSingleton = true
let isReinitialize = false

let service = OPTLogger.self
let componentIn = DefaultLogger()

let binder = Binder(sdkKey: sdkKey,
service: service,
strategy: strategy,
isSingleton: isSingleton,
inst: componentIn)

registry.registerBinding(binder: binder)
if let componentOut = registry.injectComponent(service: service,
sdkKey: sdkKey,
isReintialize: isReinitialize) as? DefaultLogger {
XCTAssertEqual(String(describing: componentOut), String(describing: componentIn))
} else {
self.dumpRegistry()
XCTAssert(false, "injectComponent failed: \(sdkKey) :: \(binder)")
}
}
}

XCTAssertTrue(result, "Concurrent tasks timed out")
}

func testConcurrentAccess_NonSingleton() {
// this type used for loggers

let numThreads = 10
let numEventsPerThread = 100

let registry = HandlerRegistryService.shared

let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 300) { thIdx in
for _ in 0..<numEventsPerThread {
let isReinitialize = false

let service = OPTLogger.self
let componentIn = DefaultLogger()

let binder = Binder<OPTLogger>(service: service,
factory: type(of: componentIn).init)

registry.registerBinding(binder: binder)
if let componentOut = registry.injectComponent(service: service,
isReintialize: isReinitialize) as? DefaultLogger {
XCTAssertEqual(String(describing: componentOut), String(describing: componentIn))
} else {
self.dumpRegistry()
XCTAssert(false, "injectComponent failed: \(binder)")
}
}
}

XCTAssertTrue(result, "Concurrent tasks timed out")
}

func testConcurrentAccess_Random() {
let numThreads = 10
let numEventsPerThread = 100

let registry = HandlerRegistryService.shared

let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 300) { thIdx in
for idx in 0..<numEventsPerThread {
let sdkKey = String(thIdx * numEventsPerThread + idx)
let strategy: ReInitializeStrategy = Bool.random() ? .reCreate : .reUse
let isSingleton = Bool.random()
let isReinitialize = Bool.random()

let service = OPTLogger.self
let componentIn = DefaultLogger()

let binder = Binder(sdkKey: sdkKey,
service: service,
strategy: strategy,
factory: type(of: componentIn).init,
isSingleton: isSingleton,
inst: componentIn)

registry.registerBinding(binder: binder)
if let componentOut = registry.injectComponent(service: service,
sdkKey: sdkKey,
isReintialize: isReinitialize) as? DefaultLogger {
XCTAssertEqual(String(describing: componentOut), String(describing: componentIn))
} else {
self.dumpRegistry()
XCTAssert(false, "injectComponent failed: \(sdkKey) \(isReinitialize) :: \(binder)")
}
}
}

XCTAssertTrue(result, "Concurrent tasks timed out")
}

// MARK: - Utils

func dumpRegistry() {
let registry = HandlerRegistryService.shared

print("[MultiClients] binders --------------")
registry.binders.performAtomic { prop in
for sk in prop.keys {
print("[MultiClients] binder for \(sk): \(prop[sk]!)")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// 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 LoggerTests_MultiClients: XCTestCase {

override func setUpWithError() throws {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add base class and put these two overrided methods in there and extend classes from that base class. I see these two methods at many locations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most test cases will use own tasks in these methods. I'll clean up these empty methods.

}

override func tearDownWithError() throws {
}

}