Skip to content

Commit c70fc12

Browse files
(fix): show alternative using atomic property for thread safe notification center (#304)
1 parent cd214de commit c70fc12

File tree

2 files changed

+190
-37
lines changed

2 files changed

+190
-37
lines changed

Sources/Implementation/DefaultNotificationCenter.swift

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,60 @@
1717
import Foundation
1818

1919
public class DefaultNotificationCenter: OPTNotificationCenter {
20-
public var notificationId: Int = 1
21-
var notificationListeners = [Int: (Int, GenericListener)]()
20+
public var notificationId: Int {
21+
get {
22+
return notifications.notificationId
23+
}
24+
set {
25+
// don't do it.
26+
}
27+
}
28+
29+
class Notifications {
30+
var notificationId: Int = 1
31+
var notificationListeners = [Int: (Int, GenericListener)]()
32+
func incrementNotificationId() -> Int {
33+
let returnValue = notificationId
34+
notificationId += 1
35+
return returnValue
36+
}
37+
}
38+
39+
private var _listeners : AtomicProperty<Notifications> = AtomicProperty<Notifications>()
40+
41+
var notifications : Notifications {
42+
get {
43+
return _listeners.property!
44+
}
45+
set {
46+
_listeners.property = newValue;
47+
}
48+
}
2249

2350
var observerLogEvent: NSObjectProtocol?
2451

2552
required public init() {
2653
addInternalNotificationListners()
54+
notifications = Notifications()
2755
}
2856

2957
deinit {
3058
removeInternalNotificationListners()
3159
}
3260

33-
internal func incrementNotificationId() -> Int {
34-
let returnValue = notificationId
35-
notificationId += 1
36-
return returnValue
37-
}
38-
3961
public func addGenericNotificationListener(notificationType: Int, listener: @escaping GenericListener) -> Int? {
40-
notificationListeners[notificationId] = (notificationType, listener)
4162

42-
return incrementNotificationId()
63+
var id = 0
64+
_listeners.performAtomic { (notifications) in
65+
notifications.notificationListeners[notifications.notificationId] = (notificationType, listener)
66+
67+
id = notifications.incrementNotificationId()
68+
}
69+
return id
4370
}
4471

4572
public func addActivateNotificationListener(activateListener: @escaping (OptimizelyExperimentData, String, OptimizelyAttributes?, OptimizelyVariationData, [String: Any]) -> Void) -> Int? {
46-
notificationListeners[notificationId] = (NotificationType.activate.rawValue, { (args: Any...) in
73+
let listener = { (args: Any...) in
4774
guard let myArgs = args[0] as? [Any?] else {
4875
return
4976
}
@@ -62,13 +89,13 @@ public class DefaultNotificationCenter: OPTNotificationCenter {
6289

6390
activateListener(experimentData, userId, attributes, variationData, event)
6491
}
65-
})
92+
}
6693

67-
return incrementNotificationId()
94+
return addGenericNotificationListener(notificationType: NotificationType.activate.rawValue, listener: listener)
6895
}
6996

7097
public func addTrackNotificationListener(trackListener: @escaping (String, String, OptimizelyAttributes?, [String: Any]?, [String: Any]) -> Void) -> Int? {
71-
notificationListeners[notificationId] = (NotificationType.track.rawValue, { (args: Any...) in
98+
let listener = { (args: Any...) in
7299
guard let myArgs = args[0] as? [Any?] else {
73100
return
74101
}
@@ -82,13 +109,13 @@ public class DefaultNotificationCenter: OPTNotificationCenter {
82109
let event = myArgs[4] as? [String: Any] {
83110
trackListener(eventKey, userId, attributes, eventTags, event)
84111
}
85-
})
112+
}
86113

87-
return incrementNotificationId()
114+
return addGenericNotificationListener(notificationType: NotificationType.track.rawValue, listener: listener)
88115
}
89116

90117
public func addDecisionNotificationListener(decisionListener: @escaping (String, String, OptimizelyAttributes?, [String: Any]) -> Void) -> Int? {
91-
notificationListeners[notificationId] = (NotificationType.decision.rawValue, { (args: Any...) in
118+
let listener = { (args: Any...) in
92119
guard let myArgs = args[0] as? [Any?] else {
93120
return
94121
}
@@ -101,13 +128,13 @@ public class DefaultNotificationCenter: OPTNotificationCenter {
101128
let decisionInfo = myArgs[3] as? [String: Any] {
102129
decisionListener(type, userId, attributes, decisionInfo)
103130
}
104-
})
131+
}
105132

106-
return incrementNotificationId()
133+
return addGenericNotificationListener(notificationType: NotificationType.decision.rawValue, listener: listener)
107134
}
108135

109136
public func addDatafileChangeNotificationListener(datafileListener: @escaping DatafileChangeListener) -> Int? {
110-
notificationListeners[notificationId] = (NotificationType.datafileChange.rawValue, { (args: Any...) in
137+
let listener = { (args: Any...) in
111138
guard let myArgs = args[0] as? [Any?] else {
112139
return
113140
}
@@ -117,13 +144,13 @@ public class DefaultNotificationCenter: OPTNotificationCenter {
117144
if let data = myArgs[0] as? Data {
118145
datafileListener(data)
119146
}
120-
})
147+
}
121148

122-
return incrementNotificationId()
149+
return addGenericNotificationListener(notificationType: NotificationType.datafileChange.rawValue, listener: listener)
123150
}
124151

125152
public func addLogEventNotificationListener(logEventListener: @escaping LogEventListener) -> Int? {
126-
notificationListeners[notificationId] = (NotificationType.logEvent.rawValue, { (args: Any...) in
153+
let listener = { (args: Any...) in
127154
guard let myArgs = args[0] as? [Any?] else {
128155
return
129156
}
@@ -134,26 +161,37 @@ public class DefaultNotificationCenter: OPTNotificationCenter {
134161
let event = myArgs[1] as? [String: Any] {
135162
logEventListener(url, event)
136163
}
137-
})
164+
}
138165

139-
return incrementNotificationId()
166+
return addGenericNotificationListener(notificationType: NotificationType.logEvent.rawValue, listener: listener)
140167
}
141168

142169
public func removeNotificationListener(notificationId: Int) {
143-
self.notificationListeners.removeValue(forKey: notificationId)
170+
_listeners.performAtomic { (listeners) in
171+
listeners.notificationListeners.removeValue(forKey: notificationId)
172+
}
144173
}
145174

146175
public func clearNotificationListeners(type: NotificationType) {
147-
self.notificationListeners = self.notificationListeners.filter({$1.0 != type.rawValue})
176+
_listeners.performAtomic { (listeners) in
177+
listeners.notificationListeners = listeners.notificationListeners.filter({$1.0 != type.rawValue})
178+
}
148179
}
149180

150181
public func clearAllNotificationListeners() {
151-
self.notificationListeners.removeAll()
182+
_listeners.performAtomic { (listeners) in
183+
listeners.notificationListeners.removeAll()
184+
}
152185
}
153186

154187
public func sendNotifications(type: Int, args: [Any?]) {
155-
for values in notificationListeners.values where values.0 == type {
156-
values.1(args)
188+
var selected = [GenericListener]()
189+
_listeners.performAtomic { (listeners) in
190+
selected = listeners.notificationListeners.values.filter{ $0.0 == type }.map{ $0.1 }
191+
}
192+
193+
for listener in selected {
194+
listener(args)
157195
}
158196
}
159197

Tests/OptimizelyTests-Common/NotificationCenterTests.swift

Lines changed: 122 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import XCTest
1818

1919
class NotificationCenterTests: XCTestCase {
2020

21-
let notificationCenter: DefaultNotificationCenter = DefaultNotificationCenter()
21+
var notificationCenter: DefaultNotificationCenter!
2222
var experiment: Experiment?
2323
var variation: Variation?
2424
var called = false
@@ -38,20 +38,22 @@ class NotificationCenterTests: XCTestCase {
3838
"forcedVariations": ["12345": "1234567890"]]
3939

4040
override func setUp() {
41-
// Put setup code here. This method is called before the invocation of each test method in the class.
41+
super.setUp()
4242

4343
let data: [String: Any] = NotificationCenterTests.sampleExperiment
4444

4545
experiment = try! OTUtils.model(from: data)
4646

4747
variation = experiment!.variations[0]
4848

49+
notificationCenter = DefaultNotificationCenter()
4950
notificationCenter.clearAllNotificationListeners()
5051
}
5152

5253
override func tearDown() {
53-
// Put teardown code here. This method is called after the invocation of each test method in the class.
5454
notificationCenter.clearAllNotificationListeners()
55+
notificationCenter = nil // deinit immediately after each test
56+
super.tearDown()
5557
}
5658

5759
func sendActivate() {
@@ -241,13 +243,126 @@ class NotificationCenterTests: XCTestCase {
241243
XCTAssertTrue(called)
242244
}
243245

246+
func testNotificationCenterThreadSafe() {
247+
let numConcurrency = 5
248+
249+
let exp = expectation(description: "x")
250+
exp.expectedFulfillmentCount = numConcurrency
251+
252+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(0)) {
253+
_ = self.addActivateListener()
254+
for _ in 0..<100000 { self.sendActivate() }
255+
exp.fulfill()
256+
}
257+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(100)) {
258+
_ = self.addTrackListener()
259+
for _ in 0..<10000 { self.sendTrack() }
260+
exp.fulfill()
261+
}
262+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(200)) {
263+
_ = self.addDecisionListener()
264+
for _ in 0..<1000 { self.sendDecision() }
265+
exp.fulfill()
266+
}
267+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(10)) {
268+
_ = self.addDatafileChangeListener()
269+
exp.fulfill()
270+
}
271+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(20)) {
272+
_ = self.addLogEventListener()
273+
exp.fulfill()
274+
}
244275

276+
wait(for: [exp], timeout: 10.0)
277+
XCTAssertEqual(notificationCenter.notificationId - 1, numConcurrency)
278+
}
279+
280+
func testNotificationCenterThreadSafe_Remove() {
281+
let numConcurrency = 5
282+
283+
let exp = expectation(description: "x")
284+
exp.expectedFulfillmentCount = numConcurrency
245285

246-
func testPerformanceExample() {
247-
// This is an example of a performance test case.
248-
self.measure {
249-
// Put the code you want to measure the time of here.
286+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(0)) {
287+
_ = self.addActivateListener()
288+
for _ in 0..<100 { self.sendActivate() }
289+
self.notificationCenter.clearNotificationListeners(type: .activate)
290+
exp.fulfill()
250291
}
292+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(10)) {
293+
_ = self.addTrackListener()
294+
for _ in 0..<100 { self.sendTrack() }
295+
self.notificationCenter.clearNotificationListeners(type: .track)
296+
exp.fulfill()
297+
}
298+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(10)) {
299+
_ = self.addDecisionListener()!
300+
for _ in 0..<100 { self.sendDecision() }
301+
self.notificationCenter.clearNotificationListeners(type: .decision)
302+
exp.fulfill()
303+
}
304+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(10)) {
305+
let id = self.addDatafileChangeListener()!
306+
for _ in 0..<100 { self.sendDatafileChange() }
307+
self.notificationCenter.removeNotificationListener(notificationId: id)
308+
exp.fulfill()
309+
}
310+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(10)) {
311+
let id = self.addLogEventListener()!
312+
for _ in 0..<100 { self.sendLogEvent() }
313+
self.notificationCenter.removeNotificationListener(notificationId: id)
314+
exp.fulfill()
315+
}
316+
317+
wait(for: [exp], timeout: 10.0)
318+
319+
self.called = false
320+
321+
sendActivate()
322+
sendTrack()
323+
sendDecision()
324+
sendDatafileChange()
325+
sendLogEvent()
326+
327+
XCTAssertFalse(called)
251328
}
329+
330+
func testNotificationCenterThreadSafe_AddRemove() {
331+
let numConcurrency = 3
332+
333+
let exp = expectation(description: "x")
334+
exp.expectedFulfillmentCount = numConcurrency
252335

336+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(0)) {
337+
for _ in 0..<1000 {
338+
let id = self.addActivateListener()!
339+
self.notificationCenter.removeNotificationListener(notificationId: id)
340+
}
341+
exp.fulfill()
342+
}
343+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(10)) {
344+
for _ in 0..<1000 {
345+
let id = self.addTrackListener()!
346+
self.notificationCenter.removeNotificationListener(notificationId: id)
347+
}
348+
exp.fulfill()
349+
}
350+
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(10)) {
351+
for _ in 0..<1000 {
352+
let id = self.addDecisionListener()!
353+
self.notificationCenter.removeNotificationListener(notificationId: id)
354+
}
355+
exp.fulfill()
356+
}
357+
358+
wait(for: [exp], timeout: 10.0)
359+
360+
self.called = false
361+
362+
sendActivate()
363+
sendTrack()
364+
365+
XCTAssertFalse(called)
366+
}
367+
253368
}

0 commit comments

Comments
 (0)