Files
seaweedfs/sw-block/runtime/volumev2/frontend_test.go
T
pingqiuandClaude Opus 4.6 b8c6944e3f feat: V2 MVP milestone — masterv2 + volumev2 + in-process failover
V2 runtime packages:
- sw-block/runtime/masterv2: identity authority (desired state,
  heartbeat handling, promotion arbitration via SelectPromotionCandidate)
- sw-block/runtime/volumev2: per-volume micro-cluster shell (node,
  orchestrator, control session, iSCSI frontend, takeover gate,
  failover session + driver, replica summary reconstruction)
- sw-block/runtime/purev2: RF1 execution shell (engine + store +
  dispatcher + local boundary observations)
- sw-block/runtime/protocolv2: three-channel separation
  (heartbeat/assignment/query + replica summary)

V2 binaries:
- sw-block/cmd/v2singleblock: single-node RF1 block server
- sw-block/cmd/purev2rf1: minimal RF1 runtime binary

Milestone capabilities:
- RF1 write/read/sync with engine-driven mode projection
- masterv2 ↔ volumev2 heartbeat convergence + assignment reissue
- Promotion query with fresh CommittedLSN/WALHeadLSN evidence
- Replica summary for bounded takeover reconstruction
- Primary-loss reconstruction from peer summaries (fail-closed gate)
- In-process failover driver with session observability
- Local boundary observations feed engine (Committed/Durable/Checkpoint)

Design docs:
- v2-two-loop-protocol.md: identity vs data-control separation
- v2-automata-ownership-map.md: event/command ownership split
- v2-loop1-surface-draft.md: heartbeat/query/assignment field spec
- v2-volumev2-single-node-mvp.md: target layering
- v2-kernel-closure-review.md: per-volume micro-cluster principle
- v2-pure-runtime-rf1-bootstrap.md, v2-capability-map.md,
  v2-proof-and-retest-pyramid.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:08:02 -07:00

234 lines
6.4 KiB
Go

