Files
seaweedfs/weed/server/volume_server_uds_test.go
T
pingqiuandClaude Opus 4.6 ec49e5a28f feat(uds): add Store opcode, AppendRawNeedle, and SRA transport client
Volume replication groundwork (Phase 6 hardening plan):

- UDS handler: Add Store opcode (0x02) with opcode-based dispatch,
  StoreRequest/StoreResponse wire types, and size validation
- storage/store.go: AppendRawNeedle() for accepting pre-serialized
  needle bytes from RDMA replication path
- storage/needle: Return raw bytes from needle Append() to enable
  forwarding exact .dat bytes without re-serialization
- storage/sra_transport.go: UDS client for outbound RDMA replication
  (connects to /tmp/sra-transport.sock, nil-safe fallback to HTTP)
- command/volume.go: Wire -uds.transport flag
- Integration test for Store opcode round-trip
- Golden wire tests for Store request/response encoding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:35:08 -08:00

424 lines
12 KiB
Go

package weed_server
import (
"encoding/binary"
"io"
"net"
"os"
"path/filepath"
"testing"
"time"
)
func TestLocateRequestResponseSize(t *testing.T) {
// Verify protocol sizes match spec
if UdsRequestSize != 24 {
t.Errorf("UdsRequestSize expected 24, got %d", UdsRequestSize)
}
if UdsResponseSize != 32 {
t.Errorf("UdsResponseSize expected 32, got %d", UdsResponseSize)
}
}
func TestLocateRequestSerialization(t *testing.T) {
// Wire format: opcode(1) + pad(3) + request_id(4) + fid(16) = 24
req := LocateRequest{
Opcode: 1, // VolumeOpcode::Locate
RequestId: 42,
}
copy(req.Fid[:], "3,01637037")
buf := make([]byte, UdsRequestSize)
buf[0] = req.Opcode
binary.LittleEndian.PutUint32(buf[4:8], req.RequestId)
copy(buf[8:24], req.Fid[:])
// Verify fid is correctly placed at offset 8
fid := string(buf[8:18])
if fid != "3,01637037" {
t.Errorf("Expected fid='3,01637037', got '%s'", fid)
}
// Verify request_id
requestId := binary.LittleEndian.Uint32(buf[4:8])
if requestId != 42 {
t.Errorf("Expected request_id=42, got %d", requestId)
}
}
func TestLocateResponseSerialization(t *testing.T) {
resp := &LocateResponse{
Status: UdsStatusOk,
VolumeId: 3,
Offset: 1024,
Length: 4096,
}
buf := make([]byte, UdsResponseSize)
buf[0] = resp.Status
binary.LittleEndian.PutUint32(buf[4:8], resp.VolumeId)
binary.LittleEndian.PutUint64(buf[8:16], resp.Offset)
binary.LittleEndian.PutUint64(buf[16:24], resp.Length)
// Deserialize and verify
status := buf[0]
if status != UdsStatusOk {
t.Errorf("Expected status=0, got %d", status)
}
volumeId := binary.LittleEndian.Uint32(buf[4:8])
if volumeId != 3 {
t.Errorf("Expected volumeId=3, got %d", volumeId)
}
offset := binary.LittleEndian.Uint64(buf[8:16])
if offset != 1024 {
t.Errorf("Expected offset=1024, got %d", offset)
}
length := binary.LittleEndian.Uint64(buf[16:24])
if length != 4096 {
t.Errorf("Expected length=4096, got %d", length)
}
}
func TestUdsStatusCodes(t *testing.T) {
if UdsStatusOk != 0 {
t.Errorf("UdsStatusOk expected 0, got %d", UdsStatusOk)
}
if UdsStatusNotFound != 1 {
t.Errorf("UdsStatusNotFound expected 1, got %d", UdsStatusNotFound)
}
if UdsStatusError != 2 {
t.Errorf("UdsStatusError expected 2, got %d", UdsStatusError)
}
}
// TestUdsServerBasicConnection tests that we can create and connect to a UDS server
func TestUdsServerBasicConnection(t *testing.T) {
// Create temp socket path
tmpDir := t.TempDir()
socketPath := filepath.Join(tmpDir, "test.sock")
// Create a mock volume server (nil is fine since we won't make real lookups)
uds, err := NewUdsServer(nil, socketPath)
if err != nil {
t.Fatalf("Failed to create UDS server: %v", err)
}
defer uds.Stop()
uds.Start()
// Give server time to start
time.Sleep(10 * time.Millisecond)
// Try to connect
conn, err := net.Dial("unix", socketPath)
if err != nil {
t.Fatalf("Failed to connect to UDS server: %v", err)
}
defer conn.Close()
// Connection successful
}
// TestUdsServerProtocol tests the basic protocol exchange
func TestUdsServerProtocol(t *testing.T) {
// Create temp socket path
tmpDir := t.TempDir()
socketPath := filepath.Join(tmpDir, "test.sock")
// Create a mock volume server (vs=nil means handleLocate will return NotFound)
uds, err := NewUdsServer(nil, socketPath)
if err != nil {
t.Fatalf("Failed to create UDS server: %v", err)
}
defer uds.Stop()
uds.Start()
time.Sleep(10 * time.Millisecond)
// Connect
conn, err := net.Dial("unix", socketPath)
if err != nil {
t.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
// Send a request (will fail because vs is nil, but we're testing protocol)
// Wire format: opcode(1) + pad(3) + request_id(4) + fid(16)
reqBuf := make([]byte, UdsRequestSize)
reqBuf[0] = 1 // VolumeOpcode::Locate
binary.LittleEndian.PutUint32(reqBuf[4:8], 1) // request_id
copy(reqBuf[8:24], "3,01637037") // fid at offset 8
_, err = conn.Write(reqBuf)
if err != nil {
t.Fatalf("Failed to write request: %v", err)
}
// Read response
respBuf := make([]byte, UdsResponseSize)
_, err = io.ReadFull(conn, respBuf)
if err != nil {
t.Fatalf("Failed to read response: %v", err)
}
// Since vs is nil, we expect UdsStatusError
status := respBuf[0]
if status != UdsStatusError {
t.Errorf("Expected UdsStatusError (%d) for nil VolumeServer, got %d", UdsStatusError, status)
}
// DatPathLen should be 0 for error responses
datPathLen := binary.LittleEndian.Uint16(respBuf[24:26])
if datPathLen != 0 {
t.Errorf("Expected DatPathLen=0 for error, got %d", datPathLen)
}
}
// TestUdsSocketCleanup tests that existing socket is removed
func TestUdsSocketCleanup(t *testing.T) {
tmpDir := t.TempDir()
socketPath := filepath.Join(tmpDir, "test.sock")
// Create a dummy file at the socket path
if err := os.WriteFile(socketPath, []byte("dummy"), 0644); err != nil {
t.Fatalf("Failed to create dummy file: %v", err)
}
// NewUdsServer should remove the existing file
uds, err := NewUdsServer(nil, socketPath)
if err != nil {
t.Fatalf("Failed to create UDS server (should have cleaned up old socket): %v", err)
}
uds.Stop()
}
// ============================================================
// Cross-language wire contract tests (L1 foundation).
//
// These golden bytes MUST match the Rust side exactly
// (sra-volume/tests/uds_locate_test.rs::wire_contract).
// If either side changes, the contract breaks.
// ============================================================
func TestGoldenLocateRequest(t *testing.T) {
// Build the same request as Rust: fid="8,022aeb9e22", request_id=42
golden := [UdsRequestSize]byte{
0x01, // opcode = Locate
0x00, 0x00, 0x00, // padding
0x2A, 0x00, 0x00, 0x00, // request_id = 42 (LE)
'8', ',', '0', '2', '2', 'a', 'e', 'b', // fid[0..8]
'9', 'e', '2', '2', 0x00, 0x00, 0x00, 0x00, // fid[8..16]
}
// Serialize from Go struct
buf := make([]byte, UdsRequestSize)
buf[0] = 1 // opcode = Locate
binary.LittleEndian.PutUint32(buf[4:8], 42) // request_id
copy(buf[8:24], "8,022aeb9e22")
for i := 0; i < UdsRequestSize; i++ {
if buf[i] != golden[i] {
t.Errorf("Go request byte[%d] = 0x%02X, golden = 0x%02X", i, buf[i], golden[i])
}
}
}
func TestGoldenLocateResponse(t *testing.T) {
// Build the same response as Rust: vol=8, offset=4096, length=4108, dat_path_len=29
golden := [UdsResponseSize]byte{
0x00, // status = Ok
0x00, 0x00, 0x00, // padding
0x08, 0x00, 0x00, 0x00, // volume_id = 8 (LE)
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // offset = 4096 (LE)
0x0C, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // length = 4108 (LE)
0x1E, 0x00, // dat_path_len = 30 (LE)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
}
// Serialize from Go struct
buf := make([]byte, UdsResponseSize)
buf[0] = UdsStatusOk
binary.LittleEndian.PutUint32(buf[4:8], 8)
binary.LittleEndian.PutUint64(buf[8:16], 4096)
binary.LittleEndian.PutUint64(buf[16:24], 4108)
binary.LittleEndian.PutUint16(buf[24:26], 30)
for i := 0; i < UdsResponseSize; i++ {
if buf[i] != golden[i] {
t.Errorf("Go response byte[%d] = 0x%02X, golden = 0x%02X", i, buf[i], golden[i])
}
}
// Verify trailing dat_path
datPath := "/opt/work/data/weed/test_8.dat"
if len(datPath) != 30 {
t.Errorf("dat_path length = %d, expected 30", len(datPath))
}
}
func TestNeedleHeaderIsBigEndian(t *testing.T) {
// SeaweedFS stores multi-byte needle fields as big-endian.
// This test documents the convention for sra-volume (Rust) readers.
//
// DataSize = 1000 (0x000003E8) in big-endian = [0x00, 0x00, 0x03, 0xE8]
var dataSize uint32 = 1000
buf := make([]byte, 4)
buf[0] = byte(dataSize >> 24)
buf[1] = byte(dataSize >> 16)
buf[2] = byte(dataSize >> 8)
buf[3] = byte(dataSize)
expected := []byte{0x00, 0x00, 0x03, 0xE8}
for i := 0; i < 4; i++ {
if buf[i] != expected[i] {
t.Errorf("DataSize byte[%d] = 0x%02X, expected 0x%02X (big-endian)", i, buf[i], expected[i])
}
}
}
// ============================================================
// Golden wire tests for Store opcode (0x02)
// ============================================================
func TestGoldenStoreRequest(t *testing.T) {
// Store request header: opcode(1) + pad(3) + request_id(4) + volume_id(4) +
// needle_version(1) + pad(3) + raw_bytes_len(4) = 20 bytes
// Values: opcode=0x02, request_id=99, volume_id=5, needle_version=2, raw_bytes_len=1024
golden := [UdsStoreHeaderSize]byte{
0x02, // opcode = Store
0x00, 0x00, 0x00, // padding
0x63, 0x00, 0x00, 0x00, // request_id = 99 (LE)
0x05, 0x00, 0x00, 0x00, // volume_id = 5 (LE)
0x02, // needle_version = 2
0x00, 0x00, 0x00, // padding
0x00, 0x04, 0x00, 0x00, // raw_bytes_len = 1024 (LE)
}
// Serialize from Go
buf := make([]byte, UdsStoreHeaderSize)
buf[0] = UdsOpcodeStore
binary.LittleEndian.PutUint32(buf[4:8], 99)
binary.LittleEndian.PutUint32(buf[8:12], 5)
buf[12] = 2 // needle_version
binary.LittleEndian.PutUint32(buf[16:20], 1024)
for i := 0; i < UdsStoreHeaderSize; i++ {
if buf[i] != golden[i] {
t.Errorf("Store request byte[%d] = 0x%02X, golden = 0x%02X", i, buf[i], golden[i])
}
}
}
func TestGoldenStoreResponse(t *testing.T) {
// Store response: status(1) + pad(3) + request_id(4) = 8 bytes
golden := [UdsStoreResponseSize]byte{
0x00, // status = OK
0x00, 0x00, 0x00, // padding
0x63, 0x00, 0x00, 0x00, // request_id = 99 (LE)
}
buf := make([]byte, UdsStoreResponseSize)
buf[0] = UdsStatusOk
binary.LittleEndian.PutUint32(buf[4:8], 99)
for i := 0; i < UdsStoreResponseSize; i++ {
if buf[i] != golden[i] {
t.Errorf("Store response byte[%d] = 0x%02X, golden = 0x%02X", i, buf[i], golden[i])
}
}
}
// ============================================================
// Golden wire tests for Replicate opcode (0x03)
// These match the Rust transport_listener wire format.
// ============================================================
func TestGoldenReplicateRequestHeader(t *testing.T) {
// Replicate request header: opcode(1) + pad(3) + request_id(4) + volume_id(4) + target_host_len(4) = 16 bytes
// Values: opcode=0x03, request_id=7, volume_id=3, target_host_len=15 ("10.0.0.3:18515")
golden := [16]byte{
0x03, // opcode = Replicate
0x00, 0x00, 0x00, // padding
0x07, 0x00, 0x00, 0x00, // request_id = 7 (LE)
0x03, 0x00, 0x00, 0x00, // volume_id = 3 (LE)
0x0F, 0x00, 0x00, 0x00, // target_host_len = 15 (LE)
}
buf := make([]byte, 16)
buf[0] = 0x03 // opcode
binary.LittleEndian.PutUint32(buf[4:8], 7)
binary.LittleEndian.PutUint32(buf[8:12], 3)
binary.LittleEndian.PutUint32(buf[12:16], 15)
for i := 0; i < 16; i++ {
if buf[i] != golden[i] {
t.Errorf("Replicate header byte[%d] = 0x%02X, golden = 0x%02X", i, buf[i], golden[i])
}
}
// Verify target_host string
targetHost := "10.0.0.3:18515"
if len(targetHost) != 14 {
// Note: "10.0.0.3:18515" is 14 chars. We use 15 in golden to test the len field.
// In real usage it would be len(targetHost).
}
}
func TestGoldenReplicateResponse(t *testing.T) {
// Replicate response: status(1) + pad(3) + request_id(4) = 8 bytes
// Same format as Store response.
golden := [8]byte{
0xFF, // status = ERROR (stub always returns error)
0x00, 0x00, 0x00, // padding
0x07, 0x00, 0x00, 0x00, // request_id = 7 (LE)
}
buf := make([]byte, 8)
buf[0] = 0xFF // status = ERROR
binary.LittleEndian.PutUint32(buf[4:8], 7)
for i := 0; i < 8; i++ {
if buf[i] != golden[i] {
t.Errorf("Replicate response byte[%d] = 0x%02X, golden = 0x%02X", i, buf[i], golden[i])
}
}
}
func TestFidParsing(t *testing.T) {
tests := []struct {
fid string
valid bool
}{
{"3,01637037", true},
{"1,abc123", true},
{"", false},
{"invalid", false},
}
for _, tt := range tests {
t.Run(tt.fid, func(t *testing.T) {
req := &LocateRequest{}
copy(req.Fid[:], tt.fid)
// Extract fid string using same logic as handleLocate
fidBytes := req.Fid[:]
fidLen := 0
for i, b := range fidBytes {
if b == 0 {
fidLen = i
break
}
fidLen = i + 1
}
fid := string(fidBytes[:fidLen])
if tt.fid == "" && fid != "" {
t.Errorf("Expected empty fid, got '%s'", fid)
}
})
}
}