Files
seaweedfs/weed/storage/blockvol/dist_group_commit.go
T
Ping QiuandClaude Opus 4.6 da1b81d1c9 feat: CP8-3-1 durability modes + testrunner platform + 21 adversarial tests
Durability mode implementation (sync_all, sync_quorum, best_effort):
- DurabilityMode type with superblock persistence, parse/validate/string
- MakeDistributedSync mode-aware barrier enforcement in dist_group_commit
- blockerr sentinel package (ErrDurabilityBarrierFailed, ErrDurabilityQuorumLost)
- gRPC create path: mode validation, idempotent create consistency, partial cleanup
- F1: strict mode rejects partial replica provisioning with cleanup
- F3: empty heartbeat does not overwrite persisted strict mode
- F4: SCSI error mapping uses errors.Is sentinels (not string matching)
- Proto/wire/blockapi/CLI/UI plumbing for durability_mode field
- Observability dashboard: cluster health cards + per-volume columns

Testrunner platform (YAML-driven integration test framework):
- Engine, parser, registry, reporter (JUnit XML + HTML), metrics scraping
- 52 registered actions: block, iSCSI, I/O, fault injection, assertions
- Baseline regression framework with 7 hard-fail conditions
- 15 YAML scenarios (smoke, crash, HA, fault, consistency, snapshot)
- 49 unit tests for testrunner internals

QA adversarial suite (21 tests, all PASS):
- Idempotent create mode/RF mismatch detection
- Heartbeat mode downgrade prevention (F3)
- sync_all/sync_quorum partial replica enforcement (F1)
- Concurrent create race safety
- Failover/expand mode preservation
- Cleanup resilience when delete fails
- Master restart auto-register mode handling
- Superblock roundtrip all 3 modes
- Validate edge cases (mode×RF matrix)
- RequiredReplicas quorum math verification
- Sentinel error categorization

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

100 lines
2.8 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 || group.AllDegraded() {
// No healthy replicas available.
switch mode {
case DurabilitySyncAll:
if group != nil && (group.Len() > 0 || group.AllDegraded()) {
if vol.Metrics != nil {
vol.Metrics.DurabilityBarrierFailedTotal.Add(1)
}
return ErrDurabilityBarrierFailed
}
case DurabilitySyncQuorum:
if group != nil && group.Len() > 0 {
// quorum = (Len+1)/2+1; with 0 healthy replicas, only primary is durable
rf := group.Len() + 1
quorum := rf/2 + 1
if 1 < quorum { // primary alone doesn't meet quorum
if vol.Metrics != nil {
vol.Metrics.DurabilityQuorumLostTotal.Add(1)
}
return ErrDurabilityQuorumLost
}
}
}
return walSync()
}
// 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
}
}