package volumev2
import (
"bytes"
"encoding/binary"
"net"
"path/filepath"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/sw-block/runtime/masterv2"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/iscsi"
)
func TestVolumeV2_ExportISCSI_StartsFrontendAndAcceptsLogin(t *testing.T) {
master := masterv2.New(masterv2.Config{})
node, err := New(Config{NodeID: "node-a"})
if err != nil {
t.Fatalf("new node: %v", err)
}
defer node.Close()
session, err := NewInProcessSession(master)
if err != nil {
t.Fatalf("new session: %v", err)
}
orchestrator, err := NewOrchestrator(node, session)
if err != nil {
t.Fatalf("new orchestrator: %v", err)
}
path := filepath.Join(t.TempDir(), "frontend-vol.blk")
if err := master.DeclarePrimary(masterv2.VolumeSpec{
Name: "frontend-vol",
Path: path,
PrimaryNodeID: "node-a",
CreateOptions: testCreateOptions(),
}); err != nil {
t.Fatalf("declare primary: %v", err)
}
if err := orchestrator.SyncOnce(); err != nil {
t.Fatalf("sync 1: %v", err)
}
if err := orchestrator.SyncOnce(); err != nil {
t.Fatalf("sync 2: %v", err)
}
export, err := node.ExportISCSI("frontend-vol", "127.0.0.1:0", "iqn.2026-04.com.seaweedfs:test.frontend-vol")
if err != nil {
t.Fatalf("export iscsi: %v", err)
}
defer export.Close()
conn, err := net.DialTimeout("tcp", export.Address(), 2*time.Second)
if err != nil {
t.Fatalf("dial target: %v", err)
}
defer conn.Close()
params := iscsi.NewParams()
params.Set("InitiatorName", "iqn.2026-04.com.seaweedfs:initiator.test")
params.Set("TargetName", export.IQN())
params.Set("SessionType", "Normal")
loginReq := &iscsi.PDU{}
loginReq.SetOpcode(iscsi.OpLoginReq)
loginReq.SetLoginStages(iscsi.StageSecurityNeg, iscsi.StageFullFeature)
loginReq.SetLoginTransit(true)
loginReq.SetISID([6]byte{0x00, 0x02, 0x3D, 0x00, 0x00, 0x01})
loginReq.SetCmdSN(1)
loginReq.DataSegment = params.Encode()
if err := iscsi.WritePDU(conn, loginReq); err != nil {
t.Fatalf("write login req: %v", err)
}
resp, err := iscsi.ReadPDU(conn)
if err != nil {
t.Fatalf("read login resp: %v", err)
}
if resp.LoginStatusClass() != iscsi.LoginStatusSuccess {
t.Fatalf("login failed: %d/%d", resp.LoginStatusClass(), resp.LoginStatusDetail())
}
}
func TestKernelDataPlaneClosure_ISCSIWriteReadVerify(t *testing.T) {
master := masterv2.New(masterv2.Config{})
node, err := New(Config{NodeID: "node-a"})
if err != nil {
t.Fatalf("new node: %v", err)
}
defer node.Close()
session, err := NewInProcessSession(master)
if err != nil {
t.Fatalf("new session: %v", err)
}
orchestrator, err := NewOrchestrator(node, session)
if err != nil {
t.Fatalf("new orchestrator: %v", err)
}
path := filepath.Join(t.TempDir(), "data-plane-vol.blk")
if err := master.DeclarePrimary(masterv2.VolumeSpec{
Name: "data-plane-vol",
Path: path,
PrimaryNodeID: "node-a",
CreateOptions: testCreateOptions(),
}); err != nil {
t.Fatalf("declare primary: %v", err)
}
if err := orchestrator.SyncOnce(); err != nil {
t.Fatalf("sync 1: %v", err)
}
if err := orchestrator.SyncOnce(); err != nil {
t.Fatalf("sync 2: %v", err)
}
export, err := node.ExportISCSI("data-plane-vol", "127.0.0.1:0", "iqn.2026-04.com.seaweedfs:test.data-plane-vol")
if err != nil {
t.Fatalf("export iscsi: %v", err)
}
defer export.Close()
conn := mustLoginISCSI(t, export.Address(), export.IQN())
defer conn.Close()
writeData := make([]byte, 4096)
for i := range writeData {
writeData[i] = byte((i * 7) % 251)
}
var writeCDB [16]byte
writeCDB[0] = iscsi.ScsiWrite10
binary.BigEndian.PutUint32(writeCDB[2:6], 0)
binary.BigEndian.PutUint16(writeCDB[7:9], 1)
resp := sendSCSICmd(t, conn, writeCDB, 2, false, true, writeData, uint32(len(writeData)))
if resp.SCSIStatus() != iscsi.SCSIStatusGood {
t.Fatalf("iscsi write failed: status=%d", resp.SCSIStatus())
}
var syncCDB [16]byte
syncCDB[0] = iscsi.ScsiSyncCache10
resp = sendSCSICmd(t, conn, syncCDB, 3, false, false, nil, 0)
if resp.SCSIStatus() != iscsi.SCSIStatusGood {
t.Fatalf("iscsi sync cache failed: status=%d", resp.SCSIStatus())
}
var readCDB [16]byte
readCDB[0] = iscsi.ScsiRead10
binary.BigEndian.PutUint32(readCDB[2:6], 0)
binary.BigEndian.PutUint16(readCDB[7:9], 1)
resp = sendSCSICmd(t, conn, readCDB, 4, true, false, nil, uint32(len(writeData)))
if resp.Opcode() != iscsi.OpSCSIDataIn {
t.Fatalf("expected Data-In, got %s", iscsi.OpcodeName(resp.Opcode()))
}
if !bytes.Equal(resp.DataSegment, writeData) {
t.Fatal("iscsi readback mismatch")
}
readBack, err := node.ReadLBA("data-plane-vol", 0, uint32(len(writeData)))
if err != nil {
t.Fatalf("backend read: %v", err)
}
if !bytes.Equal(readBack, writeData) {
t.Fatal("backend readback mismatch")
}
}
func mustLoginISCSI(t *testing.T, addr, iqn string) net.Conn {
t.Helper()
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
t.Fatalf("dial target: %v", err)
}
params := iscsi.NewParams()
params.Set("InitiatorName", "iqn.2026-04.com.seaweedfs:initiator.test")
params.Set("TargetName", iqn)
params.Set("SessionType", "Normal")
loginReq := &iscsi.PDU{}
loginReq.SetOpcode(iscsi.OpLoginReq)
loginReq.SetLoginStages(iscsi.StageSecurityNeg, iscsi.StageFullFeature)
loginReq.SetLoginTransit(true)
loginReq.SetISID([6]byte{0x00, 0x02, 0x3D, 0x00, 0x00, 0x01})
loginReq.SetCmdSN(1)
loginReq.DataSegment = params.Encode()
if err := iscsi.WritePDU(conn, loginReq); err != nil {
conn.Close()
t.Fatalf("write login req: %v", err)
}
resp, err := iscsi.ReadPDU(conn)
if err != nil {
conn.Close()
t.Fatalf("read login resp: %v", err)
}
if resp.LoginStatusClass() != iscsi.LoginStatusSuccess {
conn.Close()
t.Fatalf("login failed: %d/%d", resp.LoginStatusClass(), resp.LoginStatusDetail())
}
return conn
}
func sendSCSICmd(t *testing.T, conn net.Conn, cdb [16]byte, cmdSN uint32, read bool, write bool, dataOut []byte, expLen uint32) *iscsi.PDU {
t.Helper()
cmd := &iscsi.PDU{}
cmd.SetOpcode(iscsi.OpSCSICmd)
flags := uint8(iscsi.FlagF)
if read {
flags |= iscsi.FlagR
}
if write {
flags |= iscsi.FlagW
}
cmd.SetOpSpecific1(flags)
cmd.SetInitiatorTaskTag(cmdSN)
cmd.SetExpectedDataTransferLength(expLen)
cmd.SetCmdSN(cmdSN)
cmd.SetCDB(cdb)
if dataOut != nil {
cmd.DataSegment = dataOut
}
if err := iscsi.WritePDU(conn, cmd); err != nil {
t.Fatalf("write scsi cmd: %v", err)
}
resp, err := iscsi.ReadPDU(conn)
if err != nil {
t.Fatalf("read scsi resp: %v", err)
}
return resp
}