Files
seaweedfs/weed/storage/blockvol/repl_proto.go
T
Ping QiuandClaude Opus 4.6 548e47e482 feat: reconnect handshake + WAL catch-up protocol (CP13-5)
Adds the sync_all reconnect protocol: when a degraded shipper
reconnects, it performs a handshake (ResumeShipReq/Resp) to
determine the replica's durable progress, then streams missed
WAL entries to close the gap before resuming live shipping.

New wire messages:
- MsgResumeShipReq (0x03): primary sends epoch, headLSN, retainStart
- MsgResumeShipResp (0x04): replica returns status + flushedLSN
- MsgCatchupDone (0x05): marks end of catch-up stream

Decision matrix after handshake:
- R == H: already caught up → InSync
- S <= R+1 <= H: recoverable gap → CatchingUp → stream → InSync
- R+1 < S: gap exceeds retained WAL → NeedsRebuild
- R > H: impossible progress → NeedsRebuild

WALAccess interface: narrow abstraction (RetainedRange + StreamEntries)
avoids coupling shipper to raw WAL internals.

Bootstrap vs reconnect split: fresh shippers (HasFlushedProgress=false)
use CP13-4 bootstrap path. Previously-synced shippers use handshake.

Catch-up retry budget: maxCatchupRetries=3 before NeedsRebuild.

ReplicaReceiver now initializes receivedLSN/flushedLSN from volume's
nextLSN on construction (handles receiver restart on existing volume).

TestBug2_SyncAll_SyncCache_AfterDegradedShipperRecovers flips FAIL→PASS.
All previously-passing baseline tests remain green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:38:06 -07:00

255 lines
7.4 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
)
// 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
}