mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-22 07:06:51 +00:00
* fix(ec): planner treats each (server, disk_id) as a distinct target (#9369) master_pb.DataNodeInfo.DiskInfos is keyed by disk type, so a volume server with multiple physical disks of the same type collapses into a single DiskInfo. Per-disk attribution survives only inside the VolumeInfos[].DiskId / EcShardInfos[].DiskId records, and the active topology never put it back together. The EC planner saw N candidates instead of N×disks, returned a short plan, and createECTargets round-robined extra shards onto the same (server, disk_id) — colliding with the #9185 disk_id-aware ReceiveFile. Reconstruct per-physical-disk view in UpdateTopology by splitting each DiskInfo into one entry per observed disk_id, and index volumes / EC shards by their own DiskId so lookups stay aligned. Refuse to plan an EC task when fewer than totalShards distinct disks are available rather than packing shards onto the same disk. Threads dataShards/parityShards through planECDestinations, createECTargets and createECTaskParams so the helpers don't depend on the OSS 10+4 constants — keeps enterprise merges clean. * trim verbose comments * align EC param signatures with enterprise - dataShards/parityShards: uint32 → int (matches enterprise's ratio API) - drop unused multiPlan from createECTaskParams - minTotalDisks: total/parity+1 → ceil(total/parity), correct for non-default ratios Reduces merge surface when this PR lands in seaweed-enterprise.
300 lines
9.0 KiB
Go
300 lines
9.0 KiB
Go
package erasure_coding
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/seaweedfs/seaweedfs/weed/admin/topology"
|
||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestECPlacementPlannerApplyReservations(t *testing.T) {
|
||
activeTopology := buildActiveTopology(t, 1, []string{"hdd"}, 10, 0)
|
||
|
||
planner := newECPlacementPlanner(activeTopology, nil)
|
||
require.NotNil(t, planner)
|
||
|
||
key := ecDiskKey("10.0.0.1:8080", 0)
|
||
candidate, ok := planner.candidateByKey[key]
|
||
require.True(t, ok)
|
||
assert.Equal(t, 10, candidate.FreeSlots)
|
||
assert.Equal(t, 0, candidate.ShardCount)
|
||
assert.Equal(t, 0, candidate.LoadCount)
|
||
|
||
shardImpact := topology.CalculateECShardStorageImpact(1, 1)
|
||
destinations := make([]topology.TaskDestinationSpec, 10)
|
||
for i := 0; i < 10; i++ {
|
||
destinations[i] = topology.TaskDestinationSpec{
|
||
ServerID: "10.0.0.1:8080",
|
||
DiskID: 0,
|
||
StorageImpact: &shardImpact,
|
||
}
|
||
}
|
||
|
||
planner.applyTaskReservations(1024, nil, destinations)
|
||
|
||
candidate = planner.candidateByKey[key]
|
||
assert.Equal(t, 9, candidate.FreeSlots, "10 shard slots should reduce available volume slots by 1")
|
||
assert.Equal(t, 10, candidate.ShardCount)
|
||
assert.Equal(t, 1, candidate.LoadCount, "load should only be incremented once per disk")
|
||
}
|
||
|
||
func TestPlanECDestinationsUsesPlanner(t *testing.T) {
|
||
activeTopology := buildActiveTopology(t, 7, []string{"hdd", "ssd"}, 100, 0)
|
||
planner := newECPlacementPlanner(activeTopology, nil)
|
||
require.NotNil(t, planner)
|
||
|
||
metric := &types.VolumeHealthMetrics{
|
||
VolumeID: 1,
|
||
Server: "10.0.0.1:8080",
|
||
Size: 100 * 1024 * 1024,
|
||
Collection: "",
|
||
}
|
||
|
||
plan, err := planECDestinations(planner, metric, NewDefaultConfig(), erasure_coding.DataShardsCount, erasure_coding.ParityShardsCount)
|
||
require.NoError(t, err)
|
||
require.NotNil(t, plan)
|
||
assert.Equal(t, erasure_coding.TotalShardsCount, len(plan.Plans))
|
||
}
|
||
|
||
func TestECPlacementPlannerPrefersTaggedDisks(t *testing.T) {
|
||
activeTopology := buildActiveTopology(t, 3, []string{"hdd"}, 10, 0)
|
||
topo := activeTopology.GetTopologyInfo()
|
||
for _, dc := range topo.DataCenterInfos {
|
||
for _, rack := range dc.RackInfos {
|
||
for k, node := range rack.DataNodeInfos {
|
||
for diskType := range node.DiskInfos {
|
||
if k < 2 {
|
||
node.DiskInfos[diskType].Tags = []string{"fast"}
|
||
} else {
|
||
node.DiskInfos[diskType].Tags = []string{"slow"}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
require.NoError(t, activeTopology.UpdateTopology(topo))
|
||
|
||
planner := newECPlacementPlanner(activeTopology, []string{"fast"})
|
||
require.NotNil(t, planner)
|
||
|
||
selected, err := planner.selectDestinations("", "", 2)
|
||
require.NoError(t, err)
|
||
require.Len(t, selected, 2)
|
||
|
||
for _, candidate := range selected {
|
||
key := ecDiskKey(candidate.NodeID, candidate.DiskID)
|
||
assert.True(t, diskHasTag(planner.diskTags[key], "fast"))
|
||
}
|
||
}
|
||
|
||
func TestECPlacementPlannerFallsBackWhenTagsInsufficient(t *testing.T) {
|
||
activeTopology := buildActiveTopology(t, 3, []string{"hdd"}, 10, 0)
|
||
topo := activeTopology.GetTopologyInfo()
|
||
for _, dc := range topo.DataCenterInfos {
|
||
for _, rack := range dc.RackInfos {
|
||
for i, node := range rack.DataNodeInfos {
|
||
for diskType := range node.DiskInfos {
|
||
if i == 0 {
|
||
node.DiskInfos[diskType].Tags = []string{"fast"}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
require.NoError(t, activeTopology.UpdateTopology(topo))
|
||
|
||
planner := newECPlacementPlanner(activeTopology, []string{"fast"})
|
||
require.NotNil(t, planner)
|
||
|
||
selected, err := planner.selectDestinations("", "", 3)
|
||
require.NoError(t, err)
|
||
require.Len(t, selected, 3)
|
||
|
||
taggedCount := 0
|
||
for _, candidate := range selected {
|
||
key := ecDiskKey(candidate.NodeID, candidate.DiskID)
|
||
if diskHasTag(planner.diskTags[key], "fast") {
|
||
taggedCount++
|
||
}
|
||
}
|
||
assert.Less(t, taggedCount, len(selected))
|
||
}
|
||
|
||
func TestDetectionContextCancellation(t *testing.T) {
|
||
activeTopology := buildActiveTopology(t, 5, []string{"hdd", "ssd"}, 50, 0)
|
||
clusterInfo := &types.ClusterInfo{ActiveTopology: activeTopology}
|
||
metrics := buildVolumeMetricsForIDs(50)
|
||
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
cancel()
|
||
|
||
_, _, err := Detection(ctx, metrics, clusterInfo, NewDefaultConfig(), 0)
|
||
require.ErrorIs(t, err, context.Canceled)
|
||
}
|
||
|
||
func TestDetectionMaxResultsHonorsLimit(t *testing.T) {
|
||
// One node per shard so each shard gets its own disk (#9369).
|
||
activeTopology := buildActiveTopology(t, erasure_coding.TotalShardsCount, []string{"hdd"}, 20, 0)
|
||
clusterInfo := &types.ClusterInfo{ActiveTopology: activeTopology}
|
||
metrics := buildVolumeMetricsForIDs(3)
|
||
|
||
results, hasMore, err := Detection(context.Background(), metrics, clusterInfo, NewDefaultConfig(), 1)
|
||
require.NoError(t, err)
|
||
assert.Len(t, results, 1)
|
||
assert.True(t, hasMore)
|
||
}
|
||
|
||
// #9369: 7 servers × 2 physical HDDs must yield 14 distinct (server, disk_id)
|
||
// destinations, not 7 destinations doubled up on the same disk.
|
||
func TestPlanECDestinationsSpreadsAcrossPhysicalDisks(t *testing.T) {
|
||
const numServers = 7
|
||
const disksPerServer = 2
|
||
|
||
activeTopology := topology.NewActiveTopology(10)
|
||
nodes := make([]*master_pb.DataNodeInfo, 0, numServers)
|
||
for i := 1; i <= numServers; i++ {
|
||
volumeInfos := make([]*master_pb.VolumeInformationMessage, 0, disksPerServer)
|
||
for d := uint32(0); d < disksPerServer; d++ {
|
||
volumeInfos = append(volumeInfos, &master_pb.VolumeInformationMessage{
|
||
Id: uint32(i*100 + int(d)),
|
||
DiskId: d,
|
||
DiskType: "hdd",
|
||
})
|
||
}
|
||
nodes = append(nodes, &master_pb.DataNodeInfo{
|
||
Id: fmt.Sprintf("127.0.0.1:%d", 8080+i),
|
||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||
"hdd": {
|
||
DiskId: 0,
|
||
VolumeCount: int64(disksPerServer),
|
||
MaxVolumeCount: 200,
|
||
VolumeInfos: volumeInfos,
|
||
},
|
||
},
|
||
})
|
||
}
|
||
require.NoError(t, activeTopology.UpdateTopology(&master_pb.TopologyInfo{
|
||
DataCenterInfos: []*master_pb.DataCenterInfo{{
|
||
Id: "dc1",
|
||
RackInfos: []*master_pb.RackInfo{{
|
||
Id: "rack1",
|
||
DataNodeInfos: nodes,
|
||
}},
|
||
}},
|
||
}))
|
||
|
||
planner := newECPlacementPlanner(activeTopology, nil)
|
||
require.NotNil(t, planner)
|
||
|
||
metric := &types.VolumeHealthMetrics{
|
||
VolumeID: 42,
|
||
Server: "127.0.0.1:8081",
|
||
Size: 100 * 1024 * 1024,
|
||
Collection: "",
|
||
}
|
||
|
||
plan, err := planECDestinations(planner, metric, NewDefaultConfig(), erasure_coding.DataShardsCount, erasure_coding.ParityShardsCount)
|
||
require.NoError(t, err)
|
||
require.NotNil(t, plan)
|
||
require.Equal(t, erasure_coding.TotalShardsCount, len(plan.Plans))
|
||
|
||
seen := make(map[string]bool, len(plan.Plans))
|
||
for _, p := range plan.Plans {
|
||
key := fmt.Sprintf("%s:%d", p.TargetNode, p.TargetDisk)
|
||
assert.False(t, seen[key], "duplicate (server,disk_id) target %s", key)
|
||
seen[key] = true
|
||
}
|
||
}
|
||
|
||
func TestPlanECDestinationsFailsWithInsufficientCapacity(t *testing.T) {
|
||
activeTopology := buildActiveTopology(t, 1, []string{"hdd"}, 1, 1)
|
||
planner := newECPlacementPlanner(activeTopology, nil)
|
||
require.NotNil(t, planner)
|
||
|
||
metric := &types.VolumeHealthMetrics{
|
||
VolumeID: 2,
|
||
Server: "10.0.0.1:8080",
|
||
Size: 10 * 1024 * 1024,
|
||
Collection: "",
|
||
}
|
||
|
||
_, err := planECDestinations(planner, metric, NewDefaultConfig(), erasure_coding.DataShardsCount, erasure_coding.ParityShardsCount)
|
||
require.Error(t, err)
|
||
}
|
||
|
||
func buildVolumeMetricsForIDs(count int) []*types.VolumeHealthMetrics {
|
||
metrics := make([]*types.VolumeHealthMetrics, 0, count)
|
||
now := time.Now()
|
||
for id := 1; id <= count; id++ {
|
||
metrics = append(metrics, &types.VolumeHealthMetrics{
|
||
VolumeID: uint32(id),
|
||
Server: "10.0.0.1:8080",
|
||
Size: 200 * 1024 * 1024,
|
||
Collection: "",
|
||
FullnessRatio: 0.9,
|
||
LastModified: now.Add(-time.Hour),
|
||
Age: 10 * time.Minute,
|
||
})
|
||
}
|
||
return metrics
|
||
}
|
||
|
||
func buildActiveTopology(t *testing.T, nodeCount int, diskTypes []string, maxVolumeCount, usedVolumeCount int64) *topology.ActiveTopology {
|
||
t.Helper()
|
||
activeTopology := topology.NewActiveTopology(10)
|
||
|
||
nodes := make([]*master_pb.DataNodeInfo, 0, nodeCount)
|
||
for i := 1; i <= nodeCount; i++ {
|
||
diskInfos := make(map[string]*master_pb.DiskInfo)
|
||
for diskIndex, diskType := range diskTypes {
|
||
used := usedVolumeCount
|
||
if used > maxVolumeCount {
|
||
used = maxVolumeCount
|
||
}
|
||
volumeInfos := make([]*master_pb.VolumeInformationMessage, 0, 200)
|
||
for vid := 1; vid <= 200; vid++ {
|
||
volumeInfos = append(volumeInfos, &master_pb.VolumeInformationMessage{
|
||
Id: uint32(vid),
|
||
Collection: "",
|
||
DiskId: uint32(diskIndex),
|
||
})
|
||
}
|
||
diskInfos[diskType] = &master_pb.DiskInfo{
|
||
DiskId: uint32(diskIndex),
|
||
VolumeCount: used,
|
||
MaxVolumeCount: maxVolumeCount,
|
||
VolumeInfos: volumeInfos,
|
||
}
|
||
}
|
||
|
||
nodes = append(nodes, &master_pb.DataNodeInfo{
|
||
Id: fmt.Sprintf("10.0.0.%d:8080", i),
|
||
DiskInfos: diskInfos,
|
||
})
|
||
}
|
||
|
||
topologyInfo := &master_pb.TopologyInfo{
|
||
DataCenterInfos: []*master_pb.DataCenterInfo{
|
||
{
|
||
Id: "dc1",
|
||
RackInfos: []*master_pb.RackInfo{
|
||
{
|
||
Id: "rack1",
|
||
DataNodeInfos: nodes,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
require.NoError(t, activeTopology.UpdateTopology(topologyInfo))
|
||
return activeTopology
|
||
}
|