mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-10 13:58:24 +01:00
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>
198 lines
4.4 KiB
Go
198 lines
4.4 KiB
Go
package blockvol
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ErrReplicaDegraded = errors.New("blockvol: replica degraded")
|
|
ErrShipperStopped = errors.New("blockvol: shipper stopped")
|
|
)
|
|
|
|
const barrierTimeout = 5 * time.Second
|
|
|
|
// WALShipper streams WAL entries from the primary to a replica over TCP.
|
|
// Fire-and-forget: no per-entry ACK. Barriers provide durability confirmation.
|
|
type WALShipper struct {
|
|
dataAddr string
|
|
controlAddr string
|
|
epochFn func() uint64
|
|
|
|
mu sync.Mutex // protects dataConn
|
|
dataConn net.Conn
|
|
|
|
ctrlMu sync.Mutex // protects ctrlConn
|
|
ctrlConn net.Conn
|
|
|
|
shippedLSN atomic.Uint64
|
|
degraded atomic.Bool
|
|
stopped atomic.Bool
|
|
}
|
|
|
|
// NewWALShipper creates a WAL shipper. Connections are established lazily on
|
|
// first Ship/Barrier call. epochFn returns the current epoch for validation.
|
|
func NewWALShipper(dataAddr, controlAddr string, epochFn func() uint64) *WALShipper {
|
|
return &WALShipper{
|
|
dataAddr: dataAddr,
|
|
controlAddr: controlAddr,
|
|
epochFn: epochFn,
|
|
}
|
|
}
|
|
|
|
// Ship sends a WAL entry to the replica over the data channel.
|
|
// On write error, the shipper enters degraded mode permanently.
|
|
func (s *WALShipper) Ship(entry *WALEntry) error {
|
|
if s.stopped.Load() || s.degraded.Load() {
|
|
return nil
|
|
}
|
|
|
|
// Validate epoch: drop stale entries.
|
|
if entry.Epoch != s.epochFn() {
|
|
log.Printf("wal_shipper: dropping entry LSN=%d with stale epoch %d (current %d)",
|
|
entry.LSN, entry.Epoch, s.epochFn())
|
|
return nil
|
|
}
|
|
|
|
encoded, err := entry.Encode()
|
|
if err != nil {
|
|
return fmt.Errorf("wal_shipper: encode entry: %w", err)
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if err := s.ensureDataConn(); err != nil {
|
|
s.markDegraded()
|
|
return nil
|
|
}
|
|
|
|
if err := WriteFrame(s.dataConn, MsgWALEntry, encoded); err != nil {
|
|
s.markDegraded()
|
|
return nil
|
|
}
|
|
|
|
s.shippedLSN.Store(entry.LSN)
|
|
return nil
|
|
}
|
|
|
|
// Barrier sends a barrier request on the control channel and waits for the
|
|
// replica to confirm durability up to lsnMax. Returns ErrReplicaDegraded if
|
|
// the shipper is in degraded mode.
|
|
func (s *WALShipper) Barrier(lsnMax uint64) error {
|
|
if s.stopped.Load() {
|
|
return ErrShipperStopped
|
|
}
|
|
if s.degraded.Load() {
|
|
return ErrReplicaDegraded
|
|
}
|
|
|
|
req := EncodeBarrierRequest(BarrierRequest{
|
|
LSN: lsnMax,
|
|
Epoch: s.epochFn(),
|
|
})
|
|
|
|
s.ctrlMu.Lock()
|
|
defer s.ctrlMu.Unlock()
|
|
|
|
if err := s.ensureCtrlConn(); err != nil {
|
|
s.markDegraded()
|
|
return ErrReplicaDegraded
|
|
}
|
|
|
|
s.ctrlConn.SetDeadline(time.Now().Add(barrierTimeout))
|
|
|
|
if err := WriteFrame(s.ctrlConn, MsgBarrierReq, req); err != nil {
|
|
s.markDegraded()
|
|
return ErrReplicaDegraded
|
|
}
|
|
|
|
msgType, payload, err := ReadFrame(s.ctrlConn)
|
|
if err != nil {
|
|
s.markDegraded()
|
|
return ErrReplicaDegraded
|
|
}
|
|
|
|
if msgType != MsgBarrierResp || len(payload) < 1 {
|
|
s.markDegraded()
|
|
return ErrReplicaDegraded
|
|
}
|
|
|
|
switch payload[0] {
|
|
case BarrierOK:
|
|
return nil
|
|
case BarrierEpochMismatch:
|
|
return fmt.Errorf("wal_shipper: barrier epoch mismatch")
|
|
case BarrierTimeout:
|
|
return fmt.Errorf("wal_shipper: barrier timeout on replica")
|
|
case BarrierFsyncFailed:
|
|
return fmt.Errorf("wal_shipper: barrier fsync failed on replica")
|
|
default:
|
|
return fmt.Errorf("wal_shipper: unknown barrier status %d", payload[0])
|
|
}
|
|
}
|
|
|
|
// ShippedLSN returns the highest LSN successfully sent to the replica.
|
|
func (s *WALShipper) ShippedLSN() uint64 {
|
|
return s.shippedLSN.Load()
|
|
}
|
|
|
|
// IsDegraded returns true if the replica is unreachable.
|
|
func (s *WALShipper) IsDegraded() bool {
|
|
return s.degraded.Load()
|
|
}
|
|
|
|
// Stop shuts down the shipper and closes connections.
|
|
func (s *WALShipper) Stop() {
|
|
if s.stopped.Swap(true) {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
if s.dataConn != nil {
|
|
s.dataConn.Close()
|
|
s.dataConn = nil
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
s.ctrlMu.Lock()
|
|
if s.ctrlConn != nil {
|
|
s.ctrlConn.Close()
|
|
s.ctrlConn = nil
|
|
}
|
|
s.ctrlMu.Unlock()
|
|
}
|
|
|
|
func (s *WALShipper) ensureDataConn() error {
|
|
if s.dataConn != nil {
|
|
return nil
|
|
}
|
|
conn, err := net.DialTimeout("tcp", s.dataAddr, 3*time.Second)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.dataConn = conn
|
|
return nil
|
|
}
|
|
|
|
func (s *WALShipper) ensureCtrlConn() error {
|
|
if s.ctrlConn != nil {
|
|
return nil
|
|
}
|
|
conn, err := net.DialTimeout("tcp", s.controlAddr, 3*time.Second)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.ctrlConn = conn
|
|
return nil
|
|
}
|
|
|
|
func (s *WALShipper) markDegraded() {
|
|
s.degraded.Store(true)
|
|
log.Printf("wal_shipper: replica degraded (data=%s, ctrl=%s)", s.dataAddr, s.controlAddr)
|
|
}
|