diff --git a/damus/ContentView.swift b/damus/ContentView.swift index dc415752..1eb19bff 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -513,9 +513,21 @@ struct ContentView: View { switch phase { case .background: print("txn: 📙 DAMUS BACKGROUNDED") + let bgTask = this_app.beginBackgroundTask(withName: "Closing things down gracefully", expirationHandler: { [weak damus_state] in + Log.error("App background signal handling: RUNNING OUT OF TIME! JUST CLOSE NDB DIRECTLY!", for: .app_lifecycle) + // Background time about to expire, so close ndb directly. + // This may still cause a memory error crash if subscription tasks have not been properly closed yet, but that is less likely than a 0xdead10cc crash if we don't do anything here. + damus_state?.ndb.close() + }) + damusClosingTask = Task { @MainActor in + Log.debug("App background signal handling: App being backgrounded", for: .app_lifecycle) + let startTime = CFAbsoluteTimeGetCurrent() await damus_state.nostrNetwork.close() // Close ndb streaming tasks before closing ndb to avoid memory errors + Log.debug("App background signal handling: Nostr network closed after %.2f seconds", for: .app_lifecycle, CFAbsoluteTimeGetCurrent() - startTime) damus_state.ndb.close() + Log.debug("App background signal handling: Ndb closed after %.2f seconds", for: .app_lifecycle, CFAbsoluteTimeGetCurrent() - startTime) + this_app.endBackgroundTask(bgTask) } break case .inactive: diff --git a/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift b/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift index 4010e998..5b27699f 100644 --- a/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/NostrNetworkManager.swift @@ -61,9 +61,18 @@ class NostrNetworkManager { } func close() async { - await self.reader.cancelAllTasks() - await self.profilesManager.stop() - pool.close() + await withTaskGroup { group in + // Spawn each cancellation task in parallel for faster execution speed + group.addTask { + await self.reader.cancelAllTasks() + } + group.addTask { + await self.profilesManager.stop() + } + pool.close() + // But await on each one to prevent race conditions + for await value in group { continue } + } } func ping() { diff --git a/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift b/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift index 3d528994..43535efb 100644 --- a/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/ProfilesManager.swift @@ -42,6 +42,7 @@ extension NostrNetworkManager { try await Task.sleep(for: .seconds(1)) try Task.checkCancellation() if subscriptionNeedsUpdate { + try Task.checkCancellation() self.restartProfileListenerTask() subscriptionNeedsUpdate = false } @@ -50,10 +51,19 @@ extension NostrNetworkManager { } func stop() async { - self.subscriptionSwitcherTask?.cancel() - self.profileListenerTask?.cancel() - try? await self.subscriptionSwitcherTask?.value - try? await self.profileListenerTask?.value + await withTaskGroup { group in + // Spawn each cancellation in parallel for better execution speed + group.addTask { + await self.subscriptionSwitcherTask?.cancel() + try? await self.subscriptionSwitcherTask?.value + } + group.addTask { + await self.profileListenerTask?.cancel() + try? await self.profileListenerTask?.value + } + // But await for all of them to be done before returning to avoid race conditions + for await value in group { continue } + } } private func restartProfileListenerTask() { @@ -70,6 +80,7 @@ extension NostrNetworkManager { let pubkeys = Array(streams.keys) guard pubkeys.count > 0 else { return } let profileFilter = NostrFilter(kinds: [.metadata], authors: pubkeys) + try Task.checkCancellation() for await ndbLender in self.subscriptionManager.streamIndefinitely(filters: [profileFilter], streamMode: .ndbFirst) { try Task.checkCancellation() try? ndbLender.borrow { ev in diff --git a/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift b/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift index 0207f121..86fa8b79 100644 --- a/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift +++ b/damus/Core/Networking/NostrNetworkManager/SubscriptionManager.swift @@ -387,12 +387,21 @@ extension NostrNetworkManager { } func cancelAllTasks() async { - Log.info("Cancelling all SubscriptionManager tasks", for: .subscription_manager) - for (taskId, _) in self.tasks { - Log.info("Cancelling SubscriptionManager task %s", for: .subscription_manager, taskId.uuidString) - await cancelAndCleanUp(taskId: taskId) + await withTaskGroup { group in + Log.info("Cancelling all SubscriptionManager tasks", for: .subscription_manager) + // Start each task cancellation in parallel for faster execution + for (taskId, _) in self.tasks { + Log.info("Cancelling SubscriptionManager task %s", for: .subscription_manager, taskId.uuidString) + group.addTask { + await self.cancelAndCleanUp(taskId: taskId) + } + } + // However, wait until all cancellations are complete to avoid race conditions. + for await value in group { + continue + } + Log.info("Cancelled all SubscriptionManager tasks", for: .subscription_manager) } - Log.info("Cancelled all SubscriptionManager tasks", for: .subscription_manager) } } } diff --git a/damus/Shared/Utilities/Log.swift b/damus/Shared/Utilities/Log.swift index 216e7e53..81a8d3ce 100644 --- a/damus/Shared/Utilities/Log.swift +++ b/damus/Shared/Utilities/Log.swift @@ -14,6 +14,7 @@ enum LogCategory: String { case render case storage case networking + case app_lifecycle case subscription_manager case timeline /// Logs related to Nostr Wallet Connect components