March Docs

GlobalRegistry

GlobalRegistry: the cluster-wide name registry as a composite CRDT (P1 L5).

Maps a name -> the (node, pid) that holds it, gossip-replicated. Each binding carries a VectorClock that causally orders updates to that name, plus a presence bit so an unregister converges like an LWW-element-set removal (tombstones merge by the same rule rather than vanishing). Per-name conflict resolution:

  • causally newer (VectorClock After) -> the newer binding wins;
  • causally older (Before) / Equal -> keep the existing (idempotent);
  • Concurrent (a genuine conflict) -> a deterministic tiebreak on
  (node_id, pid) so every replica converges to the same winner.

That makes merge associative/commutative/idempotent — the CRDT join the generic gossip/anti-entropy loop uses (Merkle anti-entropy at this layer, since the registry can hold many names; see the design spec).

Types

typeEntryEntry = { node_id : String, pid : Int, clock : VectorClock, present : Bool }#
typeNamesNames = Names(Map(String, Entry))#

Functions

fnall_entriesall_entries(reg : Names) : List((String, Entry))#

Collect all (name, entry) pairs, including tombstones, for wire transfer.

fncontainscontains(reg : Names, name : String) : Bool#
fndiff_entriesdiff_entries(local : Names, remote_root : String,#

CRDT-merge [remote_leaves] into [local], but only when hashes differ. remote_leaves is the list of (name, entry) pairs from the remote node. Returns the updated registry.

fnemptyempty() : Names do Names(Map.empty()) end#
fnlookuplookup(reg : Names, name : String) : Option((String, Int))#
fnmergemerge(a : Names, b : Names) : Names#
fnnamesnames(reg : Names) : List(String)#
fnobserveobserve(reg : Names, name : String, entry : Entry) : Names#
fnregisterregister(reg : Names, name : String, node_id : String, pid : Int, clock : VectorClock) : Names#
fnroot_hashroot_hash(reg : Names) : String#

Build a Merkle tree over sorted (name, entry) pairs and return its root hash. Comparing root hashes across nodes detects divergence in O(1). Returns the empty-string sentinel when the registry is empty.

fnsizesize(reg : Names) : Int do List.length(names(reg)) end#
fnunregisterunregister(reg : Names, name : String, clock : VectorClock) : Names#