Improve task cancellation management in SubscriptionManager

The widespread usage of the SubscriptionManager caused new crashes to
occur when swapping apps.

This was caused due to an access to Ndb memory after Ndb has been closed
from the app background signal.

The issue was fixed with improved task management logic and ensuring all
subscription tasks are finished before closing Ndb.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2025-08-27 12:10:36 -07:00
parent 76b6d5c545
commit a5aff15491
6 changed files with 72 additions and 10 deletions

View File

@@ -512,6 +512,7 @@ struct ContentView: View {
case .background:
print("txn: 📙 DAMUS BACKGROUNDED")
Task { @MainActor in
await damus_state.nostrNetwork.close() // Close ndb streaming tasks before closing ndb to avoid memory errors
damus_state.ndb.close()
}
break

View File

@@ -234,7 +234,8 @@ class NostrNetworkManager {
// MARK: - App lifecycle functions
func close() {
func close() async {
await self.reader.cancelAllTasks()
pool.close()
}
}

View File

@@ -4,6 +4,7 @@
//
// Created by Daniel DAquino on 2025-03-25.
//
import Foundation
extension NostrNetworkManager {
/// Reads or fetches information from RelayPool and NostrDB, and provides an easier and unified higher-level interface.
@@ -14,10 +15,12 @@ extension NostrNetworkManager {
class SubscriptionManager {
private let pool: RelayPool
private var ndb: Ndb
private var taskManager: TaskManager
init(pool: RelayPool, ndb: Ndb) {
self.pool = pool
self.ndb = ndb
self.taskManager = TaskManager()
}
// MARK: - Reading data from Nostr
@@ -35,6 +38,7 @@ extension NostrNetworkManager {
let ndbStreamTask = Task {
do {
for await item in try self.ndb.subscribe(filters: try filters.map({ try NdbFilter(from: $0) })) {
try Task.checkCancellation()
switch item {
case .eose:
continuation.yield(.eose)
@@ -48,24 +52,71 @@ extension NostrNetworkManager {
}
lend(unownedNote)
}
try Task.checkCancellation()
continuation.yield(.event(borrow: lender))
}
}
}
catch {
Log.error("NDB streaming error: %s", for: .ndb, error.localizedDescription)
Log.error("NDB streaming error: %s", for: .subscription_manager, error.localizedDescription)
}
continuation.finish()
}
let streamTask = Task {
for await _ in self.pool.subscribe(filters: filters, to: desiredRelays) {
// NO-OP. Notes will be automatically ingested by NostrDB
// TODO: Improve efficiency of subscriptions?
do {
for await _ in self.pool.subscribe(filters: filters, to: desiredRelays) {
// NO-OP. Notes will be automatically ingested by NostrDB
// TODO: Improve efficiency of subscriptions?
try Task.checkCancellation()
}
}
catch {
Log.error("Network streaming error: %s", for: .subscription_manager, error.localizedDescription)
}
continuation.finish()
}
Task {
let ndbStreamTaskId = await self.taskManager.add(task: ndbStreamTask)
let streamTaskId = await self.taskManager.add(task: streamTask)
continuation.onTermination = { @Sendable _ in
Task {
await self.taskManager.cancelAndCleanUp(taskId: ndbStreamTaskId)
await self.taskManager.cancelAndCleanUp(taskId: streamTaskId)
}
}
}
continuation.onTermination = { @Sendable _ in
streamTask.cancel() // Close the RelayPool stream when caller stops streaming
ndbStreamTask.cancel()
}
}
func cancelAllTasks() async {
await self.taskManager.cancelAllTasks()
}
actor TaskManager {
private var tasks: [UUID: Task<Void, Never>] = [:]
func add(task: Task<Void, Never>) -> UUID {
let taskId = UUID()
self.tasks[taskId] = task
return taskId
}
func cancelAndCleanUp(taskId: UUID) async {
self.tasks[taskId]?.cancel()
await self.tasks[taskId]?.value
self.tasks[taskId] = nil
return
}
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)
}
Log.info("Cancelled all SubscriptionManager tasks", for: .subscription_manager)
}
}
}

View File

@@ -164,8 +164,10 @@ class DamusState: HeadlessDamusState {
try await self.push_notification_client.revoke_token()
}
wallet.disconnect()
nostrNetwork.close()
ndb.close()
Task {
await nostrNetwork.close() // Close ndb streaming tasks before closing ndb to avoid memory errors
ndb.close()
}
}
static var empty: DamusState {

View File

@@ -14,6 +14,7 @@ enum LogCategory: String {
case render
case storage
case networking
case subscription_manager
case timeline
/// Logs related to Nostr Wallet Connect components
case nwc