Files
seaweedfs/weed/storage/blockvol/dist_group_commit.go
T
Ping Qiu 8d6379f841 feat: replica state machine + barrier eligibility gating (CP13-4)
Replaces binary degraded flag with ReplicaState type:
Disconnected, Connecting, CatchingUp, InSync, Degraded, NeedsRebuild.

Ship() allowed from Disconnected (bootstrap: data must flow before
first barrier) and InSync (steady state). Ship does NOT change state.

Barrier() gating:
- InSync: proceed normally
- Disconnected: bootstrap path (connect + barrier)
- Degraded: reconnect both data+ctrl connections, then barrier
- Connecting/CatchingUp/NeedsRebuild: rejected immediately

Only barrier success grants InSync. Reconnect alone does not.

IsDegraded() now means "not sync-eligible" (any non-InSync state).
InSyncCount() added to ShipperGroup.

dist_group_commit.go: removed AllDegraded short-circuit that
prevented bootstrap. Barrier attempts always run — individual
shippers handle their own state-based gating.

8 CP13-4 tests + TestBarrier_RejectsReplicaNotInSync flips FAIL→PASS.
All previously-passing baseline tests remain green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 02:39:32 -07:00

84 lines
2.4 KiB
Go

package blockvol
import (
"fmt"
"sync"
)
// MakeDistributedSync creates a sync function that runs local WAL fsync and
// replica barriers in parallel. Supports N replicas via ShipperGroup.
//
// Durability semantics depend on vol.DurabilityMode():
// - best_effort: local fsync = ACK; replica failures degrade shippers but never fail writes
// - sync_all: ALL replica barriers must succeed, else write returns ErrDurabilityBarrierFailed
// - sync_quorum: quorum (RF/2+1) of nodes must be durable, else ErrDurabilityQuorumLost
func MakeDistributedSync(walSync func() error, group *ShipperGroup, vol *BlockVol) func() error {
return func() error {
mode := vol.DurabilityMode()
if group == nil || group.Len() == 0 {
// No replicas configured — local sync only.
return walSync()
}
// Note: we always attempt BarrierAll, even when all shippers are
// Disconnected or Degraded. Barrier() handles bootstrap (Disconnected)
// and reconnect (Degraded) paths. Only Connecting/CatchingUp/NeedsRebuild
// are pre-rejected by individual shippers.
// The highest LSN that needs to be durable is nextLSN-1.
lsnMax := vol.nextLSN.Load() - 1
var localErr error
var barrierErrs []error
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
localErr = walSync()
}()
go func() {
defer wg.Done()
barrierErrs = group.BarrierAll(lsnMax)
}()
wg.Wait()
if localErr != nil {
return localErr
}
// Count barrier failures and degrade shippers.
failCount := 0
for _, err := range barrierErrs {
if err != nil {
failCount++
vol.degradeReplica(err)
}
}
switch mode {
case DurabilitySyncAll:
if failCount > 0 {
if vol.Metrics != nil {
vol.Metrics.DurabilityBarrierFailedTotal.Add(1)
}
return fmt.Errorf("%w: %d of %d barriers failed",
ErrDurabilityBarrierFailed, failCount, len(barrierErrs))
}
case DurabilitySyncQuorum:
rf := group.Len() + 1 // total nodes including primary
quorum := rf/2 + 1
durableNodes := 1 + (len(barrierErrs) - failCount) // primary + successful barriers
if durableNodes < quorum {
if vol.Metrics != nil {
vol.Metrics.DurabilityQuorumLostTotal.Add(1)
}
return fmt.Errorf("%w: %d durable of %d needed",
ErrDurabilityQuorumLost, durableNodes, quorum)
}
}
// best_effort: barrier failures already logged via degradeReplica, return nil.
return nil
}
}