Skip to content

Commit 47ad0ae

Browse files
authored
fix(handler-registry-service): add support for multiple clients (#406)
Fix handler-registry-service to support multiple clients (sdkKeys). - full thread-safety for additional concurrency requirements - no resource conflicts for multiple sdkKeys support
1 parent a3a8c14 commit 47ad0ae

File tree

4 files changed

+208
-25
lines changed

4 files changed

+208
-25
lines changed

OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,8 @@
15151515
6ECB60D4234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */; };
15161516
6ECB60D5234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */; };
15171517
6ECB60D7234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60D6234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m */; };
1518+
6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */; };
1519+
6EE5918E264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE5918D264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift */; };
15181520
6EF41A332522BE1900EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */; };
15191521
6EF41A422522BE2100EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */; };
15201522
6EF8DE0624B8DA58008B9488 /* decide_datafile.json in Resources */ = {isa = PBXBuildFile; fileRef = 6EF8DE0524B8DA58008B9488 /* decide_datafile.json */; };
@@ -2026,6 +2028,8 @@
20262028
6ECB60C5234D329500016D41 /* OptimizelyClientTests_OptimizelyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_OptimizelyConfig.swift; sourceTree = "<group>"; };
20272029
6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OptimizelyConfig+ObjC.swift"; sourceTree = "<group>"; };
20282030
6ECB60D6234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OptimizelyClientTests_OptimizelyConfig_Objc.m; sourceTree = "<group>"; };
2031+
6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests_MultiClients.swift; sourceTree = "<group>"; };
2032+
6EE5918D264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandlerRegistryServiceTests_MultiClients.swift; sourceTree = "<group>"; };
20292033
6EF8DE0524B8DA58008B9488 /* decide_datafile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_datafile.json; sourceTree = "<group>"; };
20302034
6EF8DE0A24BD1BB1008B9488 /* OptimizelyDecision.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyDecision.swift; sourceTree = "<group>"; };
20312035
6EF8DE0B24BD1BB2008B9488 /* OptimizelyDecideOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyDecideOption.swift; sourceTree = "<group>"; };
@@ -2230,6 +2234,8 @@
22302234
6E2D5DAD26338CA00002077F /* AtomicDictionaryTests.swift */,
22312235
6E5D120C2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift */,
22322236
6E474C8C263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift */,
2237+
6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */,
2238+
6EE5918D264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift */,
22332239
);
22342240
path = "OptimizelyTests-MultiClients";
22352241
sourceTree = "<group>";
@@ -3687,6 +3693,7 @@
36873693
6E424CF426324B620081004A /* DecisionInfo.swift in Sources */,
36883694
6E5D120D2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift in Sources */,
36893695
6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */,
3696+
6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */,
36903697
6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */,
36913698
6E424CF626324B620081004A /* DefaultNotificationCenter.swift in Sources */,
36923699
6E424CF726324B620081004A /* DefaultDecisionService.swift in Sources */,
@@ -3738,6 +3745,7 @@
37383745
6E424CD326324B270081004A /* OptimizelyError.swift in Sources */,
37393746
6E424D5326324C4D0081004A /* OptimizelyUserContext+ObjC.swift in Sources */,
37403747
6E424CD426324B270081004A /* OptimizelyLogLevel.swift in Sources */,
3748+
6EE5918E264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift in Sources */,
37413749
6E424CD526324B270081004A /* OptimizelyClient.swift in Sources */,
37423750
6E424CD626324B270081004A /* OptimizelyClient+ObjC.swift in Sources */,
37433751
6E424CD726324B270081004A /* OptimizelyResult.swift in Sources */,

Sources/Utils/HandlerRegistryService.swift

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,43 +17,37 @@
1717
import Foundation
1818

