mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-11 17:10:40 +02:00
* Introduce a new `READS` scrub mode. `READS` performs a full volume scrub but, unlike `FULL`, it will attempt to reconstruct data for missing/damaged shard intervals from other shards in the cluster when necessary. The goal of this check is to ensure that EC volume contents _are readable by Seaweed_ even on a degraded storage state, by exercising parity data which is not read in `FULL` mode. This is useful not only to validate data is user-readable, but also to detect potential parity shard issues which may be difficult to pinpoint otherwise - particularly for older volumes lacking sidecar data, and hence unaffected by `CHECKSUM` scrubs. For regular volumes, this operation is equivalent to `FULL`. Example: ``` > ec.shard.unmount --volumeId=1 --shardId=0,3,11 --delete --apply Live shard topology for volume ID 1 (14 shards): 0@10.200.18.89:9001 1@10.200.18.89:9002 2@10.200.18.89:9003 3@10.200.18.89:9004 4@10.200.18.89:9005 5@10.200.18.89:9006 6@10.200.18.89:9007 7@10.200.18.89:9008 8@10.200.18.89:9009 9@10.200.18.89:9013 10@10.200.18.89:9010 11@10.200.18.89:9011 12@10.200.18.89:9012 13@10.200.18.89:9020 Will unmount + delete 3 shard(s): 0@10.200.18.89:9001 3@10.200.18.89:9004 11@10.200.18.89:9011 Unmounting shard 0@10.200.18.89:9001 for volume ID 1... Deleting shard 0@10.200.18.89:9001 for volume ID 1... Unmounting shard 3@10.200.18.89:9004 for volume ID 1... Deleting shard 3@10.200.18.89:9004 for volume ID 1... Unmounting shard 11@10.200.18.89:9011 for volume ID 1... Deleting shard 11@10.200.18.89:9011 for volume ID 1... All done! > ec.scrub --volumeId=1 --node=10.200.18.89:9002 --mode=full using FULL mode Scrubbing 10.200.18.89:9002 (1/1)... Scrubbed 6 EC files and 1 volumes on 1 nodes Got scrub failures on 1 EC volumes and 1 EC shards :( Affected volumes: 10.200.18.89:9002:1 Affected shards: 10.200.18.89:9002:1:0 > ec.scrub --volumeId=1 --node=10.200.18.89:9002 --mode=reads using READS mode Scrubbing 10.200.18.89:9002 (1/1)... Scrubbed 6 EC files and 1 volumes on 1 nodes ``` * ec: report the shards a READS scrub had to rebuild A READS scrub that recovers an interval was recording nothing, so a volume missing three shards came back clean and nobody repaired it. The unreadable shard is now recorded before the rebuild is attempted: READS reports the same broken shards as FULL and differs only in whether the needles themselves failed, which is the signal worth having - shards are gone, data is still there. forceDeletedNeedlesCheck now applies to READS as well, in the shell and in the RPC guard: it runs the same needle walk as FULL. Regenerated the proto instead of hand-editing it, so the pancis typo (which protoc-gen-go-grpc emits into eight other files here) and the header whitespace stay as generated. Mirrors into the Rust volume server, which also now honors force_deleted_needles_check rather than hardcoding it off. Claude-Session: https://claude.ai/code/session_014yMNebkUjSbx9sfUCWJJtq * ec: answer a deleted needle from a READS rebuild as deleted #11020 gave the Rust recovery a deleted flag alongside its bytes, and it answers a deleted needle with no bytes at all. The READS scrub appended that empty answer, which does not compile against the new signature and, once it did, would leave the needle short and report the size mismatch as damage. Zero-fill the interval instead, the way the direct read beside it already does: the assembled needle then reaches read_bytes as the delete-state mismatch the walk already tolerates. Go takes the same branch off the flag its recovery returns, rather than discarding it. Claude-Session: https://claude.ai/code/session_014yMNebkUjSbx9sfUCWJJtq --------- Co-authored-by: Lisandro Pin <lisandro.pin@proton.ch>
502 lines
18 KiB
Go
502 lines
18 KiB
Go
package volume_server_grpc_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"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"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
)
|
|
|
|
// --- Normal volume scrub tests ---
|
|
|
|
func TestScrubVolumeFullHealthy(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartSingleVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
const volumeID = uint32(200)
|
|
framework.AllocateVolume(t, grpcClient, volumeID, "")
|
|
|
|
httpClient := framework.NewHTTPClient()
|
|
framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 1, 1), []byte("data-one")))
|
|
framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 2, 2), []byte("data-two")))
|
|
framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 3, 3), []byte("data-three")))
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
|
|
VolumeIds: []uint32{volumeID},
|
|
Mode: volume_server_pb.VolumeScrubMode_FULL,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScrubVolume FULL on healthy volume failed: %v", err)
|
|
}
|
|
if resp.GetTotalVolumes() != 1 {
|
|
t.Fatalf("expected total_volumes=1, got %d", resp.GetTotalVolumes())
|
|
}
|
|
if resp.GetTotalFiles() != 3 {
|
|
t.Fatalf("expected total_files=3, got %d", resp.GetTotalFiles())
|
|
}
|
|
if len(resp.GetBrokenVolumeIds()) != 0 {
|
|
t.Fatalf("expected no broken volumes, got %v: %v", resp.GetBrokenVolumeIds(), resp.GetDetails())
|
|
}
|
|
}
|
|
|
|
func TestScrubVolumeFullCorruptData(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartSingleVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
const volumeID = uint32(201)
|
|
framework.AllocateVolume(t, grpcClient, volumeID, "")
|
|
|
|
httpClient := framework.NewHTTPClient()
|
|
framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(volumeID, 1, 1), []byte("important data")))
|
|
|
|
framework.CorruptDatFile(t, clusterHarness.BaseDir(), volumeID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
|
|
VolumeIds: []uint32{volumeID},
|
|
Mode: volume_server_pb.VolumeScrubMode_FULL,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScrubVolume FULL on corrupt volume failed: %v", err)
|
|
}
|
|
if len(resp.GetBrokenVolumeIds()) == 0 {
|
|
t.Fatalf("expected broken volume after data corruption, got none")
|
|
}
|
|
if len(resp.GetDetails()) == 0 {
|
|
t.Fatalf("expected error details for corrupt volume")
|
|
}
|
|
}
|
|
|
|
func TestScrubVolumeMixedHealthy(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartSingleVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
const healthyVol = uint32(202)
|
|
const corruptVol = uint32(203)
|
|
framework.AllocateVolume(t, grpcClient, healthyVol, "")
|
|
framework.AllocateVolume(t, grpcClient, corruptVol, "")
|
|
|
|
httpClient := framework.NewHTTPClient()
|
|
framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(healthyVol, 1, 1), []byte("healthy")))
|
|
framework.ReadAllAndClose(t, framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), framework.NewFileID(corruptVol, 1, 1), []byte("will corrupt")))
|
|
|
|
framework.CorruptIndexFile(t, clusterHarness.BaseDir(), corruptVol)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
|
|
VolumeIds: []uint32{healthyVol, corruptVol},
|
|
Mode: volume_server_pb.VolumeScrubMode_INDEX,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScrubVolume INDEX on mixed volumes failed: %v", err)
|
|
}
|
|
if resp.GetTotalVolumes() != 2 {
|
|
t.Fatalf("expected total_volumes=2, got %d", resp.GetTotalVolumes())
|
|
}
|
|
if len(resp.GetBrokenVolumeIds()) != 1 {
|
|
t.Fatalf("expected exactly 1 broken volume, got %v", resp.GetBrokenVolumeIds())
|
|
}
|
|
if resp.GetBrokenVolumeIds()[0] != corruptVol {
|
|
t.Fatalf("expected broken volume %d, got %d", corruptVol, resp.GetBrokenVolumeIds()[0])
|
|
}
|
|
}
|
|
|
|
func TestScrubVolumeMissingVolumeReturnsError(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartSingleVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
_, err := grpcClient.ScrubVolume(ctx, &volume_server_pb.ScrubVolumeRequest{
|
|
VolumeIds: []uint32{99999},
|
|
Mode: volume_server_pb.VolumeScrubMode_FULL,
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("ScrubVolume should fail for missing volume")
|
|
}
|
|
}
|
|
|
|
// --- EC volume scrub tests ---
|
|
|
|
// ecSetup creates a volume, uploads data, generates EC shards, and mounts all of them.
|
|
func ecSetup(t *testing.T, grpcClient volume_server_pb.VolumeServerClient, httpClient *http.Client, volumeURL string, volumeID uint32) {
|
|
t.Helper()
|
|
framework.AllocateVolume(t, grpcClient, volumeID, "")
|
|
|
|
fid := framework.NewFileID(volumeID, 1, 0xABCD0001)
|
|
uploadResp := framework.UploadBytes(t, httpClient, volumeURL, fid, []byte("ec-scrub-test-data-payload"))
|
|
_ = framework.ReadAllAndClose(t, uploadResp)
|
|
if uploadResp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("upload expected 201, got %d", uploadResp.StatusCode)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
_, err := grpcClient.VolumeEcShardsGenerate(ctx, &volume_server_pb.VolumeEcShardsGenerateRequest{
|
|
VolumeId: volumeID,
|
|
Collection: "",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeEcShardsGenerate failed: %v", err)
|
|
}
|
|
|
|
allShards := make([]uint32, erasure_coding.TotalShardsCount)
|
|
for i := range allShards {
|
|
allShards[i] = uint32(i)
|
|
}
|
|
_, err = grpcClient.VolumeEcShardsMount(ctx, &volume_server_pb.VolumeEcShardsMountRequest{
|
|
VolumeId: volumeID,
|
|
Collection: "",
|
|
ShardIds: allShards,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeEcShardsMount all shards failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestScrubEcVolumeIndexHealthy(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
const volumeID = uint32(210)
|
|
httpClient := framework.NewHTTPClient()
|
|
ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
|
|
VolumeIds: []uint32{volumeID},
|
|
Mode: volume_server_pb.VolumeScrubMode_INDEX,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScrubEcVolume INDEX on healthy volume failed: %v", err)
|
|
}
|
|
if resp.GetTotalVolumes() != 1 {
|
|
t.Fatalf("expected total_volumes=1, got %d", resp.GetTotalVolumes())
|
|
}
|
|
if len(resp.GetBrokenVolumeIds()) != 0 {
|
|
t.Fatalf("expected no broken volumes, got %v: %v", resp.GetBrokenVolumeIds(), resp.GetDetails())
|
|
}
|
|
}
|
|
|
|
func TestScrubEcVolumeLocalHealthy(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
const volumeID = uint32(211)
|
|
httpClient := framework.NewHTTPClient()
|
|
ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
|
|
VolumeIds: []uint32{volumeID},
|
|
Mode: volume_server_pb.VolumeScrubMode_LOCAL,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScrubEcVolume LOCAL on healthy volume failed: %v", err)
|
|
}
|
|
if resp.GetTotalVolumes() != 1 {
|
|
t.Fatalf("expected total_volumes=1, got %d", resp.GetTotalVolumes())
|
|
}
|
|
if resp.GetTotalFiles() != 1 {
|
|
t.Fatalf("expected total_files=1, got %d", resp.GetTotalFiles())
|
|
}
|
|
if len(resp.GetBrokenVolumeIds()) != 0 {
|
|
t.Fatalf("expected no broken volumes, got %v: %v", resp.GetBrokenVolumeIds(), resp.GetDetails())
|
|
}
|
|
if len(resp.GetBrokenShardInfos()) != 0 {
|
|
t.Fatalf("expected no broken shards, got %v", resp.GetBrokenShardInfos())
|
|
}
|
|
}
|
|
|
|
func TestScrubEcVolumeLocalCorruptShard(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
const volumeID = uint32(212)
|
|
httpClient := framework.NewHTTPClient()
|
|
ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeID)
|
|
|
|
// Corrupt shard 0 by truncating it.
|
|
framework.CorruptEcShardFile(t, clusterHarness.BaseDir(), volumeID, 0)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
|
|
VolumeIds: []uint32{volumeID},
|
|
Mode: volume_server_pb.VolumeScrubMode_LOCAL,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScrubEcVolume LOCAL on corrupt shard failed: %v", err)
|
|
}
|
|
if len(resp.GetBrokenVolumeIds()) == 0 {
|
|
t.Fatalf("expected broken volume after shard corruption")
|
|
}
|
|
if len(resp.GetBrokenShardInfos()) == 0 {
|
|
t.Fatalf("expected broken shard info after shard corruption")
|
|
}
|
|
// Verify all reported broken shards belong to the corrupted volume.
|
|
for _, si := range resp.GetBrokenShardInfos() {
|
|
if si.GetVolumeId() != volumeID {
|
|
t.Fatalf("broken shard info for unexpected volume %d, want %d", si.GetVolumeId(), volumeID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScrubEcVolumeAutoSelectWithEcPresent(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
const volumeIDA = uint32(213)
|
|
const volumeIDB = uint32(214)
|
|
httpClient := framework.NewHTTPClient()
|
|
ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeIDA)
|
|
ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeIDB)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Auto-select (empty VolumeIds) should find both EC volumes.
|
|
resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
|
|
Mode: volume_server_pb.VolumeScrubMode_INDEX,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScrubEcVolume auto-select failed: %v", err)
|
|
}
|
|
if resp.GetTotalVolumes() < 2 {
|
|
t.Fatalf("expected at least 2 EC volumes via auto-select, got %d", resp.GetTotalVolumes())
|
|
}
|
|
if len(resp.GetBrokenVolumeIds()) != 0 {
|
|
t.Fatalf("expected no broken volumes, got %v", resp.GetBrokenVolumeIds())
|
|
}
|
|
}
|
|
|
|
func TestScrubEcVolumeUnsupportedMode(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
const volumeID = uint32(215)
|
|
httpClient := framework.NewHTTPClient()
|
|
ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
_, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
|
|
VolumeIds: []uint32{volumeID},
|
|
Mode: volume_server_pb.VolumeScrubMode(99),
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("ScrubEcVolume should fail for unsupported mode")
|
|
}
|
|
}
|
|
|
|
func TestScrubEcVolumeIndexCorruptEcx(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
const volumeID = uint32(216)
|
|
framework.AllocateVolume(t, grpcClient, volumeID, "")
|
|
|
|
httpClient := framework.NewHTTPClient()
|
|
fid := framework.NewFileID(volumeID, 1, 0xABCD0001)
|
|
uploadResp := framework.UploadBytes(t, httpClient, clusterHarness.VolumeAdminURL(), fid, []byte("ec-ecx-corrupt-test"))
|
|
_ = framework.ReadAllAndClose(t, uploadResp)
|
|
if uploadResp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("upload expected 201, got %d", uploadResp.StatusCode)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Generate EC shards (this creates the .ecx file on disk).
|
|
_, err := grpcClient.VolumeEcShardsGenerate(ctx, &volume_server_pb.VolumeEcShardsGenerateRequest{
|
|
VolumeId: volumeID,
|
|
Collection: "",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeEcShardsGenerate failed: %v", err)
|
|
}
|
|
|
|
// Corrupt the .ecx file BEFORE mounting, so the corrupted size is loaded.
|
|
framework.CorruptEcxFile(t, clusterHarness.BaseDir(), volumeID)
|
|
|
|
// Now mount shards - the ecx file size will reflect the corruption.
|
|
allShards := make([]uint32, erasure_coding.TotalShardsCount)
|
|
for i := range allShards {
|
|
allShards[i] = uint32(i)
|
|
}
|
|
_, err = grpcClient.VolumeEcShardsMount(ctx, &volume_server_pb.VolumeEcShardsMountRequest{
|
|
VolumeId: volumeID,
|
|
Collection: "",
|
|
ShardIds: allShards,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeEcShardsMount failed: %v", err)
|
|
}
|
|
|
|
resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
|
|
VolumeIds: []uint32{volumeID},
|
|
Mode: volume_server_pb.VolumeScrubMode_INDEX,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScrubEcVolume INDEX on corrupt ecx failed: %v", err)
|
|
}
|
|
if len(resp.GetBrokenVolumeIds()) == 0 {
|
|
t.Fatalf("expected broken volume after ECX corruption")
|
|
}
|
|
}
|
|
|
|
// scrubEcUntilLocated runs an EC scrub, retrying while the server is still waiting on
|
|
// the master for the shard locations a distributed scrub needs.
|
|
func scrubEcUntilLocated(t *testing.T, ctx context.Context, grpcClient volume_server_pb.VolumeServerClient, volumeID uint32, mode volume_server_pb.VolumeScrubMode) *volume_server_pb.ScrubEcVolumeResponse {
|
|
t.Helper()
|
|
deadline := time.Now().Add(60 * time.Second)
|
|
for {
|
|
resp, err := grpcClient.ScrubEcVolume(ctx, &volume_server_pb.ScrubEcVolumeRequest{
|
|
VolumeIds: []uint32{volumeID},
|
|
Mode: mode,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ScrubEcVolume %s failed: %v", mode, err)
|
|
}
|
|
waiting := false
|
|
for _, d := range resp.GetDetails() {
|
|
if strings.Contains(d, "failed to locate shard via master grpc") {
|
|
waiting = true
|
|
break
|
|
}
|
|
}
|
|
if !waiting {
|
|
return resp
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("master never reported EC shard locations for volume %d: %v", volumeID, resp.GetDetails())
|
|
}
|
|
time.Sleep(time.Second)
|
|
}
|
|
}
|
|
|
|
// With a shard gone, FULL cannot read the intervals that lived on it, while READS
|
|
// rebuilds them from parity. Either way the missing shard has to be reported: a
|
|
// volume that scrubs clean is a volume nobody repairs.
|
|
func TestScrubEcVolumeReadsRecoversMissingShard(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
clusterHarness := framework.StartVolumeCluster(t, matrix.P1())
|
|
conn, grpcClient := framework.DialVolumeServer(t, clusterHarness.VolumeGRPCAddress())
|
|
defer conn.Close()
|
|
|
|
const volumeID = uint32(216)
|
|
const missingShard = uint32(0)
|
|
httpClient := framework.NewHTTPClient()
|
|
ecSetup(t, grpcClient, httpClient, clusterHarness.VolumeAdminURL(), volumeID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
|
defer cancel()
|
|
|
|
if _, err := grpcClient.VolumeEcShardsUnmount(ctx, &volume_server_pb.VolumeEcShardsUnmountRequest{
|
|
VolumeId: volumeID,
|
|
ShardIds: []uint32{missingShard},
|
|
}); err != nil {
|
|
t.Fatalf("VolumeEcShardsUnmount shard %d failed: %v", missingShard, err)
|
|
}
|
|
if _, err := grpcClient.VolumeEcShardsDelete(ctx, &volume_server_pb.VolumeEcShardsDeleteRequest{
|
|
VolumeId: volumeID,
|
|
ShardIds: []uint32{missingShard},
|
|
}); err != nil {
|
|
t.Fatalf("VolumeEcShardsDelete shard %d failed: %v", missingShard, err)
|
|
}
|
|
|
|
fullResp := scrubEcUntilLocated(t, ctx, grpcClient, volumeID, volume_server_pb.VolumeScrubMode_FULL)
|
|
assertOnlyBrokenShard(t, "FULL", fullResp, missingShard)
|
|
if len(fullResp.GetDetails()) == 0 {
|
|
t.Fatalf("FULL should report the needles it could not read")
|
|
}
|
|
|
|
readsResp := scrubEcUntilLocated(t, ctx, grpcClient, volumeID, volume_server_pb.VolumeScrubMode_READS)
|
|
assertOnlyBrokenShard(t, "READS", readsResp, missingShard)
|
|
if len(readsResp.GetDetails()) != 0 {
|
|
t.Fatalf("READS should rebuild every needle from parity, got: %v", readsResp.GetDetails())
|
|
}
|
|
}
|
|
|
|
func assertOnlyBrokenShard(t *testing.T, mode string, resp *volume_server_pb.ScrubEcVolumeResponse, shardID uint32) {
|
|
t.Helper()
|
|
infos := resp.GetBrokenShardInfos()
|
|
if len(infos) != 1 || infos[0].GetShardId() != shardID {
|
|
t.Fatalf("%s reported broken shards %v, want only shard %d (details: %v)", mode, infos, shardID, resp.GetDetails())
|
|
}
|
|
}
|