Repository navigation
Count only a record's residents toward replicatedConfirmation, and refuse a count they cannot meet before writing - #3148
Merged
Conversation
… and fail a count no residents can meet before committing A peer acknowledges a database-wide applied position, so a non-resident peer that received only an invalidation stub (or nothing) for a record was counted as confirming it. The commit now hands the confirmation callback one list of other residents per distinct residency among its confirmable writes; harper-pro's waiter counts a peer only for the lists it belongs to. A count larger than a record's other residents fails before the native commit. The commit handler records the residency and version of the audit entry it appended, kept across coordinated retries, and a write that stored no local record is now confirmed on that version. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…al from skipping commit bookkeeping Pre-push review: the refusal ran in save() after the write had staged, so a caller that caught it could still commit the write; it now runs in the write's commit handler before updateRecord, with the committing transaction passed to the handler. A refusal or callback error after the native commit becomes a rejected completion instead of a synchronous throw, so blob cleanup and lock/context release still run. Residents per residency id are cached (the id-to-list mapping never changes), and only writes placed by residency carry the stamped metadata, set after the audit entry is staged. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
The refusal ran in writeCommit, after updateIndices had already staged the write's index entries, so a caller that caught it could commit a dangling index entry. It now runs right after the write's residency is resolved, before anything is staged, and marks the write skipped so its saved blobs are cleaned up. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…red entry's An update whose residency omits the writer appends its audit entry without replacing the local entry, so the entry still held an older version and a resident's acknowledgement of that older version confirmed the new write. The confirmation target now prefers the version the write recorded. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…s inert, and confirm on the log position Pre-push review round 2: - confirmation requested after a write was saved is checked before native submission and aborts the transaction, so the refused write never lands; - a refused write is made inert (a retry re-save stages nothing; confirmation ignores it), so a caught refusal can neither commit it nor lend its older entry's version to the target; - the recorded position is the later of the record version and the log key, since peers acknowledge log positions, and is recorded only for audit-producing, non-copy writes; - a residency id whose list is not yet published is not cached as empty; - a post-commit refusal's rejected completion is handled at creation. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
… first one found Pre-push review round 3: the target kept the first defined value scanning back from the last write, so a trailing unplaced write's entry version (the record version) masked an earlier placed write's log key, which is higher when an explicit timestamp falls below the origin floor. The target now covers every recorded position. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…onfirmation refusal Pre-push review round 4: getResidencyId published the set-to-id mapping before the id-to-list one, so a worker discovering a just-minted id could find no list and refuse a valid confirmation; the list is now written first. Harper's BigInt.toJSON throws, so the refusal formats keys itself. The DESIGN note now bounds the refusal's guarantee to Harper's stored data: a table source's before hook runs first. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
Pre-push review round 4 (pre-existing gap): an unplaced write with an explicit timestamp below the origin floor was confirmed on its record version, which peers' acknowledged log positions can already exceed. Writes of a transaction that requests confirmation now record their position too; other writes pay nothing. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…n the committed log key Pre-push review round 5: - getResidency built a count-based update's placement as [self, ...previousResidency], and the stored list already starts with the writer, so each update repeated it and shed a replica ([S,A,B] became [S,S,A], then [S,S,S]); the new refusal turned that into a 400. The carried-over list now excludes this node. - The confirmation target is floored at the committed transaction's reserved log key, which covers delete-only, publish-only and late-confirmed writes whose record version sits below it. - getResidencyId writes both mappings with putSync, list first. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
… the replication layer refuse before commit Framing recheck (better-alternative-exists), adopted: - The confirmation target is the committed transaction's reserved log key, the position peers acknowledge. A record version can sit below it (explicit timestamp under the origin floor) or above it (a remote-origin version kept by a merge, which residents would never acknowledge), so per-write position stamps are gone. - getResidencyId never claims an id already in use, since readers cache an id's list for good. - replicationConfirmation takes an optional feasibility check, run before native submission with the resident lists, so the replication layer's deterministic refusals (recipient bound) no longer arrive after the write landed. - Count-based placement carries the previous residency over with a loop instead of filter+slice. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
Run inside the full resources suite, the 'test' database resolves under another name. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…ep a no-op commit off its unused log key Pre-push review round 6: - A commit made while read iterators are open replays its writes onto a fresh handle and submits that; the pre-submission feasibility check now runs on both submission paths (refuseUnconfirmable). - A commit whose writes all lost to newer entries appended nothing at its reserved key, which peers pass only when some later write replicates; such a commit falls back to the stored-entry target. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…nd the residents cache Framing recheck (better-alternative-exists), adopted: - getResidencyId claims a new id in one dbis transaction (retryOnBusy on RocksDB; LMDB's sync transaction is exclusive), so two workers minting the same set or drawing the same candidate id conflict and retry instead of overwriting a list readers have cached. - A delete that loses to a newer entry appends nothing; it is now marked skipped, so a commit made only of such writes confirms on the existing entry rather than on a log key nothing was written at. - The per-database residents cache is cleared past 10,000 residencies. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…feasibility check when nothing replicates Framing recheck (better-alternative-exists), adopted: a delete on a table with neither audit nor delete tracking removes the entry without an audit append, so it is marked skipReplicationConfirmation rather than sending the commit to wait on a log key nothing was written at; a commit made only of exempt writes (that, or a reload marker) no longer runs the replication layer's feasibility check. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…synchronous feasibility check, and reuse the checked lists Framing recheck (better-alternative-exists), adopted: - A write that appended its audit entry in an earlier round and lost on a retry round is skipped, yet its retained log entry still replicates at the reserved key; the target now keys off the sticky appendedAuditEntry flag rather than the final round's skipped flag. - A feasibility check that returns a promise is refused (TypeError) before submission instead of being silently left unawaited. - The resident lists checked before submission are kept for the waiter instead of rebuilt after commit. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…t, and keep a checked empty result Framing recheck (better-alternative-exists, local corrections), adopted: a feasibility check that returns a promise has it observed before the TypeError, so its rejection cannot go unhandled; a checked commit with no resident lists no longer rebuilds them after commit. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…the residency id claim, and freeze cached lists Pre-push review round 7: - The sticky appendedAuditEntry also covers an append on a handle an iterator replay discards, so the target now asks whether a confirmable write appended to the handle that was committed (appendedTo). - getResidencyId throws when the claiming transaction yields no id instead of placing the write as unplaced. - Cached resident lists are frozen, so a consumer cannot corrupt them for later commits. - The DESIGN note states when the target falls back to an entry version, and the bounded cache. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…ation targets the log key Pre-push review: appendedTo was recorded only when confirmation was already requested at save, so confirmation requested afterwards fell back to an entry version, which for an omitted-writer update is the older entry it left behind. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
Contributor
There was a problem hiding this comment.
Code Review
This pull request implements changes to ensure that replicatedConfirmation counts only the actual residents of each written record. It introduces caching of residents by residency ID, adds a feasibility check before native transaction submission to refuse unconfirmable writes early, and updates the transaction commit process to track the exact log keys and resident peer lists. A high-severity issue was identified in DatabaseTransaction.ts where accessing this.writes[0] directly could throw a TypeError if the first write was detached; it is recommended to safely find the first non-null write instead.
…he transaction A detached first write leaves this.writes[0] null, which skipped the check. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
…ignore residency from a discarded replay round Pre-push review: - A coordinated or backoff retry re-enters the submission path after the first attempt's batch was written; re-running the feasibility check there could abort a submitted transaction. It now runs only before the first native submission of a commit. - Resident lists count only writes that appended to the handle being submitted, so a residency recorded in a round an iterator replay discarded is not required. - A delete resets its confirmation exemption each round, since a replay can now append. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
… handle Pre-push review: skipping the check whenever the transaction had already submitted also skipped it for a write that commits on its own fresh handle while the outer commit is in flight, and that write's waiter reused the outer commit's lists. The skip now applies only to a retry of the same handle (isRetry), and the checked lists are used only for the handle they were checked for. A delete's exemption reset skips retry rounds, which re-append nothing. Refs HarperFast/harper-pro#1031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AtkQzQLoohXK3gEMzGFRz8 Dispatch-Task: hp-1031-residency-aware-confirmation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⊙ Problem
replicatedConfirmation: Nresolves once N peers report an applied position for the database that is at or past the write. On a table with a residency list, that position is also passed by peers that do not hold the record: a non-resident peer receives only an invalidation stub, and a peer outside the list may receive nothing at all for that record. Either one still counts. A write can therefore report "confirmed by N" while no other node holds a complete copy (Count only peers that hold a complete copy toward replicatedConfirmation — harper-pro#1031). Core is the layer that knows each record's residency, but it never passed that to the replication layer.💡 Solution
The commit now gives the confirmation callback a 4th argument,
residentPeerLists: one list of other residents per distinct residency among the transaction's confirmable writes, with the writer and duplicate names removed. Harper-pro's waiter counts a peer only toward the lists it belongs to, and every list needs N confirmed members. A write with no residency adds no list, because N confirmations from any list already come from N distinct peers. When no write has a residency, the argument isundefinedand the waiter counts as before.setResidencyById, a write that excludes the writer stores no entry at all, so it was never confirmed.residencyIdof the audit entry each write appended. A coordinated retry appends nothing and keeps it, so the residents counted are the ones the replicated entry names. Residents are resolved once per residency id.getResidencyIdnow never claims an id already in use, and it writes the id-to-list mapping before the set-to-id one, so an id that is discoverable always resolves to its one list.skipReplicationConfirmation). A commit made only of exempt writes neither waits nor runs the feasibility check. A commit whose writes all lost to newer entries in every round falls back to the stored-entry target.Cannot confirm replication to 2 node(s) for record "x" in database "data": it is resident on 1 other node(s) (node-b). The refused write turns inert, so a caller that catches the error cannot commit it, even on a retry. Just before native submission, the commit rechecks the lists (confirmation may have been requested after a write was saved). It then runs a feasibility check the replication layer can now register withreplicationConfirmation(callback, feasibility). Harper-pro uses it for its recipient bound, which used to answer 400 for a write that had already landed. An error after the native commit becomes a rejected completion, so the commit's bookkeeping (blob cleanup, lock and context release) still runs.replicateTo: Nbuilt an update's placement as[self, ...previousResidency], and the stored list already starts with the writer. Each update therefore repeated the writer and shed a replica:[S,A,B]became[S,S,A], then[S,S,S]. This was a pre-existing data-placement bug, which the new refusal surfaced as a 400. The carried-over list now leaves this node out.⚖️ Alternatives
better-alternative-existsfive times; each was adopted, and none cleared. The review CLI required a recheck after round 5. Each verdict named a smaller or safer variant inside the same design, and I adopted every one: target the committed log key, not record versions; claim residency ids atomically (the resolved lists are cached); refuse through a pre-submission feasibility hook on both submission paths; exempt entry-less deletes and marker-only commits; key the target off the stickyappendedAuditEntry; refuse and observe an asynchronous feasibility check; reuse the checked lists. The last verdict said its points "do not require replacing the design", so I adopted them without a sixth round. The original plan review cleared withFraming-Verdict: chosen-approach-sound.residencyIdby then. The entry also does not exist when the writer stored no local record.logEntries. Rejected:RocksTransactionLogStorefills that array only when anaftercommitlistener exists. It also skips appends on a retry, so it can be absent or describe a different attempt.🔧 Changes
resources/DatabaseTransaction.tsconfirmationTargetpicks the committed log key, else the stored-entry fallback, deciding by whether a confirmable write appended to the committed handle (appendedTo).residentPeerListsandconfirmingResidentsbuild the per-residency lists. Writer and duplicates are removed, and the lists come from a bounded per-database cache of frozen lists.refuseUnconfirmableruns before both native submissions: the iterator-replay path and the ordinary path. It runs the feasibility check registered withreplicationConfirmation(callback, feasibility), and aborts on refusal.resources/Table.ts_writeUpdatecommit handler refuses an unmeetable count before staging and makes the refused write inert. It records the audit entry'sresidencyId, kept across coordinated retries.getResidencyIdclaims a new id and both mappings in one transaction.skipped, and an entry-less delete is exempt from confirmation.resources/DESIGN.mdgets the invariant note, indexed from the rootDESIGN.md.✅ Verification
unitTests/resources/residencyConfirmation.test.js(19 tests, RocksDB) covers:replicateTo, and mixed/duplicate residencies;The new tests discriminate: they fail on the base sources (7 of 8 originally) and on each superseded intermediate target.
npm run test:unit:resources: 4151 passing.npm run test:unit:main(isolated HOME): 6877 passing. UnderHARPER_STORAGE_ENGINE=lmdb,writerResidencyandreloadMarkerpass, covering the atomic id claim on LMDB.End to end: harper-pro's new three-node
integrationTests/cluster/residencyConfirmation.test.mjsagainst this core passes 3/3, and fails on base. The existing confirmation suitesfullyConnectedReplication,replicationTopology,nonReplicatedDatabase,selectiveTableSubscription,removeNodeBlastRadiusandreplicationWorkerPoolpass.After the PR opened, two more fixes landed. The pre-submission check now runs only before a handle's first submission: a retry of that same handle (
isRetry) is not re-checked, and a fresh handle committing while the outer commit is in flight is. Resident lists count only writes that appended to the submitted handle. The suites below were re-run on the final head.A pre-existing flake,
unitTests/security/userRecordLookups.test.js("onUserChange re-subscribes…"), fails about 1 in 3–6 runs on base too.Refs HarperFast/harper-pro#1031. Companion: harper-pro PR (bumps
coreto this branch). After merge, documentation#723 needs to describe this contract.🤖 Generated by Claude (Anthropic, Opus 5.5); posted via @kriszyp.
Related PRs: #2918 overlaps (DatabaseTransaction save() arguments and the pre-submission abort block), #3092 overlaps (Table source-hook ordering before the commit handler), 3 others independent
Complexity: complicated
Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex,cursor-muse; adjudicated=domain; declined=cursor-grok,cursor-kimi; rounds=11; full=5 @ a8c51af
Review-Attention: deep ~40m (critical: DatabaseTransaction.ts, Table.ts; decisions: do-less-alternative, caught-refusal-semantics, refuse-before-landing, sync-only-feasibility, lmdb-unchanged) @ a8c51af