ITADN

Proposal: add opt-in NDB_FLAG_NOTLS for multiple read txns per thread

#121Closedalltheseas 创建于 2026-02-09
A
alltheseascommented
### Problem Consumers with complex UI lifecycles can open overlapping read transactions on the same thread. With LMDB default TLS behavior, this fails with `MDB_BAD_RSLOT` (one read txn per thread). A common workaround is transaction inheritance (child lookups reuse parent txn), which avoids `MDB_BAD_RSLOT` but pins a stale snapshot and misses newly committed writes. For example, in damus iOS: a SwiftUI body evaluation opens a read txn, then the ingester commits a new event, then a nested lookup inherits the parent's stale snapshot and returns nil — even though the event exists (see [damus-io/damus#3607](https://github.com/damus-io/damus/issues/3607)). ### Proposal Add an opt-in nostrdb flag that maps to `MDB_NOTLS` at env-open time. 1. Add a new config flag bit: ```c #define NDB_FLAG_NOTLS (1 << 5) // next free bit after NDB_FLAG_NO_STATS ``` 2. Thread flags into LMDB init and map to LMDB flags: ```c static int ndb_init_lmdb(const char *filename, struct ndb_lmdb *lmdb, size_t mapsize, uint32_t ndb_flags) { unsigned int mdb_flags = 0; if (ndb_flags & NDB_FLAG_NOTLS) { mdb_flags |= MDB_NOTLS; } if ((rc = mdb_env_open(lmdb->env, filename, mdb_flags, 0664))) { ... } } ``` 3. Pass `config->flags` from `ndb_init` into `ndb_init_lmdb`. ### Downstream use (example: damus iOS) Consumers that need overlapping read txns on the same thread can pass `NDB_FLAG_NOTLS` and remove txn-inheritance workarounds. Each `NdbTxn` then owns its own read txn and closes it explicitly. ### Tradeoffs **Benefits** - Avoids `MDB_BAD_RSLOT` for same-thread overlapping read txns - Each read txn gets an independent snapshot - Keeps default behavior unchanged for existing consumers **Risks** - With `MDB_NOTLS`, leaked read txns consume reader slots until txn close or env close - Every `ndb_begin_query` must be paired with `ndb_end_query`, including teardown and error paths - `mdb_reader_check()` does not reclaim same-process leaked txns — it only clears stale readers from dead other processes - Opening many concurrent read txns without closing them can eventually hit `MDB_READERS_FULL`. In practice this is unlikely — realistic peak for a mobile app is ~15 concurrent readers against the LMDB default of 126 slots. Neither nostrdb nor damus iOS currently overrides this default. Failure mode is graceful (`ndb_begin_query` returns 0, not a crash). ### Why opt-in (not default) - Preserves existing semantics for consumers that rely on one-txn-per-thread behavior - Retains TLS-based cleanup safety unless explicitly disabled - Lets each consumer choose based on its lifecycle and concurrency model ### Suggested tests 1. Without `NDB_FLAG_NOTLS`: second overlapping same-thread read txn fails (`MDB_BAD_RSLOT` path) 2. With `NDB_FLAG_NOTLS`: overlapping same-thread read txns both succeed and are distinct handles 3. Stress open/close loops: no unbounded reader-slot growth under normal operation
关闭于 2026-02-10 4 条评论