Files
seaweedfs/weed/storage/blockvol/repl_proto.go
T
pingqiuandClaude Opus 4.6 59a36013d4 feat: rebuild hardening A1-A5 + session-controlled execution path
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>
2026-04-08 14:39:11 -07:00

257 lines
7.6 KiB
Go

package blockvol
import (
"encoding/binary"
"errors"
"fmt"
"io"
)
// Data channel message types.
const (
MsgWALEntry byte = 0x01
MsgResumeShipReq byte = 0x03 // CP13-5: reconnect handshake request
MsgResumeShipResp byte = 0x04 // CP13-5: reconnect handshake response
MsgCatchupDone byte = 0x05 // CP13-5: end of catch-up stream
)
// ResumeShip status codes.
const (
ResumeOK byte = 0x00
ResumeEpochMismatch byte = 0x01
ResumeNeedsRebuild byte = 0x02
)
// Control channel message types.
const (
MsgBarrierReq byte = 0x01
MsgBarrierResp byte = 0x02
)
// Barrier response status codes.
const (
BarrierOK byte = 0x00
BarrierEpochMismatch byte = 0x01
BarrierTimeout byte = 0x02
BarrierFsyncFailed byte = 0x03
)
// BarrierRequest is sent by the primary to the replica on the control channel.
type BarrierRequest struct {
Vid uint32
LSN uint64
Epoch uint64
}
// BarrierResponse is the replica's reply to a barrier request.
// Wire format: [1B status][8B flushedLSN] = 9 bytes.
// Legacy replicas send only 1 byte (FlushedLSN defaults to 0).
type BarrierResponse struct {
Status byte
FlushedLSN uint64 // replica's durable WAL progress after this barrier
}
// EncodeBarrierResponse serializes a BarrierResponse (1+8 = 9 bytes).
func EncodeBarrierResponse(resp BarrierResponse) []byte {
buf := make([]byte, 9)
buf[0] = resp.Status
binary.BigEndian.PutUint64(buf[1:9], resp.FlushedLSN)
return buf
}
// DecodeBarrierResponse deserializes a BarrierResponse.
// Handles both 9-byte (new) and 1-byte (legacy) responses.
func DecodeBarrierResponse(buf []byte) BarrierResponse {
if len(buf) < 1 {
return BarrierResponse{}
}
resp := BarrierResponse{Status: buf[0]}
if len(buf) >= 9 {
resp.FlushedLSN = binary.BigEndian.Uint64(buf[1:9])
}
return resp
}
// Frame header: [1B type][4B payload_len].
const frameHeaderSize = 5
// maxFramePayload caps the payload size to prevent OOM on corrupt data.
const maxFramePayload = 256 * 1024 * 1024 // 256MB
var (
ErrFrameTooLarge = errors.New("repl: frame payload exceeds maximum size")
ErrFrameEmpty = errors.New("repl: empty frame payload")
)
// WriteFrame writes a length-prefixed frame: [1B type][4B len][payload].
func WriteFrame(w io.Writer, msgType byte, payload []byte) error {
hdr := make([]byte, frameHeaderSize)
hdr[0] = msgType
binary.BigEndian.PutUint32(hdr[1:5], uint32(len(payload)))
if _, err := w.Write(hdr); err != nil {
return fmt.Errorf("repl: write frame header: %w", err)
}
if len(payload) > 0 {
if _, err := w.Write(payload); err != nil {
return fmt.Errorf("repl: write frame payload: %w", err)
}
}
return nil
}
// ReadFrame reads a length-prefixed frame and returns message type and payload.
func ReadFrame(r io.Reader) (msgType byte, payload []byte, err error) {
hdr := make([]byte, frameHeaderSize)
if _, err = io.ReadFull(r, hdr); err != nil {
return 0, nil, fmt.Errorf("repl: read frame header: %w", err)
}
msgType = hdr[0]
payloadLen := binary.BigEndian.Uint32(hdr[1:5])
if payloadLen > maxFramePayload {
return 0, nil, ErrFrameTooLarge
}
payload = make([]byte, payloadLen)
if payloadLen > 0 {
if _, err = io.ReadFull(r, payload); err != nil {
return 0, nil, fmt.Errorf("repl: read frame payload: %w", err)
}
}
return msgType, payload, nil
}
// Rebuild message types (on rebuild channel).
const (
MsgRebuildReq byte = 0x10 // client -> server
MsgRebuildEntry byte = 0x11 // server -> client: WAL entry
MsgRebuildExtent byte = 0x12 // server -> client: extent chunk
MsgRebuildDone byte = 0x13 // server -> client: stream complete
MsgRebuildError byte = 0x14 // server -> client: error
)
// Rebuild request types.
const (
RebuildWALCatchUp byte = 0x01
RebuildFullExtent byte = 0x02
RebuildSnapshot byte = 0x03 // P2: exact snapshot export at requested BaseLSN
RebuildSessionBase byte = 0x04 // V2: per-block base lane for session-controlled rebuild
)
// RebuildRequest is sent by the rebuilding replica to the primary.
type RebuildRequest struct {
Type byte // RebuildWALCatchUp or RebuildFullExtent
FromLSN uint64
Epoch uint64
}
// EncodeRebuildRequest serializes a RebuildRequest (1+8+8 = 17 bytes).
func EncodeRebuildRequest(req RebuildRequest) []byte {
buf := make([]byte, 17)
buf[0] = req.Type
binary.BigEndian.PutUint64(buf[1:9], req.FromLSN)
binary.BigEndian.PutUint64(buf[9:17], req.Epoch)
return buf
}
// DecodeRebuildRequest deserializes a RebuildRequest.
func DecodeRebuildRequest(buf []byte) (RebuildRequest, error) {
if len(buf) < 17 {
return RebuildRequest{}, fmt.Errorf("repl: rebuild request too short: %d bytes", len(buf))
}
return RebuildRequest{
Type: buf[0],
FromLSN: binary.BigEndian.Uint64(buf[1:9]),
Epoch: binary.BigEndian.Uint64(buf[9:17]),
}, nil
}
// EncodeBarrierRequest serializes a BarrierRequest (4+8+8 = 20 bytes).
func EncodeBarrierRequest(req BarrierRequest) []byte {
buf := make([]byte, 20)
binary.BigEndian.PutUint32(buf[0:4], req.Vid)
binary.BigEndian.PutUint64(buf[4:12], req.LSN)
binary.BigEndian.PutUint64(buf[12:20], req.Epoch)
return buf
}
// DecodeBarrierRequest deserializes a BarrierRequest.
func DecodeBarrierRequest(buf []byte) (BarrierRequest, error) {
if len(buf) < 20 {
return BarrierRequest{}, fmt.Errorf("repl: barrier request too short: %d bytes", len(buf))
}
return BarrierRequest{
Vid: binary.BigEndian.Uint32(buf[0:4]),
LSN: binary.BigEndian.Uint64(buf[4:12]),
Epoch: binary.BigEndian.Uint64(buf[12:20]),
}, nil
}
// --- CP13-5: Reconnect handshake messages ---
// ResumeShipReq is sent by the primary on the data channel after reconnect.
type ResumeShipReq struct {
Epoch uint64
PrimaryHeadLSN uint64
WalRetainStart uint64
}
// EncodeResumeShipReq serializes a ResumeShipReq (8+8+8 = 24 bytes).
func EncodeResumeShipReq(req ResumeShipReq) []byte {
buf := make([]byte, 24)
binary.BigEndian.PutUint64(buf[0:8], req.Epoch)
binary.BigEndian.PutUint64(buf[8:16], req.PrimaryHeadLSN)
binary.BigEndian.PutUint64(buf[16:24], req.WalRetainStart)
return buf
}
// DecodeResumeShipReq deserializes a ResumeShipReq.
func DecodeResumeShipReq(buf []byte) (ResumeShipReq, error) {
if len(buf) < 24 {
return ResumeShipReq{}, fmt.Errorf("repl: resume ship req too short: %d bytes", len(buf))
}
return ResumeShipReq{
Epoch: binary.BigEndian.Uint64(buf[0:8]),
PrimaryHeadLSN: binary.BigEndian.Uint64(buf[8:16]),
WalRetainStart: binary.BigEndian.Uint64(buf[16:24]),
}, nil
}
// ResumeShipResp is the replica's reply on the data channel.
type ResumeShipResp struct {
Status byte
ReplicaFlushedLSN uint64
}
// EncodeResumeShipResp serializes a ResumeShipResp (1+8 = 9 bytes).
func EncodeResumeShipResp(resp ResumeShipResp) []byte {
buf := make([]byte, 9)
buf[0] = resp.Status
binary.BigEndian.PutUint64(buf[1:9], resp.ReplicaFlushedLSN)
return buf
}
// DecodeResumeShipResp deserializes a ResumeShipResp.
func DecodeResumeShipResp(buf []byte) (ResumeShipResp, error) {
if len(buf) < 9 {
return ResumeShipResp{}, fmt.Errorf("repl: resume ship resp too short: %d bytes", len(buf))
}
return ResumeShipResp{
Status: buf[0],
ReplicaFlushedLSN: binary.BigEndian.Uint64(buf[1:9]),
}, nil
}
// EncodeCatchupDone serializes the catch-up done marker (8 bytes: snapshotLSN).
func EncodeCatchupDone(snapshotLSN uint64) []byte {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf[0:8], snapshotLSN)
return buf
}
// DecodeCatchupDone deserializes the catch-up done marker.
func DecodeCatchupDone(buf []byte) (uint64, error) {
if len(buf) < 8 {
return 0, fmt.Errorf("repl: catchup done too short: %d bytes", len(buf))
}
return binary.BigEndian.Uint64(buf[0:8]), nil
}