Adds sw-block/ directory with:
- distsim: protocol correctness simulator (96 tests)
- cluster model with epoch fencing, barrier semantics, commit modes
- endpoint identity, control-plane flow, candidate eligibility
- timeout events, timer races, same-tick ordering
- session ownership tracking with ID-based stale fencing
- enginev2: standalone V2 sender/session implementation (63 tests)
- per-replica Sender with identity-preserving reconciliation
- RecoverySession with FSM phase transitions and session ID
- execution APIs: BeginConnect, RecordHandshake, BeginCatchUp,
RecordCatchUpProgress, CompleteSessionByID — all sender-authority-gated
- recovery outcome branching: zero-gap, catch-up, needs-rebuild
- assignment-intent orchestration with epoch fencing
- design docs: acceptance criteria, open questions, first-slice spec,
protocol development process
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.6 KiB
WAL Replication V2
Date: 2026-03-26 Status: design proposal Purpose: redesign WAL-based block replication around explicit short-gap catch-up and long-gap reconstruction
Goal
Provide a replication architecture that:
- keeps the primary write path fast
- supports correct synchronous durability semantics
- supports short-gap reconnect catch-up using WAL
- avoids paying unbounded WAL retention tax for long-lag replicas
- uses reconstruction from a real checkpoint/snapshot base for larger lag
This design replaces a "WAL does everything" mindset with a 3-tier recovery model.
Core Principle
WAL is excellent for:
- recent ordered delta
- local crash recovery
- short-gap replica catch-up
WAL is not the right long-range recovery mechanism for lagging block replicas.
Long-gap recovery should use:
- a real checkpoint/snapshot base image
- plus WAL tail replay after that base point
Correctness Boundary
Never reconstruct old state from current extent alone.
Example:
LSN 100: blockA = fooLSN 120: blockA = bar
If a replica needs state at LSN 100, current extent contains bar, not foo.
Therefore:
- current extent is latest state
- not historical state
So long-gap recovery must use a base image that is known to represent a real checkpoint/snapshot cpLSN.
3-Tier Replication Model
Tier A: Keep-up
Replica is close enough to the primary that normal ordered streaming keeps it current.
Properties:
- normal steady-state mode
- no special recovery path
- replica stays
InSync
Tier B: Lagging Catch-up
Replica fell behind, but the primary still has enough recoverable history covering the missing range.
Properties:
- reconnect handshake determines the replica durable point
- primary proves and reserves a bounded recovery window
- primary replays missing history
- replica returns to
InSynconly after replay, barrier confirmation, and promotion hold
Tier C: Reconstruction
Replica is too far behind for direct replay.
Properties:
- replica must rebuild from a real checkpoint/snapshot base
- after base image install, primary replays trailing history after
cpLSN - replica only re-enters
InSyncafter durable catch-up completes
Architecture
Primary Artifacts
The primary owns three forms of state:
Active WAL
- recent ordered metadata/delta stream
- bounded by retention policy
Checkpoint Snapshot
- immutable point-in-time base image at
cpLSN - used for long-gap reconstruction
Current Extent
- latest live block state
- not a substitute for historical checkpoint state
Replica Artifacts
Replica maintains:
- local WAL or equivalent recovery log
- replica
receivedLSN - replica
flushedLSN - local extent state
Sender Model
Do not ship recovery data inline from foreground write goroutines.
Per replica, use:
- one ordered send queue
- one sender loop
The sender loop owns:
- live stream shipping
- reconnect handling
- short-gap catch-up
- reconstruction tail replay
This guarantees:
- strict LSN order per replica
- clean transport state ownership
- no inline shipping races in the primary write path
Write Path
Primary write path:
- allocate monotonic
LSN - append recovery metadata to local WAL or journal
- enqueue the record to each replica sender queue
- return according to durability mode semantics
Flusher later:
- flushes dirty data to extent
- manages checkpoints
- manages bounded retention of WAL and other recovery dependencies
Recovery Classes
V2 supports more than one local record type.
WALInline
Properties:
- payload lives directly in WAL
- recoverable while WAL is retained
ExtentReferenced
Properties:
- journal entry contains metadata only
- payload is resolved from extent/snapshot generation state
- direct-extent writes and future smart-WAL paths fall into this class
Replica state does not encode these classes.
Instead, the primary must answer a stricter question for reconnect:
- is
(startLSN, endLSN]fully recoverable under the current epoch, and can it be reserved for the duration of recovery?
Replica Progress Model
Each replica reports progress explicitly.
receivedLSN
- highest LSN received and appended locally
- not yet a durability guarantee
flushedLSN
- highest LSN durably persisted on the replica
- authoritative sync durability signal
Only flushedLSN counts for:
sync_allsync_quorum
Replica States
Replica state is defined by wal-replication-v2-state-machine.md.
Important highlights:
BootstrappingInSyncLaggingCatchingUpPromotionHoldNeedsRebuildRebuildingCatchUpAfterRebuildFailed
Only InSync replicas count toward sync durability.
Protocol
1. Normal Streaming
Primary sender loop:
- sends ordered replicated write records
Replica:
- validates ordering
- appends locally
- advances
receivedLSN
2. Barrier / Sync
Primary sends:
BarrierReq{LSN, Epoch}
Replica:
- wait until
receivedLSN >= LSN - flush durable local state
- set
flushedLSN = LSN - reply
BarrierResp{Status, FlushedLSN}
Primary uses this to evaluate mode policy.
3. Reconnect Handshake
On reconnect, primary obtains:
- current epoch
- primary head
- replica durable
flushedLSN
Then primary evaluates recovery feasibility.
Possible outcomes:
- replica already caught up
- state ->
PromotionHoldorInSyncdepending on policy
- bounded catch-up possible
- reserve recovery window
- state ->
CatchingUp
- direct replay not possible
- state ->
NeedsRebuild
Recovery Feasibility and Reservation
The key V2 rule is:
fully recoverableis not enough- the primary must also reserve the recovery window
Recommended engine-side flow:
CheckRecoveryFeasibility(startLSN, endLSN)- if feasible,
ReserveRecoveryWindow(startLSN, endLSN) - only then start
CatchingUporCatchUpAfterRebuild
A recovery reservation pins:
- recovery metadata
- referenced payload generations
- required snapshots/base images
- current epoch lineage for the session
If the reservation is lost during recovery:
- abort the current attempt
- fall back to
NeedsRebuild
Tier B: Lagging Catch-up Algorithm
When a replica is behind but within a recoverable retained window:
- choose a bounded target
H0 - reserve
(ReplicaFlushedLSN, H0] - replay the missing range
- barrier confirms durable
flushedLSN >= H0 - enter
PromotionHold - only then restore
InSync
Tail-chasing problem
If the primary is writing faster than the replica can catch up, the replica may never converge.
To handle this:
- define a bounded catch-up window
- if catch-up rate is slower than ingest rate for too long:
- either temporarily throttle primary admission for strict
sync_all - or fail
sync_allrequests and let control-plane policy react - or abort to rebuild
- either temporarily throttle primary admission for strict
- do not let a replica remain in unbounded perpetual
CatchingUp
Important rule
For sync_all, the data path must not silently downgrade to best_effort.
Correct behavior:
- bounded retry
- then fail
Any mode change must be explicit policy, not silent transport behavior.
Tier C: Reconstruction Algorithm
When a replica is too far behind for direct replay:
- mark replica
NeedsRebuild - choose a real checkpoint/snapshot base at
cpLSN - create a rebuild reservation
- replica enters
Rebuilding - replica pulls immutable checkpoint/snapshot image
- replica installs that base image and sets base progress to
cpLSN - primary replays trailing history
(cpLSN, H0] - barrier confirms durable replay
- replica enters
PromotionHold - replica returns to
InSync
Why snapshot/base image must be real
If the replica needs state at cpLSN, the base image must represent exactly that checkpoint.
Invalid:
- current extent copied at some later time and treated as historical
cpLSN
Valid:
- immutable snapshot
- copy-on-write checkpoint image
- frozen base image
Retention and Budget
V2 retention is bounded.
WAL / recovery metadata retention
Primary keeps only a bounded recent recovery window:
max_retained_wal_bytes- optionally
max_retained_wal_time
Recovery reservation budget
Reservations are also bounded:
- timeout
- bytes pinned
- snapshot dependency lifetime
If a catch-up or rebuild session exceeds its reservation budget:
- primary aborts the session
- replica falls back to
NeedsRebuild - a newer rebuild plan may be chosen later
Sync Modes
best_effort
- ACK after primary local durability
- replicas may lag
- background catch-up or rebuild allowed
sync_all
- ACK only when all required replicas are
InSyncand durably at target LSN - bounded retry only
- no silent downgrade
sync_quorum
- ACK when enough replicas are
InSyncand durably at target LSN
Why This Direction
V2 separates three different concerns cleanly:
- fast steady-state replication
- short-gap replay
- long-gap reconstruction
This avoids forcing WAL alone to solve all recovery cases.
Implementation Order
Recommended order:
- pure FSM
- ordered sender loop
- bounded direct replay
- checkpoint/snapshot reconstruction
- smarter local write path and recovery classes
- policy and control-plane integration
Phase 13 current direction
Current Phase 13 / WAL V1 is still:
- fixing correctness of WAL-centered sync replication
- still focused mainly on bounded WAL replay and rebuild fallback
That is the right bridge.
V2 should follow after WAL V1 closes.
Bottom Line
V2 is not "more WAL features."
It is:
- explicit recovery feasibility
- explicit recovery reservations
- ordered sender loops
- short-gap replay for recent lag
- checkpoint/snapshot reconstruction for long lag
- promotion back to
InSynconly after durable proof