mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 15:04:37 +00: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>
201 lines
5.7 KiB
Go
201 lines
5.7 KiB
Go
package blockvol
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestSuperblock(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
run func(t *testing.T)
|
|
}{
|
|
{name: "superblock_roundtrip", run: testSuperblockRoundtrip},
|
|
{name: "superblock_magic_check", run: testSuperblockMagicCheck},
|
|
{name: "superblock_version_check", run: testSuperblockVersionCheck},
|
|
{name: "superblock_zero_vol_size", run: testSuperblockZeroVolSize},
|
|
{name: "durability_mode_roundtrip", run: testDurabilityModeRoundtrip},
|
|
{name: "durability_mode_v1_compat", run: testDurabilityModeV1Compat},
|
|
{name: "durability_mode_invalid_rejected", run: testDurabilityModeInvalidRejected},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.run(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testSuperblockRoundtrip(t *testing.T) {
|
|
sb, err := NewSuperblock(100*1024*1024*1024, CreateOptions{
|
|
ExtentSize: 64 * 1024,
|
|
BlockSize: 4096,
|
|
WALSize: 64 * 1024 * 1024,
|
|
Replication: "010",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSuperblock: %v", err)
|
|
}
|
|
sb.CreatedAt = 1700000000
|
|
sb.SnapshotCount = 3
|
|
sb.WALHead = 8192
|
|
sb.WALCheckpointLSN = 42
|
|
|
|
var buf bytes.Buffer
|
|
n, err := sb.WriteTo(&buf)
|
|
if err != nil {
|
|
t.Fatalf("WriteTo: %v", err)
|
|
}
|
|
if n != SuperblockSize {
|
|
t.Fatalf("WriteTo wrote %d bytes, want %d", n, SuperblockSize)
|
|
}
|
|
|
|
got, err := ReadSuperblock(&buf)
|
|
if err != nil {
|
|
t.Fatalf("ReadSuperblock: %v", err)
|
|
}
|
|
|
|
if string(got.Magic[:]) != MagicSWBK {
|
|
t.Errorf("Magic = %q, want %q", got.Magic, MagicSWBK)
|
|
}
|
|
if got.Version != CurrentVersion {
|
|
t.Errorf("Version = %d, want %d", got.Version, CurrentVersion)
|
|
}
|
|
if got.UUID != sb.UUID {
|
|
t.Errorf("UUID mismatch")
|
|
}
|
|
if got.VolumeSize != sb.VolumeSize {
|
|
t.Errorf("VolumeSize = %d, want %d", got.VolumeSize, sb.VolumeSize)
|
|
}
|
|
if got.ExtentSize != sb.ExtentSize {
|
|
t.Errorf("ExtentSize = %d, want %d", got.ExtentSize, sb.ExtentSize)
|
|
}
|
|
if got.BlockSize != sb.BlockSize {
|
|
t.Errorf("BlockSize = %d, want %d", got.BlockSize, sb.BlockSize)
|
|
}
|
|
if got.WALOffset != sb.WALOffset {
|
|
t.Errorf("WALOffset = %d, want %d", got.WALOffset, sb.WALOffset)
|
|
}
|
|
if got.WALSize != sb.WALSize {
|
|
t.Errorf("WALSize = %d, want %d", got.WALSize, sb.WALSize)
|
|
}
|
|
if got.WALHead != sb.WALHead {
|
|
t.Errorf("WALHead = %d, want %d", got.WALHead, sb.WALHead)
|
|
}
|
|
if got.WALCheckpointLSN != sb.WALCheckpointLSN {
|
|
t.Errorf("WALCheckpointLSN = %d, want %d", got.WALCheckpointLSN, sb.WALCheckpointLSN)
|
|
}
|
|
if got.Replication != sb.Replication {
|
|
t.Errorf("Replication = %q, want %q", got.Replication, sb.Replication)
|
|
}
|
|
if got.CreatedAt != sb.CreatedAt {
|
|
t.Errorf("CreatedAt = %d, want %d", got.CreatedAt, sb.CreatedAt)
|
|
}
|
|
if got.SnapshotCount != sb.SnapshotCount {
|
|
t.Errorf("SnapshotCount = %d, want %d", got.SnapshotCount, sb.SnapshotCount)
|
|
}
|
|
}
|
|
|
|
func testSuperblockMagicCheck(t *testing.T) {
|
|
// Write a valid superblock, then corrupt the magic bytes.
|
|
sb, _ := NewSuperblock(1*1024*1024*1024, CreateOptions{})
|
|
var buf bytes.Buffer
|
|
sb.WriteTo(&buf)
|
|
|
|
data := buf.Bytes()
|
|
copy(data[0:4], "XXXX") // corrupt magic
|
|
|
|
_, err := ReadSuperblock(bytes.NewReader(data))
|
|
if !errors.Is(err, ErrNotBlockVol) {
|
|
t.Errorf("expected ErrNotBlockVol, got %v", err)
|
|
}
|
|
}
|
|
|
|
func testSuperblockVersionCheck(t *testing.T) {
|
|
sb, _ := NewSuperblock(1*1024*1024*1024, CreateOptions{})
|
|
var buf bytes.Buffer
|
|
sb.WriteTo(&buf)
|
|
|
|
data := buf.Bytes()
|
|
// Version is at offset 4, uint16 little-endian.
|
|
binary.LittleEndian.PutUint16(data[4:], 99)
|
|
|
|
_, err := ReadSuperblock(bytes.NewReader(data))
|
|
if !errors.Is(err, ErrUnsupportedVersion) {
|
|
t.Errorf("expected ErrUnsupportedVersion, got %v", err)
|
|
}
|
|
}
|
|
|
|
func testSuperblockZeroVolSize(t *testing.T) {
|
|
_, err := NewSuperblock(0, CreateOptions{})
|
|
if !errors.Is(err, ErrInvalidVolumeSize) {
|
|
t.Errorf("NewSuperblock(0): expected ErrInvalidVolumeSize, got %v", err)
|
|
}
|
|
|
|
// Also test reading a superblock with zero volume size.
|
|
sb, _ := NewSuperblock(1*1024*1024*1024, CreateOptions{})
|
|
var buf bytes.Buffer
|
|
sb.WriteTo(&buf)
|
|
|
|
data := buf.Bytes()
|
|
// VolumeSize is at offset 4+2+2+16 = 24, uint64 little-endian.
|
|
binary.LittleEndian.PutUint64(data[24:], 0)
|
|
|
|
_, err = ReadSuperblock(bytes.NewReader(data))
|
|
if !errors.Is(err, ErrInvalidVolumeSize) {
|
|
t.Errorf("ReadSuperblock(vol_size=0): expected ErrInvalidVolumeSize, got %v", err)
|
|
}
|
|
}
|
|
|
|
func testDurabilityModeRoundtrip(t *testing.T) {
|
|
sb, err := NewSuperblock(1*1024*1024*1024, CreateOptions{
|
|
DurabilityMode: DurabilitySyncAll,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSuperblock: %v", err)
|
|
}
|
|
if sb.DurabilityMode != uint8(DurabilitySyncAll) {
|
|
t.Fatalf("NewSuperblock DurabilityMode = %d, want %d", sb.DurabilityMode, DurabilitySyncAll)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
sb.WriteTo(&buf)
|
|
|
|
got, err := ReadSuperblock(&buf)
|
|
if err != nil {
|
|
t.Fatalf("ReadSuperblock: %v", err)
|
|
}
|
|
if got.DurabilityMode != uint8(DurabilitySyncAll) {
|
|
t.Errorf("DurabilityMode = %d, want %d", got.DurabilityMode, DurabilitySyncAll)
|
|
}
|
|
}
|
|
|
|
func testDurabilityModeV1Compat(t *testing.T) {
|
|
// V1 file has zeros in the DurabilityMode region → reads as best_effort.
|
|
sb, _ := NewSuperblock(1*1024*1024*1024, CreateOptions{})
|
|
var buf bytes.Buffer
|
|
sb.WriteTo(&buf)
|
|
|
|
got, err := ReadSuperblock(&buf)
|
|
if err != nil {
|
|
t.Fatalf("ReadSuperblock: %v", err)
|
|
}
|
|
if DurabilityMode(got.DurabilityMode) != DurabilityBestEffort {
|
|
t.Errorf("V1 DurabilityMode = %d, want %d (best_effort)", got.DurabilityMode, DurabilityBestEffort)
|
|
}
|
|
}
|
|
|
|
func testDurabilityModeInvalidRejected(t *testing.T) {
|
|
sb, _ := NewSuperblock(1*1024*1024*1024, CreateOptions{})
|
|
sb.DurabilityMode = 99
|
|
|
|
err := sb.Validate()
|
|
if err == nil {
|
|
t.Error("Validate should reject DurabilityMode=99")
|
|
}
|
|
if !errors.Is(err, ErrInvalidSuperblock) {
|
|
t.Errorf("Validate error = %v, want ErrInvalidSuperblock", err)
|
|
}
|
|
}
|