Files
damus/nostrdb/NdbTxn.swift
Daniel D’Aquino 20dc672dbf Add sync mechanism to prevent background crashes and fix ndb reopen order
This adds a sync mechanism in Ndb.swift to coordinate certain usage of
nostrdb.c calls and the need to close nostrdb due to app lifecycle
requirements. Furthermore, it fixes the order of operations when
re-opening NostrDB, to avoid race conditions where a query uses an older
Ndb generation.

This sync mechanism allows multiple queries to happen simultaneously
(from the Swift-side), while preventing ndb from simultaneously closing
during such usages. It also does that while keeping the Ndb interface
sync and nonisolated, which keeps the API easy to use from
Swift/SwiftUI and allows for parallel operations to occur.

If Swift Actors were to be used (e.g. creating an NdbActor), the Ndb.swift
interface would change in such a way that it would propagate the need for
several changes throughout the codebase, including loading logic in
some ViewModels. Furthermore, it would likely decrease performance by
forcing Ndb.swift operations to run sequentially when they could run in
parallel.

Changelog-Fixed: Fixed crashes that happened when the app went into background mode
Closes: https://github.com/damus-io/damus/issues/3245
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-12-29 11:01:23 -08:00

323 lines
12 KiB
Swift

//
// NdbTx.swift
// damus
//
// Created by William Casarin on 2023-08-30.
//
import Foundation
#if TXNDEBUG
fileprivate var txn_count: Int = 0
#endif
// Would use struct and ~Copyable but generics aren't supported well
class NdbTxn<T>: RawNdbTxnAccessible {
var txn: ndb_txn
private var val: T!
var moved: Bool
var inherited: Bool
var ndb: Ndb
var generation: Int
var name: String
static func pure(ndb: Ndb, val: T) -> NdbTxn<T> {
.init(ndb: ndb, txn: ndb_txn(), val: val, generation: ndb.generation, inherited: true, name: "pure_txn")
}
/// Simple helper struct for the init function to avoid compiler errors encountered by using other techniques
private struct R {
let txn: ndb_txn
let generation: Int
}
init?(ndb: Ndb, with: (NdbTxn<T>) -> T = { _ in () }, name: String? = nil) {
guard !ndb.is_closed else { return nil }
self.name = name ?? "txn"
self.ndb = ndb
self.generation = ndb.generation
if let active_txn = Thread.current.threadDictionary["ndb_txn"] as? ndb_txn,
let txn_generation = Thread.current.threadDictionary["txn_generation"] as? Int,
txn_generation == ndb.generation
{
// some parent thread is active, use that instead
print("txn: inherited txn")
self.txn = active_txn
self.inherited = true
self.generation = Thread.current.threadDictionary["txn_generation"] as! Int
let ref_count = Thread.current.threadDictionary["ndb_txn_ref_count"] as! Int
let new_ref_count = ref_count + 1
Thread.current.threadDictionary["ndb_txn_ref_count"] = new_ref_count
} else {
let result: R? = try? ndb.withNdb({
var txn = ndb_txn()
#if TXNDEBUG
txn_count += 1
#endif
let ok = ndb_begin_query(ndb.ndb.ndb, &txn) != 0
guard ok else { return .none }
return .some(R(txn: txn, generation: ndb.generation))
}, maxWaitTimeout: .milliseconds(200))
guard let result else { return nil }
self.txn = result.txn
self.generation = result.generation
Thread.current.threadDictionary["ndb_txn"] = self.txn
Thread.current.threadDictionary["ndb_txn_ref_count"] = 1
Thread.current.threadDictionary["txn_generation"] = ndb.generation
self.inherited = false
}
#if TXNDEBUG
print("txn: open gen\(self.generation) '\(self.name)' \(txn_count)")
#endif
self.moved = false
self.val = with(self)
}
private init(ndb: Ndb, txn: ndb_txn, val: T, generation: Int, inherited: Bool, name: String) {
self.txn = txn
self.val = val
self.moved = false
self.inherited = inherited
self.ndb = ndb
self.generation = generation
self.name = name
}
/// Only access temporarily! Do not store database references for longterm use. If it's a primitive type you
/// can retrieve this value with `.value`
internal var unsafeUnownedValue: T {
precondition(!moved)
return val
}
deinit {
if self.generation != ndb.generation {
print("txn: OLD GENERATION (\(self.generation) != \(ndb.generation)), IGNORING")
return
}
if ndb.is_closed {
print("txn: not closing. db closed")
return
}
if let ref_count = Thread.current.threadDictionary["ndb_txn_ref_count"] as? Int {
let new_ref_count = ref_count - 1
Thread.current.threadDictionary["ndb_txn_ref_count"] = new_ref_count
assert(new_ref_count >= 0, "NdbTxn reference count should never be below zero")
if new_ref_count <= 0 {
_ = try? ndb.withNdb({
ndb_end_query(&self.txn)
}, maxWaitTimeout: .milliseconds(200))
Thread.current.threadDictionary.removeObject(forKey: "ndb_txn")
Thread.current.threadDictionary.removeObject(forKey: "ndb_txn_ref_count")
}
}
if inherited {
print("txn: not closing. inherited ")
return
}
if moved {
//print("txn: not closing. moved")
return
}
#if TXNDEBUG
txn_count -= 1;
print("txn: close gen\(generation) '\(name)' \(txn_count)")
#endif
}
// functor
func map<Y>(_ transform: (T) -> Y) -> NdbTxn<Y> {
self.moved = true
return .init(ndb: self.ndb, txn: self.txn, val: transform(val), generation: generation, inherited: inherited, name: self.name)
}
// comonad!?
// useful for moving ownership of a transaction to another value
func extend<Y>(_ with: (NdbTxn<T>) -> Y) -> NdbTxn<Y> {
self.moved = true
return .init(ndb: self.ndb, txn: self.txn, val: with(self), generation: generation, inherited: inherited, name: self.name)
}
}
protocol RawNdbTxnAccessible: AnyObject {
var txn: ndb_txn { get set }
}
class PlaceholderNdbTxn: RawNdbTxnAccessible {
var txn: ndb_txn
init(txn: ndb_txn) {
self.txn = txn
}
}
class SafeNdbTxn<T: ~Copyable> {
var txn: ndb_txn
var val: T!
var moved: Bool
var inherited: Bool
var ndb: Ndb
var generation: Int
var name: String
static func pure(ndb: Ndb, val: consuming T) -> SafeNdbTxn<T> {
.init(ndb: ndb, txn: ndb_txn(), val: val, generation: ndb.generation, inherited: true, name: "pure_txn")
}
/// Simple helper struct for the init function to avoid compiler errors encountered by using other techniques
private struct R {
let txn: ndb_txn
let generation: Int
}
static func new(on ndb: Ndb, with valueGetter: (PlaceholderNdbTxn) -> T? = { _ in () }, name: String = "txn") -> SafeNdbTxn<T>? {
guard !ndb.is_closed else { return nil }
let generation: Int
let txn: ndb_txn
let inherited: Bool
if let active_txn = Thread.current.threadDictionary["ndb_txn"] as? ndb_txn,
let txn_generation = Thread.current.threadDictionary["txn_generation"] as? Int,
txn_generation == ndb.generation
{
// some parent thread is active, use that instead
print("txn: inherited txn")
txn = active_txn
inherited = true
generation = Thread.current.threadDictionary["txn_generation"] as! Int
let ref_count = Thread.current.threadDictionary["ndb_txn_ref_count"] as! Int
let new_ref_count = ref_count + 1
Thread.current.threadDictionary["ndb_txn_ref_count"] = new_ref_count
} else {
let result: R? = try? ndb.withNdb({
var txn = ndb_txn()
#if TXNDEBUG
txn_count += 1
#endif
let ok = ndb_begin_query(ndb.ndb.ndb, &txn) != 0
guard ok else { return .none }
return .some(R(txn: txn, generation: ndb.generation))
}, maxWaitTimeout: .milliseconds(200))
guard let result else { return nil }
txn = result.txn
generation = result.generation
Thread.current.threadDictionary["ndb_txn"] = txn
Thread.current.threadDictionary["ndb_txn_ref_count"] = 1
Thread.current.threadDictionary["txn_generation"] = ndb.generation
inherited = false
}
#if TXNDEBUG
print("txn: open gen\(generation) '\(name)' \(txn_count)")
#endif
let placeholderTxn = PlaceholderNdbTxn(txn: txn)
guard let val = valueGetter(placeholderTxn) else { return nil }
return SafeNdbTxn<T>(ndb: ndb, txn: txn, val: val, generation: generation, inherited: inherited, name: name)
}
private init(ndb: Ndb, txn: ndb_txn, val: consuming T, generation: Int, inherited: Bool, name: String) {
self.txn = txn
self.val = consume val
self.moved = false
self.inherited = inherited
self.ndb = ndb
self.generation = generation
self.name = name
}
deinit {
if self.generation != ndb.generation {
print("txn: OLD GENERATION (\(self.generation) != \(ndb.generation)), IGNORING")
return
}
if ndb.is_closed {
print("txn: not closing. db closed")
return
}
if let ref_count = Thread.current.threadDictionary["ndb_txn_ref_count"] as? Int {
let new_ref_count = ref_count - 1
Thread.current.threadDictionary["ndb_txn_ref_count"] = new_ref_count
assert(new_ref_count >= 0, "NdbTxn reference count should never be below zero")
if new_ref_count <= 0 {
_ = try? ndb.withNdb({
ndb_end_query(&self.txn)
}, maxWaitTimeout: .milliseconds(200))
Thread.current.threadDictionary.removeObject(forKey: "ndb_txn")
Thread.current.threadDictionary.removeObject(forKey: "ndb_txn_ref_count")
}
}
if inherited {
print("txn: not closing. inherited ")
return
}
if moved {
//print("txn: not closing. moved")
return
}
#if TXNDEBUG
txn_count -= 1;
print("txn: close gen\(generation) '\(name)' \(txn_count)")
#endif
}
// functor
func map<Y>(_ transform: (borrowing T) -> Y) -> SafeNdbTxn<Y> {
self.moved = true
return .init(ndb: self.ndb, txn: self.txn, val: transform(val), generation: generation, inherited: inherited, name: self.name)
}
// comonad!?
// useful for moving ownership of a transaction to another value
func extend<Y>(_ with: (SafeNdbTxn<T>) -> Y) -> SafeNdbTxn<Y> {
self.moved = true
return .init(ndb: self.ndb, txn: self.txn, val: with(self), generation: generation, inherited: inherited, name: self.name)
}
consuming func maybeExtend<Y>(_ with: (consuming SafeNdbTxn<T>) -> Y?) -> SafeNdbTxn<Y>? where Y: ~Copyable {
self.moved = true
let ndb = self.ndb
let txn = self.txn
let generation = self.generation
let inherited = self.inherited
let name = self.name
guard let newVal = with(consume self) else { return nil }
return .init(ndb: ndb, txn: txn, val: newVal, generation: generation, inherited: inherited, name: name)
}
}
protocol OptionalType {
associatedtype Wrapped
var optional: Wrapped? { get }
}
extension Optional: OptionalType {
typealias Wrapped = Wrapped
var optional: Wrapped? {
return self
}
}
extension NdbTxn where T: OptionalType {
func collect() -> NdbTxn<T.Wrapped>? {
guard let unwrappedVal: T.Wrapped = val.optional else {
return nil
}
self.moved = true
return NdbTxn<T.Wrapped>(ndb: self.ndb, txn: self.txn, val: unwrappedVal, generation: generation, inherited: inherited, name: name)
}
}
extension NdbTxn where T == Bool { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == Bool? { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == Int { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == Int? { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == Double { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == Double? { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == UInt64 { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == UInt64? { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == String { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == String? { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == NoteId? { var value: T { return self.unsafeUnownedValue } }
extension NdbTxn where T == NoteId { var value: T { return self.unsafeUnownedValue } }