Browse Source
streaming reads for large files and multi-volume Rust cluster support
streaming reads for large files and multi-volume Rust cluster support
- Add StreamingBody (http_body::Body) for chunked reads of files >1MB, avoiding OOM by reading 64KB at a time via spawn_blocking + pread - Add NeedleStreamInfo and meta-only read path to avoid loading full needle body when streaming - Add RustMultiVolumeCluster test framework and MultiCluster interface so TestVolumeMoveHandlesInFlightWrites works with Rust volume servers - Remove TestVolumeMoveHandlesInFlightWrites from CI skip list (now passes) - All 117 Go integration tests + 8 Rust integration tests pass (100%)rust-volume-server
9 changed files with 720 additions and 24 deletions
-
4.github/workflows/rust-volume-server-tests.yml
-
20seaweed-volume/DEV_PLAN.md
-
204seaweed-volume/src/server/handlers.rs
-
83seaweed-volume/src/storage/needle/needle.rs
-
8seaweed-volume/src/storage/store.rs
-
96seaweed-volume/src/storage/volume.rs
-
25test/volume_server/framework/cluster_interface.go
-
300test/volume_server/framework/cluster_multi_rust.go
-
4test/volume_server/grpc/move_tail_timestamp_test.go
@ -0,0 +1,300 @@ |
|||||
|
package framework |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net" |
||||
|
"os" |
||||
|
"os/exec" |
||||
|
"path/filepath" |
||||
|
"strconv" |
||||
|
"sync" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/test/volume_server/matrix" |
||||
|
) |
||||
|
|
||||
|
// RustMultiVolumeCluster wraps a Go master + multiple Rust volume servers
|
||||
|
// for integration testing. It mirrors MultiVolumeCluster but uses the Rust
|
||||
|
// volume binary instead of the Go weed binary for volume servers.
|
||||
|
type RustMultiVolumeCluster struct { |
||||
|
testingTB testing.TB |
||||
|
profile matrix.Profile |
||||
|
|
||||
|
weedBinary string // Go weed binary (for the master)
|
||||
|
rustVolumeBinary string // Rust volume binary
|
||||
|
|
||||
|
baseDir string |
||||
|
configDir string |
||||
|
logsDir string |
||||
|
keepLogs bool |
||||
|
volumeServerCount int |
||||
|
|
||||
|
masterPort int |
||||
|
masterGrpcPort int |
||||
|
|
||||
|
volumePorts []int |
||||
|
volumeGrpcPorts []int |
||||
|
volumePubPorts []int |
||||
|
|
||||
|
masterCmd *exec.Cmd |
||||
|
volumeCmds []*exec.Cmd |
||||
|
|
||||
|
cleanupOnce sync.Once |
||||
|
} |
||||
|
|
||||
|
// StartRustMultiVolumeCluster starts a cluster with a Go master and the
|
||||
|
// specified number of Rust volume servers.
|
||||
|
func StartRustMultiVolumeCluster(t testing.TB, profile matrix.Profile, serverCount int) *RustMultiVolumeCluster { |
||||
|
t.Helper() |
||||
|
|
||||
|
if serverCount < 1 { |
||||
|
t.Fatalf("serverCount must be at least 1, got %d", serverCount) |
||||
|
} |
||||
|
|
||||
|
weedBinary, err := FindOrBuildWeedBinary() |
||||
|
if err != nil { |
||||
|
t.Fatalf("resolve weed binary: %v", err) |
||||
|
} |
||||
|
|
||||
|
rustBinary, err := FindOrBuildRustBinary() |
||||
|
if err != nil { |
||||
|
t.Fatalf("resolve rust volume binary: %v", err) |
||||
|
} |
||||
|
|
||||
|
baseDir, keepLogs, err := newWorkDir() |
||||
|
if err != nil { |
||||
|
t.Fatalf("create temp test directory: %v", err) |
||||
|
} |
||||
|
|
||||
|
configDir := filepath.Join(baseDir, "config") |
||||
|
logsDir := filepath.Join(baseDir, "logs") |
||||
|
masterDataDir := filepath.Join(baseDir, "master") |
||||
|
|
||||
|
// Create directories for master and all volume servers
|
||||
|
dirs := []string{configDir, logsDir, masterDataDir} |
||||
|
for i := 0; i < serverCount; i++ { |
||||
|
dirs = append(dirs, filepath.Join(baseDir, fmt.Sprintf("volume%d", i))) |
||||
|
} |
||||
|
for _, dir := range dirs { |
||||
|
if mkErr := os.MkdirAll(dir, 0o755); mkErr != nil { |
||||
|
t.Fatalf("create %s: %v", dir, mkErr) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if err = writeSecurityConfig(configDir, profile); err != nil { |
||||
|
t.Fatalf("write security config: %v", err) |
||||
|
} |
||||
|
|
||||
|
masterPort, masterGrpcPort, err := allocateMasterPortPair() |
||||
|
if err != nil { |
||||
|
t.Fatalf("allocate master port pair: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Allocate ports for all volume servers (3 ports per server: admin, grpc, public)
|
||||
|
// If SplitPublicPort is true, we need an additional port per server
|
||||
|
portsPerServer := 3 |
||||
|
if profile.SplitPublicPort { |
||||
|
portsPerServer = 4 |
||||
|
} |
||||
|
totalPorts := serverCount * portsPerServer |
||||
|
ports, err := allocatePorts(totalPorts) |
||||
|
if err != nil { |
||||
|
t.Fatalf("allocate volume ports: %v", err) |
||||
|
} |
||||
|
|
||||
|
c := &RustMultiVolumeCluster{ |
||||
|
testingTB: t, |
||||
|
profile: profile, |
||||
|
weedBinary: weedBinary, |
||||
|
rustVolumeBinary: rustBinary, |
||||
|
baseDir: baseDir, |
||||
|
configDir: configDir, |
||||
|
logsDir: logsDir, |
||||
|
keepLogs: keepLogs, |
||||
|
volumeServerCount: serverCount, |
||||
|
masterPort: masterPort, |
||||
|
masterGrpcPort: masterGrpcPort, |
||||
|
volumePorts: make([]int, serverCount), |
||||
|
volumeGrpcPorts: make([]int, serverCount), |
||||
|
volumePubPorts: make([]int, serverCount), |
||||
|
volumeCmds: make([]*exec.Cmd, serverCount), |
||||
|
} |
||||
|
|
||||
|
// Assign ports to each volume server
|
||||
|
for i := 0; i < serverCount; i++ { |
||||
|
baseIdx := i * portsPerServer |
||||
|
c.volumePorts[i] = ports[baseIdx] |
||||
|
c.volumeGrpcPorts[i] = ports[baseIdx+1] |
||||
|
|
||||
|
// Assign public port, using baseIdx+3 if SplitPublicPort, else baseIdx+2
|
||||
|
pubPortIdx := baseIdx + 2 |
||||
|
if profile.SplitPublicPort { |
||||
|
pubPortIdx = baseIdx + 3 |
||||
|
} |
||||
|
c.volumePubPorts[i] = ports[pubPortIdx] |
||||
|
} |
||||
|
|
||||
|
// Start master (Go)
|
||||
|
if err = c.startMaster(masterDataDir); err != nil { |
||||
|
c.Stop() |
||||
|
t.Fatalf("start master: %v", err) |
||||
|
} |
||||
|
helper := &Cluster{logsDir: logsDir} |
||||
|
if err = helper.waitForHTTP(c.MasterURL() + "/dir/status"); err != nil { |
||||
|
masterLog := helper.tailLog("master.log") |
||||
|
c.Stop() |
||||
|
t.Fatalf("wait for master readiness: %v\nmaster log tail:\n%s", err, masterLog) |
||||
|
} |
||||
|
|
||||
|
// Start all Rust volume servers
|
||||
|
for i := 0; i < serverCount; i++ { |
||||
|
volumeDataDir := filepath.Join(baseDir, fmt.Sprintf("volume%d", i)) |
||||
|
if err = c.startRustVolume(i, volumeDataDir); err != nil { |
||||
|
volumeLog := fmt.Sprintf("volume%d.log", i) |
||||
|
c.Stop() |
||||
|
t.Fatalf("start rust volume server %d: %v\nvolume log tail:\n%s", i, err, helper.tailLog(volumeLog)) |
||||
|
} |
||||
|
if err = helper.waitForHTTP(c.VolumeAdminURL(i) + "/healthz"); err != nil { |
||||
|
volumeLog := fmt.Sprintf("volume%d.log", i) |
||||
|
c.Stop() |
||||
|
t.Fatalf("wait for rust volume server %d readiness: %v\nvolume log tail:\n%s", i, err, helper.tailLog(volumeLog)) |
||||
|
} |
||||
|
if err = helper.waitForTCP(c.VolumeGRPCAddress(i)); err != nil { |
||||
|
volumeLog := fmt.Sprintf("volume%d.log", i) |
||||
|
c.Stop() |
||||
|
t.Fatalf("wait for rust volume server %d grpc readiness: %v\nvolume log tail:\n%s", i, err, helper.tailLog(volumeLog)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
t.Cleanup(func() { |
||||
|
c.Stop() |
||||
|
}) |
||||
|
|
||||
|
return c |
||||
|
} |
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) Stop() { |
||||
|
if c == nil { |
||||
|
return |
||||
|
} |
||||
|
c.cleanupOnce.Do(func() { |
||||
|
// Stop volume servers in reverse order
|
||||
|
for i := len(c.volumeCmds) - 1; i >= 0; i-- { |
||||
|
stopProcess(c.volumeCmds[i]) |
||||
|
} |
||||
|
stopProcess(c.masterCmd) |
||||
|
if !c.keepLogs && !c.testingTB.Failed() { |
||||
|
_ = os.RemoveAll(c.baseDir) |
||||
|
} else if c.baseDir != "" { |
||||
|
c.testingTB.Logf("rust multi volume server integration logs kept at %s", c.baseDir) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) startMaster(dataDir string) error { |
||||
|
logFile, err := os.Create(filepath.Join(c.logsDir, "master.log")) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
args := []string{ |
||||
|
"-config_dir=" + c.configDir, |
||||
|
"master", |
||||
|
"-ip=127.0.0.1", |
||||
|
"-port=" + strconv.Itoa(c.masterPort), |
||||
|
"-port.grpc=" + strconv.Itoa(c.masterGrpcPort), |
||||
|
"-mdir=" + dataDir, |
||||
|
"-peers=none", |
||||
|
"-volumeSizeLimitMB=" + strconv.Itoa(testVolumeSizeLimitMB), |
||||
|
"-defaultReplication=000", |
||||
|
} |
||||
|
|
||||
|
c.masterCmd = exec.Command(c.weedBinary, args...) |
||||
|
c.masterCmd.Dir = c.baseDir |
||||
|
c.masterCmd.Stdout = logFile |
||||
|
c.masterCmd.Stderr = logFile |
||||
|
return c.masterCmd.Start() |
||||
|
} |
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) startRustVolume(index int, dataDir string) error { |
||||
|
logName := fmt.Sprintf("volume%d.log", index) |
||||
|
logFile, err := os.Create(filepath.Join(c.logsDir, logName)) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
args := []string{ |
||||
|
"--port", strconv.Itoa(c.volumePorts[index]), |
||||
|
"--port.grpc", strconv.Itoa(c.volumeGrpcPorts[index]), |
||||
|
"--port.public", strconv.Itoa(c.volumePubPorts[index]), |
||||
|
"--ip", "127.0.0.1", |
||||
|
"--ip.bind", "127.0.0.1", |
||||
|
"--dir", dataDir, |
||||
|
"--max", "16", |
||||
|
"--master", "127.0.0.1:" + strconv.Itoa(c.masterPort), |
||||
|
"--securityFile", filepath.Join(c.configDir, "security.toml"), |
||||
|
"--concurrentUploadLimitMB", strconv.Itoa(c.profile.ConcurrentUploadLimitMB), |
||||
|
"--concurrentDownloadLimitMB", strconv.Itoa(c.profile.ConcurrentDownloadLimitMB), |
||||
|
"--preStopSeconds", "0", |
||||
|
} |
||||
|
if c.profile.InflightUploadTimeout > 0 { |
||||
|
args = append(args, "--inflightUploadDataTimeout", c.profile.InflightUploadTimeout.String()) |
||||
|
} |
||||
|
if c.profile.InflightDownloadTimeout > 0 { |
||||
|
args = append(args, "--inflightDownloadDataTimeout", c.profile.InflightDownloadTimeout.String()) |
||||
|
} |
||||
|
|
||||
|
cmd := exec.Command(c.rustVolumeBinary, args...) |
||||
|
cmd.Dir = c.baseDir |
||||
|
cmd.Stdout = logFile |
||||
|
cmd.Stderr = logFile |
||||
|
|
||||
|
if err = cmd.Start(); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
c.volumeCmds[index] = cmd |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// --- accessor methods (mirror MultiVolumeCluster) ---
|
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) MasterAddress() string { |
||||
|
return net.JoinHostPort("127.0.0.1", strconv.Itoa(c.masterPort)) |
||||
|
} |
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) MasterURL() string { |
||||
|
return "http://" + c.MasterAddress() |
||||
|
} |
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) VolumeAdminAddress(index int) string { |
||||
|
if index < 0 || index >= len(c.volumePorts) { |
||||
|
return "" |
||||
|
} |
||||
|
return net.JoinHostPort("127.0.0.1", strconv.Itoa(c.volumePorts[index])) |
||||
|
} |
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) VolumePublicAddress(index int) string { |
||||
|
if index < 0 || index >= len(c.volumePubPorts) { |
||||
|
return "" |
||||
|
} |
||||
|
return net.JoinHostPort("127.0.0.1", strconv.Itoa(c.volumePubPorts[index])) |
||||
|
} |
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) VolumeGRPCAddress(index int) string { |
||||
|
if index < 0 || index >= len(c.volumeGrpcPorts) { |
||||
|
return "" |
||||
|
} |
||||
|
return net.JoinHostPort("127.0.0.1", strconv.Itoa(c.volumeGrpcPorts[index])) |
||||
|
} |
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) VolumeAdminURL(index int) string { |
||||
|
return "http://" + c.VolumeAdminAddress(index) |
||||
|
} |
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) VolumePublicURL(index int) string { |
||||
|
return "http://" + c.VolumePublicAddress(index) |
||||
|
} |
||||
|
|
||||
|
func (c *RustMultiVolumeCluster) BaseDir() string { |
||||
|
return c.baseDir |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue