Skip to content

Clarify confirm/replicatedConfirmation any-N semantics in the sharding docs - #723

Draft
kriszyp wants to merge 10 commits into
mainfrom
clarify-replicated-confirmation-semantics
Draft

kriszyp wants to merge 10 commits into
mainfrom
clarify-replicated-confirmation-semantics

Conversation

@kriszyp

@kriszyp kriszyp commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

⊙ Problem

replicatedConfirmation: N / confirm=N was documented, but readers (per the QA report this task is based on) assumed confirm=1 meant a specific peer acknowledged the write — it doesn't. Confirmation counts any N peer acknowledgements, and a peer outside the record's residency counts too even though it may hold only an invalidation entry or nothing at all for that record.

❓ Your call: as specified, yes — but verifying the semantics against the actual replication code (per # Context) surfaced enough additional inaccuracies in the existing text (an unconditional "N full copies" implication, replicateTo placement vs. confirmation-count conflation, a v5.2.1 behavior change with no version note, post-commit failure framing) that this grew from two sentences into a fuller pass over both docs. Each addition below is traced to source, not inferred.

💡 Solution

Added one clarification, stated once per doc and cross-referenced from the other two Replication Control subsections (REST, Operations API, programmatic) instead of being duplicated three times — duplication is what let earlier drafts of this same note drift internally during review (see commit history).

  • confirm=N / replicatedConfirmation: N waits until any N peers acknowledge — not N specific peers, and not necessarily N peers holding the full record (a non-resident peer can count via an invalidation entry, or nothing at all). Same note, v4 semantics.
  • replicateTo / X-Replicate-To controls where the record is stored (under the default residency — setResidency/setResidencyById take precedence); it does not change which peers' acknowledgements are counted.
  • confirm/replicatedConfirmation deduplicates by peer identity as of v5.2.1; earlier v5 releases counted repeated crossings from one peer, the way v4 still does.
  • Confirmation can fail two other ways after the write has already committed locally: a 400 if N exceeds this node's actual replication recipients, or a 504 after the server's timeout (900s default, raised to match a longer blob-transfer timeout). v5.2.1 also introduced both of these; v4 has no timeout and checks against total known nodes — and can hang even on a healthy peer if its ack crosses before the wait registers.
  • Confirmation waiting is Harper Pro only — without it, confirm/replicatedConfirmation is silently a no-op.
  • N must be a positive integer; it isn't validated, and behavior for an invalid value is unspecified (deliberately not promising exact behavior here — see Verification).

🔧 Changes

  • reference/replication/sharding.md — current (v5) docs, all three Replication Control subsections under Dynamic Sharding.
  • reference_versioned_docs/version-v4/replication/sharding.md — same clarification with v4-specific semantics (no peer dedup, no timeout, server.nodes excludes the local node).

✅ Verification

Semantics verified against harper-pro source (not just read, traced and in several cases executed against extracted waiter code across tagged releases) and harperdb (v4) source, per # Context's instruction to verify before writing:

  • Any-N, not specific-N: harper-pro/replication/knownNodes.ts createConfirmationWaiter resolves on confirmed.size === confirmationCount, a Set keyed by peer name — any N peers, independent of residency (countAlreadyConfirmedPeers/getReplicationRecipients).
  • Peer dedup is v5.2.1+: git merge-base --is-ancestor 61a1531b <tag> across local tags shows the dedup commit (and the paired timeout/recipient-bound commits, same timestamp) first lands in v5.2.1; v5.0.0/v5.1.x/v5.2.0 use a bare counter like v4 still does today.
  • v4 has no dedup, no timeout: harperdb/server/replication/knownNodes.ts increments a plain counter with no per-peer tracking, and its confirmation Promise has no reject/timeout path at all.
  • Post-commit framing: both the 400 (over-large count) and 504 (timeout) throw from inside the commit's already-succeeded .then (DatabaseTransaction.ts/LMDBTransaction.ts, both versions) — the write is durable either way.
  • Pro-only: OSS core only reads confirmReplication (resources/DatabaseTransaction.ts, resources/LMDBTransaction.ts); only harper-pro/replication/knownNodes.ts calls replicationConfirmation(...) to register a handler.
  • Ran npx prettier --check on both changed files — clean.
  • Independent review: 10 rounds (1 full baseline, 5 more full passes driven by genuinely new findings each time, delta rounds in between) via the pre-push review CLI (codex + gemini + cursor + Harper-domain adjudication). Converged at round 10 with only pre-existing/out-of-scope findings remaining (below).

Findings deliberately left open (pre-existing or outside this diff)

Per the dispatch Findings section — not fixed here, since none are introduced by this diff:

  • reference/replication/sharding.md:33-38 (+ v4) — the top-level replication.replicateTo: N YAML example may never be read by core (only replication.databases[].replicateTo is) — needs verification against a live cluster.
  • reference/replication/sharding.md:40 (+ v4) — "stored on three nodes total" is unconditional; doesn't mention setResidency/setResidencyById override, unlike the per-request paths this PR fixed.
  • reference/replication/sharding.md:44 (+ v4) — non-super_user callers get a 403 for any replication parameter; undocumented anywhere on this page.
  • Product bugs (not doc bugs, filed as findings for the EM to triage into issues): the 400 throw skips transaction cleanup (blob/lock release) in DatabaseTransaction.ts; replicatedConfirmation passed as a string is never coerced and the strict-equality waiter comparison never matches it.

Related PRs: none found

Dispatch: task documentation-replicated-confirmation-semantics · queued by unknown · ran by claude/sonnet/high · worker kzyp-xps-1

Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-composer,cursor-kimi,cursor-muse; rounds=10; full=6 @ 6d513c8

Review-Attention: skim ~2m (decisions: unspecified-vs-product-validation, pro-requirement-placement, publish-timeout-constant, document-v4-defect-as-contract) @ 6d513c8

kriszyp and others added 10 commits October 9, 2026 08:48
replicatedConfirmation: N (and confirm=N) resolves once any N peers ack
the commit (knownNodes.ts createConfirmationWaiter: a Set keyed by peer
name, settled at size === confirmationCount) — not N specific peers, so
it cannot guarantee a particular node has the write. Point readers at
replicateTo with an explicit node list for that instead.

Applied to all three Replication Control subsections (REST, Operations
API, programmatic) in both reference/replication/sharding.md and the
v4 versioned copy.

Dispatch-Task: documentation-replicated-confirmation-semantics
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqjFRTLq5wic3yFKhYajq
The first pass said confirm=N guarantees "N replicas exist somewhere",
and that replicateTo's explicit node list fixes targeting for confirm.
Both are false: a peer outside the record's residency still advances
the confirmation watermark via an invalidation entry (partial record,
no full copy), and the confirmation waiter counts acks from whichever
peers this node replicates to regardless of the request's replicateTo
list (knownNodes.ts getReplicationRecipients / createConfirmationWaiter).
Also drops the implication that a count and a node list combine in one
X-Replicate-To value — REST.ts treats "2,node1" as two hostnames, not a
count plus a list.

Restates the three notes to separate what replicateTo controls (where
the record is stored) from what confirm/replicatedConfirmation counts
(any N acks, residency-independent), without claiming the combination
proves a named node has the write.

Dispatch-Task: documentation-replicated-confirmation-semantics
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqjFRTLq5wic3yFKhYajq
…excluded peers

A non-resident peer doesn't always get an invalidation entry: with
setResidencyById, or on a put/patch to a record the peer was already
excluded from in the previous residency, Harper sends it nothing for
that record at all (replicationConnection.ts skip branch, same in v4).
Its replication position — and so its confirmation count — still
advances via a later sequence update or record, independent of what
it actually holds.

Dispatch-Task: documentation-replicated-confirmation-semantics
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqjFRTLq5wic3yFKhYajq
"replicateTo controls where the record is stored" was unqualified: a
table's setResidency/setResidencyById function takes precedence over
replicateTo entirely (Table.ts: setResidencyById short-circuits before
context.replicateTo is read; setResidency overwrites the static
getResidency method outright). Added the caveat with a pointer to
Custom Sharding, in all three sections of both docs.

Also softened the two still-unqualified lines directly above/after the
any-N explanation (the confirm=1 bullet, and "confirm can be combined
with explicit node lists") so a reader skimming past the explanatory
paragraph doesn't land on a sentence that reads as a per-node
guarantee on its own.

Dispatch-Task: documentation-replicated-confirmation-semantics
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqjFRTLq5wic3yFKhYajq
v4's confirmation waiter (knownNodes.ts) is a plain counter with no
per-peer identity tracking, unlike v5 Pro's Set-based dedup
(knownNodes.ts:894-901) — a peer whose replicated position regresses
and re-crosses the write's txnTime can fire onConfirm twice. Dropped
"distinct" from the v4 copy only; v5 keeps it since it does dedupe.

Also widened the REST section's residency-precedence caveat: it only
mentioned "an explicit node list", but setResidencyById short-circuits
before context.replicateTo is ever read regardless of whether the
request used a count or a node list, so the numeric X-Replicate-To
form needed the same caveat (the Operations API/programmatic sections
already stated it generically).

Dispatch-Task: documentation-replicated-confirmation-semantics
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqjFRTLq5wic3yFKhYajq
…re modes

Three review rounds flagged the same root cause: repeating the full
any-N/placement caveat in the REST, Operations API, and programmatic
sections let their wording drift (v5's "distinct" word was missing
from two of the three; v4's non-dedup caveat wasn't propagated to the
other two after fixing REST). Restructured both docs to state the
semantics once, in the REST section, and have Operations API and
programmatic point to it — removing the drift risk instead of
re-patching each copy.

Also documents the confirmation wait's other two exits, which kept
surfacing as a gap across rounds: v5 rejects an excessive count with
400 and times out with 504 after 900s; v4 has no timeout and only
checks the count against total node count, not actual recipients.

Dispatch-Task: documentation-replicated-confirmation-semantics
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqjFRTLq5wic3yFKhYajq
…by-one

Four more verified corrections from review round 6:

- Peer-identity dedup landed in harper-pro commit 61a1531b, between
  v5.2.0 and v5.2.1 (verified via `git merge-base --is-ancestor` across
  tags) — v5.0.0/v5.1.0/v5.2.0 used a bare counter like v4 still does.
  Added <VersionBadge type="changed" version="v5.2.1" /> and a sentence
  on the earlier behavior.
- "Rejected outright (400)" / "fails with a 504" read as if the write
  itself failed. Both throw from inside the commit's success path
  (DatabaseTransaction.ts, both versions) — the local write has already
  landed either way, only the confirmation wait fails. Reworded, and
  fixed "three other ways" to "two" to match what's actually listed.
- v4's rejection check is `confirmationCount > server.nodes.length`,
  and server.nodes excludes the local node (knownNodes.ts never pushes
  getThisNodeName()) — "total number of nodes in the cluster" overcounts
  by one; reworded to "other nodes it knows about".
- v4's :67 ("acknowledgements from any N peers") read as N distinct
  peers, contradicting :54's "not necessarily N distinct ones" two
  lines up. Reworded to drop the N-peers framing for v4 specifically.

Dispatch-Task: documentation-replicated-confirmation-semantics
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqjFRTLq5wic3yFKhYajq
Verified via git history that the whole confirmation rewrite — peer
dedup (61a1531b), the 900s timeout (26b7b3b1), and bounding the reject
threshold to actual recipients (f41142aa) — landed together, first in
v5.2.1 (same commit timestamp, earliest containing tag). Badged both
the dedup claim and the failure-mode paragraph instead of just one,
since both were previously stated as unconditional v5 behavior.

Also:
- "As of <VersionBadge .../>" rendered literally as "As of Changed in:
  v5.2.1" since the component already renders its own label — dropped
  "As of" and used it as a leading fragment instead, matching the
  convention at clustering.md:144.
- "(see below)" pointed at an explanation that only exists on the
  separate v4 page, not this one — replaced with an inline comparison
  ("the way v4 still does").
- "the record exists either way" is wrong for a confirmed delete (it
  commits a tombstone, not a record) — reworded to "the write (or
  delete)" in both docs.

Dispatch-Task: documentation-replicated-confirmation-semantics
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqjFRTLq5wic3yFKhYajq
… term drift

Four more verified items from review round 8:

- N must be a positive integer in both versions: the waiter compares
  with strict equality (confirmed.size === confirmationCount in
  harper-pro, ++count === confirmationCount in v4/harperdb), so a
  string or fractional count never matches and the wait never
  resolves on its own.
- Confirmation waiting is Harper Pro only: the OSS core submodule
  reads `confirmReplication` (DatabaseTransaction.ts/LMDBTransaction.ts)
  but never calls `replicationConfirmation(...)` to register a
  handler — only harper-pro's top-level replication/knownNodes.ts
  does. Without Pro, confirm/replicatedConfirmation is silently a
  no-op.
- v4's wait can also hang on an otherwise-healthy peer: its waiter
  seeds no already-crossed state when registered (no analog to
  harper-pro's alreadyConfirmedPeers seed, knownNodes.ts:792-798), so
  an ack that lands between commit and registration is lost. Broadened
  "if a peer never catches up" accordingly.
- Replaced "watermark crossings" with "replication-position crossings"
  — the only use of "watermark" on the page, inconsistent with
  "replication position" used everywhere else.

Dispatch-Task: documentation-replicated-confirmation-semantics
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqjFRTLq5wic3yFKhYajq
My own prior claim ("never matches and the wait never resolves") was
wrong: the REST parser drops a non-numeric confirm value outright
(NaN fails the >= 0 check), the already-confirmed-peers seed can
resolve a bogus count immediately if enough peers already crossed
before the wait registered, and from v5.2.1 the wait ends in a 504
regardless. Rather than chase the exact behavior of unvalidated input
across versions and races, just state the requirement (N must be a
positive integer) and say the value isn't validated and an invalid
one's behavior is unspecified.

Dispatch-Task: documentation-replicated-confirmation-semantics
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRqjFRTLq5wic3yFKhYajq

@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 updates the replication and sharding documentation for both the current version and the v4 versioned docs, clarifying the semantics of replication confirmation (confirm=N), deduplication, timeouts, and residency. The review feedback suggests avoiding the use of the component mid-sentence in running prose, recommending plain text alternatives like (as of v5.2.1) to maintain a consistent reading flow.

- `confirm=1` — wait for confirmation from one additional node before responding
- `confirm=1` — wait for confirmation from one additional peer before responding (see below for exactly what this confirms)

`confirm=N` (and the operation-body/programmatic `replicatedConfirmation: N` used below) waits until any N peers acknowledge the commit — not N specific peers. N must be a positive integer; it isn't validated, and a non-integer value's behavior is unspecified. <VersionBadge type="changed" version="v5.2.1" /> — acknowledgements are deduplicated by peer identity, so the same peer can't satisfy more than one of the N; earlier releases, like v4 still does, counted repeated replication-position crossings from one peer as satisfying more than one of the N on their own. A peer outside the record's residency still counts, once its replication position passes the write — even though it may hold only an invalidation entry, or nothing at all for that record. So confirmation cannot guarantee that N full copies exist, or that any particular node — even one named in `X-Replicate-To`/`replicateTo` — has the write. Confirmation waiting requires Harper Pro; without it, `confirm`/`replicatedConfirmation` is accepted but has no effect — the write succeeds immediately with no wait and no error.

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

Avoid using the <VersionBadge> component mid-sentence in running prose. Instead, use plain text such as (as of v5.2.1) to maintain a consistent reading flow.

Suggested change
`confirm=N` (and the operation-body/programmatic `replicatedConfirmation: N` used below) waits until any N peers acknowledge the commit — not N specific peers. N must be a positive integer; it isn't validated, and a non-integer value's behavior is unspecified. <VersionBadge type="changed" version="v5.2.1" /> — acknowledgements are deduplicated by peer identity, so the same peer can't satisfy more than one of the N; earlier releases, like v4 still does, counted repeated replication-position crossings from one peer as satisfying more than one of the N on their own. A peer outside the record's residency still counts, once its replication position passes the write — even though it may hold only an invalidation entry, or nothing at all for that record. So confirmation cannot guarantee that N full copies exist, or that any particular node — even one named in `X-Replicate-To`/`replicateTo` — has the write. Confirmation waiting requires Harper Pro; without it, `confirm`/`replicatedConfirmation` is accepted but has no effect — the write succeeds immediately with no wait and no error.
`confirm=N` (and the operation-body/programmatic `replicatedConfirmation: N` used below) waits until any N peers acknowledge the commit — not N specific peers. N must be a positive integer; it isn't validated, and a non-integer value's behavior is unspecified. Acknowledgements are deduplicated by peer identity (as of v5.2.1), so the same peer can't satisfy more than one of the N; earlier releases, like v4 still does, counted repeated replication-position crossings from one peer as satisfying more than one of the N on their own. A peer outside the record's residency still counts, once its replication position passes the write — even though it may hold only an invalidation entry, or nothing at all for that record. So confirmation cannot guarantee that N full copies exist, or that any particular node — even one named in `X-Replicate-To`/`replicateTo` — has the write. Confirmation waiting requires Harper Pro; without it, `confirm`/`replicatedConfirmation` is accepted but has no effect — the write succeeds immediately with no wait and no error.
References
  1. Avoid using the component mid-sentence in prose as it is awkward; use plain text (e.g., (vX.Y.Z)) instead.


`X-Replicate-To`/`replicateTo` controls where the record is stored — whether given as a count or an explicit node list — but only under the default residency. A table's `setResidency` or `setResidencyById` function (see Custom Sharding below) takes precedence over it, and placement is independent of what the confirmation count counts either way.

Confirmation can also end two other ways, both after the write (or delete) has already taken effect locally — only the wait itself fails: a count larger than the peers this node actually replicates the database to is rejected with a 400; otherwise, the request fails with a 504 if confirmation doesn't complete within the server's timeout (900 seconds by default, raised to match a longer configured blob-transfer timeout). <VersionBadge type="changed" version="v5.2.1" /> — earlier releases had no timeout and checked the count against every known node rather than actual recipients, the way v4 still does.

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

Avoid using the <VersionBadge> component mid-sentence in running prose. Use plain text such as (as of v5.2.1) instead to keep the text clean and readable.

Suggested change
Confirmation can also end two other ways, both after the write (or delete) has already taken effect locally — only the wait itself fails: a count larger than the peers this node actually replicates the database to is rejected with a 400; otherwise, the request fails with a 504 if confirmation doesn't complete within the server's timeout (900 seconds by default, raised to match a longer configured blob-transfer timeout). <VersionBadge type="changed" version="v5.2.1" /> — earlier releases had no timeout and checked the count against every known node rather than actual recipients, the way v4 still does.
Confirmation can also end two other ways, both after the write (or delete) has already taken effect locally — only the wait itself fails: a count larger than the peers this node actually replicates the database to is rejected with a 400; otherwise, the request fails with a 504 if confirmation doesn't complete within the server's timeout (900 seconds by default, raised to match a longer configured blob-transfer timeout) (as of v5.2.1). Earlier releases had no timeout and checked the count against every known node rather than actual recipients, the way v4 still does.
References
  1. Avoid using the component mid-sentence in prose as it is awkward; use plain text (e.g., (vX.Y.Z)) instead.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-723

This preview will update automatically when you push new commits.

This branch was successfully deployed

1 active deployment
pr-723 — 6d513c8b Deployed Oct 9, 2026 by github-actions[bot]
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