Files
seaweedfs/weed/server/qa_block_identity_test.go
T
pingqiuandClaude Opus 4.6 3ec8fab2f1 feat: Phase 10 — control-plane closure (identity, convergence, idempotence)
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>
2026-04-02 16:25:43 -07:00

174 lines
6.4 KiB
Go

package weed_server
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/v2bridge"
engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication"
)
// ============================================================
// Phase 10 P1: Stable identity on the real control wire
//
// Proofs:
// 1. Real ingress: proto wire → decode → ProcessAssignments → ControlBridge → ReplicaID
// 2. Canonical local identity: non-default serverID flows through block/control path
// 3. Fail-closed: missing ServerID on proto wire → replica skipped
// ============================================================
// --- Real ingress proof: proto wire → engine ReplicaID ---
func TestP10P1_RealIngress_ProtoToReplicaID(t *testing.T) {
// Simulate the real control ingress chain:
// 1. Master builds proto assignment with stable ServerID
// 2. Proto is encoded (as it would be over gRPC wire)
// 3. Volume server decodes proto → Go assignment
// 4. ProcessAssignments → ControlBridge → engine
// 5. Engine has sender with correct ReplicaID = <path>/<ServerID>
// Step 1: Master builds proto (simulates master_grpc_server_block.go).
protoAssignment := &master_pb.BlockVolumeAssignment{
Path: "pvc-vol-1",
Epoch: 5,
Role: uint32(blockvol.RolePrimary),
LeaseTtlMs: 30000,
ReplicaServerId: "vs2-node.cluster:18080", // stable ID from registry
ReplicaDataAddr: "10.0.0.2:14260",
ReplicaCtrlAddr: "10.0.0.2:14261",
ReplicaAddrs: []*master_pb.ReplicaAddrMessage{
{
DataAddr: "10.0.0.2:14260",
CtrlAddr: "10.0.0.2:14261",
ServerId: "vs2-node.cluster:18080",
},
},
}
// Step 2: Proto round-trip (simulates gRPC wire encode/decode).
// In real gRPC, this is automatic. Here we verify the Go struct
// carries the fields after proto-to-Go conversion.
goAssignment := blockvol.AssignmentFromProto(protoAssignment)
// Verify: Go wire type preserves the stable ID.
if goAssignment.ReplicaServerID != "vs2-node.cluster:18080" {
t.Fatalf("scalar ReplicaServerID=%q, want vs2-node.cluster:18080", goAssignment.ReplicaServerID)
}
if len(goAssignment.ReplicaAddrs) != 1 || goAssignment.ReplicaAddrs[0].ServerID != "vs2-node.cluster:18080" {
t.Fatalf("multi-replica ServerID not preserved")
}
// Step 3-4: ProcessAssignments → ControlBridge → engine.
bs := &BlockService{
v2Bridge: v2bridge.NewControlBridge(),
v2Orchestrator: engine.NewRecoveryOrchestrator(),
localServerID: "vs1-node.cluster:18080",
}
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{goAssignment})
// Step 5: Verify engine sender has the correct stable ReplicaID.
expectedReplicaID := "pvc-vol-1/vs2-node.cluster:18080"
sender := bs.v2Orchestrator.Registry.Sender(expectedReplicaID)
if sender == nil {
t.Fatalf("engine sender not found for %s", expectedReplicaID)
}
if !sender.HasActiveSession() {
t.Fatal("sender should have active session")
}
t.Logf("P10P1 real ingress: proto(ServerId=%s) → decode → ProcessAssignments → sender(%s) with session",
"vs2-node.cluster:18080", expectedReplicaID)
}
// --- Canonical local identity: non-default serverID ---
func TestP10P1_CanonicalLocalIdentity(t *testing.T) {
// Prove that when the volume server has a canonical ID different from
// plain ip:port, the block/control path uses that canonical ID.
canonicalID := "my-custom-volume-server-id"
bs := &BlockService{
v2Bridge: v2bridge.NewControlBridge(),
v2Orchestrator: engine.NewRecoveryOrchestrator(),
localServerID: canonicalID, // explicitly NOT ip:port shaped
}
// Process a replica assignment that targets THIS volume server.
// The ControlBridge uses localServerID for replica/rebuild assignments.
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
{
Path: "pvc-vol-2",
Epoch: 3,
Role: uint32(blockvol.RolePrimary),
ReplicaServerID: "other-server-id",
ReplicaDataAddr: "10.0.0.2:9333",
ReplicaCtrlAddr: "10.0.0.2:9334",
},
})
// The sender should use the ServerID from the assignment, not localServerID.
sender := bs.v2Orchestrator.Registry.Sender("pvc-vol-2/other-server-id")
if sender == nil {
t.Fatal("sender should exist with ServerID from assignment, not from localServerID")
}
// Now process a replica assignment where THIS server is the replica.
// The ControlBridge uses localServerID for the local ReplicaID.
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
{
Path: "pvc-vol-3",
Epoch: 1,
Role: uint32(blockvol.RoleReplica),
ReplicaDataAddr: "10.0.0.1:14260",
ReplicaCtrlAddr: "10.0.0.1:14261",
},
})
// For a replica assignment, ControlBridge builds ReplicaID as <path>/<localServerID>.
expectedLocalReplicaID := "pvc-vol-3/" + canonicalID
localSender := bs.v2Orchestrator.Registry.Sender(expectedLocalReplicaID)
if localSender == nil {
t.Fatalf("local replica sender not found for %s — canonical ID not used", expectedLocalReplicaID)
}
t.Logf("P10P1 canonical identity: localServerID=%q → local ReplicaID=%s", canonicalID, expectedLocalReplicaID)
}
// --- Fail-closed: missing ServerID on proto wire ---
func TestP10P1_FailClosed_MissingServerID(t *testing.T) {
// When the proto assignment has addresses but no ServerID,
// the ControlBridge must skip that replica (fail closed).
bs := &BlockService{
v2Bridge: v2bridge.NewControlBridge(),
v2Orchestrator: engine.NewRecoveryOrchestrator(),
localServerID: "vs1:18080",
}
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
{
Path: "pvc-vol-4",
Epoch: 1,
Role: uint32(blockvol.RolePrimary),
ReplicaDataAddr: "10.0.0.2:9333",
ReplicaCtrlAddr: "10.0.0.2:9334",
// ReplicaServerID intentionally empty — should be skipped.
},
})
// No sender should exist — the replica was skipped due to missing ServerID.
// The ControlBridge logs "scalar replica assignment without ServerID" and skips.
// Check that no sender exists with an address-derived ID.
addressDerivedID := "pvc-vol-4/10.0.0.2:9333"
if bs.v2Orchestrator.Registry.Sender(addressDerivedID) != nil {
t.Fatal("address-derived sender should NOT exist — missing ServerID must fail closed")
}
t.Log("P10P1 fail-closed: missing ServerID → replica skipped, no address-derived fallback")
}