Files
seaweedfs/weed/server/qa_block_soak_test.go
T
pingqiuandClaude Opus 4.6 4bdf6c604e fix: CP13-2 — use advertisedIP (routable), not localServerID (opaque)
Bug: setupReplicaReceiver derived the advertised host from localServerID,
which can be an opaque string (from -id flag, e.g., "my-custom-server-id").
This would publish unusable endpoints like "my-custom-server-id:14260".

Fix:
- volume_server_block.go: add advertisedIP field (always a real IP from
  -ip flag), use it instead of localServerID for replica canonicalization
- volume.go: wire *v.ip → blockService.SetAdvertisedIP() at startup
- blockvol.go: StartReplicaReceiver variadic advertisedHost unchanged

Proof (sync_all_bug_test.go TestBug3, 4 sub-cases):
- fallback: wildcard bind without advertisedHost → outbound-IP
- advertisedHost: explicit IP appears in exported addresses
- StartReplicaReceiver_API: public API forwards host correctly
- opaque_identity_not_routable: proves opaque string produces
  non-routable address, confirming production must use advertisedIP

Identity vs transport separation preserved:
- localServerID: stable identity for V2 control (may be opaque)
- advertisedIP: routable IP for transport endpoints (always real IP)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:51:47 -07:00

310 lines
9.8 KiB
Go

