fix(resources): store the real prior version for LMDB audit entries - #1988
Conversation
…F-249) The LMDB write path passed a 0/1 flag as previousVersion instead of the actual prior version RocksDB stores. A flag of 1 hit auditStore's placeholder-sentinel branch, which lmdb-js's native writer substitutes from a per-environment register rather than this record's true prior version — corrupting audit-history reconstruction, which walks previousVersion to page backward through the log. Store existingEntry?.version uniformly for both engines, matching the already-correct RocksDB path. Co-Authored-By: Claude Opus <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request simplifies the assignment of previousVersion in RecordEncoder.ts by directly using existingEntry?.version instead of using placeholder flags. It also adds a unit test in auditLog.test.js to verify that audit entries correctly chain to the actual prior version. There are no review comments, so I have no feedback to provide.
|
Reviewed; no blockers found. |
…immediately CI hit a pre-existing, unrelated timing race: the background revalidation fetch is kicked off after search() already returns the stale row, so asserting sourceRequests === 1 immediately races the source call's own tick. Poll for it instead, matching the identical pattern already used a few lines above for the single-record SWR path (and the fix already landed for this same test on another in-flight branch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… version, as previousVersion Codex's independent pre-push review caught a real bug in the first fix: existingEntry.version is the record's application-level/origin version, which for LMDB can diverge from its actual audit-store key (localTime) — lmdb-js assigns localTime via its own native monotonic clock, independent of the caller-supplied version. This is normally a few ms of drift, but for a replicated write carrying a backdated origin timestamp (clock skew, catch-up replay) the two values diverge by however far the origin lags — producing a previousVersion that doesn't resolve to any real audit entry, breaking history reconstruction specifically for replicated records. RocksDB has no such divergence (its local timestamp is explicitly forced to equal the version at write time), so this only affects LMDB. Store existingEntry?.localTime instead, which is what the audit store is actually keyed by on both engines. Added a regression test for the replicated/backdated-version case, and corrected the existing chain test to assert against the resolvable audit-store key rather than the numerically-similar-but-wrong origin version. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…yOfRecord regression test Per review from Kris: use existingEntry?.localTime uniformly for both engines, but state RocksDB's branch explicitly (isRocksDB ? version : localTime) rather than relying on version and localTime happening to be equal on that engine — keeps each engine's key space explicit instead of assuming an incidental equality holds. Added a regression test driving Table.getHistoryOfRecord directly (the real production consumer) with backdated origin versions and real delays spanning multiple 100ms audit windows — this is what actually breaks in production for replicated/out-of-order writes, and it does reproduce (truncates to 1 entry instead of 4) against the version-based fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Reply re: the LMDB key-space blocker ( Fixed in b191a5c — Also added Root cause, for the record: Signed, Claude Sonnet 5 |
|
Re: Codex's latest finding ("repeat writes to one LMDB key in a transaction now form sibling audit entries and silently misreconstruct history") — I reproduced this, but it's pre-existing on Repro: So this is a separate, pre-existing LMDB limitation around repeat-write-same-key-in-one-transaction chaining, not something this PR's Signed, Claude Sonnet 5 |
Ethan-Arrowood
left a comment
There was a problem hiding this comment.
Correct previousVersion fix, fully pinned by tests including the production consumer walk — and it removes an instanceof per write.
sent with Claude Fable 5
What / why
RecordEncoder.ts:854handed LMDB a boolean0/1flag aspreviousVersionwhere RocksDB got the real prior version. A flag of1sends the write downauditStore.ts's placeholder-sentinel branch, where lmdb-js's native writer substitutes the value from a per-environment register rather than this record's true prior version. SincegetHistoryOfRecord(Table.ts) walkspreviousVersionbackward to page through audit-log windows, a corrupted pointer breaks audit-history reconstruction.Fix (revised after review):
previousVersion: isRocksDB ? existingEntry?.version : existingEntry?.localTime. My first pass usedexistingEntry?.versionuniformly, on the assumption thatversionandlocalTimeare the same field for both engines — true for RocksDB (its local timestamp is explicitly forced to equalversionat write time), but not for LMDB: lmdb-js assignslocalTimevia its own native monotonic clock, independent of the caller-suppliedversion. For a replicated/out-of-order write carrying a backdated origin version (clock skew, catch-up replay), the two diverge arbitrarily, and LMDB's audit store is keyed bylocalTime, notversion— so a.version-based pointer doesn't resolve to any real audit entry. Both Codex's independent pre-push review and @kriszyp's review caught this; details in the review thread.Refs #F-249 (qa-wave-2026072900)
Test plan
unitTests/resources/auditLog.test.js:previousVersionresolves viaauditStore.get()to the true prior entry's own key. Fails on the original 0/1-flag code (decodes to a plausible-but-wrong float), passes with the fix, on both engines.timestamp:, confirms resolution throughauditStore.get(). Fails against a.version-based fix (the bug this review round caught).delay(150)between writes so entries genuinely span multiple 100ms audit windows, asserted through the real production consumer (Table.getHistoryOfRecord). Reproduces the actual failure mode (truncates to 1 entry instead of 4) against a.version-based fix.npm run build,npm run test:unit:resources(both engines),npm run test:unit:main,npm run format:write,npm run lint:requiredall clean. The 10 pre-existingglobalIsolation.test.jsfailures are present onorigin/maintoo — unrelated to this change.Review history
.version-based fix; I initially misjudged it as a false alarm (verifiedversion/localTimeequal in a non-explicit-timestamp scenario) and pushed anyway.timestamp:writes, which is what exposed the divergence. Fixed in b191a5c — see the resolved review threads and the PR comment for the full root-cause writeup.🤖 Generated with Claude Code