Skip to content

LwwRegister concurrent merge uses document-level clock, causing silent data loss on uncontested LWW properties #50

@kkalass

Description

@kkalass

Affected Package

locorda_core

Steps to Reproduce

  1. Installations A and B share a document at a common state: both at logicalTime=3.
  2. A (offline) changes both schema:name AND schema:description → A: logicalTime=4, physicalTime=T1.
  3. B (offline) changes only schema:name → B: logicalTime=4, physicalTime=T2 where T2 > T1.
  4. A and B sync with each other.

Actual Behavior

LwwRegister.remoteMerge() computes ClockComparison.concurrent (each installation has logicalTime=4 for itself, neither has seen the other's entry). Tie-break falls back to maxPhysicalTime of the entire document: T2 > T1 → B wins all LWW properties. schema:description silently reverts to the pre-divergence value — B never changed it.

Expected Behavior

Each LWW property resolved independently:

  • schema:name: both sides changed it → physical-time tie-break → B wins (T2 > T1) ✓
  • schema:description: only A changed it → A wins unconditionally ✓

Platform

No response

Environment

No response

Additional Context

Root Cause:
The merge has no per-property change information available. Resolution granularity is the whole document. Two pieces of information are needed but currently missing or unused:

  1. Per-property HLC in the document — when saving a LWW property, a metadata statement must be written recording the HLC at the time of that change (one statement per property, overwritten on each write → O(properties), bounded). This gives the remote side visibility into when each property was last changed.

  2. Local DB query during concurrent merge — when ClockComparison.concurrent, for each LWW property:

    • Local side: query sync_property_changes table for whether/when this property was changed locally after the divergence point (the remote's last-known logical time for our installation)
    • Remote side: read the per-property HLC statement from the remote document
    • If only one side has a change after divergence → that side wins unconditionally
    • If both sides changed it → physical-time tie-break (same as current behavior, now scoped to actually contested properties only)

Both pieces are needed. The per-property document statement ensures both sides reach the same merge decision (convergence). The local DB query gives our own side's change timestamps without requiring them to be re-embedded in the document on every sync.

RemoteDocumentMerger already has an injected _storage field (currently // ignore: unused_field with the comment // Storage will be needed for property change history during actual merge) and LwwRegister.remoteMerge() already contains:

// TODO: what about augmenting the CRDT merge with the property change metadata from DB for more precise merges?

Both confirm the fix was anticipated.

Metadata

Metadata

Labels

bugSomething isn't workingpkg: locorda_corePlatform-agnostic sync enginepriority: highImportant — fix soon

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions