A1 Engine kind-routing fix: SessionProgressObserved/Completed/Failed now respect active session Kind. Rebuild progress no longer leaks into catch-up aggregate. sessionKindMismatch guard + observeRebuildProgress helper. 2 regression tests lock kind isolation. A2 Retention pin: Rebuild session ack drives progress-based WAL retention floor. Pin installed at base_lsn on accepted, advances with wal_applied_lsn, released on completed/failed/cancelled. rebuildProgressPinFloor returns min across all active replicas. Retention pin test: 100 blocks fill WAL, 5 flusher cycles with 20 pinned rebuild entries — all verified correct. A3 Progress ack emission: Automatic sessionAck(running/base_complete/completed/failed) emitted from rebuild session lifecycle transitions. sessionAckLocked builds ack under session lock. emitRebuildSessionAck callback wired through SetOnRebuildSessionAck on BlockVol. ObserveReplicaRebuildSessionAck maps acks to core engine events. WireLocalReplicaRebuildSessionAcks bridges local callback to server. 5 server tests proving ack→core, pin advance, pin cleanup. A4 Deadline/timeout: rebuildAckWatch watchdog: armed on accepted/running/base_complete, refreshed on each ack, cleared on completed/failed. Timeout cancels local session + clears pin + fail-closes. 2 tests: timeout→fail-close, progress→refresh. A5 Session-controlled execution path: v2bridge.Executor.TransferFullBase now uses session-controlled loop: beginControlledFullBase → real sessionControl over TCP → transferExtentToSession via RebuildTransportClient → PrepareFullBaseRebuild → TryCompleteRebuildSession. ReplicaReceiver control channel handles MsgSessionControl alongside MsgBarrierReq. Session acks written back on same TCP connection. RebuildSessionBase request type separates new per-block stream from legacy raw extent stream. Full-base cleanup deferred until success. Deadlock fix: ApplyBaseBlock releases session lock before ioMu. Hydration skip for full-base sessions. 23 rebuild component tests (all pass): 11 kernel correctness, 8 transport/runtime, 3 scenario-scale, including 1GB primary-initiated with CRC validation. 29 files changed, ~2500 insertions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 KiB
V2 Rebuild MVP Session Protocol
Date: 2026-04-07 Status: active draft
Goal
Define the smallest reliable VS-to-VS protocol that is sufficient to build a
working rebuild MVP on top of:
- trusted flushed-extent base transfer
- live WAL ingestion
- primary-owned session control
- replica-reported session progress
This document is intentionally narrower than the long-term protocol. It is the implementation target for the first rebuild MVP.
Non-Goals
This MVP does not try to define:
- a full
catchupprotocol rangeBitmapor delta-block rebuild execution- partial session resume from volatile in-memory rebuild state
- advanced retransmit/window semantics beyond simple transport needs
- broad product-ready transport or multi-host rollout guarantees
Control Model
The protocol keeps one strict rule:
syncasks for facts- the primary decides whether to issue
rebuild - the replica executes the session
- the replica reports session progress
- only the primary decides when the session is complete
The replica does not choose:
- the next session kind
- the target boundary
- whether it is quorum-eligible again
Message Set
The MVP needs four semantic message families:
syncsyncAcksessionControlsessionAck
And two data-plane lanes:
walDatasessionData
Optional:
sessionDataAcktransport/window control only
Message Definitions
sync
Direction:
- primary -> replica
Minimum fields:
volume_idreplica_idepochsync_idtarget_lsndeadline_ms
Purpose:
- get bounded replica facts
- observe whether a session is already active
- decide whether to stay
keepupor startrebuild
syncAck
Direction:
- replica -> primary
Minimum fields:
volume_idreplica_idepochsync_idack_kindapplied_lsndurable_lsnsession_activesession_idoptionalsession_kindoptionalsession_phaseoptionalreasonoptional
Allowed ack_kind values:
quorumtimed_outtransport_lostepoch_mismatch
Rule:
syncAckreturns facts only- it does not recommend
keepup/catchup/rebuild
Primary-side normalized sync facts
The wire syncAck is still the only replica-to-primary sync control message.
However, the primary/server layer may normalize raw wire facts and local
control-path observations into a smaller primary-side fact vocabulary before
feeding them into decision logic.
This normalized vocabulary is not a new wire protocol. It is the primary-owned semantic envelope for "what kind of sync fact just arrived?"
Current normalized kinds:
sync_quorum_ackednormal quorum-style sync closure reached through the target boundarysync_quorum_timed_outcontrol-plane sync closure timed out before normal quorum-style completionsync_replay_requiredfresh facts show the replica is behind but still replay-recoverablesync_rebuild_requiredfresh facts show the replica is outside retained replay coverage, so rebuild is requiredsync_replay_faileda replay/catch-up attempt failed and the primary must re-decide from fresh facts
Normalization rules:
- normalized kinds never recommend an action by themselves
- they preserve the same ownership rule: replica reports facts, primary decides the next session
- multiple sources may produce the same normalized kind:
- raw
syncAck - barrier timeout / closure callback
- catch-up failure callback
- planner classification after reading fresh facts
- raw
- only the primary may turn normalized sync facts into
keepup,catchup, orrebuilddecisions
sessionControl
Direction:
- primary -> replica
The MVP needs only:
start_rebuildcancel_session
Minimum fields for start_rebuild:
volume_idreplica_idepochsession_idsession_kind = rebuildbase_kind = flushed_extentbase_lsntarget_lsndeadline_ms
Rules:
session_idmust be unique under the current primary authority- a new session may supersede an older one
- epoch mismatch must be rejected
base_lsnfor rebuild is the primary's flushed/checkpoint boundary, not a merely committed boundary- the base lane is allowed to stream the current extent image directly as long
as that image is known to represent
base_lsn
sessionAck
Direction:
- replica -> primary
Minimum fields:
volume_idreplica_idepochsession_idsession_kind = rebuildphasewal_applied_lsnbase_progressbase_completeachieved_lsnon completionreasonon failure
Allowed phase values for the MVP:
acceptedrunningbase_completecompletedfailed
sessionData
Direction:
- primary -> replica
Purpose:
- send trusted flushed-extent base chunks
Minimum fields:
volume_idreplica_idepochsession_idchunk_idoffset_or_lba_rangepayloadis_last_chunk
walData
Direction:
- primary -> replica
Purpose:
- continue live WAL ingestion during rebuild
Minimum fields:
volume_idreplica_idepochlsnwrites[]
Each write should carry:
lba_rangepayload
Replica Apply Rules
Base Rule
Rebuild runs as two concurrent lanes:
- base lane from trusted flushed extent at
base_lsn - live WAL lane from
base_lsn
Bitmap Rule
The replica maintains a bitmap of LBAs covered by applied WAL.
The bit is set when the WAL write is:
- applied into replica-local WAL/recovery truth
- replayable after restart
The bit is not set when data is only:
- received on the network
- queued but not yet applied locally
Write Conflict Rule
When a base chunk targets an LBA:
- if the bitmap bit is clear, base data may be written
- if the bitmap bit is set, base data for that LBA must be skipped
Short form:
WAL appliedwins over older base data
Flush Rule
For bitmap protection, applied does not require:
- flushing the write into the final extent image
Replica-local WAL durability and replay are sufficient for the MVP.
Completion Rule
The primary may accept rebuild completed only when all are true:
base_complete = truewal_applied_lsn >= target_lsn- the session has not been cancelled or superseded
- the replica reports one explicit
achieved_lsn
base transfer finished alone is not completion.
Only after the primary accepts this completion may the replica become eligible again for normal quorum-style sync closure.
Failure Rule
Session failure does not decide the next semantic recovery path.
failed(reason) means only:
- this rebuild session did not complete
After failure:
- the replica reports fresh facts again through
syncAck - the primary re-decides whether to issue a new rebuild session
No local component may self-promote the failure into semantic needs_rebuild.
In normalized primary-side vocabulary, this means:
- session failure may first surface as
sync_replay_failed - a fresh sync/planner pass may then normalize to
sync_rebuild_required - only after that fresh re-decision may the primary issue
start_rebuild
Crash Rule
The MVP assumes bitmap may be session-local volatile state.
Therefore after replica crash or session loss:
- do not resume a partially completed rebuild from volatile bitmap state
- restart with a fresh
sync - let the primary issue a fresh rebuild session
Current contract:
- crash recovery for rebuild means
restart rebuild, notresume rebuild - the system may reuse only durable facts that survived the crash:
- local durable WAL coverage
- the fresh rebuild
base_lsn - fresh primary-side decision after
sync
- the system does not currently reuse prior in-flight base-copy offset, prior in-memory bitmap state, or prior session identity as protocol facts
This means the MVP supports:
- safe restart from durable WAL facts
But does not support:
- arbitrary mid-session resume of partial base-copy progress
- durable
base_progress - persistent rebuild bitmap / checkpoint state
- CBT-based repair of previously transferred extent ranges
Issue #3 fix detail: crash restart bitmap hydration
The volatile bitmap itself is not resumed across crashes, but its protection surface must be rebuilt before a fresh rebuild session becomes visible.
Required startup rules for a fresh rebuild session:
- strict ordering barrier
- before sending or observing
sessionAck(accepted), and before opening the base lane forsessionData, the replica must finish local bitmap hydration - the fresh session is not externally visible until hydration is complete
- before sending or observing
- hydration source
- rebuild the bitmap from replica-local durable WAL coverage newer than
base_lsn - scan only the surviving local WAL coverage after
base_lsn; older WAL is already represented by the flushed base image
- rebuild the bitmap from replica-local durable WAL coverage newer than
- coverage rule
- every recovered local write/trim record with
lsn > base_lsnmarks its covered LBAs in the bitmap before any base chunk may be applied - this reconstructs the "WAL already wins here" immunity shield after restart
- every recovered local write/trim record with
- achieved boundary rule
- initial session progress may start at
base_lsn - if local durable WAL survives beyond
base_lsn, the replica may raise its initialwal_applied_lsnto that recovered boundary
- initial session progress may start at
- silent truncation guard
- if the replica can prove its local durable base is already newer than the
claimed
base_lsn, startup must fail closed - do not start a dual-lane rebuild with a stale base boundary and a partial bitmap
- if the replica can prove its local durable base is already newer than the
claimed
Operational interpretation:
- the session bitmap remains volatile
- the bitmap protection set is deterministically re-derived from local durable WAL before the new session starts
- if that re-derivation cannot be trusted, the session must be rejected and the primary must re-decide from fresh facts
- this hydration step is required for safe
restart rebuild; it is not a claim that the MVP can resume an interrupted rebuild session from its prior in-flight base-copy position
Primary Decision Rule
The MVP decision rule should stay intentionally simple:
- if
syncAck.ack_kind = quorum, remainkeepup - otherwise, if the replica is not safely closed in normal sync semantics, issue
rebuild
The first MVP does not need a full negotiated catchup protocol.
Equivalent normalized reading:
sync_quorum_acked-> remainkeepupsync_replay_required-> futurecatchuppath, not required for rebuild MVPsync_rebuild_required-> issuestart_rebuildsync_replay_failedorsync_quorum_timed_out-> gather fresh facts and re-run the same primary decision rule
MVP Implementation Skeleton
To reduce wiring ambiguity, the first implementation should expose one explicit
replica-side control surface in blockvol:
StartRebuildSession(config)ApplyRebuildSessionWALEntry(session_id, entry)ApplyRebuildSessionBaseBlock(session_id, lba, data)MarkRebuildSessionBaseComplete(session_id, total_blocks)TryCompleteRebuildSession(session_id)CancelRebuildSession(session_id, reason)ActiveRebuildSession()
Contract:
blockvolowns only replica-local session state and dual-lane apply rules- host/server wiring owns transport routing and message decoding
- stale packets must be rejected by
session_id - supersede is explicit: a new
session_idreplaces the old active session - completion remains queryable until the host emits the matching
SessionCompleted-style event and clears the session
Current MVP implementation choices:
- use a dedicated
RebuildBitmap, notDirtyMap - use direct extent transfer from the primary's flushed/checkpoint image for the base lane
- reuse the existing rebuild TCP path for
sessionDatarather than inventing a new transport first - anchor rebuild start at flushed/checkpoint
base_lsn, not merely committed LSN, because committed data may still live only in WAL
Current server-layer skeleton:
BlockService.StartReplicaRebuildSession(path, config)BlockService.ApplyReplicaRebuildWALEntry(path, session_id, entry)BlockService.ApplyReplicaRebuildBaseBlock(path, session_id, lba, data)BlockService.MarkReplicaRebuildBaseComplete(path, session_id, total_blocks)BlockService.TryCompleteReplicaRebuildSession(path, session_id)BlockService.CancelReplicaRebuildSession(path, session_id, reason)BlockService.ReplicaRebuildSession(path)BlockService.ObserveReplicaRebuildSessionAck(path, replica_id, ack)
Server-layer responsibility:
- decode incoming
sessionControl/walData/sessionData - map them onto the local volume path
- route them into the
BlockServiceskeleton above - build
sessionAckfromReplicaRebuildSession(path) - feed received
sessionAckback into core viaObserveReplicaRebuildSessionAck(path, replica_id, ack)
Replica State Machine
flowchart TD
idle[Idle]
accepted[Accepted]
running[Running]
baseComplete[BaseComplete]
completed[Completed]
failed[Failed]
idle --> accepted
accepted --> running
running --> baseComplete
running --> failed
baseComplete --> completed
baseComplete --> failed
Interpretation:
acceptedsession contract is valid and epoch/session id are acceptedrunningbase lane and WAL lane are activebaseCompletetrusted base transfer is complete, WAL lane still determines final closurecompletedreplica reports one achieved boundary at or beyond targetfailedsession stopped without semantic completion
Test Matrix
The rebuild MVP should not be considered ready until these tests exist.
Protocol
syncAckreturns facts only and never recommends an actionstart_rebuildis rejected on epoch mismatch- a new
session_idsupersedes the previous session
Correctness
- base lane plus live WAL lane converge to target
- WAL-applied LBA is never overwritten by later base-copy data
- bitmap bit is set on
applied, not onreceived - a fresh rebuild session must hydrate bitmap coverage from locally recovered WAL before opening the base lane
- rebuild start must fail closed if the local durable base is already newer
than the claimed
base_lsn
Crash / Failure
- crash after WAL receive but before apply leaves bitmap clear and base may still cover the LBA safely
- crash after WAL apply preserves correctness through local WAL replay
- crash restart before fresh base transfer hydrates bitmap from local WAL and skips stale base blocks correctly
- stale or truncated base boundary fails closed instead of starting rebuild
- transport loss during rebuild yields
failed(reason)and requires primary re-decision - rebuild completion does not restore normal quorum eligibility until the primary accepts completion
- crash during rebuild results in a fresh
sync+ fresh rebuild session, not reuse of prior in-flightbase_progress
Follow-On Work
After this MVP is working, the next candidates are:
- negotiated
catchup rangeBitmap/ delta-block rebuild- durable rebuild checkpoints for safe mid-session resume
- richer
sessionDataAckflow control - resumable rebuild with durable
base_progress - primary-owned CBT / changed-block repair for previously transferred ranges