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

[MOB-10312] thread safe ordered dictionary wrapper #864

Merged
Merged
Changes from all commits
Commits
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 swift-sdk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -185,6 +185,8 @@
5B5AA717284F1A6D0093FED4 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5AA710284F1A6D0093FED4 /* MockNetworkSession.swift */; };
5B6C3C1127CE871F00B9A753 /* NavInboxSessionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6C3C1027CE871F00B9A753 /* NavInboxSessionUITests.swift */; };
5B88BC482805D09D004016E5 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B88BC472805D09D004016E5 /* NetworkSession.swift */; };
94D8F9D42CDC291000D4CF53 /* ThreadSafeOrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D8F9D32CDC291000D4CF53 /* ThreadSafeOrderedDictionary.swift */; };
94D8F9D62CDC294300D4CF53 /* ThreadSafeOrderedDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D8F9D52CDC294300D4CF53 /* ThreadSafeOrderedDictionaryTests.swift */; };
9F76FFFF2B17884900962526 /* EmbeddedHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F76FFFE2B17884900962526 /* EmbeddedHelper.swift */; };
9FF05EAC2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; };
9FF05EAD2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; };
@@ -609,6 +611,8 @@
5B6C3C1027CE871F00B9A753 /* NavInboxSessionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavInboxSessionUITests.swift; sourceTree = "<group>"; };
5B88BC472805D09D004016E5 /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = "<group>"; };
5BFC7CED27FC9AF300E77479 /* inbox-ui-tests-app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "inbox-ui-tests-app.entitlements"; sourceTree = "<group>"; };
94D8F9D32CDC291000D4CF53 /* ThreadSafeOrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeOrderedDictionary.swift; sourceTree = "<group>"; };
94D8F9D52CDC294300D4CF53 /* ThreadSafeOrderedDictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeOrderedDictionaryTests.swift; sourceTree = "<group>"; };
9F76FFFE2B17884900962526 /* EmbeddedHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedHelper.swift; sourceTree = "<group>"; };
9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthManager.swift; sourceTree = "<group>"; };
AC02480722791E2100495FB9 /* IterableInboxNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableInboxNavigationViewController.swift; sourceTree = "<group>"; };
@@ -1153,6 +1157,7 @@
5536781E2576FF9000DB3652 /* IterableUtilTests.swift */,
ACED4C00213F50B30055A497 /* LoggingTests.swift */,
55B37FC5229752DD0042F13A /* OrderedDictionaryTests.swift */,
94D8F9D52CDC294300D4CF53 /* ThreadSafeOrderedDictionaryTests.swift */,
ACEDF41E2183C436000B9BFE /* PendingTests.swift */,
);
name = "foundational-tests";
@@ -1280,6 +1285,7 @@
AC776DA5211A1B8A00C27C27 /* IterableRequestUtil.swift */,
AC72A0AD20CF4C16004D7997 /* IterableUtil.swift */,
AC32E16721DD55B900BD4F83 /* OrderedDictionary.swift */,
94D8F9D32CDC291000D4CF53 /* ThreadSafeOrderedDictionary.swift */,
ACD8BF852757FC4C00C2EAB2 /* UIColor+Extension.swift */,
);
name = Util;
@@ -2112,6 +2118,7 @@
ACD2B83D25B0A74A005D7A90 /* Models.swift in Sources */,
55DD2027269E5EA300773CC7 /* InboxViewControllerViewModelView.swift in Sources */,
AC1BED9523F1D4C700FDD75F /* MiscInboxClasses.swift in Sources */,
94D8F9D42CDC291000D4CF53 /* ThreadSafeOrderedDictionary.swift in Sources */,
553449A129C2621E002E4599 /* EmbeddedMessagingProcessor.swift in Sources */,
556FB1EA244FAF6A00EDF6BD /* InAppPresenter.swift in Sources */,
AC2AED4424EBC905000EE5F3 /* IterableTaskScheduler.swift in Sources */,
@@ -2222,6 +2229,7 @@
AC8F35A2239806B500302994 /* InboxViewControllerViewModelTests.swift in Sources */,
AC995F9A2166EEB50099A184 /* CommonMocks.swift in Sources */,
5588DFE128C046B7000697D7 /* MockLocalStorage.swift in Sources */,
94D8F9D62CDC294300D4CF53 /* ThreadSafeOrderedDictionaryTests.swift in Sources */,
1CBFFE1B2A97AEEF00ED57EE /* EmbeddedMessagingProcessorTests.swift in Sources */,
5588DF8128C04494000697D7 /* MockUrlDelegate.swift in Sources */,
5585DF8F22A73390000A32B9 /* IterableInboxViewControllerTests.swift in Sources */,
16 changes: 8 additions & 8 deletions swift-sdk/Internal/InAppManager+Functions.swift
Original file line number Diff line number Diff line change
@@ -5,14 +5,14 @@
import Foundation

