mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-27 02:23:23 +00:00
* fix(rust-volume): remove .ecsum sidecars on EC destroy / shard delete Rust EcVolume::destroy removed shards and .ecx/.ecj/.vif but left bitrot checksum sidecars (.ecsum / .ecsum.v*). On clusters that run weed-volume (not Go weed volume), collection.delete therefore orphans every sidecar while correctly wiping shards — observed live on 4.39 (14/14 .ecsum survived after collection.delete on a freshly encoded EC volume). Go Destroy already calls RemoveBitrotSidecars; this brings Rust to parity: - hoist remove_bitrot_sidecars into ec_bitrot (shared helper) - call it from EcVolume::destroy for dir / dir_idx / ecx_actual_dir - call it from Store::delete_ec_shards when a disk has no remaining shards - unit test: test_destroy_removes_bitrot_sidecar * rust volume: gate the shard-delete sidecar sweep on a local shard removal Only sweep a disk's .ecsum when this delete actually removed a shard file there, matching Go's found gate: a delete that never touched a disk must not strip a sidecar it does not own — a shared -dir.idx sibling with surviving shards, or an ec.rebuild index-prep copy that lands .ecx/.ecsum before any shard. The shard-presence probe now treats unexpected stat errors as "exists" so a transient failure cannot orphan-classify live shards, and check_all_ec_shards_deleted reuses it. * rust volume: destroy() sidecar sweep needs only the data and idx bases ecx_actual_dir is always one of the two, so the third branch could never run; this is now exactly Go Destroy()'s two-base sweep. * rust volume: call the shared sidecar removal helper directly * rust volume: unit-test remove_bitrot_sidecars Mirrors Go's TestRemoveBitrotSidecars: legacy and versioned sidecars are removed, a shard file and a longer-vid sidecar survive, absent is success. * rust volume: keep the shared idx-base sidecar while a sibling disk has shards One -dir.idx serves every location, so emptying one disk must not sweep <idx>/<vol>.ecsum out from under a sibling that still holds shards. Nothing reads the idx-base sidecar today, but .ecx shows index-dir files are real; this keeps the defensive sweep safe if a writer ever lands one there. * ec shard delete: keep the shared idx-base sidecar while a sibling disk has shards One -dir.idx serves every disk, so emptying one disk must not sweep <idx>/<vol>.ecsum out from under a sibling that still holds shards of the volume — the same gate the Rust volume server applies. A status error counts as in-use so a transient failure never strips it early. * rust volume: drop a shard-only disk's stale .vif with the node's last shard Go's removeEcSharedIndexFiles also clears the data-base .vif in the all-shards-gone pass, gated on .idx absence so a disk still hosting the source volume keeps its live .vif; the Rust delete path left it behind. Unexpected stat errors count as .idx-present so a transient failure never strips a live volume's .vif. --------- Co-authored-by: Chris Lu <chris.lu@gmail.com>
295 lines
9.3 KiB
Go
295 lines
9.3 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/volume_info"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func TestCheckEcVolumeStatusCountOnlyDataShards(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
dataDir := filepath.Join(tempDir, "data")
|
|
idxDir := filepath.Join(tempDir, "idx")
|
|
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir data dir: %v", err)
|
|
}
|
|
if err := os.MkdirAll(idxDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir idx dir: %v", err)
|
|
}
|
|
|
|
baseName := "7"
|
|
filesToCreate := []string{
|
|
filepath.Join(dataDir, baseName+".ec00"),
|
|
filepath.Join(dataDir, baseName+".ec09"),
|
|
filepath.Join(dataDir, baseName+".ec13"),
|
|
filepath.Join(idxDir, baseName+".ecx"),
|
|
filepath.Join(idxDir, baseName+".ecj"),
|
|
filepath.Join(idxDir, baseName+".idx"),
|
|
}
|
|
for _, fileName := range filesToCreate {
|
|
if err := os.WriteFile(fileName, []byte("x"), 0o644); err != nil {
|
|
t.Fatalf("create %s: %v", fileName, err)
|
|
}
|
|
}
|
|
|
|
location := &storage.DiskLocation{
|
|
Directory: dataDir,
|
|
IdxDirectory: idxDir,
|
|
}
|
|
|
|
hasEcxFile, hasIdxFile, shardCount, err := checkEcVolumeStatus(baseName, location)
|
|
if err != nil {
|
|
t.Fatalf("checkEcVolumeStatus: %v", err)
|
|
}
|
|
|
|
if !hasEcxFile {
|
|
t.Fatalf("expected hasEcxFile=true")
|
|
}
|
|
if !hasIdxFile {
|
|
t.Fatalf("expected hasIdxFile=true")
|
|
}
|
|
if shardCount != 3 {
|
|
t.Fatalf("expected shardCount=3, got %d", shardCount)
|
|
}
|
|
}
|
|
|
|
// TestRemoveStaleEcArtifacts: a fresh encode deletes every prior EC artifact
|
|
// (incl. shard ids beyond the default ratio and versioned bitrot sidecars)
|
|
// while leaving the source .dat/.idx/.vif untouched.
|
|
func TestRemoveStaleEcArtifacts(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
dataDir := filepath.Join(tempDir, "data")
|
|
idxDir := filepath.Join(tempDir, "idx")
|
|
for _, d := range []string{dataDir, idxDir} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", d, err)
|
|
}
|
|
}
|
|
|
|
const baseName = "7"
|
|
dataBase := filepath.Join(dataDir, baseName)
|
|
idxBase := filepath.Join(idxDir, baseName)
|
|
|
|
// EC artifacts that must be removed, including a shard id past the default
|
|
// 10+4 ratio (proves the MaxShardCount scan) and a versioned sidecar.
|
|
var ecFiles []string
|
|
for _, id := range []int{0, 9, 13, 20, erasure_coding.MaxShardCount - 1} {
|
|
ecFiles = append(ecFiles, dataBase+erasure_coding.ToExt(id))
|
|
}
|
|
ecFiles = append(ecFiles,
|
|
dataBase+".ecx", dataBase+".ecj",
|
|
idxBase+".ecx", idxBase+".ecj",
|
|
dataBase+erasure_coding.BitrotSidecarExt, // .ecsum (generation 0)
|
|
dataBase+erasure_coding.BitrotSidecarExt+".v2", // versioned sidecar
|
|
)
|
|
|
|
// Source files that must survive — the authoritative input for the encode.
|
|
srcFiles := []string{dataBase + ".dat", idxBase + ".idx", dataBase + ".vif"}
|
|
|
|
for _, f := range append(append([]string{}, ecFiles...), srcFiles...) {
|
|
if err := os.WriteFile(f, []byte("x"), 0o644); err != nil {
|
|
t.Fatalf("create %s: %v", f, err)
|
|
}
|
|
}
|
|
|
|
removeStaleEcArtifacts(dataBase, idxBase, erasure_coding.MaxShardCount)
|
|
|
|
for _, f := range ecFiles {
|
|
if util.FileExists(f) {
|
|
t.Errorf("expected EC artifact removed: %s", f)
|
|
}
|
|
}
|
|
for _, f := range srcFiles {
|
|
if !util.FileExists(f) {
|
|
t.Errorf("expected source file preserved: %s", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDeleteEcShardsWithoutLocalEcx: the delete handler removes the requested
|
|
// shard files even on a disk with no local .ecx, so a failed-copy orphan stays
|
|
// cleanable rather than being mounted later under a foreign index.
|
|
func TestDeleteEcShardsWithoutLocalEcx(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
dataDir := filepath.Join(tempDir, "data")
|
|
idxDir := filepath.Join(tempDir, "idx")
|
|
for _, d := range []string{dataDir, idxDir} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", d, err)
|
|
}
|
|
}
|
|
|
|
const baseName = "7"
|
|
// Orphan shard files with NO .ecx/.idx anywhere — a failed-copy leftover.
|
|
orphans := []string{
|
|
filepath.Join(dataDir, baseName+".ec03"),
|
|
filepath.Join(dataDir, baseName+".ec11"),
|
|
}
|
|
for _, f := range orphans {
|
|
if err := os.WriteFile(f, []byte("x"), 0o644); err != nil {
|
|
t.Fatalf("create %s: %v", f, err)
|
|
}
|
|
}
|
|
|
|
location := &storage.DiskLocation{Directory: dataDir, IdxDirectory: idxDir}
|
|
if err := deleteEcShardIdsForEachLocation(baseName, location, []*storage.DiskLocation{location}, []uint32{3, 11}); err != nil {
|
|
t.Fatalf("deleteEcShardIdsForEachLocation: %v", err)
|
|
}
|
|
for _, f := range orphans {
|
|
if util.FileExists(f) {
|
|
t.Errorf("expected orphan shard removed without a local .ecx: %s", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestVolumeEcShardsInfo_AggregatesAcrossDisks pins the multi-disk path:
|
|
// when a volume server mounts EC shards for the same volume on more than
|
|
// one disk (each disk holds its own EcVolume entry — Store.FindEcVolume
|
|
// returns only the first), VolumeEcShardsInfo used to report shards from
|
|
// a single disk. The ec.encode verification step (verifyEcShardsBeforeDelete)
|
|
// then refused to delete the source volume because the union across
|
|
// servers fell short of dataShards + parityShards. The handler must walk
|
|
// every DiskLocation so the response covers every shard the server holds.
|
|
func TestVolumeEcShardsInfo_AggregatesAcrossDisks(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
dir0 := filepath.Join(tempDir, "disk0")
|
|
dir1 := filepath.Join(tempDir, "disk1")
|
|
for _, d := range []string{dir0, dir1} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", d, err)
|
|
}
|
|
}
|
|
|
|
const collection = "ec-multi-disk-info"
|
|
vid := needle.VolumeId(42)
|
|
const dataShards, parityShards = 10, 4
|
|
const datSize int64 = 10 * 1024 * 1024
|
|
|
|
// Two shards on disk0, two on disk1. The .ecx / .ecj / .vif live on
|
|
// disk0 so each disk's EcVolume can open the index files via the
|
|
// cross-disk fallback in NewEcVolume.
|
|
shardsOnDisk0 := []erasure_coding.ShardId{0, 5}
|
|
shardsOnDisk1 := []erasure_coding.ShardId{7, 12}
|
|
diskIOProbeConfig := stats.DefaultDiskIOProbeConfig()
|
|
|
|
store := storage.NewStore(nil, "localhost", 8080, 18080, "http://localhost:8080", "store-id",
|
|
[]string{dir0, dir1},
|
|
[]int32{100, 100},
|
|
[]util.MinFreeSpace{{}, {}},
|
|
"",
|
|
storage.NeedleMapInMemory,
|
|
[]types.DiskType{types.HardDriveType, types.HardDriveType},
|
|
nil,
|
|
3,
|
|
diskIOProbeConfig,
|
|
)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-store.NewEcShardsChan:
|
|
case <-store.NewVolumesChan:
|
|
case <-store.DeletedVolumesChan:
|
|
case <-store.DeletedEcShardsChan:
|
|
case <-store.StateUpdateChan:
|
|
case <-done:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
t.Cleanup(func() {
|
|
store.Close()
|
|
close(done)
|
|
})
|
|
|
|
base0 := erasure_coding.EcShardFileName(collection, dir0, int(vid))
|
|
base1 := erasure_coding.EcShardFileName(collection, dir1, int(vid))
|
|
|
|
// .ecx, .ecj, .vif live on disk0. NewEcVolume on disk1 falls back to
|
|
// disk0's idx dir. The .ecx needs >0 bytes so HasEcxFileOnDisk does
|
|
// not treat it as the corrupt-stub case; a single zero entry is the
|
|
// smallest valid index file (WalkIndex iterates one zero-sized needle).
|
|
if err := os.WriteFile(base0+".ecx", make([]byte, 16), 0o644); err != nil {
|
|
t.Fatalf("write .ecx: %v", err)
|
|
}
|
|
if err := os.WriteFile(base0+".ecj", nil, 0o644); err != nil {
|
|
t.Fatalf("write .ecj: %v", err)
|
|
}
|
|
if err := volume_info.SaveVolumeInfo(base0+".vif", &volume_server_pb.VolumeInfo{
|
|
Version: uint32(needle.Version3),
|
|
DatFileSize: datSize,
|
|
EcShardConfig: &volume_server_pb.EcShardConfig{
|
|
DataShards: dataShards,
|
|
ParityShards: parityShards,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("save .vif: %v", err)
|
|
}
|
|
|
|
plant := func(base string, shardId erasure_coding.ShardId) {
|
|
t.Helper()
|
|
f, err := os.Create(base + erasure_coding.ToExt(int(shardId)))
|
|
if err != nil {
|
|
t.Fatalf("create shard %d: %v", shardId, err)
|
|
}
|
|
// MountEcShards does not validate shard size, so any non-empty
|
|
// truncate avoids the zero-byte ignore branch in loadAllEcShards.
|
|
if err := f.Truncate(1); err != nil {
|
|
f.Close()
|
|
t.Fatalf("truncate shard %d: %v", shardId, err)
|
|
}
|
|
f.Close()
|
|
}
|
|
for _, sid := range shardsOnDisk0 {
|
|
plant(base0, sid)
|
|
}
|
|
for _, sid := range shardsOnDisk1 {
|
|
plant(base1, sid)
|
|
}
|
|
|
|
for _, sid := range append([]erasure_coding.ShardId{}, append(shardsOnDisk0, shardsOnDisk1...)...) {
|
|
if err := store.MountEcShards(collection, vid, sid, ""); err != nil {
|
|
t.Fatalf("MountEcShards %d.%d: %v", vid, sid, err)
|
|
}
|
|
}
|
|
|
|
vs := &VolumeServer{store: store}
|
|
resp, err := vs.VolumeEcShardsInfo(context.Background(), &volume_server_pb.VolumeEcShardsInfoRequest{
|
|
VolumeId: uint32(vid),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VolumeEcShardsInfo: %v", err)
|
|
}
|
|
|
|
gotShardIds := make([]int, 0, len(resp.GetEcShardInfos()))
|
|
for _, info := range resp.GetEcShardInfos() {
|
|
if info.GetVolumeId() != uint32(vid) {
|
|
t.Errorf("EcShardInfo VolumeId=%d, want %d", info.GetVolumeId(), vid)
|
|
}
|
|
gotShardIds = append(gotShardIds, int(info.GetShardId()))
|
|
}
|
|
sort.Ints(gotShardIds)
|
|
|
|
want := []int{0, 5, 7, 12}
|
|
if len(gotShardIds) != len(want) {
|
|
t.Fatalf("VolumeEcShardsInfo returned %d shards (ids=%v), want %d (ids=%v)",
|
|
len(gotShardIds), gotShardIds, len(want), want)
|
|
}
|
|
for i, sid := range want {
|
|
if gotShardIds[i] != sid {
|
|
t.Fatalf("VolumeEcShardsInfo shard ids=%v, want %v", gotShardIds, want)
|
|
}
|
|
}
|
|
}
|