Files
seaweedfs/sw-block/design/wal-replication-v2.md
T
pingqiuandClaude Opus 4.6 edec7098e8 feat: add V2 protocol simulator and enginev2 sender/session prototype
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>
2026-03-28 10:38:27 -07:00

402 lines
9.6 KiB
Markdown

# 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:
1. `LSN 100`: block `A = foo`
2. `LSN 120`: block `A = 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 `InSync` only 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 `InSync` after durable catch-up completes
## Architecture
### Primary Artifacts
The primary owns three forms of state:
1. `Active WAL`
- recent ordered metadata/delta stream
- bounded by retention policy
2. `Checkpoint Snapshot`
- immutable point-in-time base image at `cpLSN`
- used for long-gap reconstruction
3. `Current Extent`
- latest live block state
- not a substitute for historical checkpoint state
### Replica Artifacts
Replica maintains:
1. local WAL or equivalent recovery log
2. replica `receivedLSN`
3. replica `flushedLSN`
4. 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:
1. allocate monotonic `LSN`
2. append recovery metadata to local WAL or journal
3. enqueue the record to each replica sender queue
4. 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_all`
- `sync_quorum`
## Replica States
Replica state is defined by `wal-replication-v2-state-machine.md`.
Important highlights:
- `Bootstrapping`
- `InSync`
- `Lagging`
- `CatchingUp`
- `PromotionHold`
- `NeedsRebuild`
- `Rebuilding`
- `CatchUpAfterRebuild`
- `Failed`
Only `InSync` replicas count toward sync durability.
## Protocol
### 1. Normal Streaming
Primary sender loop:
- sends ordered replicated write records
Replica:
1. validates ordering
2. appends locally
3. advances `receivedLSN`
### 2. Barrier / Sync
Primary sends:
- `BarrierReq{LSN, Epoch}`
Replica:
1. wait until `receivedLSN >= LSN`
2. flush durable local state
3. set `flushedLSN = LSN`
4. 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:
1. replica already caught up
- state -> `PromotionHold` or `InSync` depending on policy
2. bounded catch-up possible
- reserve recovery window
- state -> `CatchingUp`
3. direct replay not possible
- state -> `NeedsRebuild`
## Recovery Feasibility and Reservation
The key V2 rule is:
- `fully recoverable` is not enough
- the primary must also reserve the recovery window
Recommended engine-side flow:
1. `CheckRecoveryFeasibility(startLSN, endLSN)`
2. if feasible, `ReserveRecoveryWindow(startLSN, endLSN)`
3. only then start `CatchingUp` or `CatchUpAfterRebuild`
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:
1. choose a bounded target `H0`
2. reserve `(ReplicaFlushedLSN, H0]`
3. replay the missing range
4. barrier confirms durable `flushedLSN >= H0`
5. enter `PromotionHold`
6. 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:
1. define a bounded catch-up window
2. if catch-up rate is slower than ingest rate for too long:
- either temporarily throttle primary admission for strict `sync_all`
- or fail `sync_all` requests and let control-plane policy react
- or abort to rebuild
3. 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:
1. mark replica `NeedsRebuild`
2. choose a real checkpoint/snapshot base at `cpLSN`
3. create a rebuild reservation
4. replica enters `Rebuilding`
5. replica pulls immutable checkpoint/snapshot image
6. replica installs that base image and sets base progress to `cpLSN`
7. primary replays trailing history `(cpLSN, H0]`
8. barrier confirms durable replay
9. replica enters `PromotionHold`
10. 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 `InSync` and durably at target LSN
- bounded retry only
- no silent downgrade
### `sync_quorum`
- ACK when enough replicas are `InSync` and durably at target LSN
## Why This Direction
V2 separates three different concerns cleanly:
1. fast steady-state replication
2. short-gap replay
3. long-gap reconstruction
This avoids forcing WAL alone to solve all recovery cases.
## Implementation Order
Recommended order:
1. pure FSM
2. ordered sender loop
3. bounded direct replay
4. checkpoint/snapshot reconstruction
5. smarter local write path and recovery classes
6. 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 `InSync` only after durable proof