Files
seaweedfs/weed/shell/command_ec_common_test.go
T
chrislusf e05994ca7e ec: add diskType parameter to core EC functions
Add diskType parameter to:
- ecBalancer struct
- collectEcVolumeServersByDc()
- collectEcNodesForDC()
- collectEcNodes()
- EcBalance()

This allows EC operations to target specific disk types (hdd, ssd, etc.)
instead of being hardcoded to HardDriveType only.

For backward compatibility, all callers currently pass types.HardDriveType
as the default value. Subsequent commits will add -diskType flags to
the individual EC commands.
2025-12-10 14:24:55 -08:00

357 lines
11 KiB
Go

package shell
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
var (
topology1 = parseOutput(topoData)
topology2 = parseOutput(topoData2)
topologyEc = parseOutput(topoDataEc)
)
func errorCheck(got error, want string) error {
if got == nil && want == "" {
return nil
}
if got != nil && want == "" {
return fmt.Errorf("expected no error, got %q", got.Error())
}
if got == nil && want != "" {
return fmt.Errorf("got no error, expected %q", want)
}
if !strings.Contains(got.Error(), want) {
return fmt.Errorf("expected error %q, got %q", want, got.Error())
}
return nil
}
func TestCollectCollectionsForVolumeIds(t *testing.T) {
testCases := []struct {
topology *master_pb.TopologyInfo
vids []needle.VolumeId
want []string
}{
// normal volumes
{topology1, nil, nil},
{topology1, []needle.VolumeId{}, nil},
{topology1, []needle.VolumeId{needle.VolumeId(9999)}, nil},
{topology1, []needle.VolumeId{needle.VolumeId(2)}, []string{""}},
{topology1, []needle.VolumeId{needle.VolumeId(2), needle.VolumeId(272)}, []string{"", "collection2"}},
{topology1, []needle.VolumeId{needle.VolumeId(2), needle.VolumeId(272), needle.VolumeId(299)}, []string{"", "collection2"}},
{topology1, []needle.VolumeId{needle.VolumeId(272), needle.VolumeId(299), needle.VolumeId(95)}, []string{"collection1", "collection2"}},
{topology1, []needle.VolumeId{needle.VolumeId(272), needle.VolumeId(299), needle.VolumeId(95), needle.VolumeId(51)}, []string{"collection1", "collection2"}},
{topology1, []needle.VolumeId{needle.VolumeId(272), needle.VolumeId(299), needle.VolumeId(95), needle.VolumeId(51), needle.VolumeId(15)}, []string{"collection0", "collection1", "collection2"}},
// EC volumes
{topology2, []needle.VolumeId{needle.VolumeId(9577)}, []string{"s3qldata"}},
{topology2, []needle.VolumeId{needle.VolumeId(9577), needle.VolumeId(12549)}, []string{"s3qldata"}},
// normal + EC volumes
{topology2, []needle.VolumeId{needle.VolumeId(18111)}, []string{"s3qldata"}},
{topology2, []needle.VolumeId{needle.VolumeId(8677)}, []string{"s3qldata"}},
{topology2, []needle.VolumeId{needle.VolumeId(18111), needle.VolumeId(8677)}, []string{"s3qldata"}},
}
for _, tc := range testCases {
got := collectCollectionsForVolumeIds(tc.topology, tc.vids)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("for %v: got %v, want %v", tc.vids, got, tc.want)
}
}
}
func TestParseReplicaPlacementArg(t *testing.T) {
getDefaultReplicaPlacementOrig := getDefaultReplicaPlacement
getDefaultReplicaPlacement = func(commandEnv *CommandEnv) (*super_block.ReplicaPlacement, error) {
return super_block.NewReplicaPlacementFromString("123")
}
defer func() {
getDefaultReplicaPlacement = getDefaultReplicaPlacementOrig
}()
testCases := []struct {
argument string
want string
wantErr string
}{
{"lalala", "lal", "unexpected replication type"},
{"", "123", ""},
{"021", "021", ""},
}
for _, tc := range testCases {
commandEnv := &CommandEnv{}
got, gotErr := parseReplicaPlacementArg(commandEnv, tc.argument)
if err := errorCheck(gotErr, tc.wantErr); err != nil {
t.Errorf("argument %q: %s", tc.argument, err.Error())
continue
}
want, _ := super_block.NewReplicaPlacementFromString(tc.want)
if !got.Equals(want) {
t.Errorf("got replica placement %q, want %q", got.String(), want.String())
}
}
}
func TestEcDistribution(t *testing.T) {
// find out all volume servers with one slot left.
ecNodes, totalFreeEcSlots := collectEcVolumeServersByDc(topology1, "", types.HardDriveType)
sortEcNodesByFreeslotsDescending(ecNodes)
if totalFreeEcSlots < erasure_coding.TotalShardsCount {
t.Errorf("not enough free ec shard slots: %d", totalFreeEcSlots)
}
allocatedDataNodes := ecNodes
if len(allocatedDataNodes) > erasure_coding.TotalShardsCount {
allocatedDataNodes = allocatedDataNodes[:erasure_coding.TotalShardsCount]
}
for _, dn := range allocatedDataNodes {
// fmt.Printf("info %+v %+v\n", dn.info, dn)
fmt.Printf("=> %+v %+v\n", dn.info.Id, dn.freeEcSlot)
}
}
func TestPickRackToBalanceShardsInto(t *testing.T) {
testCases := []struct {
topology *master_pb.TopologyInfo
vid string
replicaPlacement string
wantOneOf []string
wantErr string
}{
// Non-EC volumes. We don't care about these, but the function should return all racks as a safeguard.
{topologyEc, "", "123", []string{"rack1", "rack2", "rack3", "rack4", "rack5", "rack6"}, ""},
{topologyEc, "6225", "123", []string{"rack1", "rack2", "rack3", "rack4", "rack5", "rack6"}, ""},
{topologyEc, "6226", "123", []string{"rack1", "rack2", "rack3", "rack4", "rack5", "rack6"}, ""},
{topologyEc, "6241", "123", []string{"rack1", "rack2", "rack3", "rack4", "rack5", "rack6"}, ""},
{topologyEc, "6242", "123", []string{"rack1", "rack2", "rack3", "rack4", "rack5", "rack6"}, ""},
// EC volumes.
{topologyEc, "9577", "", nil, "shards 1 > replica placement limit for other racks (0)"},
{topologyEc, "9577", "111", []string{"rack1", "rack2", "rack3"}, ""},
{topologyEc, "9577", "222", []string{"rack1", "rack2", "rack3"}, ""},
{topologyEc, "10457", "222", []string{"rack1"}, ""},
{topologyEc, "12737", "222", []string{"rack2"}, ""},
{topologyEc, "14322", "222", []string{"rack3"}, ""},
}
for _, tc := range testCases {
vid, _ := needle.NewVolumeId(tc.vid)
ecNodes, _ := collectEcVolumeServersByDc(tc.topology, "", types.HardDriveType)
rp, _ := super_block.NewReplicaPlacementFromString(tc.replicaPlacement)
ecb := &ecBalancer{
ecNodes: ecNodes,
replicaPlacement: rp,
}
racks := ecb.racks()
rackToShardCount := countShardsByRack(vid, ecNodes)
got, gotErr := ecb.pickRackToBalanceShardsInto(racks, rackToShardCount)
if err := errorCheck(gotErr, tc.wantErr); err != nil {
t.Errorf("volume %q: %s", tc.vid, err.Error())
continue
}
if string(got) == "" && len(tc.wantOneOf) == 0 {
continue
}
found := false
for _, want := range tc.wantOneOf {
if got := string(got); got == want {
found = true
break
}
}
if !(found) {
t.Errorf("expected one of %v for volume %q, got %q", tc.wantOneOf, tc.vid, got)
}
}
}
func TestPickEcNodeToBalanceShardsInto(t *testing.T) {
testCases := []struct {
topology *master_pb.TopologyInfo
nodeId string
vid string
wantOneOf []string
wantErr string
}{
{topologyEc, "", "", nil, "INTERNAL: missing source nodes"},
{topologyEc, "idontexist", "12737", nil, "INTERNAL: missing source nodes"},
// Non-EC nodes. We don't care about these, but the function should return all available target nodes as a safeguard.
{
topologyEc, "172.19.0.10:8702", "6225", []string{
"172.19.0.13:8701", "172.19.0.14:8711", "172.19.0.16:8704", "172.19.0.17:8703",
"172.19.0.19:8700", "172.19.0.20:8706", "172.19.0.21:8710", "172.19.0.3:8708",
"172.19.0.4:8707", "172.19.0.5:8705", "172.19.0.6:8713", "172.19.0.8:8709",
"172.19.0.9:8712"},
"",
},
{
topologyEc, "172.19.0.8:8709", "6226", []string{
"172.19.0.10:8702", "172.19.0.13:8701", "172.19.0.14:8711", "172.19.0.16:8704",
"172.19.0.17:8703", "172.19.0.19:8700", "172.19.0.20:8706", "172.19.0.21:8710",
"172.19.0.3:8708", "172.19.0.4:8707", "172.19.0.5:8705", "172.19.0.6:8713",
"172.19.0.9:8712"},
"",
},
// EC volumes.
{topologyEc, "172.19.0.10:8702", "14322", []string{
"172.19.0.14:8711", "172.19.0.5:8705", "172.19.0.6:8713"},
""},
{topologyEc, "172.19.0.13:8701", "10457", []string{
"172.19.0.10:8702", "172.19.0.6:8713"},
""},
{topologyEc, "172.19.0.17:8703", "12737", []string{
"172.19.0.13:8701"},
""},
{topologyEc, "172.19.0.20:8706", "14322", []string{
"172.19.0.14:8711", "172.19.0.5:8705", "172.19.0.6:8713"},
""},
}
for _, tc := range testCases {
vid, _ := needle.NewVolumeId(tc.vid)
allEcNodes, _ := collectEcVolumeServersByDc(tc.topology, "", types.HardDriveType)
ecb := &ecBalancer{
ecNodes: allEcNodes,
}
// Resolve target node by name
var ecNode *EcNode
for _, n := range allEcNodes {
if n.info.Id == tc.nodeId {
ecNode = n
break
}
}
got, gotErr := ecb.pickEcNodeToBalanceShardsInto(vid, ecNode, allEcNodes)
if err := errorCheck(gotErr, tc.wantErr); err != nil {
t.Errorf("node %q, volume %q: %s", tc.nodeId, tc.vid, err.Error())
continue
}
if got == nil {
if len(tc.wantOneOf) == 0 {
continue
}
t.Errorf("node %q, volume %q: got no node, want %q", tc.nodeId, tc.vid, tc.wantOneOf)
continue
}
found := false
for _, want := range tc.wantOneOf {
if got := got.info.Id; got == want {
found = true
break
}
}
if !(found) {
t.Errorf("expected one of %v for volume %q, got %q", tc.wantOneOf, tc.vid, got.info.Id)
}
}
}
func TestCountFreeShardSlots(t *testing.T) {
testCases := []struct {
name string
topology *master_pb.TopologyInfo
diskType types.DiskType
want map[string]int
}{
{
name: "topology #1, free HDD shards",
topology: topology1,
diskType: types.HardDriveType,
want: map[string]int{
"192.168.1.1:8080": 17330,
"192.168.1.2:8080": 1540,
"192.168.1.4:8080": 1900,
"192.168.1.5:8080": 27010,
"192.168.1.6:8080": 17420,
},
},
{
name: "topology #1, no free SSD shards available",
topology: topology1,
diskType: types.SsdType,
want: map[string]int{
"192.168.1.1:8080": 0,
"192.168.1.2:8080": 0,
"192.168.1.4:8080": 0,
"192.168.1.5:8080": 0,
"192.168.1.6:8080": 0,
},
},
{
name: "topology #2, no negative free HDD shards",
topology: topology2,
diskType: types.HardDriveType,
want: map[string]int{
"172.19.0.3:8708": 0,
"172.19.0.4:8707": 8,
"172.19.0.5:8705": 58,
"172.19.0.6:8713": 39,
"172.19.0.8:8709": 8,
"172.19.0.9:8712": 0,
"172.19.0.10:8702": 0,
"172.19.0.13:8701": 0,
"172.19.0.14:8711": 0,
"172.19.0.16:8704": 89,
"172.19.0.17:8703": 0,
"172.19.0.19:8700": 9,
"172.19.0.20:8706": 0,
"172.19.0.21:8710": 9,
},
},
{
name: "topology #2, no free SSD shards available",
topology: topology2,
diskType: types.SsdType,
want: map[string]int{
"172.19.0.10:8702": 0,
"172.19.0.13:8701": 0,
"172.19.0.14:8711": 0,
"172.19.0.16:8704": 0,
"172.19.0.17:8703": 0,
"172.19.0.19:8700": 0,
"172.19.0.20:8706": 0,
"172.19.0.21:8710": 0,
"172.19.0.3:8708": 0,
"172.19.0.4:8707": 0,
"172.19.0.5:8705": 0,
"172.19.0.6:8713": 0,
"172.19.0.8:8709": 0,
"172.19.0.9:8712": 0,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := map[string]int{}
eachDataNode(tc.topology, func(dc DataCenterId, rack RackId, dn *master_pb.DataNodeInfo) {
got[dn.Id] = countFreeShardSlots(dn, tc.diskType)
})
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("got %v, want %v", got, tc.want)
}
})
}
}