Vector Clocks
Eine Vector Clock ist ein logischer Zeitstempel, der über Replicas verteilt ist. Jede Replica unterhält einen Per-Replica-Counter; jedes persistierte Event enthält die aktuelle Vector Clock zum Schreibzeitpunkt.
Gegeben zwei Vector Clocks a und b:
| Relation | Wann |
|---|---|
a < b (“a happens-before b”) | Jede Komponente a[i] ≤ b[i] und mindestens eine strikt kleiner. |
a == b | Jede Komponente gleich. |
a nebenläufig zu b | Weder < noch >. Einige Komponenten haben a > b, andere a < b. |
Der dritte Fall — nebenläufig — ist das, was die Replicated-ES-Maschinerie verwendet, um zu erkennen “zwei Replicas haben unabhängig geschrieben, und keine hat den Write der anderen gesehen.”
Die Datenform
Abschnitt betitelt „Die Datenform“type VectorClockData = Readonly<Record<ReplicaId, number>>;Ein plain Object. Replicas, die nicht vorhanden sind, werden als
0 behandelt — nützlich für Kompaktheit auf dem Wire (Komponenten
überspringen, die null sind).
Die VectorClock-Klasse des Frameworks wickelt das ein:
import { VectorClock } from 'actor-ts/persistence';
let vc = VectorClock.empty();vc = vc.tick('replica-a'); // → { 'replica-a': 1 }vc = vc.tick('replica-a'); // → { 'replica-a': 2 }vc = vc.merge(VectorClock.fromData({ 'replica-b': 3 }));// → { 'replica-a': 2, 'replica-b': 3 }Operationen
Abschnitt betitelt „Operationen“| Methode | Zweck |
|---|---|
empty() | Null-Clock. |
fromData(record) | Aus einem plain Object bauen. |
get(replicaId) | Eine Komponente lesen. |
tick(replicaId) | Die Komponente einer Replica erhöhen — beim Persistieren aufgerufen. |
merge(other) | Per-Replica-Maximum. |
compareTo(other) | Gibt 'before' / 'after' / 'equal' / 'concurrent' zurück. |
toJSON() | Plain-Object-Form für die Serialisierung. |
fromData vertraut seinem Argument: es ist der Deserializer und nimmt
daher an, dass das Record ein Record aus Zahlen ist. Für eine Clock aus
dem eigenen Snapshot ist das in Ordnung, für eine vom Wire nicht —
darum prüft ReplicatedEventSourcedActor eine von einem Peer gelieferte
Clock, bevor er sie mergt: ein plain Object, keine Arrays, eine
begrenzte Anzahl Einträge, jede Komponente eine endliche, nicht-negative
Zahl. Eine Clock, die durchfällt, wird mit dem Envelope verworfen, der
sie trägt; siehe
das Trust-Modell.
Wer fromData selbst auf ungeprüfte Eingaben anwendet, besitzt diese
Prüfung.
Wie Replicated ES sie verwendet
Abschnitt betitelt „Wie Replicated ES sie verwendet“Replica A schreibt event_A1 bei vc { A: 1 } ← A's Counter erhöht sichReplica B (ohne Vorwissen über A's Event): schreibt event_B1 bei vc { B: 1 } ← B's Counter erhöht sich
Beide Events landen im geteilten Journal.
Später liest Replica A das Event von B: vc { B: 1 } verglichen mit A's aktuellem { A: 1 }: → nebenläufig (A > B für A's Komponente, B > A für B's Komponente) → Conflict Resolver läuft, um State aus beiden Branches zu mergenDas Framework:
- Inkrementiert bei jedem Persist die Komponente dieser Replica.
- Bettet die volle Vector Clock in das persistierte Event ein.
- Beim Lesen des Events einer anderen Replica vergleicht die eingebettete Vector Clock mit der Vector Clock des lokalen States.
- Wenn nebenläufig → den Conflict Resolver aufrufen.
- Wenn
<(kausal vorher) → bereits angewendet oder strikt älter; ignorieren (oder normal in den State falten). - Wenn
>(kausal später) → auf den State anwenden.
Ein durchgespieltes Beispiel
Abschnitt betitelt „Ein durchgespieltes Beispiel“t1: Replica A persistiert event_A1 bei vc { A: 1 } state_A = applyEvent(initial, event_A1) state_A's vc = { A: 1 }
t2: Replica B persistiert event_B1 bei vc { B: 1 } ← nebenläufig state_B = applyEvent(initial, event_B1) state_B's vc = { B: 1 }
t3: Replica A liest event_B1. Vergleich von event_B1's vc { B: 1 } mit state_A's vc { A: 1 }: → nebenläufig Resolver aufrufen: merged_state = resolver.resolve(state_A, [event_A1, event_B1]) state_A = merged_state state_A's vc = { A: 1, B: 1 } (der Merge)
t4: Replica A persistiert event_A2 bei vc { A: 2, B: 1 } state_A = applyEvent(merged_state, event_A2)
t5: Replica B sieht schließlich die gesamte Kette — die eigene, A's Pre-Merge-Events, plus A's Post-Merge-Events. Löst alle nebenläufigen Branches auf dieselbe Weise auf.Schließlich konvergieren beide Replicas — bei ausreichender Cross-Replica-Sichtbarkeit und einem deterministischen Resolver.
Kausalität ist partiell geordnet
Abschnitt betitelt „Kausalität ist partiell geordnet“Vector Clocks geben keine Totalordnung. Zwei Events bei
{ A: 5 } und { B: 5 } sind nebenläufig — keines ist “zuerst
passiert.” Die Reihenfolge, in der sie im Journal erscheinen, ist
zufällig.
Das ist der ganze Punkt — Replicas schreiben unabhängig; Clocks verfolgen Kausalität, nicht Wall-Clock-Zeit.
Für eine Totalordnung (selten) paare es mit einem Single-Writer- Lease (siehe Single-Writer-Lease).
Wire-Format
Abschnitt betitelt „Wire-Format“Vector Clocks serialisieren als plain JSON-Objekte:
{ "_vc": { "A": 5, "B": 3, "C": 1 }}Null-Wert-Komponenten werden typischerweise für Kompaktheit
weggelassen; das Framework behandelt fehlende Keys als 0.
Garbage Collection
Abschnitt betitelt „Garbage Collection“Über die Zeit hinterlassen pensionierte Replicas (dekommissionierte Regionen, entfernte Nodes) Einträge in Vector Clocks:
vc { A: 100, B: 50, C: 25, RETIRED-D: 7 }Die Komponente der pensionierten Replica ist irrelevant, nimmt aber weiterhin Platz ein. Vector-Clock-Garbage-Collection ist für v1 außerhalb des Scopes — das Framework entfernt diese Einträge nie, sodass eine Clock mit jeder jemals gesehenen Replica wächst.
Das ist für ein stabiles Cluster in Ordnung. Ein Deployment mit viel Node-Churn wird irgendwann Kompaktierung wollen, doch das ist künftige Arbeit und heute kein unterstütztes Feature.
Mit Vector Clocks in User-Code arbeiten
Abschnitt betitelt „Mit Vector Clocks in User-Code arbeiten“Tust du normalerweise nicht. Das Framework verwaltet sie intern; dein Conflict Resolver erhält Events mit ihren Vector Clocks bereits verglichen — du entscheidest nur, wie der State angesichts der konfligierenden Events gemergt wird.
Für Diagnostik gilt: Die aktuelle Clock des Actors wird privat
gehalten und ist nicht über einen öffentlichen Getter
zugänglich. Sie wird mit jedem persistierten Event und
Snapshot mitgeführt, inspiziere sie also im Journal- /
Snapshot-JSON — dem _vc-Feld, das oben unter Wire-Format
gezeigt wird — statt sie vom Actor abzulesen.
Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- Replicated Event Sourcing im Überblick — das größere Bild.
- Conflict Resolver — was die Nebenläufigkeitserkennung konsumiert.
- Snapshotting — Vector Clocks in Snapshots.
