mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-11 17:10:40 +02:00
* 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.
138 lines
4.5 KiB
Go
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",
|
|
})
|
|
}
|