package volume_server_grpc_test import ( "context" "fmt" "os" "path/filepath" "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" ) // Sends EC shards with disk_id={1, 2, 0} and verifies each lands on the // requested disk (0 → auto-select → disk 0 for a fresh volume). func TestReceiveFileEcShardHonorsDiskID(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") } const dataDirCount = 3 clusterHarness := framework.StartSingleVolumeClusterWithDataDirs(t, matrix.P1(), dataDirCount) conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress()) defer conn.Close() dataDirs := clusterHarness.VolumeDataDirs() if len(dataDirs) != dataDirCount { t.Fatalf("expected %d data dirs, got %d: %v", dataDirCount, len(dataDirs), dataDirs) } ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() const volumeID = uint32(9184) const collection = "ec-disk-placement" // DiskId=0 triggers auto-select. Since this test never calls // VolumeEcShardsMount, loc.FindEcVolume returns false on every disk, so // the "already holds shards" preference is skipped and auto-select falls // through to the first HDD (dir 0). If shards get mounted between uploads // in a future revision, the expected dir for DiskId=0 becomes the first // disk that already holds a shard. shards := []struct { shardID uint32 requestedDisk uint32 expectedDirIdx int }{ {shardID: 0, requestedDisk: 1, expectedDirIdx: 1}, {shardID: 5, requestedDisk: 2, expectedDirIdx: 2}, {shardID: 13, requestedDisk: 0, expectedDirIdx: 0}, } shardExt := func(id uint32) string { return fmt.Sprintf(".ec%02d", id) } for _, s := range shards { payload := []byte(fmt.Sprintf("ec-shard-payload-%02d", s.shardID)) stream, err := grpcClient.ReceiveFile(ctx) if err != nil { t.Fatalf("ReceiveFile stream create for shard %d: %v", s.shardID, err) } if err = stream.Send(&volume_server_pb.ReceiveFileRequest{ Data: &volume_server_pb.ReceiveFileRequest_Info{ Info: &volume_server_pb.ReceiveFileInfo{ VolumeId: volumeID, Ext: shardExt(s.shardID), Collection: collection, IsEcVolume: true, ShardId: s.shardID, FileSize: uint64(len(payload)), DiskId: s.requestedDisk, }, }, }); err != nil { t.Fatalf("ReceiveFile send info for shard %d: %v", s.shardID, err) } if err = stream.Send(&volume_server_pb.ReceiveFileRequest{ Data: &volume_server_pb.ReceiveFileRequest_FileContent{FileContent: payload}, }); err != nil { t.Fatalf("ReceiveFile send content for shard %d: %v", s.shardID, err) } resp, err := stream.CloseAndRecv() if err != nil { t.Fatalf("ReceiveFile close for shard %d: %v", s.shardID, err) } if resp.GetError() != "" { t.Fatalf("ReceiveFile shard %d error response: %s", s.shardID, resp.GetError()) } if resp.GetBytesWritten() != uint64(len(payload)) { t.Fatalf("ReceiveFile shard %d bytes_written mismatch: got %d want %d", s.shardID, resp.GetBytesWritten(), len(payload)) } } // Scan every data dir and record which shard files live where. perDirShards := make(map[int][]uint32) for dirIdx, dir := range dataDirs { for _, s := range shards { shardPath := filepath.Join(dir, fmt.Sprintf("%s_%d%s", collection, volumeID, shardExt(s.shardID))) if _, err := os.Stat(shardPath); err == nil { perDirShards[dirIdx] = append(perDirShards[dirIdx], s.shardID) } else if !os.IsNotExist(err) { t.Fatalf("unexpected stat error for %s: %v", shardPath, err) } } } for _, s := range shards { found := false for _, got := range perDirShards[s.expectedDirIdx] { if got == s.shardID { found = true break } } if !found { t.Fatalf("shard %d (requestedDisk=%d) expected on dir[%d], full layout: %v", s.shardID, s.requestedDisk, s.expectedDirIdx, perDirShards) } for dirIdx, ids := range perDirShards { if dirIdx == s.expectedDirIdx { continue } for _, id := range ids { if id == s.shardID { t.Fatalf("shard %d leaked onto dir[%d]: full layout: %v", s.shardID, dirIdx, perDirShards) } } } } t.Logf("disk_id honored: shard placement = %v", perDirShards) } func TestReceiveFileEcShardRejectsInvalidDiskID(t *testing.T) { if testing.Short() { t.Skip("skipping integration test in short mode") } const dataDirCount = 2 clusterHarness := framework.StartSingleVolumeClusterWithDataDirs(t, matrix.P1(), dataDirCount) conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress()) defer conn.Close() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() const volumeID = uint32(91840) const collection = "ec-invalid-disk" payload := []byte("invalid-disk-id-payload") stream, err := grpcClient.ReceiveFile(ctx) if err != nil { t.Fatalf("ReceiveFile stream create: %v", err) } if err = stream.Send(&volume_server_pb.ReceiveFileRequest{ Data: &volume_server_pb.ReceiveFileRequest_Info{ Info: &volume_server_pb.ReceiveFileInfo{ VolumeId: volumeID, Ext: ".ec00", Collection: collection, IsEcVolume: true, ShardId: 0, FileSize: uint64(len(payload)), DiskId: 99, }, }, }); err != nil { t.Fatalf("ReceiveFile send info: %v", err) } resp, err := stream.CloseAndRecv() if err != nil { t.Fatalf("ReceiveFile close: %v", err) } if resp.GetError() == "" { t.Fatalf("expected invalid disk_id rejection, got success response: %+v", resp) } }