Eliminates dataSetsMutex and weak_ptr::lock() contention from the steady-state
read path, unlocking high-QPS workloads with many concurrent subscribers
(e.g., CDC replication).
Read-path changes (TransactionLogStore + TransactionLogHandle):
- Per-handle file snapshot cache (cachedFiles + filesVersion). Handles refresh
the snapshot only when the store's filesVersion atomic advances (rotation,
registration, purge); steady-state reads walk the local snapshot lock-free.
- Lock-free findPosition: walks cachedFiles newest-to-oldest, skipping files
by their stored timestamp. Falls back to the store-level slow path only
when the snapshot is empty.
- Lock-free getLogFileSize fast path: reads logFile->size atomic via the
cached snapshot. Falls through to the slow path only when sequenceNumber=0
or the file is in the snapshot but not yet opened.
- Cached shared_ptr<TransactionLogStore> on the handle replaces the per-call
weak_ptr::lock() CAS. Each read-method now does an isClosing.load() check
instead. addEntry re-resolves to a fresh store and clears cachedFiles when
isClosing is observed.
- currentSequenceNumber is now atomic (was plain uint32_t) so handle
fast-path readers can compare without acquiring dataSetsMutex.
Per-file index changes (TransactionLogFile):
- Lock-free in-file timestamp index using a stable buffer + packed atomic
state (low 32 bits = entry count, high 32 bits = position indexed up to).
Replaces the std::map + indexMutex serialization. Acquire/release ordering
on indexState publishes new entries safely to lock-free readers.
- Slow-path index extension serializes only on indexExtendMutex (per-file),
not the global dataSetsMutex.
- Removed eager ensureIndexUpToDate at registerLogFile — saves up to
maxFileSize/13 × 16 bytes per recovered file at startup. The lazy slow
path handles the first reader instead.
- Inlined extendIndexLocked into findPositionByTimestamp (only caller).
Test coverage:
Adds 9 new regression tests covering the cases the changes affected:
- per-handle cache invalidation across many rotations
- concurrent first-time readers on a freshly-opened (unindexed) log
- multiple handles on the same log staying consistent
- iterator resume across rotations
- crash-free behavior under concurrent reads + purgeLogs(destroy:true)
- queries after attempting to purge earlier files
- ...and more
Bench:
Adds benchmark/worker-transaction-log-read.bench.ts — 4 access patterns
(bulk forward scan, bulk forward scan with concurrent writer, high-frequency
short-range queries, cursor-advance tail scan with writer) at 8 workers,
comparing rocksdb-js against lmdb with matched lazy-durability semantics
(noSync) and Harper-realistic LMDB options (snapshot:false, numeric keys).
Headline impact (8 workers, 3-run averages):
Short-range queries (1k iterators × 8 workers):
baseline: 114 hz | optimized: 2,576 hz (22.6× speedup, 5.0× faster than lmdb)
Bulk forward scan with concurrent writer:
baseline: 2,248 hz | optimized: 2,502 hz (+11%, 26.6× faster than lmdb)
Bulk forward scan, no writes:
baseline: 3,136 hz | optimized: 3,168 hz (+1%, 38× faster than lmdb)
The high-QPS short-range pattern is most representative of CDC subscriber
polling — that workload moves from a regression vs lmdb to a 5× lead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eliminates dataSetsMutex and weak_ptr::lock() contention from the steady-state read path, unlocking high-QPS workloads with many concurrent subscribers (e.g., CDC replication).
Read-path changes (TransactionLogStore + TransactionLogHandle):
Per-file index changes (TransactionLogFile):
Test coverage:
Adds 9 new regression tests covering the cases the changes affected:
Bench:
Adds benchmark/worker-transaction-log-read.bench.ts — 4 access patterns (bulk forward scan, bulk forward scan with concurrent writer, high-frequency short-range queries, cursor-advance tail scan with writer) at 8 workers, comparing rocksdb-js against lmdb with matched lazy-durability semantics (noSync) and Harper-realistic LMDB options (snapshot:false, numeric keys).