From f89feed8b7e77b826298b52097c4cd4d2ba34aff Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 16 Dec 2025 18:50:59 +0100 Subject: [PATCH] fix(docs): correct late-joiner replay section to describe UUIDv7 filtering The documentation incorrectly described "vector clock dominance" filtering, but the actual implementation uses UUIDv7 timestamp comparison. Updated Section 2c to accurately reflect the implementation and explain why UUIDv7 is used (SYNC_IMPORT operations break vector clock causality detection). --- .../operation-log-architecture-diagrams.md | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/app/core/persistence/operation-log/docs/operation-log-architecture-diagrams.md b/src/app/core/persistence/operation-log/docs/operation-log-architecture-diagrams.md index 1eb4b1d39c..4164f26e29 100644 --- a/src/app/core/persistence/operation-log/docs/operation-log-architecture-diagrams.md +++ b/src/app/core/persistence/operation-log/docs/operation-log-architecture-diagrams.md @@ -411,9 +411,9 @@ flowchart TB 2. **Efficiency**: Snapshot endpoint is designed for large payloads and stores state directly 3. **Server-Side Handling**: Server creates a synthetic operation record for audit purposes -## 2c. Late-Joiner Replay with Vector Clock Dominance ✅ IMPLEMENTED +## 2c. Late-Joiner Replay with UUIDv7 Timestamp Filter ✅ IMPLEMENTED -When a client receives a SYNC_IMPORT (full state from another client), it must replay any local synced operations that happened "after" the import's vector clock. This ensures local work isn't lost when receiving a full state snapshot. +When a client receives a SYNC_IMPORT (full state from another client), it must replay any local synced operations created **after** the import (by UUIDv7 timestamp). This ensures local work isn't lost when receiving a full state snapshot. **Implementation Status:** Complete. See `OperationLogSyncService._replayLocalSyncedOpsAfterImport()`. @@ -442,35 +442,35 @@ sequenceDiagram Note over B: Problem: Op3, Op4 were already synced!
If we just apply SYNC_IMPORT, we lose B's work ``` -### The Solution: Vector Clock Dominance Filter +### The Solution: UUIDv7 Timestamp Filter -Before replaying local synced ops after a SYNC_IMPORT, we filter out ops that are "dominated" by the SYNC_IMPORT's vector clock. An op is dominated if its vector clock is `LESS_THAN` the SYNC_IMPORT's clock - meaning the op's state is already captured in the imported snapshot. +Before replaying local synced ops after a SYNC_IMPORT, we filter out ops that were created **before** the SYNC_IMPORT (by comparing UUIDv7 IDs, which embed timestamps). Ops created before the import are already captured in the imported snapshot. ```mermaid flowchart TD subgraph Input["SYNC_IMPORT Received"] - SI["SYNC_IMPORT
vectorClock: A=10, B=5"] + SI["SYNC_IMPORT
id: 019abc... (T=1702900000)"] end subgraph LocalOps["Local Synced Operations"] - Op1["Op1: B=1
LESS_THAN → dominated"] - Op2["Op2: A=5, B=3
LESS_THAN → dominated"] - Op3["Op3: B=6
GREATER_THAN → NOT dominated"] - Op4["Op4: A=10, B=5, C=1
CONCURRENT → NOT dominated"] + Op1["Op1: id=019aa...
T=1702890000 → BEFORE"] + Op2["Op2: id=019ab...
T=1702895000 → BEFORE"] + Op3["Op3: id=019abd...
T=1702905000 → AFTER"] + Op4["Op4: id=019abe...
T=1702910000 → AFTER"] end - subgraph Filter["Vector Clock Comparison"] - Check["Compare each op's clock
with SYNC_IMPORT clock"] + subgraph Filter["UUIDv7 Timestamp Comparison"] + Check["Compare each op.id
with SYNC_IMPORT.id"] end subgraph Result["Ops to Replay"] - Replay["Only Op3 and Op4
not dominated"] + Replay["Only Op3 and Op4
created after import"] end SI --> Check LocalOps --> Check - Check --> |"LESS_THAN"| Skip["Skip - already in snapshot"] - Check --> |"Otherwise"| Replay + Check --> |"op.id < import.id"| Skip["Skip - created before import"] + Check --> |"op.id >= import.id"| Replay style Op1 fill:#ffcdd2,stroke:#c62828 style Op2 fill:#ffcdd2,stroke:#c62828 @@ -480,14 +480,12 @@ flowchart TD style Replay fill:#e8f5e9,stroke:#2e7d32 ``` -### Vector Clock Comparison Results +### UUIDv7 Comparison Results -| Comparison | Meaning | Action | -| -------------- | ------------------------------ | ---------------------------------- | -| `LESS_THAN` | Op happened-before SYNC_IMPORT | Skip (state already captured) | -| `EQUAL` | Same causal history | Replay (edge case, safe to replay) | -| `GREATER_THAN` | Op happened-after SYNC_IMPORT | Replay (newer than snapshot) | -| `CONCURRENT` | Independent changes | Replay (may have unique changes) | +| Comparison | Meaning | Action | +| -------------------- | -------------------------------- | ------------------------------------- | +| `op.id < import.id` | Op created before SYNC_IMPORT | Skip (state already captured) | +| `op.id >= import.id` | Op created at same time or after | Replay (newer than or equal snapshot) | ### Implementation Details @@ -502,10 +500,12 @@ const localSyncedOps = allEntries.filter((entry) => { if (entry.op.opType === OpType.SyncImport || entry.op.opType === OpType.BackupImport) return false; - // Must NOT be dominated by the SYNC_IMPORT's vector clock - const comparison = compareVectorClocks(entry.op.vectorClock, syncImportClock); - if (comparison === VectorClockComparison.LESS_THAN) { - return false; // Skip - state already captured in SYNC_IMPORT + // Must be created AFTER the SYNC_IMPORT (by UUIDv7 timestamp) + // NOTE: We use UUIDv7 comparison instead of vector clock comparison + // because imports with isForceConflict=true create a fresh vector clock + // with a new client ID, breaking vector clock causality detection. + if (entry.op.id < syncImportOp.id) { + return false; // Skip - created before SYNC_IMPORT } return true; }); @@ -515,8 +515,12 @@ const localSyncedOps = allEntries.filter((entry) => { - Only filters local ops (created by this client) - Only considers synced ops (accepted by server) -- Uses vector clock comparison to determine dominance -- `LESS_THAN` means dominated (skip), all other results mean not dominated (replay) +- Uses **UUIDv7 ID comparison** (not vector clocks) to determine temporal order +- Ops with ID < import ID are skipped (created before import) + +**Why UUIDv7 Instead of Vector Clocks?** + +SYNC_IMPORT operations with `isForceConflict=true` create a fresh vector clock with a new client ID. This breaks vector clock causality detection because the import's clock has no causal relationship with existing local operations. UUIDv7 IDs embed millisecond-precision timestamps, providing reliable "created before/after" comparison that works regardless of vector clock semantics. ---