mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-10 08:17:44 +02:00
Batch 7: Command dispatch binding extraction - New weed/server/blockcmd package: CommandHandler interface + DispatchCommands - volume_server_block.go applyCoreCommandsWithAssignment delegates to dispatcher - weed/server still owns RecordCommand, EmitCoreEvent, PublishProjection - v2bridge NOT given command-switch or event-emission semantics Phase 16C: Rebuilding assignment enters core command path Phase 16D: Rebuild recovery-task startup is command-driven Phase 16E: Catch-up recovery-task startup is command-driven Engine refinements: - RecoveryTarget on AssignmentDelivered event - shouldStartRecoveryTask / shouldStartReceiver guards - bootstrapReason: awaiting_rebuild_start Bridge/contract updates: - control_adapter.go: refined translation helpers - contract.go: executor port alignment Migration design docs (Batch 1-3 delivered, design artifacts): - v2-first/second/third-migration-batch.md + task-pack.md - v2-assignment-translation-unification.md - v2-execution-muscles-inventory.md - v2-separation-port-layer-audit.md - v2-legacy-runtime-exit-criteria.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
202 lines
6.0 KiB
Go
202 lines
6.0 KiB
Go
package blockvol
|
|
|
|
import (
|
|
"testing"
|
|
|
|
engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication"
|
|
)
|
|
|
|
// ============================================================
|
|
// Phase 07 P0/P1: Bridge adapter tests
|
|
// ============================================================
|
|
|
|
// --- E1: Stable identity ---
|
|
|
|
func TestControlAdapter_StableIdentity(t *testing.T) {
|
|
ca := NewControlAdapter()
|
|
|
|
intent := ca.ToAssignmentIntent(
|
|
MasterAssignment{VolumeName: "pvc-data-1", Epoch: 3, Role: "primary", PrimaryServerID: "vs1"},
|
|
[]MasterAssignment{
|
|
{VolumeName: "pvc-data-1", Epoch: 3, Role: "replica", ReplicaServerID: "vs2",
|
|
DataAddr: "10.0.0.2:9333", CtrlAddr: "10.0.0.2:9334", AddrVersion: 1},
|
|
},
|
|
)
|
|
|
|
r := intent.Replicas[0]
|
|
if r.ReplicaID != "pvc-data-1/vs2" {
|
|
t.Fatalf("ReplicaID=%s (must be volume/server)", r.ReplicaID)
|
|
}
|
|
if intent.RecoveryTargets["pvc-data-1/vs2"] != engine.SessionCatchUp {
|
|
t.Fatalf("recovery=%s", intent.RecoveryTargets["pvc-data-1/vs2"])
|
|
}
|
|
}
|
|
|
|
func TestControlAdapter_AddressChangePreservesIdentity(t *testing.T) {
|
|
ca := NewControlAdapter()
|
|
|
|
intent1 := ca.ToAssignmentIntent(
|
|
MasterAssignment{VolumeName: "vol1", Epoch: 1, Role: "primary"},
|
|
[]MasterAssignment{{VolumeName: "vol1", ReplicaServerID: "vs2", Role: "replica", DataAddr: "10.0.0.2:9333", AddrVersion: 1}},
|
|
)
|
|
intent2 := ca.ToAssignmentIntent(
|
|
MasterAssignment{VolumeName: "vol1", Epoch: 1, Role: "primary"},
|
|
[]MasterAssignment{{VolumeName: "vol1", ReplicaServerID: "vs2", Role: "replica", DataAddr: "10.0.0.3:9333", AddrVersion: 2}},
|
|
)
|
|
|
|
if intent1.Replicas[0].ReplicaID != intent2.Replicas[0].ReplicaID {
|
|
t.Fatal("identity changed")
|
|
}
|
|
}
|
|
|
|
func TestControlAdapter_RebuildRoleMapping(t *testing.T) {
|
|
ca := NewControlAdapter()
|
|
intent := ca.ToAssignmentIntent(
|
|
MasterAssignment{VolumeName: "vol1", Epoch: 1, Role: "primary"},
|
|
[]MasterAssignment{{VolumeName: "vol1", ReplicaServerID: "vs2", Role: "rebuilding", DataAddr: "10.0.0.2:9333"}},
|
|
)
|
|
if intent.RecoveryTargets["vol1/vs2"] != engine.SessionRebuild {
|
|
t.Fatalf("got %s", intent.RecoveryTargets["vol1/vs2"])
|
|
}
|
|
}
|
|
|
|
func TestControlAdapter_PrimaryNoRecovery(t *testing.T) {
|
|
ca := NewControlAdapter()
|
|
intent := ca.ToAssignmentIntent(
|
|
MasterAssignment{VolumeName: "vol1", Epoch: 1, Role: "primary"},
|
|
[]MasterAssignment{},
|
|
)
|
|
if len(intent.RecoveryTargets) != 0 {
|
|
t.Fatal("primary should not have recovery targets")
|
|
}
|
|
}
|
|
|
|
// --- E2: Storage adapter via contract interfaces ---
|
|
|
|
func TestStorageAdapter_RetainedHistoryFromReader(t *testing.T) {
|
|
psa := NewPushStorageAdapter()
|
|
psa.UpdateState(BlockVolState{
|
|
WALHeadLSN: 100, WALTailLSN: 30, CommittedLSN: 90,
|
|
CheckpointLSN: 50, CheckpointTrusted: true,
|
|
})
|
|
|
|
rh := psa.GetRetainedHistory()
|
|
if rh.HeadLSN != 100 || rh.TailLSN != 30 || rh.CommittedLSN != 90 {
|
|
t.Fatalf("head=%d tail=%d committed=%d", rh.HeadLSN, rh.TailLSN, rh.CommittedLSN)
|
|
}
|
|
if rh.CheckpointLSN != 50 || !rh.CheckpointTrusted {
|
|
t.Fatalf("checkpoint=%d trusted=%v", rh.CheckpointLSN, rh.CheckpointTrusted)
|
|
}
|
|
}
|
|
|
|
func TestStorageAdapter_WALPinRejectsRecycled(t *testing.T) {
|
|
psa := NewPushStorageAdapter()
|
|
psa.UpdateState(BlockVolState{WALTailLSN: 50})
|
|
|
|
_, err := psa.PinWALRetention(30)
|
|
if err == nil {
|
|
t.Fatal("should reject recycled range")
|
|
}
|
|
}
|
|
|
|
func TestStorageAdapter_SnapshotPinRejectsUntrusted(t *testing.T) {
|
|
psa := NewPushStorageAdapter()
|
|
psa.UpdateState(BlockVolState{CheckpointLSN: 50, CheckpointTrusted: false})
|
|
|
|
_, err := psa.PinSnapshot(50)
|
|
if err == nil {
|
|
t.Fatal("should reject untrusted checkpoint")
|
|
}
|
|
}
|
|
|
|
func TestStorageAdapter_PinReleaseSymmetry(t *testing.T) {
|
|
psa := NewPushStorageAdapter()
|
|
psa.UpdateState(BlockVolState{WALTailLSN: 0, CheckpointLSN: 50, CheckpointTrusted: true})
|
|
|
|
walPin, _ := psa.PinWALRetention(10)
|
|
snapPin, _ := psa.PinSnapshot(50)
|
|
basePin, _ := psa.PinFullBase(100)
|
|
|
|
// Pins tracked.
|
|
if len(psa.releaseFuncs) != 3 {
|
|
t.Fatalf("pins=%d", len(psa.releaseFuncs))
|
|
}
|
|
|
|
// Release all.
|
|
psa.ReleaseWALRetention(walPin)
|
|
psa.ReleaseSnapshot(snapPin)
|
|
psa.ReleaseFullBase(basePin)
|
|
|
|
if len(psa.releaseFuncs) != 0 {
|
|
t.Fatalf("leaked pins=%d", len(psa.releaseFuncs))
|
|
}
|
|
}
|
|
|
|
// --- E3: End-to-end bridge flow ---
|
|
|
|
func TestBridge_E2E_AssignmentToRecovery(t *testing.T) {
|
|
ca := NewControlAdapter()
|
|
psa := NewPushStorageAdapter()
|
|
psa.UpdateState(BlockVolState{
|
|
WALHeadLSN: 100, WALTailLSN: 30, CommittedLSN: 100,
|
|
CheckpointLSN: 50, CheckpointTrusted: true,
|
|
})
|
|
|
|
intent := ca.ToAssignmentIntent(
|
|
MasterAssignment{VolumeName: "vol1", Epoch: 1, Role: "primary", PrimaryServerID: "vs1"},
|
|
[]MasterAssignment{
|
|
{VolumeName: "vol1", ReplicaServerID: "vs2", Role: "replica",
|
|
DataAddr: "10.0.0.2:9333", CtrlAddr: "10.0.0.2:9334", AddrVersion: 1},
|
|
},
|
|
)
|
|
|
|
drv := engine.NewRecoveryDriver(psa)
|
|
drv.Orchestrator.ProcessAssignment(intent)
|
|
|
|
plan, err := drv.PlanRecovery("vol1/vs2", 70)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if plan.Outcome != engine.OutcomeCatchUp {
|
|
t.Fatalf("outcome=%s", plan.Outcome)
|
|
}
|
|
|
|
exec := engine.NewCatchUpExecutor(drv, plan)
|
|
if err := exec.Execute([]uint64{80, 90, 100}, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if drv.Orchestrator.Registry.Sender("vol1/vs2").State() != engine.StateInSync {
|
|
t.Fatalf("state=%s", drv.Orchestrator.Registry.Sender("vol1/vs2").State())
|
|
}
|
|
}
|
|
|
|
// --- E5: Contract interface boundary ---
|
|
|
|
func TestContract_BlockVolReaderInterface(t *testing.T) {
|
|
// Verify the contract interface is implementable.
|
|
var _ BlockVolReader = &pushReader{psa: NewPushStorageAdapter()}
|
|
var _ BlockVolPinner = &pushPinner{psa: NewPushStorageAdapter()}
|
|
var _ BlockVolCatchUpIO = fakeExecutor{}
|
|
var _ BlockVolRebuildIO = fakeExecutor{}
|
|
var _ BlockVolExecutor = fakeExecutor{}
|
|
}
|
|
|
|
type fakeExecutor struct{}
|
|
|
|
func (fakeExecutor) StreamWALEntries(startExclusive, endInclusive uint64) (uint64, error) {
|
|
return endInclusive, nil
|
|
}
|
|
|
|
func (fakeExecutor) TruncateWAL(truncateLSN uint64) error {
|
|
return nil
|
|
}
|
|
|
|
func (fakeExecutor) TransferSnapshot(snapshotLSN uint64) error {
|
|
return nil
|
|
}
|
|
|
|
func (fakeExecutor) TransferFullBase(committedLSN uint64) (uint64, error) {
|
|
return committedLSN, nil
|
|
}
|