Skip to content

Commit

Permalink
Improve Concurrent Access to the Local Storage (#32)
Browse files Browse the repository at this point in the history
# Improve Concurrent Access to the Local Storage

## ♻️ Current situation & Problem
- We are currently getting crash reports on iOS 16.6.1 about crashes
when using the local storage module for storing information in the
scheduler.


## ⚙️ Release Notes 
- Adds additional locking mechanisms for the local storage access to
reduce possible errors due to race conditions.


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
PSchmiedmayer authored Sep 28, 2023
1 parent 42c680c commit dcba988
Showing 1 changed file with 60 additions and 47 deletions.
107 changes: 60 additions & 47 deletions Sources/SpeziScheduler/Scheduler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@ public class Scheduler<Context: Codable>: NSObject, UNUserNotificationCenterDele
return
}

persistChanges()
_Concurrency.Task {
await persistChanges()
}
}
}
@AppStorage("Spezi.Scheduler.firstlaunch") private var firstLaunch = true
private var initialTasks: [Task<Context>]
private var cancellables: Set<AnyCancellable> = []
private let prescheduleNotificationLimit: Int
private let localStorageLock = Lock()


/// Indicates whether the necessary authorization to deliver local notifications is already granted.
public var localNotificationAuthorization: Bool {
Expand All @@ -45,6 +49,7 @@ public class Scheduler<Context: Codable>: NSObject, UNUserNotificationCenterDele
}
}


/// Creates a new ``Scheduler`` module.
/// - Parameter prescheduleLimit: The number of prescheduled notifications that should be registerd.
/// Must be bigger than 1 and smaller than the limit of 64 local notifications at a time.
Expand Down Expand Up @@ -83,7 +88,13 @@ public class Scheduler<Context: Codable>: NSObject, UNUserNotificationCenterDele
)

_Concurrency.Task {
guard let storedTasks = try? localStorage.read([Task<Context>].self, storageKey: Constants.taskStorageKey) else {
var storedTasks: [Task<Context>]? // swiftlint:disable:this discouraged_optional_collection

await localStorageLock.enter {
storedTasks = try? localStorage.read([Task<Context>].self, storageKey: Constants.taskStorageKey)
}

guard let storedTasks = storedTasks else {
await schedule(tasks: initialTasks)
return
}
Expand All @@ -103,40 +114,6 @@ public class Scheduler<Context: Codable>: NSObject, UNUserNotificationCenterDele
}
}

public func willFinishLaunchingWithOptions(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) {
UNUserNotificationCenter.current().delegate = self
}

public func applicationWillTerminate(_ application: UIApplication) {
persistChanges()
}

// Unfortunately, the async overload of the `UNUserNotificationCenterDelegate` results in a runtime crash.
// Reverify this in iOS versions after iOS 17.0
public func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
await sendObjectWillChange()
}

// Unfortunately, the async overload of the `UNUserNotificationCenterDelegate` results in a runtime crash.
// Reverify this in iOS versions after iOS 17.0
public func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
await sendObjectWillChange()
return [.badge, .banner, .sound, .list]
}

public func sceneWillEnterForeground(_ scene: UIScene) {
_Concurrency.Task {
await sendObjectWillChange()
}
}


/// Schedule a new ``Task`` in the ``Scheduler`` module.
/// - Parameter task: The new ``Task`` instance that should be scheduled.
public func schedule(task: Task<Context>) async {
Expand All @@ -154,29 +131,65 @@ public class Scheduler<Context: Codable>: NSObject, UNUserNotificationCenterDele
if task.notifications {
await self.updateScheduleNotifications()
}
persistChanges()
await persistChanges()

await sendObjectWillChange(skipInternalUpdates: true)
}

func persistChanges() {
do {
try self.localStorage.store(self.tasks, storageKey: Constants.taskStorageKey)
} catch {
os_log(.error, "Spezi.Scheduler: Could not persist the tasks of the scheduler module: \(error)")

// MARK: - Lifecycle
@_documentation(visibility: internal)
public func willFinishLaunchingWithOptions(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) {
UNUserNotificationCenter.current().delegate = self
}

@_documentation(visibility: internal)
public func sceneWillEnterForeground(_ scene: UIScene) {
_Concurrency.Task {
await sendObjectWillChange()
}
}

@_documentation(visibility: internal)
public func applicationWillTerminate(_ application: UIApplication) {
_Concurrency.Task {
await persistChanges()
}
}


// MARK: - Notification Center
@_documentation(visibility: internal)
@MainActor
public func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
[.badge, .banner, .sound, .list]
}


// MARK: - Helper Methods
private func persistChanges() async {
await localStorageLock.enter {
do {
try self.localStorage.store(self.tasks, storageKey: Constants.taskStorageKey)
} catch {
os_log(.error, "Spezi.Scheduler: Could not persist the tasks of the scheduler module: \(error)")
}
}
}

func sendObjectWillChange(skipInternalUpdates: Bool = false) async {
os_log(.debug, "Spezi.Scheduler: Object will change (skipInternalUpdates: \(skipInternalUpdates)")
private func sendObjectWillChange(skipInternalUpdates: Bool = false) async {
os_log(.debug, "Spezi.Scheduler: Object will change (skipInternalUpdates: \(skipInternalUpdates))")
if skipInternalUpdates {
await MainActor.run {
self.objectWillChange.send()
}
} else {
self.updateTasks()
await self.updateScheduleNotifications()
self.persistChanges()
await updateScheduleNotifications()
await persistChanges()
await MainActor.run {
self.objectWillChange.send()
}
Expand Down Expand Up @@ -286,6 +299,6 @@ public class Scheduler<Context: Codable>: NSObject, UNUserNotificationCenterDele
await task.scheduleNotification(prescheduleNotificationLimitPerTask)
}

persistChanges()
await persistChanges()
}
}

0 comments on commit dcba988

Please sign in to comment.