mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
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>
135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
package actions
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
tr "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/testrunner"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/testrunner/infra"
|
|
)
|
|
|
|
// RegisterFaultActions registers fault injection actions.
|
|
func RegisterFaultActions(r *tr.Registry) {
|
|
r.RegisterFunc("inject_netem", tr.TierChaos, injectNetemAction)
|
|
r.RegisterFunc("inject_partition", tr.TierChaos, injectPartitionAction)
|
|
r.RegisterFunc("fill_disk", tr.TierChaos, fillDiskAction)
|
|
r.RegisterFunc("corrupt_wal", tr.TierChaos, corruptWALAction)
|
|
r.RegisterFunc("clear_fault", tr.TierChaos, clearFaultAction)
|
|
}
|
|
|
|
func injectNetemAction(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
node, err := getNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("inject_netem: %w", err)
|
|
}
|
|
|
|
targetIP := act.Params["target_ip"]
|
|
if targetIP == "" {
|
|
return nil, fmt.Errorf("inject_netem: target_ip param required")
|
|
}
|
|
delayMs := parseInt(act.Params["delay_ms"], 200)
|
|
|
|
cleanupCmd, err := infra.InjectNetem(ctx, node, targetIP, delayMs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store cleanup cmd for later clear_fault.
|
|
varKey := "__cleanup_netem"
|
|
if act.Node != "" {
|
|
varKey += "_" + act.Node
|
|
}
|
|
return map[string]string{varKey: cleanupCmd}, nil
|
|
}
|
|
|
|
func injectPartitionAction(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
node, err := getNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("inject_partition: %w", err)
|
|
}
|
|
|
|
targetIP := act.Params["target_ip"]
|
|
if targetIP == "" {
|
|
return nil, fmt.Errorf("inject_partition: target_ip param required")
|
|
}
|
|
ports := parseIntSlice(act.Params["ports"])
|
|
if len(ports) == 0 {
|
|
return nil, fmt.Errorf("inject_partition: ports param required")
|
|
}
|
|
|
|
cleanupCmd, err := infra.InjectIptablesDrop(ctx, node, targetIP, ports)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
varKey := "__cleanup_partition"
|
|
if act.Node != "" {
|
|
varKey += "_" + act.Node
|
|
}
|
|
return map[string]string{varKey: cleanupCmd}, nil
|
|
}
|
|
|
|
func fillDiskAction(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
node, err := getNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fill_disk: %w", err)
|
|
}
|
|
|
|
dir := act.Params["dir"]
|
|
if dir == "" {
|
|
dir = "/tmp"
|
|
}
|
|
|
|
cleanupCmd, err := infra.FillDisk(ctx, node, dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
varKey := "__cleanup_fill_disk"
|
|
if act.Node != "" {
|
|
varKey += "_" + act.Node
|
|
}
|
|
return map[string]string{varKey: cleanupCmd}, nil
|
|
}
|
|
|
|
func corruptWALAction(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
targetName := act.Target
|
|
if targetName == "" {
|
|
return nil, fmt.Errorf("corrupt_wal: target param required")
|
|
}
|
|
|
|
tgt, err := getHATarget(actx, targetName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nBytes := parseInt(act.Params["bytes"], 4096)
|
|
|
|
return nil, infra.CorruptWALRegion(ctx, tgt.Node, tgt.VolFilePath(), nBytes)
|
|
}
|
|
|
|
func clearFaultAction(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
faultType := act.Params["type"]
|
|
if faultType == "" {
|
|
return nil, fmt.Errorf("clear_fault: type param required (netem, partition, fill_disk)")
|
|
}
|
|
|
|
node, err := getNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("clear_fault: %w", err)
|
|
}
|
|
|
|
varKey := "__cleanup_" + faultType
|
|
if act.Node != "" {
|
|
varKey += "_" + act.Node
|
|
}
|
|
|
|
cleanupCmd := actx.Vars[varKey]
|
|
if cleanupCmd == "" {
|
|
// Try without node suffix.
|
|
cleanupCmd = actx.Vars["__cleanup_"+faultType]
|
|
}
|
|
|
|
return nil, infra.ClearFault(ctx, node, cleanupCmd)
|
|
}
|