Files
seaweedfs/weed/server/volume_grpc_ec_shared_index_test.go
T
Chris Lu c7781bfca2 fix(ec): remove shared EC index only when no shard remains node-wide (#9955)
* fix(ec): remove the shared EC index only when no shard remains node-wide

deleteEcShardIdsForEachLocation removed the shared .ecx/.ecj/.vif index
as soon as a single disk's shard count hit 0, even when a sibling disk
of the same node still held shards of the volume (split-disk reconciled
layout) -- orphaning those shards without their index. Split the
non-teardown delete into two passes: delete the requested shard files
(and now-orphaned per-disk bitrot sidecars) on every disk, then remove
the shared index only once no shard of the volume remains on ANY disk.
This brings the Go volume server in line with the Rust one, which already
gates the index removal on a node-wide check.

* refactor(ec): reuse checkEcVolumeStatus across the two delete passes

Address review: cache hasEcxFile/hasIdxFile from the node-wide count pass
and pass them to removeEcSharedIndexFiles instead of re-listing each
location's directory.

* fix(ec): clean an orphaned EC .vif even when its .ecx is already gone

Address review: removeEcSharedIndexFiles returned early on !hasEcxFile,
so a node-wide teardown left a stale EC .vif behind when its .ecx was
already removed. Decouple the .vif removal (gated on !hasIdxFile) from
.ecx presence so the generation metadata doesn't leak once no shard
remains node-wide.
2026-06-14 06:36:50 -07:00

88 lines
3.0 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")
}