Skip to content

Count only resident peers toward replicatedConfirmation - #1032

Merged
kriszyp merged 26 commits into
mainfrom
fix/residency-aware-replicated-confirmation
Oct 11, 2026
Merged

kriszyp merged 26 commits into
mainfrom
fix/residency-aware-replicated-confirmation

Conversation

@kriszyp

@kriszyp kriszyp commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

⊙ Problem

replicatedConfirmation: N counts any peer whose applied position for the database has reached the write. On a table with a residency list, that includes peers that do not hold the record. A non-resident peer is sent only an invalidation stub, and a peer excluded by replicateTo (implemented as residency) may be sent nothing for that record. Both still pass the database position. A write could resolve "confirmed by N" while no other node held a complete copy. Refs #1031.

❓ Your call: Is the requirement warranted? As specified, yes. The task owner ruled on 2026-10-11: count only peers that hold a complete copy; a record needs N confirmations from its own residents; fail fast when N exceeds them; no protocol change. documentation#723 documents the old any-N behavior and is on hold until this lands. It needs to be updated to this contract after merge.

💡 Solution

Core (Count only a record's residents toward replicatedConfirmation — harper#3148) now passes the confirmation callback one list of other resident peers for each distinct residency among the written records, and targets the committed transaction's log key. createConfirmationWaiter resolves only when every list has N confirmed members. A peer still counts once, by identity (harper-pro#633), and the alreadyConfirmedPeers seed goes through the same check. The writer decides who is eligible and reuses the existing COMMITTED_UPDATE acknowledgement, so receivers of any version need nothing new. When there are no lists (a table without residency), the waiter keeps the database-wide count and the getReplicationRecipients bound as before. The recipient guard moves into core's new pre-submission feasibility hook (assertConfirmationFeasible), so its 400 arrives before the write lands rather than after it. It also checks each list against this node's recipients (assertResidentsReachable): a record homed on a node that is no longer a cluster member, or on a peer this node does not send the database to, is refused at once instead of waiting out the 900 s timeout.

Before → after, with three nodes W, B, C, a record homed on [W, B], B down, and X-Replicate-To: *;confirm=1:

before after
C applies the invalidation stub write confirmed still waiting for B
B comes back and applies the record n/a write confirmed
confirm=2 on that record waits up to 900 s, then 504 immediate 400 (resident on 1 other node(s) (B)), nothing written
record homed on [W, B, retired-node], confirm=2 waits up to 900 s, then 504 immediate 400, nothing written

⚖️ Alternatives

  • Planning review: Framing-Verdict: chosen-approach-sound. The core framing recheck (adopted there) moved this PR's recipient guard into core's pre-submission feasibility hook.

Weighed in the core PR: per-record acknowledgements from receivers would need a protocol change and break rolling upgrades. Having pro re-read residency from the committed entry would race concurrent overwrites. Both were rejected.

❓ Your call: Each confirmed transaction with resident lists runs assertResidentsReachable, an allocation-free scan of residents × this node's recipients. Its size is bounded by the cluster, and no measurement was taken. The pre-push review flags it as an unmeasured hot path; I judged a benchmark unnecessary at cluster-sized inputs.

❓ Your call: For unrestricted tables the waiter now settles at confirmed.size >= N rather than === N. For integer N nothing changes. A fractional count such as confirm=1.5, which REST accepts, used to wait out the timeout and now settles at 2 confirmations.

🔧 Changes

✅ Verification

  • integrationTests/cluster/residencyConfirmation.test.mjs with the fixture (config.yaml, resources.js, schema.graphql): three nodes, writes through REST. The operations API runs on the main thread, which loads no application setResidency.

    1. confirm=2 on a record homed on [W, B], and on [W, B, retired-node]: immediate 400, and nothing is written.
    2. With B stopped, W writes three things, each with confirm=1: a record homed on [W, B], an Open record with X-Replicate-To: B, and a two-record PUT ([W,B] + [W,C]). The test waits until all are committed on W. A later probe homed on [W, C] confirms, which proves C applied them. All three stay unconfirmed for 3 s. After B restarts and rejoins the mesh, all three confirm, and B holds the full records.

    Passes 3/3 in repeated runs. On base it fails both tests: all three writes were confirmed with B down, and confirm=2 was accepted with 204.

  • unitTests/replication/confirmationWaiter.test.mjs: 33 passing. Covers non-resident ignored, seed filtered, per-record lists, one peer counted toward several lists, shortfall message, [] treated as database-wide, and assertResidentsReachable.

  • npm run test:unit: passing. Existing confirmation cluster suites unchanged and passing: fullyConnectedReplication, replicationTopology, nonReplicatedDatabase, selectiveTableSubscription, removeNodeBlastRadius, replicationWorkerPool.

🤖 Generated by Claude (Anthropic, Opus 5.5); posted via @kriszyp.
Depends-on: HarperFast/harper#3148

Related PRs: #1030 independent (set the base core pin this bumps from)
Complexity: complicated

Origin — the dispatch brief this PR was written from

replicatedConfirmation counts only peers that hold a copy of the record

Fix harper-pro#1031: replicatedConfirmation: N must count only peers that hold a complete copy of the written record(s). Today createConfirmationWaiter in replication/knownNodes.ts counts any peer whose per-database applied watermark (CONFIRMATION_STATUS_POSITION) reaches the write's txnTime, so a non-resident peer (received only an INVALIDATED stub) or a replicateTo-excluded peer (received nothing) is counted. This is a coordinated core + pro change: core supplies the per-write resident peer set to the replicationConfirmation(...) callback; pro's waiter ignores peers outside it. Use the coordinated-PR flow (harper-pro worktree with its own core submodule checkout; harper PR + harper-pro PR with the bumped core pointer). Open both as drafts.

Acceptance

  • Test (fails on current main): record-based residency list where the only non-writer resident is down; a write with replicatedConfirmation: 1 is not confirmed by a non-resident or replicateTo-excluded peer that has advanced its watermark.
  • Test: N exceeding other residents fails fast with a clear error.
  • Test: multi-record transaction with differing residency applies the per-record rule.
  • Existing confirmation unit/integration tests for unsharded tables pass unchanged.
  • Both PRs draft; harper-pro PR bumps core to the harper PR's commit; PR bodies reference harper-pro#1031 and note documentation#723 needs updating after merge.

Dispatch: task hp-1031-residency-aware-confirmation · queued by kris-session · ran by claude/opus/high · worker kzyp-xps-1

Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-kimi,cursor-muse; rounds=11; full=5 @ d1cf078

Review-Attention: study ~20m (critical: knownNodes.ts; decisions: per-residency-each-meets-count, pre-submission-feasibility-hook, direct-recipients-only, writer-side-eligibility, do-less-alternative) @ d1cf078

kriszyp and others added 22 commits October 11, 2026 07:41
createConfirmationWaiter now takes the per-residency lists of other resident peers that core passes
for the written records, and resolves only once every list has the requested number of confirmed
members. A non-resident peer (sent an invalidation stub, or nothing) passes the database watermark
without holding the record and no longer counts. Tables without residency lists keep the database-wide
count. Bumps core for the residency hand-off and pre-commit fail-fast.

Refs #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
…cy confirmation test

Bumps core for the pre-staging refusal and the post-commit refusal path.

Refs #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
Refs #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
Waits for the tested writes to be visible on the writer before issuing the probe write C holds, so C
confirming the probe proves C applied them. Bumps core for the confirmation-version fix.

Refs #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
A record homed on a node that is no longer a cluster member, or on a peer this node does not send the
database to, could never be confirmed and waited out the 900 s timeout. The existing recipient guard
now also requires each resident list to have the count among this node's recipients.

Refs #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
…-submission refusal

Refs #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
…ist Sets

Pre-push review: the waiter and the reachability guard allocated a Set per resident list (and a
recipient-name Set) for every confirmed transaction. Core already deduplicates each list, so membership
is counted over the arrays directly.

Refs #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
Refs #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
Refs #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
Refs #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
Refs #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
Plain loops instead of per-call closures in the confirmation predicate and the recipient lookup.

Refs #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
…ey target

Refs #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 feasibility hook

The recipient bound (count against this node's recipients, and against each written record's residents
among them) is now registered as core's pre-submission feasibility check instead of running in the
post-commit confirmation callback, so its 400 no longer arrives for a write that already landed. Bumps
core for the committed-log-key target, unique residency ids and the hook.

Refs #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
Refs #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
…target

Refs #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
… target

Refs #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
Refs #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
…heck

Refs #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
Refs #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
Refs #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
…resident rejoins

Bumps core for the late-requested confirmation target.

Refs #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
@kriszyp kriszyp added this to the v5.4 milestone Oct 11, 2026

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

Copy link
Copy Markdown

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 updates the replication confirmation logic to ensure that replicatedConfirmation only counts peers that are resident for the written records. It introduces resident peer lists tracking in createConfirmationWaiter and adds pre-submission feasibility checks to refuse impossible confirmation counts early. The changes are accompanied by comprehensive unit and integration tests, as well as updated design documentation. Feedback on the changes highlights a potential TypeScript compilation error under strictNullChecks in replication/knownNodes.ts where residentPeerLists is mapped directly without being narrowed; using the already-narrowed residentGroups variable is suggested to resolve this.

Comment thread replication/knownNodes.ts
kriszyp and others added 4 commits October 11, 2026 14:09
Refs #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
…eout message

Refs #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
…irst-submission feasibility check

Refs #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
Refs #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
@kriszyp
kriszyp marked this pull request as ready for review October 11, 2026 22:05
@kriszyp
kriszyp merged commit 02a1866 into main Oct 11, 2026
52 of 53 checks passed
@kriszyp
kriszyp deleted the fix/residency-aware-replicated-confirmation branch October 11, 2026 22:05
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