kubernetes/pkg/scheduler/framework/plugins/defaultpreemption/default_preemption_test.go

3622 lines
164 KiB
Go

/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package defaultpreemption
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"math/rand"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1"
"k8s.io/api/scheduling/v1alpha3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/wait"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/informers"
clientsetfake "k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/events"
featuregatetesting "k8s.io/component-base/featuregate/testing"
corev1helpers "k8s.io/component-helpers/scheduling/corev1"
"k8s.io/klog/v2"
"k8s.io/klog/v2/ktesting"
kubeschedulerconfigv1 "k8s.io/kube-scheduler/config/v1"
extenderv1 "k8s.io/kube-scheduler/extender/v1"
fwk "k8s.io/kube-scheduler/framework"
apipod "k8s.io/kubernetes/pkg/api/v1/pod"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/scheduler/apis/config"
configv1 "k8s.io/kubernetes/pkg/scheduler/apis/config/v1"
apicache "k8s.io/kubernetes/pkg/scheduler/backend/api_cache"
apidispatcher "k8s.io/kubernetes/pkg/scheduler/backend/api_dispatcher"
internalcache "k8s.io/kubernetes/pkg/scheduler/backend/cache"
internalqueue "k8s.io/kubernetes/pkg/scheduler/backend/queue"
"k8s.io/kubernetes/pkg/scheduler/framework"
apicalls "k8s.io/kubernetes/pkg/scheduler/framework/api_calls"
"k8s.io/kubernetes/pkg/scheduler/framework/parallelize"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/defaultbinder"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/feature"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/interpodaffinity"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/noderesources"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/podtopologyspread"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/queuesort"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/tainttoleration"
"k8s.io/kubernetes/pkg/scheduler/framework/preemption"
frameworkruntime "k8s.io/kubernetes/pkg/scheduler/framework/runtime"
"k8s.io/kubernetes/pkg/scheduler/metrics"
st "k8s.io/kubernetes/pkg/scheduler/testing"
tf "k8s.io/kubernetes/pkg/scheduler/testing/framework"
"k8s.io/kubernetes/pkg/scheduler/util"
)
var (
negPriority, lowPriority, midPriority, highPriority, veryHighPriority = int32(-100), int32(0), int32(100), int32(1000), int32(10000)
smallRes = map[v1.ResourceName]string{
v1.ResourceCPU: "100m",
v1.ResourceMemory: "100",
}
mediumRes = map[v1.ResourceName]string{
v1.ResourceCPU: "200m",
v1.ResourceMemory: "200",
}
largeRes = map[v1.ResourceName]string{
v1.ResourceCPU: "300m",
v1.ResourceMemory: "300",
}
veryLargeRes = map[v1.ResourceName]string{
v1.ResourceCPU: "500m",
v1.ResourceMemory: "500",
}
epochTime = metav1.NewTime(time.Unix(0, 0))
epochTime1 = metav1.NewTime(time.Unix(0, 1))
epochTime2 = metav1.NewTime(time.Unix(0, 2))
epochTime3 = metav1.NewTime(time.Unix(0, 3))
epochTime4 = metav1.NewTime(time.Unix(0, 4))
epochTime5 = metav1.NewTime(time.Unix(0, 5))
epochTime6 = metav1.NewTime(time.Unix(0, 6))
)
func init() {
metrics.Register()
}
func getDefaultDefaultPreemptionArgs() *config.DefaultPreemptionArgs {
v1dpa := &kubeschedulerconfigv1.DefaultPreemptionArgs{}
configv1.SetDefaults_DefaultPreemptionArgs(v1dpa)
dpa := &config.DefaultPreemptionArgs{}
configv1.Convert_v1_DefaultPreemptionArgs_To_config_DefaultPreemptionArgs(v1dpa, dpa, nil)
return dpa
}
var nodeResourcesFitFunc = frameworkruntime.FactoryAdapter(feature.Features{}, noderesources.NewFit)
var podTopologySpreadFunc = frameworkruntime.FactoryAdapter(feature.Features{}, podtopologyspread.New)
// TestPlugin returns Error status when trying to `AddPod` or `RemovePod` on the nodes which have the {k,v} label pair defined on the nodes.
type TestPlugin struct {
name string
}
func newTestPlugin(_ context.Context, injArgs runtime.Object, f fwk.Handle) (fwk.Plugin, error) {
return &TestPlugin{name: "test-plugin"}, nil
}
func (pl *TestPlugin) AddPod(ctx context.Context, state fwk.CycleState, podToSchedule *v1.Pod, podInfoToAdd fwk.PodInfo, nodeInfo fwk.NodeInfo) *fwk.Status {
if nodeInfo.Node().GetLabels()["error"] == "true" {
return fwk.AsStatus(fmt.Errorf("failed to add pod: %v", podToSchedule.Name))
}
return nil
}
func (pl *TestPlugin) RemovePod(ctx context.Context, state fwk.CycleState, podToSchedule *v1.Pod, podInfoToRemove fwk.PodInfo, nodeInfo fwk.NodeInfo) *fwk.Status {
if nodeInfo.Node().GetLabels()["error"] == "true" {
return fwk.AsStatus(fmt.Errorf("failed to remove pod: %v", podToSchedule.Name))
}
return nil
}
func (pl *TestPlugin) Name() string {
return pl.name
}
func (pl *TestPlugin) PreFilterExtensions() fwk.PreFilterExtensions {
return pl
}
func (pl *TestPlugin) PreFilter(ctx context.Context, state fwk.CycleState, p *v1.Pod, nodes []fwk.NodeInfo) (*fwk.PreFilterResult, *fwk.Status) {
return nil, nil
}
func (pl *TestPlugin) Filter(ctx context.Context, state fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
return nil
}
const (
LabelKeyIsViolatingPDB = "test.kubernetes.io/is-violating-pdb"
LabelValueViolatingPDB = "violating"
LabelValueNonViolatingPDB = "non-violating"
)
func TestPostFilter(t *testing.T) {
onePodRes := map[v1.ResourceName]string{v1.ResourcePods: "1"}
nodeRes := map[v1.ResourceName]string{v1.ResourceCPU: "200m", v1.ResourceMemory: "400"}
tests := []struct {
name string
pod *v1.Pod
pods []*v1.Pod
pdbs []*policy.PodDisruptionBudget
nodes []*v1.Node
podGroups []*v1alpha3.PodGroup
filteredNodesStatuses *framework.NodeToStatus
features feature.Features
extender fwk.Extender
wantResult *fwk.PostFilterResult
wantStatus *fwk.Status
}{
{
name: "pod with higher priority can be made schedulable",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
wantResult: framework.NewPostFilterResultWithNominatedNode("node1"),
wantStatus: fwk.NewStatus(fwk.Success, "preemption: found a potential placement for pod on node node1, preempting 1 victims"),
},
{
name: "pod with tied priority is still unschedulable",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
wantResult: framework.NewPostFilterResultWithNominatedNode(""),
wantStatus: fwk.NewStatus(fwk.Unschedulable, "preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod."),
},
{
name: "preemption should respect filteredNodesStatuses",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.UnschedulableAndUnresolvable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
wantResult: framework.NewPostFilterResultWithNominatedNode(""),
wantStatus: fwk.NewStatus(fwk.Unschedulable, "preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling."),
},
{
name: "preemption should respect absent NodeToStatusReader entry meaning UnschedulableAndUnresolvable",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
},
filteredNodesStatuses: framework.NewDefaultNodeToStatus(),
wantResult: framework.NewPostFilterResultWithNominatedNode(""),
wantStatus: fwk.NewStatus(fwk.Unschedulable, "preemption: 0/1 nodes are available: 1 Preemption is not helpful for scheduling."),
},
{
name: "pod can be made schedulable on one node",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(midPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Priority(highPriority).Node("node1").Obj(),
st.MakePod().Name("p2").UID("p2").Namespace(v1.NamespaceDefault).Priority(lowPriority).Node("node2").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
st.MakeNode().Name("node2").Capacity(onePodRes).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
"node2": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
wantResult: framework.NewPostFilterResultWithNominatedNode("node2"),
wantStatus: fwk.NewStatus(fwk.Success, "preemption: found a potential placement for pod on node node2, preempting 1 victims"),
},
{
name: "pod can be made schedulable on minHighestPriority node",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(veryHighPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Label(LabelKeyIsViolatingPDB, LabelValueNonViolatingPDB).Namespace(v1.NamespaceDefault).Priority(highPriority).Node("node1").Obj(),
st.MakePod().Name("p2").UID("p2").Label(LabelKeyIsViolatingPDB, LabelValueViolatingPDB).Namespace(v1.NamespaceDefault).Priority(lowPriority).Node("node1").Obj(),
st.MakePod().Name("p3").UID("p3").Label(LabelKeyIsViolatingPDB, LabelValueViolatingPDB).Namespace(v1.NamespaceDefault).Priority(midPriority).Node("node2").Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
st.MakePDB().Name("violating-pdb").Namespace(v1.NamespaceDefault).MatchLabel(LabelKeyIsViolatingPDB, LabelValueViolatingPDB).MinAvailable("100%").Obj(),
st.MakePDB().Name("non-violating-pdb").Namespace(v1.NamespaceDefault).MatchLabel(LabelKeyIsViolatingPDB, LabelValueNonViolatingPDB).MinAvailable("0").DisruptionsAllowed(math.MaxInt32).Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
st.MakeNode().Name("node2").Capacity(onePodRes).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
"node2": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
wantResult: framework.NewPostFilterResultWithNominatedNode("node2"),
wantStatus: fwk.NewStatus(fwk.Success, "preemption: found a potential placement for pod on node node2, preempting 1 victims"),
},
{
name: "preemption result filtered out by extenders",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
st.MakePod().Name("p2").UID("p2").Namespace(v1.NamespaceDefault).Node("node2").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
st.MakeNode().Name("node2").Capacity(onePodRes).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
"node2": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
extender: &tf.FakeExtender{
ExtenderName: "FakeExtender1",
Predicates: []tf.FitPredicate{tf.Node1PredicateExtender},
},
wantResult: framework.NewPostFilterResultWithNominatedNode("node1"),
wantStatus: fwk.NewStatus(fwk.Success, "preemption: found a potential placement for pod on node node1, preempting 1 victims"),
},
{
name: "no candidate nodes found, no enough resource after removing low priority pods",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
st.MakePod().Name("p2").UID("p2").Namespace(v1.NamespaceDefault).Node("node2").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(nodeRes).Obj(), // no enough CPU resource
st.MakeNode().Name("node2").Capacity(nodeRes).Obj(), // no enough CPU resource
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
"node2": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
wantResult: framework.NewPostFilterResultWithNominatedNode(""),
wantStatus: fwk.NewStatus(fwk.Unschedulable, "preemption: 0/2 nodes are available: 2 Insufficient cpu."),
},
{
name: "no candidate nodes found with mixed reasons, no lower priority pod and no enough CPU resource",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Priority(highPriority).Obj(),
st.MakePod().Name("p2").UID("p2").Namespace(v1.NamespaceDefault).Node("node2").Obj(),
st.MakePod().Name("p3").UID("p3").Namespace(v1.NamespaceDefault).Node("node3").Priority(highPriority).Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(), // no pod will be preempted
st.MakeNode().Name("node2").Capacity(nodeRes).Obj(), // no enough CPU resource
st.MakeNode().Name("node3").Capacity(onePodRes).Obj(), // no pod will be preempted
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
"node2": fwk.NewStatus(fwk.Unschedulable),
"node3": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
wantResult: framework.NewPostFilterResultWithNominatedNode(""),
wantStatus: fwk.NewStatus(fwk.Unschedulable, "preemption: 0/3 nodes are available: 1 Insufficient cpu, 2 No preemption victims found for incoming pod."),
},
{
name: "no candidate nodes found with mixed reason, 2 UnschedulableAndUnresolvable nodes and 2 nodes don't have enough CPU resource",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
st.MakePod().Name("p2").UID("p2").Namespace(v1.NamespaceDefault).Node("node2").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(nodeRes).Obj(),
st.MakeNode().Name("node2").Capacity(nodeRes).Obj(),
st.MakeNode().Name("node3").Capacity(nodeRes).Obj(),
st.MakeNode().Name("node4").Capacity(nodeRes).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
"node2": fwk.NewStatus(fwk.Unschedulable),
"node4": fwk.NewStatus(fwk.UnschedulableAndUnresolvable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
wantResult: framework.NewPostFilterResultWithNominatedNode(""),
wantStatus: fwk.NewStatus(fwk.Unschedulable, "preemption: 0/4 nodes are available: 2 Insufficient cpu, 2 Preemption is not helpful for scheduling."),
},
{
name: "only one node but failed with TestPlugin",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
},
// label the node with key as "error" so that the TestPlugin will fail with error.
nodes: []*v1.Node{st.MakeNode().Name("node1").Capacity(largeRes).Label("error", "true").Obj()},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
wantResult: nil,
wantStatus: fwk.AsStatus(errors.New("preemption: running RemovePod on PreFilter plugin \"test-plugin\": failed to remove pod: p")),
},
{
name: "one failed with TestPlugin and the other pass",
pod: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
st.MakePod().Name("p2").UID("p2").Namespace(v1.NamespaceDefault).Node("node2").Req(mediumRes).Obj(),
},
// even though node1 will fail with error but node2 will still be returned as a valid nominated node.
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(largeRes).Label("error", "true").Obj(),
st.MakeNode().Name("node2").Capacity(largeRes).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
"node2": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
wantResult: framework.NewPostFilterResultWithNominatedNode("node2"),
wantStatus: fwk.NewStatus(fwk.Success, "preemption: found a potential placement for pod on node node2, preempting 1 victims"),
},
{
name: "pod with SchedulingGroup with TAS with scheduling constraint enabled should not preempt",
pod: st.MakePod().Name("p-with-podgroup").Namespace(v1.NamespaceDefault).PodGroupName("foo").Priority(highPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("foo").Namespace(v1.NamespaceDefault).TopologyKey("rack").Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
features: feature.Features{EnableGenericWorkload: true, EnableTopologyAwareWorkloadScheduling: true},
wantResult: nil,
wantStatus: fwk.NewStatus(fwk.Unschedulable, "preemption: not eligible due to workload aware preemption enabled"),
},
{
name: "pod with SchedulingGroup with TAS without scheduling constraint enabled should not preempt",
pod: st.MakePod().Name("p-with-podgroup").Namespace(v1.NamespaceDefault).PodGroupName("foo").Priority(highPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("foo").Namespace(v1.NamespaceDefault).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
features: feature.Features{EnableGenericWorkload: true, EnableTopologyAwareWorkloadScheduling: true},
wantResult: nil,
wantStatus: fwk.NewStatus(fwk.Unschedulable, "preemption: not eligible due to workload aware preemption enabled"),
},
{
name: "pod with SchedulingGroup should not preempt",
pod: st.MakePod().Name("p-with-podgroup").PodGroupName("foo").Priority(highPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
features: feature.Features{EnableGenericWorkload: true},
wantResult: nil,
wantStatus: fwk.NewStatus(fwk.Unschedulable, "preemption: not eligible due to workload aware preemption enabled"),
},
{
name: "pod with SchedulingGroup with GenericWorkload and TAS disabled should preempt",
pod: st.MakePod().Name("p-with-podgroup").PodGroupName("foo").Priority(highPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
},
nodes: []*v1.Node{
st.MakeNode().Name("node1").Capacity(onePodRes).Obj(),
},
filteredNodesStatuses: framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable)),
features: feature.Features{EnableGenericWorkload: false, EnableTopologyAwareWorkloadScheduling: false},
wantResult: framework.NewPostFilterResultWithNominatedNode("node1"),
wantStatus: fwk.NewStatus(fwk.Success, "preemption: found a potential placement for pod on node node1, preempting 1 victims"),
},
}
for _, asyncAPICallsEnabled := range []bool{true, false} {
for _, tt := range tests {
t.Run(fmt.Sprintf("%s (Async API calls enabled: %v)", tt.name, asyncAPICallsEnabled), func(t *testing.T) {
// index the potential victim pods in the fake client so that the victims deletion logic does not fail
podItems := []v1.Pod{}
for _, pod := range tt.pods {
podItems = append(podItems, *pod)
}
cs := clientsetfake.NewClientset(&v1.PodList{Items: podItems})
informerFactory := informers.NewSharedInformerFactory(cs, 0)
podInformer := informerFactory.Core().V1().Pods().Informer()
if err := podInformer.GetStore().Add(tt.pod); err != nil {
t.Fatal(err)
}
for i := range tt.pods {
if err := podInformer.GetStore().Add(tt.pods[i]); err != nil {
t.Fatal(err)
}
}
pdbInformer := informerFactory.Policy().V1().PodDisruptionBudgets().Informer()
for i := range tt.pdbs {
if err := pdbInformer.GetStore().Add(tt.pdbs[i]); err != nil {
t.Fatal(err)
}
}
pgInformer := informerFactory.Scheduling().V1alpha3().PodGroups().Informer()
for i := range tt.podGroups {
if err := pgInformer.GetStore().Add(tt.podGroups[i]); err != nil {
t.Fatal(err)
}
}
// Register NodeResourceFit as the Filter & PreFilter plugin.
registeredPlugins := []tf.RegisterPluginFunc{
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
tf.RegisterPluginAsExtensions("test-plugin", newTestPlugin, "PreFilter"),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
}
var extenders []fwk.Extender
if tt.extender != nil {
extenders = append(extenders, tt.extender)
}
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var apiDispatcher *apidispatcher.APIDispatcher
if asyncAPICallsEnabled {
apiDispatcher = apidispatcher.New(cs, 16, apicalls.Relevances)
apiDispatcher.Run(logger)
defer apiDispatcher.Close()
}
cache := internalcache.New(ctx, apiDispatcher, tt.features.EnableGenericWorkload)
for _, podGroup := range tt.podGroups {
cache.AddPodGroup(podGroup)
}
snapshot := internalcache.NewTestSnapshotWithPodGroups(tt.pods, tt.nodes, tt.podGroups)
f, err := tf.NewFramework(ctx, registeredPlugins, "",
frameworkruntime.WithClientSet(cs),
frameworkruntime.WithAPIDispatcher(apiDispatcher),
frameworkruntime.WithEventRecorder(&events.FakeRecorder{}),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithPodNominator(internalqueue.NewSchedulingQueue(nil, informerFactory)),
frameworkruntime.WithExtenders(extenders),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithLogger(logger),
frameworkruntime.WithWaitingPods(frameworkruntime.NewWaitingPodsMap()),
frameworkruntime.WithPodsInPreBind(frameworkruntime.NewPodsInPreBindMap()),
frameworkruntime.WithPodGroupManager(cache),
)
if err != nil {
t.Fatal(err)
}
if asyncAPICallsEnabled {
f.SetAPICacher(apicache.New(nil, cache))
}
p, err := New(ctx, getDefaultDefaultPreemptionArgs(), f, tt.features)
if err != nil {
t.Fatal(err)
}
state := framework.NewCycleState()
// Ensure <state> is populated.
if _, status, _ := f.RunPreFilterPlugins(ctx, state, tt.pod); !status.IsSuccess() {
t.Errorf("Unexpected PreFilter Status: %v", status)
}
gotResult, gotStatus := p.PostFilter(ctx, state, tt.pod, tt.filteredNodesStatuses)
// As we cannot compare two errors directly due to miss the equal method for how to compare two errors, so just need to compare the reasons.
if gotStatus.Code() == fwk.Error {
if diff := cmp.Diff(tt.wantStatus.Reasons(), gotStatus.Reasons()); diff != "" {
t.Errorf("Unexpected status (-want, +got):\n%s", diff)
}
} else {
if diff := cmp.Diff(tt.wantStatus, gotStatus); diff != "" {
t.Errorf("Unexpected status (-want, +got):\n%s", diff)
}
}
if diff := cmp.Diff(tt.wantResult, gotResult); diff != "" {
t.Errorf("Unexpected postFilterResult (-want, +got):\n%s", diff)
}
})
}
}
}
type candidate struct {
victims *extenderv1.Victims
name string
}
func TestDryRunPreemption(t *testing.T) {
tests := []struct {
name string
args *config.DefaultPreemptionArgs
nodeNames []string
testPods []*v1.Pod
initPods []*v1.Pod
registerPlugins []tf.RegisterPluginFunc
pdbs []*policy.PodDisruptionBudget
fakeFilterRC fwk.Code // return code for fake filter plugin
disableParallelism bool
expected [][]candidate
expectedNumFilterCalled []int32
}{
{
name: "a pod that does not fit on any node",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterFilterPlugin("FalseFilter", tf.NewFalseFilterPlugin),
},
nodeNames: []string{"node1", "node2"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Obj(),
},
expected: [][]candidate{{}},
expectedNumFilterCalled: []int32{2},
},
{
name: "a pod that fits with no preemption",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterFilterPlugin("TrueFilter", tf.NewTrueFilterPlugin),
},
nodeNames: []string{"node1", "node2"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Obj(),
},
expected: [][]candidate{{}},
fakeFilterRC: fwk.Unschedulable,
expectedNumFilterCalled: []int32{2},
},
{
name: "a pod that fits on one node with no preemption",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterFilterPlugin("MatchFilter", tf.NewMatchFilterPlugin),
},
nodeNames: []string{"node1", "node2"},
testPods: []*v1.Pod{
// Name the pod as "node1" to fit "MatchFilter" plugin.
st.MakePod().Name("node1").UID("node1").Priority(highPriority).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Obj(),
},
expected: [][]candidate{{}},
fakeFilterRC: fwk.Unschedulable,
expectedNumFilterCalled: []int32{2},
},
{
name: "a pod that fits on both nodes when lower priority pods are preempted",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Req(largeRes).Obj(),
},
expected: [][]candidate{
{
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).Obj()},
},
name: "node1",
},
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Req(largeRes).Obj()},
},
name: "node2",
},
},
},
expectedNumFilterCalled: []int32{4},
},
{
name: "a pod that would fit on the nodes, but other pods running are higher priority, no preemption would happen",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(lowPriority).Req(largeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Req(largeRes).Obj(),
},
expected: [][]candidate{{}},
expectedNumFilterCalled: []int32{0},
},
{
name: "medium priority pod is preempted, but lower priority one stays as it is small",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Req(largeRes).Obj(),
},
expected: [][]candidate{
{
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(midPriority).Req(largeRes).Obj()},
},
name: "node1",
},
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Req(largeRes).Obj()},
},
name: "node2",
},
},
},
expectedNumFilterCalled: []int32{5},
},
{
name: "mixed priority pods are preempted",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(midPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.3").UID("p1.3").Node("node1").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.4").UID("p1.4").Node("node1").Priority(highPriority).Req(smallRes).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(highPriority).Req(largeRes).Obj(),
},
expected: [][]candidate{
{
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.3").UID("p1.3").Node("node1").Priority(midPriority).Req(mediumRes).Obj(),
},
},
name: "node1",
},
},
},
expectedNumFilterCalled: []int32{4},
},
{
name: "mixed priority pods are preempted, pick later StartTime one when priorities are equal",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(lowPriority).Req(smallRes).StartTime(epochTime5).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(lowPriority).Req(smallRes).StartTime(epochTime4).Obj(),
st.MakePod().Name("p1.3").UID("p1.3").Node("node1").Priority(midPriority).Req(mediumRes).StartTime(epochTime3).Obj(),
st.MakePod().Name("p1.4").UID("p1.4").Node("node1").Priority(highPriority).Req(smallRes).StartTime(epochTime2).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(highPriority).Req(largeRes).StartTime(epochTime1).Obj(),
},
expected: [][]candidate{
{
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(lowPriority).Req(smallRes).StartTime(epochTime5).Obj(),
st.MakePod().Name("p1.3").UID("p1.3").Node("node1").Priority(midPriority).Req(mediumRes).StartTime(epochTime3).Obj(),
},
},
name: "node1",
},
},
},
expectedNumFilterCalled: []int32{4}, // no preemption would happen on node2 and no filter call is counted.
},
{
name: "pod with anti-affinity is preempted",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
tf.RegisterPluginAsExtensions(interpodaffinity.Name, frameworkruntime.FactoryAdapter(feature.Features{}, interpodaffinity.New), "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Label("foo", "").Priority(highPriority).Req(smallRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Label("foo", "").Priority(lowPriority).Req(smallRes).
PodAntiAffinityExists("foo", "hostname", st.PodAntiAffinityWithRequiredReq).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(midPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.3").UID("p1.3").Node("node1").Priority(highPriority).Req(smallRes).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(highPriority).Req(smallRes).Obj(),
},
expected: [][]candidate{
{
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Label("foo", "").Priority(lowPriority).Req(smallRes).
PodAntiAffinityExists("foo", "hostname", st.PodAntiAffinityWithRequiredReq).Obj(),
},
},
name: "node1",
},
},
},
expectedNumFilterCalled: []int32{3}, // no preemption would happen on node2 and no filter call is counted.
},
{
name: "preemption to resolve pod topology spread filter failure",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(podtopologyspread.Name, podTopologySpreadFunc, "PreFilter", "Filter"),
},
nodeNames: []string{"node-a/zone1", "node-b/zone1", "node-x/zone2"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Label("foo", "").Priority(highPriority).
SpreadConstraint(1, "zone", v1.DoNotSchedule, st.MakeLabelSelector().Exists("foo").Obj(), nil, nil, nil, nil).
SpreadConstraint(1, "hostname", v1.DoNotSchedule, st.MakeLabelSelector().Exists("foo").Obj(), nil, nil, nil, nil).
Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("pod-a1").UID("pod-a1").Node("node-a").Label("foo", "").Priority(midPriority).Obj(),
st.MakePod().Name("pod-a2").UID("pod-a2").Node("node-a").Label("foo", "").Priority(lowPriority).Obj(),
st.MakePod().Name("pod-b1").UID("pod-b1").Node("node-b").Label("foo", "").Priority(lowPriority).Obj(),
st.MakePod().Name("pod-x1").UID("pod-x1").Node("node-x").Label("foo", "").Priority(highPriority).Obj(),
st.MakePod().Name("pod-x2").UID("pod-x2").Node("node-x").Label("foo", "").Priority(highPriority).Obj(),
},
expected: [][]candidate{
{
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("pod-a2").UID("pod-a2").Node("node-a").Label("foo", "").Priority(lowPriority).Obj()},
},
name: "node-a",
},
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("pod-b1").UID("pod-b1").Node("node-b").Label("foo", "").Priority(lowPriority).Obj()},
},
name: "node-b",
},
},
},
expectedNumFilterCalled: []int32{5}, // node-a (3), node-b (2), node-x (0)
},
{
name: "get Unschedulable in the preemption phase when the filter plugins filtering the nodes",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Req(largeRes).Obj(),
},
fakeFilterRC: fwk.Unschedulable,
expected: [][]candidate{{}},
expectedNumFilterCalled: []int32{2},
},
{
name: "preemption with violation of same pdb",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 1},
},
},
expected: [][]candidate{
{
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
},
NumPDBViolations: 1,
},
name: "node1",
},
},
},
expectedNumFilterCalled: []int32{3},
},
{
name: "preemption with violation of the pdb with pod whose eviction was processed, the victim doesn't belong to DisruptedPods",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 1, DisruptedPods: map[string]metav1.Time{"p2": {Time: time.Now()}}},
},
},
expected: [][]candidate{
{
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
},
NumPDBViolations: 1,
},
name: "node1",
},
},
},
expectedNumFilterCalled: []int32{3},
},
{
name: "preemption with violation of the pdb with pod whose eviction was processed, the victim belongs to DisruptedPods",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 1, DisruptedPods: map[string]metav1.Time{"p1.2": {Time: time.Now()}}},
},
},
expected: [][]candidate{
{
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
},
NumPDBViolations: 0,
},
name: "node1",
},
},
},
expectedNumFilterCalled: []int32{3},
},
{
name: "preemption with violation of the pdb with pod whose eviction was processed, the victim which belongs to DisruptedPods is treated as 'nonViolating'",
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.3").UID("p1.3").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 1, DisruptedPods: map[string]metav1.Time{"p1.3": {Time: time.Now()}}},
},
},
expected: [][]candidate{
{
candidate{
victims: &extenderv1.Victims{
Pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
st.MakePod().Name("p1.3").UID("p1.3").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
},
NumPDBViolations: 1,
},
name: "node1",
},
},
},
expectedNumFilterCalled: []int32{4},
},
{
name: "all nodes are possible candidates, but DefaultPreemptionArgs limits to 2",
args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 40, MinCandidateNodesAbsolute: 1},
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2", "node3", "node4", "node5"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p3").UID("p3").Node("node3").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p4").UID("p4").Node("node4").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p5").UID("p5").Node("node5").Priority(midPriority).Req(largeRes).Obj(),
},
disableParallelism: true,
expected: [][]candidate{
{
// cycle=0 => offset=4 => node5 (yes), node1 (yes)
candidate{
name: "node1",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).Obj()},
},
},
candidate{
name: "node5",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p5").UID("p5").Node("node5").Priority(midPriority).Req(largeRes).Obj()},
},
},
},
},
expectedNumFilterCalled: []int32{4},
},
{
name: "some nodes are not possible candidates, DefaultPreemptionArgs limits to 2",
args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 40, MinCandidateNodesAbsolute: 1},
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2", "node3", "node4", "node5"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(veryHighPriority).Req(largeRes).Obj(),
st.MakePod().Name("p3").UID("p3").Node("node3").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p4").UID("p4").Node("node4").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p5").UID("p5").Node("node5").Priority(veryHighPriority).Req(largeRes).Obj(),
},
disableParallelism: true,
expected: [][]candidate{
{
// cycle=0 => offset=4 => node5 (no), node1 (yes), node2 (no), node3 (yes)
candidate{
name: "node1",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).Obj()},
},
},
candidate{
name: "node3",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p3").UID("p3").Node("node3").Priority(midPriority).Req(largeRes).Obj()},
},
},
},
},
expectedNumFilterCalled: []int32{4},
},
{
name: "preemption offset across multiple scheduling cycles and wrap around",
args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 40, MinCandidateNodesAbsolute: 1},
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2", "node3", "node4", "node5"},
testPods: []*v1.Pod{
st.MakePod().Name("tp1").UID("tp1").Priority(highPriority).Req(largeRes).Obj(),
st.MakePod().Name("tp2").UID("tp2").Priority(highPriority).Req(largeRes).Obj(),
st.MakePod().Name("tp3").UID("tp3").Priority(highPriority).Req(largeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p3").UID("p3").Node("node3").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p4").UID("p4").Node("node4").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p5").UID("p5").Node("node5").Priority(midPriority).Req(largeRes).Obj(),
},
disableParallelism: true,
expected: [][]candidate{
{
// cycle=0 => offset=4 => node5 (yes), node1 (yes)
candidate{
name: "node1",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).Obj()},
},
},
candidate{
name: "node5",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p5").UID("p5").Node("node5").Priority(midPriority).Req(largeRes).Obj()},
},
},
},
{
// cycle=1 => offset=1 => node2 (yes), node3 (yes)
candidate{
name: "node2",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Req(largeRes).Obj()},
},
},
candidate{
name: "node3",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p3").UID("p3").Node("node3").Priority(midPriority).Req(largeRes).Obj()},
},
},
},
{
// cycle=2 => offset=3 => node4 (yes), node5 (yes)
candidate{
name: "node4",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p4").UID("p4").Node("node4").Priority(midPriority).Req(largeRes).Obj()},
},
},
candidate{
name: "node5",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p5").UID("p5").Node("node5").Priority(midPriority).Req(largeRes).Obj()},
},
},
},
},
expectedNumFilterCalled: []int32{4, 4, 4},
},
{
name: "preemption looks past numCandidates until a non-PDB violating node is found",
args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 40, MinCandidateNodesAbsolute: 2},
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
},
nodeNames: []string{"node1", "node2", "node3", "node4", "node5"},
testPods: []*v1.Pod{
st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Label("app", "foo").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Label("app", "foo").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p3").UID("p3").Node("node3").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p4").UID("p4").Node("node4").Priority(midPriority).Req(largeRes).Obj(),
st.MakePod().Name("p5").UID("p5").Node("node5").Label("app", "foo").Priority(midPriority).Req(largeRes).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 0},
},
},
disableParallelism: true,
expected: [][]candidate{
{
// Even though the DefaultPreemptionArgs constraints suggest that the
// minimum number of candidates is 2, we get three candidates here
// because we're okay with being a little over (in production, if a
// non-PDB violating candidate isn't found close to the offset, the
// number of additional candidates returned will be at most
// approximately equal to the parallelism in dryRunPreemption).
// cycle=0 => offset=4 => node5 (yes, pdb), node1 (yes, pdb), node2 (no, pdb), node3 (yes)
candidate{
name: "node1",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p1").UID("p1").Node("node1").Label("app", "foo").Priority(midPriority).Req(largeRes).Obj()},
NumPDBViolations: 1,
},
},
candidate{
name: "node3",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p3").UID("p3").Node("node3").Priority(midPriority).Req(largeRes).Obj()},
},
},
candidate{
name: "node5",
victims: &extenderv1.Victims{
Pods: []*v1.Pod{st.MakePod().Name("p5").UID("p5").Node("node5").Label("app", "foo").Priority(midPriority).Req(largeRes).Obj()},
NumPDBViolations: 1,
},
},
},
},
expectedNumFilterCalled: []int32{8},
},
}
labelKeys := []string{"hostname", "zone", "region"}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nodes := make([]*v1.Node, len(tt.nodeNames))
fakeFilterRCMap := make(map[string]fwk.Code, len(tt.nodeNames))
for i, nodeName := range tt.nodeNames {
nodeWrapper := st.MakeNode().Capacity(veryLargeRes)
// Split node name by '/' to form labels in a format of
// {"hostname": tpKeys[0], "zone": tpKeys[1], "region": tpKeys[2]}
tpKeys := strings.Split(nodeName, "/")
nodeWrapper.Name(tpKeys[0])
for i, labelVal := range strings.Split(nodeName, "/") {
nodeWrapper.Label(labelKeys[i], labelVal)
}
nodes[i] = nodeWrapper.Obj()
fakeFilterRCMap[nodeName] = tt.fakeFilterRC
}
snapshot := internalcache.NewSnapshot(tt.initPods, nodes)
// For each test, register a FakeFilterPlugin along with essential plugins and tt.registerPlugins.
fakePlugin := tf.FakeFilterPlugin{
FailedNodeReturnCodeMap: fakeFilterRCMap,
}
registeredPlugins := append([]tf.RegisterPluginFunc{
tf.RegisterFilterPlugin(
"FakeFilter",
func(_ context.Context, _ runtime.Object, fh fwk.Handle) (fwk.Plugin, error) {
return &fakePlugin, nil
},
)},
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
)
registeredPlugins = append(registeredPlugins, tt.registerPlugins...)
var objs []runtime.Object
for _, p := range append(tt.testPods, tt.initPods...) {
objs = append(objs, p)
}
for _, n := range nodes {
objs = append(objs, n)
}
informerFactory := informers.NewSharedInformerFactory(clientsetfake.NewClientset(objs...), 0)
parallelism := parallelize.DefaultParallelism
if tt.disableParallelism {
// We need disableParallelism because of the non-deterministic nature
// of the results of tests that set custom minCandidateNodesPercentage
// or minCandidateNodesAbsolute. This is only done in a handful of tests.
parallelism = 1
}
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
fwk, err := tf.NewFramework(
ctx,
registeredPlugins, "",
frameworkruntime.WithPodNominator(internalqueue.NewSchedulingQueue(nil, informerFactory)),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithParallelism(parallelism),
frameworkruntime.WithLogger(logger),
)
if err != nil {
t.Fatal(err)
}
nodeInfos, err := snapshot.NodeInfos().List()
if err != nil {
t.Fatal(err)
}
sort.Slice(nodeInfos, func(i, j int) bool {
return nodeInfos[i].Node().Name < nodeInfos[j].Node().Name
})
if tt.args == nil {
tt.args = getDefaultDefaultPreemptionArgs()
}
pl, err := New(ctx, tt.args, fwk, feature.Features{})
if err != nil {
t.Fatal(err)
}
// Using 4 as a seed source to test getOffsetAndNumCandidates() deterministically.
// However, we need to do it after informerFactory.WaitforCacheSync() which might
// set a seed.
getOffsetRand = rand.New(rand.NewSource(4)).Int31n
var prevNumFilterCalled int32
for cycle, pod := range tt.testPods {
state := framework.NewCycleState()
// Some tests rely on PreFilter plugin to compute its CycleState.
if _, status, _ := fwk.RunPreFilterPlugins(ctx, state, pod); !status.IsSuccess() {
t.Errorf("cycle %d: Unexpected PreFilter Status: %v", cycle, status)
}
offset, numCandidates := pl.GetOffsetAndNumCandidates(int32(len(nodeInfos)))
got, _, _ := pl.Evaluator.DryRunPreemption(ctx, state, pod, nodeInfos, tt.pdbs, offset, numCandidates)
// Sort the values (inner victims) and the candidate itself (by its NominatedNodeName).
for i := range got {
victims := got[i].Victims().Pods
sort.Slice(victims, func(i, j int) bool {
return victims[i].Name < victims[j].Name
})
}
sort.Slice(got, func(i, j int) bool {
return got[i].Name() < got[j].Name()
})
candidates := []candidate{}
for i := range got {
candidates = append(candidates, candidate{victims: got[i].Victims(), name: got[i].Name()})
}
if fakePlugin.NumFilterCalled-prevNumFilterCalled != tt.expectedNumFilterCalled[cycle] {
t.Errorf("cycle %d: got NumFilterCalled=%d, want %d", cycle, fakePlugin.NumFilterCalled-prevNumFilterCalled, tt.expectedNumFilterCalled[cycle])
}
prevNumFilterCalled = fakePlugin.NumFilterCalled
if diff := cmp.Diff(tt.expected[cycle], candidates, cmp.AllowUnexported(candidate{})); diff != "" {
t.Errorf("cycle %d: unexpected candidates (-want, +got): %s", cycle, diff)
}
}
})
}
}
func TestSelectBestCandidate(t *testing.T) {
tests := []struct {
name string
registerPlugin tf.RegisterPluginFunc
nodeNames []string
pod *v1.Pod
pods []*v1.Pod
expected []string // any of the items is valid
}{
{
name: "a pod that fits on both nodes when lower priority pods are preempted",
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
nodeNames: []string{"node1", "node2"},
pod: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p2").UID("p2").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
expected: []string{"node1", "node2"},
},
{
name: "node with min highest priority pod is picked",
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(midPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Node("node2").Priority(midPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p2.2").UID("p2.2").Node("node2").Priority(lowPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Node("node3").Priority(lowPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.2").UID("p3.2").Node("node3").Priority(lowPriority).Req(mediumRes).StartTime(epochTime).Obj(),
},
expected: []string{"node3"},
},
{
name: "when highest priorities are the same, minimum sum of priorities is picked",
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(midPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p2.2").UID("p2.2").Node("node2").Priority(lowPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Node("node3").Priority(midPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.2").UID("p3.2").Node("node3").Priority(midPriority).Req(mediumRes).StartTime(epochTime).Obj(),
},
expected: []string{"node2"},
},
{
name: "when highest priority and sum are the same, minimum number of pods is picked",
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(midPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(negPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p1.3").UID("p1.3").Node("node1").Priority(midPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p1.4").UID("p1.4").Node("node1").Priority(negPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p2.2").UID("p2.2").Node("node2").Priority(negPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Node("node3").Priority(midPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.2").UID("p3.2").Node("node3").Priority(negPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.3").UID("p3.3").Node("node3").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
},
expected: []string{"node2"},
},
{
// pickOneNodeForPreemption adjusts pod priorities when finding the sum of the victims. This
// test ensures that the logic works correctly.
name: "sum of adjusted priorities is considered",
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(midPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(negPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p1.3").UID("p1.3").Node("node1").Priority(negPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p2.2").UID("p2.2").Node("node2").Priority(negPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Node("node3").Priority(midPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.2").UID("p3.2").Node("node3").Priority(negPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.3").UID("p3.3").Node("node3").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
},
expected: []string{"node2"},
},
{
name: "non-overlapping lowest high priority, sum priorities, and number of pods",
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
nodeNames: []string{"node1", "node2", "node3", "node4"},
pod: st.MakePod().Name("p").UID("p").Priority(veryHighPriority).Req(veryLargeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(midPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p1.3").UID("p1.3").Node("node1").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Node("node2").Priority(highPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Node("node3").Priority(midPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.2").UID("p3.2").Node("node3").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.3").UID("p3.3").Node("node3").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p3.4").UID("p3.4").Node("node3").Priority(lowPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p4.1").UID("p4.1").Node("node4").Priority(midPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p4.2").UID("p4.2").Node("node4").Priority(midPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p4.3").UID("p4.3").Node("node4").Priority(midPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("p4.4").UID("p4.4").Node("node4").Priority(negPriority).Req(smallRes).StartTime(epochTime).Obj(),
},
expected: []string{"node1"},
},
{
name: "same priority, same number of victims, different start time for each node's pod",
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(midPriority).Req(mediumRes).StartTime(epochTime2).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(midPriority).Req(mediumRes).StartTime(epochTime2).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Node("node2").Priority(midPriority).Req(mediumRes).StartTime(epochTime3).Obj(),
st.MakePod().Name("p2.2").UID("p2.2").Node("node2").Priority(midPriority).Req(mediumRes).StartTime(epochTime3).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Node("node3").Priority(midPriority).Req(mediumRes).StartTime(epochTime1).Obj(),
st.MakePod().Name("p3.2").UID("p3.2").Node("node3").Priority(midPriority).Req(mediumRes).StartTime(epochTime1).Obj(),
},
expected: []string{"node2"},
},
{
name: "same priority, same number of victims, different start time for all pods",
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(midPriority).Req(mediumRes).StartTime(epochTime4).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(midPriority).Req(mediumRes).StartTime(epochTime2).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Node("node2").Priority(midPriority).Req(mediumRes).StartTime(epochTime5).Obj(),
st.MakePod().Name("p2.2").UID("p2.2").Node("node2").Priority(midPriority).Req(mediumRes).StartTime(epochTime1).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Node("node3").Priority(midPriority).Req(mediumRes).StartTime(epochTime3).Obj(),
st.MakePod().Name("p3.2").UID("p3.2").Node("node3").Priority(midPriority).Req(mediumRes).StartTime(epochTime6).Obj(),
},
expected: []string{"node3"},
},
{
name: "different priority, same number of victims, different start time for all pods",
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(veryLargeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(lowPriority).Req(mediumRes).StartTime(epochTime4).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(midPriority).Req(mediumRes).StartTime(epochTime2).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Node("node2").Priority(midPriority).Req(mediumRes).StartTime(epochTime6).Obj(),
st.MakePod().Name("p2.2").UID("p2.2").Node("node2").Priority(lowPriority).Req(mediumRes).StartTime(epochTime1).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Node("node3").Priority(lowPriority).Req(mediumRes).StartTime(epochTime3).Obj(),
st.MakePod().Name("p3.2").UID("p3.2").Node("node3").Priority(midPriority).Req(mediumRes).StartTime(epochTime5).Obj(),
},
expected: []string{"node2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
getOffsetRand = rand.New(rand.NewSource(4)).Int31n
nodes := make([]*v1.Node, len(tt.nodeNames))
for i, nodeName := range tt.nodeNames {
nodes[i] = st.MakeNode().Name(nodeName).Capacity(veryLargeRes).Obj()
}
var objs []runtime.Object
objs = append(objs, tt.pod)
for _, pod := range tt.pods {
objs = append(objs, pod)
}
cs := clientsetfake.NewClientset(objs...)
informerFactory := informers.NewSharedInformerFactory(cs, 0)
snapshot := internalcache.NewSnapshot(tt.pods, nodes)
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
fwk, err := tf.NewFramework(
ctx,
[]tf.RegisterPluginFunc{
tt.registerPlugin,
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
},
"",
frameworkruntime.WithPodNominator(internalqueue.NewSchedulingQueue(nil, informerFactory)),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithLogger(logger),
)
if err != nil {
t.Fatal(err)
}
nodeInfos, err := fwk.SnapshotSharedLister().NodeInfos().List()
if err != nil {
t.Fatal(err)
}
state := framework.NewCycleState()
// Some tests rely on PreFilter plugin to compute its CycleState.
if _, status, _ := fwk.RunPreFilterPlugins(ctx, state, tt.pod); !status.IsSuccess() {
t.Errorf("Unexpected PreFilter Status: %v", status)
}
pl, err := New(ctx, getDefaultDefaultPreemptionArgs(), fwk, feature.Features{})
if err != nil {
t.Fatal(err)
}
offset, numCandidates := pl.GetOffsetAndNumCandidates(int32(len(nodeInfos)))
candidates, _, _ := pl.Evaluator.DryRunPreemption(ctx, state, tt.pod, nodeInfos, nil, offset, numCandidates)
s := pl.Evaluator.SelectCandidate(ctx, candidates)
if s == nil || len(s.Name()) == 0 {
t.Fatalf("expected any node in %v, but candidate is missing", tt.expected)
}
found := false
for _, nodeName := range tt.expected {
if nodeName == s.Name() {
found = true
break
}
}
if !found {
t.Errorf("expect any node in %v, but got %v", tt.expected, s.Name())
}
})
}
}
func TestCustomSelection(t *testing.T) {
victimLabelsAreEligible := func(key, val string) IsEligiblePodFunc {
return func(nodeInfo fwk.NodeInfo, victim preemption.Victim, preemptor *v1.Pod) bool {
for _, pod := range victim.Pods() {
pval, ok := pod.GetPod().Labels[key]
if !ok {
return false
}
if pval != val {
return false
}
}
return true
}
}
nodeNameIsEligible := func(name string) IsEligiblePodFunc {
return func(nodeInfo fwk.NodeInfo, victim preemption.Victim, preemptor *v1.Pod) bool {
return nodeInfo.Node().Name == name
}
}
priorityBelowThresholdCannotPreempt := func(minPreempting int32) IsEligiblePodFunc {
return func(nodeInfo fwk.NodeInfo, victim preemption.Victim, preemptor *v1.Pod) bool {
return corev1helpers.PodPriority(preemptor) >= minPreempting
}
}
priorityAboveThresholdCannotBePreempted := func(maxPreemptible int32) IsEligiblePodFunc {
return func(nodeInfo fwk.NodeInfo, victim preemption.Victim, preemptor *v1.Pod) bool {
return victim.Priority() <= maxPreemptible
}
}
tests := []struct {
name string
eligiblePods IsEligiblePodFunc
nodeNames []string
pod *v1.Pod
pods []*v1.Pod
podGroups []*v1alpha3.PodGroup
features feature.Features
expected map[string][]string
}{
{
name: "filter for matching pod label: high priority",
eligiblePods: victimLabelsAreEligible("preemptible", "yes"),
nodeNames: []string{"node1", "node2", "node3", "node4"},
pod: st.MakePod().Name("p1").UID("p1").Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Label("preemptible", "no").Node("node1").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Label("preemptible", "yes").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Label("preemptible", "yes").Node("node3").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v4").UID("v4").Node("node4").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
expected: map[string][]string{"node2": {"v2"}, "node3": {"v3"}},
},
{
name: "filter for matching pod label: mid priority",
eligiblePods: victimLabelsAreEligible("preemptible", "yes"),
nodeNames: []string{"node1", "node2", "node3", "node4"},
pod: st.MakePod().Name("p2").UID("p2").Priority(midPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Label("preemptible", "no").Node("node1").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Label("preemptible", "yes").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Label("preemptible", "yes").Node("node3").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v4").UID("v4").Node("node4").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
expected: map[string][]string{"node3": {"v3"}},
},
{
name: "filter for matching pod label: low priority",
eligiblePods: victimLabelsAreEligible("preemptible", "yes"),
nodeNames: []string{"node1", "node2", "node3", "node4"},
pod: st.MakePod().Name("p3").UID("p3").Priority(lowPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Label("preemptible", "no").Node("node1").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Label("preemptible", "yes").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Label("preemptible", "yes").Node("node3").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v4").UID("v4").Node("node4").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
expected: map[string][]string{},
},
{
name: "filter for matching victim node: high priority",
eligiblePods: nodeNameIsEligible("node1"),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p3").UID("p3").Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Node("node1").Priority(highPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Node("node1").Priority(midPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Node("node1").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v4").UID("v4").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v5").UID("v5").Node("node3").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
expected: map[string][]string{"node1": {"v2", "v3"}},
},
{
name: "filter for matching victim node: mid priority",
eligiblePods: nodeNameIsEligible("node1"),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p3").UID("p3").Priority(midPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Node("node1").Priority(highPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Node("node1").Priority(midPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Node("node1").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v4").UID("v4").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v5").UID("v5").Node("node3").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
expected: map[string][]string{"node1": {"v3"}},
},
{
name: "filter for matching victim node: low priority",
eligiblePods: nodeNameIsEligible("node1"),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p3").UID("p3").Priority(lowPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Node("node1").Priority(highPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Node("node1").Priority(midPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Node("node1").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v4").UID("v4").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v5").UID("v5").Node("node3").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
expected: map[string][]string{},
},
{
name: "only pods at or above specified priority can preempted: high priority",
eligiblePods: priorityBelowThresholdCannotPreempt(highPriority),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p1").UID("p1").Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Node("node1").Priority(highPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Node("node3").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
// highPriority can preempt anything, but not other highPriority
expected: map[string][]string{"node2": {"v2"}, "node3": {"v3"}},
},
{
name: "only pods at or above specified priority can preempted: mid priority",
eligiblePods: priorityBelowThresholdCannotPreempt(highPriority),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p2").UID("p2").Priority(midPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Node("node1").Priority(highPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Node("node3").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
// midPriority can't preempt anything
expected: map[string][]string{},
},
{
name: "only pods at or below specified priority can be preempted: high priority",
eligiblePods: priorityAboveThresholdCannotBePreempted(midPriority),
nodeNames: []string{"node1", "node2", "node3"},
pod: st.MakePod().Name("p1").UID("p1").Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Node("node1").Priority(highPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Node("node2").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Node("node3").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
// the lowPriority and midPriority pods can be preempted but not the highPriority pod
expected: map[string][]string{"node2": {"v2"}, "node3": {"v3"}},
},
{
name: "filter for matching pod group: all affected nodes eligible",
eligiblePods: victimLabelsAreEligible("preemptible", "yes"),
nodeNames: []string{"node1", "node2"},
pod: st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Namespace(v1.NamespaceDefault).Node("node1").Label("preemptible", "yes").PodGroupName("pg1").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Namespace(v1.NamespaceDefault).Node("node2").Label("preemptible", "yes").PodGroupName("pg1").Priority(lowPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).DisruptionModeAll().Obj(),
},
features: feature.Features{EnableGenericWorkload: true},
expected: map[string][]string{"node1": {"v1", "v2"}, "node2": {"v1", "v2"}},
},
{
name: "filter for matching pod group: one node ineligible rejects entire pod group",
eligiblePods: nodeNameIsEligible("node1"),
nodeNames: []string{"node1", "node2"},
pod: st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg1").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Namespace(v1.NamespaceDefault).Node("node2").PodGroupName("pg1").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Namespace(v1.NamespaceDefault).Node("node1").Priority(midPriority).Req(largeRes).StartTime(epochTime).Obj(),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).DisruptionModeAll().Obj(),
},
features: feature.Features{EnableGenericWorkload: true},
expected: map[string][]string{"node1": {"v3"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate,
featuregatetesting.FeatureOverrides{
features.GenericWorkload: tt.features.EnableGenericWorkload,
})
nodes := make([]*v1.Node, len(tt.nodeNames))
for i, nodeName := range tt.nodeNames {
nodes[i] = st.MakeNode().Name(nodeName).Capacity(veryLargeRes).Obj()
}
var objs []runtime.Object
objs = append(objs, tt.pod)
for _, pod := range tt.pods {
objs = append(objs, pod)
}
for _, pg := range tt.podGroups {
objs = append(objs, pg)
}
for _, node := range nodes {
objs = append(objs, node)
}
cs := clientsetfake.NewClientset(objs...)
informerFactory := informers.NewSharedInformerFactory(cs, 0)
_ = informerFactory.Scheduling().V1alpha3().PodGroups().Informer()
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
informerFactory.Start(ctx.Done())
informerFactory.WaitForCacheSync(ctx.Done())
cache := internalcache.New(ctx, nil, tt.features.EnableGenericWorkload)
for _, pg := range tt.podGroups {
cache.AddPodGroup(pg)
}
snapshot := internalcache.NewTestSnapshotWithPodGroups(tt.pods, nodes, tt.podGroups)
fwk, err := tf.NewFramework(
ctx,
[]tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
},
"",
frameworkruntime.WithPodNominator(internalqueue.NewSchedulingQueue(nil, informerFactory)),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithLogger(logger),
frameworkruntime.WithPodGroupManager(cache),
)
if err != nil {
t.Fatal(err)
}
state := framework.NewCycleState()
// Some tests rely on PreFilter plugin to compute its CycleState.
if _, status, _ := fwk.RunPreFilterPlugins(ctx, state, tt.pod); !status.IsSuccess() {
t.Errorf("Unexpected PreFilter Status: %v", status)
}
nodeInfos, err := snapshot.NodeInfos().List()
if err != nil {
t.Fatal(err)
}
pl, err := New(ctx, getDefaultDefaultPreemptionArgs(), fwk, tt.features)
if err != nil {
t.Fatal(err)
}
// Override eligibility logic
if tt.eligiblePods != nil {
pl.IsEligiblePod = tt.eligiblePods
}
offset, numCandidates := pl.GetOffsetAndNumCandidates(int32(len(nodeInfos)))
candidates, _, _ := pl.Evaluator.DryRunPreemption(ctx, state, tt.pod, nodeInfos, nil, offset, numCandidates)
// check that the candidates match what's expected
if len(tt.expected) != len(candidates) {
candidateNames := []string{}
for _, c := range candidates {
candidateNames = append(candidateNames, c.Name())
}
t.Fatalf("expected %d candidates (%+v) but got %d: %+v", len(tt.expected), tt.expected, len(candidates), candidateNames)
}
for len(candidates) > 0 {
selected := pl.Evaluator.SelectCandidate(ctx, candidates)
expectVictims, ok := tt.expected[selected.Name()]
if !ok {
t.Fatalf("got unexpected candidate %+v, when expected is %+v", selected, tt.expected)
}
gotVictims := []string{}
for _, p := range selected.Victims().Pods {
gotVictims = append(gotVictims, p.Name)
}
sort.Strings(gotVictims)
sort.Strings(expectVictims)
if diff := cmp.Diff(expectVictims, gotVictims); diff != "" {
t.Errorf("Unexpected victims on node %s (-want,+got):\n%s", selected.Name(), diff)
}
// remove selected from candidates
notSelected := []preemption.Candidate{}
for _, c := range candidates {
if c.Name() != selected.Name() {
notSelected = append(notSelected, c)
}
}
candidates = notSelected
}
})
}
}
func TestCustomOrdering(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.GenericWorkload, true)
// Two arbitrary examples of custom selection ordering to check that they behave as expected
orderByOldestStart := func(pod1, pod2 *v1.Pod) bool {
return util.GetPodStartTime(pod1).Before(util.GetPodStartTime(pod2))
}
orderByOldestStartVictim := func(vi1, vi2 preemption.Victim) bool {
sort.Slice(vi1.Pods(), func(i, j int) bool {
return orderByOldestStart(vi1.Pods()[i].GetPod(), vi1.Pods()[j].GetPod())
})
sort.Slice(vi2.Pods(), func(i, j int) bool {
return orderByOldestStart(vi2.Pods()[i].GetPod(), vi2.Pods()[j].GetPod())
})
return util.GetPodStartTime(vi1.Pods()[0].GetPod()).Before(util.GetPodStartTime(vi2.Pods()[0].GetPod()))
}
orderByPodNameVictim := func(vi1, vi2 preemption.Victim) bool {
sort.Slice(vi1.Pods(), func(i, j int) bool {
return vi1.Pods()[i].GetPod().Name < vi1.Pods()[j].GetPod().Name
})
sort.Slice(vi2.Pods(), func(i, j int) bool {
return vi2.Pods()[i].GetPod().Name < vi2.Pods()[j].GetPod().Name
})
return vi1.Pods()[0].GetPod().Name < vi2.Pods()[0].GetPod().Name
}
orderByStandaloneOverPodGroupVictim := func(vi1, vi2 preemption.Victim) bool {
sort.Slice(vi1.Pods(), func(i, j int) bool {
return vi1.Pods()[i].GetPod().Name < vi1.Pods()[j].GetPod().Name
})
sort.Slice(vi2.Pods(), func(i, j int) bool {
return vi2.Pods()[i].GetPod().Name < vi2.Pods()[j].GetPod().Name
})
if vi1.IsPodGroup() != vi2.IsPodGroup() {
return !vi1.IsPodGroup()
}
return preemption.MoreImportantVictim(vi1, vi2, true)
}
orderBySmallerPodGroupOverLargerVictim := func(vi1, vi2 preemption.Victim) bool {
sort.Slice(vi1.Pods(), func(i, j int) bool {
return vi1.Pods()[i].GetPod().Name < vi1.Pods()[j].GetPod().Name
})
sort.Slice(vi2.Pods(), func(i, j int) bool {
return vi2.Pods()[i].GetPod().Name < vi2.Pods()[j].GetPod().Name
})
if vi1.IsPodGroup() && vi2.IsPodGroup() && len(vi1.Pods()) != len(vi2.Pods()) {
return len(vi1.Pods()) < len(vi2.Pods())
}
return preemption.MoreImportantVictim(vi1, vi2, true)
}
orderByPodGroupNameVictim := func(vi1, vi2 preemption.Victim) bool {
sort.Slice(vi1.Pods(), func(i, j int) bool {
return vi1.Pods()[i].GetPod().Name < vi1.Pods()[j].GetPod().Name
})
sort.Slice(vi2.Pods(), func(i, j int) bool {
return vi2.Pods()[i].GetPod().Name < vi2.Pods()[j].GetPod().Name
})
if vi1.IsPodGroup() && vi2.IsPodGroup() {
pg1 := vi1.Pods()[0].GetPod().Spec.SchedulingGroup.PodGroupName
pg2 := vi2.Pods()[0].GetPod().Spec.SchedulingGroup.PodGroupName
if pg1 != nil && pg2 != nil && *pg1 != *pg2 {
return *pg1 < *pg2
}
}
return preemption.MoreImportantVictim(vi1, vi2, true)
}
tests := []struct {
name string
orderVictims MoreImportantVictimFunc
nodeNames []string
preemptor *v1.Pod
pods []*v1.Pod
podGroups []*v1alpha3.PodGroup
expectedPods []string
}{
{
name: "select newest pods",
orderVictims: orderByOldestStartVictim,
nodeNames: []string{"node1"},
preemptor: st.MakePod().Name("p2").UID("p2").Priority(highPriority).Req(largeRes).Obj(),
// size victims to require at least two to be preempted
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Node("node1").Priority(lowPriority).Req(mediumRes).StartTime(epochTime2).Obj(),
st.MakePod().Name("v2").UID("v2").Node("node1").Priority(lowPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Node("node1").Priority(lowPriority).Req(mediumRes).StartTime(epochTime1).Obj(),
},
// the two newest pods (v1, v3) are selected; v2 (oldest) is reprieved
expectedPods: []string{"v3", "v1"},
},
{
name: "select alphabetically-last pods",
orderVictims: orderByPodNameVictim,
nodeNames: []string{"node1"},
preemptor: st.MakePod().Name("p2").UID("p2").Priority(highPriority).Req(largeRes).Obj(),
// size victims to require at least two to be preempted
pods: []*v1.Pod{
st.MakePod().Name("foo").UID("v1").Node("node1").Priority(lowPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("bar").UID("v2").Node("node1").Priority(lowPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("baz").UID("v3").Node("node1").Priority(midPriority).Req(mediumRes).StartTime(epochTime).Obj(),
},
// the last pods in alphabetic order are selected, despite one with higher priority
expectedPods: []string{"baz", "foo"},
},
{
name: "select pod group over standalone pod due to custom ordering",
orderVictims: orderByStandaloneOverPodGroupVictim,
nodeNames: []string{"node1"},
preemptor: st.MakePod().Name("p2").UID("p2").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Namespace(v1.NamespaceDefault).Node("node1").Priority(lowPriority).Req(mediumRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg1").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg1").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).DisruptionModeAll().Obj(),
},
// v1 (standalone) is reprieved because it is more important than pg1 in this custom ordering; pg1 pods (v2, v3) are selected
expectedPods: []string{"v2", "v3"},
},
{
name: "select larger pod group over smaller pod group due to custom ordering",
orderVictims: orderBySmallerPodGroupOverLargerVictim,
nodeNames: []string{"node1"},
preemptor: st.MakePod().Name("p2").UID("p2").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg-small").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v2").UID("v2").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg-large").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
st.MakePod().Name("v3").UID("v3").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg-large").Priority(lowPriority).Req(smallRes).StartTime(epochTime).Obj(),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg-small").UID("pg-small").Namespace(v1.NamespaceDefault).DisruptionModeAll().Obj(),
st.MakePodGroup().Name("pg-large").UID("pg-large").Namespace(v1.NamespaceDefault).DisruptionModeAll().Obj(),
},
// pg-small is reprieved because smaller groups are more important in this custom ordering; pg-large pods (v2, v3) are selected
expectedPods: []string{"v2", "v3"},
},
{
name: "select pod group by name alphabetically due to custom ordering",
orderVictims: orderByPodGroupNameVictim,
nodeNames: []string{"node1"},
preemptor: st.MakePod().Name("p2").UID("p2").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(largeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg-a").Priority(lowPriority).Req(mediumRes).StartTime(epochTime1).Obj(),
st.MakePod().Name("v2").UID("v2").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg-z").Priority(lowPriority).Req(mediumRes).StartTime(epochTime).Obj(),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg-a").UID("pg-a").Namespace(v1.NamespaceDefault).DisruptionModeAll().Obj(),
st.MakePodGroup().Name("pg-z").UID("pg-z").Namespace(v1.NamespaceDefault).DisruptionModeAll().Obj(),
},
// pg-a is reprieved because it is alphabetically earlier; pg-z pod (v2) is selected
expectedPods: []string{"v2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
nodes := make([]*v1.Node, len(tt.nodeNames))
for i, nodeName := range tt.nodeNames {
nodes[i] = st.MakeNode().Name(nodeName).Capacity(veryLargeRes).Obj()
}
var objs []runtime.Object
objs = append(objs, tt.preemptor)
for _, pod := range tt.pods {
objs = append(objs, pod)
}
for _, pg := range tt.podGroups {
objs = append(objs, pg)
}
cs := clientsetfake.NewClientset(objs...)
informerFactory := informers.NewSharedInformerFactory(cs, 0)
_ = informerFactory.Scheduling().V1alpha3().PodGroups().Informer()
informerFactory.Start(ctx.Done())
informerFactory.WaitForCacheSync(ctx.Done())
snapshot := internalcache.NewTestSnapshotWithPodGroups(tt.pods, nodes, tt.podGroups)
cache := internalcache.New(ctx, nil, true)
for _, pg := range tt.podGroups {
cache.AddPodGroup(pg)
}
fwk, err := tf.NewFramework(
ctx,
[]tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
},
"",
frameworkruntime.WithPodNominator(internalqueue.NewSchedulingQueue(nil, informerFactory)),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithLogger(logger),
frameworkruntime.WithPodGroupManager(cache),
)
if err != nil {
t.Fatal(err)
}
state := framework.NewCycleState()
// Some tests rely on PreFilter plugin to compute its CycleState.
if _, status, _ := fwk.RunPreFilterPlugins(ctx, state, tt.preemptor); !status.IsSuccess() {
t.Errorf("Unexpected PreFilter Status: %v", status)
}
nodeInfos, err := snapshot.NodeInfos().List()
if err != nil {
t.Fatal(err)
}
pl, err := New(ctx, getDefaultDefaultPreemptionArgs(), fwk, feature.Features{
EnableGenericWorkload: true,
})
if err != nil {
t.Fatal(err)
}
// Override ordering logic
if tt.orderVictims != nil {
pl.MoreImportantVictim = tt.orderVictims
}
offset, numCandidates := pl.GetOffsetAndNumCandidates(int32(len(nodeInfos)))
candidates, _, _ := pl.Evaluator.DryRunPreemption(ctx, state, tt.preemptor, nodeInfos, nil, offset, numCandidates)
if len(candidates) != 1 {
t.Fatalf("expected exactly one node but got %+v", candidates)
}
podNames := []string{}
for _, p := range candidates[0].Victims().Pods {
podNames = append(podNames, p.Name)
}
// For PodGroups, the order of pods within a group is indeterminate due to map iteration in ScheduledPods().
// For standalone pods, MoreImportantVictim establishes a deterministic order that must be preserved.
if len(tt.podGroups) > 0 {
sort.Strings(podNames)
sort.Strings(tt.expectedPods)
}
if diff := cmp.Diff(tt.expectedPods, podNames); diff != "" {
t.Errorf("expect pods %+v, but got pods %+v", tt.expectedPods, podNames)
}
})
}
}
func TestPodEligibleToPreemptOthers(t *testing.T) {
tests := []struct {
name string
pod *v1.Pod
pods []*v1.Pod
podGroups []*v1alpha3.PodGroup
nodes []string
features feature.Features
nominatedNodeStatus *fwk.Status
expected bool
}{
{
name: "Pod with nominated node",
pod: st.MakePod().Name("p_with_nominated_node").UID("p").Priority(highPriority).NominatedNodeName("node1").Obj(),
pods: []*v1.Pod{st.MakePod().Name("p1").UID("p1").Priority(lowPriority).Node("node1").Terminating().Obj()},
nodes: []string{"node1"},
nominatedNodeStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, tainttoleration.ErrReasonNotMatch),
expected: true,
},
{
name: "Pod without nominated node",
pod: st.MakePod().Name("p_without_nominated_node").UID("p").Priority(highPriority).Obj(),
pods: []*v1.Pod{},
nodes: []string{},
nominatedNodeStatus: nil,
expected: true,
},
{
name: "Pod with 'PreemptNever' preemption policy",
pod: st.MakePod().Name("p_with_preempt_never_policy").UID("p").Priority(highPriority).PreemptionPolicy(v1.PreemptNever).Obj(),
pods: []*v1.Pod{},
nodes: []string{},
nominatedNodeStatus: nil,
expected: false,
},
{
name: "preemption victim pod terminating, as indicated by the dedicated DisruptionTarget condition",
pod: st.MakePod().Name("p_with_nominated_node").UID("p").Priority(highPriority).NominatedNodeName("node1").Obj(),
pods: []*v1.Pod{st.MakePod().Name("p1").UID("p1").Priority(lowPriority).Node("node1").Terminating().
Condition(v1.DisruptionTarget, v1.ConditionTrue, v1.PodReasonPreemptionByScheduler).Obj()},
nodes: []string{"node1"},
expected: false,
},
{
name: "non-victim Pods terminating",
pod: st.MakePod().Name("p_with_nominated_node").UID("p").Priority(highPriority).NominatedNodeName("node1").Obj(),
pods: []*v1.Pod{st.MakePod().Name("p1").UID("p1").Priority(lowPriority).Node("node1").Terminating().Obj()},
nodes: []string{"node1"},
expected: true,
},
{
name: "Preemptor with SchedulingGroup, victim priority overridden by PodGroup in victim namespace",
pod: st.MakePod().Name("p").UID("p").Namespace("ns1").Priority(highPriority).PodGroupName("pg1").NominatedNodeName("node1").Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Namespace("ns2").Node("node1").Priority(veryHighPriority).PodGroupName("pg1").Terminating().
Condition(v1.DisruptionTarget, v1.ConditionTrue, v1.PodReasonPreemptionByScheduler).Obj(),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace("ns2").Priority(lowPriority).Obj(),
},
nodes: []string{"node1"},
features: feature.Features{EnableGenericWorkload: true},
expected: false,
},
{
name: "Preemptor with SchedulingGroup, PodGroup not found in victim namespace, fallback to pod priority",
pod: st.MakePod().Name("p").UID("p").Namespace("ns1").Priority(highPriority).PodGroupName("pg1").NominatedNodeName("node1").Obj(),
pods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Namespace("ns2").Node("node1").Priority(veryHighPriority).PodGroupName("pg1").Terminating().
Condition(v1.DisruptionTarget, v1.ConditionTrue, v1.PodReasonPreemptionByScheduler).Obj(),
},
nodes: []string{"node1"},
features: feature.Features{EnableGenericWorkload: true},
expected: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var nodes []*v1.Node
for _, n := range test.nodes {
nodes = append(nodes, st.MakeNode().Name(n).Obj())
}
var objs []runtime.Object
objs = append(objs, test.pod)
for _, pod := range test.pods {
objs = append(objs, pod)
}
for _, pg := range test.podGroups {
objs = append(objs, pg)
}
cs := clientsetfake.NewClientset(objs...)
informerFactory := informers.NewSharedInformerFactory(cs, 0)
_ = informerFactory.Scheduling().V1alpha3().PodGroups().Informer()
informerFactory.Start(ctx.Done())
informerFactory.WaitForCacheSync(ctx.Done())
registeredPlugins := []tf.RegisterPluginFunc{
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
}
cache := internalcache.New(ctx, nil, test.features.EnableGenericWorkload)
for _, pg := range test.podGroups {
cache.AddPodGroup(pg)
}
snapshot := internalcache.NewTestSnapshotWithPodGroups(test.pods, nodes, test.podGroups)
f, err := tf.NewFramework(ctx, registeredPlugins, "",
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithClientSet(cs),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithLogger(logger),
frameworkruntime.WithPodGroupManager(cache),
)
if err != nil {
t.Fatal(err)
}
pl, err := New(ctx, getDefaultDefaultPreemptionArgs(), f, test.features)
if err != nil {
t.Fatal(err)
}
if got, _ := pl.PodEligibleToPreemptOthers(ctx, test.pod, test.nominatedNodeStatus); got != test.expected {
t.Errorf("expected %t, got %t for pod: %s", test.expected, got, test.pod.Name)
}
})
}
}
func TestPreempt(t *testing.T) {
metrics.Register()
tests := []struct {
name string
preemptor *v1.Pod
pods []*v1.Pod
extenders []*tf.FakeExtender
nodeNames []string
registerPlugin tf.RegisterPluginFunc
want *fwk.PostFilterResult
expectedPods []string // list of preempted pods
}{
{
name: "basic preemption logic",
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(veryLargeRes).PreemptionPolicy(v1.PreemptLowerPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Node("node2").Priority(highPriority).Req(largeRes).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Node("node3").Priority(midPriority).Req(mediumRes).Obj(),
},
nodeNames: []string{"node1", "node2", "node3"},
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
want: framework.NewPostFilterResultWithNominatedNode("node1"),
expectedPods: []string{"p1.1", "p1.2"},
},
{
name: "preemption for topology spread constraints",
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Label("foo", "").Priority(highPriority).
SpreadConstraint(1, "zone", v1.DoNotSchedule, st.MakeLabelSelector().Exists("foo").Obj(), nil, nil, nil, nil).
SpreadConstraint(1, "hostname", v1.DoNotSchedule, st.MakeLabelSelector().Exists("foo").Obj(), nil, nil, nil, nil).
Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p-a1").UID("p-a1").Namespace(v1.NamespaceDefault).Node("node-a").Label("foo", "").Priority(highPriority).Obj(),
st.MakePod().Name("p-a2").UID("p-a2").Namespace(v1.NamespaceDefault).Node("node-a").Label("foo", "").Priority(highPriority).Obj(),
st.MakePod().Name("p-b1").UID("p-b1").Namespace(v1.NamespaceDefault).Node("node-b").Label("foo", "").Priority(lowPriority).Obj(),
st.MakePod().Name("p-x1").UID("p-x1").Namespace(v1.NamespaceDefault).Node("node-x").Label("foo", "").Priority(highPriority).Obj(),
st.MakePod().Name("p-x2").UID("p-x2").Namespace(v1.NamespaceDefault).Node("node-x").Label("foo", "").Priority(highPriority).Obj(),
},
nodeNames: []string{"node-a/zone1", "node-b/zone1", "node-x/zone2"},
registerPlugin: tf.RegisterPluginAsExtensions(podtopologyspread.Name, podTopologySpreadFunc, "PreFilter", "Filter"),
want: framework.NewPostFilterResultWithNominatedNode("node-b"),
expectedPods: []string{"p-b1"},
},
{
name: "Scheduler extenders allow only node1, otherwise node3 would have been chosen",
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(veryLargeRes).PreemptionPolicy(v1.PreemptLowerPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Namespace(v1.NamespaceDefault).Node("node1").Priority(midPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Namespace(v1.NamespaceDefault).Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Namespace(v1.NamespaceDefault).Node("node3").Priority(midPriority).Req(largeRes).Obj(),
},
nodeNames: []string{"node1", "node2", "node3"},
extenders: []*tf.FakeExtender{
{
ExtenderName: "FakeExtender1",
Predicates: []tf.FitPredicate{tf.TruePredicateExtender},
},
{
ExtenderName: "FakeExtender2",
Predicates: []tf.FitPredicate{tf.Node1PredicateExtender},
},
},
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
want: framework.NewPostFilterResultWithNominatedNode("node1"),
expectedPods: []string{"p1.1", "p1.2"},
},
{
name: "Scheduler extenders do not allow any preemption",
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(veryLargeRes).PreemptionPolicy(v1.PreemptLowerPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Namespace(v1.NamespaceDefault).Node("node1").Priority(midPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Namespace(v1.NamespaceDefault).Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Namespace(v1.NamespaceDefault).Node("node2").Priority(midPriority).Req(largeRes).Obj(),
},
nodeNames: []string{"node1", "node2", "node3"},
extenders: []*tf.FakeExtender{
{
ExtenderName: "FakeExtender1",
Predicates: []tf.FitPredicate{tf.FalsePredicateExtender},
},
},
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
want: nil,
expectedPods: []string{},
},
{
name: "One scheduler extender allows only node1, the other returns error but ignorable. Only node1 would be chosen",
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(veryLargeRes).PreemptionPolicy(v1.PreemptLowerPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Namespace(v1.NamespaceDefault).Node("node1").Priority(midPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Namespace(v1.NamespaceDefault).Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Namespace(v1.NamespaceDefault).Node("node2").Priority(midPriority).Req(largeRes).Obj(),
},
nodeNames: []string{"node1", "node2", "node3"},
extenders: []*tf.FakeExtender{
{
Predicates: []tf.FitPredicate{tf.ErrorPredicateExtender},
Ignorable: true,
ExtenderName: "FakeExtender1",
},
{
Predicates: []tf.FitPredicate{tf.Node1PredicateExtender},
ExtenderName: "FakeExtender2",
},
},
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
want: framework.NewPostFilterResultWithNominatedNode("node1"),
expectedPods: []string{"p1.1", "p1.2"},
},
{
name: "One scheduler extender allows only node1, but it is not interested in given pod, otherwise node1 would have been chosen",
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(veryLargeRes).PreemptionPolicy(v1.PreemptLowerPriority).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Namespace(v1.NamespaceDefault).Node("node1").Priority(midPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Namespace(v1.NamespaceDefault).Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Namespace(v1.NamespaceDefault).Node("node2").Priority(midPriority).Req(largeRes).Obj(),
},
nodeNames: []string{"node1", "node2"},
extenders: []*tf.FakeExtender{
{
ExtenderName: "FakeExtender1",
Predicates: []tf.FitPredicate{tf.Node1PredicateExtender},
UnInterested: true,
},
{
ExtenderName: "FakeExtender2",
Predicates: []tf.FitPredicate{tf.TruePredicateExtender},
},
},
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
// sum of priorities of all victims on node1 is larger than node2, node2 is chosen.
want: framework.NewPostFilterResultWithNominatedNode("node2"),
expectedPods: []string{"p2.1"},
},
{
name: "no preempting in pod",
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(veryLargeRes).PreemptionPolicy(v1.PreemptNever).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Namespace(v1.NamespaceDefault).Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Namespace(v1.NamespaceDefault).Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Namespace(v1.NamespaceDefault).Node("node2").Priority(highPriority).Req(largeRes).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Namespace(v1.NamespaceDefault).Node("node3").Priority(midPriority).Req(mediumRes).Obj(),
},
nodeNames: []string{"node1", "node2", "node3"},
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
want: nil,
expectedPods: nil,
},
{
name: "PreemptionPolicy is nil",
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Req(veryLargeRes).Obj(),
pods: []*v1.Pod{
st.MakePod().Name("p1.1").UID("p1.1").Namespace(v1.NamespaceDefault).Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p1.2").UID("p1.2").Namespace(v1.NamespaceDefault).Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("p2.1").UID("p2.1").Namespace(v1.NamespaceDefault).Node("node2").Priority(highPriority).Req(largeRes).Obj(),
st.MakePod().Name("p3.1").UID("p3.1").Namespace(v1.NamespaceDefault).Node("node3").Priority(midPriority).Req(mediumRes).Obj(),
},
nodeNames: []string{"node1", "node2", "node3"},
registerPlugin: tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
want: framework.NewPostFilterResultWithNominatedNode("node1"),
expectedPods: []string{"p1.1", "p1.2"},
},
}
labelKeys := []string{"hostname", "zone", "region"}
for _, asyncPreemptionEnabled := range []bool{true, false} {
for _, asyncAPICallsEnabled := range []bool{true, false} {
for _, test := range tests {
t.Run(fmt.Sprintf("%s (Async preemption enabled: %v, Async API calls enabled: %v)", test.name, asyncPreemptionEnabled, asyncAPICallsEnabled), func(t *testing.T) {
client := clientsetfake.NewClientset()
informerFactory := informers.NewSharedInformerFactory(client, 0)
podInformer := informerFactory.Core().V1().Pods().Informer()
testPreemptor := test.preemptor.DeepCopy()
var testPods []*v1.Pod
for _, pod := range test.pods {
testPods = append(testPods, pod.DeepCopy())
}
if err := podInformer.GetStore().Add(testPreemptor); err != nil {
t.Fatalf("Failed to add test pod %s: %v", testPreemptor.Name, err)
}
for i := range testPods {
if err := podInformer.GetStore().Add(testPods[i]); err != nil {
t.Fatalf("Failed to add test pod %s: %v", testPods[i], err)
}
}
// Need to protect deletedPodNames and patchedPodNames to prevent DATA RACE panic.
var mu sync.RWMutex
deletedPodNames := sets.New[string]()
patchedPodNames := sets.New[string]()
patchedPods := []*v1.Pod{}
client.PrependReactor("patch", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) {
patchAction := action.(clienttesting.PatchAction)
podName := patchAction.GetName()
namespace := patchAction.GetNamespace()
patch := patchAction.GetPatch()
pod, err := informerFactory.Core().V1().Pods().Lister().Pods(namespace).Get(podName)
if err != nil {
t.Fatalf("Failed to get the original pod %s/%s before patching: %v\n", namespace, podName, err)
}
marshalledPod, err := json.Marshal(pod)
if err != nil {
t.Fatalf("Failed to marshal the original pod %s/%s: %v", namespace, podName, err)
}
updated, err := strategicpatch.StrategicMergePatch(marshalledPod, patch, v1.Pod{})
if err != nil {
t.Fatalf("Failed to apply strategic merge patch %q on pod %#v: %v", patch, marshalledPod, err)
}
updatedPod := &v1.Pod{}
if err := json.Unmarshal(updated, updatedPod); err != nil {
t.Fatalf("Failed to unmarshal updated pod %q: %v", updated, err)
}
patchedPods = append(patchedPods, updatedPod)
mu.Lock()
defer mu.Unlock()
patchedPodNames.Insert(podName)
return true, nil, nil
})
client.PrependReactor("delete", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) {
mu.Lock()
defer mu.Unlock()
deletedPodNames.Insert(action.(clienttesting.DeleteAction).GetName())
return true, nil, nil
})
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
waitingPods := frameworkruntime.NewWaitingPodsMap()
var apiDispatcher *apidispatcher.APIDispatcher
if asyncAPICallsEnabled {
apiDispatcher = apidispatcher.New(client, 16, apicalls.Relevances)
apiDispatcher.Run(logger)
defer apiDispatcher.Close()
}
cache := internalcache.New(ctx, apiDispatcher, false)
for _, pod := range testPods {
if err := cache.AddPod(logger, pod.DeepCopy()); err != nil {
t.Fatalf("Failed to add pod %s: %v", pod.Name, err)
}
}
cachedNodeInfoMap := map[string]*framework.NodeInfo{}
nodes := make([]*v1.Node, len(test.nodeNames))
for i, name := range test.nodeNames {
node := st.MakeNode().Name(name).Capacity(veryLargeRes).Obj()
// Split node name by '/' to form labels in a format of
// {"hostname": node.Name[0], "zone": node.Name[1], "region": node.Name[2]}
node.Labels = make(map[string]string)
for i, label := range strings.Split(node.Name, "/") {
node.Labels[labelKeys[i]] = label
}
node.Name = node.Labels["hostname"]
t.Logf("node is added: %v. labels: %#v", node.Name, node.Labels)
cache.AddNode(logger, node)
nodes[i] = node
// Set nodeInfo to extenders to mock extenders' cache for preemption.
cachedNodeInfo := framework.NewNodeInfo()
cachedNodeInfo.SetNode(node)
cachedNodeInfoMap[node.Name] = cachedNodeInfo
}
var extenders []fwk.Extender
for _, extender := range test.extenders {
// Set nodeInfoMap as extenders cached node information.
extender.CachedNodeNameToInfo = cachedNodeInfoMap
extenders = append(extenders, extender)
}
snapshot := internalcache.NewSnapshot(testPods, nodes)
schedFramework, err := tf.NewFramework(
ctx,
[]tf.RegisterPluginFunc{
test.registerPlugin,
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
},
"",
frameworkruntime.WithClientSet(client),
frameworkruntime.WithAPIDispatcher(apiDispatcher),
frameworkruntime.WithEventRecorder(&events.FakeRecorder{}),
frameworkruntime.WithExtenders(extenders),
frameworkruntime.WithPodNominator(internalqueue.NewSchedulingQueue(nil, informerFactory)),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithWaitingPods(waitingPods),
frameworkruntime.WithPodsInPreBind(frameworkruntime.NewPodsInPreBindMap()),
frameworkruntime.WithLogger(logger),
frameworkruntime.WithPodActivator(&fakePodActivator{}),
)
if err != nil {
t.Fatal(err)
}
if asyncAPICallsEnabled {
schedFramework.SetAPICacher(apicache.New(nil, cache))
}
cycleState := framework.NewCycleState()
if _, s, _ := schedFramework.RunPreFilterPlugins(ctx, cycleState, testPreemptor); !s.IsSuccess() {
t.Errorf("Unexpected preFilterStatus: %v", s)
}
// Call preempt and check the expected results.
features := feature.Features{
EnableAsyncPreemption: asyncPreemptionEnabled,
}
pl, err := New(ctx, getDefaultDefaultPreemptionArgs(), schedFramework, features)
if err != nil {
t.Fatal(err)
}
// so that these nodes are eligible for preemption, we set their status
// to Unschedulable.
nodeToStatusMap := framework.NewDefaultNodeToStatus()
for _, n := range nodes {
nodeToStatusMap.Set(n.Name, fwk.NewStatus(fwk.Unschedulable))
}
res, status := pl.Evaluator.Preempt(ctx, cycleState, test.preemptor, nodeToStatusMap)
if !status.IsSuccess() && !status.IsRejected() {
t.Errorf("unexpected error in preemption: %v", status.AsError())
}
if diff := cmp.Diff(test.want, res); diff != "" {
t.Errorf("Unexpected status (-want, +got):\n%s", diff)
}
if asyncPreemptionEnabled {
// Wait for the pod to be deleted.
if err := wait.PollUntilContextTimeout(ctx, time.Millisecond*200, wait.ForeverTestTimeout, false, func(ctx context.Context) (bool, error) {
mu.RLock()
defer mu.RUnlock()
return len(deletedPodNames) == len(test.expectedPods), nil
}); err != nil {
t.Errorf("expected %v pods to be deleted, got %v.", len(test.expectedPods), len(deletedPodNames))
}
} else {
mu.RLock()
// If async preemption is disabled, the pod should be deleted immediately.
if len(deletedPodNames) != len(test.expectedPods) {
t.Errorf("expected %v pods to be deleted, got %v.", len(test.expectedPods), len(deletedPodNames))
}
mu.RUnlock()
}
mu.RLock()
if diff := cmp.Diff(sets.List(patchedPodNames), sets.List(deletedPodNames)); diff != "" {
t.Errorf("unexpected difference in the set of patched and deleted pods: %s", diff)
}
// Make sure that the DisruptionTarget condition has been added to the pod status
for _, patchedPod := range patchedPods {
expectedPodCondition := &v1.PodCondition{
Type: v1.DisruptionTarget,
Status: v1.ConditionTrue,
Reason: v1.PodReasonPreemptionByScheduler,
Message: fmt.Sprintf("%s: preempting to accommodate a higher priority pod", patchedPod.Spec.SchedulerName),
}
_, condition := apipod.GetPodCondition(&patchedPod.Status, v1.DisruptionTarget)
if diff := cmp.Diff(condition, expectedPodCondition, cmpopts.IgnoreFields(v1.PodCondition{}, "LastTransitionTime")); diff != "" {
t.Fatalf("unexpected difference in the pod %q DisruptionTarget condition: %s", patchedPod.Name, diff)
}
}
for victimName := range deletedPodNames {
found := false
for _, expPod := range test.expectedPods {
if expPod == victimName {
found = true
break
}
}
if !found {
t.Errorf("pod %v is not expected to be a victim.", victimName)
}
}
if res != nil && res.NominatingInfo != nil {
test.preemptor.Status.NominatedNodeName = res.NominatedNodeName
}
// Manually set the deleted Pods' deletionTimestamp to non-nil.
for _, pod := range testPods {
if deletedPodNames.Has(pod.Name) {
now := metav1.Now()
pod.DeletionTimestamp = &now
deletedPodNames.Delete(pod.Name)
}
}
mu.RUnlock()
// Call preempt again and make sure it doesn't preempt any more pods.
res, status = pl.Evaluator.Preempt(ctx, framework.NewCycleState(), test.preemptor, framework.NewDefaultNodeToStatus())
if !status.IsSuccess() && !status.IsRejected() {
t.Errorf("unexpected error in preemption: %v", status.AsError())
}
if res != nil && res.NominatingInfo != nil && len(deletedPodNames) > 0 {
t.Errorf("didn't expect any more preemption. Node %v is selected for preemption.", res.NominatedNodeName)
}
})
}
}
}
}
type fakePodActivator struct {
}
func (f *fakePodActivator) Activate(logger klog.Logger, pods map[string]*v1.Pod) {}
type mockProposedAssignment struct {
nodeName string
podInfo fwk.PodInfo
cycleState fwk.CycleState
}
func (pa *mockProposedAssignment) GetNodeName() string {
return pa.nodeName
}
func (pa *mockProposedAssignment) GetPod() *v1.Pod {
return pa.podInfo.GetPod()
}
func (pa *mockProposedAssignment) GetPodInfo() fwk.PodInfo {
return pa.podInfo
}
func (pa *mockProposedAssignment) GetCycleState() fwk.CycleState {
return pa.cycleState
}
func TestSelectVictimsOnNode(t *testing.T) {
tests := []struct {
name string
nodeNames []string
mainNode string
initPods []*v1.Pod
preemptor *v1.Pod
pdbs []*policy.PodDisruptionBudget
podGroups []*v1alpha3.PodGroup
registerPlugins []tf.RegisterPluginFunc
features feature.Features
expectedPods sets.Set[string]
expectedNumViolatingVictim int
expectedStatus *fwk.Status
}{
{
name: "Basic: Preempt single lower priority pod",
nodeNames: []string{"node1"},
mainNode: "node1",
initPods: []*v1.Pod{
st.MakePod().Name("victim").UID("v1").Node("node1").Priority(lowPriority).Req(largeRes).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
expectedPods: sets.New("victim"),
expectedNumViolatingVictim: 0,
},
{
name: "Priority: Prefer lower priority victim",
nodeNames: []string{"node1"},
mainNode: "node1",
initPods: []*v1.Pod{
st.MakePod().Name("high-prio").UID("v3").Node("node1").Priority(highPriority).Req(smallRes).Obj(),
st.MakePod().Name("mid-prio").UID("v2").Node("node1").Priority(midPriority).Req(smallRes).Obj(),
st.MakePod().Name("low-prio").UID("v1").Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
expectedPods: sets.New("low-prio"),
expectedNumViolatingVictim: 0,
},
{
name: "PDB: Prefer non-violating victim with higher priority over violating victim with lower priority",
nodeNames: []string{"node1"},
mainNode: "node1",
initPods: []*v1.Pod{
st.MakePod().Name("victim-pdb").UID("v1").Node("node1").Label("app", "foo").Priority(lowPriority).Req(mediumRes).Obj(),
st.MakePod().Name("victim-no-pdb").UID("v2").Node("node1").Priority(midPriority).Req(mediumRes).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 0},
},
},
preemptor: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
expectedPods: sets.New("victim-no-pdb"),
expectedNumViolatingVictim: 0,
},
{
name: "PDB: Prefer lower prioirity pod for preemption, when preemption without pdb violation is not possible",
nodeNames: []string{"node1"},
mainNode: "node1",
initPods: []*v1.Pod{
st.MakePod().Name("v1").UID("v1").Node("node1").Label("app", "foo").Priority(lowPriority).Req(mediumRes).Obj(),
st.MakePod().Name("v2").UID("v2").Node("node1").Label("app", "foo").Priority(midPriority).Req(mediumRes).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 0},
},
},
preemptor: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
expectedPods: sets.New("v1"),
expectedNumViolatingVictim: 1,
},
{
name: "Workload Aware: Atomic preemption of PodGroup",
nodeNames: []string{"node1"},
mainNode: "node1",
features: feature.Features{EnableGenericWorkload: true},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(lowPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("g1-1").UID("g1").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("g1-2").UID("g2").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg1").Priority(lowPriority).Req(mediumRes).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
expectedPods: sets.New("g1-1", "g1-2"),
expectedNumViolatingVictim: 0,
},
{
name: "Workload Aware: prefer single pod over podGroup for preemption candidate",
nodeNames: []string{"node1"},
mainNode: "node1",
features: feature.Features{EnableGenericWorkload: true},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(lowPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("g1-1").UID("g1").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg1").Priority(lowPriority).Req(smallRes).Obj(),
st.MakePod().Name("g1-2").UID("g2").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg1").Priority(lowPriority).Req(smallRes).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
expectedPods: sets.New("p1"),
expectedNumViolatingVictim: 0,
},
{
name: "Workload Aware: preempt lower priority pod group instead of higher priority single pod",
nodeNames: []string{"node1"},
mainNode: "node1",
features: feature.Features{EnableGenericWorkload: true},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(lowPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(highPriority).Req(mediumRes).Obj(),
st.MakePod().Name("g1-1").UID("g1").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg1").Priority(lowPriority).Req(mediumRes).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Priority(veryHighPriority).Req(largeRes).Obj(),
expectedPods: sets.New("g1-1"),
expectedNumViolatingVictim: 0,
},
{
name: "Workload Aware: preempt lower priority single pod instead of higher priority pod group",
nodeNames: []string{"node1"},
mainNode: "node1",
features: feature.Features{EnableGenericWorkload: true},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(highPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Node("node1").Priority(lowPriority).Req(mediumRes).Obj(),
st.MakePod().Name("g1-1").UID("g1").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg1").Priority(highPriority).Req(mediumRes).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Priority(veryHighPriority).Req(largeRes).Obj(),
expectedPods: sets.New("p1"),
expectedNumViolatingVictim: 0,
},
{
name: "Workload Aware: PDB: PodGroup as victim with 2 PDB violations",
nodeNames: []string{"node1"},
mainNode: "node1",
features: feature.Features{EnableGenericWorkload: true},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(lowPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("g1-1").UID("g1").Namespace(v1.NamespaceDefault).Node("node1").Label("app", "foo").PodGroupName("pg1").Priority(lowPriority).Req(mediumRes).Obj(),
st.MakePod().Name("g1-2").UID("g2").Namespace(v1.NamespaceDefault).Node("node1").Label("app", "foo").PodGroupName("pg1").Priority(lowPriority).Req(mediumRes).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
ObjectMeta: metav1.ObjectMeta{Namespace: v1.NamespaceDefault},
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 0},
},
},
preemptor: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
expectedPods: sets.New("g1-1", "g1-2"),
expectedNumViolatingVictim: 2,
},
{
name: "Workload Aware: PDB: Prefer non-violating PodGroup over lower-priority violating PodGroup (DisruptionModeAll)",
nodeNames: []string{"node1"},
mainNode: "node1",
features: feature.Features{EnableGenericWorkload: true},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg-violating").Namespace(v1.NamespaceDefault).UID("pg-violating").Priority(lowPriority).DisruptionModeAll().Obj(),
st.MakePodGroup().Name("pg-non-violating").Namespace(v1.NamespaceDefault).UID("pg-non-violating").Priority(midPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("g1-1").UID("g1-1").Namespace(v1.NamespaceDefault).Node("node1").Label("app", "foo").PodGroupName("pg-violating").Priority(lowPriority).Req(mediumRes).Obj(),
st.MakePod().Name("g2-1").UID("g2-1").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg-non-violating").Priority(midPriority).Req(mediumRes).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
ObjectMeta: metav1.ObjectMeta{Namespace: v1.NamespaceDefault},
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 0},
},
},
preemptor: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
expectedPods: sets.New("g2-1"),
expectedNumViolatingVictim: 0,
},
{
name: "Workload Aware: PDB: Prefer non-violating PodGroup over lower-priority violating PodGroup (DisruptionModeSingle)",
nodeNames: []string{"node1"},
mainNode: "node1",
features: feature.Features{EnableGenericWorkload: true},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg-violating").Namespace(v1.NamespaceDefault).UID("pg-violating").Priority(lowPriority).DisruptionModeSingle().Obj(),
st.MakePodGroup().Name("pg-non-violating").Namespace(v1.NamespaceDefault).UID("pg-non-violating").Priority(midPriority).DisruptionModeSingle().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("g1-1").UID("g1-1").Namespace(v1.NamespaceDefault).Node("node1").Label("app", "foo").PodGroupName("pg-violating").Priority(lowPriority).Req(mediumRes).Obj(),
st.MakePod().Name("g2-1").UID("g2-1").Namespace(v1.NamespaceDefault).Node("node1").PodGroupName("pg-non-violating").Priority(midPriority).Req(mediumRes).Obj(),
},
pdbs: []*policy.PodDisruptionBudget{
{
ObjectMeta: metav1.ObjectMeta{Namespace: v1.NamespaceDefault},
Spec: policy.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "foo"}}},
Status: policy.PodDisruptionBudgetStatus{DisruptionsAllowed: 0},
},
},
preemptor: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
expectedPods: sets.New("g2-1"),
expectedNumViolatingVictim: 0,
},
{
name: "Failure: Cannot preempt the victim with higher priority",
nodeNames: []string{"node1"},
mainNode: "node1",
initPods: []*v1.Pod{
st.MakePod().Name("victim").UID("v1").Node("node1").Priority(highPriority).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Priority(midPriority).Obj(),
expectedPods: nil,
expectedNumViolatingVictim: 0,
expectedStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable),
},
{
name: "Failure: Cannot preempt if node is empty",
nodeNames: []string{"node1"},
mainNode: "node1",
initPods: []*v1.Pod{},
preemptor: st.MakePod().Name("p").UID("p").Priority(midPriority).Obj(),
expectedPods: nil,
expectedNumViolatingVictim: 0,
expectedStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable),
},
{
name: "Cross-node PodGroup: zone anti-affinity blocks reprieve via PreFilterExtension on node-a",
nodeNames: []string{"node-a/zone1", "node-b/zone1"},
mainNode: "node-a",
features: feature.Features{EnableGenericWorkload: true},
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(interpodaffinity.Name, frameworkruntime.FactoryAdapter(feature.Features{}, interpodaffinity.New), "Filter", "PreFilter"),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(lowPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("pod-x").UID("pod-x").Namespace(v1.NamespaceDefault).Node("node-a").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("pod-y").UID("pod-y").Namespace(v1.NamespaceDefault).Node("node-b").Label("foo", "").PodGroupName("pg1").Priority(lowPriority).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).
PodAntiAffinityExists("foo", "zone", st.PodAntiAffinityWithRequiredReq).Obj(),
expectedPods: sets.New("pod-x", "pod-y"),
expectedNumViolatingVictim: 0,
},
{
name: "Cross-node PodGroup: zone anti-affinity blocks reprieve via PreFilterExtension on node-b",
nodeNames: []string{"node-a/zone1", "node-b/zone1"},
mainNode: "node-b",
features: feature.Features{EnableGenericWorkload: true},
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(interpodaffinity.Name, frameworkruntime.FactoryAdapter(feature.Features{}, interpodaffinity.New), "Filter", "PreFilter"),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(lowPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("pod-x").UID("pod-x").Namespace(v1.NamespaceDefault).Node("node-a").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("pod-y").UID("pod-y").Namespace(v1.NamespaceDefault).Node("node-b").Label("foo", "").PodGroupName("pg1").Priority(lowPriority).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).
PodAntiAffinityExists("foo", "zone", st.PodAntiAffinityWithRequiredReq).Obj(),
expectedPods: sets.New("pod-x", "pod-y"),
expectedNumViolatingVictim: 0,
},
{
name: "Cross-node PodGroup: zone anti-affinity reprieve succeeds when remote pod lacks constraint label (negative control)",
nodeNames: []string{"node-a/zone1", "node-b/zone1"},
mainNode: "node-a",
features: feature.Features{EnableGenericWorkload: true},
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(interpodaffinity.Name, frameworkruntime.FactoryAdapter(feature.Features{}, interpodaffinity.New), "Filter", "PreFilter"),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(lowPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("pod-x").UID("pod-x").Namespace(v1.NamespaceDefault).Node("node-a").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("pod-y").UID("pod-y").Namespace(v1.NamespaceDefault).Node("node-b").PodGroupName("pg1").Priority(lowPriority).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).
PodAntiAffinityExists("foo", "zone", st.PodAntiAffinityWithRequiredReq).Obj(),
expectedPods: nil,
expectedNumViolatingVictim: 0,
},
{
name: "Cross-node PodGroup: topology spread blocks reprieve via PreFilterExtension on node-a",
nodeNames: []string{"node-a/zone1", "node-b/zone1", "node-c/zone2"},
mainNode: "node-a",
features: feature.Features{EnableGenericWorkload: true},
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(podtopologyspread.Name, podTopologySpreadFunc, "PreFilter", "Filter"),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(lowPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("pod-a").UID("pod-a").Namespace(v1.NamespaceDefault).Node("node-a").Label("foo", "").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("pod-b").UID("pod-b").Namespace(v1.NamespaceDefault).Node("node-b").Label("foo", "").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("pod-c").UID("pod-c").Namespace(v1.NamespaceDefault).Node("node-c").Label("foo", "").Priority(veryHighPriority).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Label("foo", "").Priority(highPriority).
SpreadConstraint(1, "zone", v1.DoNotSchedule, st.MakeLabelSelector().Exists("foo").Obj(), nil, nil, nil, nil).Obj(),
expectedPods: sets.New("pod-a", "pod-b"),
expectedNumViolatingVictim: 0,
},
{
name: "Cross-node PodGroup: topology spread blocks reprieve via PreFilterExtension on node-c",
nodeNames: []string{"node-a/zone1", "node-b/zone1", "node-c/zone2"},
mainNode: "node-c",
features: feature.Features{EnableGenericWorkload: true},
registerPlugins: []tf.RegisterPluginFunc{
tf.RegisterPluginAsExtensions(podtopologyspread.Name, podTopologySpreadFunc, "PreFilter", "Filter"),
},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(lowPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("pod-a").UID("pod-a").Namespace(v1.NamespaceDefault).Node("node-a").Label("foo", "").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("pod-b").UID("pod-b").Namespace(v1.NamespaceDefault).Node("node-b").Label("foo", "").PodGroupName("pg1").Priority(lowPriority).Obj(),
st.MakePod().Name("pod-c").UID("pod-c").Namespace(v1.NamespaceDefault).Node("node-c").Label("foo", "").Priority(veryHighPriority).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Label("foo", "").Priority(highPriority).
SpreadConstraint(1, "zone", v1.DoNotSchedule, st.MakeLabelSelector().Exists("foo").Obj(), nil, nil, nil, nil).Obj(),
expectedPods: nil,
expectedNumViolatingVictim: 0,
expectedStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable),
},
{
name: "Cross-node PodGroup: whole pod group across multiple nodes expected to be preempted",
nodeNames: []string{"node-a", "node-b"},
mainNode: "node-a",
features: feature.Features{EnableGenericWorkload: true},
podGroups: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").Namespace(v1.NamespaceDefault).UID("pg1").Priority(lowPriority).DisruptionModeAll().Obj(),
},
initPods: []*v1.Pod{
st.MakePod().Name("pod-a").UID("pod-a").Namespace(v1.NamespaceDefault).Node("node-a").PodGroupName("pg1").Priority(lowPriority).Req(largeRes).Obj(),
st.MakePod().Name("pod-b").UID("pod-b").Namespace(v1.NamespaceDefault).Node("node-b").PodGroupName("pg1").Priority(lowPriority).Req(largeRes).Obj(),
},
preemptor: st.MakePod().Name("p").UID("p").Priority(highPriority).Req(largeRes).Obj(),
expectedPods: sets.New("pod-a", "pod-b"),
expectedNumViolatingVictim: 0,
},
}
labelKeys := []string{"hostname", "zone", "region"}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.GenericWorkload, tt.features.EnableGenericWorkload)
nodes := make([]*v1.Node, len(tt.nodeNames))
fakeFilterRCMap := make(map[string]fwk.Code, len(tt.nodeNames))
for i, nodeName := range tt.nodeNames {
nodeWrapper := st.MakeNode().Capacity(veryLargeRes)
tpKeys := strings.Split(nodeName, "/")
nodeWrapper.Name(tpKeys[0])
for i, labelVal := range strings.Split(nodeName, "/") {
nodeWrapper.Label(labelKeys[i], labelVal)
}
nodes[i] = nodeWrapper.Obj()
}
fakePlugin := tf.FakeFilterPlugin{
FailedNodeReturnCodeMap: fakeFilterRCMap,
}
registeredPlugins := append([]tf.RegisterPluginFunc{
tf.RegisterFilterPlugin(
"FakeFilter",
func(_ context.Context, _ runtime.Object, fh fwk.Handle) (fwk.Plugin, error) {
return &fakePlugin, nil
},
)},
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
)
registeredPlugins = append(registeredPlugins, tt.registerPlugins...)
var objs []runtime.Object
objs = append(objs, tt.preemptor)
for _, n := range nodes {
objs = append(objs, n)
}
for _, pg := range tt.podGroups {
objs = append(objs, pg)
}
informerFactory := informers.NewSharedInformerFactory(clientsetfake.NewClientset(objs...), 0)
parallelism := parallelize.DefaultParallelism
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
cache := internalcache.New(ctx, nil, tt.features.EnableGenericWorkload)
for _, pg := range tt.podGroups {
cache.AddPodGroup(pg)
}
snapshot := internalcache.NewTestSnapshotWithPodGroups(tt.initPods, nodes, tt.podGroups)
testingFwk, err := tf.NewFramework(
ctx,
registeredPlugins, "",
frameworkruntime.WithPodNominator(internalqueue.NewSchedulingQueue(nil, informerFactory)),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithParallelism(parallelism),
frameworkruntime.WithLogger(logger),
frameworkruntime.WithPodGroupManager(cache),
)
if err != nil {
t.Fatal(err)
}
cycleState := framework.NewCycleState()
if _, status, _ := testingFwk.RunPreFilterPlugins(ctx, cycleState, tt.preemptor); !status.IsSuccess() {
t.Errorf("Unexpected PreFilter Status: %v", status)
}
pl, err := New(ctx, getDefaultDefaultPreemptionArgs(), testingFwk, tt.features)
if err != nil {
t.Fatal(err)
}
informerFactory.Start(ctx.Done())
informerFactory.WaitForCacheSync(ctx.Done())
mainNodeInfo, err := snapshot.NodeInfos().Get(tt.mainNode)
if err != nil {
t.Fatal(err)
}
allPossibleVictims, err := pl.Evaluator.GetVictimsOnNode(ctx, mainNodeInfo)
if err != nil {
t.Fatal(err)
}
gotPods, gotNumViolating, gotStatus := pl.SelectVictimsOnNode(ctx, cycleState.Clone(), tt.preemptor, mainNodeInfo, allPossibleVictims, tt.pdbs)
wantStatus := tt.expectedStatus
wantCode := wantStatus.Code()
gotCode := gotStatus.Code()
if gotCode != wantCode {
t.Errorf("Status mismatch. Want %v, Got %v", wantCode, gotCode)
}
if wantCode != fwk.Success {
return
}
if gotNumViolating != tt.expectedNumViolatingVictim {
t.Errorf("Violating victim count mismatch. Want %d, Got %d", tt.expectedNumViolatingVictim, gotNumViolating)
}
gotNames := sets.New[string]()
for _, p := range gotPods {
gotNames.Insert(p.Name)
}
if diff := cmp.Diff(tt.expectedPods, gotNames); diff != "" {
t.Errorf("Victims mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestPreEnqueue(t *testing.T) {
onePodRes := map[v1.ResourceName]string{v1.ResourcePods: "1"}
tests := []struct {
name string
podToTriggerPreemption *v1.Pod
podToCheck *v1.Pod
pgs []*v1alpha3.PodGroup
features feature.Features
expectPreemption bool
wantStatus *fwk.Status
}{
{
name: "AsyncPreemption disabled, returns nil",
podToTriggerPreemption: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
podToCheck: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
features: feature.Features{EnableAsyncPreemption: false},
expectPreemption: false,
wantStatus: nil,
},
{
name: "AsyncPreemption enabled, same pod, returns UnschedulableAndUnresolvable",
podToTriggerPreemption: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
podToCheck: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
features: feature.Features{EnableAsyncPreemption: true},
expectPreemption: true,
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "waiting for the preemption for this pod to be finished"),
},
{
name: "AsyncPreemption enabled, different pod, returns nil",
podToTriggerPreemption: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
podToCheck: st.MakePod().Name("p_other").UID("p_other").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
features: feature.Features{EnableAsyncPreemption: true},
expectPreemption: true,
wantStatus: nil,
},
{
name: "GenericWorkload enabled, pod in same PodGroup, returns UnschedulableAndUnresolvable",
podToTriggerPreemption: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).PodGroupName("pg1").Priority(highPriority).Obj(),
podToCheck: st.MakePod().Name("p_other").UID("p_other").Namespace(v1.NamespaceDefault).PodGroupName("pg1").Priority(highPriority).Obj(),
pgs: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
},
features: feature.Features{EnableAsyncPreemption: true, EnableGenericWorkload: true},
expectPreemption: true,
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "waiting for the preemption for this pod group to be finished"),
},
{
name: "GenericWorkload disabled, pod in same PodGroup, returns nil",
podToTriggerPreemption: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).PodGroupName("pg1").Priority(highPriority).Obj(),
podToCheck: st.MakePod().Name("p_other").UID("p_other").Namespace(v1.NamespaceDefault).PodGroupName("pg1").Priority(highPriority).Obj(),
pgs: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).Obj(),
},
features: feature.Features{EnableAsyncPreemption: true, EnableGenericWorkload: false},
expectPreemption: true,
wantStatus: nil,
},
{
name: "GenericWorkload enabled, pod in different PodGroup, returns nil",
podToTriggerPreemption: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).PodGroupName("pg1").Priority(highPriority).Obj(),
podToCheck: st.MakePod().Name("p_other").UID("p_other").Namespace(v1.NamespaceDefault).PodGroupName("pg2").Priority(highPriority).Obj(),
pgs: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
st.MakePodGroup().Name("pg2").UID("pg2").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
},
features: feature.Features{EnableAsyncPreemption: true, EnableGenericWorkload: true},
expectPreemption: true,
wantStatus: nil,
},
{
name: "GenericWorkload enabled, pod group not found, returns nil",
podToTriggerPreemption: st.MakePod().Name("p").UID("p").Namespace(v1.NamespaceDefault).PodGroupName("pg1").Priority(highPriority).Obj(),
podToCheck: st.MakePod().Name("p_other").UID("p_other").Namespace(v1.NamespaceDefault).PodGroupName("pg_missing").Priority(highPriority).Obj(),
pgs: []*v1alpha3.PodGroup{
st.MakePodGroup().Name("pg1").UID("pg1").Namespace(v1.NamespaceDefault).Priority(highPriority).Obj(),
},
features: feature.Features{EnableAsyncPreemption: true, EnableGenericWorkload: true},
expectPreemption: false,
wantStatus: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
features.GenericWorkload: tt.features.EnableGenericWorkload,
})
pods := []*v1.Pod{
st.MakePod().Name("p1").UID("p1").Namespace(v1.NamespaceDefault).Node("node1").Obj(),
}
filteredNodesStatuses := framework.NewNodeToStatus(map[string]*fwk.Status{
"node1": fwk.NewStatus(fwk.Unschedulable),
}, fwk.NewStatus(fwk.UnschedulableAndUnresolvable))
podItems := []v1.Pod{}
for _, pod := range pods {
podItems = append(podItems, *pod)
}
cs := clientsetfake.NewClientset(&v1.PodList{Items: podItems})
informerFactory := informers.NewSharedInformerFactory(cs, 0)
podInformer := informerFactory.Core().V1().Pods().Informer()
if err := podInformer.GetStore().Add(tt.podToTriggerPreemption); err != nil {
t.Fatal(err)
}
for i := range pods {
if err := podInformer.GetStore().Add(pods[i]); err != nil {
t.Fatal(err)
}
}
pgInformer := informerFactory.Scheduling().V1alpha3().PodGroups().Informer()
for i := range tt.pgs {
if err := pgInformer.GetStore().Add(tt.pgs[i]); err != nil {
t.Fatal(err)
}
}
registeredPlugins := []tf.RegisterPluginFunc{
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterPluginAsExtensions(noderesources.Name, nodeResourcesFitFunc, "Filter", "PreFilter"),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
}
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
cache := internalcache.New(ctx, nil, tt.features.EnableGenericWorkload)
for _, podGroup := range tt.pgs {
cache.AddPodGroup(podGroup)
}
snapshot := internalcache.NewTestSnapshotWithPodGroups(pods, []*v1.Node{st.MakeNode().Name("node1").Capacity(onePodRes).Obj()}, tt.pgs)
f, err := tf.NewFramework(ctx, registeredPlugins, "",
frameworkruntime.WithClientSet(cs),
frameworkruntime.WithEventRecorder(&events.FakeRecorder{}),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithPodNominator(internalqueue.NewSchedulingQueue(nil, informerFactory)),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithLogger(logger),
frameworkruntime.WithWaitingPods(frameworkruntime.NewWaitingPodsMap()),
frameworkruntime.WithPodsInPreBind(frameworkruntime.NewPodsInPreBindMap()),
frameworkruntime.WithPodGroupManager(cache),
)
if err != nil {
t.Fatal(err)
}
p, err := New(ctx, getDefaultDefaultPreemptionArgs(), f, tt.features)
if err != nil {
t.Fatal(err)
}
finishPreemption := make(chan struct{})
p.Executor.PreemptPod = func(ctx context.Context, c preemption.Candidate, preemptor preemption.ExecutorPreemptor, victim *v1.Pod, pluginName string) (bool, error) {
if !tt.features.EnableAsyncPreemption {
return false, nil
}
<-finishPreemption
return false, nil
}
// Fill the cycle state
state := framework.NewCycleState()
if _, status, _ := f.RunPreFilterPlugins(ctx, state, tt.podToTriggerPreemption); !status.IsSuccess() {
t.Errorf("Unexpected PreFilter Status: %v", status)
}
// Trigger preemption. Given custom PreemptPod implementation, the async preemption will not finish until
// finishPreemption is closed.
if tt.features.EnableGenericWorkload && tt.podToTriggerPreemption.Spec.SchedulingGroup != nil {
name := *tt.podToTriggerPreemption.Spec.SchedulingGroup.PodGroupName
namespace := tt.podToTriggerPreemption.Namespace
pg, err := snapshot.PodGroups().Get(namespace, name)
if err != nil {
t.Fatalf("PodGroup not found: %v", err)
}
pgInfo := &framework.PodGroupInfo{
Name: name,
Namespace: namespace,
UnscheduledPods: []*v1.Pod{tt.podToTriggerPreemption},
PodGroup: pg,
}
var pgSchedulingFunc fwk.PodGroupSchedulingFunc = func(_ context.Context) (*fwk.PodGroupAssignments, *fwk.Status) {
nodeInfo, _ := f.SnapshotSharedLister().NodeInfos().Get("node1")
if len(nodeInfo.GetPods()) == 0 {
triggerPodInfo, _ := framework.NewPodInfo(tt.podToTriggerPreemption)
checkPodInfo, _ := framework.NewPodInfo(tt.podToCheck)
return &fwk.PodGroupAssignments{
ProposedAssignments: []fwk.ProposedAssignment{
&mockProposedAssignment{
podInfo: triggerPodInfo,
nodeName: "node1",
cycleState: framework.NewCycleState(),
},
&mockProposedAssignment{
podInfo: checkPodInfo,
nodeName: "node1",
cycleState: framework.NewCycleState(),
},
},
}, fwk.NewStatus(fwk.Success)
}
return nil, fwk.NewStatus(fwk.Unschedulable, "need to preempt")
}
p.PodGroupPostFilter(ctx, state, pgInfo, pgSchedulingFunc)
} else {
p.PostFilter(ctx, state, tt.podToTriggerPreemption, filteredNodesStatuses)
}
status := p.PreEnqueue(ctx, tt.podToCheck)
close(finishPreemption)
if diff := cmp.Diff(tt.wantStatus, status); diff != "" {
t.Errorf("Unexpected status (-want, +got):\n%s", diff)
}
})
}
}
func TestDefaultPreemption_PodGroupPostFilter_ErrorWrapping(t *testing.T) {
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Create a node and a pod with an empty UID to induce a raw cache failure during preemption.
node := st.MakeNode().Name("node1").Capacity(veryLargeRes).Obj()
invalidPod := st.MakePod().Name("pod-empty-uid").UID("").Node("node1").Priority(lowPriority).Obj()
preemptorPG := st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).Obj()
testPods := []*v1.Pod{invalidPod}
nodes := []*v1.Node{node}
client := clientsetfake.NewClientset(invalidPod, preemptorPG)
informerFactory := informers.NewSharedInformerFactory(client, 0)
registeredPlugins := []tf.RegisterPluginFunc{
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
}
cache := internalcache.New(ctx, nil, true)
cache.AddPodGroup(preemptorPG)
snapshot := internalcache.NewTestSnapshotWithPodGroups(testPods, nodes, []*v1alpha3.PodGroup{preemptorPG})
f, err := tf.NewFramework(ctx, registeredPlugins, "",
frameworkruntime.WithClientSet(client),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithLogger(logger),
frameworkruntime.WithPodGroupManager(cache),
)
if err != nil {
t.Fatal(err)
}
features := feature.Features{
EnableGenericWorkload: true,
}
pl, err := New(ctx, getDefaultDefaultPreemptionArgs(), f, features)
if err != nil {
t.Fatal(err)
}
preemptorPods := []*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()}
mockSchedulingFunc := func(ctx context.Context) (*fwk.PodGroupAssignments, *fwk.Status) {
return nil, fwk.NewStatus(fwk.Unschedulable)
}
pgInfo := &framework.PodGroupInfo{
Name: preemptorPG.Name,
Namespace: preemptorPG.Namespace,
UnscheduledPods: preemptorPods,
PodGroup: preemptorPG,
}
state := framework.NewCycleState()
_, gotStatus := pl.PodGroupPostFilter(ctx, state, pgInfo, mockSchedulingFunc)
if gotStatus.Code() != fwk.Error {
t.Fatalf("Expected status code %v, got status: %v", fwk.Error, gotStatus)
}
expectedMsg := "pod group preemption: cannot get cache key for pod with empty UID"
gotMsg := gotStatus.Message()
if gotMsg != expectedMsg {
t.Errorf("Expected wrapped error message %q, got %q", expectedMsg, gotMsg)
}
}
func TestDefaultPreemption_PodGroupPostFilter_SchedulingConstraints(t *testing.T) {
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
node := st.MakeNode().Name("node1").Capacity(veryLargeRes).Obj()
pod := st.MakePod().Name("pod1").Node("node1").Priority(lowPriority).Obj()
pgWithConstraints := st.MakePodGroup().Name("preemptor-pg").Priority(highPriority).TopologyKey("rack").Obj()
testPods := []*v1.Pod{pod}
nodes := []*v1.Node{node}
client := clientsetfake.NewClientset(pod, pgWithConstraints)
informerFactory := informers.NewSharedInformerFactory(client, 0)
registeredPlugins := []tf.RegisterPluginFunc{
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
}
cache := internalcache.New(ctx, nil, true)
cache.AddPodGroup(pgWithConstraints)
snapshot := internalcache.NewTestSnapshotWithPodGroups(testPods, nodes, []*v1alpha3.PodGroup{pgWithConstraints})
f, err := tf.NewFramework(ctx, registeredPlugins, "",
frameworkruntime.WithClientSet(client),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(snapshot),
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithLogger(logger),
frameworkruntime.WithPodGroupManager(cache),
)
if err != nil {
t.Fatal(err)
}
features := feature.Features{
EnableGenericWorkload: true,
}
pl, err := New(ctx, getDefaultDefaultPreemptionArgs(), f, features)
if err != nil {
t.Fatal(err)
}
preemptorPods := []*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()}
mockSchedulingFunc := func(ctx context.Context) (*fwk.PodGroupAssignments, *fwk.Status) {
return nil, nil
}
pgInfo := &framework.PodGroupInfo{
Name: pgWithConstraints.Name,
Namespace: pgWithConstraints.Namespace,
UnscheduledPods: preemptorPods,
PodGroup: pgWithConstraints,
}
state := framework.NewCycleState()
_, gotStatus := pl.PodGroupPostFilter(ctx, state, pgInfo, mockSchedulingFunc)
if gotStatus.Code() != fwk.Unschedulable {
t.Fatalf("Expected status code %v, got status: %v", fwk.Unschedulable, gotStatus)
}
expectedMsg := "pod group preemption: not supported with topology constraints"
if gotStatus.Message() != expectedMsg {
t.Errorf("Expected error message %q, got %q", expectedMsg, gotStatus.Message())
}
}
func TestDefaultPreemption_PodGroupPostFilter_InvalidSnapshot(t *testing.T) {
tests := []struct {
name string
endMutationError error
startMutationError error
expectedMsg string
}{
{
name: "start mutation error",
endMutationError: nil,
startMutationError: errors.New("start mutation error"),
expectedMsg: "pod group preemption: failed to start mutations: start mutation error",
},
{
name: "end mutation error",
endMutationError: errors.New("end mutation error"),
startMutationError: nil,
expectedMsg: "pod group preemption: failed to end mutations: end mutation error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger, ctx := ktesting.NewTestContext(t)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
node := st.MakeNode().Name("node1").Capacity(veryLargeRes).Obj()
pod := st.MakePod().Name("pod1").Node("node1").Priority(lowPriority).Obj()
pgOk := st.MakePodGroup().Name("preemptor-pg-ok").Priority(highPriority).Obj()
testPods := []*v1.Pod{pod}
nodes := []*v1.Node{node}
client := clientsetfake.NewClientset(pod, pgOk)
informerFactory := informers.NewSharedInformerFactory(client, 0)
registeredPlugins := []tf.RegisterPluginFunc{
tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New),
tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New),
}
cache := internalcache.New(ctx, nil, true)
cache.AddPodGroup(pgOk)
snapshot := internalcache.NewTestSnapshotWithPodGroups(testPods, nodes, []*v1alpha3.PodGroup{pgOk})
f, err := tf.NewFramework(ctx, registeredPlugins, "",
frameworkruntime.WithClientSet(client),
frameworkruntime.WithSnapshotSharedLister(snapshot),
frameworkruntime.WithMutableSnapshotLister(&mockMutableSnapshotLister{MutableSnapshotSharedLister: snapshot, startMutationError: tt.startMutationError, endMutationError: tt.endMutationError}), // not concrete *cache.Snapshot
frameworkruntime.WithInformerFactory(informerFactory),
frameworkruntime.WithLogger(logger),
frameworkruntime.WithPodGroupManager(cache),
)
if err != nil {
t.Fatal(err)
}
features := feature.Features{
EnableGenericWorkload: true,
}
pl, err := New(ctx, getDefaultDefaultPreemptionArgs(), f, features)
if err != nil {
t.Fatal(err)
}
preemptorPods := []*v1.Pod{st.MakePod().Name("p").UID("p").Priority(highPriority).Obj()}
mockSchedulingFunc := func(ctx context.Context) (*fwk.PodGroupAssignments, *fwk.Status) {
return nil, fwk.NewStatus(fwk.Unschedulable)
}
pgInfo := &framework.PodGroupInfo{
Name: pgOk.GetName(),
Namespace: pgOk.GetNamespace(),
UnscheduledPods: preemptorPods,
PodGroup: pgOk,
}
state := framework.NewCycleState()
_, gotStatus := pl.PodGroupPostFilter(ctx, state, pgInfo, mockSchedulingFunc)
if gotStatus.Code() != fwk.Error {
t.Fatalf("Expected status code %v, got status: %v", fwk.Error, gotStatus)
}
if gotStatus.Message() != tt.expectedMsg {
t.Errorf("Expected error message %q, got %q", tt.expectedMsg, gotStatus.Message())
}
})
}
}
type mockMutableSnapshotLister struct {
fwk.MutableSnapshotSharedLister
startMutationError error
endMutationError error
}
func (m *mockMutableSnapshotLister) AddPod(podInfo fwk.PodInfo, nodeName string) error {
return nil
}
func (m *mockMutableSnapshotLister) RemovePod(logger klog.Logger, pod *v1.Pod, nodeName string) error {
return nil
}
func (m *mockMutableSnapshotLister) StartMutations() error {
return m.startMutationError
}
func (m *mockMutableSnapshotLister) EndMutations() error {
return m.endMutationError
}