Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
568 lines
20 KiB
Go
568 lines
20 KiB
Go
package volume_server_grpc_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/test/volume_server/framework"
|
|
"github.com/seaweedfs/seaweedfs/test/volume_server/matrix"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
)
|
|
|
|
// deleteAndWaitForTombstone issues an HTTP DELETE for the given fid on
|
|
// volumeURL, asserts a successful response, then polls with GET until
|
|
// the file returns 404 (tombstone visible) or the timeout elapses.
|
|
func deleteAndWaitForTombstone(t *testing.T, httpClient *http.Client, volumeURL, fid string) {
|
|
t.Helper()
|
|
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/%s", volumeURL, fid), nil)
|
|
if err != nil {
|
|
t.Fatalf("build delete request for %s: %v", fid, err)
|
|
}
|
|
resp := framework.DoRequest(t, httpClient, req)
|
|
framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("delete %s: expected 200 or 202, got %d", fid, resp.StatusCode)
|
|
}
|
|
// Poll until GET returns 404 (tombstone flushed to disk).
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
getResp := framework.ReadBytes(t, httpClient, volumeURL, fid)
|
|
status := getResp.StatusCode
|
|
framework.ReadAllAndClose(t, getResp)
|
|
if status == http.StatusNotFound {
|
|
return
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
t.Fatalf("delete %s: tombstone not visible after 5s", fid)
|
|
}
|
|
|
|
// TestMixedBalanceCopyGoToRust verifies that VolumeCopy works from a Go volume
|
|
// server to a Rust volume server. This is the core operation behind volume
|
|
// balancing in a mixed Go+Rust cluster.
|
|
func TestMixedBalanceCopyGoToRust(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
cluster := framework.StartMixedVolumeCluster(t, matrix.P1(), 1, 1)
|
|
|
|
// server 0 = Go, server 1 = Rust
|
|
conn0, goClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(0))
|
|
defer conn0.Close()
|
|
conn1, rustClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(1))
|
|
defer conn1.Close()
|
|
|
|
httpClient := framework.NewHTTPClient()
|
|
|
|
const volumeID = uint32(50)
|
|
|
|
// Allocate volume on Go server and upload test data
|
|
framework.AllocateVolume(t, goClient, volumeID, "")
|
|
|
|
testFiles := []struct {
|
|
key uint64
|
|
cookie uint32
|
|
data []byte
|
|
}{
|
|
{1, 0xAABBCCDD, []byte("hello from Go server")},
|
|
{2, 0x11223344, []byte("second file for balance test")},
|
|
{3, 0xDEADBEEF, make([]byte, 4096)}, // larger file
|
|
}
|
|
|
|
for _, f := range testFiles {
|
|
fid := framework.NewFileID(volumeID, f.key, f.cookie)
|
|
resp := framework.UploadBytes(t, httpClient, cluster.VolumeAdminURL(0), fid, f.data)
|
|
body := framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("upload %s: expected 201, got %d: %s", fid, resp.StatusCode, body)
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Read source volume status before copy
|
|
sourceStatus, err := goClient.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReadVolumeFileStatus on Go server: %v", err)
|
|
}
|
|
t.Logf("Source: dat=%d idx=%d files=%d version=%d",
|
|
sourceStatus.GetDatFileSize(), sourceStatus.GetIdxFileSize(),
|
|
sourceStatus.GetFileCount(), sourceStatus.GetVersion())
|
|
|
|
// Copy volume from Go (server 0) to Rust (server 1)
|
|
copyStream, err := rustClient.VolumeCopy(ctx, &volume_server_pb.VolumeCopyRequest{
|
|
VolumeId: volumeID,
|
|
SourceDataNode: cluster.VolumeServerAddress(0),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeCopy call failed: %v", err)
|
|
}
|
|
|
|
var lastAppendAtNs uint64
|
|
for {
|
|
resp, recvErr := copyStream.Recv()
|
|
if recvErr != nil {
|
|
if recvErr != io.EOF {
|
|
t.Fatalf("VolumeCopy recv error: %v", recvErr)
|
|
}
|
|
break
|
|
}
|
|
if resp.GetLastAppendAtNs() != 0 {
|
|
lastAppendAtNs = resp.GetLastAppendAtNs()
|
|
}
|
|
}
|
|
t.Logf("VolumeCopy completed, lastAppendAtNs=%d", lastAppendAtNs)
|
|
|
|
// Verify: read volume status from Rust server
|
|
targetStatus, err := rustClient.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReadVolumeFileStatus on Rust server: %v", err)
|
|
}
|
|
t.Logf("Target: dat=%d idx=%d files=%d version=%d",
|
|
targetStatus.GetDatFileSize(), targetStatus.GetIdxFileSize(),
|
|
targetStatus.GetFileCount(), targetStatus.GetVersion())
|
|
|
|
if sourceStatus.GetDatFileSize() != targetStatus.GetDatFileSize() {
|
|
t.Fatalf("dat file size mismatch: source=%d target=%d",
|
|
sourceStatus.GetDatFileSize(), targetStatus.GetDatFileSize())
|
|
}
|
|
if sourceStatus.GetIdxFileSize() != targetStatus.GetIdxFileSize() {
|
|
t.Fatalf("idx file size mismatch: source=%d target=%d",
|
|
sourceStatus.GetIdxFileSize(), targetStatus.GetIdxFileSize())
|
|
}
|
|
if sourceStatus.GetFileCount() != targetStatus.GetFileCount() {
|
|
t.Fatalf("file count mismatch: source=%d target=%d",
|
|
sourceStatus.GetFileCount(), targetStatus.GetFileCount())
|
|
}
|
|
|
|
// Verify data can be read from Rust server
|
|
for _, f := range testFiles {
|
|
fid := framework.NewFileID(volumeID, f.key, f.cookie)
|
|
resp := framework.ReadBytes(t, httpClient, cluster.VolumeAdminURL(1), fid)
|
|
body := framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("read %s from Rust server: expected 200, got %d", fid, resp.StatusCode)
|
|
}
|
|
if !bytes.Equal(body, f.data) {
|
|
t.Fatalf("read %s from Rust server: content mismatch (got %d bytes, want %d)", fid, len(body), len(f.data))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMixedBalanceCopyRustToGo verifies that VolumeCopy works from a Rust
|
|
// volume server to a Go volume server.
|
|
func TestMixedBalanceCopyRustToGo(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
cluster := framework.StartMixedVolumeCluster(t, matrix.P1(), 1, 1)
|
|
|
|
conn0, goClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(0))
|
|
defer conn0.Close()
|
|
conn1, rustClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(1))
|
|
defer conn1.Close()
|
|
|
|
httpClient := framework.NewHTTPClient()
|
|
|
|
const volumeID = uint32(51)
|
|
|
|
// Allocate volume on Rust server and upload test data
|
|
framework.AllocateVolume(t, rustClient, volumeID, "")
|
|
|
|
testFiles := []struct {
|
|
key uint64
|
|
cookie uint32
|
|
data []byte
|
|
}{
|
|
{1, 0xAABBCCDD, []byte("hello from Rust server")},
|
|
{2, 0x11223344, []byte("second file for reverse balance")},
|
|
}
|
|
|
|
for _, f := range testFiles {
|
|
fid := framework.NewFileID(volumeID, f.key, f.cookie)
|
|
resp := framework.UploadBytes(t, httpClient, cluster.VolumeAdminURL(1), fid, f.data)
|
|
body := framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("upload %s: expected 201, got %d: %s", fid, resp.StatusCode, body)
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
sourceStatus, err := rustClient.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReadVolumeFileStatus on Rust server: %v", err)
|
|
}
|
|
|
|
// Copy volume from Rust (server 1) to Go (server 0)
|
|
copyStream, err := goClient.VolumeCopy(ctx, &volume_server_pb.VolumeCopyRequest{
|
|
VolumeId: volumeID,
|
|
SourceDataNode: cluster.VolumeServerAddress(1),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeCopy call failed: %v", err)
|
|
}
|
|
for {
|
|
_, recvErr := copyStream.Recv()
|
|
if recvErr != nil {
|
|
if recvErr != io.EOF {
|
|
t.Fatalf("VolumeCopy recv error: %v", recvErr)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
targetStatus, err := goClient.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReadVolumeFileStatus on Go server: %v", err)
|
|
}
|
|
|
|
if sourceStatus.GetDatFileSize() != targetStatus.GetDatFileSize() {
|
|
t.Fatalf("dat file size mismatch: source=%d target=%d",
|
|
sourceStatus.GetDatFileSize(), targetStatus.GetDatFileSize())
|
|
}
|
|
if sourceStatus.GetIdxFileSize() != targetStatus.GetIdxFileSize() {
|
|
t.Fatalf("idx file size mismatch: source=%d target=%d",
|
|
sourceStatus.GetIdxFileSize(), targetStatus.GetIdxFileSize())
|
|
}
|
|
if sourceStatus.GetFileCount() != targetStatus.GetFileCount() {
|
|
t.Fatalf("file count mismatch: source=%d target=%d",
|
|
sourceStatus.GetFileCount(), targetStatus.GetFileCount())
|
|
}
|
|
|
|
// Verify data can be read from Go server
|
|
for _, f := range testFiles {
|
|
fid := framework.NewFileID(volumeID, f.key, f.cookie)
|
|
resp := framework.ReadBytes(t, httpClient, cluster.VolumeAdminURL(0), fid)
|
|
body := framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("read %s from Go server: expected 200, got %d", fid, resp.StatusCode)
|
|
}
|
|
if !bytes.Equal(body, f.data) {
|
|
t.Fatalf("read %s from Go server: content mismatch (got %d bytes, want %d)", fid, len(body), len(f.data))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMixedBalanceCopyWithDeletes verifies that VolumeCopy correctly handles
|
|
// volumes that have both active and deleted needles.
|
|
func TestMixedBalanceCopyWithDeletes(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
cluster := framework.StartMixedVolumeCluster(t, matrix.P1(), 1, 1)
|
|
|
|
conn0, goClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(0))
|
|
defer conn0.Close()
|
|
conn1, rustClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(1))
|
|
defer conn1.Close()
|
|
|
|
httpClient := framework.NewHTTPClient()
|
|
|
|
const volumeID = uint32(52)
|
|
framework.AllocateVolume(t, goClient, volumeID, "")
|
|
|
|
// Upload files
|
|
for i := uint64(1); i <= 5; i++ {
|
|
fid := framework.NewFileID(volumeID, i, 0x12340000+uint32(i))
|
|
resp := framework.UploadBytes(t, httpClient, cluster.VolumeAdminURL(0), fid, []byte(fmt.Sprintf("file-%d", i)))
|
|
framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("upload file %d: expected 201, got %d", i, resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// Delete some files and wait for tombstones to be visible
|
|
for _, key := range []uint64{2, 4} {
|
|
fid := framework.NewFileID(volumeID, key, 0x12340000+uint32(key))
|
|
deleteAndWaitForTombstone(t, httpClient, cluster.VolumeAdminURL(0), fid)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
sourceStatus, err := goClient.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReadVolumeFileStatus: %v", err)
|
|
}
|
|
t.Logf("Source after deletes: dat=%d idx=%d files=%d",
|
|
sourceStatus.GetDatFileSize(), sourceStatus.GetIdxFileSize(),
|
|
sourceStatus.GetFileCount())
|
|
|
|
// Copy volume from Go to Rust
|
|
copyStream, err := rustClient.VolumeCopy(ctx, &volume_server_pb.VolumeCopyRequest{
|
|
VolumeId: volumeID,
|
|
SourceDataNode: cluster.VolumeServerAddress(0),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeCopy: %v", err)
|
|
}
|
|
var lastAppendAtNs uint64
|
|
for {
|
|
resp, recvErr := copyStream.Recv()
|
|
if recvErr != nil {
|
|
if recvErr != io.EOF {
|
|
t.Fatalf("VolumeCopy recv error: %v", recvErr)
|
|
}
|
|
break
|
|
}
|
|
if resp.GetLastAppendAtNs() != 0 {
|
|
lastAppendAtNs = resp.GetLastAppendAtNs()
|
|
}
|
|
}
|
|
if lastAppendAtNs == 0 {
|
|
t.Fatalf("VolumeCopy did not return a lastAppendAtNs timestamp")
|
|
}
|
|
t.Logf("VolumeCopy completed, lastAppendAtNs=%d", lastAppendAtNs)
|
|
|
|
targetStatusAfterCopy, err := rustClient.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReadVolumeFileStatus on Rust: %v", err)
|
|
}
|
|
t.Logf("Target after copy: dat=%d idx=%d files=%d",
|
|
targetStatusAfterCopy.GetDatFileSize(), targetStatusAfterCopy.GetIdxFileSize(),
|
|
targetStatusAfterCopy.GetFileCount())
|
|
|
|
if sourceStatus.GetDatFileSize() != targetStatusAfterCopy.GetDatFileSize() {
|
|
t.Fatalf("dat file size mismatch: source=%d target=%d",
|
|
sourceStatus.GetDatFileSize(), targetStatusAfterCopy.GetDatFileSize())
|
|
}
|
|
if sourceStatus.GetIdxFileSize() != targetStatusAfterCopy.GetIdxFileSize() {
|
|
t.Fatalf("idx file size mismatch: source=%d target=%d",
|
|
sourceStatus.GetIdxFileSize(), targetStatusAfterCopy.GetIdxFileSize())
|
|
}
|
|
|
|
// Tail from the copy checkpoint — source is unchanged (deletes happened
|
|
// before copy), so tailing should not append any data.
|
|
_, err = rustClient.VolumeTailReceiver(ctx, &volume_server_pb.VolumeTailReceiverRequest{
|
|
VolumeId: volumeID,
|
|
SinceNs: lastAppendAtNs,
|
|
IdleTimeoutSeconds: 3,
|
|
SourceVolumeServer: cluster.VolumeServerAddress(0),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeTailReceiver: %v", err)
|
|
}
|
|
|
|
targetStatusAfterTail, err := rustClient.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReadVolumeFileStatus after tail: %v", err)
|
|
}
|
|
if targetStatusAfterTail.GetDatFileSize() != targetStatusAfterCopy.GetDatFileSize() {
|
|
t.Fatalf("dat grew after tail: before=%d after=%d",
|
|
targetStatusAfterCopy.GetDatFileSize(), targetStatusAfterTail.GetDatFileSize())
|
|
}
|
|
if targetStatusAfterTail.GetIdxFileSize() != targetStatusAfterCopy.GetIdxFileSize() {
|
|
t.Fatalf("idx grew after tail: before=%d after=%d",
|
|
targetStatusAfterCopy.GetIdxFileSize(), targetStatusAfterTail.GetIdxFileSize())
|
|
}
|
|
|
|
// Verify surviving files are readable from Rust
|
|
for _, key := range []uint64{1, 3, 5} {
|
|
fid := framework.NewFileID(volumeID, key, 0x12340000+uint32(key))
|
|
resp := framework.ReadBytes(t, httpClient, cluster.VolumeAdminURL(1), fid)
|
|
body := framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("read surviving file %d from Rust: expected 200, got %d", key, resp.StatusCode)
|
|
}
|
|
expected := fmt.Sprintf("file-%d", key)
|
|
if string(body) != expected {
|
|
t.Fatalf("data mismatch for file %d: got %q want %q", key, body, expected)
|
|
}
|
|
}
|
|
|
|
// Verify deleted files return 404
|
|
for _, key := range []uint64{2, 4} {
|
|
fid := framework.NewFileID(volumeID, key, 0x12340000+uint32(key))
|
|
resp := framework.ReadBytes(t, httpClient, cluster.VolumeAdminURL(1), fid)
|
|
framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("read deleted file %d from Rust: expected 404, got %d", key, resp.StatusCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMixedBalanceFullMoveGoToRust exercises the complete volume balance move
|
|
// flow: mark readonly → copy → tail → verify sizes → delete source.
|
|
// This mirrors the steps in balance_task.go Execute().
|
|
func TestMixedBalanceFullMoveGoToRust(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
cluster := framework.StartMixedVolumeCluster(t, matrix.P1(), 1, 1)
|
|
|
|
conn0, goClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(0))
|
|
defer conn0.Close()
|
|
conn1, rustClient := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress(1))
|
|
defer conn1.Close()
|
|
|
|
httpClient := framework.NewHTTPClient()
|
|
|
|
const volumeID = uint32(53)
|
|
framework.AllocateVolume(t, goClient, volumeID, "")
|
|
|
|
// Upload test data
|
|
for i := uint64(1); i <= 5; i++ {
|
|
fid := framework.NewFileID(volumeID, i, 0xABCD0000+uint32(i))
|
|
resp := framework.UploadBytes(t, httpClient, cluster.VolumeAdminURL(0), fid, []byte(fmt.Sprintf("balance-move-file-%d", i)))
|
|
framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("upload file %d: expected 201, got %d", i, resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
// Step 1: Copy volume to Rust server (source stays writable so we can
|
|
// delete after copy to exercise the tail tombstone path)
|
|
copyStream, err := rustClient.VolumeCopy(ctx, &volume_server_pb.VolumeCopyRequest{
|
|
VolumeId: volumeID,
|
|
SourceDataNode: cluster.VolumeServerAddress(0),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeCopy: %v", err)
|
|
}
|
|
var lastAppendAtNs uint64
|
|
for {
|
|
resp, recvErr := copyStream.Recv()
|
|
if recvErr != nil {
|
|
if recvErr != io.EOF {
|
|
t.Fatalf("VolumeCopy recv: %v", recvErr)
|
|
}
|
|
break
|
|
}
|
|
if resp.GetLastAppendAtNs() != 0 {
|
|
lastAppendAtNs = resp.GetLastAppendAtNs()
|
|
}
|
|
}
|
|
t.Logf("Copy done, lastAppendAtNs=%d", lastAppendAtNs)
|
|
|
|
// Step 2: Delete file 3 on the source AFTER copy. This creates a
|
|
// tombstone needle that the tail step must propagate to the Rust server.
|
|
deleteAndWaitForTombstone(t, httpClient, cluster.VolumeAdminURL(0),
|
|
framework.NewFileID(volumeID, 3, 0xABCD0003))
|
|
|
|
// Step 3: Mark source readonly so no further writes arrive during tail
|
|
_, err = goClient.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("mark readonly: %v", err)
|
|
}
|
|
|
|
// Read source status (the reference for post-tail verification)
|
|
sourceStatus, err := goClient.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("read source status: %v", err)
|
|
}
|
|
t.Logf("Source: dat=%d idx=%d files=%d",
|
|
sourceStatus.GetDatFileSize(), sourceStatus.GetIdxFileSize(),
|
|
sourceStatus.GetFileCount())
|
|
|
|
// Step 4: Tail for updates — this must pick up the delete tombstone
|
|
_, err = rustClient.VolumeTailReceiver(ctx, &volume_server_pb.VolumeTailReceiverRequest{
|
|
VolumeId: volumeID,
|
|
SinceNs: lastAppendAtNs,
|
|
IdleTimeoutSeconds: 5,
|
|
SourceVolumeServer: cluster.VolumeServerAddress(0),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeTailReceiver: %v", err)
|
|
}
|
|
|
|
// Step 5: Verify dat/idx sizes match after tail.
|
|
// We compare file sizes (byte-level correctness) rather than file_count
|
|
// because the tail writes the tombstone via the write path (which
|
|
// increments file_count) while the source used the delete path (which
|
|
// only increments deletion_count). This is consistent with Go behavior.
|
|
targetStatus, err := rustClient.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("read target status: %v", err)
|
|
}
|
|
t.Logf("Target: dat=%d idx=%d files=%d",
|
|
targetStatus.GetDatFileSize(), targetStatus.GetIdxFileSize(),
|
|
targetStatus.GetFileCount())
|
|
|
|
if sourceStatus.GetDatFileSize() != targetStatus.GetDatFileSize() {
|
|
t.Fatalf("dat size mismatch after tail: source=%d target=%d",
|
|
sourceStatus.GetDatFileSize(), targetStatus.GetDatFileSize())
|
|
}
|
|
if sourceStatus.GetIdxFileSize() != targetStatus.GetIdxFileSize() {
|
|
t.Fatalf("idx size mismatch after tail: source=%d target=%d",
|
|
sourceStatus.GetIdxFileSize(), targetStatus.GetIdxFileSize())
|
|
}
|
|
|
|
// Step 6: Delete volume from source
|
|
_, err = goClient.VolumeDelete(ctx, &volume_server_pb.VolumeDeleteRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("delete source volume: %v", err)
|
|
}
|
|
|
|
// Verify source volume is gone
|
|
_, err = goClient.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("expected error reading deleted source volume, got nil")
|
|
}
|
|
t.Logf("Source volume deleted successfully (error as expected: %v)", err)
|
|
|
|
// Verify all surviving data is readable from Rust (the new home)
|
|
for _, key := range []uint64{1, 2, 4, 5} {
|
|
fid := framework.NewFileID(volumeID, key, 0xABCD0000+uint32(key))
|
|
resp := framework.ReadBytes(t, httpClient, cluster.VolumeAdminURL(1), fid)
|
|
body := framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("read file %d from Rust after move: expected 200, got %d", key, resp.StatusCode)
|
|
}
|
|
expected := fmt.Sprintf("balance-move-file-%d", key)
|
|
if string(body) != expected {
|
|
t.Fatalf("data mismatch for file %d after move: got %q want %q", key, body, expected)
|
|
}
|
|
}
|
|
|
|
// Verify deleted file is 404 on Rust (tombstone propagated via tail)
|
|
{
|
|
fid := framework.NewFileID(volumeID, 3, 0xABCD0003)
|
|
resp := framework.ReadBytes(t, httpClient, cluster.VolumeAdminURL(1), fid)
|
|
framework.ReadAllAndClose(t, resp)
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("deleted file 3 on Rust: expected 404, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
t.Logf("Full balance move completed: volume %d moved from Go to Rust, source purged", volumeID)
|
|
}
|