Files
seaweedfs/weed/storage/blockvol/testrunner/actions/fault.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

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)
}