Files
seaweedfs/sw-block/bridge/blockvol/bridge_test.go
T
pingqiuandClaude Opus 4.6 8c326c871c feat: add contract interfaces and pin/release via release-func pattern (Phase 07 P1)
E5 handoff contract (contract.go):
- BlockVolReader: ReadState() → BlockVolState from real blockvol
- BlockVolPinner: HoldWALRetention/HoldSnapshot/HoldFullBase → release func
- BlockVolExecutor: StreamWALEntries/TransferSnapshot/TransferFullBase/TruncateWAL
- Clear import direction: weed-side imports sw-block, not reverse

StorageAdapter refactored:
- Consumes BlockVolReader + BlockVolPinner interfaces
- Pin/release uses release-func pattern (not map-based tracking)
- PushStorageAdapter for tests (push-based, no blockvol dependency)

10 bridge tests:
- 4 control adapter (identity, address change, role mapping, primary)
- 4 storage adapter (retained history, WAL pin reject, snapshot reject, symmetry)
- 1 E2E (assignment → adapter → engine → plan → execute → InSync)
- 1 contract interface verification

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:07:20 -07:00

181 lines
5.4 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()}
}