Files
seaweedfs/weed/storage/blockvol/durability_mode_test.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

164 lines
4.5 KiB
Go

package blockvol
import (
"errors"
"os"
"path/filepath"
"testing"
)
func TestParseDurabilityMode_Valid(t *testing.T) {
tests := []struct {
input string
want DurabilityMode
}{
{"best_effort", DurabilityBestEffort},
{"sync_all", DurabilitySyncAll},
{"sync_quorum", DurabilitySyncQuorum},
{"", DurabilityBestEffort}, // empty = backward compat
}
for _, tt := range tests {
got, err := ParseDurabilityMode(tt.input)
if err != nil {
t.Errorf("ParseDurabilityMode(%q) error: %v", tt.input, err)
}
if got != tt.want {
t.Errorf("ParseDurabilityMode(%q) = %v, want %v", tt.input, got, tt.want)
}
}
}
func TestParseDurabilityMode_Invalid(t *testing.T) {
_, err := ParseDurabilityMode("invalid_mode")
if !errors.Is(err, ErrInvalidDurabilityMode) {
t.Errorf("ParseDurabilityMode(invalid) error = %v, want ErrInvalidDurabilityMode", err)
}
}
func TestDurabilityMode_StringRoundTrip(t *testing.T) {
for _, m := range []DurabilityMode{DurabilityBestEffort, DurabilitySyncAll, DurabilitySyncQuorum} {
s := m.String()
got, err := ParseDurabilityMode(s)
if err != nil {
t.Errorf("round-trip %v -> %q: parse error: %v", m, s, err)
}
if got != m {
t.Errorf("round-trip %v -> %q -> %v", m, s, got)
}
}
}
func TestDurabilityMode_Validate_SyncQuorum_RF2_Rejected(t *testing.T) {
err := DurabilitySyncQuorum.Validate(2)
if !errors.Is(err, ErrSyncQuorumRequiresRF3) {
t.Errorf("Validate(sync_quorum, RF=2) = %v, want ErrSyncQuorumRequiresRF3", err)
}
}
func TestDurabilityMode_Validate_SyncQuorum_RF3_OK(t *testing.T) {
if err := DurabilitySyncQuorum.Validate(3); err != nil {
t.Errorf("Validate(sync_quorum, RF=3) = %v, want nil", err)
}
}
func TestDurabilityMode_Validate_BestEffort_RF1_OK(t *testing.T) {
if err := DurabilityBestEffort.Validate(1); err != nil {
t.Errorf("Validate(best_effort, RF=1) = %v, want nil", err)
}
}
func TestDurabilityMode_RequiredReplicas_SyncAll_RF2(t *testing.T) {
got := DurabilitySyncAll.RequiredReplicas(2)
if got != 1 {
t.Errorf("RequiredReplicas(sync_all, RF=2) = %d, want 1", got)
}
}
func TestDurabilityMode_RequiredReplicas_SyncQuorum_RF3(t *testing.T) {
got := DurabilitySyncQuorum.RequiredReplicas(3)
if got != 1 { // quorum=2, primary counts as 1, need 1 replica
t.Errorf("RequiredReplicas(sync_quorum, RF=3) = %d, want 1", got)
}
}
func TestDurabilityMode_IsStrict(t *testing.T) {
if DurabilityBestEffort.IsStrict() {
t.Error("best_effort should not be strict")
}
if !DurabilitySyncAll.IsStrict() {
t.Error("sync_all should be strict")
}
if !DurabilitySyncQuorum.IsStrict() {
t.Error("sync_quorum should be strict")
}
}
func TestDurabilityMode_CreateCloseOpenRoundTrip(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.blk")
vol, err := CreateBlockVol(path, CreateOptions{
VolumeSize: 4 * 1024 * 1024,
DurabilityMode: DurabilitySyncAll,
})
if err != nil {
t.Fatalf("Create: %v", err)
}
if vol.DurabilityMode() != DurabilitySyncAll {
t.Fatalf("DurabilityMode() = %v, want sync_all", vol.DurabilityMode())
}
vol.Close()
vol2, err := OpenBlockVol(path)
if err != nil {
t.Fatalf("Open: %v", err)
}
defer vol2.Close()
if vol2.DurabilityMode() != DurabilitySyncAll {
t.Errorf("DurabilityMode() after reopen = %v, want sync_all", vol2.DurabilityMode())
}
}
func TestDurabilityMode_Accessor_Default(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.blk")
vol, err := CreateBlockVol(path, CreateOptions{VolumeSize: 4 * 1024 * 1024})
if err != nil {
t.Fatalf("Create: %v", err)
}
defer vol.Close()
if vol.DurabilityMode() != DurabilityBestEffort {
t.Errorf("default DurabilityMode() = %v, want best_effort", vol.DurabilityMode())
}
}
func TestDurabilityMode_InvalidOnDisk_OpenFails(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.blk")
vol, err := CreateBlockVol(path, CreateOptions{VolumeSize: 4 * 1024 * 1024})
if err != nil {
t.Fatalf("Create: %v", err)
}
vol.Close()
// Corrupt the DurabilityMode byte on disk.
// Epoch ends at offset 104. DurabilityMode is at offset 104.
// Actually, let me compute: 4(magic)+2(ver)+2(flags)+16(uuid)+8(volsz)+4(ext)+4(blk)+
// 8(waloff)+8(walsz)+8(walhead)+8(waltail)+8(walcplsn)+4(repl)+8(created)+4(snapcnt)+8(epoch) = 104
f, err := os.OpenFile(path, os.O_RDWR, 0644)
if err != nil {
t.Fatalf("open: %v", err)
}
if _, err := f.WriteAt([]byte{99}, 104); err != nil {
t.Fatalf("write corrupt byte: %v", err)
}
f.Close()
_, err = OpenBlockVol(path)
if err == nil {
t.Fatal("OpenBlockVol should fail with invalid DurabilityMode")
}
}