mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 12:16:36 +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>
152 lines
5.2 KiB
Go
152 lines
5.2 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"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/util"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// The non-teardown EC shard delete must not remove the shared .ecx/.ecj index while
|
|
// a sibling disk still holds shards of the same volume (split-disk layout) — doing so
|
|
// orphans those shards. The shared index is removed only once no shard remains across
|
|
// any disk of the node.
|
|
func TestEcShardDeleteKeepsSharedIndexWhileSiblingHasShards(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
dir0 := filepath.Join(tempDir, "disk0")
|
|
dir1 := filepath.Join(tempDir, "disk1")
|
|
for _, d := range []string{dir0, dir1} {
|
|
require.NoError(t, os.MkdirAll(d, 0o755))
|
|
}
|
|
const collection = "ec-shared-index"
|
|
vid := needle.VolumeId(77)
|
|
|
|
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, stats.DefaultDiskIOProbeConfig())
|
|
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))
|
|
// Shared index lives on disk0.
|
|
require.NoError(t, os.WriteFile(base0+".ecx", make([]byte, 16), 0o644))
|
|
require.NoError(t, os.WriteFile(base0+".ecj", nil, 0o644))
|
|
require.NoError(t, os.WriteFile(base0+".vif", []byte("x"), 0o644))
|
|
plant := func(dir string, ids ...int) {
|
|
base := erasure_coding.EcShardFileName(collection, dir, int(vid))
|
|
for _, id := range ids {
|
|
require.NoError(t, os.WriteFile(base+erasure_coding.ToExt(id), []byte("s"), 0o644))
|
|
}
|
|
}
|
|
plant(dir0, 0, 5)
|
|
plant(dir1, 7, 12)
|
|
|
|
vs := &VolumeServer{store: store}
|
|
del := func(ids ...uint32) {
|
|
_, err := vs.VolumeEcShardsDelete(context.Background(), &volume_server_pb.VolumeEcShardsDeleteRequest{
|
|
VolumeId: uint32(vid),
|
|
Collection: collection,
|
|
ShardIds: ids,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Delete disk0's shards; disk1 still holds 7 and 12, so the shared index stays.
|
|
del(0, 5)
|
|
require.False(t, util.FileExists(base0+erasure_coding.ToExt(0)), "deleted shard file should be gone")
|
|
require.True(t, util.FileExists(base0+".ecx"), "shared .ecx must be preserved while a sibling disk holds shards")
|
|
|
|
// Delete disk1's shards; no shard remains node-wide, so the shared index is removed.
|
|
del(7, 12)
|
|
require.False(t, util.FileExists(base0+".ecx"), "shared .ecx must be removed once no shard remains node-wide")
|
|
}
|
|
|
|
// Same protection for the idx-base bitrot sidecar: with one -dir.idx shared by
|
|
// every disk, emptying one disk must not sweep <idx>/<volume>.ecsum while a
|
|
// sibling disk still holds shards; it goes when the last sibling is emptied.
|
|
func TestEcShardDeleteKeepsSharedIdxSidecarWhileSiblingHasShards(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
dir0 := filepath.Join(tempDir, "disk0")
|
|
dir1 := filepath.Join(tempDir, "disk1")
|
|
idxDir := filepath.Join(tempDir, "idx")
|
|
for _, d := range []string{dir0, dir1, idxDir} {
|
|
require.NoError(t, os.MkdirAll(d, 0o755))
|
|
}
|
|
const collection = "ec-shared-idx-sidecar"
|
|
vid := needle.VolumeId(78)
|
|
|
|
store := storage.NewStore(nil, "localhost", 8080, 18080, "http://localhost:8080", "store-id",
|
|
[]string{dir0, dir1}, []int32{100, 100}, []util.MinFreeSpace{{}, {}}, idxDir,
|
|
storage.NeedleMapInMemory, []types.DiskType{types.HardDriveType, types.HardDriveType}, nil, 3, stats.DefaultDiskIOProbeConfig())
|
|
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)
|
|
})
|
|
|
|
plant := func(dir string, ids ...int) {
|
|
base := erasure_coding.EcShardFileName(collection, dir, int(vid))
|
|
for _, id := range ids {
|
|
require.NoError(t, os.WriteFile(base+erasure_coding.ToExt(id), []byte("s"), 0o644))
|
|
}
|
|
}
|
|
plant(dir0, 0)
|
|
plant(dir1, 7)
|
|
idxSidecar := erasure_coding.EcShardFileName(collection, idxDir, int(vid)) + erasure_coding.BitrotSidecarExt
|
|
require.NoError(t, os.WriteFile(idxSidecar, []byte("x"), 0o644))
|
|
|
|
vs := &VolumeServer{store: store}
|
|
del := func(ids ...uint32) {
|
|
_, err := vs.VolumeEcShardsDelete(context.Background(), &volume_server_pb.VolumeEcShardsDeleteRequest{
|
|
VolumeId: uint32(vid),
|
|
Collection: collection,
|
|
ShardIds: ids,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
del(0)
|
|
require.True(t, util.FileExists(idxSidecar), "shared idx sidecar must survive while a sibling disk holds shards")
|
|
|
|
del(7)
|
|
require.False(t, util.FileExists(idxSidecar), "shared idx sidecar must be removed once no shard remains node-wide")
|
|
}
|