Skip to content

feat(crdt): add PN-Counter, OR-Set, and LWW-Map CRDTs#40

Merged
brunoga merged 4 commits intomainfrom
feat/crdt-counter-set-map
Mar 25, 2026
Merged

feat(crdt): add PN-Counter, OR-Set, and LWW-Map CRDTs#40
brunoga merged 4 commits intomainfrom
feat/crdt-counter-set-map

Conversation

@brunoga
Copy link
Copy Markdown
Owner

@brunoga brunoga commented Mar 25, 2026

This PR introduces three new state-based Conflict-free Replicated Data Types (CRDTs) to the crdt package. These types provide high-level
abstractions for common distributed data patterns, all leveraging the project's core CRDT[T] generic base and Hybrid Logical Clocks (HLC).

New CRDTs

  • PN-Counter: A Positive-Negative Counter that supports both increments and decrements. It tracks independent totals across nodes and resolves
    to a single convergent value (sum(increments) - sum(decrements)).
  • OR-Set (Observed-Remove Set): An "Add-Wins" set implementation. It allows concurrent additions and removals of the same element; in the event
    of a conflict, the addition takes precedence.
  • LWW-Map (Last-Write-Wins Map): A generic key-value map where concurrent writes to the same key are resolved using HLC timestamps. It supports
    both Set and Delete operations with proper tombstone propagation.

Key Changes & Refactoring

  • Core Integration: While Set and Map were initially drafted with standalone logic, they have been refactored into thin wrappers around the
    CRDT[T] base. This ensures consistent behavior for synchronization, delta application, and state-based merging across all types.
  • Code Simplification: By delegating concurrency control and causal tracking to the underlying CRDT[T] engine, the implementation of individual
    types is now significantly cleaner and less prone to manual locking or timestamp errors.
  • Convergent Testing: Each new type includes a comprehensive test suite (counter_test.go, set_test.go, map_test.go) verifying:
    • Basic CRUD operations.
    • Idempotency: Multiple merges of the same state produce no side effects.
    • Commutativity: The order of merges does not affect the final state.
    • Convergence: Nodes with different edit histories reach the same state after synchronization.

brunoga added 4 commits March 25, 2026 06:54
Counter wraps CRDT[counterState] where counterState holds two
map[string]int64 maps (Inc, Dec), one slot per node ID.

Because each node writes only to its own slot, LWW in CRDT.Merge
is always correct: the newer write always carries the larger value,
so the max-semantics of a G-Counter fall out of the existing
infrastructure for free.

Value() = sum(Inc) - sum(Dec).

Tests: basic increment/decrement, zero/negative delta no-op,
two-node merge, concurrent goroutine increments, idempotency,
commutativity, negative values.
Set[T comparable] is an Observed-Remove Set with add-wins conflict
resolution. Each Add creates a uniquely HLC-tagged entry; Remove only
tombstones entries visible at call time. A concurrent Add from another
node produces a different tag, so after Merge the element survives
(add wins over concurrent remove).

Implemented as a standalone type (map[string]setEntry[T] keyed by
HLC.String()) rather than wrapping CRDT[T]: the LWW clock-filter in
CRDT.Merge discards remote entries that have no local clock entry,
which breaks union semantics for disjoint adds.

Tests: add/contains, remove, re-add after remove, Items dedup,
duplicate add, two-node convergence, add-wins property, idempotency,
remove-non-existent no-op, commutativity.
Map[K comparable, V any] is a distributed key-value map where
concurrent writes to the same key are resolved by Last-Write-Wins
(higher HLC timestamp wins). Deletes are tombstones, so delete/set
ordering is determined purely by timestamp.

Standalone implementation (same reasoning as Set): CRDT[T] LWW
discards remote entries without local clock history, breaking
convergence for disjoint key sets.

Exercises two-parameter generics and validates the HLC clock
update-before-compare pattern for correct causal ordering.

Tests: set/get, delete, overwrite, Keys(), disjoint-key merge,
LWW same-key conflict, delete-wins-over-older-set,
set-wins-over-older-delete, idempotency, commutativity,
string keys, int keys.
Both types previously maintained their own sync.RWMutex, clock, and merge
logic. They now delegate entirely to the generic CRDT[T] engine, which
provides per-path LWW clocks, tombstones, and full-state merge for free.

Map[K,V] wraps CRDT[map[K]V]: each Set/Delete edits the map value, producing
OpAdd/OpReplace/OpRemove ops at path "/<key>"; the CRDT clock and tombstone
maps handle LWW resolution and delete-wins / re-add-wins correctly.

Set[T] wraps CRDT[setInner[T]] where setInner holds a map[string]setEntry[T]
keyed by HLC.String(). Each Add allocates a unique tag via Clock().Now(),
inserting a new entry at a distinct path "/Entries/<hlc>"; concurrent Adds
from other nodes land at different paths and are therefore unaffected by
Remove, preserving the Add-Wins (OR-Set) property without any custom merge
logic.
@brunoga brunoga merged commit aaba2e7 into main Mar 25, 2026
1 check passed
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