Files
seaweedfs/weed/storage/blockvol/v2bridge/control_test.go
T
pingqiuandClaude Opus 4.6 46ef79ce35 fix: stable ServerID in assignments, fail-closed on missing identity, wire into ProcessAssignments
Finding 1: Identity no longer address-derived
- ReplicaAddr.ServerID field added (stable server identity from registry)
- BlockVolumeAssignment.ReplicaServerID field added (scalar RF=2 path)
- ControlBridge uses ServerID, NOT address, for ReplicaID
- Missing ServerID → replica skipped (fail closed), logged

Finding 2: Wired into real ProcessAssignments
- BlockService.v2Bridge field initialized in StartBlockService
- ProcessAssignments converts each assignment via v2Bridge.ConvertAssignment
  BEFORE existing V1 processing (parallel, not replacing yet)
- Logged at glog V(1)

Finding 3: Fail-closed on missing identity
- Empty ServerID in ReplicaAddrs → replica skipped with log
- Empty ReplicaServerID in scalar path → no replica created
- Test: MissingServerID_FailsClosed verifies both paths

7 tests: StableServerID, AddressChange_IdentityPreserved,
MultiReplica_StableServerIDs, MissingServerID_FailsClosed,
EpochFencing_IntegratedPath, RebuildAssignment, ReplicaAssignment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:46:17 -07:00

193 lines
6.0 KiB
Go