package weed_server
import (
"context"
"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 P2: Soak / Long-Run Stability Hardening
//
// Proves: repeated chosen-path cycles return to bounded truth
// without hidden state drift or unbounded runtime artifacts.
//
// NOT diagnosability, NOT performance-floor, NOT rollout readiness.
// ============================================================
const soakCycles = 5
type soakSetup struct {
ms *MasterServer
bs *BlockService
store *storage.BlockVolumeStore
dir string
}
func newSoakSetup(t *testing.T) *soakSetup {
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",
advertisedIP: "127.0.0.1",
v2Bridge: v2bridge.NewControlBridge(),
v2Orchestrator: engine.NewRecoveryOrchestrator(),
replStates: make(map[string]*volReplState),
}
bs.v2Recovery = NewRecoveryManager(bs)
t.Cleanup(func() {
bs.v2Recovery.Shutdown()
store.Close()
})
return &soakSetup{ms: ms, bs: bs, store: store, dir: dir}
}
func (s *soakSetup) deliver(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)
}
// --- Repeated create/failover/recover cycles with end-of-cycle truth checks ---
func TestP12P2_RepeatedCycles_NoDrift(t *testing.T) {
s := newSoakSetup(t)
ctx := context.Background()
for cycle := 1; cycle <= soakCycles; cycle++ {
volName := fmt.Sprintf("soak-vol-%d", cycle)
// Step 1: Create.
createResp, err := s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{
Name: volName, SizeBytes: 1 << 20,
})
if err != nil {
t.Fatalf("cycle %d create: %v", cycle, err)
}
primaryVS := createResp.VolumeServer
// Deliver initial assignment.
s.bs.localServerID = primaryVS
s.deliver(primaryVS)
time.Sleep(100 * time.Millisecond)
entry, ok := s.ms.blockRegistry.Lookup(volName)
if !ok {
t.Fatalf("cycle %d: volume not in registry", cycle)
}
// Step 2: Failover.
s.ms.blockRegistry.UpdateEntry(volName, func(e *BlockVolumeEntry) {
e.LastLeaseGrant = time.Now().Add(-1 * time.Minute)
})
s.ms.failoverBlockVolumes(primaryVS)
entryAfter, _ := s.ms.blockRegistry.Lookup(volName)
if entryAfter.Epoch <= entry.Epoch {
t.Fatalf("cycle %d: epoch did not increase: %d <= %d", cycle, entryAfter.Epoch, entry.Epoch)
}
// Deliver failover assignment.
newPrimary := entryAfter.VolumeServer
s.bs.localServerID = newPrimary
s.deliver(newPrimary)
time.Sleep(100 * time.Millisecond)
// Step 3: Reconnect.
s.ms.recoverBlockVolumes(primaryVS)
s.bs.localServerID = primaryVS
s.deliver(primaryVS)
s.bs.localServerID = newPrimary
s.deliver(newPrimary)
time.Sleep(100 * time.Millisecond)
// === End-of-cycle truth checks ===
// Registry: volume exists, epoch monotonic.
finalEntry, ok := s.ms.blockRegistry.Lookup(volName)
if !ok {
t.Fatalf("cycle %d: volume missing from registry at end", cycle)
}
if finalEntry.Epoch < entryAfter.Epoch {
t.Fatalf("cycle %d: registry epoch regressed: %d < %d", cycle, finalEntry.Epoch, entryAfter.Epoch)
}
// VS-visible: promoted vol epoch matches.
var volEpoch uint64
if err := s.store.WithVolume(entryAfter.Path, func(vol *blockvol.BlockVol) error {
volEpoch = vol.Epoch()
return nil
}); err != nil {
t.Fatalf("cycle %d: promoted vol access failed: %v", cycle, err)
}
if volEpoch < entryAfter.Epoch {
t.Fatalf("cycle %d: vol epoch=%d < registry=%d", cycle, volEpoch, entryAfter.Epoch)
}
// Publication: lookup matches registry truth (not just non-empty).
lookupResp, err := s.ms.LookupBlockVolume(ctx, &master_pb.LookupBlockVolumeRequest{Name: volName})
if err != nil {
t.Fatalf("cycle %d: lookup failed: %v", cycle, err)
}
if lookupResp.IscsiAddr != finalEntry.ISCSIAddr {
t.Fatalf("cycle %d: lookup iSCSI=%q != registry=%q", cycle, lookupResp.IscsiAddr, finalEntry.ISCSIAddr)
}
if lookupResp.VolumeServer != finalEntry.VolumeServer {
t.Fatalf("cycle %d: lookup VS=%q != registry=%q", cycle, lookupResp.VolumeServer, finalEntry.VolumeServer)
}
t.Logf("cycle %d: registry=%d vol=%d lookup=registry ✓",
cycle, finalEntry.Epoch, volEpoch)
}
t.Logf("P12P2 repeated cycles: %d cycles, all end-of-cycle truth checks passed", soakCycles)
}
// --- Runtime state hygiene: no unbounded leftovers after cycles ---
func TestP12P2_RuntimeHygiene_NoLeftovers(t *testing.T) {
s := newSoakSetup(t)
ctx := context.Background()
// Create and delete several volumes to exercise lifecycle.
for i := 1; i <= soakCycles; i++ {
name := fmt.Sprintf("hygiene-vol-%d", i)
s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{
Name: name, SizeBytes: 1 << 20,
})
entry, _ := s.ms.blockRegistry.Lookup(name)
s.bs.localServerID = entry.VolumeServer
s.deliver(entry.VolumeServer)
time.Sleep(50 * time.Millisecond)
}
// Delete all volumes.
for i := 1; i <= soakCycles; i++ {
name := fmt.Sprintf("hygiene-vol-%d", i)
s.ms.DeleteBlockVolume(ctx, &master_pb.DeleteBlockVolumeRequest{Name: name})
}
time.Sleep(200 * time.Millisecond)
// Check: no stale recovery tasks.
activeTasks := s.bs.v2Recovery.ActiveTaskCount()
if activeTasks > 0 {
t.Fatalf("stale recovery tasks: %d (expected 0 after all volumes deleted)", activeTasks)
}
// Check: registry should have no entries for deleted volumes.
for i := 1; i <= soakCycles; i++ {
name := fmt.Sprintf("hygiene-vol-%d", i)
if _, ok := s.ms.blockRegistry.Lookup(name); ok {
t.Fatalf("stale registry entry: %s (should be deleted)", name)
}
}
// Check: assignment queue should not have unbounded stale entries.
for _, server := range []string{"vs1:9333", "vs2:9333"} {
pending := s.ms.blockAssignmentQueue.Peek(server)
// Some pending entries may exist (lease grants etc), but check for bounded size.
if len(pending) > soakCycles*2 {
t.Fatalf("unbounded stale assignments for %s: %d", server, len(pending))
}
}
t.Logf("P12P2 hygiene: %d volumes created+deleted, 0 stale tasks, 0 stale registry, bounded queue", soakCycles)
}
// --- Steady-state repeated delivery: idempotence holds over many cycles ---
func TestP12P2_SteadyState_IdempotenceHolds(t *testing.T) {
s := newSoakSetup(t)
ctx := context.Background()
s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{
Name: "steady-vol-1", SizeBytes: 1 << 20,
})
entry, _ := s.ms.blockRegistry.Lookup("steady-vol-1")
s.bs.localServerID = entry.VolumeServer
s.deliver(entry.VolumeServer)
time.Sleep(200 * time.Millisecond)
replicaID := entry.Path + "/" + entry.Replicas[0].Server
eventsAfterFirst := len(s.bs.v2Orchestrator.Log.EventsFor(replicaID))
if eventsAfterFirst == 0 {
t.Fatal("first delivery must create events")
}
// Deliver the same assignment many times.
for i := 0; i < soakCycles*2; i++ {
s.deliver(entry.VolumeServer)
time.Sleep(20 * time.Millisecond)
}
eventsAfterSoak := len(s.bs.v2Orchestrator.Log.EventsFor(replicaID))
if eventsAfterSoak != eventsAfterFirst {
t.Fatalf("idempotence drift: events %d → %d after %d repeated deliveries",
eventsAfterFirst, eventsAfterSoak, soakCycles*2)
}
// Verify: registry, vol, and lookup are still coherent.
finalEntry, _ := s.ms.blockRegistry.Lookup("steady-vol-1")
if finalEntry.Epoch != entry.Epoch {
t.Fatalf("epoch drifted: %d → %d", entry.Epoch, finalEntry.Epoch)
}
lookupResp, _ := s.ms.LookupBlockVolume(ctx, &master_pb.LookupBlockVolumeRequest{Name: "steady-vol-1"})
if lookupResp.IscsiAddr != finalEntry.ISCSIAddr {
t.Fatalf("lookup iSCSI=%q != registry=%q after soak", lookupResp.IscsiAddr, finalEntry.ISCSIAddr)
}
if lookupResp.VolumeServer != finalEntry.VolumeServer {
t.Fatalf("lookup VS=%q != registry=%q after soak", lookupResp.VolumeServer, finalEntry.VolumeServer)
}
t.Logf("P12P2 steady state: %d repeated deliveries, events stable at %d, lookup=registry ✓",
soakCycles*2, eventsAfterFirst)
}