diff --git a/weed/placement/placement.go b/weed/placement/placement.go index 274fb3e13..29f333cc8 100644 --- a/weed/placement/placement.go +++ b/weed/placement/placement.go @@ -20,6 +20,12 @@ type PlacementPreference struct { // Exclude names nodes already spoken for by the same plan, so a caller // placing several copies does not stack them on one node. Exclude map[string]bool + // Accept rejects candidates the caller cannot use for reasons placement does + // not model -- replica placement rules, a node already holding the volume. + // It receives the candidate's rack and data center because the constraints + // that need them are exactly the ones a bare node cannot express. Nil + // accepts everything. + Accept func(node *master_pb.DataNodeInfo, dataCenter, rack string) bool // VolumeBytes is what this move will actually consume on the destination. // Zero falls back to the tier's average volume size, which is all a caller // planning a not-yet-created volume can know. @@ -73,6 +79,9 @@ func PickTarget(topo *master_pb.TopologyInfo, pref PlacementPreference) *master_ if d == nil || d.VolumeCount >= d.MaxVolumeCount { continue } + if pref.Accept != nil && !pref.Accept(n.info, n.dc, n.rack) { + continue + } c := candidate{node: n} c.bytes, c.hasBytes = d.DiskFreeBytes, d.DiskTotalBytes != 0 c.slots = d.MaxVolumeCount - d.VolumeCount diff --git a/weed/placement/placement_test.go b/weed/placement/placement_test.go index f36faa8fc..1b553bc87 100644 --- a/weed/placement/placement_test.go +++ b/weed/placement/placement_test.go @@ -224,3 +224,35 @@ func TestPickTargetReservesTheVolumeSize(t *testing.T) { 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) + } +}