mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
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>
255 lines
7.4 KiB
Go
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
|
|
}
|