Files
seaweedfs/weed/operation/volume_move/ec_move_test.go
T
Chris Lu 94f8e2caf9 EC: handle zero-sized shard files uniformly (moves, rebuilds, startup cleanup) (#10753)
* volume_move: treat zero-sized EC shards as absent in move verification

A zero-sized shard file is residue of a failed operation (issue 10730),
not a shard - but VerifyEcShards only checked presence, so a copy that
landed as an empty file passed verification and the source was deleted
behind it. Size zero now reads as absent, with a distinct error naming
the zero-sized shard so the operator can tell a broken copy from a
missing one.

* storage: exclude zero-sized EC shards from rebuilds and clean up stale ones

The reproducer in issue 10730: a zero-sized shard file left by a failed
operation was selected as a Reed-Solomon input and failed the whole
rebuild with an input size mismatch, because input discovery checked
existence, not substance.

- RebuildEcFiles treats a zero-sized shard file as missing and
  regenerates over it in place (the reclassified-corrupt path: temp
  file beside the residue, atomic rename).
- The startup/rescan shard loader, which always skipped zero-sized
  files, now deletes them once they are older than an hour - young
  enough files can be an in-flight copy's just-created file, since the
  same scan runs from LoadNewVolumes while serving.

Regression tests: a rebuild with one emptied shard regenerates it
byte-identical; the loader deletes a stale zero-sized shard and leaves
a fresh one alone.

* storage: age-check each zero-shard cleanup candidate individually

The shard scan merges the data and idx directory listings, so the
age-checked entry and a deletion candidate can be different files
sharing one name - a stale zero-sized file in one directory next to a
fresh same-named file in the other (possibly an in-flight copy's
just-created one) could get the fresh file deleted. Each candidate's
own modification time now decides, both directories are handled in one
pass, and the split-directory case is pinned by a test.
2026-08-13 21:38:22 -07:00

138 lines
4.5 KiB
Go

package volume_move
import (
"context"
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
)
func ecMove(shardIds ...erasure_coding.ShardId) EcShardMove {
return EcShardMove{
VolumeId: 7,
Collection: "c1",
ShardIds: shardIds,
Source: srcAddr,
Target: dstAddr,
TargetDisk: 2,
}
}
func dstShards(shardIds ...uint32) []*volume_server_pb.EcShardInfo {
var infos []*volume_server_pb.EcShardInfo
for _, sid := range shardIds {
infos = append(infos, &volume_server_pb.EcShardInfo{VolumeId: 7, ShardId: sid, Size: 1024})
}
return infos
}
func TestMoveEcShardsSequence(t *testing.T) {
cluster := newFakeCluster()
cluster.ecShards[string(dstAddr)] = dstShards(3, 4)
err := cluster.mover().MoveEcShards(context.Background(), ecMove(3, 4), EcMoveOptions{IoBytePerSecond: 77})
if err != nil {
t.Fatalf("MoveEcShards: %v", err)
}
assertCalls(t, cluster.callList(), []string{
"dst:8080 VolumeEcShardsCopy",
"dst:8080 VolumeEcShardsMount",
"dst:8080 VolumeEcShardsInfo",
"src:8080 VolumeEcShardsUnmount",
"src:8080 VolumeEcShardsDelete",
})
copyReq := cluster.ecCopyReqs[0]
if !copyReq.CopyEcxFile || !copyReq.CopyEcjFile || !copyReq.CopyVifFile || !copyReq.CopyEcsumFile {
t.Errorf("shard sidecars not all copied: %+v", copyReq)
}
if copyReq.DiskId != 2 || copyReq.SourceDataNode != string(srcAddr) || copyReq.Collection != "c1" || copyReq.IoBytePerSecond != 77 {
t.Errorf("copy request not propagated: %+v", copyReq)
}
}
func TestMoveEcShardsVerifyFailureKeepsSource(t *testing.T) {
cluster := newFakeCluster()
cluster.ecShards[string(dstAddr)] = dstShards(3) // shard 4 didn't register
err := cluster.mover().MoveEcShards(context.Background(), ecMove(3, 4), EcMoveOptions{})
if err == nil || !strings.Contains(err.Error(), "missing EC shard 7.4") {
t.Fatalf("expected missing-shard error, got: %v", err)
}
for _, call := range cluster.callList() {
if call == "src:8080 VolumeEcShardsUnmount" || call == "src:8080 VolumeEcShardsDelete" {
t.Fatalf("source touched despite verification failure: %v", cluster.callList())
}
}
}
func TestMoveEcShardsZeroSizedDestinationKeepsSource(t *testing.T) {
// A zero-sized shard on the destination is residue of a failed operation
// (seaweedfs issue 10730); verification must not count it as a delivered
// shard, or the source is deleted behind a broken copy.
cluster := newFakeCluster()
cluster.ecShards[string(dstAddr)] = dstShards(3, 4)
cluster.ecShards[string(dstAddr)][1].Size = 0 // shard 4 landed as an empty file
err := cluster.mover().MoveEcShards(context.Background(), ecMove(3, 4), EcMoveOptions{})
if err == nil || !strings.Contains(err.Error(), "zero-sized EC shard 7.4") {
t.Fatalf("expected zero-sized-shard rejection, got: %v", err)
}
for _, call := range cluster.callList() {
if call == "src:8080 VolumeEcShardsUnmount" || call == "src:8080 VolumeEcShardsDelete" {
t.Fatalf("source touched despite zero-sized destination shard: %v", cluster.callList())
}
}
}
func TestMoveEcShardsRejectsSameServer(t *testing.T) {
// The second target is the same server written with an explicit grpc port;
// the guard must see through the representation difference.
for _, target := range []pb.ServerAddress{srcAddr, pb.ServerAddress("src:8080.18080")} {
cluster := newFakeCluster()
move := ecMove(3)
move.Target = target
err := cluster.mover().MoveEcShards(context.Background(), move, EcMoveOptions{})
if err == nil || !strings.Contains(err.Error(), "its own server") {
t.Fatalf("target %q: expected same-server rejection, got: %v", target, err)
}
if len(cluster.callList()) != 0 {
t.Fatalf("target %q: RPCs issued for a rejected move: %v", target, cluster.callList())
}
}
}
func TestRemoveEcShards(t *testing.T) {
cluster := newFakeCluster()
err := cluster.mover().RemoveEcShards(context.Background(), 7, "c1", srcAddr, []erasure_coding.ShardId{3})
if err != nil {
t.Fatalf("RemoveEcShards: %v", err)
}
assertCalls(t, cluster.callList(), []string{
"src:8080 VolumeEcShardsUnmount",
"src:8080 VolumeEcShardsDelete",
})
}
func TestCopyAndMountEcShardsSameAddressMountsOnly(t *testing.T) {
cluster := newFakeCluster()
err := cluster.mover().CopyAndMountEcShards(context.Background(), 7, "c1", []erasure_coding.ShardId{3}, srcAddr, srcAddr, 0, 0, nil)
if err != nil {
t.Fatalf("CopyAndMountEcShards: %v", err)
}
assertCalls(t, cluster.callList(), []string{
"src:8080 VolumeEcShardsMount",
})
}