mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-26 01:44:48 +00:00
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>
257 lines
7.6 KiB
Go
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
|
|
}
|