Files
damus/nostrdb/NdbTxn.swift
T
Daniel D’Aquino 034f667869 Fix per-thread txn leaks when SafeNdbTxn init fails
This addresses one failure path where if the value getter for a new
transaction returns `nil`, it would leak a read transaction, causing it
to remain open indefinitely, and leading to runaway storage usage.

Changelog-Fixed: Fixed database transaction leak that could lead to
higher storage usage

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2026-05-07 17:09:34 -07:00

338 lines
13 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 {
if inherited {
if let refCount = Thread.current.threadDictionary["ndb_txn_ref_count"] as? Int {
Thread.current.threadDictionary["ndb_txn_ref_count"] = max(0, refCount - 1)
}
return nil
}
_ = try? ndb.withNdb({
ndb_end_query(&placeholderTxn.txn)
}, maxWaitTimeout: .milliseconds(200))
Thread.current.threadDictionary.removeObject(forKey: "ndb_txn")
Thread.current.threadDictionary.removeObject(forKey: "ndb_txn_ref_count")
Thread.current.threadDictionary.removeObject(forKey: "txn_generation")
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 } }