Skip to content

fix: propagate residency read failures and refresh stub indexes - #3322

Merged
kriszyp merged 6 commits into
mainfrom
fix/residency-source-errors
Oct 21, 2026
Merged

kriszyp merged 6 commits into
mainfrom
fix/residency-source-errors

Conversation

@kriszyp

@kriszyp kriszyp commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

⊙ 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.

Read or write Result
Full read, loader rejects Original error propagates
Full read, loader returns no entry Absence
Stored indexed-field projection Local stub fields
Received invalidation Current indexes and primary key
Tied copy to a full resident Keep the full record

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.

  • Forced builds of base and branch; focused core regressions pass 30/30 on RocksDB; 29/29 on LMDB (HNSW follows its existing RocksDB-only test policy).
  • The same final tests against rebuilt core 1fd591ba2 fail 24 cases on RocksDB and 23 on LMDB. Each engine has six passing controls; LMDB also skips HNSW.
  • Core type tests, typecheck, lint, formatting and DESIGN checks pass. Full-suite history and environment failures are below.
  • Final computed-selection resource rerun: RocksDB 3,775 pass, 47 pending, one unchanged transaction-monitor failure at 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 with npx 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 1eb97cc28 on 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 reviewed d9cf83ba2 on 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:

  • Core type tests/typecheck, lint, and formatting pass. Full affected suites at cd4db3140: RocksDB resources: 3,774 pass; indexes: 661; API: 202. LMDB resources: 3,351; API: 201. The earlier LMDB bin run at 880e26e09 passed 371 tests. Final freshness-fix API reruns pass 202/201 on RocksDB/LMDB.
  • Broad main-unit runs remain red on path and synthetic bundler-fixture assumptions; see details. Full integration at core 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.

Test Base Reviewed 1eb97cc28 Branch
propagates the original source error from a single-record read fail / fail pass / pass pass / pass
propagates the source error from a scan selecting non-indexed fields fail / fail pass / pass pass / pass
loads the complete record for a scan fail / fail pass / pass pass / pass
propagates the source error from an indexed query selecting non-indexed fields fail / fail pass / pass pass / pass
loads the complete record for an indexed query fail / fail pass / pass pass / pass
keeps successful absence distinct from a source failure fail / fail pass / pass pass / pass
propagates a failure from an instance ensureLoaded without changing its stub fail / fail pass / pass pass / pass
clears an instance when ensureLoaded receives successful absence fail / fail pass / pass pass / pass
updates an instance when ensureLoaded receives a complete record fail / fail pass / pass pass / pass
advances request freshness when ensureLoaded adopts a newer resident version fail / fail not run (fails on later cd4db3140) pass / pass
uses indexed-only projections without contacting a resident fail / fail pass / pass pass / pass
replaces indexed values when an existing stub is invalidated again fail / fail pass / pass pass / pass
removes an omitted indexed value from a received residency stub fail / fail pass / pass pass / pass
keeps committed indexed values when a fresh resource invalidates a local cache entry fail / fail pass / pass pass / pass
reads indexed accessors from an inherited incoming projection fail / fail pass / pass pass / pass
retains inherited non-indexed source fields when copying a frozen invalidation fail / fail fail / fail pass / pass
never promotes an incomplete cache patch after invalidation in the same transaction fail / pass fail / fail pass / pass
retains audit-referenced blobs from a superseded invalidation pass / pass fail / fail pass / pass
keeps a primary-key projection when a residency table has no secondary indexes fail / fail pass / pass pass / pass
preserves a computed index without evaluating its resolver on an incomplete stub fail / fail pass / pass pass / pass
loads resolver inputs when an indexed query selects a computed index pass / pass not run (fails on later d9cf83ba2) pass / pass
maintains indexes for put then invalidate in one transaction fail / fail pass / pass pass / pass
maintains indexes for invalidate then put in one transaction pass / fail pass / pass pass / pass
maintains indexes for invalidate then invalidate in one transaction fail / fail pass / pass pass / pass
maintains indexes for invalidate then delete in one transaction fail / fail pass / pass pass / pass
retains stored vectors on invalidation and maintains their graph on replacement and deletion fail / skip pass / skip pass / skip
preserves the correct full/stub state for an equal-version copy addressed to a resident fail / fail pass / pass pass / pass
preserves the correct full/stub state for a newer invalidation pass / pass pass / pass pass / pass
preserves the correct full/stub state for a demotion to a non-resident pass / pass pass / pass pass / pass
preserves the correct full/stub state for an ordinary cache invalidation pass / pass pass / pass pass / pass

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

  • 14395a03abada4b5 major correctness — resources/Table.ts — Index-free ordinary invalidation stores a null projection — NOT FILED
  • 5c0b5d46332a2ba1 major correctness — resources/Table.ts — Non-indexed predicates evaluate residency projections before loading — NOT FILED
  • 18412b1d07002be9 major correctness — resources/Table.ts — Full writes and removals resolve computed indexes from incomplete stubs — NOT FILED
  • 975a162189bb51a9 major correctness — resources/Table.ts — Replication-disabled point reads return incomplete residency stubs — NOT FILED

Review-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

kriszyp and others added 6 commits October 19, 2026 22:56
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>

@gemini-code-assist gemini-code-assist Bot left a comment

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.

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.

Comment on lines +9 to +10
const { INVALIDATED } = require('#src/resources/Table');
const { server } = require('#src/server/Server');

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.

medium

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.

Suggested change
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');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

@kriszyp
kriszyp marked this pull request as ready for review October 21, 2026 23:53
@kriszyp
kriszyp merged commit d97dc51 into main Oct 21, 2026
60 checks passed
@kriszyp
kriszyp deleted the fix/residency-source-errors branch October 21, 2026 23:53
cb1kenobi pushed a commit that referenced this pull request Oct 23, 2026
#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
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.

1 participant