mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 20:26:45 +00:00
Replica placement rules and "this node already holds the volume" are constraints the picker has no way to model. The predicate receives the candidate's rack and data center, because the constraints needing them are exactly the ones a bare node cannot express. Claude-Session: https://claude.ai/code/session_01Ks16jnt4S7gdDk8cheQ3xu
259 lines
9.6 KiB
Go
259 lines
9.6 KiB
Go
package placement
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
)
|
|
|
|
func placementNode(id string, maxVolumes, used int64, totalBytes, freeBytes uint64) *master_pb.DataNodeInfo {
|
|
return &master_pb.DataNodeInfo{Id: id, DiskInfos: map[string]*master_pb.DiskInfo{
|
|
"ssd": {
|
|
MaxVolumeCount: maxVolumes, VolumeCount: used,
|
|
DiskTotalBytes: totalBytes, DiskFreeBytes: freeBytes,
|
|
},
|
|
}}
|
|
}
|
|
|
|
// placementTopo builds a one-datacenter topology, taking each node's first
|
|
// character as its rack so the tests read compactly.
|
|
func placementTopo(nodes ...*master_pb.DataNodeInfo) *master_pb.TopologyInfo {
|
|
byRack := map[string][]*master_pb.DataNodeInfo{}
|
|
var order []string
|
|
for _, n := range nodes {
|
|
rack := n.Id[:1]
|
|
if _, seen := byRack[rack]; !seen {
|
|
order = append(order, rack)
|
|
}
|
|
byRack[rack] = append(byRack[rack], n)
|
|
}
|
|
var racks []*master_pb.RackInfo
|
|
for _, r := range order {
|
|
racks = append(racks, &master_pb.RackInfo{Id: r, DataNodeInfos: byRack[r]})
|
|
}
|
|
return &master_pb.TopologyInfo{DataCenterInfos: []*master_pb.DataCenterInfo{{Id: "dc1", RackInfos: racks}}}
|
|
}
|
|
|
|
func TestPickTargetPrefersTheEmptiestByFreeBytes(t *testing.T) {
|
|
topo := placementTopo(
|
|
placementNode("a1", 10, 1, 1000, 200),
|
|
placementNode("a2", 10, 1, 1000, 900),
|
|
placementNode("a3", 10, 1, 1000, 500),
|
|
)
|
|
got := PickTarget(topo, PlacementPreference{Source: "z9", DiskType: types.SsdType})
|
|
if got.GetId() != "a2" {
|
|
t.Fatalf("got %q, want the node with the most free bytes (a2)", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetFallsBackToSlotsWithoutReportedBytes(t *testing.T) {
|
|
// A volume server too old to report filesystem bytes must still be ordered
|
|
// sensibly rather than read as having zero free.
|
|
topo := placementTopo(
|
|
placementNode("a1", 10, 9, 0, 0),
|
|
placementNode("a2", 10, 2, 0, 0),
|
|
)
|
|
got := PickTarget(topo, PlacementPreference{Source: "z9", DiskType: types.SsdType})
|
|
if got.GetId() != "a2" {
|
|
t.Fatalf("got %q, want the node with the most free slots (a2)", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetSkipsSourceExcludedAndFull(t *testing.T) {
|
|
topo := placementTopo(
|
|
placementNode("a1", 10, 1, 1000, 900), // the source itself
|
|
placementNode("a2", 10, 1, 1000, 800), // already spoken for by the plan
|
|
placementNode("a3", 5, 5, 1000, 700), // no free slot
|
|
placementNode("a4", 10, 1, 1000, 100),
|
|
)
|
|
got := PickTarget(topo, PlacementPreference{
|
|
Source: "a1", DiskType: types.SsdType, Exclude: map[string]bool{"a2": true},
|
|
})
|
|
if got.GetId() != "a4" {
|
|
t.Fatalf("got %q, want a4", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetReturnsNilWhenNothingFits(t *testing.T) {
|
|
topo := placementTopo(placementNode("a1", 5, 5, 1000, 900))
|
|
if got := PickTarget(topo, PlacementPreference{Source: "z9", DiskType: types.SsdType}); got != nil {
|
|
t.Fatalf("got %q, want nil", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetHonoursAnchorDataCenter(t *testing.T) {
|
|
topo := &master_pb.TopologyInfo{DataCenterInfos: []*master_pb.DataCenterInfo{
|
|
{Id: "dc1", RackInfos: []*master_pb.RackInfo{{Id: "r1", DataNodeInfos: []*master_pb.DataNodeInfo{
|
|
placementNode("a1", 10, 1, 1000, 100),
|
|
}}}},
|
|
{Id: "dc2", RackInfos: []*master_pb.RackInfo{{Id: "r2", DataNodeInfos: []*master_pb.DataNodeInfo{
|
|
placementNode("b1", 10, 1, 1000, 900),
|
|
}}}},
|
|
}}
|
|
got := PickTarget(topo, PlacementPreference{
|
|
Source: "z9", DiskType: types.SsdType, AnchorDataCenter: "dc1",
|
|
})
|
|
if got.GetId() != "a1" {
|
|
t.Fatalf("got %q, want a1 -- the anchor outranks an emptier node elsewhere", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetPrefersTheSourceRackOverAnEmptierStranger(t *testing.T) {
|
|
topo := placementTopo(
|
|
placementNode("a1", 10, 1, 1000, 500), // source, rack a
|
|
placementNode("a2", 10, 1, 1000, 100), // same rack, tighter
|
|
placementNode("b1", 10, 1, 1000, 900), // other rack, emptiest
|
|
)
|
|
got := PickTarget(topo, PlacementPreference{Source: "a1", DiskType: types.SsdType})
|
|
if got.GetId() != "a2" {
|
|
t.Fatalf("got %q, want a2 -- same rack outranks emptier", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetLeavesTheRackWhenItCannotHoldTheVolume(t *testing.T) {
|
|
// Locality is a preference over candidates, never a reason to overfill.
|
|
topo := placementTopo(
|
|
placementNode("a1", 10, 1, 1000, 500), // source
|
|
placementNode("a2", 5, 5, 1000, 900), // same rack, no free slot
|
|
placementNode("b1", 10, 1, 1000, 100),
|
|
)
|
|
got := PickTarget(topo, PlacementPreference{Source: "a1", DiskType: types.SsdType})
|
|
if got.GetId() != "b1" {
|
|
t.Fatalf("got %q, want b1 -- a full same-rack node is not a candidate", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetPrefersSameDataCenterOverAnother(t *testing.T) {
|
|
topo := &master_pb.TopologyInfo{DataCenterInfos: []*master_pb.DataCenterInfo{
|
|
{Id: "dc1", RackInfos: []*master_pb.RackInfo{
|
|
{Id: "r1", DataNodeInfos: []*master_pb.DataNodeInfo{placementNode("a1", 10, 1, 1000, 500)}},
|
|
{Id: "r2", DataNodeInfos: []*master_pb.DataNodeInfo{placementNode("a2", 10, 1, 1000, 100)}},
|
|
}},
|
|
{Id: "dc2", RackInfos: []*master_pb.RackInfo{
|
|
{Id: "r3", DataNodeInfos: []*master_pb.DataNodeInfo{placementNode("b1", 10, 1, 1000, 900)}},
|
|
}},
|
|
}}
|
|
got := PickTarget(topo, PlacementPreference{Source: "a1", DiskType: types.SsdType})
|
|
if got.GetId() != "a2" {
|
|
t.Fatalf("got %q, want a2 -- another rack in the same dc beats another dc", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetFallsBackToCapacityForAnUnknownSource(t *testing.T) {
|
|
// A source that is not in the topology has no rack to be near, so ordering
|
|
// degrades to capacity rather than to topology order.
|
|
topo := placementTopo(
|
|
placementNode("a1", 10, 1, 1000, 100),
|
|
placementNode("b1", 10, 1, 1000, 900),
|
|
)
|
|
got := PickTarget(topo, PlacementPreference{Source: "gone:8080", DiskType: types.SsdType})
|
|
if got.GetId() != "b1" {
|
|
t.Fatalf("got %q, want b1", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetSpendsCapacitySoABatchSpreads(t *testing.T) {
|
|
// The reservation this exists for: planning several moves from one snapshot
|
|
// must not stack them all on whichever node started emptiest.
|
|
topo := placementTopo(
|
|
placementNode("a1", 4, 0, 4000, 4000),
|
|
placementNode("a2", 4, 0, 4000, 3000),
|
|
)
|
|
seen := map[string]int{}
|
|
for i := 0; i < 4; i++ {
|
|
got := PickTarget(topo, PlacementPreference{Source: "z9", DiskType: types.SsdType})
|
|
if got == nil {
|
|
t.Fatalf("pick %d returned nil", i)
|
|
}
|
|
seen[got.GetId()]++
|
|
}
|
|
if seen["a1"] == 4 || seen["a2"] == 4 {
|
|
t.Fatalf("all four picks landed on one node: %v", seen)
|
|
}
|
|
if seen["a1"]+seen["a2"] != 4 {
|
|
t.Fatalf("unexpected spread: %v", seen)
|
|
}
|
|
}
|
|
|
|
func TestPickTargetStopsWhenTheSnapshotIsSpent(t *testing.T) {
|
|
// Reservation has to make a node stop being a candidate, not merely rank it
|
|
// lower, or a batch could overcommit a tier.
|
|
topo := placementTopo(placementNode("a1", 2, 0, 2000, 2000))
|
|
for i := 0; i < 2; i++ {
|
|
if got := PickTarget(topo, PlacementPreference{Source: "z9", DiskType: types.SsdType}); got == nil {
|
|
t.Fatalf("pick %d returned nil while slots remained", i)
|
|
}
|
|
}
|
|
if got := PickTarget(topo, PlacementPreference{Source: "z9", DiskType: types.SsdType}); got != nil {
|
|
t.Fatalf("got %q, want nil once the snapshot is spent", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetUsesOneMetricWhenReportingIsMixed(t *testing.T) {
|
|
// b1 has the most free bytes but the fewest slots; a2 the reverse. With a3
|
|
// reporting no bytes at all, every candidate must be ordered on slots, or
|
|
// the comparator is intransitive and the winner depends on sort order.
|
|
topo := placementTopo(
|
|
placementNode("a1", 10, 1, 1000, 500), // source
|
|
placementNode("a2", 10, 1, 1000, 100),
|
|
placementNode("a3", 10, 2, 0, 0), // too old to report bytes
|
|
placementNode("a4", 10, 8, 1000, 900),
|
|
)
|
|
got := PickTarget(topo, PlacementPreference{Source: "a1", DiskType: types.SsdType})
|
|
if got.GetId() != "a2" {
|
|
t.Fatalf("got %q, want a2 -- the most free slots once bytes are unusable", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetReservesTheVolumeSize(t *testing.T) {
|
|
// A volume far bigger than the tier average must be charged at its real
|
|
// size, or a batch of them overcommits the destination.
|
|
topo := placementTopo(
|
|
placementNode("a1", 10, 0, 1000, 1000),
|
|
placementNode("a2", 10, 0, 1000, 900),
|
|
)
|
|
first := PickTarget(topo, PlacementPreference{Source: "z9", DiskType: types.SsdType, VolumeBytes: 800})
|
|
if first.GetId() != "a1" {
|
|
t.Fatalf("first pick %q, want a1", first.GetId())
|
|
}
|
|
// a1 now reports 200 free against a2's 900, so the next move must go there
|
|
// rather than to the node that merely started emptiest.
|
|
second := PickTarget(topo, PlacementPreference{Source: "z9", DiskType: types.SsdType, VolumeBytes: 800})
|
|
if second.GetId() != "a2" {
|
|
t.Fatalf("second pick %q, want a2 after 800 bytes were spent on a1", second.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetHonoursTheCallerPredicate(t *testing.T) {
|
|
// Constraints placement does not model -- replica placement, a node already
|
|
// holding the volume -- stay with the caller, which keeps them out of the
|
|
// ranking rather than duplicating them here.
|
|
topo := placementTopo(
|
|
placementNode("a1", 10, 1, 1000, 900),
|
|
placementNode("a2", 10, 1, 1000, 500),
|
|
)
|
|
got := PickTarget(topo, PlacementPreference{
|
|
Source: "z9", DiskType: types.SsdType,
|
|
Accept: func(n *master_pb.DataNodeInfo, dc, rack string) bool { return n.Id != "a1" },
|
|
})
|
|
if got.GetId() != "a2" {
|
|
t.Fatalf("got %q, want a2 -- a1 was rejected despite being emptier", got.GetId())
|
|
}
|
|
}
|
|
|
|
func TestPickTargetPredicateSeesRackAndDataCenter(t *testing.T) {
|
|
topo := placementTopo(placementNode("a1", 10, 1, 1000, 900))
|
|
var sawDc, sawRack string
|
|
PickTarget(topo, PlacementPreference{
|
|
Source: "z9", DiskType: types.SsdType,
|
|
Accept: func(n *master_pb.DataNodeInfo, dc, rack string) bool {
|
|
sawDc, sawRack = dc, rack
|
|
return true
|
|
},
|
|
})
|
|
if sawDc != "dc1" || sawRack != "a" {
|
|
t.Fatalf("predicate saw dc=%q rack=%q, want dc1/a", sawDc, sawRack)
|
|
}
|
|
}
|