Skip to content

docs: auto-update for PR #2969 — fix(lww-register): add value byte tie-break to fix merge-mode divergence#2977

Closed
meroreviewer[bot] wants to merge 2 commits into
masterfrom
docs/auto-pr2969-bc827a8
Closed

docs: auto-update for PR #2969 — fix(lww-register): add value byte tie-break to fix merge-mode divergence#2977
meroreviewer[bot] wants to merge 2 commits into
masterfrom
docs/auto-pr2969-bc827a8

Conversation

@meroreviewer

@meroreviewer meroreviewer Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Automatic Documentation Update

Opened automatically after PR #2969 merged.

Each block shows the documentation change as a diff (added lines in green, removed in red); expand "Why this changed" for the source rationale.

Documentation changes

architecture/storage-schema.html — Add value-byte tie-break to make LwwRegister merge commutative under zero stamps; Tighten trait bound on LwwRegister merge operations to require BorshSerialize; Add regression test for commutativity of zero-stamp merges

 seq (8 BE) Big-endian sequence number for ordered scans
 var Variable-length suffix (application keys)
+LwwRegister Merge: Three-Level Tie-Breaking & BorshSerialize Bound
+The LwwRegister<T> CRDT previously used a two-level comparison to decide which
+value wins during a merge: first by timestamp, then by node_id. In merge-mode,
+both fields are intentionally zeroed so that migrated data is not tied to any
+particular node's clock or identity. This degenerate case meant that two
+registers carrying different values but identical stamps would resolve non-
+deterministically — merge(A, B) kept A while merge(B, A) kept B, permanently
+violating CRDT convergence.
+Updated Merge Ordering (Three Levels)
+Higher timestamp wins. Normal wall-clock or HLC comparison. Most merges are
+resolved here.
+Higher node_id wins on timestamp tie. Breaks ties between concurrent writes from
+different nodes.
+Higher Borsh-serialized bytes win when both stamp fields are equal. A private
+helper other_wins borsh-serializes both values and compares the resulting byte
+slices lexicographically, producing a deterministic total order regardless of
+call order. Serialization failure panics via expect rather than silently
+substituting an empty Vec, which would corrupt ordering.
+Together these three levels form a strict total order over all possible register
+states, guaranteeing that merge(A, B).get() == merge(B, A).get() in every case,
+including the zero-stamp merge-mode path.
+New BorshSerialize Bound on T
+The following items now require T: Clone + borsh::BorshSerialize (previously
+only T: Clone):
+LwwRegister::merge(&mut self, other: &LwwRegister<T>)
+LwwRegister::would_update(&self, other: &LwwRegister<T>) -> bool
+The Mergeable blanket impl for LwwRegister<T>
+Any type stored in a LwwRegister that does not implement BorshSerialize will now
+produce a compile-time error when these methods are called. Types that only call
+get, set, or other non-merge methods are unaffected.
+Updated Doc-Comment Contract for merge
+/// Merges `other` into `self`, retaining the value that wins under a
+/// three-level total order:
+///
+/// 1. Higher `timestamp` wins.
+/// 2. On timestamp tie, higher `node_id` wins.
+/// 3. When both `timestamp` and `node_id` are equal (e.g. merge-mode
+/// registers where both fields are zeroed), the value whose
Why this changed (source: PR #2969)

The merge ordering logic previously had only two comparison levels: timestamp, then node_id. In merge-mode, both fields are zeroed, so two registers with different values had identical stamps, making merge(A,B) keep A while merge(B,A) kept B — a permanently diverging, non-commutative outcome. A new private helper other_wins now adds a third level: when both timestamp and node_id are equal, it borsh-serializes both values and compares the resulting byte slices lexicographically, producing a deterministic total order regardless of call order. Serialization failure panics with expect rather than silently substituting an empty vec (which would corrupt the ordering).

The impl<T: Clone> LwwRegister<T> block providing merge and would_update, and the Mergeable blanket impl, now require T: Clone + borsh::BorshSerialize. This is a narrowing of the public API surface for those methods.

A new test merge_mode_equal_stamps_different_values_converge creates two merge-mode registers with value 1 and 2 respectively (both having zero timestamps and zero node_ids), then asserts that merge(A,B).get() == merge(B,A).get(), directly catching any future regression to non-commutative behavior.

architecture/migrations.html — Add value-byte tie-break to make LwwRegister merge commutative under zero stamps; Tighten trait bound on LwwRegister merge operations to require BorshSerialize; Add regression test for commutativity of zero-stamp merges

 Node-local timestamps. LwwRegister::new(...) / .set(...) and Element update tim…
 Random collection ids. New collections and Vector/AuthoredVector elements are r…
-So a migration that only carries fields and adds new ones with ::new() cannot
-trigger a determinism bug.
+So a migration that only carries fields and adds new ones with ::new() cannot
+trigger a determinism bug. When two registers arrive with equal timestamps and
+equal node ids (both zeroed in merge mode), LwwRegister::merge adds a third tie-
+break level: it borsh-serializes both values and keeps whichever byte slice
+compares higher lexicographically, making merge commutative even under zero
+stamps. This means merge(A, B) and merge(B, A) always agree on the same value.
+Note that merge, would_update, and the Mergeable blanket impl for LwwRegister<T>
+now require T: Clone + borsh::BorshSerialize.
 What you must still avoid (app-level)
 Wall-clock / RNG / iteration order. If you materialize an ordered structure (a …
Why this changed (source: PR #2969)

The merge ordering logic previously had only two comparison levels: timestamp, then node_id. In merge-mode, both fields are zeroed, so two registers with different values had identical stamps, making merge(A,B) keep A while merge(B,A) kept B — a permanently diverging, non-commutative outcome. A new private helper other_wins now adds a third level: when both timestamp and node_id are equal, it borsh-serializes both values and compares the resulting byte slices lexicographically, producing a deterministic total order regardless of call order. Serialization failure panics with expect rather than silently substituting an empty vec (which would corrupt the ordering).

The impl<T: Clone> LwwRegister<T> block providing merge and would_update, and the Mergeable blanket impl, now require T: Clone + borsh::BorshSerialize. This is a narrowing of the public API surface for those methods.

A new test merge_mode_equal_stamps_different_values_converge creates two merge-mode registers with value 1 and 2 respectively (both having zero timestamps and zero node_ids), then asserts that merge(A,B).get() == merge(B,A).get(), directly catching any future regression to non-commutative behavior.


Generated by ai-reviewer update-docs. Nothing was auto-merged.

@meroreviewer meroreviewer Bot added automated-docs documentation Improvements or additions to documentation labels Jun 26, 2026

@meroreviewer meroreviewer Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 AI Code Reviewer

Reviewed by 1 agents | Quality score: 31% | Review time: 46.7s

💡 1 suggestions. See inline comments.


🤖 Generated by AI Code Reviewer | Review ID: review-956039fb

/// Borsh-serialized bytes compare greater wins.
///
/// This ordering is commutative, associative, and idempotent, satisfying
/// the CRDT convergence guarantee even for merge-mode registers that carry

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

💡 Doc states merge is 'commutative, associative, and idempotent' but only commutativity is tested

The doc-comment contract in the new section claims the three-level ordering is 'commutative, associative, and idempotent' and the regression test only asserts commutativity (merge(A,B) == merge(B,A)). The documentation would be more precise if it noted that associativity and idempotency are properties of the total order itself (not separately tested), or if it pointed to where those properties are verified. This is a minor documentation accuracy issue.

Suggested fix:

Either add a note that associativity and idempotency follow from the strict total order property (no separate test needed), or add a brief explanation of why those properties hold given the lexicographic byte comparison.

@chefsale chefsale marked this pull request as ready for review June 29, 2026 05:50
@chefsale chefsale closed this Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automated-docs documentation Improvements or additions to documentation external

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant