Files
Jon Chery 29ffb42898 docs(milestone): complete v0.4 — Edge + Messaging + Language-Derived Docs
---ci---
project: atelier
phase: 0
milestone: v0.4
status: complete
requirements:
  covered: [ATELIER-92, ATELIER-93, ATELIER-94, ATELIER-95, ATELIER-96, ATELIER-97, ATELIER-98, ATELIER-99, ATELIER-100, ATELIER-101, ATELIER-102, ATELIER-103, ATELIER-104, ATELIER-105, ATELIER-106, ATELIER-107, ATELIER-108, ATELIER-109, ATELIER-110, ATELIER-111, ATELIER-112, ATELIER-113, ATELIER-114, ATELIER-115, ATELIER-116, ATELIER-117]
  partial: []
---/ci---
2026-08-05 16:23:15 +00:00

158 lines
6.9 KiB
Markdown

# Bad Example: Edge Sync Loop (Livelock)
> A two-node sync that oscillates divergent state forever. This is
> the pre-specified `edge-sync-loop` chaos anti-pattern (ATELIER-110).
> **Single-breach per D-068:** the principle breached is **Edge P4
> (Sync Conflicts are Bounded, Not Infinite)**. The prose explains
> why this is a livelock, not eventual consistency.
## The Code
```typescript
// Two edge nodes (a tablet and a dispatcher workstation) sync a
// work-order status. The merge uses wall-clock timestamps with no
// vector clock and no deterministic tiebreak. A clock skew flips
// the winner on every merge pass; the state oscillates forever.
interface WorkOrderState {
orderId: string;
status: string;
wallClock: number; // P4 VIOLATION: wall time, not monotonic
nodeId: string;
}
// P4 VIOLATION: the merge picks the later wall-clock write as the
// winner. Wall time skews across nodes; a skew of even a few
// milliseconds flips the winner. With no vector clock, concurrent
// writes are not detected; with no deterministic tiebreak, equal
// timestamps are resolved by whichever node's clock is ahead.
function mergeBad(local: WorkOrderState, remote: WorkOrderState): WorkOrderState {
// No vector clock. No monotonic logical clock. No deterministic
// tiebreak by nodeId. This is wall-clock-only LWW on durable
// state — the row in the CRDT-vs-LWW decision matrix that is a
// P4 violation for anything but ephemeral state.
if (local.wallClock >= remote.wallClock) return local;
return remote;
}
// The sync loop: each node merges the other's state, writes the
// result, and the next sync pass flips it back. The loop never
// terminates.
async function syncLoop(node: "tablet" | "dispatcher", peer: WorkOrderState) {
let local = store.get(peer.orderId);
while (true) {
const merged = mergeBad(local, peer);
store.set(merged.orderId, merged);
await pushToPeer(merged); // peer receives, merges, pushes back
peer = await pullFromPeer(); // peer's clock is now ahead — flips winner
local = store.get(peer.orderId); // local re-merges; flips again
// The loop runs forever. Convergence is never reached. This is
// a livelock: the system is making progress (each pass writes)
// but the state never converges.
}
}
```
The tablet sets `status: "completed"` at wall time `10:00:00.500`.
The dispatcher's clock is 50ms ahead; it sets `status: "reassigned"`
at wall time `10:00:00.550`. The first merge: dispatcher wins
(`10:00:00.550 > 10:00:00.500`). The tablet receives `reassigned`,
but its clock drifts ahead by 100ms during the next sync pass; it
writes `completed` at `10:00:00.650`. The second merge: tablet
wins. The dispatcher's clock drifts ahead again; it writes
`reassigned` at `10:00:00.750`. The third merge: dispatcher wins.
The state flips between `completed` and `reassigned` on every sync
pass. The loop runs forever.
## Why It Violates
### Sync Conflicts are Bounded, Not Infinite (Edge P4, C1, C5)
- **The breach:** the merge has no convergence bound. The CRDT-vs-
LWW decision matrix in `domains/edge/sync.md` is explicit:
wall-clock-only LWW on durable state carries an **unbounded
failure mode** — clock skew = oscillation — and is a P4
violation for anything but ephemeral state. This code is the
matrix's failure column made real.
- **Why it is a livelock, not eventual consistency:** eventual
consistency guarantees that, in the absence of new writes, all
replicas eventually converge. This system never converges even
with no new writes: the clock skew alone drives the oscillation.
Each sync pass writes (so the system is "busy"), but the state
never settles — the definition of a livelock. The P4 contract is
that the merge terminates and converges; this merge does neither.
- The missing pieces, per the decision matrix:
- **No monotonic logical clock** (a hybrid logical clock or
vector clock) — wall time skews, and the skew is unbounded.
- **No vector clock** — concurrent writes are not detected, so
the conflict is invisible; the merge silently flips instead of
surfacing.
- **No deterministic tiebreak** (e.g., `nodeId`) on equal
timestamps — equal wall times are resolved by whichever node's
clock is ahead, which is not a stable property.
- The P4 bound requires that the merge function be associative,
commutative, and idempotent, terminating in one pass. A
wall-clock merge with clock skew satisfies none of these: it is
not associative (order of merges flips the winner), not
idempotent (a re-merge after a clock drift flips the result),
and not terminating (the loop runs forever).
## The Fix
```typescript
// Fix: replace wall-clock LWW with a CRDT register (state-based
// CvRDT) using a monotonic logical clock and a deterministic
// tiebreak by nodeId. The merge is now a total order — no
// oscillation (P4 bounded). See the CRDT-vs-LWW decision matrix:
// for a register on an unreliable network, a state-based CRDT is
// the correct row.
interface LWWRegister<T> {
value: T;
timestamp: number; // monotonic logical clock (HLC), not wall time
nodeId: string; // deterministic tiebreak
}
function mergeLWWRegister<T>(
local: LWWRegister<T>,
remote: LWWRegister<T>,
): LWWRegister<T> {
// Associative, commutative, idempotent (P4 bound, P5 idempotent).
// (timestamp, nodeId) is a total order — convergence in one pass.
if (local.timestamp > remote.timestamp) return local;
if (local.timestamp < remote.timestamp) return remote;
return local.nodeId > remote.nodeId ? local : remote;
}
// The sync is now one pass: merge, write, done. No loop, no
// oscillation. A re-merge of the same two replicas yields the same
// result (P5 idempotent), so a retried sync is safe.
async function syncOnce(local: LWWRegister<unknown>, remote: LWWRegister<unknown>) {
const converged = mergeLWWRegister(local, remote);
store.set(converged);
await pushToPeer(converged);
// Done. No while(true). The merge terminates.
}
```
The fix selects the CRDT row of the decision matrix (a register on
an unreliable network): a monotonic logical clock (HLC) eliminates
clock skew; a deterministic tiebreak by `nodeId` eliminates the
equal-timestamp flip. The merge is now a total order that converges
in one pass — the P4 bound. See `examples/good/edge-offline-sync.md`
for the full good-example version of this pattern.
## Cross-Domain Links
- `domains/edge/sync.md` — the CRDT-vs-LWW decision matrix; the
wall-clock-only LWW row is the failure mode this example
instantiates; the CRDT row is the fix.
- `domains/edge/first-principles.md` — P4 (Sync Conflicts are
Bounded, Not Infinite) is the principle breached.
- `review/anti-patterns.md` — the `edge-sync-loop` chaos
anti-pattern (edge P4, C1, C5 — infinite oscillation is a
correctness failure, not eventual consistency; the sync is a
livelock).
- `examples/good/edge-offline-sync.md` — the good-example version
of this pattern: a CRDT register merge that converges in one
pass.