enum MessagesProcessorResult {
case show(message: IterableInAppMessage, messagesMap: OrderedDictionary<String, IterableInAppMessage>)
case noShow(messagesMap: OrderedDictionary<String, IterableInAppMessage>)
case show(message: IterableInAppMessage, messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>)
case noShow(messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>)
}

struct MessagesProcessor {
init(inAppDelegate: IterableInAppDelegate,
inAppDisplayChecker: InAppDisplayChecker,
messagesMap: OrderedDictionary<String, IterableInAppMessage>) {
messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>) {
ITBInfo()

self.inAppDelegate = inAppDelegate
@@ -97,18 +97,18 @@ struct MessagesProcessor {

private let inAppDelegate: IterableInAppDelegate
private let inAppDisplayChecker: InAppDisplayChecker
private var messagesMap: OrderedDictionary<String, IterableInAppMessage>
private var messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>
}

struct MergeMessagesResult {
let inboxChanged: Bool
let messagesMap: OrderedDictionary<String, IterableInAppMessage>
let messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>
let deliveredMessages: [IterableInAppMessage]
}

/// Merges the results and determines whether inbox changed needs to be fired.
struct MessagesObtainedHandler {
init(messagesMap: OrderedDictionary<String, IterableInAppMessage>, messages: [IterableInAppMessage]) {
init(messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>, messages: [IterableInAppMessage]) {
ITBInfo()
self.messagesMap = messagesMap
self.messages = messages
@@ -123,7 +123,7 @@ struct MessagesObtainedHandler {
let addedInboxCount = addedMessages.reduce(0) { $1.saveToInbox ? $0 + 1 : $0 }

var messagesOverwritten = 0
var newMessagesMap = OrderedDictionary<String, IterableInAppMessage>()
let newMessagesMap = ThreadSafeOrderedDictionary<String, IterableInAppMessage>()
messages.forEach { serverMessage in
let messageId = serverMessage.messageId
if let existingMessage = messagesMap[messageId] {
@@ -145,7 +145,7 @@ struct MessagesObtainedHandler {
deliveredMessages: deliveredMessages)
}

private let messagesMap: OrderedDictionary<String, IterableInAppMessage>
private let messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>
private let messages: [IterableInAppMessage]

// We should only overwrite if the server is read and client is not read.
8 changes: 4 additions & 4 deletions swift-sdk/Internal/InAppManager.swift
Original file line number Diff line number Diff line change
@@ -284,7 +284,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol {
lastSyncTime = dateProvider.currentDate
}

private func getMessagesMap(fromMessagesProcessorResult messagesProcessorResult: MessagesProcessorResult) -> OrderedDictionary<String, IterableInAppMessage> {
private func getMessagesMap(fromMessagesProcessorResult messagesProcessorResult: MessagesProcessorResult) -> ThreadSafeOrderedDictionary<String, IterableInAppMessage> {
switch messagesProcessorResult {
case let .noShow(messagesMap: messagesMap):
return messagesMap
@@ -303,7 +303,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol {
}
}

private func processAndShowMessage(messagesMap: OrderedDictionary<String, IterableInAppMessage>) {
private func processAndShowMessage(messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>) {
var processor = MessagesProcessor(inAppDelegate: inAppDelegate, inAppDisplayChecker: self, messagesMap: messagesMap)
let messagesProcessorResult = processor.processMessages()
self.messagesMap = getMessagesMap(fromMessagesProcessorResult: messagesProcessorResult)
@@ -552,7 +552,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol {
private let notificationCenter: NotificationCenterProtocol

private let persister: InAppPersistenceProtocol
private var messagesMap = OrderedDictionary<String, IterableInAppMessage>()
private var messagesMap = ThreadSafeOrderedDictionary<String, IterableInAppMessage>()
private let dateProvider: DateProviderProtocol
private var lastDismissedTime: Date?
private var lastDisplayTime: Date?
@@ -607,7 +607,7 @@ extension InAppManager: InAppNotifiable {
ITBInfo()

updateQueue.async { [weak self] in
if let _ = self?.messagesMap.filter({ $0.key == messageId }).first {
if let _ = self?.messagesMap.keys.filter({ $0 == messageId }).first {
if let messagesMap = self?.messagesMap {
self?.messagesMap.removeValue(forKey: messageId)
self?.persister.persist(messagesMap.values)
85 changes: 85 additions & 0 deletions swift-sdk/Internal/ThreadSafeOrderedDictionary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// ThreadSafeOrderedDictionary.swift
// swift-sdk
//
// Created by Ricky on 11/6/24.
// Copyright © 2024 Iterable. All rights reserved.
//

import Foundation

public final class ThreadSafeOrderedDictionary<K: Hashable, V> {
private var orderedDictionary = OrderedDictionary<K, V>()
private let lock = NSRecursiveLock()

public var keys: [K] {
lock.withLock {
orderedDictionary.keys
}
}

public var count: Int {
lock.withLock {
orderedDictionary.count
}
}

public var values: [V] {
lock.withLock {
orderedDictionary.values
}
}

public subscript(key: K) -> V? {
get {
lock.withLock {
orderedDictionary[key]
}
}
set {
lock.withLock {
orderedDictionary[key] = newValue
}
}
}

@discardableResult public func updateValue(_ value: V?, forKey key: K) -> V? {
lock.withLock {
orderedDictionary.updateValue(value, forKey: key)
}
}

@discardableResult public func removeValue(forKey key: K) -> V? {
lock.withLock {
orderedDictionary.removeValue(forKey: key)
}
}

public func reset() {
lock.withLock {
orderedDictionary.reset()
}
}

public func makeIterator() -> AnyIterator<(key: K, value: V)> {
lock.withLock {
orderedDictionary.makeIterator()
}
}

public var description: String {
lock.withLock {
orderedDictionary.description
}
}
}

// Conformance to ExpressibleByDictionaryLiteral
extension ThreadSafeOrderedDictionary: ExpressibleByDictionaryLiteral {
public convenience init(dictionaryLiteral elements: (K, V)...) {
self.init()
for (key, value) in elements {
self[key] = value
}
}
}
4 changes: 2 additions & 2 deletions tests/unit-tests/InAppMessageProcessorTests.swift
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ class InAppMessageProcessorTests: XCTestCase {
let serverMessage = Self.makeEmptyInboxMessage(messageId)
serverMessage.read = true

let messagesMap: OrderedDictionary<String, IterableInAppMessage> = [messageId: localMessage]
let messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage> = [messageId: localMessage]
let newMessages = [serverMessage]

let result = MessagesObtainedHandler(messagesMap: messagesMap,
@@ -31,7 +31,7 @@ class InAppMessageProcessorTests: XCTestCase {
let serverMessage2 = Self.makeEmptyInboxMessage("msg-3")
serverMessage2.read = false

let messagesMap: OrderedDictionary<String, IterableInAppMessage> = ["msg-1": localMessage]
let messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage> = ["msg-1": localMessage]
let newMessages = [serverMessage1, serverMessage2]

let result = MessagesObtainedHandler(messagesMap: messagesMap,
55 changes: 55 additions & 0 deletions tests/unit-tests/ThreadSafeOrderedDictionaryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// ThreadSafeOrderedDictionaryTests.swift
// unit-tests
//
// Created by Ricky on 11/6/24.
// Copyright © 2024 Iterable. All rights reserved.
//

import XCTest

@testable import IterableSDK

class ThreadSafeOrderedDictionaryTests: XCTestCase {
func testConcurrentAccess() {
let orderedDict = ThreadSafeOrderedDictionary<String, Int>()

// Initialize with some data
orderedDict["initialKey"] = 0

let iterations = 1_000
let concurrentQueue = DispatchQueue.global(qos: .userInitiated)

// Dispatch group to wait for all tasks to complete
let dispatchGroup = DispatchGroup()

// Concurrently write to the dictionary
dispatchGroup.enter()
concurrentQueue.async {
DispatchQueue.concurrentPerform(iterations: iterations) { i in
print(i, "Here 1")
orderedDict["key_\(i)"] = i
}
dispatchGroup.leave()
}

// Concurrently read from the dictionary
dispatchGroup.enter()
concurrentQueue.async {
DispatchQueue.concurrentPerform(iterations: iterations) { i in
print(i, "Here 2")
_ = orderedDict["key_\(i % 10)"]
}
dispatchGroup.leave()
}

// Wait for all operations to finish
let result = dispatchGroup.wait(timeout: .now() + 10)

// Assert that all operations completed
XCTAssertEqual(result, .success, "Test timed out - possible deadlock or crash occurred.")

// Check if dictionary contains at least one expected entry
XCTAssertGreaterThanOrEqual(orderedDict.count, 1, "Count should be greater than or equal to 1 after concurrent writes")
}
}