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

Assignees

No one assigned

    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