mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-25 17:34:28 +00:00
CP8-1: HTTP REST API (create/delete/lookup/list/assign/servers), blockapi Go client with multi-master failover, 5 shell commands, HTML dashboard at /block/. CP8-2: RF=2/RF=3 multi-replica support -- ShipperGroup fan-out, distributed sync, health scoring, segment-based scrub, gated promotion (heartbeat freshness + WAL LSN + role checks), failover/rebuild for N>2 replicas. CP8-3: CSI snapshot + expansion -- CreateSnapshot/DeleteSnapshot/ListSnapshots RPCs, NodeExpandVolume with iSCSI rescan, snapshot ID helpers, 20 adversarial tests covering concurrent ops, edge cases, and error injection. CP8-4: Observability -- EngineMetrics atomic counters for flusher/group-commit/ WAL-shipper/scrub, 10 new Prometheus metrics, barrier_lag_lsn SLO gauge, failover/promotion/rebuild counters, request ID correlation in master gRPC logs, baseline regression framework with 7 hard-fail conditions. Total: 63 files, ~11.2K LOC, 160+ new tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
298 lines
7.3 KiB
Go
298 lines
7.3 KiB
Go
package blockvol
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func createScrubVol(t *testing.T) *BlockVol {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
cfg := DefaultConfig()
|
|
cfg.FlushInterval = 5 * time.Millisecond
|
|
v, err := CreateBlockVol(filepath.Join(dir, "scrub.blk"), CreateOptions{
|
|
VolumeSize: 64 * 1024, // 64 KiB = 16 blocks
|
|
BlockSize: 4096,
|
|
WALSize: 64 * 1024,
|
|
}, cfg)
|
|
if err != nil {
|
|
t.Fatalf("CreateBlockVol: %v", err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func TestScrub_CleanVolume(t *testing.T) {
|
|
v := createScrubVol(t)
|
|
defer v.Close()
|
|
|
|
scrubber := NewScrubber(v, 24*time.Hour)
|
|
scrubber.throttle = 0
|
|
|
|
// First pass: establishes baseline CRCs.
|
|
scrubber.runPass()
|
|
stats := scrubber.Stats()
|
|
if stats.PassCount != 1 {
|
|
t.Fatalf("PassCount: got %d, want 1", stats.PassCount)
|
|
}
|
|
if stats.ErrorCount != 0 {
|
|
t.Fatalf("ErrorCount: got %d, want 0", stats.ErrorCount)
|
|
}
|
|
|
|
// Second pass: same data, no errors.
|
|
scrubber.runPass()
|
|
stats = scrubber.Stats()
|
|
if stats.PassCount != 2 {
|
|
t.Fatalf("PassCount: got %d, want 2", stats.PassCount)
|
|
}
|
|
if stats.ErrorCount != 0 {
|
|
t.Fatalf("ErrorCount: got %d, want 0", stats.ErrorCount)
|
|
}
|
|
}
|
|
|
|
func TestScrub_DetectCorruption(t *testing.T) {
|
|
v := createScrubVol(t)
|
|
defer v.Close()
|
|
|
|
// Write some data and flush it to extent.
|
|
data := make([]byte, 4096)
|
|
for i := range data {
|
|
data[i] = 0xAA
|
|
}
|
|
if err := v.WriteLBA(0, data); err != nil {
|
|
t.Fatalf("WriteLBA: %v", err)
|
|
}
|
|
if err := v.SyncCache(); err != nil {
|
|
t.Fatalf("SyncCache: %v", err)
|
|
}
|
|
// Wait for flusher to flush.
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
scrubber := NewScrubber(v, 24*time.Hour)
|
|
scrubber.throttle = 0
|
|
|
|
// First pass: establish baseline.
|
|
scrubber.runPass()
|
|
if scrubber.Stats().ErrorCount != 0 {
|
|
t.Fatalf("first pass errors: %d", scrubber.Stats().ErrorCount)
|
|
}
|
|
|
|
// Corrupt the extent data directly.
|
|
extentStart := v.super.WALOffset + v.super.WALSize
|
|
corruptBuf := []byte{0xFF, 0xFF, 0xFF, 0xFF}
|
|
v.fd.WriteAt(corruptBuf, int64(extentStart))
|
|
v.fd.Sync()
|
|
|
|
// Second pass: should detect corruption.
|
|
scrubber.runPass()
|
|
stats := scrubber.Stats()
|
|
if stats.ErrorCount == 0 {
|
|
t.Fatal("expected corruption error but got none")
|
|
}
|
|
}
|
|
|
|
func TestScrub_SkipDirtyBlocks(t *testing.T) {
|
|
v := createScrubVol(t)
|
|
defer v.Close()
|
|
|
|
scrubber := NewScrubber(v, 24*time.Hour)
|
|
scrubber.throttle = 0
|
|
|
|
// First pass: baseline.
|
|
scrubber.runPass()
|
|
|
|
// Write data (stays in dirty map since flusher interval is slow relative to us).
|
|
data := make([]byte, 4096)
|
|
for i := range data {
|
|
data[i] = 0xBB
|
|
}
|
|
if err := v.WriteLBA(0, data); err != nil {
|
|
t.Fatalf("WriteLBA: %v", err)
|
|
}
|
|
|
|
// Second pass: segment 0 has dirty blocks, should be skipped.
|
|
scrubber.runPass()
|
|
stats := scrubber.Stats()
|
|
if stats.ErrorCount != 0 {
|
|
t.Fatalf("dirty block should be skipped, not flagged as error: errors=%d", stats.ErrorCount)
|
|
}
|
|
}
|
|
|
|
func TestScrub_SkipRecentlyWritten(t *testing.T) {
|
|
v := createScrubVol(t)
|
|
defer v.Close()
|
|
|
|
scrubber := NewScrubber(v, 24*time.Hour)
|
|
scrubber.throttle = 0
|
|
|
|
// First pass: baseline.
|
|
scrubber.runPass()
|
|
|
|
// Write, sync, flush (data in extent, dirty map cleared).
|
|
data := make([]byte, 4096)
|
|
for i := range data {
|
|
data[i] = 0xCC
|
|
}
|
|
if err := v.WriteLBA(0, data); err != nil {
|
|
t.Fatalf("WriteLBA: %v", err)
|
|
}
|
|
v.SyncCache()
|
|
time.Sleep(50 * time.Millisecond) // let flusher clear dirty map
|
|
|
|
// Manually notify scrubber of write (simulates hook in appendWithRetry).
|
|
scrubber.NotifyWrite(0, 1)
|
|
|
|
// Second pass: segment 0 was recently written, skip CRC comparison.
|
|
scrubber.runPass()
|
|
stats := scrubber.Stats()
|
|
if stats.ErrorCount != 0 {
|
|
t.Fatalf("recently written segment should be skipped: errors=%d", stats.ErrorCount)
|
|
}
|
|
}
|
|
|
|
func TestScrub_StatsUpdated(t *testing.T) {
|
|
v := createScrubVol(t)
|
|
defer v.Close()
|
|
|
|
scrubber := NewScrubber(v, 24*time.Hour)
|
|
scrubber.throttle = 0
|
|
scrubber.runPass()
|
|
|
|
stats := scrubber.Stats()
|
|
if stats.LastPassTime == 0 {
|
|
t.Fatal("LastPassTime should be set")
|
|
}
|
|
if stats.SegmentsTotal == 0 {
|
|
t.Fatal("SegmentsTotal should be > 0")
|
|
}
|
|
}
|
|
|
|
func TestScrub_TriggerNow(t *testing.T) {
|
|
v := createScrubVol(t)
|
|
defer v.Close()
|
|
|
|
scrubber := NewScrubber(v, 1*time.Hour) // long interval
|
|
scrubber.throttle = 0
|
|
scrubber.Start()
|
|
defer scrubber.Stop()
|
|
|
|
scrubber.TriggerNow()
|
|
// Wait for the triggered pass to complete.
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
stats := scrubber.Stats()
|
|
if stats.PassCount < 1 {
|
|
t.Fatalf("TriggerNow did not cause a pass: PassCount=%d", stats.PassCount)
|
|
}
|
|
}
|
|
|
|
func TestScrub_StopIdempotent(t *testing.T) {
|
|
v := createScrubVol(t)
|
|
defer v.Close()
|
|
|
|
scrubber := NewScrubber(v, 24*time.Hour)
|
|
scrubber.Start()
|
|
scrubber.Stop()
|
|
scrubber.Stop() // second stop should be no-op
|
|
}
|
|
|
|
func TestScrub_HealthScoreImpact(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := DefaultConfig()
|
|
cfg.FlushInterval = 5 * time.Millisecond
|
|
path := filepath.Join(dir, "health.blk")
|
|
v, err := CreateBlockVol(path, CreateOptions{
|
|
VolumeSize: 64 * 1024,
|
|
BlockSize: 4096,
|
|
WALSize: 64 * 1024,
|
|
}, cfg)
|
|
if err != nil {
|
|
t.Fatalf("CreateBlockVol: %v", err)
|
|
}
|
|
defer v.Close()
|
|
|
|
if v.HealthScore() != 1.0 {
|
|
t.Fatalf("initial health: got %f, want 1.0", v.HealthScore())
|
|
}
|
|
|
|
// Write + flush to extent.
|
|
data := make([]byte, 4096)
|
|
for i := range data {
|
|
data[i] = 0xDD
|
|
}
|
|
v.WriteLBA(0, data)
|
|
v.SyncCache()
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
scrubber := NewScrubber(v, 24*time.Hour)
|
|
scrubber.throttle = 0
|
|
scrubber.runPass()
|
|
|
|
// Corrupt extent.
|
|
extentStart := v.super.WALOffset + v.super.WALSize
|
|
corruptBuf := []byte{0xFF}
|
|
v.fd.WriteAt(corruptBuf, int64(extentStart))
|
|
v.fd.Sync()
|
|
|
|
scrubber.runPass()
|
|
if v.HealthScore() >= 1.0 {
|
|
t.Fatalf("health should drop after corruption: got %f", v.HealthScore())
|
|
}
|
|
|
|
// Cleanup temp files on Windows: remove delta files if any.
|
|
_ = os.RemoveAll(dir)
|
|
}
|
|
|
|
// Fix #5: Write arriving during a scrub pass must not cause a false corruption detection.
|
|
func TestScrub_InPassWrite_NoFalsePositive(t *testing.T) {
|
|
v := createScrubVol(t)
|
|
defer v.Close()
|
|
|
|
// Write initial data and flush to extent.
|
|
data := make([]byte, 4096)
|
|
for i := range data {
|
|
data[i] = 0xAA
|
|
}
|
|
if err := v.WriteLBA(0, data); err != nil {
|
|
t.Fatalf("WriteLBA: %v", err)
|
|
}
|
|
v.SyncCache()
|
|
time.Sleep(50 * time.Millisecond) // let flusher clear dirty map
|
|
|
|
scrubber := NewScrubber(v, 24*time.Hour)
|
|
scrubber.throttle = 0
|
|
|
|
// First pass: establish baseline CRC for segment 0.
|
|
scrubber.runPass()
|
|
if scrubber.Stats().ErrorCount != 0 {
|
|
t.Fatalf("baseline pass should have no errors: %d", scrubber.Stats().ErrorCount)
|
|
}
|
|
|
|
// Now write different data and flush — this changes the extent.
|
|
for i := range data {
|
|
data[i] = 0xBB
|
|
}
|
|
if err := v.WriteLBA(0, data); err != nil {
|
|
t.Fatalf("WriteLBA: %v", err)
|
|
}
|
|
v.SyncCache()
|
|
time.Sleep(50 * time.Millisecond) // flushed to extent, dirty map cleared
|
|
|
|
// Simulate in-pass write: NotifyWrite arrives AFTER pass would have
|
|
// snapshotted writtenSegs (old bug), but BEFORE segment 0 is read.
|
|
// With the fix, writtenSegs is checked live during the pass.
|
|
scrubber.NotifyWrite(0, 1)
|
|
|
|
// Second pass: segment 0 extent data changed, but NotifyWrite should
|
|
// prevent false corruption — segment should be skipped.
|
|
scrubber.runPass()
|
|
stats := scrubber.Stats()
|
|
if stats.ErrorCount != 0 {
|
|
t.Fatalf("in-pass write caused false corruption: errors=%d", stats.ErrorCount)
|
|
}
|
|
if stats.SegmentsDirty == 0 {
|
|
t.Fatal("segment 0 should be counted as dirty (recently written)")
|
|
}
|
|
}
|