mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-21 22:56:55 +00:00
Stable identity on wire: - ServerID fields in proto (replica_server_id, server_id on ReplicaAddrMessage) - volumeServerId wired through volume.go → BlockService.SetServerID - Identity derived from canonical server ID, not transport addresses Assignment convergence: - V2 idempotence via lastAppliedAssignment.equals (full replica set comparison) - setupPrimaryReplication/Multi idempotence guards - ProcessAssignments with V2 + V1 dual-path assignment handling Master-driven control loop: - RecoveryManager: serialized cancel-and-drain via done channels - Per-replica heartbeat state reporting (ReplicaShipperStatus) - masterServerBackend: VolumeBackend calling real MasterServer in-process - RestoreBlockSnapshot RPC (master + volume server proto) QA tests (P10 P1-P4): - Identity: ServerID on wire, fail-closed on missing - Convergence: assignment delivery, epoch monotonicity, registry coherence - Idempotence: repeated assignment, multi-replica set comparison - Control loop: integrationMaster + real allocator + proto round-trip Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
127 lines
4.2 KiB
Go
127 lines
4.2 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/v2bridge"
|
|
)
|
|
|
|
// ============================================================
|
|
// Phase 10 P4: Adversarial — stale epoch on PROMOTED volume
|
|
//
|
|
// Corrected: targets the promoted BS + promoted volume path,
|
|
// matching the real failover test shape.
|
|
// ============================================================
|
|
|
|
func TestAdversarial_P10P4_StaleEpochOnPromotedVolume(t *testing.T) {
|
|
s := newP4Setup(t)
|
|
|
|
// Step 1: create volume at epoch 1 (primary=vs1, replica=vs2).
|
|
ctx := context.Background()
|
|
createResp, err := s.ms.CreateBlockVolume(ctx, &master_pb.CreateBlockVolumeRequest{
|
|
Name: "pvc-stale-2",
|
|
SizeBytes: 1 << 20,
|
|
ReplicaFactor: 2,
|
|
DurabilityMode: "sync_all",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
primaryVS := createResp.VolumeServer
|
|
t.Logf("created: primary=%s", primaryVS)
|
|
|
|
// Deliver epoch 1 to primary BS.
|
|
s.deliverAssignments(primaryVS)
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Step 2: failover → epoch 2, new primary is the old replica.
|
|
s.ms.blockRegistry.UpdateEntry("pvc-stale-2", func(e *BlockVolumeEntry) {
|
|
e.LastLeaseGrant = time.Now().Add(-1 * time.Minute)
|
|
})
|
|
s.ms.failoverBlockVolumes(primaryVS)
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
entryAfter, ok := s.ms.blockRegistry.Lookup("pvc-stale-2")
|
|
if !ok {
|
|
t.Fatal("volume not found after failover")
|
|
}
|
|
if entryAfter.Epoch != 2 {
|
|
t.Fatalf("registry epoch: %d, want 2", entryAfter.Epoch)
|
|
}
|
|
newPrimary := entryAfter.VolumeServer
|
|
t.Logf("after failover: new primary=%s epoch=2", newPrimary)
|
|
|
|
// Step 3: create a SEPARATE promoted BlockService (same as P4 failover test).
|
|
// This is the promoted VS — it has its own store with the promoted volume.
|
|
sanitized := strings.ReplaceAll(newPrimary, ":", "_")
|
|
promotedDir := filepath.Join(s.dir, sanitized+"_promoted")
|
|
promotedStore := storage.NewBlockVolumeStore()
|
|
volPath := filepath.Join(s.dir, sanitized, "pvc-stale-2.blk")
|
|
if _, err := promotedStore.AddBlockVolume(volPath, ""); err != nil {
|
|
t.Fatalf("add promoted vol: %v", err)
|
|
}
|
|
promotedBS := &BlockService{
|
|
blockStore: promotedStore,
|
|
blockDir: promotedDir,
|
|
listenAddr: "127.0.0.1:3260",
|
|
localServerID: newPrimary,
|
|
v2Bridge: v2bridge.NewControlBridge(),
|
|
v2Orchestrator: engine.NewRecoveryOrchestrator(),
|
|
replStates: make(map[string]*volReplState),
|
|
}
|
|
promotedBS.v2Recovery = NewRecoveryManager(promotedBS)
|
|
t.Cleanup(func() {
|
|
promotedBS.v2Recovery.Shutdown()
|
|
promotedStore.Close()
|
|
})
|
|
|
|
// Deliver epoch 2 assignment to promoted BS (not s.bs).
|
|
pending := s.ms.blockAssignmentQueue.Peek(newPrimary)
|
|
if len(pending) > 0 {
|
|
protoAssignments := blockvol.AssignmentsToProto(pending)
|
|
goAssignments := blockvol.AssignmentsFromProto(protoAssignments)
|
|
promotedBS.ProcessAssignments(goAssignments)
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Verify: promoted vol has epoch 2.
|
|
var promotedEpoch uint64
|
|
promotedStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error {
|
|
promotedEpoch = vol.Epoch()
|
|
return nil
|
|
})
|
|
if promotedEpoch != 2 {
|
|
t.Fatalf("promoted vol epoch: %d, want 2", promotedEpoch)
|
|
}
|
|
|
|
// Step 4: deliver STALE epoch 1 to the PROMOTED BS.
|
|
staleAssignment := blockvol.BlockVolumeAssignment{
|
|
Path: volPath,
|
|
Epoch: 1, // STALE — lower than current epoch 2
|
|
Role: uint32(blockvol.RolePrimary),
|
|
LeaseTtlMs: 30000,
|
|
}
|
|
promotedBS.ProcessAssignments([]blockvol.BlockVolumeAssignment{staleAssignment})
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Step 5: verify NOT reverted on the PROMOTED volume.
|
|
var afterStale uint64
|
|
promotedStore.WithVolume(volPath, func(vol *blockvol.BlockVol) error {
|
|
afterStale = vol.Epoch()
|
|
return nil
|
|
})
|
|
if afterStale < 2 {
|
|
t.Fatalf("BUG: promoted vol epoch reverted from 2 to %d after stale delivery", afterStale)
|
|
}
|
|
|
|
t.Logf("adversarial: stale epoch 1 on promoted vol → epoch stays at %d (HandleAssignment rejects regression)", afterStale)
|
|
}
|