package v2bridge
import (
"testing"
engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
)
// ============================================================
// Phase 08 P1: Real control delivery tests
// Identity: ReplicaID = <path>/<ServerID> — NOT address-derived.
// ============================================================
func TestControl_PrimaryAssignment_StableServerID(t *testing.T) {
cb := NewControlBridge()
a := blockvol.BlockVolumeAssignment{
Path: "pvc-data-1",
Epoch: 3,
Role: uint32(blockvol.RolePrimary),
ReplicaServerID: "vs2",
ReplicaDataAddr: "10.0.0.2:9333",
ReplicaCtrlAddr: "10.0.0.2:9334",
}
intent := cb.ConvertAssignment(a, "vs1")
if len(intent.Replicas) != 1 {
t.Fatalf("replicas=%d", len(intent.Replicas))
}
r := intent.Replicas[0]
// ReplicaID uses ServerID, not address.
if r.ReplicaID != "pvc-data-1/vs2" {
t.Fatalf("ReplicaID=%s, want pvc-data-1/vs2", r.ReplicaID)
}
if r.Endpoint.DataAddr != "10.0.0.2:9333" {
t.Fatalf("DataAddr=%s", r.Endpoint.DataAddr)
}
if intent.RecoveryTargets["pvc-data-1/vs2"] != engine.SessionCatchUp {
t.Fatalf("recovery=%s", intent.RecoveryTargets["pvc-data-1/vs2"])
}
}
func TestControl_AddressChange_IdentityPreserved(t *testing.T) {
cb := NewControlBridge()
// Same ServerID, different address.
a1 := blockvol.BlockVolumeAssignment{
Path: "vol1", Epoch: 1, Role: uint32(blockvol.RolePrimary),
ReplicaServerID: "vs2",
ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334",
}
a2 := blockvol.BlockVolumeAssignment{
Path: "vol1", Epoch: 1, Role: uint32(blockvol.RolePrimary),
ReplicaServerID: "vs2",
ReplicaDataAddr: "10.0.0.5:9333", ReplicaCtrlAddr: "10.0.0.5:9334",
}
intent1 := cb.ConvertAssignment(a1, "vs1")
intent2 := cb.ConvertAssignment(a2, "vs1")
if intent1.Replicas[0].ReplicaID != intent2.Replicas[0].ReplicaID {
t.Fatalf("identity changed: %s → %s",
intent1.Replicas[0].ReplicaID, intent2.Replicas[0].ReplicaID)
}
if intent2.Replicas[0].Endpoint.DataAddr != "10.0.0.5:9333" {
t.Fatal("endpoint should be updated")
}
}
func TestControl_MultiReplica_StableServerIDs(t *testing.T) {
cb := NewControlBridge()
a := blockvol.BlockVolumeAssignment{
Path: "vol1", Epoch: 2, Role: uint32(blockvol.RolePrimary),
ReplicaAddrs: []blockvol.ReplicaAddr{
{DataAddr: "10.0.0.2:9333", CtrlAddr: "10.0.0.2:9334", ServerID: "vs2"},
{DataAddr: "10.0.0.3:9333", CtrlAddr: "10.0.0.3:9334", ServerID: "vs3"},
},
}
intent := cb.ConvertAssignment(a, "vs1")
if len(intent.Replicas) != 2 {
t.Fatalf("replicas=%d", len(intent.Replicas))
}
ids := map[string]bool{}
for _, r := range intent.Replicas {
ids[r.ReplicaID] = true
}
if !ids["vol1/vs2"] || !ids["vol1/vs3"] {
t.Fatalf("IDs: %v (should use ServerID, not address)", ids)
}
}
func TestControl_MissingServerID_FailsClosed(t *testing.T) {
cb := NewControlBridge()
// Scalar: no ServerID → no replica created.
a1 := blockvol.BlockVolumeAssignment{
Path: "vol1", Epoch: 1, Role: uint32(blockvol.RolePrimary),
ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334",
// ReplicaServerID intentionally empty.
}
intent1 := cb.ConvertAssignment(a1, "vs1")
if len(intent1.Replicas) != 0 {
t.Fatalf("scalar without ServerID should produce 0 replicas, got %d", len(intent1.Replicas))
}
// Multi: one with ServerID, one without → only one replica.
a2 := blockvol.BlockVolumeAssignment{
Path: "vol1", Epoch: 1, Role: uint32(blockvol.RolePrimary),
ReplicaAddrs: []blockvol.ReplicaAddr{
{DataAddr: "10.0.0.2:9333", ServerID: "vs2"},
{DataAddr: "10.0.0.3:9333", ServerID: ""}, // empty → skipped
},
}
intent2 := cb.ConvertAssignment(a2, "vs1")
if len(intent2.Replicas) != 1 {
t.Fatalf("multi with 1 missing ServerID: replicas=%d, want 1", len(intent2.Replicas))
}
}
func TestControl_EpochFencing_IntegratedPath(t *testing.T) {
cb := NewControlBridge()
driver := engine.NewRecoveryDriver(nil)
a1 := blockvol.BlockVolumeAssignment{
Path: "vol1", Epoch: 1, Role: uint32(blockvol.RolePrimary),
ReplicaServerID: "vs2", ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334",
}
driver.Orchestrator.ProcessAssignment(cb.ConvertAssignment(a1, "vs1"))
s := driver.Orchestrator.Registry.Sender("vol1/vs2")
if s == nil || !s.HasActiveSession() {
t.Fatal("should have session at epoch 1")
}
driver.Orchestrator.InvalidateEpoch(2)
driver.Orchestrator.UpdateSenderEpoch("vol1/vs2", 2)
if s.HasActiveSession() {
t.Fatal("old session should be invalidated")
}
a2 := blockvol.BlockVolumeAssignment{
Path: "vol1", Epoch: 2, Role: uint32(blockvol.RolePrimary),
ReplicaServerID: "vs2", ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334",
}
driver.Orchestrator.ProcessAssignment(cb.ConvertAssignment(a2, "vs1"))
if !s.HasActiveSession() {
t.Fatal("should have new session at epoch 2")
}
hasInvalidation := false
for _, e := range driver.Orchestrator.Log.EventsFor("vol1/vs2") {
if e.Event == "session_invalidated" {
hasInvalidation = true
}
}
if !hasInvalidation {
t.Fatal("log must show invalidation")
}
}
func TestControl_RebuildAssignment(t *testing.T) {
cb := NewControlBridge()
a := blockvol.BlockVolumeAssignment{
Path: "vol1", Epoch: 3, Role: uint32(blockvol.RoleRebuilding),
ReplicaDataAddr: "10.0.0.2:9333", ReplicaCtrlAddr: "10.0.0.2:9334",
RebuildAddr: "10.0.0.1:15000",
}
intent := cb.ConvertAssignment(a, "vs2")
if intent.RecoveryTargets["vol1/vs2"] != engine.SessionRebuild {
t.Fatalf("recovery=%s", intent.RecoveryTargets["vol1/vs2"])
}
}
func TestControl_ReplicaAssignment(t *testing.T) {
cb := NewControlBridge()
a := blockvol.BlockVolumeAssignment{
Path: "vol1", Epoch: 1, Role: uint32(blockvol.RoleReplica),
ReplicaDataAddr: "10.0.0.1:14260", ReplicaCtrlAddr: "10.0.0.1:14261",
}
intent := cb.ConvertAssignment(a, "vs2")
if intent.Replicas[0].ReplicaID != "vol1/vs2" {
t.Fatalf("ReplicaID=%s", intent.Replicas[0].ReplicaID)
}
}