WAL Design - Causal+ Ready (with Performance Notes)
This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at wal-causal-plus.md, and llms.txt lists every page.Status: partially shipped, and not everywhere in the shape sections 3-8 sketch. Read those sections as the design rationale; what ships today is:
- Entry schema (section 1) - live.
WalRecordcarries the two additive[Id]slotsVectorClockandDependencySummary(today both hold the same frontier), stamped on the commit path from the ambientLatticeVectorClockContext, plus a diagnostic minor-version bump onReplicationBatchEnvelope(alias andWireVersionunchanged, so legacy peers decode both slots asnull). Records carry the absolute frontier: an internal delta codec for vector clocks ships, but no shipped write or wire path uses it.- Receiver-side dependency check (sections 3-6) - shipped in the replication apply pipeline, per tree rather than per shard. An entry whose frontier names a clock the receiver's local vector clock has not reached (ignoring the entry's own origin and the receiver's own cluster) is parked in a bounded per-tree buffer (
CausalBufferMaxEntries/CausalBufferMaxBytes, overflow routed to the dead-letter queue) and retried as later applies advance the local clock. Unlike section 6, the per-origin high-water mark is not a duplicate-drop gate for steady-state writes: re-delivery is filtered by a snapshot-pinned causal floor, a shadow-forward identity cache, and per-key last-writer-wins idempotence. See Replication apply.- Causal-stable GC clause (section 7) - implemented in the core WAL GC: once any consumer reports a vector frontier through the vector overload of
IWalCursorRegistry.ReportCursorAsync, an entry is trimmed only if the pointwise-minimum frontier dominates itsVectorClock. The clause is AND-ed with an entitlement clause (the consumer HLC cursor, the retention TTL, or the durable materialiser offset floor) and a blocked-floor clause. No shipped consumer reports a vector frontier today, so in a default deployment the clause is inert.- Snapshot cut-point (section 8) - the replication snapshot provider cuts at the causal-stable frontier when one exists and otherwise at the producer's local vector clock; the receiver pins that frontier during the bootstrap handoff.
Companion to
../lattice.replication/wal.md(the replication-side partitioned WAL overlay) andwal.md(the cross-cutting WAL contract).
This document defines the causal+-ready Write-Ahead Log (WAL) for Orleans.Lattice. It extends the existing WAL design without breaking any of its invariants:
- Append-then-apply remains the commit point.
- Per-shard monotonic offsets remain the ordering backbone.
- Origin stamping remains the cycle-break mechanism.
- Replay is still strictly ordered by WAL offset.
Causal+ introduces additional metadata and apply-time rules, but does not alter the WAL's durability or ordering semantics.
1. WAL entry schema
Each WAL entry is extended to carry causal metadata required for causal+ consistency.
1.1 Existing fields (unchanged)
TreeIdShardIndexKeyOp(MutationKind: Set / Delete / DeleteRange, plus theTxCommit/TxAbortsaga terminal marks and theTombstonecompaction reap mark)ValueandDelta- two distinct slots, not one combined field:Valuecarries a full value,Deltacarries a typed CRDT delta, and a given entry populates whichever its merge mode calls for.OriginClusterIdTimestamp(Hybrid Logical Clock)Mode(LatticeMergeMode)- The remaining envelope slots, which causal+ does not touch:
EndExclusiveKey,IsTombstone,ExpiresAtTicks,TransactionId,IsPrepared,AtomicShardCount,IsMerge,IsBackstop,Category,MatchedKeys,CrossTreeOperationIdandCrossTreeParticipants(theAtomicBatchSize/AtomicBatchIndexpair is described in 1.2).
The per-shard monotonic offset is not a field on the entry. It is assigned by the owning WAL shard as the entry is appended and travels alongside it, which is why replay can be ordered by offset without the offset being part of the entry's wire shape.
1.2 New fields (added for causal+)
VectorClock
A per-origin vector clock representing the full causal frontier at commit time.
- Sparse map:
{ origin → hlc }. - Designed to be encoded compactly (delta-encoded relative to the previous entry on the same shard); the shipped record carries the absolute frontier (see the status note above).
- Backwards compatible: a missing field decodes as
null, which receivers treat as the empty frontier.
Performance note (avoid Mistake 1 - full VC bloat):
- Store vector clocks per shard as a baseline, and encode each entry's VC as a delta from the previous entry.
- Use a compact representation (e.g. sorted
(origin, hlc)pairs with varint encoding). - Do not re-emit the full VC for every entry when only one origin advanced.
- GC-safety rule: every entry whose predecessor on the same shard was trimmed by GC, and the first entry of every shipped batch, must carry an absolute VC (not a delta). Intermediate entries may delta-encode safely. This preserves the trim-from-the-head invariant in §7 without requiring GC to rewrite surviving entries.
DependencySummary
A compact representation of the causal predecessors of this entry.
- Shape 1: the vector clock itself.
- Shape 2 (optional future): a compact hash / Bloom filter of predecessor HLCs.
Performance note:
- Start with "VC as dependency summary"; only introduce Bloom filters if real-world pressure demands it.
- Keep this field read-only and immutable to avoid recomputation.
AtomicBatchSize / AtomicBatchIndex
Two purely additive [Id] slots on WalRecord (and the corresponding LatticeMutation slots on the observer-side surface) that carry the size and zero-based index of the enclosing atomic transaction.
AtomicBatchSize- total number of entries in the enclosing atomic transaction.0for non-atomic single-key writes and non-atomic batches;Non every per-key prepared emit produced by aSetManyAtomicAsyncsaga of sizeN. An aborted saga emits no per-key rollback writes, so there are no compensation emits to stamp (see Atomic writes).AtomicBatchIndex- zero-based position of this entry within the enclosing batch;0for non-atomic writes. Within a batch the index covers0..Size-1exactly once each, taken from the entry's position in the batch the saga was given, whichever shard the entry routes to.- Sibling membership is keyed by the existing
TransactionIdslot, but batch completeness is not detected by counting siblings. The saga appends a separate per-shardTxCommit/TxAbortterminal record after its prepared writes, and a receiver tallies those terminals against the stampedAtomicShardCountbefore it flips the batch visible - see Atomic writes: Multi-shard receiver gate. - Strictly additive on the wire: legacy peers and entries authored before these slots existed decode both fields as
0. Shipped paths do consume them: the replication receiver (a prepared write with a non-zero batch size bypasses the snapshot-floor dedup and the causal-dependency park, and a range delete carrying batch metadata is rejected), the replication anti-entropy leaf re-replay (which ships a batch as one unit), and the materialised-view maintainer (which flushes a staged batch only once its prepares reach the declared size and its terminals have arrived).
Performance note:
- Two
intfields per entry, packed by the Orleans serializer; storage cost is negligible relative to the existingOriginClusterIdand HLC slots. - The observer pass-through is two struct-field copies on the commit-time hot path; no allocation, no resolver lookup, no decision branch.
- The slots are unconditional on every emit so a peer flipping atomic-batch delivery on does not require a producer restart or any wire-format change.
2. WAL append semantics (unchanged)
The WAL remains the commit point:
append(entry);
persist(entry);
apply_to_local_state(entry);
Causal+ does not change:
- when entries are appended,
- how entries are persisted,
- how offsets advance,
- or how failures propagate.
Performance note:
- All causal+ logic is off the commit path.
- Do not introduce dependency checks or buffering into the append path.
3. WAL replay semantics (extended for causal+)
Replay remains strictly ordered by WAL offset, but apply is now conditional.
3.1 Replay loop (new semantics)
for entry in WAL in offset order:
if is_duplicate(entry): // HWM check
continue;
if dependencies_satisfied(entry): // VC check
apply(entry);
advance_local_vector_clock(entry);
wake_buffered_entries();
else:
buffer(entry);
Performance note (avoid Mistake 2 - global locks):
- This loop runs per shard, inside the shard's single-threaded grain.
- Do not introduce cross-shard locks or global coordination here.
- All state (
local_vector_clock, buffers) is shard-local and single-threaded.
4. Dependency satisfaction
4.1 Definition
An entry E is safe to apply when:
∀ origin in E.VectorClock:
local_vector_clock[origin] ≥ E.VectorClock[origin]
This ensures:
- If B depends on A, B cannot apply before A.
- If B arrives before A, B is buffered.
- If A arrives later, A applies, then B becomes eligible.
Performance note (avoid Mistake 4 - recomputing closure):
- Maintain
local_vector_clockincrementally; never recompute from WAL history. - The dependency check is just a map lookup + integer comparison per origin.
- Keep
local_vector_clockin memory and persist it alongside the HWM.
4.2 Buffering
Each shard maintains:
- a ready queue (dependencies satisfied),
- a blocked queue (dependencies missing).
Blocked entries are keyed by:
- origin,
- missing predecessor clocks.
When local clocks advance, blocked entries are re-evaluated.
Performance note (avoid Mistake 3 - unbounded buffering):
- Enforce a per-shard buffer size cap (e.g. max N blocked entries or max M bytes).
- When the cap is hit, either:
- apply backpressure to the transport (slow down or temporarily stop pulling), or
- park the oldest blocked entries via the existing dead-letter pipeline with explicit metrics + alerts (never silently drop).
- Use simple data structures: per-origin lists or small priority queues; avoid complex global heaps.
5. Local vector clock
Each shard maintains a local vector clock:
local_vc[origin] = highest applied HLC from that origin
This is the natural generalisation of the existing per-origin high-water-mark table - the HWM is the diagonal of the local VC, keyed by the entry's own origin only. Causal+ widens the check to consult every origin in the entry's VC, not just the authoring origin.
Updated only after a successful apply. Persisted in the same grain state as the HWM table.
Performance note:
- Keep
local_vcin a small dictionary keyed by origin string or interned id. - Origins are few (clusters), so this is tiny.
- Persist
local_vcas part of the shard's metadata; no extra grain needed.
6. Interaction with high-water marks
The existing HWM table remains valid.
Order of checks:
if entry.HLC <= HWM[origin]:
// duplicate, no work
return;
if dependencies_satisfied(entry):
apply(entry);
advance_local_vector_clock(entry);
advance_HWM(entry);
wake_buffered_entries();
else:
buffer(entry);
Performance note:
- The HWM check is O(1) and remains the first gate.
- This avoids unnecessary dependency checks for duplicates.
7. Causal-stable frontier
The WAL GC predicate is extended.
7.1 Old predicate
min_acked_offset_per_peer
7.2 New predicate
causal_stable = min_over_all_peers(peer_vector_clock)
An entry is GC-eligible when:
- its vector clock ≤
causal_stable, and - its WAL offset ≤
min_acked_offset.
Performance note:
- The number of peers and origins is small; computing
min_over_all_peersis cheap. - Cache the computed
causal_stablefrontier and recompute only on ack updates, not per entry.
The predicate must consult every change-feed consumer's VC - including any future local materialiser - not just remote peers. A lagging consumer must pin the log identically regardless of whether it is remote or in-process.
7.3 Shipped form
The shipped GC implements the causal-stable clause, but not the peer-offset predicate of 7.1: replication peers pin the log by HLC cursor, and the only offset floor it applies is the durable materialiser (leaf checkpoint) floor. It walks each WAL partition from the head and trims the longest prefix whose entries are all eligible, where an entry is eligible only when it passes three independent clauses:
- Entitlement - the minimum consumer HLC cursor has passed the entry, or the entry is older than the configured retention TTL, or the durable materialiser offset floor admits it (that floor can also refuse an entry only the in-memory cursor would admit). Separately, the scan stops at the first entry above the partition's durable materialiser offset floor.
- Causal-stable - when any consumer has reported a vector frontier, the pointwise minimum of the reported frontiers dominates the entry's
VectorClock(an entry with anullfrontier always passes). Consumers that report only an HLC cursor still pin the log through the entitlement clause but are left out of the frontier minimum, and no shipped consumer reports a frontier today, so this clause is inert in a default deployment. - Blocked floor - the entry's HLC is strictly below the lowest buffer pin any consumer reports.
The cursor registry caches the frontier and the blocked floor and recomputes them only after a consumer's report changes the registry or a consumer unregisters; each GC pass samples them once rather than per entry.
8. Snapshot semantics
Snapshots must be taken at a causal-stable frontier.
8.1 Snapshot cut-point
snapshot_frontier = causal_stable
8.2 Snapshot contents
Include all entries whose vector clocks are ≤ snapshot_frontier.
8.3 Incremental catch-up
Incremental replication begins at:
start = snapshot_frontier
The receiver pins both the as-of HLC and the snapshot's VC frontier in its persistent metadata, then resumes incremental delivery from that VC. The dependency check in §4 runs from the pinned frontier on the first incremental entry - exactly-once across the snapshot/incremental boundary.
Performance note:
- Snapshot cost is dominated by state scan, not causal metadata.
- Causal+ only changes the cut-point, not the scan algorithm.
9. Transport requirements (WAL-driven)
The WAL schema drives the transport requirements:
- vector clocks must be transmitted,
- dependency summaries must be transmitted,
- per-origin FIFO must be preserved,
- out-of-order arrivals must be detected and buffered.
The transport does not reorder entries. The transport does not need to enforce causal ordering. The transport only needs to:
- preserve FIFO per origin,
- deliver metadata intact.
Performance note:
- Keep transport logic dumb and fast; all causal reasoning stays in the apply pipeline.
- Use one long-lived connection per peer to minimise per-batch overhead. The shipped gRPC push transport keeps one long-lived, HTTP/2-multiplexed channel per peer cluster and sends each batch over it as a unary
PushRPC - not a stream.
10. Summary of WAL changes
| Area | Change | Backwards compatible | Performance notes |
|---|---|---|---|
| Schema | Add vector clock | ✔ | Delta-encode per shard; absolute on batch / post-trim boundaries |
| Schema | Add dependency summary | ✔ | Start with VC; only optimise if needed |
| Replay | Add dependency checks | ✔ | Per-shard, single-threaded, O(#origins) |
| Replay | Add buffering | ✔ | Cap buffers; backpressure transport |
| GC | Causal-stable frontier | ✔ | Recompute on ack updates only |
| Snapshot | Cut at causal-stable frontier | ✔ | No change to scan algorithm |
| Transport | Carry new metadata | ✔ | Keep transport simple; no causal logic |
11. Non-goals
Causal+ does not require:
- changing WAL offset semantics,
- reordering WAL entries,
- rewriting WAL entries,
- changing commit-time capture,
- changing the storage provider contract.
The WAL remains:
- append-only,
- strictly ordered,
- durable,
- the canonical mutation record.
Causal+ only adds:
- richer metadata,
- stricter apply rules,
- stronger GC and snapshot invariants.
With the performance notes above, the design avoids the four common failure modes:
- Full vector clock bloat - use delta encoding with absolute anchors at batch / post-trim boundaries.
- Global locks or cross-shard contention - keep apply per-shard.
- Unbounded buffering - cap, backpressure, and route overflow through the existing dead-letter pipeline with classified reason tags.
- Recomputing dependency closure - maintain the local VC incrementally.
12. Completeness wave - full coverage of structural & atomic write paths
Sections 1–11 cover the point-write, single-tree, single-shard-per-mutation path. The completeness-wave replication items (and their core-side wire-additive dependencies) extend the same causal+ guarantee to every write path the core library exposes, so a host that enables any one of those features still gets full causal+ semantics rather than dropping back to per-origin LWW for that specific path. Cross-tree causality is intentionally out of scope.
12.1 Coverage matrix
| Write path | Hazard if not covered |
|---|---|
SetManyAtomic (multi-key atomic write) |
Per-key VC drift fabricates a dependency graph the writer never authored; remote peer parks key K waiting for key K-1. |
| Resize / rebalance / compaction | Structural rewrites stamp fresh VC under maintenance pressure; pollutes the dependency graph and inflates wire traffic. |
| Shard split / merge / saga shadow-forward | Shadow-forward emit captures a fresh VC against the destination shard rather than preserving the originating commit's frontier. |
| Multi-shard user write (range delete, multi-leaf saga) | Per-grain VC reads disagree on cross-shard origins because each grain only sees inbound applies routed to it. |
| Snapshot / restore (intra-cluster) | Live HWM table is wiped on restore; receiver replays restored entries against a zeroed VC and either re-parks them or re-merges. |
12.2 Design invariants preserved
Every item in the completeness wave is constrained by the same rules that bound sections 1–10:
- No commit-path change for point writes. VC capture happens on the leaf's existing commit path, where each WAL record is stamped. Atomic writes are the exception: their prepared writes are appended invisibly, the saga appends a separate per-shard
TxCommit/TxAbortterminal record after them, and the batch becomes visible at the tree's transaction-registry decision rather than at each entry's append (see Atomic writes). - Append-only, monotonic, durable. No item rewrites a WAL entry, no item changes offset semantics, no item changes the commit point.
- No cross-shard locks in apply. The shadow-forward receiver-side dedupe cache is held per tree, and nothing in the apply path takes a cross-shard lock. The producer ships the causal frontier straight from the per-shard leaf WAL, so there is no separate in-memory producer-side cache to coordinate.
- Wire-additive only. The new structural-rewrite and shadow-forward
[Id]slots have decode-as-empty defaults. Legacy peers and legacy persisted state continue to decode, and apply exactly as entries whose slots are empty. - Idempotent under re-delivery. The shadow-forward identity cache - a bounded FIFO of recent
(origin, hlc, key, op)tuples per tree - is the receiver's primary exact-identity dedup for steady-state writes: a re-delivery it has already evicted falls through to the idempotent per-key last-writer-wins apply at the leaf, and entries at or below a snapshot-pinned causal floor are dropped up front. The per-origin high-water mark is not a drop criterion for steady-state writes.
12.2.1 Atomic multi-key write shipped mechanism
The atomic-transaction boundary (TransactionId) is the per-emit signal a causal+ consumer reads to detect that several entries belong to the same enclosing batch. The producer-side VC capture is the batch-wide consistency half: the saga persists the caller's ambient frontier once, on its first prepare, in a wire-compatible slot of its own state (a legacy persisted saga decodes it as null), and re-establishes it on LatticeVectorClockContext.Current every time it resumes driving, so the single batched dispatch that stages the batch's prepared writes reads the identical ambient and each leaf stamps it onto the entry it stages. The saga-wide stamp survives crash recovery because the persisted slot is the single source of truth. There is no per-key compensation stamp: an aborted saga issues no rollback writes - it records the abort and its terminal marks discard the staged prepares - so no rolled-back key ever re-lands (see Atomic writes).
12.2.2 Intra-cluster snapshot/restore shipped mechanism
The intra-cluster snapshot/restore path (operator snapshots a tree, restores it later - same cluster, possibly different timestamp) is the dual of the cross-cluster bootstrap path. Cross-cluster bootstrap pins the snapshot's causalStableFrontier directly onto the receiver's durable per-tree local vector clock, because the wire envelope carries the frontier verbatim from sender to receiver. Intra-cluster restore has no such envelope: the operator workflow tears down the live tree state (including that per-tree local vector clock, the per-origin high-water marks the receiver's dependency check reads) and rehydrates the values from a snapshot artefact, but the frontier metadata that drove the dependency check is gone.
The restore seeder reconstructs that frontier from the values themselves. The core library carries each entry's commit-time vector clock on its entry records (empty for legacy persisted state) and preserves it end-to-end through every persistence / merge / snapshot / restore / bulk-load / compaction path with the same discipline applied to OriginClusterId. The replication package ships IReplicationLocalVcSeeder.SeedFromTreeAsync(treeName, ct): for every physical shard the tree's live routing reaches - the shards its shard map names on the physical copy its alias resolves to, so a restored or resized tree is read from the live copy rather than the retired one - it walks that shard's leaf chain from the leftmost leaf along the sibling pointers, reading each leaf's live raw entries (the same leaf walk a tree snapshot copy uses), accumulates frontier.MergeFrom(entry.VectorClock) for every non-null VC slot (pointwise-max - the same accumulator the bounded dependency buffer uses), and seeds the durable local vector clock. A shard an adaptive split added above the pinned shard count is walked like any other, because the map routes keys to it. The pin replaces the tree's whole local vector clock durably, so the receiver's dependency check reads the reconstructed frontier across silo restarts. There is no in-memory producer-side mirror to prime: the shipped causal frontier is sourced from the leaf WAL the shipper tails, so the durable pin is the only seed the restore path needs. The pin carries HybridLogicalClock.Zero as its snapshot as-of clock because intra-cluster restore has no snapshot timestamp - the frontier is the authoritative seed, and the pin does not consult the as-of value, which is kept only for symmetry with the cross-cluster path.
Non-replicated trees (ILatticeMergeModeResolver.Resolve(treeName) returns null) are a no-op: the seeder returns LocalVcSeedReport { SeedApplied = false, EntriesScanned = 0, Frontier = null } without consulting the tree. Empty trees pin an empty frontier, so even an empty tree's local vector clock is consistent after the seed. Partial restores (where the operator restores a subset of values) seed correctly from the surviving subset because the pointwise-max accumulator handles missing-origin components as zero. The seeder is not automatic - restore tooling is host-defined and the seeder is a single-call public API the operator runs once per restored tree as a post-restore step, mutually exclusive with the cross-cluster bootstrap path per restore event.
13. Examples
13.1 Simplified causal+ entry sketch
message WalRecord {
Metadata metadata = 1;
oneof operation {
// multi-key atomic upsert
AtomicUpsert upsert = 2;
// single-key delete
Delete delete = 3;
}
// per-entry causal metadata
VectorClock vector_clock = 10;
DependencySummary dependency_summary = 11;
}
13.2 Causal+ entry lifecycle
- Initial state:
local_vc = {}
- Produce an entry:
// Set X=1
upsert = {
"X": 1
};
vector_clock = {
"A": 5,
"B": 5
};
// no dependencies
dependency_summary = {};
- Append as offset 123:
append({
"offset": 123,
"operation": {
"upsert": upsert
},
"vector_clock": vector_clock,
"dependency_summary": dependency_summary
});
- On replay, before apply:
WAL: [ ..., { offset: 123, vector_clock: { A: 5, B: 5 } } , ... ]
local_vc: { A: 4, B: 5 } // or { A: 5, B: 4 } -- both are ok
entry: { offset: 123, vector_clock: { A: 5, B: 5 } }
// metadata matches, applies idempotently
- On replay, after apply:
local_vc: { A: 5, B: 5 } // or { A: 5, B: 5 } -- both are ok
// buffered entries with satisfied dependencies are eligible
- Produce a conflicting entry:
// Set A=2
upsert = {
"A": 2
};
vector_clock = {
"A": 6, // advance A's clock
"B": 5
};
// depends on A's prior value
dependency_summary = {
"A": 5
};
- Append as offset 124:
append({
"offset": 124,
"operation": {
"upsert": upsert
},
"vector_clock": vector_clock,
"dependency_summary": dependency_summary
});
- On replay, before apply:
WAL: [ ..., { offset: 124, vector_clock: { A: 6, B: 5 } } , ... ]
local_vc: { A: 5, B: 5 }
entry: { offset: 124, vector_clock: { A: 6, B: 5 } }
// A's VC is ahead, but the dependency (summary) is satisfied
- On replay, after apply:
local_vc: { A: 6, B: 5 }
// the dependency on A's prior value allows this to apply
// buffered entries with satisfied dependencies are eligible
- Produce an entry with indirect dependency:
// Set B=3 (indirectly dependent on A)
upsert = {
"B": 3
};
vector_clock = {
"A": 7, // advance A's clock
"B": 6
};
// depends on A's new value
dependency_summary = {
"A": 6
};
- Append as offset 125:
append({
"offset": 125,
"operation": {
"upsert": upsert
},
"vector_clock": vector_clock,
"dependency_summary": dependency_summary
});
- On replay, before apply:
WAL: [ ..., { offset: 125, vector_clock: { A: 7, B: 6 } } , ... ]
local_vc: { A: 6, B: 5 }
entry: { offset: 125, vector_clock: { A: 7, B: 6 } }
// B's VC is ahead, and the dependency (summary) is satisfied
- On replay, after apply:
local_vc: { A: 7, B: 6 }
// the dependency on A's new value allows this to apply
// buffered entries with satisfied dependencies are eligible
- Effects of trim and GC:
// GC trims up to offset 124, entries 123 and 124 are preserved
trim_to_offset = 124;
WAL: [ ... , { offset: 123, vector_clock: { A: 5, B: 5 } } , { offset: 124, vector_clock: { A: 6, B: 5 } } , ... ]
// causal stable is now at 124, any entry with VC <= 124 is GC eligible
- After GC:
WAL: [ ... , { offset: 124, vector_clock: { A: 6, B: 5 } } , ... ]
// entry 123 is gone, but its effects are preserved by entry 124
- Snapshot at causal stable:
// snapshot_frontier is stable at 124
snapshot_frontier = 124;
// any entry with vector_clock <= 124 is included in the snapshot
// this includes the last stable state for all keys
- Incremental replication starts at the snapshot frontier:
start = 124;
// subsequent entries are delivered starting from the snapshot frontier
// the receiver's local VC is updated to 124 for all origins
- End-to-end causal+ replication guarantees:
- causally ordered delivery
- no cycles
- no duplicates
- snapshots include all necessary state
- incremental from the latest stable snapshot
Performance notes:
- The above example uses an idealised entry sketch for clarity.
- Actual implementation details may vary, e.g., use of forward deltas or batch encapsulation.
- Focus on the causal relationships and VC/DS semantics.