Skip to content

Record caching - #410

Merged
kriszyp merged 34 commits into
mainfrom
record-caching
Jul 11, 2026
Merged

Record caching#410
kriszyp merged 34 commits into
mainfrom
record-caching

Conversation

@kriszyp

@kriszyp kriszyp commented Apr 26, 2026

Copy link
Copy Markdown
Member

Introduces CachingRocksDatabase, a RocksDatabase subclass that layers a
per-instance WeakLRUCache on top of the process-wide VerificationTable.
Cache freshness is checked via entry.version (Harper's localTime timestamp)
rather than a txnId, so reads hitting a fresh VT slot skip disk entirely.

Primary RocksDB stores now use CachingRocksDatabase instead of RocksDatabase.

DatabaseTransaction creates transactions with coordinatedRetry: true, so
IsBusy conflicts are signalled as RETRY_NOW_VALUE (immediate retry, no
backoff) rather than an ERR_BUSY rejection. The old quadratic-backoff retry
handler is removed.

kriszyp and others added 2 commits April 25, 2026 20:56
Introduces CachingRocksDatabase, a RocksDatabase subclass that layers a
per-instance WeakLRUCache on top of the process-wide VerificationTable.
Cache freshness is checked via entry.version (Harper's localTime timestamp)
rather than a txnId, so reads hitting a fresh VT slot skip disk entirely.

Primary RocksDB stores now use CachingRocksDatabase instead of RocksDatabase.

DatabaseTransaction creates transactions with coordinatedRetry: true, so
IsBusy conflicts are signalled as RETRY_NOW_VALUE (immediate retry, no
backoff) rather than an ERR_BUSY rejection. The old quadratic-backoff retry
handler is removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests cover:
- primaryStore is a CachingRocksDatabase (and RocksDatabase) instance
- Basic read/write correctness
- VT slot population after two reads enabling the fast-path
- Third read returns value without DB access (VT fast-path)
- Write clears VT slot so stale version cannot verify
- Remove clears cache; subsequent read returns undefined
- Transaction-scoped reads bypass cache and return correct value
- Concurrent writes complete without error via coordinatedRetry
- Write-then-read returns updated value, not stale cache

Tests are skipped when HARPER_STORAGE_ENGINE=lmdb.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread resources/CachingRocksDatabase.ts Outdated

getSync(id: any, options?: any): any {
if (options?.transaction) {
return super.getSync(id, options);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untested branch (blocker): The if (options?.transaction) bypass is never exercised by the test suite. The test labelled "Read with a transaction bypasses the cache" passes context = {}, which has no transaction property — options?.transaction is falsy and the cache path is taken, not the bypass. Both legs of this production-vs-fallback split need coverage. An actual RocksTransaction (or a Harper DatabaseTransaction-backed context) needs to be passed to verify the bypass reaches super.getSync without touching the cache.

Comment thread resources/DatabaseTransaction.ts Outdated
if (commitResult === RETRY_NOW_VALUE) {
this.retries++;
harperLogger.debug?.('coordinated retry', transaction.id, this.retries);
return this.commit({ transaction });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No application-level retry bound: this.retries++ is incremented on each RETRY_NOW_VALUE but never checked against a limit. The old ERR_BUSY handler had MAX_RETRIES = 40 (now dead code at line 23) that threw ServerError after 40 attempts. If @harperfast/rocksdb-js guarantees it will eventually stop returning RETRY_NOW_VALUE under any contention scenario, please document that guarantee. Otherwise, reinstate an application-level cap so sustained conflicts don't produce unbounded recursion.

@claude

claude Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Review: Record caching (PR #410)

1. weak-lru-cache missing from dependencies.md — BLOCKER

File: package.json

What: A new runtime dependency (weak-lru-cache) is added but has no entry in dependencies.md.

Why it matters: The repo policy (documented in dependencies.md itself) requires every new runtime dep to be reviewed and documented there — size, security track record, memory cost, eventual-removal plan, etc. This is enforced in code review.

Suggested fix: Add a ## weak-lru-cache section to dependencies.md answering the standard questions. (The author, Kris Zyp, is the original author of weak-lru-cache, so the section can be brief, but it still needs to exist so the policy holds for future reviewers.)


2. Transaction-bypass branch untested — BLOCKER

File: resources/CachingRocksDatabase.ts:22–23 (also get, line 44)

What: Both getSync and get have an if (options?.transaction) branch that bypasses the cache and delegates directly to super. The test labelled "Read with a transaction bypasses the cache" passes context = {} — an empty object with no transaction property — so options?.transaction is falsy and the non-bypass (cache) path is taken. The bypass branch itself is never exercised.

Why it matters: Per the testing rules, a PR-introduced production-vs-fallback split requires both legs to have coverage. A read executed inside an active transaction is a real production path; silently falling through to cached data when a transaction is present would be a correctness bug.

Suggested fix: Add a test that creates an actual transaction (e.g. via DatabaseTransaction or a direct RocksTransaction) and passes it in the options, then asserts the result is correct and that no WeakLRUCache entry was written for that key.


3. MAX_RETRIES is dead code; coordinated-retry loop has no application-level bound — finding

File: resources/DatabaseTransaction.ts:23 / 269

What: const MAX_RETRIES = 40 is unreferenced — its only consumer was the removed ERR_BUSY handler. The new RETRY_NOW_VALUE path increments this.retries on each retry but never checks it against any limit.

Why it matters: The old code would throw a ServerError after 40 attempts, preventing unbounded recursion under sustained contention. If the coordinatedRetry: true mechanism in @harperfast/rocksdb-js internally bounds the number of RETRY_NOW_VALUE returns (i.e. it eventually throws an error rather than looping forever), the application-level guard isn't needed — but that guarantee should be stated explicitly. If there's no such guarantee, the removal is a regression in resilience.

Suggested fix: Either (a) remove MAX_RETRIES and add a comment explaining that the C++ layer bounds retries and will throw after N attempts, or (b) keep an application-level cap and throw ServerError if this.retries exceeds it.


Surfaces verified

  • CachingRocksDatabase: cache invalidation on putSync/removeSync ✓; put/remove patched to putSync/removeSync in openRocksDatabase ✓; verificationTable: true forced in constructor ✓; FRESH_VERSION_FLAG fast-path logic ✓; dupSort stores (RocksIndexStore) correctly bypass caching ✓
  • DatabaseTransaction: coordinatedRetry: true wiring ✓; success-path retry loop ✓; old ERR_BUSY/backoff removal ✓
  • databases.ts: CachingRocksDatabase replaces RocksDatabase only for non-dupSort primary stores ✓
  • dependencies.md: weak-lru-cache absent ✗ (finding 1)
  • Test file: happy-path CRUD, VT population, write-clears-cache, concurrent-writes ✓; transaction-bypass branch ✗ (finding 2)

kriszyp and others added 4 commits April 25, 2026 22:19
Benchmarks six scenarios with 2,000 records (~80-byte values each):

  1. getSync cold          — first read, cache miss (both go to DB)
  2. getSync soft VT miss  — 2nd read: cache warm, expectedVersion passed,
                             DB read happens but slot is populated + FRESH returned
  3. getSync VT fast-path  — 3rd+ read: VT slot matches, no DB access
  4. getSync hot key       — repeated single-key reads
  5. get (async) VT hit    — async path with warm cache
  6. putSync               — write throughput (cache invalidation cost)

Each scenario reports ops/sec for plain RocksDatabase and CachingRocksDatabase
with a speedup ratio, printed as a formatted table.

Adds `npm run bench` script. Also fixes `test:unit:resources` to exclude
*.bench.js files so they don't run as part of the normal test suite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…integration

Renames CachingRocksDatabase to PrimaryRocksDatabase and moves all RocksDB-specific
entry handling (previously done by handleLocalTimeForGets instance patches) into real
class methods. Caching now operates at the getEntry level using full Entry objects with
`version`, using verifyVersion/populateVersion for VT-based freshness checks rather
than expectedVersion. handleLocalTimeForGets delegates to initStore() for
PrimaryRocksDatabase instances via the isPrimaryRocksDatabase marker. All 10 new tests
and 1720 existing tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rsion) call

The rocksdb-js store.getSync/get natively accept expectedVersion: on a VT hit they
return FRESH_VERSION_FLAG without touching disk; on a VT miss they read from the DB
and auto-populate the VT slot. Replacing the two-step verifyVersion+populateVersion
pattern with a single native call removes one round-trip per read on the warm cache
path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nchmark

- PrimaryRocksDatabase.getEntry now uses populateVersion option for cold
  reads (single native call, no separate populateVersion()) and merges
  options when passing expectedVersion so transaction snapshot is preserved
- handleLocalTimeForGets: remove all isRocksDB branches — RocksDB stores
  now use the isPrimaryRocksDatabase early-return path exclusively; the
  remaining code is LMDB-only, so isRocksDB is always false there
- Benchmark: use RecordEncoder + recordUpdater with Date.now()-range
  versions so values carry encoded version bytes and caching works correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread resources/DatabaseTransaction.ts Outdated
if (commitResult === RETRY_NOW_VALUE) {
this.retries++;
harperLogger.debug?.('coordinated retry', transaction.id, this.retries);
return this.commit({ transaction });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No retry cap on the coordinatedRetry path.

MAX_RETRIES = 40 is still defined at the top of the file but is now unreachable dead code — nothing checks it. If RocksDB keeps returning RETRY_NOW_VALUE (sustained write contention, edge-case in the native layer, etc.) this recurses without bound until stack exhaustion.

Suggested change
return this.commit({ transaction });
if (commitResult === RETRY_NOW_VALUE) {
this.retries++;
if (this.retries > MAX_RETRIES) {
throw new ServerError(
`After ${MAX_RETRIES} retries, unable to commit transaction, transaction is in conflict with ongoing writes`
);
}
harperLogger.debug?.('coordinated retry', transaction.id, this.retries);
return this.commit({ transaction });
}

Comment thread package.json Outdated
@claude

claude Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Review: Record caching (PR #410)

2 blockers found.


1. No retry cap on the coordinatedRetry path

File: resources/DatabaseTransaction.ts:266-270
What: The old ERR_BUSY handler enforced MAX_RETRIES = 40 before throwing a ServerError. The new RETRY_NOW_VALUE path increments this.retries and recurses unconditionally — MAX_RETRIES is now dead code (defined but never read).
Why it matters: If RocksDB keeps returning RETRY_NOW_VALUE under sustained write contention or any edge case in the native layer, commit recurses without bound until stack exhaustion or process hang. The protection existed before this PR and was silently dropped.
Suggested fix: Add if (this.retries > MAX_RETRIES) throw new ServerError(...) inside the RETRY_NOW_VALUE branch (inline suggestion posted).


2. weak-lru-cache not documented in dependencies.md

File: package.json:199
What: weak-lru-cache: ^1.2.2 is added as a runtime dependency. dependencies.md has no entry for it.
Why it matters: The repo's own policy (documented at the top of dependencies.md) requires every new runtime dependency to answer the standard checklist: size + transitive deps, security track record, memory cost, overlap with existing packages, removal plan, etc. That checklist exists to keep the dependency surface intentional and reviewable.
Suggested fix: Add a ## weak-lru-cache section to dependencies.md before merging.


What I traced (for spot-check)

  • PrimaryRocksDatabasegetEntry cache path correct: FRESH_VERSION_FLAG returns cached entry, cold reads seed the VT slot via populateVersion, putSync/removeSync both invalidate the cache. databases.ts assigns db.put = db.putSync so the async surface also invalidates. ✓
  • handleLocalTimeForGets early-return — all callers of handleLocalTimeForGets on RocksDB stores go through openRocksDatabase which now returns a PrimaryRocksDatabase; the isPrimaryRocksDatabase guard correctly short-circuits to initStore. LMDB stores fall through to the unchanged path. ✓
  • encoder.isRocksDB for LMDB — previously set to false explicitly; now never set for LMDB stores. undefined is falsy so all if (this.isRocksDB) branches in RecordEncoder behave identically. ✓
  • coordinatedRetry testcaching-rocks-database.test.js has a concurrent-write test that exercises the retry path; it skips under LMDB. Primary (RocksDB) path is covered. ✓
  • getRange not caching — intentional; range reads bypass the per-key cache. No correctness issue. ✓

attributes: [
{ name: 'id', isPrimaryKey: true },
{ name: 'embedding', indexed: { type: 'HNSW' }, type: 'Array' },
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — missing worker script

vectorIndex-thread.js does not exist anywhere in this repository (confirmed with git log and directory listing). Every Worker instantiation here will throw ENOENT immediately, causing the entire before() hook to fail and the test suite to error out without running.

Please add the missing worker script (it should live at unitTests/resources/vectorIndex-thread.js) that receives the { type: 'insert', start, count, dims } message and posts back a result/error reply.

Comment thread package.json
import { setTimeout as delay } from 'node:timers/promises';
import { Transaction as RocksTransaction, type Store as RocksStore } from '@harperfast/rocksdb-js';
import { Transaction as RocksTransaction, type Store as RocksStore, constants } from '@harperfast/rocksdb-js';
const RETRY_NOW_VALUE = constants.RETRY_NOW_VALUE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — MAX_RETRIES (line 23) is now dead code

The constant MAX_RETRIES = 40 is still defined but no longer used — the ERR_BUSY backoff guard that consumed it was removed when switching to coordinatedRetry. Consider removing it, or reinstate it as a cap inside the new RETRY_NOW_VALUE branch to preserve the JS-side safety net if the native layer ever loops unexpectedly.

@claude

claude Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Review: Record caching (PR #410)

What I traced

Surface Verified
PrimaryRocksDatabase — cache hit/miss paths, getEntry/getSync/get/getRange/putSync/removeSync
DatabaseTransactioncoordinatedRetry wiring, RETRY_NOW_VALUE branch, old ERR_BUSY removal
RecordEncoder.handleLocalTimeForGets — early-return for isPrimaryRocksDatabase, LMDB path equivalence
databases.tsopenRocksDatabase swap from RocksDatabase.open to PrimaryRocksDatabase
Cache-write safety: put/remove redirection to putSync/removeSync in databases.ts:129-130
LMDB encoder behavioural parity after removing store.encoder.isRocksDB = false assignment ✓ (all uses are truthy-checks; undefinedfalse)
dependencies.md for new runtime dep ✗ — see finding 1
Worker script referenced by new HNSW concurrent test ✗ — see finding 2

1. weak-lru-cache not documented in dependencies.mdblocker

File: package.json
What: weak-lru-cache is added as a runtime dependency with no entry in dependencies.md.
Why it matters: Per repo policy (dependencies.md intro), every new runtime dependency must answer the documented checklist (size, security history, transitive deps, overlap, memory cost, removal plan) so the whole engineering team can review it alongside the diff. The inline comment on package.json:200 has more detail.
Suggested fix: Add the entry to dependencies.md before merging.


2. Missing vectorIndex-thread.js worker script — blocker

File: unitTests/resources/vectorIndex.test.js:313
What: The new HNSW concurrent-PUT test (describe('HNSW concurrent PUT race condition…')) creates Worker instances pointing at __dirname + '/vectorIndex-thread.js', but that file does not exist in the repository.
Why it matters: The before() hook will throw ENOENT the moment workers are spawned, causing the entire test suite to fail before any assertion runs. The new test that is supposed to validate the coordinatedRetry fix for issue #386 effectively does not run.
Suggested fix: Add unitTests/resources/vectorIndex-thread.js implementing the { type: 'insert', start, count, dims } protocol the test expects.


Minor notes (not blockers)

  • MAX_RETRIES is dead code. const MAX_RETRIES = 40 in DatabaseTransaction.ts:23 is no longer referenced — the backoff guard that used it was removed. Remove it, or consider wiring it back as an upper bound inside the new RETRY_NOW_VALUE recursion so there is a JS-side safety net if the native layer ever loops unexpectedly (inline comment on DatabaseTransaction.ts has more detail).

  • this.retries is incremented but never checked in the new path. The old logic raised ServerError after 40 retries; now the count grows without consequence. Probably fine if coordinatedRetry is designed to converge, but worth a comment in the code explaining that invariant.

…che coherency

Brings the record-caching work up to date with main (~942 commits).
Resolved conflicts in package.json, DatabaseTransaction.ts, RecordEncoder.ts,
databases.ts, and vectorIndex.test.js. Kept main's structural improvements
to the commit path (write filtering, abort try/catch, blob cleanup) while
preserving record-caching's coordinated-retry (RETRY_NOW) path; dropped the
obsolete ERR_BUSY quadratic-backoff handler per the feature design.

Fixes exposed by the merge's test run:
- PrimaryRocksDatabase.clearSync()/clear() now drop the per-instance
  WeakLRUCache, so a cleared record can no longer be served as a stale
  cache hit (was throwing "Record already exists").
- Type reconciliation against the merged rocksdb-js API: commitResolution
  typed Promise<number|void>|void for the RETRY_NOW signal; the primary
  store cast to RocksRootDatabase.

Pairs with the matching @harperfast/rocksdb-js verification-table build
(PR #526). All 551 resources unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread package.json
@claude

claude Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

kriszyp and others added 4 commits June 1, 2026 05:44
The two record-caching-added files were never run through Prettier, failing
the Format Check CI job. No behavioral change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ions

Cross-model review (Codex) found that dropping the ERR_BUSY backoff handler in
favor of coordinated retry left a gap: writes that reach save() without a prior
getReadTxn() (immediate/publish/invalidate writes) create their RocksTransaction
without coordinatedRetry, so on a write conflict they reject with ERR_BUSY rather
than resolving RETRY_NOW — and with the backoff removed those rejections were no
longer retried, failing spuriously under concurrent writes.

Restore the ERR_BUSY backoff as a fallback. It coexists with the RETRY_NOW path:
coordinated transactions resolve RETRY_NOW (handled in the resolve branch) and
never reach the reject handler with ERR_BUSY; non-coordinated ones fall back to
the backoff retry. Re-adds the delay import and MAX_RETRIES.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…es are reclaimable

PrimaryRocksDatabase.getEntry cached the full Entry via WeakLRUCache.set(). A
raw Entry has neither a .deref (WeakRef) nor a .cache hook, so the LRFU expirer
never reclaims it once it cycles out of the LRU stages — the backing Map grows
unbounded (a leak). It now caches the record *value* via setValue (which
WeakRef-wraps it, making it collectable) and recovers the Entry metadata
(version, flags) via the existing value→Entry WeakMap while the value is live.

This also matches weak-lru-cache's contract: setValue expects the value object,
not Entry metadata — passing an Entry (which carries localTime) trips its
"value is a Date, not a value object" guard on every write.

Object-valued records get the full version fast path; primitive/empty values
fall through uncached (still correct, just no fast path).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Harper's reads happen inside RocksTransactions, so the caching benchmark now
reads through a per-sweep RocksTransaction in two new scenarios: transactional
cold read (gated VT populate) and transactional VT fast-path. This exercises the
Transaction::GetSync path and the snapshot-current fast-skip in
vtPopulateIfSettled. Versions are written safely in the past so the
single-accessible-version gate permits population while the read snapshot is open.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The dependent rocksdb-js Verification Table PR (#526) landed and published as
2.2.0, so update the dependency from ^1.4.0 and reconcile main's drift.

Conflict resolutions:
- package.json: rocksdb-js ^2.2.0, lmdb 3.5.5 (main), keep weak-lru-cache ^1.2.2
- DatabaseTransaction.commit: keep coordinated RETRY_NOW_VALUE retry path while
  adopting main's robust onCommit error handling, ERR_TRY_AGAIN retry (#308), and
  capped backoff (MAX_RETRY_DELAY_MS)
- RecordEncoder.handleLocalTimeForGets: keep the PrimaryRocksDatabase early-return
  (LMDB-only read-txn tracking) and fold in main's `if (this.isDone) return` guard

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…llision between benchmark steps

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kriszyp
kriszyp marked this pull request as ready for review July 4, 2026 21:22
kriszyp added a commit that referenced this pull request Jul 6, 2026
…durable-session test

`subscribe with QoS=1 and reconnect with non-clean session` sequenced connect/
disconnect/publish steps with hard-coded `delay(10)`/`delay(50)` calls instead
of waiting for the actual server-side state to settle. `client.endAsync()`
only resolves once the local socket closes — it does not wait for the broker
to finish tearing down the durable session — so reconnecting with the same
clientId immediately after could race the broker (issue #1138 flagged this
file as the #2-worst offender for this pattern). Under CI load the delays
were occasionally too short, causing the intermittent "got 0 messages"/mocha
timeout flakes seen on #410 and #1476.

Replace the disconnect-then-reconnect delays with a wait on the broker's own
`disconnected` event (already exposed via `server.mqtt.events`) for the
matching clientId, and replace the pre-disconnect delay with a wait for the
client's outgoing PUBACK. Also await the first offline publish (it was
fire-and-forget while the other two were awaited), so all three publishes are
durably committed before the reconnecting client asserts on redelivery.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedprettier@​3.9.5 ⏵ 3.9.499 +310097 +398 +1100

View full report

Comment thread integrationTests/database/record-caching.test.ts Outdated
Merge of main bumped @aws-sdk/lib-storage, globals, and prettier in
package.json but left the auto-merged lockfile on the old resolved
versions, breaking npm ci. Regenerate to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread integrationTests/database/record-caching.test.ts Outdated
Kris Zyp and others added 8 commits July 7, 2026 20:44
…ports)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eserve options on retry

Fix 1: The RETRY_NOW_VALUE resolve branch had no cap, unlike the ERR_BUSY reject path
which throws at MAX_RETRIES for non-sourceApply transactions and warns periodically for
sourceApply. Mirror that policy for the coordinated-retry path.

Fix 2: `isRetry` was derived from the lifetime `this.retries > 0` in save(), so a
reused DatabaseTransaction that had a conflict in batch A would stamp isRetry=true on
a FRESH RocksTransaction for batch B, causing RocksTransactionLogStore.put to skip
writing the txn-log entries for batch B — silently dropping its replication. Fix: set
`transaction.isRetry = true` at the RETRY_NOW and ERR_BUSY retry sites on the SPECIFIC
native transaction being retried, and remove the save() derivation. Also reset
`this.retries = 0` in the post-commit reset block so a reused DatabaseTransaction
starts fresh for its next batch.

Fix 3: Retry re-entries called `this.commit({ transaction })`, dropping caller options
(notably `options.flush`). Change all retry sites to `{ ...options, transaction }`.
Thread `options` into save() as a 4th parameter so the immediateCommit path can also
forward them to the recursive commit call.

Fix 4: Update the stale comment at the `transaction.commit() as Promise<void>` cast
which claimed coordinatedRetry was not passed. getReadTxn now passes it, so the
RETRY_NOW sentinel IS handled by the resolve callback above.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t-in for primary stores

Fix 5: resolveIndexFormat's emptiness probe used `getKeys({ start: 0, end: Infinity,
limit: 1 })`, which only scanned numeric keys. HNSW stores also hold Symbol/array keys
(e.g. entryPoint, [KEY_PREFIX, pk]) that sort BELOW 0 and string-pk safeKeys that sort
ABOVE Infinity. Corruption path: a delete-all left only safeKey orphan mappings after a
cleanup, the numeric-only scan reported empty, the store was re-opened as 'versioned'
over legacy values, and re-inserts were misdecoded. Fix: use `getKeys({ limit: 1 })` —
no start/end so any surviving key marks non-empty.

Fix 6: openRocksDatabase previously constructed PrimaryRocksDatabase with cache and
verificationTable defaulting ON for every non-dupSort store — the __dbis__ catalog,
root containers, internal catalog stores, and non-HNSW indexes all got a WeakLRUCache
and VT for zero benefit (no versioned entries → no cache hits, only per-write
cache.delete() overhead). Caching is now opt-in: PrimaryRocksDatabase requires explicit
`cache: true` and it is passed only at (a) table primary store open sites in initStores
and table(), where it is the PR's intended target; and (b) custom-object index stores
(isCustomObjectIndex) in openIndex, where the VT is required for versioned HNSW
traversal regardless of format (the format is resolved after opening).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rt-circuit getRange

Fix 7a: getEntry's when-callback already sets entryMap for any entry it returns —
#withEntry sets it for entries with METADATA, and the version+cache block sets it for
versioned entries. The identical sets in getSync() and get() after calling getEntry()
are redundant (running up to 3× per read). Remove them.

Fix 7b: getRange()'s map() callback was a pure passthrough (return entry) when
!hasRecordEncoder. Return the iterable directly before building the wrapper, avoiding
the per-entry function call overhead on the cold path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…explicit cache:true

Fix 8: Add weak-lru-cache entry to dependencies.md per AGENTS.md requirement that every
dependency have a rationale. Rationale: same WeakLRUCache library lmdb-js uses for its
CachingStore; weak-reference LRU so cached records are GC-reclaimable; powers the
PrimaryRocksDatabase record cache.

Update bench: PrimaryRocksDatabase now requires explicit `cache: true` (fix 6 flipped
the default). Update the caching bench case to pass it explicitly so the benchmark
continues measuring the intended cache-on vs cache-off comparison.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…in getEntry

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread resources/DatabaseTransaction.ts
kriszyp and others added 4 commits July 9, 2026 17:24
… vs ERR_TRY_AGAIN

Resolve the sole conflict in resources/DatabaseTransaction.ts: keep both
complementary fixes in the non-coordinated ERR_BUSY/ERR_TRY_AGAIN commit-retry
handler. Main's ERR_TRY_AGAIN fresh-transaction replay (#1696) runs first, then
record-caching's explicit retry-site isRetry stamp runs after the possible swap
so the fresh replay transaction is also marked (else it re-stages audit/change-feed
log entries on recommit). The old save()-site isRetry assignment that record-caching
removed (leak fix) is confirmed NOT reintroduced.

Also align the ERR_BUSY subtest in sourceApplyConflictRetry.test.js with record-caching's
coordinatedRetry convergence: a coordinated source-apply transaction resolves commit() with
the RETRY_NOW_VALUE sentinel on conflict rather than rejecting with ERR_BUSY, so the commit
spy now records the resolved value and the subtest accepts that sentinel (or a raw ERR_BUSY/
ERR_TRY_AGAIN rejection on the uncoordinated path) as the detected-and-retried conflict, while
still requiring a later genuine commit to succeed.
…rop sweep, non-txn write settle, snapshot TOCTOU, backdated-version gate)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The review fix that moved isRetry stamping to the conflict-retry sites removed
the retries>0 derivation in save() that replayLogs relied on (via retries=1) to
suppress txn-log re-appends. Replay then re-appended every replayed write into
the log it was iterating and never converged — silent boot hang after a crash
(CI shard 5, all runtimes). Key the suppression off the explicit isReplay
marker instead of the retries side-channel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kriszyp
kriszyp merged commit 4d9fe57 into main Jul 11, 2026
93 of 94 checks passed
@kriszyp
kriszyp deleted the record-caching branch July 11, 2026 12:03
pbrumblay pushed a commit to pbrumblay/harper that referenced this pull request Jul 13, 2026
…d cross-worker coherence anchors

The HarperFast#410 record-caching integration test's "4-worker" suite actually ran
single-worker: HARPER_WORKER_COUNT is not a Harper env var (it's a test-file
convention translated into config.threads.count elsewhere), and the harness
forces --THREADS_COUNT=1, so cross-worker cache invalidation -- the load-bearing
correctness promise of the per-worker WeakLRUCache + shared VerificationTable --
was never exercised.

Fix the existing suite to configure threads via config.threads.count with a
runtime worker-count guard (assertMultiWorker) so it can't silently regress to a
no-op, and add two promoted exploratory-QA anchors:
  - cross-worker coherence under update/delete/hot-key churn
  - cached point-read vs uncached scan/query coherence

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants