Files
seaweedfs/weed/storage/blockvol/replica_apply.go
T
Ping QiuandClaude Opus 4.6 16a796e56d feat: Phase 4A CP2 — WAL shipping, replica barrier, distributed group commit
Primary ships WAL entries to replica over TCP (data channel), confirms
durability via barrier RPC (control channel). SyncCache runs local fsync
and replica barrier in parallel via MakeDistributedSync. When replica is
unreachable, shipper enters permanent degraded mode and falls back to
local-only sync (Phase 3 behavior).

Key design: two separate TCP ports (data+control), contiguous LSN
enforcement, epoch equality check, WAL-full retry on replica,
cond.Wait-based barrier with configurable timeout, BarrierFsyncFailed
status code. Close lifecycle: shipper → receiver → drain → committer →
flusher → fd.

New files: repl_proto.go, wal_shipper.go, replica_apply.go,
replica_barrier.go, dist_group_commit.go
Modified: blockvol.go, blockvol_test.go

27 dev tests + 21 QA tests = 48 new tests; 889 total (609 engine + 280
iSCSI), all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:12:39 -08:00

288 lines
7.0 KiB
Go

package blockvol
import (
"errors"
"fmt"
"log"
"net"
"sync"
"time"
)
var (
ErrStaleEpoch = errors.New("blockvol: stale epoch")
ErrDuplicateLSN = errors.New("blockvol: duplicate or out-of-order LSN")
)
// ReplicaReceiver listens for WAL entries from a primary and applies them
// to the local BlockVol. It runs two listeners: one for the data channel
// (WAL entries) and one for the control channel (barrier requests).
type ReplicaReceiver struct {
vol *BlockVol
barrierTimeout time.Duration
mu sync.Mutex
receivedLSN uint64
cond *sync.Cond
connMu sync.Mutex // protects activeConns
activeConns map[net.Conn]struct{}
dataListener net.Listener
ctrlListener net.Listener
stopCh chan struct{}
stopped bool
wg sync.WaitGroup
}
const defaultBarrierTimeout = 5 * time.Second
// NewReplicaReceiver creates and starts listening on the data and control ports.
func NewReplicaReceiver(vol *BlockVol, dataAddr, ctrlAddr string) (*ReplicaReceiver, error) {
dataLn, err := net.Listen("tcp", dataAddr)
if err != nil {
return nil, fmt.Errorf("replica: listen data %s: %w", dataAddr, err)
}
ctrlLn, err := net.Listen("tcp", ctrlAddr)
if err != nil {
dataLn.Close()
return nil, fmt.Errorf("replica: listen ctrl %s: %w", ctrlAddr, err)
}
r := &ReplicaReceiver{
vol: vol,
barrierTimeout: defaultBarrierTimeout,
dataListener: dataLn,
ctrlListener: ctrlLn,
stopCh: make(chan struct{}),
activeConns: make(map[net.Conn]struct{}),
}
r.cond = sync.NewCond(&r.mu)
return r, nil
}
// Serve starts accept loops for both listeners. Call Stop() to shut down.
func (r *ReplicaReceiver) Serve() {
r.wg.Add(2)
go r.acceptDataLoop()
go r.acceptCtrlLoop()
}
// Stop shuts down both listeners, closes active connections, and waits for goroutines.
func (r *ReplicaReceiver) Stop() {
r.mu.Lock()
if r.stopped {
r.mu.Unlock()
return
}
r.stopped = true
r.mu.Unlock()
close(r.stopCh)
r.dataListener.Close()
r.ctrlListener.Close()
// Close all active connections to unblock ReadFrame calls.
r.connMu.Lock()
for conn := range r.activeConns {
conn.Close()
}
r.connMu.Unlock()
// Wake any barrier waiters so they can exit (must hold mu for cond).
r.mu.Lock()
r.cond.Broadcast()
r.mu.Unlock()
r.wg.Wait()
}
func (r *ReplicaReceiver) trackConn(conn net.Conn) {
r.connMu.Lock()
r.activeConns[conn] = struct{}{}
r.connMu.Unlock()
}
func (r *ReplicaReceiver) untrackConn(conn net.Conn) {
r.connMu.Lock()
delete(r.activeConns, conn)
r.connMu.Unlock()
}
func (r *ReplicaReceiver) acceptDataLoop() {
defer r.wg.Done()
for {
conn, err := r.dataListener.Accept()
if err != nil {
select {
case <-r.stopCh:
return
default:
log.Printf("replica: data accept error: %v", err)
return
}
}
r.trackConn(conn)
r.wg.Add(1)
go func() {
defer r.wg.Done()
defer r.untrackConn(conn)
r.handleDataConn(conn)
}()
}
}
func (r *ReplicaReceiver) acceptCtrlLoop() {
defer r.wg.Done()
for {
conn, err := r.ctrlListener.Accept()
if err != nil {
select {
case <-r.stopCh:
return
default:
log.Printf("replica: ctrl accept error: %v", err)
return
}
}
r.trackConn(conn)
r.wg.Add(1)
go func() {
defer r.wg.Done()
defer r.untrackConn(conn)
r.handleControlConn(conn)
}()
}
}
// handleDataConn reads WAL entry frames and applies them to the local volume.
func (r *ReplicaReceiver) handleDataConn(conn net.Conn) {
defer conn.Close()
for {
select {
case <-r.stopCh:
return
default:
}
msgType, payload, err := ReadFrame(conn)
if err != nil {
select {
case <-r.stopCh:
default:
log.Printf("replica: data read error: %v", err)
}
return
}
if msgType != MsgWALEntry {
log.Printf("replica: unexpected data message type 0x%02x", msgType)
continue
}
if err := r.applyEntry(payload); err != nil {
log.Printf("replica: apply entry error: %v", err)
}
}
}
// applyEntry decodes and applies a single WAL entry to the local volume.
// The entire apply (LSN check → WAL append → dirty map → receivedLSN update)
// is serialized under mu to prevent TOCTOU races between concurrent entries.
func (r *ReplicaReceiver) applyEntry(payload []byte) error {
entry, err := DecodeWALEntry(payload)
if err != nil {
return fmt.Errorf("decode WAL entry: %w", err)
}
// Validate epoch: replicas must NOT accept epoch bumps from WAL stream.
// Only the master can change epochs (via SetEpoch in CP3).
localEpoch := r.vol.epoch.Load()
if entry.Epoch != localEpoch {
return fmt.Errorf("%w: entry epoch %d != local %d", ErrStaleEpoch, entry.Epoch, localEpoch)
}
r.mu.Lock()
defer r.mu.Unlock()
// Enforce contiguous LSN: only accept the next expected entry.
// This prevents gaps that would let a barrier pass incorrectly.
if entry.LSN <= r.receivedLSN {
log.Printf("replica: skipping duplicate/old LSN %d (received %d)", entry.LSN, r.receivedLSN)
return nil
}
if entry.LSN != r.receivedLSN+1 {
return fmt.Errorf("%w: expected LSN %d, got %d (gap)", ErrDuplicateLSN, r.receivedLSN+1, entry.LSN)
}
// Append to local WAL (with retry on WAL full).
walOff, err := r.replicaAppendWithRetry(&entry)
if err != nil {
return fmt.Errorf("WAL append: %w", err)
}
// Update dirty map.
switch entry.Type {
case EntryTypeWrite, EntryTypeTrim:
blocks := entry.Length / r.vol.super.BlockSize
for i := uint32(0); i < blocks; i++ {
r.vol.dirtyMap.Put(entry.LBA+uint64(i), walOff, entry.LSN, r.vol.super.BlockSize)
}
}
// Update receivedLSN and signal barrier waiters.
r.receivedLSN = entry.LSN
r.cond.Broadcast()
return nil
}
// replicaAppendWithRetry appends a WAL entry, retrying on WAL-full by
// triggering the flusher. Caller must hold r.mu.
func (r *ReplicaReceiver) replicaAppendWithRetry(entry *WALEntry) (uint64, error) {
walOff, err := r.vol.wal.Append(entry)
if !errors.Is(err, ErrWALFull) {
return walOff, err
}
deadline := time.After(r.vol.config.WALFullTimeout)
for errors.Is(err, ErrWALFull) {
select {
case <-r.stopCh:
return 0, fmt.Errorf("replica: stopped during WAL retry")
default:
}
if r.vol.flusher != nil {
r.vol.flusher.NotifyUrgent()
}
// Release mu briefly so barrier waiters can proceed and
// the flusher can make progress (it may need dirty map lock).
r.mu.Unlock()
select {
case <-deadline:
r.mu.Lock()
return 0, fmt.Errorf("replica: WAL full timeout: %w", ErrWALFull)
case <-time.After(1 * time.Millisecond):
}
r.mu.Lock()
walOff, err = r.vol.wal.Append(entry)
}
return walOff, err
}
// ReceivedLSN returns the highest LSN received and written to the local WAL.
func (r *ReplicaReceiver) ReceivedLSN() uint64 {
r.mu.Lock()
defer r.mu.Unlock()
return r.receivedLSN
}
// DataAddr returns the data listener's address (useful for tests with :0 ports).
func (r *ReplicaReceiver) DataAddr() string {
return r.dataListener.Addr().String()
}
// CtrlAddr returns the control listener's address.
func (r *ReplicaReceiver) CtrlAddr() string {
return r.ctrlListener.Addr().String()
}