1919
class HandlerRegistryService {
20-
2120
static let shared = HandlerRegistryService()
2221

2322
struct ServiceKey: Hashable {
2423
var service: String
2524
var sdkKey: String?
2625
}
2726

28-
var binders: AtomicProperty<[ServiceKey: BinderProtocol]> = {
29-
var binders = AtomicProperty<[ServiceKey: BinderProtocol]>()
30-
binders.property = [ServiceKey: BinderProtocol]()
31-
return binders
32-
}()
27+
var binders = AtomicProperty(property: [ServiceKey: BinderProtocol]())
3328

3429
private init() {}
3530

3631
func registerBinding(binder: BinderProtocol) {
3732
let sk = ServiceKey(service: "\(type(of: binder.service))", sdkKey: binder.sdkKey)
38-
if binders.property?[sk] != nil {
39-
} else {
40-
binders.property?[sk] = binder
33+
binders.performAtomic{ prop in
34+
if prop[sk] == nil {
35+
prop[sk] = binder
36+
}
4137
}
4238
}
4339

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

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

5248
func updateBinder(b: BinderProtocol) {
53-
if binders.property?[skLocal] != nil {
54-
binders.property?[skLocal] = b
55-
} else {
56-
binders.property?[skGlobal] = b
49+
binders.performAtomic{ prop in
50+
prop[sk] = b
5751
}
5852
}
5953

@@ -110,7 +104,7 @@ struct Binder<T>: BinderProtocol {
110104
var sdkKey: String?
111105
var service: Any
112106
var strategy: ReInitializeStrategy = .reCreate
113-
var factory: () -> Any? = { return nil as Any? }
107+
var factory: () -> Any?
114108
var isSingleton = false
115109
var inst: T?
116110

@@ -125,33 +119,39 @@ struct Binder<T>: BinderProtocol {
125119
}
126120
}
127121

128-
init(sdkKey: String? = nil, service: Any, strategy: ReInitializeStrategy = .reCreate, factory: @escaping (() -> Any?) = { ()->Any? in { return nil as Any? }}, isSingleton: Bool = false, inst: T? = nil) {
122+
init(sdkKey: String? = nil,
123+
service: Any,
124+
strategy: ReInitializeStrategy = .reCreate,
125+
factory: (() -> Any?)? = nil,
126+
isSingleton: Bool = false,
127+
inst: T? = nil) {
128+
129129
self.sdkKey = sdkKey
130130
self.service = service
131131
self.strategy = strategy
132-
self.factory = factory
132+
self.factory = factory ?? { return nil as Any? }
133133
self.isSingleton = isSingleton
134134
self.inst = inst
135135
}
136136
}
137137

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

143-
func injectNotificationCenter(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTNotificationCenter? {
143+
func injectNotificationCenter(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTNotificationCenter? {
144144
return injectComponent(service: OPTNotificationCenter.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTNotificationCenter?
145145
}
146-
func injectDecisionService(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTDecisionService? {
146+
func injectDecisionService(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTDecisionService? {
147147
return injectComponent(service: OPTDecisionService.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTDecisionService?
148148
}
149149

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

154-
func injectDatafileHandler(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTDatafileHandler? {
154+
func injectDatafileHandler(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTDatafileHandler? {
155155
return injectComponent(service: OPTDatafileHandler.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTDatafileHandler?
156156
}
157157
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//
2+
// Copyright 2021, Optimizely, Inc. and contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import XCTest
18+
19+
class HandlerRegistryServiceTests_MultiClients: XCTestCase {
20+
21+
override func setUpWithError() throws {
22+
}
23+
24+
override func tearDownWithError() throws {
25+
}
26+
27+
func testConcurrentAccess_Singleton() {
28+
// this type used for all handlers except for logger
29+
30+
let numThreads = 10
31+
let numEventsPerThread = 100
32+
33+
let registry = HandlerRegistryService.shared
34+
35+
let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 300) { thIdx in
36+
for idx in 0..<numEventsPerThread {
37+
let sdkKey = String(thIdx * numEventsPerThread + idx)
38+
let strategy: ReInitializeStrategy = .reUse
39+
let isSingleton = true
40+
let isReinitialize = false
41+
42+
let service = OPTLogger.self
43+
let componentIn = DefaultLogger()
44+
45+
let binder = Binder(sdkKey: sdkKey,
46+
service: service,
47+
strategy: strategy,
48+
isSingleton: isSingleton,
49+
inst: componentIn)
50+
51+
registry.registerBinding(binder: binder)
52+
if let componentOut = registry.injectComponent(service: service,
53+
sdkKey: sdkKey,
54+
isReintialize: isReinitialize) as? DefaultLogger {
55+
XCTAssertEqual(String(describing: componentOut), String(describing: componentIn))
56+
} else {
57+
self.dumpRegistry()
58+
XCTAssert(false, "injectComponent failed: \(sdkKey) :: \(binder)")
59+
}
60+
}
61+
}
62+
63+
XCTAssertTrue(result, "Concurrent tasks timed out")
64+
}
65+
66+
func testConcurrentAccess_NonSingleton() {
67+
// this type used for loggers
68+
69+
let numThreads = 10
70+
let numEventsPerThread = 100
71+
72+
let registry = HandlerRegistryService.shared
73+
74+
let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 300) { thIdx in
75+
for _ in 0..<numEventsPerThread {
76+
let isReinitialize = false
77+
78+
let service = OPTLogger.self
79+
let componentIn = DefaultLogger()
80+
81+
let binder = Binder<OPTLogger>(service: service,
82+
factory: type(of: componentIn).init)
83+
84+
registry.registerBinding(binder: binder)
85+
if let componentOut = registry.injectComponent(service: service,
86+
isReintialize: isReinitialize) as? DefaultLogger {
87+
XCTAssertEqual(String(describing: componentOut), String(describing: componentIn))
88+
} else {
89+
self.dumpRegistry()
90+
XCTAssert(false, "injectComponent failed: \(binder)")
91+
}
92+
}
93+
}
94+
95+
XCTAssertTrue(result, "Concurrent tasks timed out")
96+
}
97+
98+
func testConcurrentAccess_Random() {
99+
let numThreads = 10
100+
let numEventsPerThread = 100
101+
102+
let registry = HandlerRegistryService.shared
103+
104+
let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 300) { thIdx in
105+
for idx in 0..<numEventsPerThread {
106+
let sdkKey = String(thIdx * numEventsPerThread + idx)
107+
let strategy: ReInitializeStrategy = Bool.random() ? .reCreate : .reUse
108+
let isSingleton = Bool.random()
109+
let isReinitialize = Bool.random()
110+
111+
let service = OPTLogger.self
112+
let componentIn = DefaultLogger()
113+
114+
let binder = Binder(sdkKey: sdkKey,
115+
service: service,
116+
strategy: strategy,
117+
factory: type(of: componentIn).init,
118+
isSingleton: isSingleton,
119+
inst: componentIn)
120+
121+
registry.registerBinding(binder: binder)
122+
if let componentOut = registry.injectComponent(service: service,
123+
sdkKey: sdkKey,
124+
isReintialize: isReinitialize) as? DefaultLogger {
125+
XCTAssertEqual(String(describing: componentOut), String(describing: componentIn))
126+
} else {
127+
self.dumpRegistry()
128+
XCTAssert(false, "injectComponent failed: \(sdkKey) \(isReinitialize) :: \(binder)")
129+
}
130+
}
131+
}
132+
133+
XCTAssertTrue(result, "Concurrent tasks timed out")
134+
}
135+
136+
// MARK: - Utils
137+
138+
func dumpRegistry() {
139+
let registry = HandlerRegistryService.shared
140+
141+
print("[MultiClients] binders --------------")
142+
registry.binders.performAtomic { prop in
143+
for sk in prop.keys {
144+
print("[MultiClients] binder for \(sk): \(prop[sk]!)")
145+
}
146+
}
147+
}
148+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// Copyright 2021, Optimizely, Inc. and contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import XCTest
18+
19+
class LoggerTests_MultiClients: XCTestCase {
20+
21+
override func setUpWithError() throws {
22+
}
23+
24+
override func tearDownWithError() throws {
25+
}
26+
27+
}

0 commit comments

Comments
 (0)