Skip to content

Commit

Permalink
Merge pull request #1454 from OneSignal/fix/add_dispatch_queues_to_al…
Browse files Browse the repository at this point in the history
…l_executors

[Bug] Add Dispatch Queues to all executors
  • Loading branch information
nan-li committed Jul 1, 2024
2 parents f1489cc + 8248762 commit 70fcfb0
Show file tree
Hide file tree
Showing 9 changed files with 532 additions and 312 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import OneSignalOSCore
public class MockDispatchQueue: OSDispatchQueue {
let requestDispatch = DispatchQueue(label: "MockDispatchQueue")
var numDispatches = 0

public init() {}

public func async(execute work: @escaping @convention(block) () -> Void) {
Expand All @@ -46,8 +46,8 @@ public class MockDispatchQueue: OSDispatchQueue {
self.numDispatches += 1
}
}
public func waitForDispatches(_ numDispatches: Int) -> Void {

public func waitForDispatches(_ numDispatches: Int) {
while self.numDispatches < numDispatches {
requestDispatch.sync {
Thread.sleep(forTimeInterval: TimeInterval(1))
Expand Down
12 changes: 10 additions & 2 deletions iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ import OneSignalCore
*/
@objc
public class MockOneSignalClient: NSObject, IOneSignalClient {
public let executionQueue: DispatchQueue = DispatchQueue(label: "com.onesignal.execution")
public let executionQueue: DispatchQueue = DispatchQueue(label: "com.onesignal.execution", attributes: .concurrent)
let lock = NSLock()

var mockResponses: [String: [String: Any]] = [:]
var mockFailureResponses: [String: NSError] = [:]
public var lastHTTPRequest: OneSignalRequest?
public var networkRequestCount = 0
public var executedRequests: [OneSignalRequest] = []
public var executeInstantaneously = false
/// Set to true to make it unnecessary to setup mock responses for every request possible
public var fireSuccessForAllRequests = false

var remoteParamsResponse: [String: Any]?
var shouldUseProvisionalAuthorization = false // new in iOS 12 (aka Direct to History)
Expand Down Expand Up @@ -90,7 +93,9 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
public func execute(_ request: OneSignalRequest, onSuccess successBlock: @escaping OSResultSuccessBlock, onFailure failureBlock: @escaping OSFailureBlock) {
print("🧪 MockOneSignalClient execute called")

executedRequests.append(request)
lock.withLock {
executedRequests.append(request)
}

if executeInstantaneously {
finishExecutingRequest(request, onSuccess: successBlock, onFailure: failureBlock)
Expand Down Expand Up @@ -135,6 +140,9 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
successBlock(mockResponses[stringifiedRequest])
} else if (mockFailureResponses[stringifiedRequest]) != nil {
failureBlock(mockFailureResponses[stringifiedRequest])
} else if fireSuccessForAllRequests {
allRequestsHandled = false
successBlock([:])
} else {
allRequestsHandled = false
print("🧪 cannot find a mock response for request: \(stringifiedRequest)")
Expand Down
16 changes: 8 additions & 8 deletions iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ public class OneSignalCoreMocks: NSObject {
let expectation = XCTestExpectation(description: "Wait for \(seconds) seconds")
_ = XCTWaiter.wait(for: [expectation], timeout: seconds)
}

@objc public static func backgroundApp() {
if (OSBundleUtils.isAppUsingUIScene()) {
if OSBundleUtils.isAppUsingUIScene() {
if #available(iOS 13.0, *) {
NotificationCenter.default.post(name: UIScene.willDeactivateNotification, object: nil)
NotificationCenter.default.post(name: UIScene.didEnterBackgroundNotification, object: nil)
Expand All @@ -63,9 +63,9 @@ public class OneSignalCoreMocks: NSObject {
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
}
}

@objc public static func foregroundApp() {
if (OSBundleUtils.isAppUsingUIScene()) {
if OSBundleUtils.isAppUsingUIScene() {
if #available(iOS 13.0, *) {
NotificationCenter.default.post(name: UIScene.willEnterForegroundNotification, object: nil)
NotificationCenter.default.post(name: UIScene.didActivateNotification, object: nil)
Expand All @@ -75,19 +75,19 @@ public class OneSignalCoreMocks: NSObject {
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)
}
}

@objc public static func resignActive() {
if (OSBundleUtils.isAppUsingUIScene()) {
if OSBundleUtils.isAppUsingUIScene() {
if #available(iOS 13.0, *) {
NotificationCenter.default.post(name: UIScene.willDeactivateNotification, object: nil)
}
} else {
NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil)
}
}

@objc public static func becomeActive() {
if (OSBundleUtils.isAppUsingUIScene()) {
if OSBundleUtils.isAppUsingUIScene() {
if #available(iOS 13.0, *) {
NotificationCenter.default.post(name: UIScene.didActivateNotification, object: nil)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import OneSignalCoreMocks
import UIKit

final class OneSignalNotificationsTests: XCTestCase {

var notifTypes: Int32 = 0
var token: String = ""

Expand All @@ -52,7 +52,7 @@ final class OneSignalNotificationsTests: XCTestCase {
// Ensure that badge count == 0
XCTAssertEqual(UIApplication.shared.applicationIconBadgeNumber, 0)
}

func testDontclearBadgesWhenAppBecomesActive() throws {
// NotificationManager Start to register lifecycle listener
OSNotificationsManager.start()
Expand All @@ -65,25 +65,24 @@ final class OneSignalNotificationsTests: XCTestCase {
// Ensure that badge count == 1
XCTAssertEqual(UIApplication.shared.applicationIconBadgeNumber, 1)
}

func testUpdateNotificationTypesOnAppEntersForeground() throws {
// NotificationManager Start to register lifecycle listener
OSNotificationsManager.start()

OSNotificationsManager.delegate = self

XCTAssertEqual(self.notifTypes, 0)

// Then background the app
OneSignalCoreMocks.backgroundApp()

// Foreground the app for within 30 seconds
OneSignalCoreMocks.foregroundApp()

// Ensure that the delegate is updated with the new notification type
XCTAssertEqual(self.notifTypes, ERROR_PUSH_NEVER_PROMPTED)
}


}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
var addRequestQueue: [OSRequestAddAliases] = []
var removeRequestQueue: [OSRequestRemoveAlias] = []

// The Identity executor dispatch queue, serial. This synchronizes access to the delta and request queues.
private let dispatchQueue = DispatchQueue(label: "OneSignal.OSIdentityOperationExecutor", target: .global())

init() {
// Read unfinished deltas from cache, if any...
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] {
Expand Down Expand Up @@ -101,53 +104,60 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
}

func enqueueDelta(_ delta: OSDelta) {
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityOperationExecutor enqueueDelta: \(delta)")
deltaQueue.append(delta)
self.dispatchQueue.async {
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityOperationExecutor enqueueDelta: \(delta)")
self.deltaQueue.append(delta)
}
}

func cacheDeltaQueue() {
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue)
self.dispatchQueue.async {
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue)
}
}

func processDeltaQueue(inBackground: Bool) {
if !deltaQueue.isEmpty {
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityOperationExecutor processDeltaQueue with queue: \(deltaQueue)")
}
for delta in deltaQueue {
guard let model = delta.model as? OSIdentityModel,
let aliases = delta.value as? [String: String]
else {
// Log error
continue
self.dispatchQueue.async {
if !self.deltaQueue.isEmpty {
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityOperationExecutor processDeltaQueue with queue: \(self.deltaQueue)")
}
for delta in self.deltaQueue {
guard let model = delta.model as? OSIdentityModel,
let aliases = delta.value as? [String: String]
else {
// Log error
continue
}

switch delta.name {
case OS_ADD_ALIAS_DELTA:
let request = OSRequestAddAliases(aliases: aliases, identityModel: model)
addRequestQueue.append(request)
switch delta.name {
case OS_ADD_ALIAS_DELTA:
let request = OSRequestAddAliases(aliases: aliases, identityModel: model)
self.addRequestQueue.append(request)

case OS_REMOVE_ALIAS_DELTA:
for (label, _) in aliases {
let request = OSRequestRemoveAlias(labelToRemove: label, identityModel: model)
removeRequestQueue.append(request)
}
case OS_REMOVE_ALIAS_DELTA:
for (label, _) in aliases {
let request = OSRequestRemoveAlias(labelToRemove: label, identityModel: model)
self.removeRequestQueue.append(request)
}

default:
OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSIdentityOperationExecutor met incompatible OSDelta type: \(delta)")
default:
OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSIdentityOperationExecutor met incompatible OSDelta type: \(delta)")
}
}
}

self.deltaQueue = [] // TODO: Check that we can simply clear all the deltas in the deltaQueue
self.deltaQueue = [] // TODO: Check that we can simply clear all the deltas in the deltaQueue

// persist executor's requests (including new request) to storage
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
// persist executor's requests (including new request) to storage
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)

OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) // This should be empty, can remove instead?
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) // This should be empty, can remove instead?

processRequestQueue(inBackground: inBackground)
self.processRequestQueue(inBackground: inBackground)
}
}

/// This method is called by `processDeltaQueue` only and does not need to be added to the dispatchQueue.
func processRequestQueue(inBackground: Bool) {
let requestQueue: [OneSignalRequest] = addRequestQueue + removeRequestQueue

Expand Down Expand Up @@ -188,38 +198,42 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
OneSignalCoreImpl.sharedClient().execute(request) { _ in
// No hydration from response
// On success, remove request from cache
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
self.dispatchQueue.async {
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
}
} onFailure: { error in
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor add aliases request failed with error: \(error.debugDescription)")
if let nsError = error as? NSError {
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
if responseType == .missing {
// Remove from cache and queue
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
// Logout if the user in the SDK is the same
guard OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel)
else {
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
self.dispatchQueue.async {
if let nsError = error as? NSError {
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
if responseType == .missing {
// Remove from cache and queue
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
// Logout if the user in the SDK is the same
guard OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel)
else {
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
return
}
return
// The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model
OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil
OneSignalUserManagerImpl.sharedInstance._logout()
} else if responseType != .retryable {
// Fail, no retry, remove from cache and queue
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
}
// The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model
OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil
OneSignalUserManagerImpl.sharedInstance._logout()
} else if responseType != .retryable {
// Fail, no retry, remove from cache and queue
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
}
}
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
}
}
}
Expand All @@ -243,25 +257,28 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
OneSignalCoreImpl.sharedClient().execute(request) { _ in
// There is nothing to hydrate
// On success, remove request from cache
self.removeRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
self.dispatchQueue.async {
self.removeRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
}
} onFailure: { error in
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor remove alias request failed with error: \(error.debugDescription)")

if let nsError = error as? NSError {
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
if responseType != .retryable {
// Fail, no retry, remove from cache and queue
// A response of .missing could mean the alias doesn't exist on this user OR this user has been deleted
self.removeRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
self.dispatchQueue.async {
if let nsError = error as? NSError {
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
if responseType != .retryable {
// Fail, no retry, remove from cache and queue
// A response of .missing could mean the alias doesn't exist on this user OR this user has been deleted
self.removeRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
}
}
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
}
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
}
}
Expand Down
Loading

0 comments on commit 70fcfb0

Please sign in to comment.