mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-28 04:30:22 +00:00
NVMe over Fabrics (TCP) target implementation sharing the same BlockVol engine, fencing, replication, and failover as the existing iSCSI target. New package: weed/storage/blockvol/nvme/ (11 files, 2,242 production LOC) - protocol.go: PDU types, opcodes, status codes, marshal/unmarshal - wire.go: TCP reader/writer with header bounds validation - controller.go: IC handshake, per-queue state, command dispatch, KATO - fabric.go: Connect (admin+IO), PropertyGet/Set, Disconnect - identify.go: Controller/Namespace/NS list/NS descriptors (Linux 5.15) - admin.go: SetFeatures, GetFeatures, GetLogPage (SMART/ANA), KeepAlive - io.go: Read (C2HData), Write (inline), Flush, WriteZeros/Trim - server.go: TCP listener, admin session registry, graceful shutdown - adapter.go: BlockVol-to-NVMe bridge, error mapping, ANA state Integration: NVMeConfig + CLI flags (-block.nvme.*), disabled by default. Key design: inline-data writes only (no R2T), MaxH2CDataLength=32KB, single ANA group coherent with BlockVol role, CNTLID session registry for cross-connection IO queues, HostNQN continuity enforcement. Tests: 65 dev + 44 QA adversarial = 109 total, all passing. Bugs fixed during review: IO queue cross-connection (A), header bounds validation (B), write payload size check (C), disconnect error (D), stream desync prevention (E), HostNQN enforcement (F), capsule-before-IC state guard (H), flowCtlOff SQHD timing (I). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
232 lines
6.7 KiB
Go
232 lines
6.7 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
|
|
)
|
|
|
|
func createTestBlockVolFile(t *testing.T, dir, name string) string {
|
|
t.Helper()
|
|
path := filepath.Join(dir, name)
|
|
vol, err := blockvol.CreateBlockVol(path, blockvol.CreateOptions{
|
|
VolumeSize: 1024 * 4096,
|
|
BlockSize: 4096,
|
|
ExtentSize: 65536,
|
|
WALSize: 1 << 20,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
vol.Close()
|
|
return path
|
|
}
|
|
|
|
func TestBlockServiceDisabledByDefault(t *testing.T) {
|
|
// Empty blockDir means feature is disabled.
|
|
bs := StartBlockService("0.0.0.0:3260", "", "", "", NVMeConfig{})
|
|
if bs != nil {
|
|
bs.Shutdown()
|
|
t.Fatal("expected nil BlockService when blockDir is empty")
|
|
}
|
|
|
|
// Shutdown on nil should be safe (no panic).
|
|
var nilBS *BlockService
|
|
nilBS.Shutdown()
|
|
}
|
|
|
|
func TestBlockServiceStartAndShutdown(t *testing.T) {
|
|
dir := t.TempDir()
|
|
createTestBlockVolFile(t, dir, "testvol.blk")
|
|
|
|
bs := StartBlockService("127.0.0.1:0", dir, "iqn.2024-01.com.test:vol.", "127.0.0.1:3260,1", NVMeConfig{})
|
|
if bs == nil {
|
|
t.Fatal("expected non-nil BlockService")
|
|
}
|
|
defer bs.Shutdown()
|
|
|
|
// Verify the volume was registered.
|
|
paths := bs.blockStore.ListBlockVolumes()
|
|
if len(paths) != 1 {
|
|
t.Fatalf("expected 1 volume, got %d", len(paths))
|
|
}
|
|
|
|
expected := filepath.Join(dir, "testvol.blk")
|
|
if paths[0] != expected {
|
|
t.Fatalf("expected path %s, got %s", expected, paths[0])
|
|
}
|
|
}
|
|
|
|
// newTestBlockServiceDirect creates a BlockService without iSCSI target for unit testing.
|
|
func newTestBlockServiceDirect(t *testing.T) *BlockService {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
store := storage.NewBlockVolumeStore()
|
|
t.Cleanup(func() { store.Close() })
|
|
return &BlockService{
|
|
blockStore: store,
|
|
blockDir: dir,
|
|
listenAddr: "0.0.0.0:3260",
|
|
iqnPrefix: "iqn.2024-01.com.seaweedfs:vol.",
|
|
replStates: make(map[string]*volReplState),
|
|
}
|
|
}
|
|
|
|
func createTestVolDirect(t *testing.T, bs *BlockService, name string) string {
|
|
t.Helper()
|
|
path := filepath.Join(bs.blockDir, name+".blk")
|
|
vol, err := blockvol.CreateBlockVol(path, blockvol.CreateOptions{VolumeSize: 4 * 1024 * 1024})
|
|
if err != nil {
|
|
t.Fatalf("create %s: %v", name, err)
|
|
}
|
|
vol.Close()
|
|
if _, err := bs.blockStore.AddBlockVolume(path, "ssd"); err != nil {
|
|
t.Fatalf("register %s: %v", name, err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestBlockService_ProcessAssignment_Primary(t *testing.T) {
|
|
bs := newTestBlockServiceDirect(t)
|
|
path := createTestVolDirect(t, bs, "vol1")
|
|
|
|
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
|
|
{Path: path, Epoch: 1, Role: blockvol.RoleToWire(blockvol.RolePrimary), LeaseTtlMs: 30000},
|
|
})
|
|
|
|
vol, ok := bs.blockStore.GetBlockVolume(path)
|
|
if !ok {
|
|
t.Fatal("volume not found")
|
|
}
|
|
s := vol.Status()
|
|
if s.Role != blockvol.RolePrimary {
|
|
t.Fatalf("expected Primary, got %v", s.Role)
|
|
}
|
|
if s.Epoch != 1 {
|
|
t.Fatalf("expected epoch 1, got %d", s.Epoch)
|
|
}
|
|
}
|
|
|
|
func TestBlockService_ProcessAssignment_Replica(t *testing.T) {
|
|
bs := newTestBlockServiceDirect(t)
|
|
path := createTestVolDirect(t, bs, "vol1")
|
|
|
|
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
|
|
{Path: path, Epoch: 1, Role: blockvol.RoleToWire(blockvol.RoleReplica), LeaseTtlMs: 30000},
|
|
})
|
|
|
|
vol, ok := bs.blockStore.GetBlockVolume(path)
|
|
if !ok {
|
|
t.Fatal("volume not found")
|
|
}
|
|
s := vol.Status()
|
|
if s.Role != blockvol.RoleReplica {
|
|
t.Fatalf("expected Replica, got %v", s.Role)
|
|
}
|
|
}
|
|
|
|
func TestBlockService_ProcessAssignment_UnknownVolume(t *testing.T) {
|
|
bs := newTestBlockServiceDirect(t)
|
|
// Should log warning but not panic.
|
|
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
|
|
{Path: "/nonexistent.blk", Epoch: 1, Role: blockvol.RoleToWire(blockvol.RolePrimary)},
|
|
})
|
|
}
|
|
|
|
func TestBlockService_ProcessAssignment_LeaseRefresh(t *testing.T) {
|
|
bs := newTestBlockServiceDirect(t)
|
|
path := createTestVolDirect(t, bs, "vol1")
|
|
|
|
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
|
|
{Path: path, Epoch: 1, Role: blockvol.RoleToWire(blockvol.RolePrimary), LeaseTtlMs: 30000},
|
|
})
|
|
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
|
|
{Path: path, Epoch: 1, Role: blockvol.RoleToWire(blockvol.RolePrimary), LeaseTtlMs: 60000},
|
|
})
|
|
|
|
vol, _ := bs.blockStore.GetBlockVolume(path)
|
|
s := vol.Status()
|
|
if s.Role != blockvol.RolePrimary || s.Epoch != 1 {
|
|
t.Fatalf("unexpected: role=%v epoch=%d", s.Role, s.Epoch)
|
|
}
|
|
}
|
|
|
|
func TestBlockService_ProcessAssignment_WithReplicaAddrs(t *testing.T) {
|
|
bs := newTestBlockServiceDirect(t)
|
|
path := createTestVolDirect(t, bs, "vol1")
|
|
|
|
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
|
|
{
|
|
Path: path, Epoch: 1, Role: blockvol.RoleToWire(blockvol.RolePrimary),
|
|
LeaseTtlMs: 30000, ReplicaDataAddr: "10.0.0.2:4260", ReplicaCtrlAddr: "10.0.0.2:4261",
|
|
},
|
|
})
|
|
|
|
vol, _ := bs.blockStore.GetBlockVolume(path)
|
|
if vol.Status().Role != blockvol.RolePrimary {
|
|
t.Fatalf("expected Primary")
|
|
}
|
|
}
|
|
|
|
func TestBlockService_HeartbeatIncludesReplicaAddrs(t *testing.T) {
|
|
bs := newTestBlockServiceDirect(t)
|
|
path := createTestVolDirect(t, bs, "vol1")
|
|
|
|
bs.replMu.Lock()
|
|
bs.replStates[path] = &volReplState{
|
|
replicaDataAddr: "10.0.0.5:4260",
|
|
replicaCtrlAddr: "10.0.0.5:4261",
|
|
}
|
|
bs.replMu.Unlock()
|
|
|
|
dataAddr, ctrlAddr := bs.GetReplState(path)
|
|
if dataAddr != "10.0.0.5:4260" || ctrlAddr != "10.0.0.5:4261" {
|
|
t.Fatalf("got data=%q ctrl=%q", dataAddr, ctrlAddr)
|
|
}
|
|
}
|
|
|
|
func TestBlockService_ReplicationPorts_Deterministic(t *testing.T) {
|
|
bs := &BlockService{listenAddr: "0.0.0.0:3260"}
|
|
d1, c1, r1 := bs.ReplicationPorts("/data/vol1.blk")
|
|
d2, c2, r2 := bs.ReplicationPorts("/data/vol1.blk")
|
|
if d1 != d2 || c1 != c2 || r1 != r2 {
|
|
t.Fatalf("ports not deterministic")
|
|
}
|
|
if c1 != d1+1 || r1 != d1+2 {
|
|
t.Fatalf("port offsets wrong: data=%d ctrl=%d rebuild=%d", d1, c1, r1)
|
|
}
|
|
}
|
|
|
|
func TestBlockService_ReplicationPorts_StableAcrossRestarts(t *testing.T) {
|
|
bs1 := &BlockService{listenAddr: "0.0.0.0:3260"}
|
|
bs2 := &BlockService{listenAddr: "0.0.0.0:3260"}
|
|
d1, _, _ := bs1.ReplicationPorts("/data/vol1.blk")
|
|
d2, _, _ := bs2.ReplicationPorts("/data/vol1.blk")
|
|
if d1 != d2 {
|
|
t.Fatalf("ports not stable: %d vs %d", d1, d2)
|
|
}
|
|
}
|
|
|
|
func TestBlockService_ProcessAssignment_InvalidTransition(t *testing.T) {
|
|
bs := newTestBlockServiceDirect(t)
|
|
path := createTestVolDirect(t, bs, "vol1")
|
|
|
|
// Assign as primary epoch 5.
|
|
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
|
|
{Path: path, Epoch: 5, Role: blockvol.RoleToWire(blockvol.RolePrimary), LeaseTtlMs: 30000},
|
|
})
|
|
|
|
// Try to assign with lower epoch — should be rejected silently.
|
|
bs.ProcessAssignments([]blockvol.BlockVolumeAssignment{
|
|
{Path: path, Epoch: 3, Role: blockvol.RoleToWire(blockvol.RoleReplica), LeaseTtlMs: 30000},
|
|
})
|
|
|
|
vol, _ := bs.blockStore.GetBlockVolume(path)
|
|
s := vol.Status()
|
|
if s.Epoch != 5 {
|
|
t.Fatalf("epoch should still be 5, got %d", s.Epoch)
|
|
}
|
|
}
|