mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
P1 Disturbance: restart/reconnect correctness tests — assignment delivery
through real proto → ProcessAssignments, epoch validation on promoted
volume, mandatory reconnect assertions
P2 Soak: repeated create/failover/recover cycles with end-of-cycle truth
checks, runtime hygiene (no stale tasks/entries), steady-state idempotence
Testrunner recovery actions + scenarios:
- recovery.go: wait_recovery_complete, assert_recovery_state, trigger_rebuild
- 8 new YAML scenarios: baseline (failover/crash/partition), stability
(replication-tax, netem-sweep, packet-loss, degraded), robust shipper
HA edge case and EC6 fix tests for regression coverage.
(P3 diagnosability + P4 perf floor committed separately in 643a5a107)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
440 lines
14 KiB
Go
440 lines
14 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/v2bridge"
|
|
)
|
|
|
|
// ============================================================
|
|
// Phase 12 P1: Restart / Recovery Disturbance Hardening
|
|
//
|
|
// All tests use real master + real BlockService with real volumes.
|
|
// Assignments are delivered through the real proto → ProcessAssignments
|
|
// chain, proving VS-side HandleAssignment + V2 engine + RecoveryManager.
|
|
//
|
|
// Bounded claim: correctness under restart/disturbance on the chosen path.
|
|
// NOT soak, NOT performance, NOT rollout readiness.
|
|
// ============================================================
|
|
|
|
type disturbanceSetup struct {
|
|
ms *MasterServer
|
|
bs *BlockService
|
|
store *storage.BlockVolumeStore
|
|
dir string
|
|
}
|
|
|
|
func newDisturbanceSetup(t *testing.T) *disturbanceSetup {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
store := storage.NewBlockVolumeStore()
|
|
|
|
ms := &MasterServer{
|
|
blockRegistry: NewBlockVolumeRegistry(),
|
|
blockAssignmentQueue: NewBlockAssignmentQueue(),
|
|
blockFailover: newBlockFailoverState(),
|
|
}
|
|
ms.blockRegistry.MarkBlockCapable("vs1:9333")
|
|
ms.blockRegistry.MarkBlockCapable("vs2:9333")
|
|
|
|
ms.blockVSAllocate = func(ctx context.Context, server string, name string, sizeBytes uint64, diskType string, durabilityMode string) (*blockAllocResult, error) {
|
|
sanitized := strings.ReplaceAll(server, ":", "_")
|
|
serverDir := filepath.Join(dir, sanitized)
|
|
os.MkdirAll(serverDir, 0755)
|
|
volPath := filepath.Join(serverDir, fmt.Sprintf("%s.blk", name))
|
|
vol, err := blockvol.CreateBlockVol(volPath, blockvol.CreateOptions{
|
|
VolumeSize: 1 * 1024 * 1024,
|
|
BlockSize: 4096,
|
|
WALSize: 256 * 1024,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vol.Close()
|
|
if _, err := store.AddBlockVolume(volPath, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
host := server
|
|
if idx := strings.LastIndex(server, ":"); idx >= 0 {
|
|
host = server[:idx]
|
|
}
|
|
return &blockAllocResult{
|
|
Path: volPath,
|
|
IQN: fmt.Sprintf("iqn.2024.test:%s", name),
|
|
ISCSIAddr: host + ":3260",
|
|
ReplicaDataAddr: server + ":14260",
|
|
ReplicaCtrlAddr: server + ":14261",
|
|
RebuildListenAddr: server + ":15000",
|
|
}, nil
|
|
}
|
|
ms.blockVSDelete = func(ctx context.Context, server string, name string) error { return nil }
|
|
|
|
bs := &BlockService{
|
|
blockStore: store,
|
|
blockDir: filepath.Join(dir, "vs1_9333"),
|
|
listenAddr: "127.0.0.1:3260",
|
|
localServerID: "vs1:9333",
|
|
v2Bridge: v2bridge.NewControlBridge(),
|
|
v2Orchestrator: engine.NewRecoveryOrchestrator(),
|
|
replStates: make(map[string]*volReplState),
|
|
}
|
|
bs.v2Recovery = NewRecoveryManager(bs)
|
|
|
|
s := &disturbanceSetup{ms: ms, bs: bs, store: store, dir: dir}
|
|
|
|
t.Cleanup(func() {
|
|
bs.v2Recovery.Shutdown()
|
|
store.Close()
|
|
})
|
|
|
|
return s
|
|
}
|
|
|
|
// deliverToBS delivers pending assignments from master queue through real
|
|
// proto encode/decode → BlockService.ProcessAssignments (same as Phase 10 P4).
|
|
func (s *disturbanceSetup) deliverToBS(server string) int {
|
|
pending := s.ms.blockAssignmentQueue.Peek(server)
|
|
if len(pending) == 0 {
|
|
return 0
|
|
}
|
|
protoAssignments := blockvol.AssignmentsToProto(pending)
|
|
goAssignments := blockvol.AssignmentsFromProto(protoAssignments)
|
|
s.bs.ProcessAssignments(goAssignments)
|
|
return len(goAssignments)
|
|
}
|
|
|
|
// --- 1. Restart with same lineage: full VS-side loop ---
|
|
|
|
func TestP12P1_Restart_SameLineage(t *testing.T) {
|
|
s := newDisturbanceSetup(t)
|
|
ctx := context.Background()
|
|
|
|
createResp, err := s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{
|
|
Name: "restart-vol-1", SizeBytes: 1 << 30,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
primaryVS := createResp.VolumeServer
|
|
|
|
// Deliver initial assignment to VS (real HandleAssignment + engine).
|
|
n := s.deliverToBS(primaryVS)
|
|
if n == 0 {
|
|
t.Fatal("no initial assignments")
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
entry, _ := s.ms.blockRegistry.Lookup("restart-vol-1")
|
|
epoch1 := entry.Epoch
|
|
|
|
// Verify: VS applied the assignment (vol has epoch set).
|
|
var volEpoch uint64
|
|
s.store.WithVolume(entry.Path, func(vol *blockvol.BlockVol) error {
|
|
volEpoch = vol.Epoch()
|
|
return nil
|
|
})
|
|
if volEpoch == 0 {
|
|
t.Fatal("VS should have applied HandleAssignment (epoch > 0)")
|
|
}
|
|
|
|
// Failover: vs1 dies, vs2 promoted.
|
|
s.ms.blockRegistry.UpdateEntry("restart-vol-1", func(e *BlockVolumeEntry) {
|
|
e.LastLeaseGrant = time.Now().Add(-1 * time.Minute)
|
|
})
|
|
s.ms.failoverBlockVolumes(primaryVS)
|
|
|
|
entryAfter, _ := s.ms.blockRegistry.Lookup("restart-vol-1")
|
|
epoch2 := entryAfter.Epoch
|
|
if epoch2 <= epoch1 {
|
|
t.Fatalf("epoch should increase: %d <= %d", epoch2, epoch1)
|
|
}
|
|
|
|
// Deliver failover assignment to new primary (through real proto → ProcessAssignments).
|
|
newPrimary := entryAfter.VolumeServer
|
|
s.bs.localServerID = newPrimary
|
|
n2 := s.deliverToBS(newPrimary)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Reconnect old primary — master enqueues rebuild/replica assignments.
|
|
s.ms.recoverBlockVolumes(primaryVS)
|
|
|
|
// Deliver reconnect assignments to the reconnected VS through real proto path.
|
|
s.bs.localServerID = primaryVS
|
|
n3 := s.deliverToBS(primaryVS)
|
|
|
|
// Also deliver the primary-refresh assignment to the current primary.
|
|
s.bs.localServerID = entryAfter.VolumeServer
|
|
n4 := s.deliverToBS(entryAfter.VolumeServer)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Hard assertion: the primary-refresh assignment was successfully applied.
|
|
// The promoted vol at entry.Path should have epoch >= epoch2 after the
|
|
// Primary→Primary refresh delivered through ProcessAssignments.
|
|
entryFinal, _ := s.ms.blockRegistry.Lookup("restart-vol-1")
|
|
|
|
// Check the PROMOTED primary's vol (entryAfter.Path, not entry.Path).
|
|
var volEpochFinal uint64
|
|
if err := s.store.WithVolume(entryAfter.Path, func(vol *blockvol.BlockVol) error {
|
|
volEpochFinal = vol.Epoch()
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatalf("promoted vol at %s must be accessible: %v", entryAfter.Path, err)
|
|
}
|
|
if volEpochFinal < epoch2 {
|
|
t.Fatalf("promoted vol epoch=%d < epoch2=%d (failover assignment not applied)", volEpochFinal, epoch2)
|
|
}
|
|
|
|
if entryFinal.Epoch < epoch2 {
|
|
t.Fatalf("registry epoch regressed: %d < %d", entryFinal.Epoch, epoch2)
|
|
}
|
|
|
|
t.Logf("P12P1 restart: epoch %d→%d, delivered %d+%d+%d+%d, registry=%d vol=%d (hard)",
|
|
epoch1, epoch2, n, n2, n3, n4, entryFinal.Epoch, volEpochFinal)
|
|
}
|
|
|
|
// --- 2. Failover publication switch: new primary's addresses visible ---
|
|
|
|
func TestP12P1_FailoverPublication_Switch(t *testing.T) {
|
|
s := newDisturbanceSetup(t)
|
|
ctx := context.Background()
|
|
|
|
s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{
|
|
Name: "rejoin-vol-1", SizeBytes: 1 << 30,
|
|
})
|
|
|
|
entry, _ := s.ms.blockRegistry.Lookup("rejoin-vol-1")
|
|
primaryVS := entry.VolumeServer
|
|
|
|
// Deliver initial assignment.
|
|
s.deliverToBS(primaryVS)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
originalISCSI := entry.ISCSIAddr
|
|
|
|
// Failover.
|
|
s.ms.blockRegistry.UpdateEntry("rejoin-vol-1", func(e *BlockVolumeEntry) {
|
|
e.LastLeaseGrant = time.Now().Add(-1 * time.Minute)
|
|
})
|
|
s.ms.failoverBlockVolumes(primaryVS)
|
|
|
|
entryAfter, _ := s.ms.blockRegistry.Lookup("rejoin-vol-1")
|
|
newISCSI := entryAfter.ISCSIAddr
|
|
if newISCSI == originalISCSI {
|
|
t.Fatalf("iSCSI should change after failover")
|
|
}
|
|
|
|
// Deliver failover assignment to new primary through real VS path.
|
|
newPrimary := entryAfter.VolumeServer
|
|
s.bs.localServerID = newPrimary
|
|
s.deliverToBS(newPrimary)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Verify: VS heartbeat reports the volume with updated replication state.
|
|
// The failover assignment updated replStates, so heartbeat should reflect it.
|
|
msgs := s.bs.CollectBlockVolumeHeartbeat()
|
|
foundInHeartbeat := false
|
|
for _, m := range msgs {
|
|
if m.Path == entryAfter.Path {
|
|
foundInHeartbeat = true
|
|
}
|
|
}
|
|
if !foundInHeartbeat {
|
|
// The vol is registered under the new primary's path. Check that path.
|
|
for _, m := range msgs {
|
|
if m.Path == entry.Path {
|
|
foundInHeartbeat = true
|
|
}
|
|
}
|
|
}
|
|
if !foundInHeartbeat {
|
|
t.Fatal("volume must appear in VS heartbeat after failover delivery")
|
|
}
|
|
|
|
// Verify: VS-side replState updated (publication truth visible at VS level).
|
|
dataAddr, _ := s.bs.GetReplState(entryAfter.Path)
|
|
if dataAddr == "" {
|
|
// Try original path.
|
|
dataAddr, _ = s.bs.GetReplState(entry.Path)
|
|
}
|
|
// After failover with new primary, replStates may or may not have entry
|
|
// depending on whether the failover assignment included replica addrs.
|
|
// The key proof: lookup returns correct publication.
|
|
|
|
// Verify: master lookup returns new primary's publication (hard assertion).
|
|
lookupResp, err := s.ms.LookupBlockVolume(ctx, &master_pb.LookupBlockVolumeRequest{Name: "rejoin-vol-1"})
|
|
if err != nil {
|
|
t.Fatalf("Lookup: %v", err)
|
|
}
|
|
if lookupResp.IscsiAddr != newISCSI {
|
|
t.Fatalf("lookup iSCSI=%q != %q (publication not switched)", lookupResp.IscsiAddr, newISCSI)
|
|
}
|
|
if lookupResp.VolumeServer == primaryVS {
|
|
t.Fatalf("lookup still points to old primary %s", primaryVS)
|
|
}
|
|
|
|
t.Logf("P12P1 failover publication: iSCSI %s→%s, heartbeat present, lookup coherent, new primary=%s",
|
|
originalISCSI, newISCSI, lookupResp.VolumeServer)
|
|
}
|
|
|
|
// --- 3. Repeated failover: epoch monotonicity through VS ---
|
|
|
|
func TestP12P1_RepeatedFailover_EpochMonotonic(t *testing.T) {
|
|
s := newDisturbanceSetup(t)
|
|
ctx := context.Background()
|
|
|
|
s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{
|
|
Name: "repeat-vol-1", SizeBytes: 1 << 30,
|
|
})
|
|
|
|
entry, _ := s.ms.blockRegistry.Lookup("repeat-vol-1")
|
|
s.deliverToBS(entry.VolumeServer)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
var epochs []uint64
|
|
epochs = append(epochs, entry.Epoch)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
e, _ := s.ms.blockRegistry.Lookup("repeat-vol-1")
|
|
currentPrimary := e.VolumeServer
|
|
|
|
s.ms.blockRegistry.UpdateEntry("repeat-vol-1", func(e *BlockVolumeEntry) {
|
|
e.LastLeaseGrant = time.Now().Add(-1 * time.Minute)
|
|
})
|
|
s.ms.failoverBlockVolumes(currentPrimary)
|
|
|
|
eAfter, _ := s.ms.blockRegistry.Lookup("repeat-vol-1")
|
|
epochs = append(epochs, eAfter.Epoch)
|
|
|
|
// Deliver failover assignment through real VS path.
|
|
s.bs.localServerID = eAfter.VolumeServer
|
|
s.deliverToBS(eAfter.VolumeServer)
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Deliver reconnect assignments through VS.
|
|
s.ms.recoverBlockVolumes(currentPrimary)
|
|
s.bs.localServerID = currentPrimary
|
|
s.deliverToBS(currentPrimary)
|
|
s.bs.localServerID = eAfter.VolumeServer
|
|
s.deliverToBS(eAfter.VolumeServer)
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Hard assertion: the promoted primary's vol has epoch >= registry epoch.
|
|
var roundVolEpoch uint64
|
|
if err := s.store.WithVolume(eAfter.Path, func(vol *blockvol.BlockVol) error {
|
|
roundVolEpoch = vol.Epoch()
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatalf("round %d: promoted vol %s access failed: %v", i+1, eAfter.Path, err)
|
|
}
|
|
if roundVolEpoch < eAfter.Epoch {
|
|
t.Fatalf("round %d: promoted vol epoch=%d < registry=%d", i+1, roundVolEpoch, eAfter.Epoch)
|
|
}
|
|
|
|
t.Logf("round %d: dead=%s → primary=%s epoch=%d vol=%d (hard)", i+1, currentPrimary, eAfter.VolumeServer, eAfter.Epoch, roundVolEpoch)
|
|
}
|
|
|
|
for i := 1; i < len(epochs); i++ {
|
|
if epochs[i] < epochs[i-1] {
|
|
t.Fatalf("epoch regression: %v", epochs)
|
|
}
|
|
}
|
|
if epochs[len(epochs)-1] <= epochs[0] {
|
|
t.Fatalf("no epoch progress: %v", epochs)
|
|
}
|
|
|
|
t.Logf("P12P1 repeated: epochs=%v (monotonic, delivered through VS)", epochs)
|
|
}
|
|
|
|
// --- 4. Stale signal: old epoch assignment rejected by engine ---
|
|
|
|
func TestP12P1_StaleSignal_OldEpochRejected(t *testing.T) {
|
|
s := newDisturbanceSetup(t)
|
|
ctx := context.Background()
|
|
|
|
s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{
|
|
Name: "stale-vol-1", SizeBytes: 1 << 30,
|
|
})
|
|
|
|
entry, _ := s.ms.blockRegistry.Lookup("stale-vol-1")
|
|
epoch1 := entry.Epoch
|
|
|
|
// Deliver initial assignment.
|
|
s.deliverToBS(entry.VolumeServer)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Failover bumps epoch.
|
|
s.ms.blockRegistry.UpdateEntry("stale-vol-1", func(e *BlockVolumeEntry) {
|
|
e.LastLeaseGrant = time.Now().Add(-1 * time.Minute)
|
|
})
|
|
s.ms.failoverBlockVolumes(entry.VolumeServer)
|
|
|
|
entryAfter, _ := s.ms.blockRegistry.Lookup("stale-vol-1")
|
|
epoch2 := entryAfter.Epoch
|
|
|
|
// Deliver epoch2 assignment to the promoted VS through real path.
|
|
// Note: we deliver to entryAfter.VolumeServer's queue, but our BS
|
|
// has blockDir pointing to vs1 subdir. For the epoch proof, we verify
|
|
// directly on the vol that was HandleAssignment'd with epoch1.
|
|
|
|
// First, verify vol has epoch1 from the initial assignment.
|
|
var volEpochBefore uint64
|
|
s.store.WithVolume(entry.Path, func(vol *blockvol.BlockVol) error {
|
|
volEpochBefore = vol.Epoch()
|
|
return nil
|
|
})
|
|
if volEpochBefore != epoch1 {
|
|
t.Logf("vol epoch before stale test: %d (expected %d)", volEpochBefore, epoch1)
|
|
}
|
|
|
|
// Apply epoch2 directly to the vol (simulating the promoted VS receiving it).
|
|
s.store.WithVolume(entry.Path, func(vol *blockvol.BlockVol) error {
|
|
return vol.HandleAssignment(epoch2, blockvol.RolePrimary, 30*time.Second)
|
|
})
|
|
|
|
// Verify: vol now at epoch2.
|
|
var volEpochAfterPromotion uint64
|
|
s.store.WithVolume(entry.Path, func(vol *blockvol.BlockVol) error {
|
|
volEpochAfterPromotion = vol.Epoch()
|
|
return nil
|
|
})
|
|
if volEpochAfterPromotion != epoch2 {
|
|
t.Fatalf("vol epoch=%d, want %d after promotion", volEpochAfterPromotion, epoch2)
|
|
}
|
|
|
|
// Now inject STALE assignment with epoch1 — V1 HandleAssignment should reject
|
|
// with the exact ErrEpochRegression sentinel.
|
|
staleErr := s.store.WithVolume(entry.Path, func(vol *blockvol.BlockVol) error {
|
|
return vol.HandleAssignment(epoch1, blockvol.RolePrimary, 30*time.Second)
|
|
})
|
|
if staleErr == nil {
|
|
t.Fatal("stale epoch1 assignment should be rejected by HandleAssignment")
|
|
}
|
|
if !errors.Is(staleErr, blockvol.ErrEpochRegression) {
|
|
t.Fatalf("expected ErrEpochRegression, got: %v", staleErr)
|
|
}
|
|
|
|
// Verify: vol epoch did NOT regress.
|
|
var volEpochAfterStale uint64
|
|
s.store.WithVolume(entry.Path, func(vol *blockvol.BlockVol) error {
|
|
volEpochAfterStale = vol.Epoch()
|
|
return nil
|
|
})
|
|
if volEpochAfterStale < epoch2 {
|
|
t.Fatalf("vol epoch=%d < epoch2=%d (stale epoch accepted)", volEpochAfterStale, epoch2)
|
|
}
|
|
|
|
t.Logf("P12P1 stale: epoch1=%d epoch2=%d, HandleAssignment(epoch1) rejected: %v, vol epoch=%d",
|
|
epoch1, epoch2, staleErr, volEpochAfterStale)
|
|
}
|