Repository navigation
fix: propagate residency read failures and refresh stub indexes - #3322
Conversation
Dispatch-Task: hp-residency-read-correctness Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: hp-residency-read-correctness Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: hp-residency-read-correctness Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: hp-residency-read-correctness Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: hp-residency-read-correctness Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: hp-residency-read-correctness Co-Authored-By: GPT-5 Codex <noreply@openai.com>
There was a problem hiding this comment.
Code Review
This pull request enhances the residency and invalidation mechanisms in the database table resource. Key changes include integrating _writeInvalidate into the same-key write chain, ensuring patches onto incomplete bases remain invalidated, and correctly propagating residency loader errors during reads. A comprehensive test suite has been added to validate these residency stub reads. The review feedback points out a missing import for createBlob in the newly added test file, which would cause a ReferenceError when executed.
| const { INVALIDATED } = require('#src/resources/Table'); | ||
| const { server } = require('#src/server/Server'); |
There was a problem hiding this comment.
The newly added test cases call createBlob(data) on lines 213 and 214, but createBlob is not explicitly imported in this test file. To prevent potential ReferenceErrors when running this test suite in isolation, explicitly import createBlob from #src/resources/blob.
| const { INVALIDATED } = require('#src/resources/Table'); | |
| const { server } = require('#src/server/Server'); | |
| const { INVALIDATED } = require('#src/resources/Table'); | |
| const { createBlob } = require('#src/resources/blob'); | |
| const { server } = require('#src/server/Server'); |
There was a problem hiding this comment.
Codex: this suite uses Harper's global createBlob API. resources/blob.ts:1787 registers it through _assignPackageExport, and globals.js:14 assigns the global. The normal test bootstrap loads that module. The file passes in isolation on both engines (30 RocksDB cases; 29 LMDB cases plus the existing HNSW skip), including the blob case, so there is no ReferenceError to repair.
#3322 and #3084 each passed the design-doc check alone and together took the file to 1001 lines, failing Lint Source Code on main. Tighten the nested-selection convention and the RocksDB teardown note; no facts dropped beyond restatements. 991 lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: main-red-kriszyp_harper_0a030f35a_5684deb1
⊙ Problem
A read of a residency stub can turn a failed remote fetch into absence or return a partial query row. An invalidate write also leaves secondary indexes stale, and a copied stub can replace an equally current resident record.
🤔 Preserve the existing residency model
This change corrects those reads and invalidations. Placement and relocation-on-read stay unchanged; the separate ownership redesign is outside this change.
💡 Solution
Propagate loader failures from single-record and full-query reads. Keep projections of stored indexed fields local; computed fields load their resolver inputs. Invalidated caching tables also refetch inputs for selected computed fields. Full queries, SQL scans and exports fetch each stub row serially and fail on a resident outage instead of returning partial data. A non-resident GET during an outage changes from 404 to the existing 502; collection reads can fail after their response headers or earlier rows have been sent. Apply each received stub's stored indexed fields and primary key, including removed fields, and preserve a full resident record against an equal-origin/equal-version invalidation.
⚖️ Alternatives
Adopted the planning review's protection for tied resident copies. Rejected its proposed no-relocate query API because this task preserves existing read placement behavior. No new protocol or storage format is needed.
🤔 Propagate the existing loader error
As specified, a residency fetch failure stops the read even when a caching source is also configured. It no longer becomes a cache miss or triggers that fallback. The existing loader error is preserved; no new wrapping or fallback policy is added.
🔧 Changes
Product and architecture tour: reads, indexes and limits
Product and architecture tour
Reads and invalidation state
resources/Table.ts owns both the loader boundary and the invalidation transaction. The main review point is the equal-version guard: a newer invalidation, a genuine non-resident demotion, and an ordinary cache invalidation still apply.
Computed indexes retain their committed entries because a stub lacks resolver inputs; stored vector indexes still update. Invalidations join the same-key write chain and publish their staged projection so later writes remove the right index entries on both engines. Ordinary cache invalidations carry fields from that commit basis; subsequent patches retain their incomplete status. Source projections preserve inherited fields, and superseded invalidations retain audit-owned blobs;
ensureLoaded()adopts successful absence atomically and advances request freshness when a newer resident version is loaded.#2155 independently changes
updateIndices's fifth argument to an expiry value; reconcile that signature when either PR lands. #3301 adds a continuation-version check in the async materializer; preserve that check when combining. #3279 adds nested selections and cancellation handling in the same materializer; retain those guards too. #3316 also changes nested selection there to enforce related-table permissions, which must survive combination.resources/DESIGN.md documents these invariants; DESIGN.md updates their index entry.
🤔 Preserve computed-index storage
Retain computed index entries because stubs lack resolver inputs; loading inputs inside commit would add remote work. Later full writes/deletes still need a separate derived-field design. Equal received vectors can repeat JavaScript HNSW work; that unmeasured cost is accepted here.
Limits and tradeoffs
Invalidation retains computed index entries because the projection lacks their resolver inputs. A later full write or delete can still resolve against that incomplete projection and throw or miss an old index value. This existing limitation needs separate derived-field storage work; this change maintains stored-field and vector indexes without redesigning computed values. Received vector values can trigger re-indexing in the JavaScript HNSW path even when their elements are unchanged, as full writes already do; the default native plane already skips equal vectors. That additional work is unmeasured; no throughput improvement is claimed and no array-equality cache is introduced.
🤔 Existing query lifecycle and relocation
Keep existing point-read relocation semantics, as required; a new no-relocate API belongs to #436/#980. This brings existing relocation and query-lifecycle limits to full queries. These remain explicit follow-ups, not guarantees of this fix.
Limits and tradeoffs
Full-query materialization uses the existing point-read loader, as required. Each stub adds a serial remote fetch while the read snapshot remains open; that added latency is unmeasured. Without default placement, relocation can mark peers' stubs complete; on RocksDB its audit append can fail after returning the read. These pre-existing point-read defects now also apply to full queries and remain outside this task. Non-indexed filters/sorts still run before materialization. A query closed while awaiting a peer can still resume its transform against the ended read transaction; that existing cancellation gap remains separate from error propagation. Point-read peer results also still omit freshness bookkeeping in
loadLocalRecord; the new instance-load guard does not fix that pre-existing sibling.✅ Verification
End-to-end route: the companion Pro four-node tests exercise HTTP GET, collection reads, placement changes, unavailable residents, and GET_RECORD retries with this core commit.
1fd591ba2fail 24 cases on RocksDB and 23 on LMDB. Each engine has six passing controls; LMDB also skips HNSW.txn-tracking.test.js:464(also red in main’s Node24 unit job). The new residency cases pass; the monitor file passes its separate rerun (75 tests, 3 pending). Final LMDB resources: 3,353 pass, 739 pending; complete API, wire and four-node reruns pass on both engines.Regression evidence
A/B method: swap only the changed production sources to the named base revisions, rebuild core with
npm run build(fresh TypeScript program), and Pro withnpx tsc --build tsconfig.json --force; run the same tests, restore sources, rebuild, and repeat. Fixtures and assertions stay identical between runs.unitTests/resources/residencyReads.test.js reaches these base failures: swallowed single-read error; scan/indexed-query failures or partial rows; successful query absence reported as a stub; missing stub primary key; stale/missing secondary indexes; frozen projection mutation; and tied copy replacing a full row. Branch tests cover those outcomes plus the unchanged controls. Five edge cases also fail against the first implementation: ensureLoaded absence, unloaded cache invalidation, inherited indexed accessors, primary-only projections, and computed-index preservation. The final same-key cases fail on LMDB without the repair (put→invalidate, invalidate→put, invalidate→invalidate, invalidate→delete); all but invalidate→put also fail on RocksDB main. A real HNSW replacement/delete case fails on rebuilt main and the prior reviewed build. Raw index assertions detect orphaned entries even after deletion. Three follow-ups fail on reviewed core
1eb97cc28on both engines: inherited source fields, incomplete cache patches, and audit-owned blobs after supersession. The blob test verifies that an audit-owned blob remains reusable by a later write to the same record ID and that its decoded bytes remain readable.The final freshness case fails on both rebuilt main and reviewed
cd4db3140: ensureLoaded adopted the newer row but left context.lastModified at the old version. The additional computed-index selection regression passes on main, fails on reviewedd9cf83ba2on both engines with a missing resolver input, and passes after excluding computed attributes from the local projection shortcut. Final focused coverage is 30 cases (one existing HNSW skip on LMDB).Earlier full-suite results:
cd4db3140: RocksDB resources: 3,774 pass; indexes: 661; API: 202. LMDB resources: 3,351; API: 201. The earlier LMDB bin run at880e26e09passed 371 tests. Final freshness-fix API reruns pass 202/201 on RocksDB/LMDB.880e26e09: 2,533 pass, 10 Unix-socket path failures, and 6 Ollama cases cancelled by a source JSON-import setup error. Both path suites pass their shorter-root rerun (15/15).Broad-unit limits: absolute paths exceed Unix socket limits; a short path fixes those but exposes alias-versus-realpath expectations. The affected git-tag file passes its isolated rerun; the complete Pro unit suite passes under a normal path. Two bundler tests fail unchanged base code because synthetic fixtures discover ancestor-installed dependencies. These are not counted as passing gates.
Planning verdict:
Framing-Verdict: better-alternative-exists; the tied-copy protection was adopted and the no-relocate alternative was rejected for the explicit scope boundary above.Per-test rebuilt-base proof (RocksDB / LMDB)
Passes on base are controls or engine-specific non-reproductions; skipped HNSW coverage follows the existing suite.
1eb97cc28cd4db3140)d9cf83ba2)Refs HarperFast/harper-pro#436. No documentation companion: this corrects existing behavior and documents the internal invariants in DESIGN notes. The Pro companion #1087 covers the wire changes. The operations API's main-thread loader gap and existing RocksDB relocation transaction error remain separate findings.
🤖 Generated by OpenAI Codex; posted via @kriszyp.
Release target: v5.4 — matches the residency task's issue milestone.
Related PRs: #2155 overlaps; #3301 overlaps; #3279 overlaps; #3316 overlaps; 20 others independent
Complexity: complicated
Deferred findings
14395a03abada4b5major correctness —resources/Table.ts— Index-free ordinary invalidation stores a null projection — NOT FILED5c0b5d46332a2ba1major correctness —resources/Table.ts— Non-indexed predicates evaluate residency projections before loading — NOT FILED18412b1d07002be9major correctness —resources/Table.ts— Full writes and removals resolve computed indexes from incomplete stubs — NOT FILED975a162189bb51a9major correctness —resources/Table.ts— Replication-disabled point reads return incomplete residency stubs — NOT FILEDReview-Coverage: authored=codex; ran=cursor-composer,gemini,claude,cursor-muse; adjudicated=domain; declined=cursor-grok,cursor-kimi,muse; rounds=6; full=5 @ aad39af
Review-Attention: study ~20m (critical: Table.ts; decisions: projection-local-reads, computed-index-retention, transport-enforcement-layer) @ aad39af