Files
seaweedfs/weed/storage/blockvol/replica_barrier.go
T
pingqiuandClaude Opus 4.6 59a36013d4 feat: rebuild hardening A1-A5 + session-controlled execution path
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>
2026-04-08 14:39:11 -07:00

163 lines
4.0 KiB
Go

package blockvol
import (
"log"
"net"
"sync"
"time"
)
// handleControlConn reads barrier requests from the control channel and
// responds with barrier status after ensuring durability.
func (r *ReplicaReceiver) handleControlConn(conn net.Conn) {
defer conn.Close()
var writeMu sync.Mutex
writeFrame := func(msgType byte, payload []byte) error {
writeMu.Lock()
defer writeMu.Unlock()
return WriteFrame(conn, msgType, payload)
}
for {
select {
case <-r.stopCh:
return
default:
}
msgType, payload, err := ReadFrame(conn)
if err != nil {
select {
case <-r.stopCh:
default:
log.Printf("replica: ctrl read error: %v", err)
}
return
}
switch msgType {
case MsgBarrierReq:
req, err := DecodeBarrierRequest(payload)
if err != nil {
log.Printf("replica: decode barrier request: %v", err)
continue
}
resp := r.handleBarrier(req)
respPayload := EncodeBarrierResponse(resp)
if err := writeFrame(MsgBarrierResp, respPayload); err != nil {
log.Printf("replica: write barrier response: %v", err)
return
}
case MsgSessionControl:
if err := r.handleSessionControl(conn, payload, writeFrame); err != nil {
log.Printf("replica: session control error: %v", err)
return
}
default:
log.Printf("replica: unexpected ctrl message type 0x%02x", msgType)
}
}
}
func (r *ReplicaReceiver) handleSessionControl(conn net.Conn, payload []byte, writeFrame func(byte, []byte) error) error {
ctrl, err := DecodeSessionControl(payload)
if err != nil {
return err
}
switch ctrl.Command {
case SessionCmdStartRebuild:
r.vol.SetOnRebuildSessionAck(func(ack SessionAckMsg) {
if ack.SessionID != ctrl.SessionID {
return
}
if err := writeFrame(MsgSessionAck, EncodeSessionAck(ack)); err != nil {
log.Printf("replica: write session ack: %v", err)
}
})
if err := r.vol.StartRebuildSession(RebuildSessionConfig{
SessionID: ctrl.SessionID,
Epoch: ctrl.Epoch,
BaseLSN: ctrl.BaseLSN,
TargetLSN: ctrl.TargetLSN,
}); err != nil {
_ = writeFrame(MsgSessionAck, EncodeSessionAck(SessionAckMsg{
Epoch: ctrl.Epoch,
SessionID: ctrl.SessionID,
Phase: SessionAckFailed,
WALAppliedLSN: ctrl.BaseLSN,
}))
return err
}
return nil
case SessionCmdCancel:
if err := r.vol.CancelRebuildSession(ctrl.SessionID, "remote_cancel"); err != nil {
return err
}
return nil
default:
return net.InvalidAddrError("unsupported session control command")
}
}
// handleBarrier waits until all WAL entries up to req.LSN have been received,
// then fsyncs the WAL to ensure durability.
func (r *ReplicaReceiver) handleBarrier(req BarrierRequest) BarrierResponse {
// Fail fast on epoch mismatch.
localEpoch := r.vol.epoch.Load()
if req.Epoch != localEpoch {
return BarrierResponse{Status: BarrierEpochMismatch}
}
// Use a timer goroutine to wake us on deadline.
timer := time.NewTimer(r.barrierTimeout)
defer timer.Stop()
timedOut := make(chan struct{})
go func() {
select {
case <-timer.C:
close(timedOut)
// Broadcast to wake any cond.Wait blocked in the loop below.
r.mu.Lock()
r.cond.Broadcast()
r.mu.Unlock()
case <-r.stopCh:
}
}()
r.mu.Lock()
for r.receivedLSN < req.LSN {
// Check if timed out or stopped.
select {
case <-timedOut:
r.mu.Unlock()
return BarrierResponse{Status: BarrierTimeout}
case <-r.stopCh:
r.mu.Unlock()
return BarrierResponse{Status: BarrierTimeout}
default:
}
// Block on cond.Wait -- woken by applyEntry or timeout goroutine.
r.cond.Wait()
}
r.mu.Unlock()
// fsync WAL AFTER confirming all entries received.
if err := r.vol.fd.Sync(); err != nil {
log.Printf("replica: barrier fsync error: %v", err)
return BarrierResponse{Status: BarrierFsyncFailed}
}
// Advance durable progress. Only after fd.Sync() succeeds and contiguous
// receipt through req.LSN has been proven (step 2 above).
r.mu.Lock()
if req.LSN > r.flushedLSN {
r.flushedLSN = req.LSN
}
flushed := r.flushedLSN
r.mu.Unlock()
return BarrierResponse{Status: BarrierOK, FlushedLSN: flushed}
}