Skip to content

Commit acd5d18

Browse files
committed
nrt: score: add tests for partial NRT data
Currently, the NRT (NodeResourceTopology) filter plugin just exits and allows silently nodes which don't have NRT objects available. This was done back in time to allow the plugin to coexist with the regular scheduler, and in general work in mixed scenarios on which only some node have NRT data reported. IOW, the decision wants to enable clusters with non-homogeneous nodes. The score plugins need to work in these scenarios as well. It needs to tolerate gracefully partial NRT data availability (some nodes have, some nodes have not) and should always prefer nodes which have NRT data over nodes which don't. In this patch we start tests for these scenarios. More to come. Signed-off-by: Francesco Romani <[email protected]>
1 parent d906cb8 commit acd5d18

File tree

1 file changed

+154
-5
lines changed

1 file changed

+154
-5
lines changed

pkg/noderesourcetopology/score_test.go

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ const (
4141

4242
type nodeToScoreMap map[string]int64
4343

44-
func initTest(nodeTopologies []*topologyv1alpha2.NodeResourceTopology) (map[string]*v1.Node, ctrlclient.Client) {
44+
type nrtFilterFn func(nrt *topologyv1alpha2.NodeResourceTopology) *topologyv1alpha2.NodeResourceTopology
45+
46+
func nrtPassthrough(nrt *topologyv1alpha2.NodeResourceTopology) *topologyv1alpha2.NodeResourceTopology {
47+
return nrt
48+
}
49+
50+
func initTest(nodeTopologies []*topologyv1alpha2.NodeResourceTopology, nrtFilter nrtFilterFn) (map[string]*v1.Node, ctrlclient.Client) {
4551
nodesMap := make(map[string]*v1.Node)
4652

4753
// init node objects
@@ -62,7 +68,12 @@ func initTest(nodeTopologies []*topologyv1alpha2.NodeResourceTopology) (map[stri
6268
panic(err)
6369
}
6470

65-
for _, obj := range nodeTopologies {
71+
for _, obj_ := range nodeTopologies {
72+
obj := nrtFilter(obj_)
73+
if obj == nil {
74+
continue
75+
}
76+
6677
if err := fakeClient.Create(context.Background(), obj.DeepCopy()); err != nil {
6778
panic(err)
6879
}
@@ -133,7 +144,7 @@ func TestNodeResourceScorePlugin(t *testing.T) {
133144
}
134145

135146
for _, test := range tests {
136-
nodesMap, lister := initTest(defaultNUMANodes(withPolicy(topologyv1alpha2.SingleNUMANodeContainerLevel)))
147+
nodesMap, lister := initTest(defaultNUMANodes(withPolicy(topologyv1alpha2.SingleNUMANodeContainerLevel)), nrtPassthrough)
137148
t.Run(test.name, func(t *testing.T) {
138149
tm := &TopologyMatch{
139150
scoreStrategyFunc: test.strategy,
@@ -170,7 +181,7 @@ func TestNodeResourceScorePlugin(t *testing.T) {
170181
}
171182

172183
if wantScore != gotScore {
173-
t.Errorf("wrong score for node %q: wanted: %q, got: %d", gotNode, wantScore, gotScore)
184+
t.Errorf("wrong score for node %q: wanted: %d, got: %d", gotNode, wantScore, gotScore)
174185
}
175186
}
176187
}
@@ -428,7 +439,7 @@ func TestNodeResourceScorePluginLeastNUMA(t *testing.T) {
428439

429440
for _, tc := range testCases {
430441
t.Run(tc.name, func(t *testing.T) {
431-
nodesMap, lister := initTest(tc.nodes)
442+
nodesMap, lister := initTest(tc.nodes, nrtPassthrough)
432443

433444
tm := &TopologyMatch{
434445
scoreStrategyType: apiconfig.LeastNUMANodes,
@@ -461,6 +472,144 @@ func TestNodeResourceScorePluginLeastNUMA(t *testing.T) {
461472
}
462473
}
463474

475+
// when only a subset of nodes has NRT data available[1], prefer the nodes which have the NRT data over the other nodes;
476+
// IOW, a node without NRT data available should always have score == 0
477+
func TestNodeResourcePartialDataScorePlugin(t *testing.T) {
478+
type podRequests struct {
479+
pod *v1.Pod
480+
name string
481+
wantStatus *framework.Status
482+
}
483+
pRequests := []podRequests{
484+
{
485+
pod: makePodByResourceList(&v1.ResourceList{
486+
v1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI),
487+
v1.ResourceMemory: *resource.NewQuantity(20*1024*1024, resource.DecimalSI)}),
488+
name: "Pod1",
489+
wantStatus: nil,
490+
},
491+
}
492+
493+
type testScenario struct {
494+
name string
495+
wantedRes nodeToScoreMap
496+
requests []podRequests
497+
strategy scoreStrategyFn
498+
nrtFilter nrtFilterFn
499+
}
500+
501+
tests := []testScenario{
502+
{
503+
name: "No data at all, MostAllocated strategy",
504+
wantedRes: nodeToScoreMap{},
505+
requests: pRequests,
506+
strategy: mostAllocatedScoreStrategy,
507+
nrtFilter: func(nrt *topologyv1alpha2.NodeResourceTopology) *topologyv1alpha2.NodeResourceTopology {
508+
return nil
509+
},
510+
},
511+
{
512+
name: "No data at all, LeastAllocated strategy",
513+
wantedRes: nodeToScoreMap{},
514+
requests: pRequests,
515+
strategy: leastAllocatedScoreStrategy,
516+
nrtFilter: func(nrt *topologyv1alpha2.NodeResourceTopology) *topologyv1alpha2.NodeResourceTopology {
517+
return nil
518+
},
519+
},
520+
{
521+
name: "No data at all, BalancedAllocation strategy",
522+
wantedRes: nodeToScoreMap{},
523+
requests: pRequests,
524+
strategy: balancedAllocationScoreStrategy,
525+
nrtFilter: func(nrt *topologyv1alpha2.NodeResourceTopology) *topologyv1alpha2.NodeResourceTopology {
526+
return nil
527+
},
528+
},
529+
{
530+
name: "One node with NRT data, MostAllocated strategy",
531+
wantedRes: nodeToScoreMap{"Node1": 27},
532+
requests: pRequests,
533+
strategy: mostAllocatedScoreStrategy,
534+
nrtFilter: func(nrt *topologyv1alpha2.NodeResourceTopology) *topologyv1alpha2.NodeResourceTopology {
535+
if nrt.Name != "Node1" {
536+
return nil
537+
}
538+
return nrt
539+
},
540+
},
541+
{
542+
name: "One node with NRT data, LeastAllocated strategy",
543+
wantedRes: nodeToScoreMap{"Node1": 73},
544+
requests: pRequests,
545+
strategy: leastAllocatedScoreStrategy,
546+
nrtFilter: func(nrt *topologyv1alpha2.NodeResourceTopology) *topologyv1alpha2.NodeResourceTopology {
547+
if nrt.Name != "Node1" {
548+
return nil
549+
}
550+
return nrt
551+
},
552+
},
553+
{
554+
name: "One node with NRT data, BalancedAllocation strategy",
555+
wantedRes: nodeToScoreMap{"Node1": 89},
556+
requests: pRequests,
557+
strategy: balancedAllocationScoreStrategy,
558+
nrtFilter: func(nrt *topologyv1alpha2.NodeResourceTopology) *topologyv1alpha2.NodeResourceTopology {
559+
if nrt.Name != "Node1" {
560+
return nil
561+
}
562+
return nrt
563+
},
564+
},
565+
}
566+
567+
for _, test := range tests {
568+
nodesMap, lister := initTest(defaultNUMANodes(withPolicy(topologyv1alpha2.SingleNUMANodeContainerLevel)), test.nrtFilter)
569+
t.Run(test.name, func(t *testing.T) {
570+
tm := &TopologyMatch{
571+
scoreStrategyFunc: test.strategy,
572+
nrtCache: nrtcache.NewPassthrough(lister),
573+
}
574+
575+
for _, req := range test.requests {
576+
nodeToScore := make(nodeToScoreMap, len(nodesMap))
577+
for _, node := range nodesMap {
578+
score, gotStatus := tm.Score(
579+
context.Background(),
580+
framework.NewCycleState(),
581+
req.pod,
582+
node.ObjectMeta.Name)
583+
584+
t.Logf("%v; %v; %v; score: %v; status: %v\n",
585+
test.name,
586+
req.name,
587+
node.ObjectMeta.Name,
588+
score,
589+
gotStatus)
590+
591+
if !reflect.DeepEqual(gotStatus, req.wantStatus) {
592+
t.Errorf("status does not match: %v, want: %v\n", gotStatus, req.wantStatus)
593+
}
594+
nodeToScore[node.ObjectMeta.Name] = score
595+
}
596+
gotNode := findMaxScoreNode(nodeToScore)
597+
gotScore := nodeToScore[gotNode]
598+
t.Logf("%q: got node %q with score %d\n", test.name, gotNode, gotScore)
599+
for wantNode, wantScore := range test.wantedRes {
600+
if wantNode != gotNode {
601+
t.Errorf("failed to select the desired node: wanted: %q, got: %q", wantNode, gotNode)
602+
}
603+
604+
if wantScore != gotScore {
605+
t.Errorf("wrong score for node %q: wanted: %d, got: %d", gotNode, wantScore, gotScore)
606+
}
607+
}
608+
}
609+
})
610+
}
611+
}
612+
464613
// return the name of the node with the highest score
465614
func findMaxScoreNode(nodeToScore nodeToScoreMap) string {
466615
max := int64(0)

0 commit comments

Comments
 (0)