gabriel / muse public
vclock.py python
185 lines 6.8 KB
Raw
sha256:06dba78c2a78e251b580422dd1fd547f3c8357ff18f7709a860873b2d24dbbbf chore: bump version to 0.2.0rc14 Sonnet 4.6 patch 2 days ago
1 """Vector clock for causal ordering in distributed multi-agent writes.
2
3 A vector clock (Lamport, 1978 / Fidge, 1988) tracks how many events each
4 agent has observed. Two clocks can be compared to determine whether one
5 *causally precedes* the other or whether they are *concurrent* (neither
6 dominates).
7
8 This is the foundational primitive for all CRDT coordination in Muse:
9
10 - :class:`LWWRegister` uses vector clock comparison to break same-timestamp
11 ties deterministically.
12 - :class:`RGA` uses ``agent_id`` for deterministic concurrent-insert ordering.
13 - The ``CRDTPlugin.join()`` protocol uses causal ordering to detect which
14 writes truly conflict vs. which are simply out-of-delivery-order.
15
16 Public API
17 ----------
18 - :class:`VClockDict` — ``TypedDict`` wire format ``{agent_id: count}``.
19 - :class:`VectorClock` — the clock itself, with ``increment``, ``merge``,
20 ``happens_before``, ``concurrent_with``, ``to_dict``, ``from_dict``.
21 """
22
23 from typing import TypedDict
24
25 type _CountMap = dict[str, int]
26
27 class VClockDict(TypedDict, total=False):
28 """Wire format for a vector clock — ``{agent_id: event_count}``.
29
30 ``total=False`` because the presence of a key is meaningful (an absent key
31 is equivalent to the value ``0``). Serialise with :meth:`VectorClock.to_dict`
32 and deserialise with :meth:`VectorClock.from_dict`.
33 """
34
35 class VectorClock:
36 """Causal clock for distributed agent writes.
37
38 Stores a mapping from agent identifiers (arbitrary strings) to the number
39 of events that agent has performed. An absent agent is equivalent to
40 count ``0``.
41
42 Instances are **immutable** from the outside: every mutating method returns
43 a new :class:`VectorClock` rather than modifying ``self``. This makes
44 clocks safe to store as dict values without defensive copying.
45
46 Lattice laws satisfied by :meth:`merge`:
47 - **Commutativity**: ``merge(a, b) == merge(b, a)``
48 - **Associativity**: ``merge(merge(a, b), c) == merge(a, merge(b, c))``
49 - **Idempotency**: ``merge(a, a) == a``
50 """
51
52 def __init__(self, counts: _CountMap | None = None) -> None:
53 """Create a vector clock, optionally pre-populated from *counts*.
54
55 Args:
56 counts: Initial ``{agent_id: count}`` mapping. Copied defensively.
57 """
58 self._counts: _CountMap = dict(counts) if counts else {}
59
60 # ------------------------------------------------------------------
61 # Mutation (returns new clock)
62 # ------------------------------------------------------------------
63
64 def increment(self, agent_id: str) -> VectorClock:
65 """Return a new clock with ``agent_id``'s counter incremented by 1.
66
67 Args:
68 agent_id: The agent performing an event.
69
70 Returns:
71 A new :class:`VectorClock` with the updated count.
72 """
73 new_counts = dict(self._counts)
74 new_counts[agent_id] = new_counts.get(agent_id, 0) + 1
75 return VectorClock(new_counts)
76
77 def merge(self, other: VectorClock) -> VectorClock:
78 """Return the least-upper-bound of ``self`` and *other*.
79
80 For each agent, the result holds the *maximum* count seen in either
81 clock. This is the lattice join operation; it satisfies
82 commutativity, associativity, and idempotency.
83
84 Args:
85 other: The clock to merge with.
86
87 Returns:
88 A new :class:`VectorClock` holding per-agent maximums.
89 """
90 all_agents = set(self._counts) | set(other._counts)
91 merged = {
92 agent: max(self._counts.get(agent, 0), other._counts.get(agent, 0))
93 for agent in all_agents
94 }
95 return VectorClock(merged)
96
97 # ------------------------------------------------------------------
98 # Comparison
99 # ------------------------------------------------------------------
100
101 def happens_before(self, other: VectorClock) -> bool:
102 """Return ``True`` if ``self`` causally precedes *other*.
103
104 ``a`` happens before ``b`` iff every agent counter in ``a`` is
105 ≤ the corresponding counter in ``b``, and at least one counter is
106 strictly less (i.e. ``a != b``).
107
108 Args:
109 other: The clock to compare against.
110
111 Returns:
112 ``True`` when ``self < other`` in causal order.
113 """
114 all_agents = set(self._counts) | set(other._counts)
115 leq = all(
116 self._counts.get(agent, 0) <= other._counts.get(agent, 0)
117 for agent in all_agents
118 )
119 return leq and not self.equivalent(other)
120
121 def concurrent_with(self, other: VectorClock) -> bool:
122 """Return ``True`` if neither clock causally precedes the other.
123
124 Two clocks are concurrent when each has at least one counter strictly
125 greater than the other's corresponding counter. This is the condition
126 that a CRDT ``join`` must handle: there is no causal order between the
127 two writes, so neither can be simply discarded.
128
129 Args:
130 other: The clock to compare against.
131
132 Returns:
133 ``True`` when ``self`` and *other* are incomparable.
134 """
135 return not self.happens_before(other) and not other.happens_before(self) and not self.equivalent(other)
136
137 # ------------------------------------------------------------------
138 # Serialisation
139 # ------------------------------------------------------------------
140
141 def to_dict(self) -> _CountMap:
142 """Return a JSON-serialisable ``{agent_id: count}`` mapping.
143
144 Returns:
145 A shallow copy of the internal counts dictionary.
146 """
147 return dict(self._counts)
148
149 @classmethod
150 def from_dict(cls, data: _CountMap) -> VectorClock:
151 """Reconstruct a :class:`VectorClock` from its wire representation.
152
153 Args:
154 data: ``{agent_id: count}`` mapping as produced by :meth:`to_dict`.
155
156 Returns:
157 A new :class:`VectorClock` with the given counts.
158 """
159 return cls(data)
160
161 # ------------------------------------------------------------------
162 # Python dunder helpers
163 # ------------------------------------------------------------------
164
165 def equivalent(self, other: VectorClock) -> bool:
166 """Return ``True`` if both clocks represent identical causal state.
167
168 Two clocks are equivalent when every agent's count is the same in both,
169 treating absent agents as count 0. This is a stricter check than
170 ``happens_before`` — it requires exact equality, not domination.
171
172 Args:
173 other: The vector clock to compare against.
174
175 Returns:
176 ``True`` when ``self`` and *other* are causally identical.
177 """
178 all_agents = set(self._counts) | set(other._counts)
179 return all(
180 self._counts.get(a, 0) == other._counts.get(a, 0)
181 for a in all_agents
182 )
183
184 def __repr__(self) -> str:
185 return f"VectorClock({self._counts!r})"
File History 1 commit
sha256:06dba78c2a78e251b580422dd1fd547f3c8357ff18f7709a860873b2d24dbbbf chore: bump version to 0.2.0rc14 Sonnet 4.6 patch 2 days ago