placement: let callers reject candidates placement cannot judge (#10581)

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
This commit is contained in:
Chris Lu
2026-08-05 00:18:22 -07:00
committed by GitHub
parent aceab0802e
commit 03388c4beb
2 changed files with 41 additions and 0 deletions
+9
View File
@@ -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
+32
View File
@@ -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)
}
}