mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-06-19 14:33:44 -04:00
7330 lines
275 KiB
Go
7330 lines
275 KiB
Go
/*
|
|
Copyright 2017 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 queue
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
v1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
"k8s.io/client-go/informers"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
|
"k8s.io/component-base/metrics/testutil"
|
|
"k8s.io/klog/v2"
|
|
fwk "k8s.io/kube-scheduler/framework"
|
|
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
|
|
"k8s.io/kubernetes/pkg/features"
|
|
"k8s.io/kubernetes/pkg/scheduler/framework"
|
|
plfeature "k8s.io/kubernetes/pkg/scheduler/framework/plugins/feature"
|
|
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/names"
|
|
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/queuesort"
|
|
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/schedulinggates"
|
|
"k8s.io/kubernetes/pkg/scheduler/metrics"
|
|
st "k8s.io/kubernetes/pkg/scheduler/testing"
|
|
"k8s.io/kubernetes/test/utils/ktesting"
|
|
testingclock "k8s.io/utils/clock/testing"
|
|
)
|
|
|
|
const queueMetricMetadata = `
|
|
# HELP scheduler_queue_incoming_pods_total [STABLE] Number of pods added to scheduling queues by event and queue type.
|
|
# TYPE scheduler_queue_incoming_pods_total counter
|
|
`
|
|
|
|
var (
|
|
// nodeAdd is the event when a new node is added to the cluster.
|
|
nodeAdd = fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Add}
|
|
// pvAdd is the event when a persistent volume is added in the cluster.
|
|
pvAdd = fwk.ClusterEvent{Resource: fwk.PersistentVolume, ActionType: fwk.Add}
|
|
// pvUpdate is the event when a persistent volume is updated in the cluster.
|
|
pvUpdate = fwk.ClusterEvent{Resource: fwk.PersistentVolume, ActionType: fwk.Update}
|
|
// pvcAdd is the event when a persistent volume claim is added in the cluster.
|
|
pvcAdd = fwk.ClusterEvent{Resource: fwk.PersistentVolumeClaim, ActionType: fwk.Add}
|
|
// csiNodeUpdate is the event when a CSI node is updated in the cluster.
|
|
csiNodeUpdate = fwk.ClusterEvent{Resource: fwk.CSINode, ActionType: fwk.Update}
|
|
|
|
lowPriority, midPriority, highPriority = int32(0), int32(100), int32(1000)
|
|
mediumPriority = (lowPriority + highPriority) / 2
|
|
|
|
highPriorityPodInfo = mustNewPodInfo(
|
|
st.MakePod().Name("hpp").Namespace("ns1").UID("hppns1").Priority(highPriority).Obj(),
|
|
)
|
|
highPriNominatedPodInfo = mustNewPodInfo(
|
|
st.MakePod().Name("hpp").Namespace("ns1").UID("hppns1").Priority(highPriority).NominatedNodeName("node1").Obj(),
|
|
)
|
|
medPriorityPodInfo = mustNewPodInfo(
|
|
st.MakePod().Name("mpp").Namespace("ns2").UID("mppns2").Annotation("annot2", "val2").Priority(mediumPriority).NominatedNodeName("node1").Obj(),
|
|
)
|
|
unschedulablePodInfo = mustNewPodInfo(
|
|
st.MakePod().Name("up").Namespace("ns1").UID("upns1").Annotation("annot2", "val2").Priority(lowPriority).NominatedNodeName("node1").Condition(v1.PodScheduled, v1.ConditionFalse, v1.PodReasonUnschedulable).Obj(),
|
|
)
|
|
nonExistentPodInfo = mustNewPodInfo(
|
|
st.MakePod().Name("ne").Namespace("ns1").UID("nens1").Obj(),
|
|
)
|
|
scheduledPodInfo = mustNewPodInfo(
|
|
st.MakePod().Name("sp").Namespace("ns1").UID("spns1").Node("foo").Obj(),
|
|
)
|
|
|
|
nominatorCmpOpts = []cmp.Option{
|
|
cmp.AllowUnexported(nominator{}, podRef{}),
|
|
cmpopts.IgnoreFields(nominator{}, "podLister", "nLock"),
|
|
}
|
|
|
|
queueHintReturnQueue = func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
|
|
return fwk.Queue, nil
|
|
}
|
|
queueHintReturnSkip = func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
|
|
return fwk.QueueSkip, nil
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
metrics.Register()
|
|
}
|
|
|
|
func setQueuedPodInfoGated(queuedPodInfo *framework.QueuedPodInfo, gatingPlugin string, gatingPluginEvents []fwk.ClusterEvent) *framework.QueuedPodInfo {
|
|
queuedPodInfo.GatingPlugin = gatingPlugin
|
|
// GatingPlugin should also be registered in UnschedulablePlugins.
|
|
queuedPodInfo.UnschedulablePlugins = sets.New(gatingPlugin)
|
|
queuedPodInfo.GatingPluginEvents = gatingPluginEvents
|
|
return queuedPodInfo
|
|
}
|
|
|
|
func getUnschedulablePod(p *PriorityQueue, pod *v1.Pod) *v1.Pod {
|
|
pInfo := p.unschedulableEntities.get(newQueuedPodInfoForLookup(pod))
|
|
if pInfo != nil {
|
|
return pInfo.(*framework.QueuedPodInfo).Pod
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// makeEmptyQueueingHintMapPerProfile initializes an empty QueueingHintMapPerProfile for "" profile name.
|
|
func makeEmptyQueueingHintMapPerProfile() QueueingHintMapPerProfile {
|
|
m := make(QueueingHintMapPerProfile)
|
|
m[""] = make(QueueingHintMap)
|
|
return m
|
|
}
|
|
|
|
func withPodGroupName(pod *v1.Pod, podGroupName string) *v1.Pod {
|
|
pod = pod.DeepCopy()
|
|
pod.Spec.SchedulingGroup = &v1.PodSchedulingGroup{PodGroupName: &podGroupName}
|
|
return pod
|
|
}
|
|
|
|
func TestPriorityQueue_Add(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
usePodGroups bool
|
|
genericWorkloadEnabled bool
|
|
}{
|
|
{
|
|
name: "individual pod with GenericWorkload gate disabled",
|
|
usePodGroups: false,
|
|
genericWorkloadEnabled: false,
|
|
},
|
|
{
|
|
name: "individual pod with GenericWorkload gate enabled",
|
|
usePodGroups: false,
|
|
genericWorkloadEnabled: true,
|
|
},
|
|
{
|
|
name: "pod group member with GenericWorkload gate enabled",
|
|
usePodGroups: true,
|
|
genericWorkloadEnabled: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.GenericWorkload, tt.genericWorkloadEnabled)
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
var medPod, unschedPod, highPod = medPriorityPodInfo.Pod, unschedulablePodInfo.Pod, highPriorityPodInfo.Pod
|
|
if tt.usePodGroups {
|
|
medPod = withPodGroupName(medPod, "pg-med")
|
|
unschedPod = withPodGroupName(unschedPod, "pg-unsched")
|
|
highPod = withPodGroupName(highPod, "pg-high")
|
|
}
|
|
|
|
objs := []runtime.Object{medPod, unschedPod, highPod}
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), objs)
|
|
q.Add(ctx, medPod)
|
|
q.Add(ctx, unschedPod)
|
|
q.Add(ctx, highPod)
|
|
expectedNominatedPods := &nominator{
|
|
nominatedPodToNode: map[types.UID]string{
|
|
medPod.UID: "node1",
|
|
unschedPod.UID: "node1",
|
|
},
|
|
nominatedPods: map[string][]podRef{
|
|
"node1": {podToRef(medPod), podToRef(unschedPod)},
|
|
},
|
|
}
|
|
if diff := cmp.Diff(q.nominator, expectedNominatedPods, nominatorCmpOpts...); diff != "" {
|
|
t.Errorf("Unexpected diff after adding pods (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
getPod := func(entity framework.QueuedEntityInfo) *v1.Pod {
|
|
if tt.usePodGroups {
|
|
return entity.(*framework.QueuedPodGroupInfo).QueuedPodInfos[0].Pod
|
|
}
|
|
return entity.(*framework.QueuedPodInfo).Pod
|
|
}
|
|
|
|
if entity, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(highPod, getPod(entity)); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
if entity, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(medPod, getPod(entity)); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
if entity, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(unschedPod, getPod(entity)); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
if len(q.nominator.nominatedPods["node1"]) != 2 {
|
|
t.Errorf("Expected medPod and unschedPod to be still present in nominatedPods: %v", q.nominator.nominatedPods["node1"])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_AddNominatedGatedPod(t *testing.T) {
|
|
gatedPod := st.MakePod().Name("pod-gated").Namespace("ns1").UID("pod-gated").NominatedNodeName("node1").Obj()
|
|
objs := []runtime.Object{gatedPod}
|
|
_, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
plugin := &preEnqueuePlugin{allowlists: []string{"allow"}}
|
|
m := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
"preEnqueuePlugin": plugin,
|
|
},
|
|
}
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), objs, WithPreEnqueuePluginMap(m))
|
|
q.Add(ctx, gatedPod)
|
|
|
|
// Verify the pod is gated
|
|
pInfo := q.unschedulableEntities.get(newQueuedPodInfoForLookup(gatedPod))
|
|
if pInfo == nil || !pInfo.Gated() {
|
|
t.Fatalf("Expected pod to be gated in unschedulableEntities")
|
|
}
|
|
|
|
// Verify the pod is added to nominator
|
|
if len(q.nominator.nominatedPods["node1"]) != 1 {
|
|
t.Errorf("Expected pod-gated in nominatedPods")
|
|
}
|
|
if q.nominator.nominatedPodToNode[gatedPod.UID] != "node1" {
|
|
t.Errorf("Expected pod-gated in nominatedPodToNode")
|
|
}
|
|
}
|
|
|
|
func newDefaultQueueSort() fwk.LessFunc {
|
|
sort := &queuesort.PrioritySort{}
|
|
return sort.Less
|
|
}
|
|
|
|
func TestPriorityQueue_AddWithReversePriorityLessFunc(t *testing.T) {
|
|
objs := []runtime.Object{medPriorityPodInfo.Pod, highPriorityPodInfo.Pod}
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), objs)
|
|
q.Add(ctx, medPriorityPodInfo.Pod)
|
|
q.Add(ctx, highPriorityPodInfo.Pod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(highPriorityPodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(medPriorityPodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func Test_InFlightPods(t *testing.T) {
|
|
logger, _ := ktesting.NewTestContext(t)
|
|
pod1 := st.MakePod().Name("targetpod").UID("pod1").Obj()
|
|
pod2 := st.MakePod().Name("targetpod2").UID("pod2").Obj()
|
|
pod3 := st.MakePod().Name("targetpod3").UID("pod3").Obj()
|
|
|
|
pgName := "pg-test"
|
|
pgPod1 := st.MakePod().Name("pgpod1").UID("pgpod1").PodGroupName(pgName).Obj()
|
|
pgPod2 := st.MakePod().Name("pgpod2").UID("pgpod2").PodGroupName(pgName).Obj()
|
|
|
|
var poppedPod, poppedPod2 *framework.QueuedPodInfo
|
|
|
|
type action struct {
|
|
// ONLY ONE of the following should be set.
|
|
eventHappens *fwk.ClusterEvent
|
|
podPopped *v1.Pod
|
|
// podCreated is the Pod that is created and inserted into the activeQ.
|
|
podCreated *v1.Pod
|
|
// podEnqueued is the Pod that is enqueued back to activeQ.
|
|
podEnqueued *framework.QueuedPodInfo
|
|
// podGroupAttempted is the PodGroup that was attempted to schedule.
|
|
podGroupAttempted *framework.QueuedPodGroupInfo
|
|
callback func(t *testing.T, q *PriorityQueue)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
queueingHintMap QueueingHintMapPerProfile
|
|
// initialPods is the initial Pods in the activeQ.
|
|
initialPods []*v1.Pod
|
|
actions []action
|
|
genericWorkloadEnabled []bool
|
|
wantInFlightPods []*v1.Pod
|
|
wantInFlightEvents []interface{}
|
|
wantActiveQPodNames []string
|
|
wantBackoffQPodNames []string
|
|
wantUnschedPodPoolPodNames []string
|
|
}{
|
|
{
|
|
name: "Pod and interested events are registered in inFlightPods/inFlightEvents",
|
|
initialPods: []*v1.Pod{pod1},
|
|
actions: []action{
|
|
// This won't be added to inFlightEvents because no inFlightPods at this point.
|
|
{eventHappens: &pvcAdd},
|
|
{podPopped: pod1},
|
|
// This gets added for the pod.
|
|
{eventHappens: &pvAdd},
|
|
// This doesn't get added because no plugin is interested in PvUpdate.
|
|
{eventHappens: &pvUpdate},
|
|
},
|
|
wantInFlightPods: []*v1.Pod{pod1},
|
|
wantInFlightEvents: []interface{}{pod1, pvAdd},
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Pod, registered in inFlightPods, is enqueued back to backoffQ",
|
|
initialPods: []*v1.Pod{pod1, pod2},
|
|
actions: []action{
|
|
// This won't be added to inFlightEvents because no inFlightPods at this point.
|
|
{eventHappens: &pvcAdd},
|
|
{podPopped: pod1},
|
|
{eventHappens: &pvAdd},
|
|
{podPopped: pod2},
|
|
{eventHappens: &nodeAdd},
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod1)},
|
|
},
|
|
wantBackoffQPodNames: []string{"targetpod"},
|
|
wantInFlightPods: []*v1.Pod{pod2}, // only pod2 is registered because pod is already enqueued back.
|
|
wantInFlightEvents: []interface{}{pod2, nodeAdd},
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
nodeAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
pvcAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "All Pods registered in inFlightPods are enqueued back to activeQ",
|
|
initialPods: []*v1.Pod{pod1, pod2},
|
|
actions: []action{
|
|
// This won't be added to inFlightEvents because no inFlightPods at this point.
|
|
{eventHappens: &pvcAdd},
|
|
{podPopped: pod1},
|
|
{eventHappens: &pvAdd},
|
|
{podPopped: pod2},
|
|
{eventHappens: &nodeAdd},
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod1)},
|
|
{eventHappens: &csiNodeUpdate},
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod2)},
|
|
},
|
|
wantBackoffQPodNames: []string{"targetpod", "targetpod2"},
|
|
wantInFlightPods: nil, // empty
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
nodeAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
pvcAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
csiNodeUpdate: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "One intermediate Pod registered in inFlightPods is enqueued back to activeQ",
|
|
initialPods: []*v1.Pod{pod1, pod2, pod3},
|
|
actions: []action{
|
|
// This won't be added to inFlightEvents because no inFlightPods at this point.
|
|
{eventHappens: &pvcAdd},
|
|
{podPopped: pod1},
|
|
{eventHappens: &pvAdd},
|
|
{podPopped: pod2},
|
|
{eventHappens: &nodeAdd},
|
|
// This Pod won't be requeued again.
|
|
{podPopped: pod3},
|
|
{eventHappens: &framework.EventAssignedPodAdd},
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod2)},
|
|
},
|
|
wantBackoffQPodNames: []string{"targetpod2"},
|
|
wantInFlightPods: []*v1.Pod{pod1, pod3},
|
|
wantInFlightEvents: []interface{}{pod1, pvAdd, nodeAdd, pod3, framework.EventAssignedPodAdd},
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
nodeAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
framework.EventAssignedPodAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "events before popping Pod are ignored when Pod is enqueued back to queue",
|
|
initialPods: []*v1.Pod{pod1},
|
|
actions: []action{
|
|
{eventHappens: &framework.EventUnschedulableTimeout},
|
|
{podPopped: pod1},
|
|
{eventHappens: &framework.EventAssignedPodAdd},
|
|
// This Pod won't be requeued to activeQ/backoffQ because fooPlugin1 returns QueueSkip.
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod1, "fooPlugin1")},
|
|
},
|
|
wantUnschedPodPoolPodNames: []string{"targetpod"},
|
|
wantInFlightPods: nil,
|
|
wantInFlightEvents: nil,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
// fooPlugin1 has a queueing hint function for framework.AssignedPodAdd,
|
|
// but hint fn tells that this event doesn't make a Pod scheudlable.
|
|
framework.EventAssignedPodAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnSkip,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "pod is enqueued to backoff if no failed plugin",
|
|
initialPods: []*v1.Pod{pod1},
|
|
actions: []action{
|
|
{podPopped: pod1},
|
|
{eventHappens: &framework.EventAssignedPodAdd},
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod1)},
|
|
},
|
|
wantBackoffQPodNames: []string{"targetpod"},
|
|
wantInFlightPods: nil,
|
|
wantInFlightEvents: nil,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
// It will be ignored because no failed plugin.
|
|
framework.EventAssignedPodAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "pod is enqueued to unschedulable pod pool if no events that can make the pod schedulable",
|
|
initialPods: []*v1.Pod{pod1},
|
|
actions: []action{
|
|
{podPopped: pod1},
|
|
{eventHappens: &nodeAdd},
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod1, "fooPlugin1")},
|
|
},
|
|
wantUnschedPodPoolPodNames: []string{"targetpod"},
|
|
wantInFlightPods: nil,
|
|
wantInFlightEvents: nil,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
// fooPlugin1 has no queueing hint function for NodeAdd.
|
|
framework.EventAssignedPodAdd: {
|
|
{
|
|
// It will be ignored because the event is not NodeAdd.
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "pod is enqueued to unschedulable pod pool because the failed plugin has a hint fn but it returns Skip",
|
|
initialPods: []*v1.Pod{pod1},
|
|
actions: []action{
|
|
{podPopped: pod1},
|
|
{eventHappens: &framework.EventAssignedPodAdd},
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod1, "fooPlugin1")},
|
|
},
|
|
wantUnschedPodPoolPodNames: []string{"targetpod"},
|
|
wantInFlightPods: nil,
|
|
wantInFlightEvents: nil,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
// fooPlugin1 has a queueing hint function for framework.AssignedPodAdd,
|
|
// but hint fn tells that this event doesn't make a Pod scheudlable.
|
|
framework.EventAssignedPodAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnSkip,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "pod is enqueued to activeQ because the Pending plugins has a hint fn and it returns Queue",
|
|
initialPods: []*v1.Pod{pod1},
|
|
actions: []action{
|
|
{podPopped: pod1},
|
|
{eventHappens: &framework.EventAssignedPodAdd},
|
|
{podEnqueued: &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(pod1),
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin2", "fooPlugin3"),
|
|
PendingPlugins: sets.New("fooPlugin1"),
|
|
},
|
|
}},
|
|
},
|
|
wantActiveQPodNames: []string{"targetpod"},
|
|
wantInFlightPods: nil,
|
|
wantInFlightEvents: nil,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
framework.EventAssignedPodAdd: {
|
|
{
|
|
PluginName: "fooPlugin3",
|
|
QueueingHintFn: queueHintReturnSkip,
|
|
},
|
|
{
|
|
PluginName: "fooPlugin2",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
// The hint fn tells that this event makes a Pod scheudlable immediately.
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "pod is enqueued to backoffQ because the failed plugin has a hint fn and it returns Queue",
|
|
initialPods: []*v1.Pod{pod1},
|
|
actions: []action{
|
|
{podPopped: pod1},
|
|
{eventHappens: &framework.EventAssignedPodAdd},
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod1, "fooPlugin1", "fooPlugin2")},
|
|
},
|
|
wantBackoffQPodNames: []string{"targetpod"},
|
|
wantInFlightPods: nil,
|
|
wantInFlightEvents: nil,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
framework.EventAssignedPodAdd: {
|
|
{
|
|
// it will be ignored because the hint fn returns Skip that is weaker than queueHintReturnQueue from fooPlugin1.
|
|
PluginName: "fooPlugin2",
|
|
QueueingHintFn: queueHintReturnSkip,
|
|
},
|
|
{
|
|
// The hint fn tells that this event makes a Pod schedulable.
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "pod is enqueued to activeQ because the pending plugin has a hint fn and it returns Queue for a concurrent event that was received while some other pod was in flight",
|
|
initialPods: []*v1.Pod{pod1, pod2},
|
|
actions: []action{
|
|
{callback: func(t *testing.T, q *PriorityQueue) { poppedPod = popPod(t, logger, q, pod1) }},
|
|
{eventHappens: &nodeAdd},
|
|
{callback: func(t *testing.T, q *PriorityQueue) { poppedPod2 = popPod(t, logger, q, pod2) }},
|
|
{eventHappens: &framework.EventAssignedPodAdd},
|
|
{callback: func(t *testing.T, q *PriorityQueue) {
|
|
logger, _ := ktesting.NewTestContext(t)
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, poppedPod, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
}},
|
|
{callback: func(t *testing.T, q *PriorityQueue) {
|
|
logger, _ := ktesting.NewTestContext(t)
|
|
poppedPod2.UnschedulablePlugins = sets.New("fooPlugin2", "fooPlugin3")
|
|
poppedPod2.PendingPlugins = sets.New("fooPlugin1")
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, poppedPod2, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
}},
|
|
},
|
|
wantActiveQPodNames: []string{pod2.Name},
|
|
wantBackoffQPodNames: []string{pod1.Name},
|
|
wantInFlightPods: nil,
|
|
wantInFlightEvents: nil,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
framework.EventAssignedPodAdd: {
|
|
{
|
|
// it will be ignored because the hint fn returns QueueSkip that is weaker than queueHintReturnQueueImmediately from fooPlugin1.
|
|
PluginName: "fooPlugin3",
|
|
QueueingHintFn: queueHintReturnSkip,
|
|
},
|
|
{
|
|
// it will be ignored because the fooPlugin2 is registered in UnschedulablePlugins and it's interpret as Queue that is weaker than QueueImmediately from fooPlugin1.
|
|
PluginName: "fooPlugin2",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
// The hint fn tells that this event makes a Pod scheudlable.
|
|
// Given fooPlugin1 is registered as Pendings, we interpret Queue as queueImmediately.
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "popped pod preserves UnschedulablePlugins and PendingPlugins",
|
|
initialPods: []*v1.Pod{pod1},
|
|
actions: []action{
|
|
{callback: func(t *testing.T, q *PriorityQueue) { poppedPod = popPod(t, logger, q, pod1) }},
|
|
{callback: func(t *testing.T, q *PriorityQueue) {
|
|
logger, _ := ktesting.NewTestContext(t)
|
|
// Unschedulable due to PendingPlugins.
|
|
poppedPod.PendingPlugins = sets.New("fooPlugin1")
|
|
poppedPod.UnschedulablePlugins = sets.New("fooPlugin2")
|
|
if err := q.AddUnschedulablePodIfNotPresent(logger, poppedPod, q.SchedulingCycle()); err != nil {
|
|
t.Errorf("Unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
}},
|
|
{eventHappens: &pvAdd}, // Active again.
|
|
{callback: func(t *testing.T, q *PriorityQueue) {
|
|
poppedPod = popPod(t, logger, q, pod1)
|
|
// UnschedulablePlugins should be preserved for logging/debugging
|
|
if !poppedPod.GetUnschedulablePlugins().Equal(sets.New("fooPlugin2")) {
|
|
t.Errorf("QueuedPodInfo from Pop should preserve UnschedulablePlugins, expected fooPlugin2, got: %+v", poppedPod.GetUnschedulablePlugins())
|
|
}
|
|
// PendingPlugins are preserved after Pop() for logging
|
|
if !poppedPod.PendingPlugins.Equal(sets.New("fooPlugin1")) {
|
|
t.Errorf("QueuedPodInfo from Pop should preserve PendingPlugins, expected fooPlugin1, got: %+v", poppedPod.PendingPlugins)
|
|
}
|
|
}},
|
|
{callback: func(t *testing.T, q *PriorityQueue) {
|
|
logger, _ := ktesting.NewTestContext(t)
|
|
// Failed (i.e. no UnschedulablePlugins). Should go to backoff.
|
|
if err := q.AddUnschedulablePodIfNotPresent(logger, poppedPod, q.SchedulingCycle()); err != nil {
|
|
t.Errorf("Unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
}},
|
|
},
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
// The hint fn tells that this event makes a Pod scheudlable immediately.
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// This scenario shouldn't happen unless we make the similar bug like https://github.com/kubernetes/kubernetes/issues/118226.
|
|
// But, given the bug could make a serious memory leak and likely would be hard to detect,
|
|
// we should have a safe guard from the same bug so that, at least, we can prevent the memory leak.
|
|
name: "Pop is made twice for the same Pod, but the cleanup still happen correctly",
|
|
initialPods: []*v1.Pod{pod1, pod2},
|
|
actions: []action{
|
|
// This won't be added to inFlightEvents because no inFlightPods at this point.
|
|
{eventHappens: &pvcAdd},
|
|
{podPopped: pod1},
|
|
{eventHappens: &pvAdd},
|
|
{podPopped: pod2},
|
|
// Simulate a bug, putting pod into activeQ, while pod is being scheduled.
|
|
{callback: func(t *testing.T, q *PriorityQueue) {
|
|
q.activeQ.add(logger, newQueuedPodInfoForLookup(pod1), framework.EventUnscheduledPodAdd.Label())
|
|
}},
|
|
// At this point, in the activeQ, we have pod1 and pod3 in this order.
|
|
{podCreated: pod3},
|
|
// pod3 is poped, not pod1.
|
|
// In detail, this Pop() first tries to pop pod1, but it's already being scheduled and hence discarded.
|
|
// Then, it pops the next pod, pod3.
|
|
{podPopped: pod3},
|
|
{callback: func(t *testing.T, q *PriorityQueue) {
|
|
// Make sure that pod1 is discarded and hence no pod in activeQ.
|
|
if len(q.activeQ.list()) != 0 {
|
|
t.Fatalf("activeQ should be empty, but got: %v", q.activeQ.list())
|
|
}
|
|
}},
|
|
{eventHappens: &nodeAdd},
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod1)},
|
|
{eventHappens: &csiNodeUpdate},
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod2)},
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod3)},
|
|
},
|
|
wantBackoffQPodNames: []string{"targetpod", "targetpod2", "targetpod3"},
|
|
wantInFlightPods: nil, // should be empty
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
nodeAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
pvcAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
csiNodeUpdate: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Pod group members and interested events are registered in inFlightPods/inFlightEvents",
|
|
genericWorkloadEnabled: []bool{true},
|
|
initialPods: []*v1.Pod{pgPod1, pgPod2},
|
|
actions: []action{
|
|
{podPopped: pgPod1}, // Pops group, so pgPod1 and pgPod2 are inFlight
|
|
{eventHappens: &pvAdd},
|
|
},
|
|
wantInFlightPods: []*v1.Pod{pgPod1, pgPod2},
|
|
wantInFlightEvents: []any{pgPod1, pgPod2, pvAdd},
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Pop is made twice for all pods in the group, but the cleanup still happens correctly",
|
|
genericWorkloadEnabled: []bool{true},
|
|
initialPods: []*v1.Pod{pgPod1, pgPod2},
|
|
actions: []action{
|
|
// This won't be added to inFlightEvents because no inFlightPods at this point.
|
|
{eventHappens: &pvcAdd},
|
|
// Pop group, so pgPod1 and pgPod2 are inFlight.
|
|
{podPopped: pgPod1},
|
|
{eventHappens: &pvAdd},
|
|
{podGroupAttempted: newQueuedPodGroupInfoForLookup(pgPod1)},
|
|
// Simulate a bug: add pgPod1 and pgPod2 back to activeQ while pod group is in-flight.
|
|
{podCreated: pgPod1},
|
|
{podCreated: pgPod2},
|
|
// At this point, in the activeQ, we have pod group (with pgPod1 and pgPod2) and pod3 in this order.
|
|
{podCreated: pod3},
|
|
// pod3 is poped, not pgPod1.
|
|
// In detail, this Pop() first tries to pop pod group with pgPod1 and pgPod2, but it's already being scheduled and hence discarded.
|
|
// Then, it pops the next pod, pod3.
|
|
{podPopped: pod3},
|
|
{callback: func(t *testing.T, q *PriorityQueue) {
|
|
// Make sure that pod group is discarded and hence no entity in activeQ.
|
|
if len(q.activeQ.list()) != 0 {
|
|
t.Fatalf("activeQ should be empty, but got: %v", q.activeQ.list())
|
|
}
|
|
}},
|
|
{eventHappens: &nodeAdd},
|
|
// This pod will be requeued to activeQ because it's a pod group member and the queued pod group itself is missing.
|
|
{podEnqueued: newQueuedPodInfoForLookup(pgPod1)},
|
|
{eventHappens: &csiNodeUpdate},
|
|
// This pod will be requeued to activeQ because it's a pod group member and the queued pod group itself is missing.
|
|
{podEnqueued: newQueuedPodInfoForLookup(pgPod2)},
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod3)},
|
|
},
|
|
wantActiveQPodNames: []string{"pgpod1", "pgpod2"},
|
|
wantBackoffQPodNames: []string{"targetpod3"},
|
|
wantInFlightPods: nil, // should be empty
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
nodeAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
pvcAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
csiNodeUpdate: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Pop is made twice for a subset (one) pod in the group, but the cleanup still happens correctly",
|
|
genericWorkloadEnabled: []bool{true},
|
|
initialPods: []*v1.Pod{pgPod1},
|
|
actions: []action{
|
|
// This won't be added to inFlightEvents because no inFlightPods at this point.
|
|
{eventHappens: &pvcAdd},
|
|
// Pop group, so pgPod1 is inFlight.
|
|
{podPopped: pgPod1},
|
|
{eventHappens: &pvAdd},
|
|
{podGroupAttempted: newQueuedPodGroupInfoForLookup(pgPod1)},
|
|
// Simulate a bug: add pgPod1 back to activeQ while pod group is in-flight.
|
|
{podCreated: pgPod1},
|
|
// Add a new, pgPod2 to activeQ.
|
|
{podCreated: pgPod2},
|
|
// At this point, in the activeQ, we have pod group (with pgPod1 and pgPod2) and pod3 in this order.
|
|
{podCreated: pod3},
|
|
// pgPod2 is popped, while pgPod1 is discarded.
|
|
{podPopped: pgPod2},
|
|
// pod3 is poped.
|
|
{podPopped: pod3},
|
|
{callback: func(t *testing.T, q *PriorityQueue) {
|
|
// Make sure that pgPod1 was discarded and hence no entity in activeQ.
|
|
if len(q.activeQ.list()) != 0 {
|
|
t.Fatalf("activeQ should be empty, but got: %v", q.activeQ.list())
|
|
}
|
|
}},
|
|
{eventHappens: &nodeAdd},
|
|
// This pod will be requeued to activeQ because it's a pod group member and the queued pod group itself is missing.
|
|
{podEnqueued: newQueuedPodInfoForLookup(pgPod1)},
|
|
{eventHappens: &csiNodeUpdate},
|
|
// This pod will be requeued to activeQ because it's a pod group member and the queued pod group itself is missing.
|
|
{podEnqueued: newQueuedPodInfoForLookup(pgPod2)},
|
|
// This pod will be requeued to backoffQ immediately because no plugin is registered as unschedulable plugin,
|
|
// which means the pod encountered an unexpected error (e.g., a network error).
|
|
{podEnqueued: newQueuedPodInfoForLookup(pod3)},
|
|
},
|
|
wantActiveQPodNames: []string{"pgpod1", "pgpod2"},
|
|
wantInFlightPods: nil, // should be empty
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
nodeAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
pvcAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
csiNodeUpdate: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "pod group is enqueued to backoff if no failed plugin",
|
|
genericWorkloadEnabled: []bool{true},
|
|
initialPods: []*v1.Pod{pgPod1, pgPod2},
|
|
actions: []action{
|
|
{podPopped: pgPod1}, // Pops group, so pgPod1 and pgPod2 are inFlight
|
|
// Requeue them sequentially with no plugins (unexpected errors)
|
|
{podEnqueued: &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(pgPod1),
|
|
}},
|
|
{podEnqueued: &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(pgPod2),
|
|
}},
|
|
{podGroupAttempted: newQueuedPodGroupInfoForLookup(pgPod1)},
|
|
},
|
|
wantBackoffQPodNames: []string{"pgpod1", "pgpod2"},
|
|
},
|
|
{
|
|
name: "pod group is enqueued to backoff even if there were no events that can make the pod group schedulable",
|
|
genericWorkloadEnabled: []bool{true},
|
|
initialPods: []*v1.Pod{pgPod1, pgPod2},
|
|
actions: []action{
|
|
{podPopped: pgPod1}, // Pops group, so pgPod1 and pgPod2 are inFlight
|
|
// Requeue them sequentially with failed plugins
|
|
{podEnqueued: &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(pgPod1),
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1"),
|
|
},
|
|
}},
|
|
{podEnqueued: &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(pgPod2),
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1"),
|
|
},
|
|
}},
|
|
{podGroupAttempted: newQueuedPodGroupInfoForLookup(pgPod1)},
|
|
},
|
|
wantBackoffQPodNames: []string{"pgpod1", "pgpod2"},
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
for _, genericWorkloadEnabled := range test.genericWorkloadEnabled {
|
|
t.Run(fmt.Sprintf("%s (genericWorkloadEnabled: %v)", test.name, genericWorkloadEnabled), func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
|
features.GenericWorkload: genericWorkloadEnabled,
|
|
})
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
obj := make([]runtime.Object, 0, len(test.initialPods))
|
|
for _, p := range test.initialPods {
|
|
obj = append(obj, p)
|
|
}
|
|
fakeClock := testingclock.NewFakeClock(time.Now())
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), obj, WithQueueingHintMapPerProfile(test.queueingHintMap), WithClock(fakeClock))
|
|
sortOpt := cmpopts.SortSlices(func(a, b string) bool { return a < b })
|
|
|
|
// When a Pod is added to the queue, the QueuedPodInfo will have a new timestamp.
|
|
// On Windows, time.Now() is not as precise, 2 consecutive calls may return the same timestamp.
|
|
// Thus, all the QueuedPodInfos can have the same timestamps, which can be an issue
|
|
// when we're expecting them to be popped in a certain order (the Less function
|
|
// sorts them by Timestamps if they have the same Pod Priority).
|
|
// Using a fake clock for the queue and incrementing it after each added Pod will
|
|
// solve this issue on Windows unit test runs.
|
|
// For more details on the Windows clock resolution issue, see: https://github.com/golang/go/issues/8687
|
|
for _, p := range test.initialPods {
|
|
q.Add(ctx, p)
|
|
fakeClock.Step(time.Second)
|
|
}
|
|
|
|
for _, action := range test.actions {
|
|
switch {
|
|
case action.podCreated != nil:
|
|
q.Add(ctx, action.podCreated)
|
|
case action.podPopped != nil:
|
|
popPod(t, logger, q, action.podPopped)
|
|
case action.eventHappens != nil:
|
|
q.MoveAllToActiveOrBackoffQueue(logger, *action.eventHappens, nil, nil, nil)
|
|
case action.podEnqueued != nil:
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, action.podEnqueued, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
case action.podGroupAttempted != nil:
|
|
err := q.AddAttemptedPodGroupIfNeeded(logger, action.podGroupAttempted, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddAttemptedPodGroupIfNeeded: %v", err)
|
|
}
|
|
case action.callback != nil:
|
|
action.callback(t, q)
|
|
}
|
|
}
|
|
|
|
actualInFlightPods := make(map[types.UID]*v1.Pod)
|
|
for _, pod := range q.activeQ.listInFlightPods() {
|
|
actualInFlightPods[pod.UID] = pod
|
|
}
|
|
wantInFlightPods := make(map[types.UID]*v1.Pod)
|
|
for _, pod := range test.wantInFlightPods {
|
|
wantInFlightPods[pod.UID] = pod
|
|
}
|
|
if diff := cmp.Diff(wantInFlightPods, actualInFlightPods); diff != "" {
|
|
t.Errorf("Unexpected diff in inFlightPods (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
var wantInFlightEvents []interface{}
|
|
for _, value := range test.wantInFlightEvents {
|
|
if event, ok := value.(fwk.ClusterEvent); ok {
|
|
value = &clusterEvent{event: event}
|
|
}
|
|
wantInFlightEvents = append(wantInFlightEvents, value)
|
|
}
|
|
if diff := cmp.Diff(wantInFlightEvents, q.activeQ.listInFlightEvents(), cmp.AllowUnexported(clusterEvent{}), cmpopts.EquateComparable(fwk.ClusterEvent{})); diff != "" {
|
|
t.Errorf("Unexpected diff in inFlightEvents (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
if test.wantActiveQPodNames != nil {
|
|
pods := q.activeQ.list()
|
|
var podNames []string
|
|
for _, pod := range pods {
|
|
podNames = append(podNames, pod.Name)
|
|
}
|
|
if diff := cmp.Diff(test.wantActiveQPodNames, podNames, sortOpt); diff != "" {
|
|
t.Fatalf("Unexpected diff of activeQ pod names (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
wantPodNames := sets.New(test.wantActiveQPodNames...)
|
|
for _, pod := range pods {
|
|
if !wantPodNames.Has(pod.Name) {
|
|
t.Fatalf("Pod %v was not expected to be in the activeQ.", pod.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
if test.wantBackoffQPodNames != nil {
|
|
pods := q.backoffQ.list()
|
|
var podNames []string
|
|
for _, pod := range pods {
|
|
podNames = append(podNames, pod.Name)
|
|
}
|
|
if diff := cmp.Diff(test.wantBackoffQPodNames, podNames, sortOpt); diff != "" {
|
|
t.Fatalf("Unexpected diff of backoffQ pod names (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
wantPodNames := sets.New(test.wantBackoffQPodNames...)
|
|
for _, podGotFromBackoffQ := range pods {
|
|
if !wantPodNames.Has(podGotFromBackoffQ.Name) {
|
|
t.Fatalf("Pod %v was not expected to be in the backoffQ.", podGotFromBackoffQ.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, podName := range test.wantUnschedPodPoolPodNames {
|
|
p := getUnschedulablePod(q, &st.MakePod().Name(podName).Pod)
|
|
if p == nil {
|
|
t.Fatalf("Pod %v was not found in the unschedulableEntities.", podName)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func popPod(t *testing.T, logger klog.Logger, q *PriorityQueue, pod *v1.Pod) *framework.QueuedPodInfo {
|
|
entity, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Fatalf("Pop failed: %v", err)
|
|
}
|
|
var pInfo *framework.QueuedPodInfo
|
|
switch specificEntity := entity.(type) {
|
|
case *framework.QueuedPodInfo:
|
|
pInfo = specificEntity
|
|
case *framework.QueuedPodGroupInfo:
|
|
for _, pi := range specificEntity.QueuedPodInfos {
|
|
if pi.Pod.UID == pod.UID {
|
|
pInfo = pi
|
|
break
|
|
}
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected popped entity type: %T", entity)
|
|
}
|
|
if pInfo == nil {
|
|
t.Fatalf("Pod %s was not found in the popped entity", pod.UID)
|
|
}
|
|
if pInfo.Pod.UID != pod.UID {
|
|
t.Errorf("Unexpected popped pod: expected %s, got %s", pod.UID, pInfo.Pod.UID)
|
|
}
|
|
return pInfo
|
|
}
|
|
|
|
func TestPop(t *testing.T) {
|
|
pod := st.MakePod().Name("targetpod").UID("pod1").Obj()
|
|
queueingHintMap := QueueingHintMapPerProfile{
|
|
"": {
|
|
pvAdd: {
|
|
{
|
|
// The hint fn tells that this event makes a Pod scheudlable.
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), []runtime.Object{pod}, WithQueueingHintMapPerProfile(queueingHintMap))
|
|
q.Add(ctx, pod)
|
|
|
|
// Simulate failed attempt that makes the pod unschedulable.
|
|
poppedPod := popPod(t, logger, q, pod)
|
|
// We put register the plugin to PendingPlugins so that it's interpreted as queueImmediately and skip backoff.
|
|
poppedPod.PendingPlugins = sets.New("fooPlugin1")
|
|
if err := q.AddUnschedulablePodIfNotPresent(logger, poppedPod, q.SchedulingCycle()); err != nil {
|
|
t.Errorf("Unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
|
|
// Activate it again.
|
|
q.MoveAllToActiveOrBackoffQueue(logger, pvAdd, nil, nil, nil)
|
|
|
|
// Now check result of Pop.
|
|
poppedPod = popPod(t, logger, q, pod)
|
|
// PendingPlugins are preserved after Pop() so they can be logged if scheduling
|
|
// succeeds, or cleared in handleSchedulingFailure() if it fails.
|
|
if !poppedPod.PendingPlugins.Equal(sets.New("fooPlugin1")) {
|
|
t.Errorf("QueuedPodInfo from Pop should preserve PendingPlugins, expected fooPlugin1, got instead: %+v", poppedPod)
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_AddUnschedulablePodIfNotPresent(t *testing.T) {
|
|
objs := []runtime.Object{highPriNominatedPodInfo.Pod, unschedulablePodInfo.Pod}
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), objs)
|
|
// insert unschedulablePodInfo and pop right after that
|
|
// because the scheduling queue records unschedulablePod as in-flight Pod.
|
|
q.Add(ctx, unschedulablePodInfo.Pod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(unschedulablePodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
q.Add(ctx, highPriNominatedPodInfo.Pod)
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, newQueuedPodInfoForLookup(unschedulablePodInfo.Pod, "plugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(highPriNominatedPodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
if len(q.nominator.nominatedPods) != 1 {
|
|
t.Errorf("Expected nominatedPods to have one element: %v", q.nominator)
|
|
}
|
|
// unschedulablePodInfo is inserted to unschedulable pod pool because no events happened during scheduling.
|
|
if diff := cmp.Diff(unschedulablePodInfo.Pod, getUnschedulablePod(q, unschedulablePodInfo.Pod)); diff != "" {
|
|
t.Errorf("Unexpected pod in unschedulableEntities (-want, +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// TestPriorityQueue_AddUnschedulablePodIfNotPresent_Backoff tests the scenarios when
|
|
// AddUnschedulablePodIfNotPresent is called asynchronously.
|
|
// Pods in and before current scheduling cycle will be put back to activeQueue
|
|
// if we were trying to schedule them when we received move request.
|
|
func TestPriorityQueue_AddUnschedulablePodIfNotPresent_Backoff(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(testingclock.NewFakeClock(time.Now())))
|
|
totalNum := 10
|
|
expectedPods := make([]v1.Pod, 0, totalNum)
|
|
for i := 0; i < totalNum; i++ {
|
|
priority := int32(i)
|
|
p := st.MakePod().Name(fmt.Sprintf("pod%d", i)).Namespace(fmt.Sprintf("ns%d", i)).UID(fmt.Sprintf("upns%d", i)).Priority(priority).Obj()
|
|
expectedPods = append(expectedPods, *p)
|
|
// priority is to make pods ordered in the PriorityQueue
|
|
q.Add(ctx, p)
|
|
}
|
|
|
|
// Pop all pods except for the first one
|
|
for i := totalNum - 1; i > 0; i-- {
|
|
p, _ := q.Pop(logger)
|
|
if diff := cmp.Diff(&expectedPods[i], p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod (-want, +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// move all pods to active queue when we were trying to schedule them
|
|
q.MoveAllToActiveOrBackoffQueue(logger, framework.EventUnschedulableTimeout, nil, nil, nil)
|
|
oldCycle := q.SchedulingCycle()
|
|
|
|
item, _ := q.Pop(logger)
|
|
firstPod := item.(*framework.QueuedPodInfo)
|
|
if diff := cmp.Diff(&expectedPods[0], firstPod.Pod); diff != "" {
|
|
t.Errorf("Unexpected pod (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
// mark pods[1] ~ pods[totalNum-1] as unschedulable and add them back
|
|
for i := 1; i < totalNum; i++ {
|
|
unschedulablePod := expectedPods[i].DeepCopy()
|
|
unschedulablePod.Status = v1.PodStatus{
|
|
Conditions: []v1.PodCondition{
|
|
{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
Reason: v1.PodReasonUnschedulable,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, newQueuedPodInfoForLookup(unschedulablePod, "plugin"), oldCycle)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
}
|
|
|
|
// Since there was a move request at the same cycle as "oldCycle", these pods
|
|
// should be in the backoff queue.
|
|
for i := 1; i < totalNum; i++ {
|
|
if !q.backoffQ.has(newQueuedPodInfoForLookup(&expectedPods[i])) {
|
|
t.Errorf("Expected %v to be added to backoffQ.", expectedPods[i].Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// tryPop tries to pop one entity from the queue and returns it.
|
|
// It waits 5 seconds before timing out, assuming the queue is then empty.
|
|
func tryPop(t *testing.T, logger klog.Logger, q *PriorityQueue) framework.QueuedEntityInfo {
|
|
t.Helper()
|
|
|
|
var gotEntity framework.QueuedEntityInfo
|
|
popped := make(chan struct{}, 1)
|
|
go func() {
|
|
entity, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Errorf("Failed to pop entity from scheduling queue: %s", err)
|
|
}
|
|
if entity != nil {
|
|
gotEntity = entity
|
|
}
|
|
popped <- struct{}{}
|
|
}()
|
|
|
|
timer := time.NewTimer(5 * time.Second)
|
|
select {
|
|
case <-timer.C:
|
|
q.Close()
|
|
case <-popped:
|
|
timer.Stop()
|
|
}
|
|
return gotEntity
|
|
}
|
|
|
|
func TestPriorityQueue_Pop(t *testing.T) {
|
|
highPriorityPodInfo2 := mustNewPodInfo(
|
|
st.MakePod().Name("hpp2").Namespace("ns1").UID("hpp2ns1").Priority(highPriority).Obj(),
|
|
)
|
|
tests := []struct {
|
|
name string
|
|
popFromBackoffQEnabled bool
|
|
genericWorkloadEnabled bool
|
|
usePodGroups bool
|
|
wantPods []string
|
|
}{
|
|
{
|
|
name: "individual pod, GenericWorkload enabled, PopFromBackoffQ enabled, pops from both activeQ and backoffQ",
|
|
popFromBackoffQEnabled: true,
|
|
genericWorkloadEnabled: true,
|
|
usePodGroups: false,
|
|
wantPods: []string{medPriorityPodInfo.Pod.Name, highPriorityPodInfo.Pod.Name},
|
|
},
|
|
{
|
|
name: "individual pod, GenericWorkload enabled, PopFromBackoffQ disabled, pops only from activeQ",
|
|
popFromBackoffQEnabled: false,
|
|
genericWorkloadEnabled: true,
|
|
usePodGroups: false,
|
|
wantPods: []string{medPriorityPodInfo.Pod.Name},
|
|
},
|
|
{
|
|
name: "individual pod, GenericWorkload disabled, PopFromBackoffQ enabled, pops from both activeQ and backoffQ",
|
|
popFromBackoffQEnabled: true,
|
|
genericWorkloadEnabled: false,
|
|
usePodGroups: false,
|
|
wantPods: []string{medPriorityPodInfo.Pod.Name, highPriorityPodInfo.Pod.Name},
|
|
},
|
|
{
|
|
name: "individual pod, GenericWorkload disabled, PopFromBackoffQ disabled, pops only from activeQ",
|
|
popFromBackoffQEnabled: false,
|
|
genericWorkloadEnabled: false,
|
|
usePodGroups: false,
|
|
wantPods: []string{medPriorityPodInfo.Pod.Name},
|
|
},
|
|
{
|
|
name: "pod group, PopFromBackoffQ enabled, pops from both activeQ and backoffQ",
|
|
popFromBackoffQEnabled: true,
|
|
genericWorkloadEnabled: true,
|
|
usePodGroups: true,
|
|
wantPods: []string{medPriorityPodInfo.Pod.Name, highPriorityPodInfo.Pod.Name},
|
|
},
|
|
{
|
|
name: "pod group, PopFromBackoffQ disabled, pops only from activeQ",
|
|
popFromBackoffQEnabled: false,
|
|
genericWorkloadEnabled: true,
|
|
usePodGroups: true,
|
|
wantPods: []string{medPriorityPodInfo.Pod.Name},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
|
features.GenericWorkload: tt.genericWorkloadEnabled,
|
|
features.SchedulerPopFromBackoffQ: tt.popFromBackoffQEnabled,
|
|
})
|
|
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
medPod, backoffPod, errorBackoffPod, unschedPod := medPriorityPodInfo.Pod, highPriorityPodInfo.Pod, highPriorityPodInfo2.Pod, unschedulablePodInfo.Pod
|
|
if tt.usePodGroups {
|
|
medPod = withPodGroupName(medPriorityPodInfo.Pod, "pg-med")
|
|
backoffPod = withPodGroupName(highPriorityPodInfo.Pod, "pg-backoff")
|
|
errorBackoffPod = withPodGroupName(highPriorityPodInfo2.Pod, "pg-errbackoff")
|
|
unschedPod = withPodGroupName(unschedulablePodInfo.Pod, "pg-unsched")
|
|
}
|
|
|
|
objs := []runtime.Object{medPod, backoffPod, errorBackoffPod, unschedPod}
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), objs)
|
|
|
|
var medEntity, backoffEntity, errorBackoffEntity, unschedEntity framework.QueuedEntityInfo
|
|
if tt.usePodGroups {
|
|
medEntity = q.newQueuedPodGroupInfo(q.newQueuedPodInfo(ctx, medPod))
|
|
backoffPodGroup := q.newQueuedPodGroupInfo(q.newQueuedPodInfo(ctx, backoffPod, "plugin"))
|
|
backoffPodGroup.UnschedulablePlugins = sets.New("plugin")
|
|
backoffEntity = backoffPodGroup
|
|
errorBackoffEntity = q.newQueuedPodGroupInfo(q.newQueuedPodInfo(ctx, errorBackoffPod))
|
|
unschedPodGroup := q.newQueuedPodGroupInfo(q.newQueuedPodInfo(ctx, unschedPod, "plugin"))
|
|
unschedPodGroup.UnschedulablePlugins = sets.New("plugin")
|
|
unschedEntity = unschedPodGroup
|
|
} else {
|
|
medEntity = q.newQueuedPodInfo(ctx, medPod)
|
|
backoffEntity = q.newQueuedPodInfo(ctx, backoffPod, "plugin")
|
|
errorBackoffEntity = q.newQueuedPodInfo(ctx, errorBackoffPod)
|
|
unschedEntity = q.newQueuedPodInfo(ctx, unschedPod, "plugin")
|
|
}
|
|
|
|
// Add medium priority entity to the activeQ
|
|
q.activeQ.add(logger, medEntity, framework.EventUnscheduledPodAdd.Label())
|
|
// Add high priority entity to the backoffQ
|
|
q.backoffQ.add(logger, backoffEntity, framework.EventUnscheduledPodAdd.Label())
|
|
// Add high priority entity to the errorBackoffQ
|
|
q.backoffQ.add(logger, errorBackoffEntity, framework.EventUnscheduledPodAdd.Label())
|
|
// Add entity to the unschedulableEntities
|
|
q.unschedulableEntities.addOrUpdate(unschedEntity, false, framework.EventUnscheduledPodAdd.Label())
|
|
|
|
var gotPods []string
|
|
for i := 0; i < len(tt.wantPods)+1; i++ {
|
|
gotEntity := tryPop(t, logger, q)
|
|
if gotEntity == nil {
|
|
break
|
|
}
|
|
if _, isPodGroup := gotEntity.(*framework.QueuedPodGroupInfo); isPodGroup != tt.usePodGroups {
|
|
t.Errorf("Expected queued pod group: %v, got: %v", tt.usePodGroups, isPodGroup)
|
|
}
|
|
gotEntity.ForEachPodInfo(func(pInfo *framework.QueuedPodInfo) bool {
|
|
gotPods = append(gotPods, pInfo.Pod.Name)
|
|
return true
|
|
})
|
|
}
|
|
|
|
if diff := cmp.Diff(tt.wantPods, gotPods); diff != "" {
|
|
t.Errorf("Unexpected popped pods (-want, +got): %s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_Update(t *testing.T) {
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
|
|
queuePlugin := "queuePlugin"
|
|
skipPlugin := "skipPlugin"
|
|
queueingHintMap := QueueingHintMapPerProfile{
|
|
"": {
|
|
framework.EventTargetPodUpdate: {
|
|
{
|
|
PluginName: queuePlugin,
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
PluginName: skipPlugin,
|
|
QueueingHintFn: queueHintReturnSkip,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
withGate := func(p *v1.Pod) *v1.Pod {
|
|
newPod := p.DeepCopy()
|
|
newPod.Labels = map[string]string{"deny": "true"}
|
|
return newPod
|
|
}
|
|
|
|
notInAnyQueue := "NotInAnyQueue"
|
|
tests := []struct {
|
|
name string
|
|
wantQ string
|
|
// wantAddedToNominated is whether a Pod from the test case should be registered as a nominated Pod in the nominator.
|
|
wantAddedToNominated bool
|
|
// prepareFunc is the function called to prepare pods in the queue before the test case calls Update().
|
|
// This function returns three values;
|
|
// - oldPod/newPod: each test will call Update() with these oldPod and newPod.
|
|
prepareFunc func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod)
|
|
}{
|
|
{
|
|
name: "Update pod that didn't exist in the queue",
|
|
wantQ: activeQ,
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod) {
|
|
updatedPod := medPriorityPodInfo.Pod.DeepCopy()
|
|
updatedPod.Annotations["foo"] = "test"
|
|
return medPriorityPodInfo.Pod, updatedPod
|
|
},
|
|
},
|
|
{
|
|
name: "Update gated pod that didn't exist in the queue",
|
|
wantQ: unschedulableQ,
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod) {
|
|
updatedPod := withGate(medPriorityPodInfo.Pod)
|
|
updatedPod.Annotations["foo"] = "test"
|
|
return withGate(medPriorityPodInfo.Pod), updatedPod
|
|
},
|
|
},
|
|
{
|
|
name: "Update non-existent highPriorityPodInfo and add a nominatedNodeName to it",
|
|
wantQ: activeQ,
|
|
wantAddedToNominated: true,
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod) {
|
|
return highPriorityPodInfo.Pod, highPriNominatedPodInfo.Pod
|
|
},
|
|
},
|
|
{
|
|
name: "Update non-existent gated highPriorityPodInfo and add a nominatedNodeName to it",
|
|
wantQ: unschedulableQ,
|
|
wantAddedToNominated: true,
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod) {
|
|
return withGate(highPriorityPodInfo.Pod), withGate(highPriNominatedPodInfo.Pod)
|
|
},
|
|
},
|
|
{
|
|
name: "When updating a pod that is already in activeQ, the pod should remain in activeQ after Update()",
|
|
wantQ: activeQ,
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod) {
|
|
q.Add(tCtx, highPriorityPodInfo.Pod)
|
|
return highPriorityPodInfo.Pod, highPriorityPodInfo.Pod
|
|
},
|
|
},
|
|
{
|
|
name: "When updating a pod that is in backoff queue and is still backing off, it will be updated in backoff queue",
|
|
wantQ: backoffQ,
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod) {
|
|
podInfo := q.newQueuedPodInfo(tCtx, medPriorityPodInfo.Pod)
|
|
q.backoffQ.add(klog.FromContext(tCtx), podInfo, framework.EventUnscheduledPodAdd.Label())
|
|
return podInfo.Pod, podInfo.Pod
|
|
},
|
|
},
|
|
{
|
|
name: "when updating a pod in unschedulableEntities, if its backoff timer has not yet expired, it moves to backoffQ",
|
|
wantQ: backoffQ,
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod) {
|
|
pInfo := q.newQueuedPodInfo(tCtx, medPriorityPodInfo.Pod, queuePlugin)
|
|
// needs to increment to make the pod backing off
|
|
pInfo.UnschedulableCount++
|
|
q.unschedulableEntities.addOrUpdate(pInfo, false, framework.EventUnscheduledPodAdd.Label())
|
|
updatedPod := medPriorityPodInfo.Pod.DeepCopy()
|
|
updatedPod.Annotations["foo"] = "test"
|
|
return medPriorityPodInfo.Pod, updatedPod
|
|
},
|
|
},
|
|
{
|
|
name: "when updating a pod in unschedulableEntities, if its backoff timer has expired, it moves to activeQ",
|
|
wantQ: activeQ,
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod) {
|
|
pInfo := q.newQueuedPodInfo(tCtx, medPriorityPodInfo.Pod, queuePlugin)
|
|
// needs to increment to make the pod backing off
|
|
pInfo.UnschedulableCount++
|
|
q.unschedulableEntities.addOrUpdate(pInfo, false, framework.EventUnscheduledPodAdd.Label())
|
|
updatedPod := medPriorityPodInfo.Pod.DeepCopy()
|
|
updatedPod.Annotations["foo"] = "test1"
|
|
// Move clock by podMaxBackoffDuration, so that pods in the unschedulableEntities would pass the backing off,
|
|
// and the pods will be moved into activeQ.
|
|
c.Step(q.backoffQ.podMaxBackoffDuration())
|
|
return medPriorityPodInfo.Pod, updatedPod
|
|
},
|
|
},
|
|
{
|
|
name: "when updating a pod in unschedulableEntities, if the scheduling hint returns QueueSkip, it remains in unschedulableEntities",
|
|
wantQ: unschedulableQ,
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod) {
|
|
q.unschedulableEntities.addOrUpdate(q.newQueuedPodInfo(tCtx, medPriorityPodInfo.Pod, skipPlugin), false, framework.EventUnscheduledPodAdd.Label())
|
|
updatedPod := medPriorityPodInfo.Pod.DeepCopy()
|
|
updatedPod.Annotations["foo"] = "test1"
|
|
return medPriorityPodInfo.Pod, updatedPod
|
|
},
|
|
},
|
|
{
|
|
name: "when updating a pod which is in flightPods, the pod will not be added to any queue",
|
|
wantQ: notInAnyQueue,
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) (oldPod, newPod *v1.Pod) {
|
|
// We need to once add this Pod to activeQ and Pop() it so that this Pod is registered correctly in inFlightPods.
|
|
q.Add(tCtx, medPriorityPodInfo.Pod)
|
|
if p, err := q.Pop(klog.FromContext(tCtx)); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(medPriorityPodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
updatedPod := medPriorityPodInfo.Pod.DeepCopy()
|
|
updatedPod.Annotations["foo"] = "bar"
|
|
return medPriorityPodInfo.Pod, updatedPod
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tCtx := ktesting.Init(t)
|
|
objs := []runtime.Object{highPriorityPodInfo.Pod, unschedulablePodInfo.Pod, medPriorityPodInfo.Pod}
|
|
plugin := &denyingPreEnqueuePlugin{denylists: []string{"deny"}}
|
|
m := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
"denyingPreEnqueuePlugin": plugin,
|
|
},
|
|
}
|
|
q := NewTestQueueWithObjects(tCtx, newDefaultQueueSort(), objs, WithClock(c), WithQueueingHintMapPerProfile(queueingHintMap), WithPreEnqueuePluginMap(m))
|
|
|
|
oldPod, newPod := tt.prepareFunc(tCtx, q)
|
|
|
|
q.Update(tCtx, oldPod, newPod)
|
|
|
|
var pInfo *framework.QueuedPodInfo
|
|
|
|
// validate expected queue
|
|
if pInfoFromBackoff, exists := q.backoffQ.get(newQueuedPodInfoForLookup(newPod)); exists {
|
|
if tt.wantQ != backoffQ {
|
|
t.Errorf("expected pod %s not to be queued to backoffQ, but it was", newPod.Name)
|
|
}
|
|
pInfo = pInfoFromBackoff.(*framework.QueuedPodInfo)
|
|
}
|
|
|
|
if pInfoFromActive, exists := q.activeQ.get(newQueuedPodInfoForLookup(newPod)); exists {
|
|
if tt.wantQ != activeQ {
|
|
t.Errorf("expected pod %s not to be queued to activeQ, but it was", newPod.Name)
|
|
}
|
|
pInfo = pInfoFromActive.(*framework.QueuedPodInfo)
|
|
}
|
|
|
|
if pInfoFromUnsched := q.unschedulableEntities.get(newQueuedPodInfoForLookup(newPod)); pInfoFromUnsched != nil {
|
|
if tt.wantQ != unschedulableQ {
|
|
t.Errorf("expected pod %s to not be queued to unschedulableEntities, but it was", newPod.Name)
|
|
}
|
|
pInfo = pInfoFromUnsched.(*framework.QueuedPodInfo)
|
|
}
|
|
|
|
if tt.wantQ == notInAnyQueue {
|
|
// skip the rest of the test if pod is not expected to be in any of the queues.
|
|
return
|
|
}
|
|
|
|
if diff := cmp.Diff(newPod, pInfo.PodInfo.Pod); diff != "" {
|
|
t.Errorf("Unexpected updated pod diff (-want, +got): %s", diff)
|
|
}
|
|
|
|
if tt.wantAddedToNominated && len(q.nominator.nominatedPods) != 1 {
|
|
t.Errorf("Expected one item in nominatedPods map: %v", q.nominator.nominatedPods)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPriorityQueue_UpdateWhenInflight ensures to requeue a Pod back to activeQ/backoffQ
|
|
// if it actually got an update that may make it schedulable while being scheduled.
|
|
// See https://github.com/kubernetes/kubernetes/pull/125578#discussion_r1648338033 for more context.
|
|
func TestPriorityQueue_UpdateWhenInflight(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
// fakePlugin could change its scheduling result by any updates in Pods.
|
|
m[""][framework.EventTargetPodUpdate] = []*QueueingHintFunction{
|
|
{
|
|
PluginName: "fakePlugin",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
}
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithQueueingHintMapPerProfile(m), WithClock(c))
|
|
|
|
// test-pod is created and popped out from the queue
|
|
testPod := st.MakePod().Name("test-pod").Namespace("test-ns").UID("test-uid").Obj()
|
|
q.Add(ctx, testPod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(testPod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
// testPod is updated while being scheduled.
|
|
updatedPod := testPod.DeepCopy()
|
|
updatedPod.Spec.Tolerations = []v1.Toleration{
|
|
{
|
|
Key: "foo",
|
|
Effect: v1.TaintEffectNoSchedule,
|
|
},
|
|
}
|
|
|
|
q.Update(ctx, testPod, updatedPod)
|
|
// test-pod got rejected by fakePlugin,
|
|
// but the update event that it just got may change this scheduling result,
|
|
// and hence we should put this pod to activeQ/backoffQ.
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, newQueuedPodInfoForLookup(updatedPod, "fakePlugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
|
|
item, exists := q.backoffQ.get(newQueuedPodInfoForLookup(updatedPod))
|
|
if !exists {
|
|
t.Fatalf("expected pod %s to be queued to backoffQ, but it wasn't.", updatedPod.Name)
|
|
}
|
|
pInfo := item.(*framework.QueuedPodInfo)
|
|
if diff := cmp.Diff(updatedPod, pInfo.PodInfo.Pod); diff != "" {
|
|
t.Errorf("Unexpected updated pod diff (-want, +got): %s", diff)
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_Delete(t *testing.T) {
|
|
metrics.Register()
|
|
timestamp := time.Now()
|
|
|
|
pod1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Obj()
|
|
pInfo1 := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewTestPodInfo(t, pod1),
|
|
QueueingParams: framework.QueueingParams{
|
|
Timestamp: timestamp,
|
|
UnschedulablePlugins: sets.New("fakePlugin"),
|
|
PendingPlugins: sets.New("fakePendingPlugin"),
|
|
},
|
|
}
|
|
pod2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Obj()
|
|
pInfo2 := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewTestPodInfo(t, pod2),
|
|
QueueingParams: framework.QueueingParams{
|
|
Timestamp: timestamp,
|
|
UnschedulablePlugins: sets.New("fakePlugin2"),
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
operations []operation
|
|
operands []*framework.QueuedPodInfo
|
|
podToDelete *v1.Pod
|
|
expectedAbsentPods []*v1.Pod
|
|
expectedPresentPods []*v1.Pod
|
|
expectedMetrics map[string]int
|
|
}{
|
|
{
|
|
name: "Delete pod from activeQ",
|
|
operations: []operation{
|
|
add,
|
|
add,
|
|
},
|
|
operands: []*framework.QueuedPodInfo{pInfo1, pInfo2},
|
|
podToDelete: pod1,
|
|
expectedAbsentPods: []*v1.Pod{pod1},
|
|
expectedPresentPods: []*v1.Pod{pod2},
|
|
expectedMetrics: map[string]int{
|
|
"fakePlugin": 0,
|
|
"fakePendingPlugin": 0,
|
|
"fakePlugin2": 0,
|
|
},
|
|
},
|
|
{
|
|
name: "Delete pod from backoffQ",
|
|
operations: []operation{
|
|
popAndRequeueAsBackoff,
|
|
add,
|
|
},
|
|
operands: []*framework.QueuedPodInfo{pInfo1, pInfo2},
|
|
podToDelete: pod1,
|
|
expectedAbsentPods: []*v1.Pod{pod1},
|
|
expectedPresentPods: []*v1.Pod{pod2},
|
|
expectedMetrics: map[string]int{
|
|
"fakePlugin": 0,
|
|
"fakePendingPlugin": 0,
|
|
"fakePlugin2": 0,
|
|
},
|
|
},
|
|
{
|
|
name: "Delete pod from unschedulablePods",
|
|
operations: []operation{
|
|
popAndRequeueAsUnschedulable,
|
|
popAndRequeueAsUnschedulable,
|
|
},
|
|
operands: []*framework.QueuedPodInfo{pInfo1, pInfo2},
|
|
podToDelete: pod1,
|
|
expectedAbsentPods: []*v1.Pod{pod1},
|
|
expectedPresentPods: []*v1.Pod{pod2},
|
|
expectedMetrics: map[string]int{
|
|
"fakePlugin": 0,
|
|
"fakePendingPlugin": 0,
|
|
"fakePlugin2": 1,
|
|
},
|
|
},
|
|
{
|
|
name: "Delete nominated pod from activeQ",
|
|
operations: []operation{
|
|
add,
|
|
},
|
|
operands: []*framework.QueuedPodInfo{{PodInfo: highPriNominatedPodInfo}},
|
|
podToDelete: highPriNominatedPodInfo.Pod,
|
|
expectedAbsentPods: []*v1.Pod{highPriNominatedPodInfo.Pod},
|
|
expectedMetrics: map[string]int{
|
|
"fakePlugin": 0,
|
|
"fakePendingPlugin": 0,
|
|
"fakePlugin2": 0,
|
|
},
|
|
},
|
|
{
|
|
name: "Delete non-existing pod",
|
|
operations: []operation{
|
|
popAndRequeueAsUnschedulable,
|
|
},
|
|
operands: []*framework.QueuedPodInfo{pInfo1},
|
|
podToDelete: pod2,
|
|
expectedAbsentPods: []*v1.Pod{pod2},
|
|
expectedPresentPods: []*v1.Pod{pod1},
|
|
expectedMetrics: map[string]int{
|
|
"fakePlugin": 1,
|
|
"fakePendingPlugin": 1,
|
|
"fakePlugin2": 0,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tCtx := ktesting.Init(t)
|
|
q := NewTestQueue(tCtx, newDefaultQueueSort(), WithClock(testingclock.NewFakeClock(timestamp)))
|
|
|
|
// Reset metrics for plugins used in the test
|
|
allPlugins := sets.New("fakePlugin", "fakePendingPlugin", "fakePlugin2")
|
|
for plugin := range allPlugins {
|
|
metrics.UnschedulableReason(plugin, pod1.Spec.SchedulerName).Set(0)
|
|
}
|
|
|
|
// Execute operations
|
|
for i, op := range tt.operations {
|
|
op(tCtx, q, tt.operands[i])
|
|
}
|
|
|
|
q.Delete(tCtx.Logger(), tt.podToDelete)
|
|
|
|
// Verification
|
|
for _, pod := range tt.expectedAbsentPods {
|
|
pInfoLookup := newQueuedPodInfoForLookup(pod)
|
|
if q.activeQ.has(pInfoLookup) || q.backoffQ.has(pInfoLookup) || q.unschedulableEntities.get(pInfoLookup) != nil {
|
|
t.Errorf("Expected pod %v to be absent, but it is present", pod.Name)
|
|
}
|
|
}
|
|
|
|
for _, pod := range tt.expectedPresentPods {
|
|
pInfoLookup := newQueuedPodInfoForLookup(pod)
|
|
if !q.activeQ.has(pInfoLookup) && !q.backoffQ.has(pInfoLookup) && q.unschedulableEntities.get(pInfoLookup) == nil {
|
|
t.Errorf("Expected pod %v to be present, but it is absent", pod.Name)
|
|
}
|
|
}
|
|
|
|
for plugin, expectedVal := range tt.expectedMetrics {
|
|
val, _ := testutil.GetGaugeMetricValue(metrics.UnschedulableReason(plugin, tt.podToDelete.Spec.SchedulerName))
|
|
if diff := cmp.Diff(float64(expectedVal), val); diff != "" {
|
|
t.Errorf("Unexpected metric value for plugin %v after delete (-want, +got):\n%s", plugin, diff)
|
|
}
|
|
}
|
|
|
|
if len(q.nominator.nominatedPods) != 0 || len(q.nominator.nominatedPodToNode) != 0 {
|
|
t.Errorf("Expected nominatedPods and nominatedPodToNode to be empty, but got %v and %v", q.nominator.nominatedPods, q.nominator.nominatedPodToNode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_Activate(t *testing.T) {
|
|
metrics.Register()
|
|
tests := []struct {
|
|
name string
|
|
qPodInfoInUnschedulableEntities []*framework.QueuedPodInfo
|
|
qPodInfoInBackoffQ []*framework.QueuedPodInfo
|
|
qPodInActiveQ []*v1.Pod
|
|
qPodInfoToActivate *framework.QueuedPodInfo
|
|
qPodInInFlightPod *v1.Pod
|
|
expectedInFlightEvent *clusterEvent
|
|
want []*framework.QueuedPodInfo
|
|
}{
|
|
{
|
|
name: "pod already in activeQ",
|
|
qPodInActiveQ: []*v1.Pod{highPriNominatedPodInfo.Pod},
|
|
qPodInfoToActivate: &framework.QueuedPodInfo{PodInfo: highPriNominatedPodInfo},
|
|
want: []*framework.QueuedPodInfo{{PodInfo: highPriNominatedPodInfo}}, // 1 already active
|
|
},
|
|
{
|
|
name: "pod not in unschedulableEntities/backoffQ",
|
|
qPodInfoToActivate: &framework.QueuedPodInfo{PodInfo: highPriNominatedPodInfo},
|
|
want: []*framework.QueuedPodInfo{},
|
|
},
|
|
{
|
|
name: "pod not in unschedulableEntities/backoffQ but in-flight",
|
|
qPodInfoToActivate: &framework.QueuedPodInfo{PodInfo: highPriNominatedPodInfo},
|
|
qPodInInFlightPod: highPriNominatedPodInfo.Pod,
|
|
expectedInFlightEvent: &clusterEvent{oldObj: (*v1.Pod)(nil), newObj: highPriNominatedPodInfo.Pod, event: framework.EventForceActivate},
|
|
want: []*framework.QueuedPodInfo{},
|
|
},
|
|
{
|
|
name: "pod not in unschedulableEntities/backoffQ and not in-flight",
|
|
qPodInfoToActivate: &framework.QueuedPodInfo{PodInfo: highPriNominatedPodInfo},
|
|
qPodInInFlightPod: medPriorityPodInfo.Pod, // different pod is in-flight
|
|
want: []*framework.QueuedPodInfo{},
|
|
},
|
|
{
|
|
name: "pod in unschedulableEntities",
|
|
qPodInfoInUnschedulableEntities: []*framework.QueuedPodInfo{{PodInfo: highPriNominatedPodInfo}},
|
|
qPodInfoToActivate: &framework.QueuedPodInfo{PodInfo: highPriNominatedPodInfo},
|
|
want: []*framework.QueuedPodInfo{{PodInfo: highPriNominatedPodInfo}},
|
|
},
|
|
{
|
|
name: "pod in backoffQ",
|
|
qPodInfoInBackoffQ: []*framework.QueuedPodInfo{{PodInfo: highPriNominatedPodInfo}},
|
|
qPodInfoToActivate: &framework.QueuedPodInfo{PodInfo: highPriNominatedPodInfo},
|
|
want: []*framework.QueuedPodInfo{{PodInfo: highPriNominatedPodInfo}},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
var objs []runtime.Object
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), objs)
|
|
|
|
if tt.qPodInInFlightPod != nil {
|
|
// Put -> Pop the Pod to make it registered in inFlightPods.
|
|
q.activeQ.add(logger, newQueuedPodInfoForLookup(tt.qPodInInFlightPod), framework.EventUnscheduledPodAdd.Label())
|
|
p, err := q.activeQ.pop(logger)
|
|
if err != nil {
|
|
t.Fatalf("Pop failed: %v", err)
|
|
}
|
|
if p.(*framework.QueuedPodInfo).Pod.Name != tt.qPodInInFlightPod.Name {
|
|
t.Errorf("Unexpected popped pod: %v", p.(*framework.QueuedPodInfo).Pod.Name)
|
|
}
|
|
if len(q.activeQ.listInFlightEvents()) != 1 {
|
|
t.Fatal("Expected the pod to be recorded in in-flight events, but it doesn't")
|
|
}
|
|
}
|
|
|
|
// Prepare activeQ/unschedulableEntities/backoffQ according to the table
|
|
for _, qPod := range tt.qPodInActiveQ {
|
|
q.Add(ctx, qPod)
|
|
}
|
|
|
|
for _, qPodInfo := range tt.qPodInfoInUnschedulableEntities {
|
|
q.unschedulableEntities.addOrUpdate(qPodInfo, false, framework.EventUnscheduledPodAdd.Label())
|
|
}
|
|
|
|
for _, qPodInfo := range tt.qPodInfoInBackoffQ {
|
|
q.backoffQ.add(logger, qPodInfo, framework.EventUnscheduledPodAdd.Label())
|
|
}
|
|
|
|
// Activate specific pod according to the table
|
|
q.Activate(logger, map[string]*v1.Pod{"test_pod": tt.qPodInfoToActivate.PodInfo.Pod})
|
|
|
|
// Check the result after activation by the length of activeQ
|
|
if wantLen := len(tt.want); q.activeQ.len() != wantLen {
|
|
t.Fatalf("length compare: want %v, got %v", wantLen, q.activeQ.len())
|
|
}
|
|
|
|
if tt.expectedInFlightEvent != nil {
|
|
if len(q.activeQ.listInFlightEvents()) != 2 {
|
|
t.Fatalf("Expected two in-flight event to be recorded, but got %v events", len(q.activeQ.listInFlightEvents()))
|
|
}
|
|
found := false
|
|
for _, e := range q.activeQ.listInFlightEvents() {
|
|
event, ok := e.(*clusterEvent)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if d := cmp.Diff(tt.expectedInFlightEvent, event, cmpopts.EquateComparable(clusterEvent{})); d != "" {
|
|
t.Fatalf("Unexpected in-flight event (-want, +got):\n%s", d)
|
|
}
|
|
found = true
|
|
}
|
|
|
|
if !found {
|
|
t.Fatalf("Expected in-flight event to be recorded, but it wasn't.")
|
|
}
|
|
}
|
|
|
|
// Check if the specific pod exists in activeQ
|
|
for _, want := range tt.want {
|
|
if !q.activeQ.has(newQueuedPodInfoForLookup(want.PodInfo.Pod)) {
|
|
t.Errorf("podInfo not exist in activeQ: want %v", want.PodInfo.Pod.Name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type preEnqueuePlugin struct {
|
|
allowlists []string
|
|
name string
|
|
}
|
|
|
|
func (pl *preEnqueuePlugin) Name() string {
|
|
if pl.name != "" {
|
|
return pl.name
|
|
}
|
|
return "preEnqueuePlugin"
|
|
}
|
|
|
|
func (pl *preEnqueuePlugin) PreEnqueue(ctx context.Context, p *v1.Pod) *fwk.Status {
|
|
for _, allowed := range pl.allowlists {
|
|
for label := range p.Labels {
|
|
if label == allowed {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "pod label not in allowlists")
|
|
}
|
|
|
|
type denyingPreEnqueuePlugin struct {
|
|
denylists []string
|
|
}
|
|
|
|
func (pl *denyingPreEnqueuePlugin) Name() string {
|
|
return "denyingPreEnqueuePlugin"
|
|
}
|
|
|
|
func (pl *denyingPreEnqueuePlugin) PreEnqueue(ctx context.Context, p *v1.Pod) *fwk.Status {
|
|
for _, denied := range pl.denylists {
|
|
for label := range p.Labels {
|
|
if label == denied {
|
|
return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "pod label in denylists")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestPriorityQueue_moveToActiveQ(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
plugins []fwk.PreEnqueuePlugin
|
|
pod *v1.Pod
|
|
event string
|
|
movesFromBackoffQ bool
|
|
popFromBackoffQEnabled []bool
|
|
wantUnschedulablePods int
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "no plugins registered",
|
|
pod: st.MakePod().Name("p").Label("p", "").Obj(),
|
|
event: framework.EventUnscheduledPodAdd.Label(),
|
|
wantUnschedulablePods: 0,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, pod name not in allowlists",
|
|
plugins: []fwk.PreEnqueuePlugin{&preEnqueuePlugin{}, &preEnqueuePlugin{}},
|
|
pod: st.MakePod().Name("p").Label("p", "").Obj(),
|
|
event: framework.EventUnscheduledPodAdd.Label(),
|
|
wantUnschedulablePods: 1,
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, pod failed one preEnqueue plugin",
|
|
plugins: []fwk.PreEnqueuePlugin{
|
|
&preEnqueuePlugin{allowlists: []string{"foo", "bar"}},
|
|
&preEnqueuePlugin{allowlists: []string{"foo"}},
|
|
},
|
|
pod: st.MakePod().Name("bar").Label("bar", "").Obj(),
|
|
event: framework.EventUnscheduledPodAdd.Label(),
|
|
wantUnschedulablePods: 1,
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, preEnqueue rejects the pod, even if it is after backoff",
|
|
plugins: []fwk.PreEnqueuePlugin{
|
|
&preEnqueuePlugin{allowlists: []string{"foo", "bar"}},
|
|
&preEnqueuePlugin{allowlists: []string{"foo"}},
|
|
},
|
|
pod: st.MakePod().Name("bar").Label("bar", "").Obj(),
|
|
event: framework.BackoffComplete,
|
|
popFromBackoffQEnabled: []bool{false},
|
|
wantUnschedulablePods: 1,
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
// With SchedulerPopFromBackoffQ enabled, the queue assumes the pod has already passed PreEnqueue,
|
|
// and it doesn't run PreEnqueue again, always puts the pod to activeQ.
|
|
name: "preEnqueue plugin registered, pod would fail one preEnqueue plugin, but it is moved from backoffQ after completing backoff, so preEnqueue is not executed",
|
|
plugins: []fwk.PreEnqueuePlugin{
|
|
&preEnqueuePlugin{allowlists: []string{"foo", "bar"}},
|
|
&preEnqueuePlugin{allowlists: []string{"foo"}},
|
|
},
|
|
pod: st.MakePod().Name("bar").Label("bar", "").Obj(),
|
|
event: framework.BackoffComplete,
|
|
movesFromBackoffQ: true,
|
|
popFromBackoffQEnabled: []bool{true},
|
|
wantUnschedulablePods: 0,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, pod failed one preEnqueue plugin when activated from unschedulableEntities",
|
|
plugins: []fwk.PreEnqueuePlugin{
|
|
&preEnqueuePlugin{allowlists: []string{"foo", "bar"}},
|
|
&preEnqueuePlugin{allowlists: []string{"foo"}},
|
|
},
|
|
pod: st.MakePod().Name("bar").Label("bar", "").Obj(),
|
|
event: framework.ForceActivate,
|
|
movesFromBackoffQ: false,
|
|
popFromBackoffQEnabled: []bool{true},
|
|
wantUnschedulablePods: 1,
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, pod would fail one preEnqueue plugin, but was activated from backoffQ, so preEnqueue is not executed",
|
|
plugins: []fwk.PreEnqueuePlugin{
|
|
&preEnqueuePlugin{allowlists: []string{"foo", "bar"}},
|
|
&preEnqueuePlugin{allowlists: []string{"foo"}},
|
|
},
|
|
pod: st.MakePod().Name("bar").Label("bar", "").Obj(),
|
|
event: framework.ForceActivate,
|
|
movesFromBackoffQ: true,
|
|
popFromBackoffQEnabled: []bool{true},
|
|
wantUnschedulablePods: 0,
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, pod passed all preEnqueue plugins",
|
|
plugins: []fwk.PreEnqueuePlugin{
|
|
&preEnqueuePlugin{allowlists: []string{"foo", "bar"}},
|
|
&preEnqueuePlugin{allowlists: []string{"bar"}},
|
|
},
|
|
pod: st.MakePod().Name("bar").Label("bar", "").Obj(),
|
|
event: framework.EventUnscheduledPodAdd.Label(),
|
|
wantUnschedulablePods: 0,
|
|
wantSuccess: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
if tt.popFromBackoffQEnabled == nil {
|
|
tt.popFromBackoffQEnabled = []bool{true, false}
|
|
}
|
|
for _, popFromBackoffQEnabled := range tt.popFromBackoffQEnabled {
|
|
t.Run(fmt.Sprintf("%s popFromBackoffQEnabled(%v)", tt.name, popFromBackoffQEnabled), func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SchedulerPopFromBackoffQ, popFromBackoffQEnabled)
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
m := map[string]map[string]fwk.PreEnqueuePlugin{"": make(map[string]fwk.PreEnqueuePlugin, len(tt.plugins))}
|
|
for _, plugin := range tt.plugins {
|
|
m[""][plugin.Name()] = plugin
|
|
}
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), []runtime.Object{tt.pod}, WithPreEnqueuePluginMap(m),
|
|
WithPodInitialBackoffDuration(time.Second*30), WithPodMaxBackoffDuration(time.Second*60))
|
|
got := q.moveToActiveQ(logger, q.newQueuedPodInfo(ctx, tt.pod), tt.event, tt.movesFromBackoffQ)
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("Unexpected result: want %v, but got %v", tt.wantSuccess, got)
|
|
}
|
|
if tt.wantUnschedulablePods != len(q.unschedulableEntities.entityInfoMap) {
|
|
t.Errorf("Unexpected unschedulableEntities: want %v, but got %v", tt.wantUnschedulablePods, len(q.unschedulableEntities.entityInfoMap))
|
|
}
|
|
|
|
// Simulate an update event.
|
|
clone := tt.pod.DeepCopy()
|
|
metav1.SetMetaDataAnnotation(&clone.ObjectMeta, "foo", "")
|
|
q.Update(ctx, tt.pod, clone)
|
|
// Ensure the pod is still located in unschedulableEntities.
|
|
if tt.wantUnschedulablePods != len(q.unschedulableEntities.entityInfoMap) {
|
|
t.Errorf("Unexpected unschedulableEntities: want %v, but got %v", tt.wantUnschedulablePods, len(q.unschedulableEntities.entityInfoMap))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_moveToBackoffQ(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
plugins []fwk.PreEnqueuePlugin
|
|
pod *v1.Pod
|
|
popFromBackoffQEnabled []bool
|
|
wantSuccess bool
|
|
}{
|
|
{
|
|
name: "no plugins registered",
|
|
pod: st.MakePod().Name("p").Label("p", "").Obj(),
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, pod name would not be in allowlists",
|
|
plugins: []fwk.PreEnqueuePlugin{&preEnqueuePlugin{}, &preEnqueuePlugin{}},
|
|
pod: st.MakePod().Name("p").Label("p", "").Obj(),
|
|
popFromBackoffQEnabled: []bool{false},
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, pod name not in allowlists",
|
|
plugins: []fwk.PreEnqueuePlugin{&preEnqueuePlugin{}, &preEnqueuePlugin{}},
|
|
pod: st.MakePod().Name("p").Label("p", "").Obj(),
|
|
popFromBackoffQEnabled: []bool{true},
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, preEnqueue plugin would reject the pod, but isn't run",
|
|
plugins: []fwk.PreEnqueuePlugin{
|
|
&preEnqueuePlugin{allowlists: []string{"foo", "bar"}},
|
|
&preEnqueuePlugin{allowlists: []string{"foo"}},
|
|
},
|
|
pod: st.MakePod().Name("bar").Label("bar", "").Obj(),
|
|
popFromBackoffQEnabled: []bool{false},
|
|
wantSuccess: true,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, pod failed one preEnqueue plugin",
|
|
plugins: []fwk.PreEnqueuePlugin{
|
|
&preEnqueuePlugin{allowlists: []string{"foo", "bar"}},
|
|
&preEnqueuePlugin{allowlists: []string{"foo"}},
|
|
},
|
|
pod: st.MakePod().Name("bar").Label("bar", "").Obj(),
|
|
popFromBackoffQEnabled: []bool{true},
|
|
wantSuccess: false,
|
|
},
|
|
{
|
|
name: "preEnqueue plugin registered, pod passed all preEnqueue plugins",
|
|
plugins: []fwk.PreEnqueuePlugin{
|
|
&preEnqueuePlugin{allowlists: []string{"foo", "bar"}},
|
|
&preEnqueuePlugin{allowlists: []string{"bar"}},
|
|
},
|
|
pod: st.MakePod().Name("bar").Label("bar", "").Obj(),
|
|
wantSuccess: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
if tt.popFromBackoffQEnabled == nil {
|
|
tt.popFromBackoffQEnabled = []bool{true, false}
|
|
}
|
|
for _, popFromBackoffQEnabled := range tt.popFromBackoffQEnabled {
|
|
t.Run(fmt.Sprintf("%s popFromBackoffQEnabled(%v)", tt.name, popFromBackoffQEnabled), func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SchedulerPopFromBackoffQ, popFromBackoffQEnabled)
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
m := map[string]map[string]fwk.PreEnqueuePlugin{"": make(map[string]fwk.PreEnqueuePlugin, len(tt.plugins))}
|
|
for _, plugin := range tt.plugins {
|
|
m[""][plugin.Name()] = plugin
|
|
}
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), []runtime.Object{tt.pod}, WithPreEnqueuePluginMap(m),
|
|
WithPodInitialBackoffDuration(time.Second*30), WithPodMaxBackoffDuration(time.Second*60))
|
|
pInfo := q.newQueuedPodInfo(ctx, tt.pod)
|
|
got := q.moveToBackoffQ(logger, pInfo, framework.EventUnscheduledPodAdd.Label())
|
|
if got != tt.wantSuccess {
|
|
t.Errorf("Unexpected result: want %v, but got %v", tt.wantSuccess, got)
|
|
}
|
|
if tt.wantSuccess {
|
|
if !q.backoffQ.has(pInfo) {
|
|
t.Errorf("Expected pod to be in backoffQ, but it isn't")
|
|
}
|
|
if q.unschedulableEntities.get(pInfo) != nil {
|
|
t.Errorf("Expected pod not to be in unschedulableEntities, but it is")
|
|
}
|
|
} else {
|
|
if q.backoffQ.has(pInfo) {
|
|
t.Errorf("Expected pod not to be in backoffQ, but it is")
|
|
}
|
|
if q.unschedulableEntities.get(pInfo) == nil {
|
|
t.Errorf("Expected pod to be in unschedulableEntities, but it isn't")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkMoveAllToActiveOrBackoffQueue(b *testing.B) {
|
|
tests := []struct {
|
|
name string
|
|
moveEvent fwk.ClusterEvent
|
|
}{
|
|
{
|
|
name: "baseline",
|
|
moveEvent: framework.EventUnschedulableTimeout,
|
|
},
|
|
{
|
|
name: "worst",
|
|
moveEvent: nodeAdd,
|
|
},
|
|
{
|
|
name: "random",
|
|
// leave "moveEvent" unspecified
|
|
},
|
|
}
|
|
|
|
podTemplates := []*v1.Pod{
|
|
highPriorityPodInfo.Pod, highPriNominatedPodInfo.Pod,
|
|
medPriorityPodInfo.Pod, unschedulablePodInfo.Pod,
|
|
}
|
|
|
|
events := []fwk.ClusterEvent{
|
|
{Resource: fwk.Node, ActionType: fwk.Add},
|
|
{Resource: fwk.Node, ActionType: fwk.UpdateNodeTaint},
|
|
{Resource: fwk.Node, ActionType: fwk.UpdateNodeAllocatable},
|
|
{Resource: fwk.Node, ActionType: fwk.UpdateNodeCondition},
|
|
{Resource: fwk.Node, ActionType: fwk.UpdateNodeLabel},
|
|
{Resource: fwk.Node, ActionType: fwk.UpdateNodeAnnotation},
|
|
{Resource: fwk.PersistentVolumeClaim, ActionType: fwk.Add},
|
|
{Resource: fwk.PersistentVolumeClaim, ActionType: fwk.Update},
|
|
{Resource: fwk.PersistentVolume, ActionType: fwk.Add},
|
|
{Resource: fwk.PersistentVolume, ActionType: fwk.Update},
|
|
{Resource: fwk.StorageClass, ActionType: fwk.Add},
|
|
{Resource: fwk.StorageClass, ActionType: fwk.Update},
|
|
{Resource: fwk.CSINode, ActionType: fwk.Add},
|
|
{Resource: fwk.CSINode, ActionType: fwk.Update},
|
|
{Resource: fwk.CSIDriver, ActionType: fwk.Add},
|
|
{Resource: fwk.CSIDriver, ActionType: fwk.Update},
|
|
{Resource: fwk.CSIStorageCapacity, ActionType: fwk.Add},
|
|
{Resource: fwk.CSIStorageCapacity, ActionType: fwk.Update},
|
|
}
|
|
|
|
pluginNum := 20
|
|
var plugins []string
|
|
// Mimic that we have 20 plugins loaded in runtime.
|
|
for i := 0; i < pluginNum; i++ {
|
|
plugins = append(plugins, fmt.Sprintf("fake-plugin-%v", i))
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
for _, podsInUnschedulablePods := range []int{1000, 5000} {
|
|
b.Run(fmt.Sprintf("%v-%v", tt.name, podsInUnschedulablePods), func(b *testing.B) {
|
|
logger, ctx := ktesting.NewTestContext(b)
|
|
for i := 0; i < b.N; i++ {
|
|
b.StopTimer()
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
// - All plugins registered for events[0], which is NodeAdd.
|
|
// - 1/2 of plugins registered for events[1]
|
|
// - 1/3 of plugins registered for events[2]
|
|
// - ...
|
|
for j := 0; j < len(events); j++ {
|
|
for k := 0; k < len(plugins); k++ {
|
|
if (k+1)%(j+1) == 0 {
|
|
m[""][events[j]] = append(m[""][events[j]], &QueueingHintFunction{
|
|
PluginName: plugins[k],
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c), WithQueueingHintMapPerProfile(m))
|
|
|
|
// Init pods in unschedulableEntities.
|
|
for j := 0; j < podsInUnschedulablePods; j++ {
|
|
p := podTemplates[j%len(podTemplates)].DeepCopy()
|
|
p.Name, p.UID = fmt.Sprintf("%v-%v", p.Name, j), types.UID(fmt.Sprintf("%v-%v", p.UID, j))
|
|
var podInfo *framework.QueuedPodInfo
|
|
// The ultimate goal of composing each PodInfo is to cover the path that intersects
|
|
// (unschedulable) plugin names with the plugins that register the moveEvent,
|
|
// here the rational is:
|
|
// - in baseline case, don't inject unschedulable plugin names, so podMatchesEvent()
|
|
// never gets executed.
|
|
// - in worst case, make both ends (of the intersection) a big number,i.e.,
|
|
// M intersected with N instead of M with 1 (or 1 with N)
|
|
// - in random case, each pod failed by a random plugin, and also the moveEvent
|
|
// is randomized.
|
|
if tt.name == "baseline" {
|
|
podInfo = q.newQueuedPodInfo(ctx, p)
|
|
} else if tt.name == "worst" {
|
|
// Each pod failed by all plugins.
|
|
podInfo = q.newQueuedPodInfo(ctx, p, plugins...)
|
|
} else {
|
|
// Random case.
|
|
podInfo = q.newQueuedPodInfo(ctx, p, plugins[j%len(plugins)])
|
|
}
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, podInfo, q.SchedulingCycle())
|
|
if err != nil {
|
|
b.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
}
|
|
|
|
b.StartTimer()
|
|
if tt.moveEvent.Resource != "" {
|
|
q.MoveAllToActiveOrBackoffQueue(logger, tt.moveEvent, nil, nil, nil)
|
|
} else {
|
|
// Random case.
|
|
q.MoveAllToActiveOrBackoffQueue(logger, events[i%len(events)], nil, nil, nil)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_MoveAllToActiveOrBackoffQueueWithQueueingHint(t *testing.T) {
|
|
now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
p := st.MakePod().Name("pod1").Namespace("ns1").UID("1").Label("foo", "bar").Obj()
|
|
gatedPod := st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()
|
|
tests := []struct {
|
|
name string
|
|
podInfo *framework.QueuedPodInfo
|
|
hint fwk.QueueingHintFn
|
|
// duration is the duration that the Pod has been in the unschedulable queue.
|
|
duration time.Duration
|
|
// triggerEvent is the event to trigger the move. If unset, defaults to nodeAdd.
|
|
triggerEvent *fwk.ClusterEvent
|
|
// expectedQ is the queue name (activeQ, backoffQ, or unschedulableEntities) that this Pod should be queued to.
|
|
expectedQ string
|
|
}{
|
|
{
|
|
name: "Queue queues pod to activeQ",
|
|
// This pod has PendingPlugins and hence will be pushed directly to activeQ
|
|
podInfo: &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(p),
|
|
QueueingParams: framework.QueueingParams{
|
|
PendingPlugins: sets.New("foo"),
|
|
},
|
|
},
|
|
hint: queueHintReturnQueue,
|
|
expectedQ: activeQ,
|
|
},
|
|
{
|
|
name: "Queue queues pod to backoffQ if Pod is backing off",
|
|
// needs UnschedulableCount to make it backing off.
|
|
podInfo: &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(p),
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("foo"),
|
|
},
|
|
},
|
|
hint: queueHintReturnQueue,
|
|
expectedQ: backoffQ,
|
|
},
|
|
{
|
|
name: "Queue queues pod to activeQ if Pod is not backing off",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(p),
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("foo"),
|
|
},
|
|
},
|
|
hint: queueHintReturnQueue,
|
|
// The pod is assumed to failed the scheduling cycle once, which would get DefaultPodInitialBackoffDuration as the penalty.
|
|
// To finish the backoff, waiting for DefaultPodInitialBackoffDuration isn't enough, need to wait for +1
|
|
// because the pod is determined to be still backing off if `{backoff expiration time} == trancate({current time})`
|
|
duration: DefaultPodInitialBackoffDuration + time.Second,
|
|
expectedQ: activeQ,
|
|
},
|
|
{
|
|
name: "Skip queues pod to unschedulableEntities",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(p),
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("foo"),
|
|
},
|
|
},
|
|
hint: queueHintReturnSkip,
|
|
expectedQ: unschedulableQ,
|
|
},
|
|
{
|
|
name: "Queue queues pod to backoffQ if Pod is not gated and the event is wildcard",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(p),
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("foo"),
|
|
},
|
|
},
|
|
triggerEvent: &framework.EventUnschedulableTimeout,
|
|
hint: func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
|
|
return fwk.Queue, fmt.Errorf("QueueingHintFn should not be called as trigger event is wildcard")
|
|
},
|
|
expectedQ: backoffQ,
|
|
},
|
|
{
|
|
name: "Queue queues pod to backoffQ when Pod is no longer gated and the event is wildcard",
|
|
podInfo: setQueuedPodInfoGated(&framework.QueuedPodInfo{PodInfo: mustNewPodInfo(p)}, "foo", []fwk.ClusterEvent{framework.EventUnscheduledPodUpdate}),
|
|
triggerEvent: &framework.EventUnschedulableTimeout,
|
|
hint: func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
|
|
return fwk.Queue, fmt.Errorf("QueueingHintFn should not be called as trigger event is wildcard")
|
|
},
|
|
expectedQ: backoffQ,
|
|
},
|
|
{
|
|
name: "Queue queues pod to unschedulableQ when Pod is gated and the event is wildcard",
|
|
podInfo: setQueuedPodInfoGated(&framework.QueuedPodInfo{PodInfo: mustNewPodInfo(gatedPod)}, "foo", []fwk.ClusterEvent{framework.EventUnscheduledPodUpdate}),
|
|
triggerEvent: &framework.EventUnschedulableTimeout,
|
|
hint: func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
|
|
return fwk.Queue, fmt.Errorf("QueueingHintFn should not be called as trigger event is wildcard")
|
|
},
|
|
expectedQ: unschedulableQ,
|
|
},
|
|
{
|
|
name: "QueueHintFunction is not called when Pod is gated by the plugin that isn't interested in the event",
|
|
podInfo: setQueuedPodInfoGated(&framework.QueuedPodInfo{PodInfo: mustNewPodInfo(p)}, names.SchedulingGates, []fwk.ClusterEvent{framework.EventUnscheduledPodUpdate}),
|
|
// The hintFn should not be called as the pod is gated by SchedulingGates plugin,
|
|
// the scheduling gate isn't interested in the node add event,
|
|
// and the queue should keep this Pod in the unschedQ without calling the hintFn.
|
|
hint: func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
|
|
return fwk.Queue, fmt.Errorf("QueueingHintFn should not be called as pod is gated")
|
|
},
|
|
expectedQ: unschedulableQ,
|
|
},
|
|
{
|
|
name: "QueueHintFunction is called when Pod is gated by the plugin that is interested in the event",
|
|
podInfo: setQueuedPodInfoGated(&framework.QueuedPodInfo{PodInfo: mustNewPodInfo(p)}, "foo", []fwk.ClusterEvent{nodeAdd}),
|
|
// In this case, the hintFn should be called as the pod is gated by foo plugin that is interested in the NodeAdd event.
|
|
hint: queueHintReturnQueue,
|
|
// and, as a result, this pod should be queued to backoffQ.
|
|
expectedQ: backoffQ,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
m[""][nodeAdd] = []*QueueingHintFunction{
|
|
{
|
|
PluginName: "foo",
|
|
QueueingHintFn: test.hint,
|
|
},
|
|
}
|
|
m[""][framework.EventUnscheduledPodUpdate] = []*QueueingHintFunction{
|
|
{
|
|
PluginName: names.SchedulingGates,
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
}
|
|
cl := testingclock.NewFakeClock(now)
|
|
plugin, _ := schedulinggates.New(ctx, nil, nil, plfeature.Features{})
|
|
preEnqM := map[string]map[string]fwk.PreEnqueuePlugin{"": {
|
|
names.SchedulingGates: plugin.(fwk.PreEnqueuePlugin),
|
|
"foo": &preEnqueuePlugin{allowlists: []string{"foo"}},
|
|
}}
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithQueueingHintMapPerProfile(m), WithClock(cl), WithPreEnqueuePluginMap(preEnqM))
|
|
q.Add(ctx, test.podInfo.Pod)
|
|
if q.activeQ.len() > 0 {
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(test.podInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
// add to unsched pod pool
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, test.podInfo, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
} else {
|
|
// The pod was already moved to unschedulableEntities because it's gated.
|
|
// Update it with the test's configured podInfo to ensure custom test fields are set.
|
|
q.unschedulableEntities.addOrUpdate(test.podInfo, test.podInfo.Gated(), "test-setup")
|
|
}
|
|
cl.Step(test.duration)
|
|
|
|
event := nodeAdd
|
|
if test.triggerEvent != nil {
|
|
event = *test.triggerEvent
|
|
}
|
|
q.MoveAllToActiveOrBackoffQueue(logger, event, nil, nil, nil)
|
|
|
|
if q.backoffQ.len() == 0 && test.expectedQ == backoffQ {
|
|
t.Fatalf("expected pod to be queued to backoffQ, but it was not")
|
|
}
|
|
|
|
if q.activeQ.len() == 0 && test.expectedQ == activeQ {
|
|
t.Fatalf("expected pod to be queued to activeQ, but it was not")
|
|
}
|
|
|
|
if q.unschedulableEntities.get(test.podInfo) == nil && test.expectedQ == unschedulableQ {
|
|
t.Fatalf("expected pod to be queued to unschedulableEntities, but it was not")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_MoveAllToActiveOrBackoffQueue(t *testing.T) {
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
|
|
m[""][nodeAdd] = []*QueueingHintFunction{
|
|
{
|
|
PluginName: "fooPlugin",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
}
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c), WithQueueingHintMapPerProfile(m))
|
|
// To simulate the pod is failed in scheduling in the real world, Pop() the pod from activeQ before AddUnschedulablePodIfNotPresent()s below.
|
|
q.Add(ctx, unschedulablePodInfo.Pod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(unschedulablePodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
expectInFlightPods(t, q, unschedulablePodInfo.Pod.UID)
|
|
q.Add(ctx, highPriorityPodInfo.Pod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(highPriorityPodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
expectInFlightPods(t, q, unschedulablePodInfo.Pod.UID, highPriorityPodInfo.Pod.UID)
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, unschedulablePodInfo.Pod, "fooPlugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, highPriorityPodInfo.Pod, "fooPlugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
expectInFlightPods(t, q)
|
|
// Construct a Pod, but don't associate its scheduler failure to any plugin
|
|
hpp1 := clonePod(highPriorityPodInfo.Pod, "hpp1")
|
|
q.Add(ctx, hpp1)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(hpp1, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
expectInFlightPods(t, q, hpp1.UID)
|
|
// This Pod will go to backoffQ because no failure plugin is associated with it.
|
|
hpp1PodInfo := q.newQueuedPodInfo(ctx, hpp1)
|
|
hpp1PodInfo.UnschedulableCount++
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, hpp1PodInfo, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
expectInFlightPods(t, q)
|
|
// Construct another Pod, and associate its scheduler failure to plugin "barPlugin".
|
|
hpp2 := clonePod(highPriorityPodInfo.Pod, "hpp2")
|
|
q.Add(ctx, hpp2)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(hpp2, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
expectInFlightPods(t, q, hpp2.UID)
|
|
// This Pod will go to the unschedulable Pod pool.
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, hpp2, "barPlugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
expectInFlightPods(t, q)
|
|
// This NodeAdd event moves unschedulablePodInfo and highPriorityPodInfo to the backoffQ,
|
|
// because of the queueing hint function registered for NodeAdd/fooPlugin.
|
|
q.MoveAllToActiveOrBackoffQueue(logger, nodeAdd, nil, nil, nil)
|
|
q.Add(ctx, medPriorityPodInfo.Pod)
|
|
if q.activeQ.len() != 1 {
|
|
t.Errorf("Expected 1 item to be in activeQ, but got: %v", q.activeQ.len())
|
|
}
|
|
// Pop out the medPriorityPodInfo in activeQ.
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(medPriorityPodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
expectInFlightPods(t, q, medPriorityPodInfo.Pod.UID)
|
|
// hpp2 won't be moved.
|
|
if q.backoffQ.len() != 3 {
|
|
t.Fatalf("Expected 3 items to be in backoffQ, but got: %v", q.backoffQ.len())
|
|
}
|
|
|
|
// pop out the pods in the backoffQ.
|
|
// This doesn't make them in-flight pods.
|
|
c.Step(q.backoffQ.podMaxBackoffDuration())
|
|
_ = q.backoffQ.popAllBackoffCompleted(logger)
|
|
expectInFlightPods(t, q, medPriorityPodInfo.Pod.UID)
|
|
|
|
q.Add(ctx, unschedulablePodInfo.Pod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(unschedulablePodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
expectInFlightPods(t, q, medPriorityPodInfo.Pod.UID, unschedulablePodInfo.Pod.UID)
|
|
q.Add(ctx, highPriorityPodInfo.Pod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(highPriorityPodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
expectInFlightPods(t, q, medPriorityPodInfo.Pod.UID, unschedulablePodInfo.Pod.UID, highPriorityPodInfo.Pod.UID)
|
|
q.Add(ctx, hpp1)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(hpp1, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
unschedulableQueuedPodInfo := q.newQueuedPodInfo(ctx, unschedulablePodInfo.Pod, "fooPlugin")
|
|
highPriorityQueuedPodInfo := q.newQueuedPodInfo(ctx, highPriorityPodInfo.Pod, "fooPlugin")
|
|
hpp1QueuedPodInfo := q.newQueuedPodInfo(ctx, hpp1)
|
|
expectInFlightPods(t, q, medPriorityPodInfo.Pod.UID, unschedulablePodInfo.Pod.UID, highPriorityPodInfo.Pod.UID, hpp1.UID)
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, unschedulableQueuedPodInfo, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
expectInFlightPods(t, q, medPriorityPodInfo.Pod.UID, highPriorityPodInfo.Pod.UID, hpp1.UID)
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, highPriorityQueuedPodInfo, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
expectInFlightPods(t, q, medPriorityPodInfo.Pod.UID, hpp1.UID)
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, hpp1QueuedPodInfo, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
expectInFlightPods(t, q, medPriorityPodInfo.Pod.UID)
|
|
q.Add(ctx, medPriorityPodInfo.Pod)
|
|
// hpp1 will go to backoffQ because no failure plugin is associated with it.
|
|
// All plugins other than hpp1 are enqueued to the unschedulable Pod pool.
|
|
for _, pod := range []*v1.Pod{unschedulablePodInfo.Pod, highPriorityPodInfo.Pod, hpp2} {
|
|
if q.unschedulableEntities.get(newQueuedPodInfoForLookup(pod)) == nil {
|
|
t.Errorf("Expected %v in the unschedulableEntities", pod.Name)
|
|
}
|
|
}
|
|
if !q.backoffQ.has(hpp1QueuedPodInfo) {
|
|
t.Errorf("Expected %v in the backoffQ", hpp1.Name)
|
|
}
|
|
// all the remaining Pods should be in activeQ.
|
|
if q.activeQ.len() != 1 {
|
|
t.Errorf("Expected %v in the activeQ", medPriorityPodInfo.Pod.Name)
|
|
}
|
|
|
|
// Move clock by podMaxBackoffDuration, so that pods in the unschedulableEntities would pass the backing off,
|
|
// and the pods will be moved into activeQ.
|
|
c.Step(q.backoffQ.podMaxBackoffDuration())
|
|
q.flushBackoffQCompleted(logger) // flush the completed backoffQ to move hpp1 to activeQ.
|
|
q.MoveAllToActiveOrBackoffQueue(logger, nodeAdd, nil, nil, nil)
|
|
if q.activeQ.len() != 4 {
|
|
t.Errorf("Expected 4 items to be in activeQ, but got: %v", q.activeQ.len())
|
|
}
|
|
if q.backoffQ.len() != 0 {
|
|
t.Errorf("Expected 0 item to be in backoffQ, but got: %v", q.backoffQ.len())
|
|
}
|
|
expectInFlightPods(t, q, medPriorityPodInfo.Pod.UID)
|
|
if len(q.unschedulableEntities.entityInfoMap) != 1 {
|
|
// hpp2 won't be moved regardless of its backoff timer.
|
|
t.Errorf("Expected 1 item to be in unschedulableEntities, but got: %v", len(q.unschedulableEntities.entityInfoMap))
|
|
}
|
|
}
|
|
|
|
func clonePod(pod *v1.Pod, newName string) *v1.Pod {
|
|
pod = pod.DeepCopy()
|
|
pod.Name = newName
|
|
pod.UID = types.UID(pod.Name + pod.Namespace)
|
|
return pod
|
|
}
|
|
|
|
func expectInFlightPods(t *testing.T, q *PriorityQueue, uids ...types.UID) {
|
|
t.Helper()
|
|
var actualUIDs []types.UID
|
|
for _, pod := range q.activeQ.listInFlightPods() {
|
|
actualUIDs = append(actualUIDs, pod.UID)
|
|
}
|
|
sortUIDs := cmpopts.SortSlices(func(a, b types.UID) bool { return a < b })
|
|
if diff := cmp.Diff(uids, actualUIDs, sortUIDs); diff != "" {
|
|
t.Fatalf("Unexpected content of inFlightPods (-want, +have):\n%s", diff)
|
|
}
|
|
actualUIDs = nil
|
|
events := q.activeQ.listInFlightEvents()
|
|
for _, e := range events {
|
|
if pod, ok := e.(*v1.Pod); ok {
|
|
actualUIDs = append(actualUIDs, pod.UID)
|
|
}
|
|
}
|
|
if diff := cmp.Diff(uids, actualUIDs, sortUIDs); diff != "" {
|
|
t.Fatalf("Unexpected pods in inFlightEvents (-want, +have):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// TestPriorityQueue_AssignedPodAdded tests AssignedPodAdded. It checks that
|
|
// when a pod with pod affinity is in unschedulableEntities and another pod with a
|
|
// matching label is added, the unschedulable pod is moved to activeQ.
|
|
func TestPriorityQueue_AssignedPodAdded_(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
unschedPod *v1.Pod
|
|
unschedPlugin string
|
|
updatedAssignedPod *v1.Pod
|
|
wantToRequeue bool
|
|
}{
|
|
{
|
|
name: "Pod rejected by pod affinity is requeued with matching Pod's update",
|
|
unschedPod: st.MakePod().Name("afp").Namespace("ns1").UID("afp").Annotation("annot2", "val2").PodAffinityExists("service", "region", st.PodAffinityWithRequiredReq).Obj(),
|
|
unschedPlugin: names.InterPodAffinity,
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("ns1").Label("service", "securityscan").Node("node1").Obj(),
|
|
wantToRequeue: true,
|
|
},
|
|
{
|
|
name: "Pod rejected by pod affinity isn't requeued with unrelated Pod's update",
|
|
unschedPod: st.MakePod().Name("afp").Namespace("ns1").UID("afp").Annotation("annot2", "val2").PodAffinityExists("service", "region", st.PodAffinityWithRequiredReq).Obj(),
|
|
unschedPlugin: names.InterPodAffinity,
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("unrelated").Label("unrelated", "unrelated").Node("node1").Obj(),
|
|
wantToRequeue: false,
|
|
},
|
|
{
|
|
name: "Pod rejected by pod topology spread is requeued with Pod's update in the same namespace",
|
|
unschedPod: st.MakePod().Name("tsp").Namespace("ns1").UID("tsp").SpreadConstraint(1, "node", v1.DoNotSchedule, nil, nil, nil, nil, nil).Obj(),
|
|
unschedPlugin: names.PodTopologySpread,
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("ns1").Label("service", "securityscan").Node("node1").Obj(),
|
|
wantToRequeue: true,
|
|
},
|
|
{
|
|
name: "Pod rejected by pod topology spread isn't requeued with unrelated Pod's update",
|
|
unschedPod: st.MakePod().Name("afp").Namespace("ns1").UID("afp").Annotation("annot2", "val2").PodAffinityExists("service", "region", st.PodAffinityWithRequiredReq).Obj(),
|
|
unschedPlugin: names.PodTopologySpread,
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("unrelated").Label("unrelated", "unrelated").Node("node1").Obj(),
|
|
wantToRequeue: false,
|
|
},
|
|
{
|
|
name: "Pod rejected by other plugins isn't requeued with any Pod's update",
|
|
unschedPod: st.MakePod().Name("afp").Namespace("ns1").UID("afp").Annotation("annot2", "val2").Obj(),
|
|
unschedPlugin: "fakePlugin",
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("unrelated").Label("unrelated", "unrelated").Node("node1").Obj(),
|
|
wantToRequeue: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
m[""][framework.EventAssignedPodAdd] = []*QueueingHintFunction{
|
|
{
|
|
PluginName: "fakePlugin",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
PluginName: names.InterPodAffinity,
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
PluginName: names.PodTopologySpread,
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
}
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c), WithQueueingHintMapPerProfile(m))
|
|
|
|
// To simulate the pod is failed in scheduling in the real world, Pop() the pod from activeQ before AddUnschedulablePodIfNotPresent()s below.
|
|
q.Add(ctx, tt.unschedPod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(tt.unschedPod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, tt.unschedPod, tt.unschedPlugin), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
|
|
// Move clock to make the unschedulable pods complete backoff.
|
|
c.Step(DefaultPodInitialBackoffDuration + time.Second)
|
|
|
|
q.AssignedPodAdded(logger, tt.updatedAssignedPod)
|
|
|
|
if q.activeQ.has(newQueuedPodInfoForLookup(tt.unschedPod)) != tt.wantToRequeue {
|
|
t.Fatalf("unexpected Pod move: Pod should be requeued: %v. Pod is actually requeued: %v", tt.wantToRequeue, !tt.wantToRequeue)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_AssignedPodUpdated(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
unschedPod *v1.Pod
|
|
unschedPlugin string
|
|
updatedAssignedPod *v1.Pod
|
|
event fwk.ClusterEvent
|
|
wantToRequeue bool
|
|
}{
|
|
{
|
|
name: "Pod rejected by pod affinity is requeued with matching Pod's update",
|
|
unschedPod: st.MakePod().Name("afp").Namespace("ns1").UID("afp").Annotation("annot2", "val2").PodAffinityExists("service", "region", st.PodAffinityWithRequiredReq).Obj(),
|
|
unschedPlugin: names.InterPodAffinity,
|
|
event: fwk.ClusterEvent{Resource: fwk.AssignedPod, ActionType: fwk.UpdatePodLabel},
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("ns1").Label("service", "securityscan").Node("node1").Obj(),
|
|
wantToRequeue: true,
|
|
},
|
|
{
|
|
name: "Pod rejected by pod affinity isn't requeued with unrelated Pod's update",
|
|
unschedPod: st.MakePod().Name("afp").Namespace("ns1").UID("afp").Annotation("annot2", "val2").PodAffinityExists("service", "region", st.PodAffinityWithRequiredReq).Obj(),
|
|
unschedPlugin: names.InterPodAffinity,
|
|
event: fwk.ClusterEvent{Resource: fwk.AssignedPod, ActionType: fwk.UpdatePodLabel},
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("unrelated").Label("unrelated", "unrelated").Node("node1").Obj(),
|
|
wantToRequeue: false,
|
|
},
|
|
{
|
|
name: "Pod rejected by pod topology spread is requeued with Pod's update in the same namespace",
|
|
unschedPod: st.MakePod().Name("tsp").Namespace("ns1").UID("tsp").SpreadConstraint(1, "node", v1.DoNotSchedule, nil, nil, nil, nil, nil).Obj(),
|
|
unschedPlugin: names.PodTopologySpread,
|
|
event: fwk.ClusterEvent{Resource: fwk.AssignedPod, ActionType: fwk.UpdatePodLabel},
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("ns1").Label("service", "securityscan").Node("node1").Obj(),
|
|
wantToRequeue: true,
|
|
},
|
|
{
|
|
name: "Pod rejected by pod topology spread isn't requeued with unrelated Pod's update",
|
|
unschedPod: st.MakePod().Name("afp").Namespace("ns1").UID("afp").Annotation("annot2", "val2").PodAffinityExists("service", "region", st.PodAffinityWithRequiredReq).Obj(),
|
|
unschedPlugin: names.PodTopologySpread,
|
|
event: fwk.ClusterEvent{Resource: fwk.AssignedPod, ActionType: fwk.UpdatePodLabel},
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("unrelated").Label("unrelated", "unrelated").Node("node1").Obj(),
|
|
wantToRequeue: false,
|
|
},
|
|
{
|
|
name: "Pod rejected by resource fit is requeued with assigned Pod's scale down",
|
|
unschedPod: st.MakePod().Name("rp").Namespace("ns1").UID("afp").Annotation("annot2", "val2").Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).Obj(),
|
|
unschedPlugin: names.NodeResourcesFit,
|
|
event: fwk.ClusterEvent{Resource: fwk.AssignedPod, ActionType: fwk.UpdatePodScaleDown},
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("ns2").Node("node1").Obj(),
|
|
wantToRequeue: true,
|
|
},
|
|
{
|
|
name: "Pod rejected by other plugins isn't requeued with any Pod's update",
|
|
unschedPod: st.MakePod().Name("afp").Namespace("ns1").UID("afp").Annotation("annot2", "val2").Obj(),
|
|
unschedPlugin: "fakePlugin",
|
|
event: fwk.ClusterEvent{Resource: fwk.AssignedPod, ActionType: fwk.UpdatePodLabel},
|
|
updatedAssignedPod: st.MakePod().Name("lbp").Namespace("unrelated").Label("unrelated", "unrelated").Node("node1").Obj(),
|
|
wantToRequeue: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
m[""] = map[fwk.ClusterEvent][]*QueueingHintFunction{
|
|
{Resource: fwk.AssignedPod, ActionType: fwk.UpdatePodLabel}: {
|
|
{
|
|
PluginName: "fakePlugin",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
PluginName: names.InterPodAffinity,
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
PluginName: names.PodTopologySpread,
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
{Resource: fwk.AssignedPod, ActionType: fwk.UpdatePodScaleDown}: {
|
|
{
|
|
PluginName: names.NodeResourcesFit,
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
}
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c), WithQueueingHintMapPerProfile(m))
|
|
|
|
// To simulate the pod is failed in scheduling in the real world, Pop() the pod from activeQ before AddUnschedulablePodIfNotPresent()s below.
|
|
q.Add(ctx, tt.unschedPod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(tt.unschedPod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, tt.unschedPod, tt.unschedPlugin), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
|
|
// Move clock to make the unschedulable pods complete backoff.
|
|
c.Step(DefaultPodInitialBackoffDuration + time.Second)
|
|
|
|
q.AssignedPodUpdated(logger, nil, tt.updatedAssignedPod, tt.event)
|
|
|
|
if q.activeQ.has(newQueuedPodInfoForLookup(tt.unschedPod)) != tt.wantToRequeue {
|
|
t.Fatalf("unexpected Pod move: Pod should be requeued: %v. Pod is actually requeued: %v", tt.wantToRequeue, !tt.wantToRequeue)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_NominatedPodsForNode(t *testing.T) {
|
|
objs := []runtime.Object{medPriorityPodInfo.Pod, unschedulablePodInfo.Pod, highPriorityPodInfo.Pod}
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), objs)
|
|
q.Add(ctx, medPriorityPodInfo.Pod)
|
|
q.Add(ctx, unschedulablePodInfo.Pod)
|
|
q.Add(ctx, highPriorityPodInfo.Pod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(highPriorityPodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
expectedList := []fwk.PodInfo{medPriorityPodInfo, unschedulablePodInfo}
|
|
podInfos := q.NominatedPodsForNode("node1")
|
|
if diff := cmp.Diff(expectedList, podInfos, cmpopts.IgnoreUnexported(framework.PodInfo{})); diff != "" {
|
|
t.Errorf("Unexpected list of nominated Pods for node: (-want, +got):\n%s", diff)
|
|
}
|
|
podInfos[0].GetPod().Name = "not mpp"
|
|
if diff := cmp.Diff(podInfos, q.NominatedPodsForNode("node1"), cmpopts.IgnoreUnexported(framework.PodInfo{})); diff == "" {
|
|
t.Error("Expected list of nominated Pods for node2 is different from podInfos")
|
|
}
|
|
if len(q.NominatedPodsForNode("node2")) != 0 {
|
|
t.Error("Expected list of nominated Pods for node2 to be empty.")
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_NominatedPodDeleted(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
podInfo *framework.PodInfo
|
|
deletePod bool
|
|
wantLen int
|
|
}{
|
|
{
|
|
name: "alive pod gets added into PodNominator",
|
|
podInfo: medPriorityPodInfo,
|
|
wantLen: 1,
|
|
},
|
|
{
|
|
name: "deleted pod shouldn't be added into PodNominator",
|
|
podInfo: highPriNominatedPodInfo,
|
|
deletePod: true,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "pod without .status.nominatedPodName specified shouldn't be added into PodNominator",
|
|
podInfo: highPriorityPodInfo,
|
|
wantLen: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
cs := fake.NewClientset(tt.podInfo.Pod)
|
|
informerFactory := informers.NewSharedInformerFactory(cs, 0)
|
|
podLister := informerFactory.Core().V1().Pods().Lister()
|
|
|
|
// Build a PriorityQueue.
|
|
q := NewPriorityQueue(newDefaultQueueSort(), informerFactory, WithPodLister(podLister))
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
informerFactory.Start(ctx.Done())
|
|
informerFactory.WaitForCacheSync(ctx.Done())
|
|
|
|
if tt.deletePod {
|
|
// Simulate that the test pod gets deleted physically.
|
|
informerFactory.Core().V1().Pods().Informer().GetStore().Delete(tt.podInfo.Pod)
|
|
}
|
|
|
|
q.AddNominatedPod(logger, tt.podInfo, nil)
|
|
|
|
if got := len(q.NominatedPodsForNode(tt.podInfo.Pod.Status.NominatedNodeName)); got != tt.wantLen {
|
|
t.Errorf("Expected %v nominated pods for node, but got %v", tt.wantLen, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_PendingPods(t *testing.T) {
|
|
makeSet := func(pods []*v1.Pod) map[*v1.Pod]struct{} {
|
|
pendingSet := map[*v1.Pod]struct{}{}
|
|
for _, p := range pods {
|
|
pendingSet[p] = struct{}{}
|
|
}
|
|
return pendingSet
|
|
}
|
|
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort())
|
|
// To simulate the pod is failed in scheduling in the real world, Pop() the pod from activeQ before AddUnschedulablePodIfNotPresent()s below.
|
|
q.Add(ctx, unschedulablePodInfo.Pod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(unschedulablePodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
q.Add(ctx, highPriorityPodInfo.Pod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(highPriorityPodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
q.Add(ctx, medPriorityPodInfo.Pod)
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, unschedulablePodInfo.Pod, "plugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, highPriorityPodInfo.Pod, "plugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
|
|
expectedSet := makeSet([]*v1.Pod{medPriorityPodInfo.Pod, unschedulablePodInfo.Pod, highPriorityPodInfo.Pod})
|
|
gotPods, gotSummary := q.PendingPods()
|
|
if diff := cmp.Diff(expectedSet, makeSet(gotPods)); diff != "" {
|
|
t.Errorf("Unexpected list of pending Pods (-want, +got):\n%s", diff)
|
|
}
|
|
if wantSummary := fmt.Sprintf(pendingPodsSummary, 1, 0, 2); wantSummary != gotSummary {
|
|
t.Errorf("Unexpected pending pods summary: want %v, but got %v.", wantSummary, gotSummary)
|
|
}
|
|
// Move all to active queue. We should still see the same set of pods.
|
|
q.MoveAllToActiveOrBackoffQueue(logger, framework.EventUnschedulableTimeout, nil, nil, nil)
|
|
gotPods, gotSummary = q.PendingPods()
|
|
if diff := cmp.Diff(expectedSet, makeSet(gotPods)); diff != "" {
|
|
t.Errorf("Unexpected list of pending Pods (-want, +got):\n%s", diff)
|
|
}
|
|
if wantSummary := fmt.Sprintf(pendingPodsSummary, 1, 2, 0); wantSummary != gotSummary {
|
|
t.Errorf("Unexpected pending pods summary: want %v, but got %v.", wantSummary, gotSummary)
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_UpdateNominatedPodForNode(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
objs := []runtime.Object{medPriorityPodInfo.Pod, unschedulablePodInfo.Pod, highPriorityPodInfo.Pod, scheduledPodInfo.Pod}
|
|
q := NewTestQueueWithObjects(ctx, newDefaultQueueSort(), objs)
|
|
q.Add(ctx, medPriorityPodInfo.Pod)
|
|
// Update unschedulablePodInfo on a different node than specified in the pod.
|
|
q.AddNominatedPod(logger, mustNewTestPodInfo(t, unschedulablePodInfo.Pod),
|
|
&fwk.NominatingInfo{NominatingMode: fwk.ModeOverride, NominatedNodeName: "node5"})
|
|
|
|
// Update nominated node name of a pod on a node that is not specified in the pod object.
|
|
q.AddNominatedPod(logger, mustNewTestPodInfo(t, highPriorityPodInfo.Pod),
|
|
&fwk.NominatingInfo{NominatingMode: fwk.ModeOverride, NominatedNodeName: "node2"})
|
|
expectedNominatedPods := &nominator{
|
|
nominatedPodToNode: map[types.UID]string{
|
|
medPriorityPodInfo.Pod.UID: "node1",
|
|
highPriorityPodInfo.Pod.UID: "node2",
|
|
unschedulablePodInfo.Pod.UID: "node5",
|
|
},
|
|
nominatedPods: map[string][]podRef{
|
|
"node1": {podToRef(medPriorityPodInfo.Pod)},
|
|
"node2": {podToRef(highPriorityPodInfo.Pod)},
|
|
"node5": {podToRef(unschedulablePodInfo.Pod)},
|
|
},
|
|
}
|
|
if diff := cmp.Diff(q.nominator, expectedNominatedPods, nominatorCmpOpts...); diff != "" {
|
|
t.Errorf("Unexpected diff after adding pods (-want, +got):\n%s", diff)
|
|
}
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(medPriorityPodInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
// List of nominated pods shouldn't change after popping them from the queue.
|
|
if diff := cmp.Diff(q.nominator, expectedNominatedPods, nominatorCmpOpts...); diff != "" {
|
|
t.Errorf("Unexpected diff after popping pods (-want, +got):\n%s", diff)
|
|
}
|
|
// Update one of the nominated pods that doesn't have nominatedNodeName in the
|
|
// pod object. It should be updated correctly.
|
|
q.AddNominatedPod(logger, highPriorityPodInfo, &fwk.NominatingInfo{NominatingMode: fwk.ModeOverride, NominatedNodeName: "node4"})
|
|
expectedNominatedPods = &nominator{
|
|
nominatedPodToNode: map[types.UID]string{
|
|
medPriorityPodInfo.Pod.UID: "node1",
|
|
highPriorityPodInfo.Pod.UID: "node4",
|
|
unschedulablePodInfo.Pod.UID: "node5",
|
|
},
|
|
nominatedPods: map[string][]podRef{
|
|
"node1": {podToRef(medPriorityPodInfo.Pod)},
|
|
"node4": {podToRef(highPriorityPodInfo.Pod)},
|
|
"node5": {podToRef(unschedulablePodInfo.Pod)},
|
|
},
|
|
}
|
|
if diff := cmp.Diff(q.nominator, expectedNominatedPods, nominatorCmpOpts...); diff != "" {
|
|
t.Errorf("Unexpected diff after updating pods (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
// Attempt to nominate a pod that was deleted from the informer cache.
|
|
// Nothing should change.
|
|
q.AddNominatedPod(logger, nonExistentPodInfo, &fwk.NominatingInfo{NominatingMode: fwk.ModeOverride, NominatedNodeName: "node1"})
|
|
if diff := cmp.Diff(q.nominator, expectedNominatedPods, nominatorCmpOpts...); diff != "" {
|
|
t.Errorf("Unexpected diff after nominating a deleted pod (-want, +got):\n%s", diff)
|
|
}
|
|
// Attempt to nominate a pod that was already scheduled in the informer cache.
|
|
// Nothing should change.
|
|
scheduledPodCopy := scheduledPodInfo.Pod.DeepCopy()
|
|
scheduledPodInfo.Pod.Spec.NodeName = ""
|
|
q.AddNominatedPod(logger, mustNewTestPodInfo(t, scheduledPodCopy), &fwk.NominatingInfo{NominatingMode: fwk.ModeOverride, NominatedNodeName: "node1"})
|
|
if diff := cmp.Diff(q.nominator, expectedNominatedPods, nominatorCmpOpts...); diff != "" {
|
|
t.Errorf("Unexpected diff after nominating a scheduled pod (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
// Delete a nominated pod that doesn't have nominatedNodeName in the pod
|
|
// object. It should be deleted.
|
|
q.DeleteNominatedPodIfExists(highPriorityPodInfo.Pod)
|
|
expectedNominatedPods = &nominator{
|
|
nominatedPodToNode: map[types.UID]string{
|
|
medPriorityPodInfo.Pod.UID: "node1",
|
|
unschedulablePodInfo.Pod.UID: "node5",
|
|
},
|
|
nominatedPods: map[string][]podRef{
|
|
"node1": {podToRef(medPriorityPodInfo.Pod)},
|
|
"node5": {podToRef(unschedulablePodInfo.Pod)},
|
|
},
|
|
}
|
|
if diff := cmp.Diff(q.nominator, expectedNominatedPods, nominatorCmpOpts...); diff != "" {
|
|
t.Errorf("Unexpected diff after deleting pods (-want, +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_NewWithOptions(t *testing.T) {
|
|
_, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx,
|
|
newDefaultQueueSort(),
|
|
WithPodInitialBackoffDuration(2*time.Second),
|
|
WithPodMaxBackoffDuration(20*time.Second),
|
|
)
|
|
|
|
if q.backoffQ.podInitialBackoffDuration() != 2*time.Second {
|
|
t.Errorf("Unexpected pod backoff initial duration. Expected: %v, got: %v", 2*time.Second, q.backoffQ.podInitialBackoffDuration())
|
|
}
|
|
|
|
if q.backoffQ.podMaxBackoffDuration() != 20*time.Second {
|
|
t.Errorf("Unexpected pod backoff max duration. Expected: %v, got: %v", 2*time.Second, q.backoffQ.podMaxBackoffDuration())
|
|
}
|
|
}
|
|
|
|
func TestSchedulingQueue_Close(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort())
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
entity, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Errorf("Expected nil err from Pop() if queue is closed, but got %q", err.Error())
|
|
}
|
|
if entity != nil {
|
|
t.Errorf("Expected nil item from Pop() if queue is closed, but got: %v", entity)
|
|
}
|
|
}()
|
|
q.Close()
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestRecentlyTriedPodsGoBack tests that pods which are recently tried and are
|
|
// unschedulable go behind other pods with the same priority. This behavior
|
|
// ensures that an unschedulable pod does not block head of the queue when there
|
|
// are frequent events that move pods to the active queue.
|
|
func TestRecentlyTriedPodsGoBack(t *testing.T) {
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c))
|
|
// Add a few pods to priority queue.
|
|
for i := 0; i < 5; i++ {
|
|
p := st.MakePod().Name(fmt.Sprintf("test-pod-%v", i)).Namespace("ns1").UID(fmt.Sprintf("tp00%v", i)).Priority(highPriority).Node("node1").NominatedNodeName("node1").Obj()
|
|
q.Add(ctx, p)
|
|
}
|
|
c.Step(time.Microsecond)
|
|
// Simulate a pod being popped by the scheduler, determined unschedulable, and
|
|
// then moved back to the active queue.
|
|
entity, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Errorf("Error while popping the head of the queue: %v", err)
|
|
}
|
|
p1 := entity.(*framework.QueuedPodInfo)
|
|
// Update pod condition to unschedulable.
|
|
podutil.UpdatePodCondition(&p1.PodInfo.Pod.Status, &v1.PodCondition{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
Reason: v1.PodReasonUnschedulable,
|
|
Message: "fake scheduling failure",
|
|
LastProbeTime: metav1.Now(),
|
|
})
|
|
p1.UnschedulablePlugins = sets.New("plugin")
|
|
// Put in the unschedulable queue.
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, p1, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
c.Step(q.backoffQ.podMaxBackoffDuration())
|
|
// Move all unschedulable pods to the active queue.
|
|
q.MoveAllToActiveOrBackoffQueue(logger, framework.EventUnschedulableTimeout, nil, nil, nil)
|
|
// Simulation is over. Now let's pop all pods. The pod popped first should be
|
|
// the last one we pop here.
|
|
for i := 0; i < 5; i++ {
|
|
entity, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Errorf("Error while popping pods from the queue: %v", err)
|
|
}
|
|
p := entity.(*framework.QueuedPodInfo)
|
|
if (i == 4) != (p1 == p) {
|
|
t.Errorf("A pod tried before is not the last pod popped: i: %v, pod name: %v", i, p.PodInfo.Pod.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPodFailedSchedulingMultipleTimesDoesNotBlockNewerPod tests
|
|
// that a pod determined as unschedulable multiple times doesn't block any newer pod.
|
|
// This behavior ensures that an unschedulable pod does not block head of the queue when there
|
|
// are frequent events that move pods to the active queue.
|
|
func TestPodFailedSchedulingMultipleTimesDoesNotBlockNewerPod(t *testing.T) {
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c))
|
|
|
|
// Add an unschedulable pod to a priority queue.
|
|
// This makes a situation that the pod was tried to schedule
|
|
// and had been determined unschedulable so far
|
|
unschedulablePod := st.MakePod().Name("test-pod-unscheduled").Namespace("ns1").UID("tp001").Priority(highPriority).NominatedNodeName("node1").Obj()
|
|
|
|
// Update pod condition to unschedulable.
|
|
podutil.UpdatePodCondition(&unschedulablePod.Status, &v1.PodCondition{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
Reason: v1.PodReasonUnschedulable,
|
|
Message: "fake scheduling failure",
|
|
})
|
|
|
|
// To simulate the pod is failed in scheduling in the real world, Pop() the pod from activeQ before AddUnschedulablePodIfNotPresent() below.
|
|
q.Add(ctx, unschedulablePod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(unschedulablePod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
// Put in the unschedulable queue
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, newQueuedPodInfoForLookup(unschedulablePod, "plugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
// Move clock to make the unschedulable pods complete backoff.
|
|
c.Step(DefaultPodInitialBackoffDuration + time.Second)
|
|
// Move all unschedulable pods to the active queue.
|
|
q.MoveAllToActiveOrBackoffQueue(logger, framework.EventUnschedulableTimeout, nil, nil, nil)
|
|
|
|
// Simulate a pod being popped by the scheduler,
|
|
// At this time, unschedulable pod should be popped.
|
|
entity, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Errorf("Error while popping the head of the queue: %v", err)
|
|
}
|
|
p1 := entity.(*framework.QueuedPodInfo)
|
|
if diff := cmp.Diff(unschedulablePod, p1.Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
// Assume newer pod was added just after unschedulable pod
|
|
// being popped and before being pushed back to the queue.
|
|
newerPod := st.MakePod().Name("test-newer-pod").Namespace("ns1").UID("tp002").CreationTimestamp(metav1.Now()).Priority(highPriority).NominatedNodeName("node1").Obj()
|
|
q.Add(ctx, newerPod)
|
|
|
|
// And then unschedulablePodInfo was determined as unschedulable AGAIN.
|
|
podutil.UpdatePodCondition(&unschedulablePod.Status, &v1.PodCondition{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
Reason: v1.PodReasonUnschedulable,
|
|
Message: "fake scheduling failure",
|
|
})
|
|
|
|
// And then, put unschedulable pod to the unschedulable queue
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, newQueuedPodInfoForLookup(unschedulablePod, "plugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
// Move clock to make the unschedulable pods complete backoff.
|
|
c.Step(DefaultPodInitialBackoffDuration + time.Second)
|
|
// Move all unschedulable pods to the active queue.
|
|
q.MoveAllToActiveOrBackoffQueue(logger, framework.EventUnschedulableTimeout, nil, nil, nil)
|
|
|
|
// At this time, newerPod should be popped
|
|
// because it is the oldest tried pod.
|
|
item2, err2 := q.Pop(logger)
|
|
if err2 != nil {
|
|
t.Errorf("Error while popping the head of the queue: %v", err2)
|
|
} else {
|
|
p2 := item2.(*framework.QueuedPodInfo)
|
|
if diff := cmp.Diff(newerPod, p2.Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHighPriorityBackoff tests that a high priority pod does not block
|
|
// other pods if it is unschedulable
|
|
func TestHighPriorityBackoff(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort())
|
|
|
|
midPod := st.MakePod().Name("test-midpod").Namespace("ns1").UID("tp-mid").Priority(midPriority).NominatedNodeName("node1").Obj()
|
|
highPod := st.MakePod().Name("test-highpod").Namespace("ns1").UID("tp-high").Priority(highPriority).NominatedNodeName("node1").Obj()
|
|
q.Add(ctx, midPod)
|
|
q.Add(ctx, highPod)
|
|
// Simulate a pod being popped by the scheduler, determined unschedulable, and
|
|
// then moved back to the active queue.
|
|
entity, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Errorf("Error while popping the head of the queue: %v", err)
|
|
}
|
|
p := entity.(*framework.QueuedPodInfo)
|
|
if diff := cmp.Diff(highPod, p.Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
// Update pod condition to unschedulable.
|
|
podutil.UpdatePodCondition(&p.Pod.Status, &v1.PodCondition{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
Reason: v1.PodReasonUnschedulable,
|
|
Message: "fake scheduling failure",
|
|
})
|
|
// Put in the unschedulable queue.
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, newQueuedPodInfoForLookup(p.Pod, "fooPlugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
// Move all unschedulable pods to the active/backoff queue.
|
|
q.MoveAllToActiveOrBackoffQueue(logger, framework.EventUnschedulableTimeout, nil, nil, nil)
|
|
|
|
entity, err = q.Pop(logger)
|
|
p = entity.(*framework.QueuedPodInfo)
|
|
if err != nil {
|
|
t.Errorf("Error while popping the head of the queue: %v", err)
|
|
} else if diff := cmp.Diff(midPod, p.Pod); diff != "" {
|
|
// high pod should be in backoffQ
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// TestHighPriorityFlushUnschedulableEntitiesLeftover tests that entities will be moved to
|
|
// activeQ after one minutes if it is in unschedulableEntities.
|
|
func TestHighPriorityFlushUnschedulableEntitiesLeftover(t *testing.T) {
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
m[""][nodeAdd] = []*QueueingHintFunction{
|
|
{
|
|
PluginName: "fakePlugin",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
}
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c), WithQueueingHintMapPerProfile(m))
|
|
midPod := st.MakePod().Name("test-midpod").Namespace("ns1").UID("tp-mid").Priority(midPriority).NominatedNodeName("node1").Obj()
|
|
highPod := st.MakePod().Name("test-highpod").Namespace("ns1").UID("tp-high").Priority(highPriority).NominatedNodeName("node1").Obj()
|
|
|
|
// Update pod condition to highPod.
|
|
podutil.UpdatePodCondition(&highPod.Status, &v1.PodCondition{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
Reason: v1.PodReasonUnschedulable,
|
|
Message: "fake scheduling failure",
|
|
})
|
|
|
|
// Update pod condition to midPod.
|
|
podutil.UpdatePodCondition(&midPod.Status, &v1.PodCondition{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
Reason: v1.PodReasonUnschedulable,
|
|
Message: "fake scheduling failure",
|
|
})
|
|
|
|
// To simulate the pod is failed in scheduling in the real world, Pop() the pod from activeQ before AddUnschedulablePodIfNotPresent()s below.
|
|
q.Add(ctx, highPod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(highPod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
q.Add(ctx, midPod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(midPod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, highPod, "fakePlugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, midPod, "fakePlugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
c.Step(DefaultPodMaxInUnschedulablePodsDuration + time.Second)
|
|
q.flushUnschedulableEntitiesLeftover(logger)
|
|
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(highPod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(midPod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// TestFlushUnschedulableEntitiesLeftoverSetsFlag verifies that the WasFlushedFromUnschedulable
|
|
// flag is correctly set when entities are flushed and cleared when they return to the queue.
|
|
func TestFlushUnschedulableEntitiesLeftoverSetsFlag(t *testing.T) {
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c), WithQueueingHintMapPerProfile(m))
|
|
|
|
pod := st.MakePod().Name("test-pod").Namespace("ns1").UID("tp-1").Priority(midPriority).NominatedNodeName("node1").Obj()
|
|
|
|
// Add pod to activeQ and pop it to simulate a scheduling attempt
|
|
q.Add(ctx, pod)
|
|
entity, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from Pop: %v", err)
|
|
}
|
|
pInfo := entity.(*framework.QueuedPodInfo)
|
|
|
|
// Verify flag is initially false
|
|
if pInfo.WasFlushedFromUnschedulable {
|
|
t.Errorf("Expected WasFlushedFromUnschedulable to be false initially, but got true")
|
|
}
|
|
|
|
// Add pod to unschedulableEntities (simulating failed scheduling)
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, pod, "fakePlugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
|
|
// Advance time past the flush duration and flush
|
|
c.Step(DefaultPodMaxInUnschedulablePodsDuration + time.Second)
|
|
q.flushUnschedulableEntitiesLeftover(logger)
|
|
|
|
// Pop the pod and verify flag is now true
|
|
entity, err = q.Pop(logger)
|
|
pInfo = entity.(*framework.QueuedPodInfo)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from Pop after flush: %v", err)
|
|
}
|
|
if !pInfo.WasFlushedFromUnschedulable {
|
|
t.Errorf("Expected WasFlushedFromUnschedulable to be true after flush, but got false")
|
|
}
|
|
|
|
// Simulate pod failing to schedule again and returning to queue
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, q.newQueuedPodInfo(ctx, pInfo.Pod, "fakePlugin"), q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
|
|
// Verify flag is cleared when pod returns to queue
|
|
internalPInfo := q.unschedulableEntities.get(newQueuedPodInfoForLookup(pod))
|
|
if internalPInfo == nil {
|
|
t.Fatalf("pod should be in unschedulableEntities")
|
|
}
|
|
if internalPInfo.(*framework.QueuedPodInfo).WasFlushedFromUnschedulable {
|
|
t.Errorf("Expected WasFlushedFromUnschedulable to be cleared (false) after returning to queue, but got true")
|
|
}
|
|
}
|
|
|
|
func TestFlushUnschedulablePodsLeftoverSetsFlag_GatedPod(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
gatedBeforeFlush bool
|
|
gatedAfterFlush bool
|
|
backingOff bool
|
|
wantWasFlushedFromUnschedulable bool
|
|
wantQ string
|
|
}{
|
|
{
|
|
name: "flag is set when pod is not gated",
|
|
wantWasFlushedFromUnschedulable: true,
|
|
wantQ: activeQ,
|
|
},
|
|
{
|
|
name: "flag is set when pod is no longer gated",
|
|
gatedBeforeFlush: true,
|
|
wantWasFlushedFromUnschedulable: true,
|
|
wantQ: activeQ,
|
|
},
|
|
{
|
|
name: "flag is unset when pod is newly gated",
|
|
gatedAfterFlush: true,
|
|
wantWasFlushedFromUnschedulable: false,
|
|
wantQ: unschedulableQ,
|
|
},
|
|
{
|
|
name: "flag is unset when pod is still gated",
|
|
gatedBeforeFlush: true,
|
|
gatedAfterFlush: true,
|
|
wantWasFlushedFromUnschedulable: false,
|
|
wantQ: unschedulableQ,
|
|
},
|
|
{
|
|
name: "flag is set when pod is not gated and backoff is not complete",
|
|
backingOff: true,
|
|
wantWasFlushedFromUnschedulable: true,
|
|
wantQ: backoffQ,
|
|
},
|
|
{
|
|
name: "flag is set when pod is no longer gated and backoff is not complete",
|
|
gatedBeforeFlush: true,
|
|
backingOff: true,
|
|
wantWasFlushedFromUnschedulable: true,
|
|
wantQ: backoffQ,
|
|
},
|
|
{
|
|
name: "flag is unset when pod is newly gated and backoff is not complete",
|
|
gatedAfterFlush: true,
|
|
backingOff: true,
|
|
wantWasFlushedFromUnschedulable: false,
|
|
wantQ: unschedulableQ,
|
|
},
|
|
{
|
|
name: "flag is unset when pod is still gated and backoff is not complete",
|
|
gatedBeforeFlush: true,
|
|
gatedAfterFlush: true,
|
|
backingOff: true,
|
|
wantWasFlushedFromUnschedulable: false,
|
|
wantQ: unschedulableQ,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
const allowedLabel = "allow"
|
|
const preEnqueuePluginName = "preEnqueuePlugin"
|
|
|
|
var backoffDuration time.Duration
|
|
if tt.backingOff {
|
|
backoffDuration = DefaultPodMaxInUnschedulablePodsDuration + time.Minute
|
|
}
|
|
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
preEnqM := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
preEnqueuePluginName: &preEnqueuePlugin{allowlists: []string{allowedLabel}},
|
|
},
|
|
}
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c), WithQueueingHintMapPerProfile(m), WithPreEnqueuePluginMap(preEnqM),
|
|
WithPodInitialBackoffDuration(backoffDuration), WithPodMaxBackoffDuration(backoffDuration))
|
|
|
|
pod := st.MakePod().Name("pod1").Namespace("ns1").UID("1").Label(allowedLabel, "").Obj()
|
|
podInfo := &framework.QueuedPodInfo{PodInfo: mustNewPodInfo(pod), QueueingParams: framework.QueueingParams{UnschedulablePlugins: sets.New("foo")}}
|
|
if tt.gatedBeforeFlush {
|
|
podInfo = setQueuedPodInfoGated(podInfo, preEnqueuePluginName, []fwk.ClusterEvent{})
|
|
}
|
|
|
|
q.Add(ctx, podInfo.Pod)
|
|
_, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Fatalf("Failed to pop from active queue: %v", err)
|
|
}
|
|
|
|
if tt.gatedAfterFlush {
|
|
delete(pod.Labels, allowedLabel)
|
|
}
|
|
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, podInfo, q.SchedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("Failed to add pod to unschedulable: %v", err)
|
|
}
|
|
|
|
// Advance time past the flush duration and flush
|
|
c.Step(DefaultPodMaxInUnschedulablePodsDuration + time.Second)
|
|
q.flushUnschedulableEntitiesLeftover(logger)
|
|
|
|
queueSizes := map[string]int{
|
|
unschedulableQ: len(q.UnschedulablePods()),
|
|
backoffQ: len(q.PodsInBackoffQ()),
|
|
activeQ: len(q.PodsInActiveQ()),
|
|
}
|
|
|
|
if queueSizes[tt.wantQ] == 0 {
|
|
t.Errorf("Pod not found in %s", tt.wantQ)
|
|
}
|
|
actualPod, ok := q.GetPod(podInfo.Pod.Name, podInfo.Pod.Namespace, nil)
|
|
if !ok {
|
|
t.Fatalf("Pod not found in scheduling queue")
|
|
}
|
|
if actualPod.WasFlushedFromUnschedulable != tt.wantWasFlushedFromUnschedulable {
|
|
t.Errorf("Unexpected WasFlushedFromUnschedulable value: %v", actualPod.WasFlushedFromUnschedulable)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAddAttemptedPodGroupIfNeeded verifies that AddAttemptedPodGroupIfNeeded
|
|
// correctly handles pod groups with or without failed plugins.
|
|
func TestAddAttemptedPodGroupIfNeeded(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.GenericWorkload, true)
|
|
|
|
pgName := "test-pg"
|
|
pod1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Priority(midPriority).PodGroupName(pgName).Obj()
|
|
pod2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Priority(midPriority).PodGroupName(pgName).Obj()
|
|
pgLookup := newQueuedPodGroupInfoForLookup(pod1)
|
|
|
|
tests := []struct {
|
|
name string
|
|
setup func(t ktesting.TContext, q *PriorityQueue, pgInfo *framework.QueuedPodGroupInfo)
|
|
disableBackoff bool
|
|
blockOnPreEnqueue bool
|
|
expectedUnschedulableCount int
|
|
expectedConsecutiveErrorsCount int
|
|
expectedInActiveQ bool
|
|
expectedInBackoffQ bool
|
|
expectedInUnschedulableEntities bool
|
|
expectedPodsInGroup int
|
|
}{
|
|
{
|
|
name: "No pending pods, pod group is not enqueued",
|
|
setup: func(tCtx ktesting.TContext, q *PriorityQueue, pgInfo *framework.QueuedPodGroupInfo) {
|
|
// No setup needed, pendingPodGroupPods is empty
|
|
},
|
|
expectedPodsInGroup: 1,
|
|
},
|
|
{
|
|
name: "Pods present, no unschedulable plugins, pod group not backing off",
|
|
setup: func(tCtx ktesting.TContext, q *PriorityQueue, pgInfo *framework.QueuedPodGroupInfo) {
|
|
pInfo1 := q.newQueuedPodInfo(tCtx, pod1)
|
|
pInfo2 := q.newQueuedPodInfo(tCtx, pod2)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo1)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo2)
|
|
},
|
|
disableBackoff: true,
|
|
expectedConsecutiveErrorsCount: 1,
|
|
expectedInActiveQ: true,
|
|
expectedPodsInGroup: 2,
|
|
},
|
|
{
|
|
name: "Pods present, one with unschedulable plugins, pod group not backing off",
|
|
setup: func(tCtx ktesting.TContext, q *PriorityQueue, pgInfo *framework.QueuedPodGroupInfo) {
|
|
pInfo1 := q.newQueuedPodInfo(tCtx, pod1)
|
|
pInfo2 := q.newQueuedPodInfo(tCtx, pod2, "fakePlugin")
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo1)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo2)
|
|
},
|
|
disableBackoff: true,
|
|
expectedConsecutiveErrorsCount: 1,
|
|
expectedInActiveQ: true,
|
|
expectedPodsInGroup: 2,
|
|
},
|
|
{
|
|
name: "Pods present, with unschedulable plugins, pod group not backing off",
|
|
setup: func(tCtx ktesting.TContext, q *PriorityQueue, pgInfo *framework.QueuedPodGroupInfo) {
|
|
pInfo1 := q.newQueuedPodInfo(tCtx, pod1, "fakePlugin")
|
|
pInfo2 := q.newQueuedPodInfo(tCtx, pod2, "fakePlugin")
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo1)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo2)
|
|
pgInfo.ConsecutiveErrorsCount = 5 // Set to non-zero to verify reset
|
|
},
|
|
disableBackoff: true,
|
|
expectedUnschedulableCount: 1,
|
|
expectedInActiveQ: true,
|
|
expectedPodsInGroup: 2,
|
|
},
|
|
{
|
|
name: "Pods present, with unschedulable plugins, pod group not backing off, but blocked on PreEnqueue",
|
|
setup: func(tCtx ktesting.TContext, q *PriorityQueue, pgInfo *framework.QueuedPodGroupInfo) {
|
|
pInfo1 := q.newQueuedPodInfo(tCtx, pod1, "fakePlugin")
|
|
pInfo2 := q.newQueuedPodInfo(tCtx, pod2, "fakePlugin")
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo1)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo2)
|
|
},
|
|
disableBackoff: true,
|
|
blockOnPreEnqueue: true,
|
|
expectedUnschedulableCount: 1,
|
|
expectedInUnschedulableEntities: true,
|
|
expectedPodsInGroup: 2,
|
|
},
|
|
{
|
|
name: "Pods present, no unschedulable plugins, pod group backing off",
|
|
setup: func(tCtx ktesting.TContext, q *PriorityQueue, pgInfo *framework.QueuedPodGroupInfo) {
|
|
pInfo1 := q.newQueuedPodInfo(tCtx, pod1)
|
|
pInfo2 := q.newQueuedPodInfo(tCtx, pod2)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo1)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo2)
|
|
},
|
|
expectedConsecutiveErrorsCount: 1,
|
|
expectedInBackoffQ: true,
|
|
expectedPodsInGroup: 2,
|
|
},
|
|
{
|
|
name: "Pods present, one with unschedulable plugin, pod group backing off",
|
|
setup: func(tCtx ktesting.TContext, q *PriorityQueue, pgInfo *framework.QueuedPodGroupInfo) {
|
|
pInfo1 := q.newQueuedPodInfo(tCtx, pod1)
|
|
pInfo2 := q.newQueuedPodInfo(tCtx, pod2, "fakePlugin")
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo1)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo2)
|
|
},
|
|
expectedConsecutiveErrorsCount: 1,
|
|
expectedInBackoffQ: true,
|
|
expectedPodsInGroup: 2,
|
|
},
|
|
{
|
|
name: "Pods present, with unschedulable plugins, pod group backing off",
|
|
setup: func(tCtx ktesting.TContext, q *PriorityQueue, pgInfo *framework.QueuedPodGroupInfo) {
|
|
pInfo1 := q.newQueuedPodInfo(tCtx, pod1, "fakePlugin")
|
|
pInfo2 := q.newQueuedPodInfo(tCtx, pod2, "fakePlugin")
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo1)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo2)
|
|
pgInfo.ConsecutiveErrorsCount = 5 // Set to non-zero to verify reset
|
|
},
|
|
expectedUnschedulableCount: 1,
|
|
expectedInBackoffQ: true,
|
|
expectedPodsInGroup: 2,
|
|
},
|
|
{
|
|
name: "Pods present, with unschedulable plugins, pod group backing off, but blocked on PreEnqueue",
|
|
setup: func(tCtx ktesting.TContext, q *PriorityQueue, pgInfo *framework.QueuedPodGroupInfo) {
|
|
pInfo1 := q.newQueuedPodInfo(tCtx, pod1)
|
|
pInfo2 := q.newQueuedPodInfo(tCtx, pod2)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo1)
|
|
q.pendingPodGroupPods.add(pgLookup, pInfo2)
|
|
},
|
|
blockOnPreEnqueue: true,
|
|
expectedConsecutiveErrorsCount: 1,
|
|
expectedInUnschedulableEntities: true,
|
|
expectedPodsInGroup: 2,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
tCtx := ktesting.Init(t)
|
|
|
|
c := testingclock.NewFakeClock(time.Now())
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
preEnqueueMap := make(map[string]map[string]fwk.PreEnqueuePlugin)
|
|
if test.blockOnPreEnqueue {
|
|
preEnqueueMap[""] = map[string]fwk.PreEnqueuePlugin{
|
|
"testPlugin": &preEnqueuePlugin{},
|
|
}
|
|
}
|
|
|
|
opts := []Option{WithClock(c), WithQueueingHintMapPerProfile(m), WithPreEnqueuePluginMap(preEnqueueMap)}
|
|
if test.disableBackoff {
|
|
opts = append(opts, WithPodInitialBackoffDuration(0), WithPodMaxBackoffDuration(0))
|
|
}
|
|
q := NewTestQueue(tCtx, newDefaultQueueSort(), opts...)
|
|
|
|
pgInfo := q.newQueuedPodGroupInfo(q.newQueuedPodInfo(tCtx, pod1))
|
|
|
|
test.setup(tCtx, q, pgInfo)
|
|
|
|
err := q.AddAttemptedPodGroupIfNeeded(tCtx.Logger(), pgInfo, q.SchedulingCycle())
|
|
if err != nil {
|
|
tCtx.Fatalf("Unexpected error from AddAttemptedPodGroupIfNeeded: %v", err)
|
|
}
|
|
|
|
if pgInfo.UnschedulableCount != test.expectedUnschedulableCount {
|
|
tCtx.Errorf("Expected UnschedulableCount to be %v, got %v", test.expectedUnschedulableCount, pgInfo.UnschedulableCount)
|
|
}
|
|
if pgInfo.ConsecutiveErrorsCount != test.expectedConsecutiveErrorsCount {
|
|
tCtx.Errorf("Expected ConsecutiveErrorsCount to be %v, got %v", test.expectedConsecutiveErrorsCount, pgInfo.ConsecutiveErrorsCount)
|
|
}
|
|
if isInActiveQ := q.activeQ.has(pgInfo); isInActiveQ != test.expectedInActiveQ {
|
|
tCtx.Errorf("Expected pod group to be in activeQ: %v, got %v", test.expectedInActiveQ, isInActiveQ)
|
|
}
|
|
if isInBackoffQ := q.backoffQ.has(pgInfo); isInBackoffQ != test.expectedInBackoffQ {
|
|
tCtx.Errorf("Expected pod group to be in backoffQ: %v, got %v", test.expectedInBackoffQ, isInBackoffQ)
|
|
}
|
|
if isInUnschedulable := q.unschedulableEntities.get(pgInfo) != nil; isInUnschedulable != test.expectedInUnschedulableEntities {
|
|
tCtx.Errorf("Expected pod group to be in unschedulableEntities: %v, got %v", test.expectedInUnschedulableEntities, isInUnschedulable)
|
|
}
|
|
if len(q.pendingPodGroupPods.get(pgInfo)) != 0 {
|
|
tCtx.Errorf("Expected pendingPodGroupPods to be cleared")
|
|
}
|
|
if len(pgInfo.QueuedPodInfos) != test.expectedPodsInGroup {
|
|
tCtx.Errorf("Expected QueuedPodInfos to have %v elements, got %v", test.expectedPodsInGroup, len(pgInfo.QueuedPodInfos))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_initPodMaxInUnschedulablePodsDuration(t *testing.T) {
|
|
pod1 := st.MakePod().Name("test-pod-1").Namespace("ns1").UID("tp-1").NominatedNodeName("node1").Obj()
|
|
pod2 := st.MakePod().Name("test-pod-2").Namespace("ns2").UID("tp-2").NominatedNodeName("node2").Obj()
|
|
|
|
var timestamp = time.Now()
|
|
pInfo1 := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewTestPodInfo(t, pod1),
|
|
QueueingParams: framework.QueueingParams{
|
|
Timestamp: timestamp.Add(-time.Second),
|
|
},
|
|
}
|
|
pInfo2 := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewTestPodInfo(t, pod2),
|
|
QueueingParams: framework.QueueingParams{
|
|
Timestamp: timestamp.Add(-2 * time.Second),
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
podMaxInUnschedulablePodsDuration time.Duration
|
|
operations []operation
|
|
operands []*framework.QueuedPodInfo
|
|
expected []*framework.QueuedPodInfo
|
|
}{
|
|
{
|
|
name: "New priority queue by the default value of podMaxInUnschedulablePodsDuration",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
addPodUnschedulablePods,
|
|
flushUnscheduledQ,
|
|
},
|
|
operands: []*framework.QueuedPodInfo{pInfo1, pInfo2, nil},
|
|
expected: []*framework.QueuedPodInfo{pInfo2, pInfo1},
|
|
},
|
|
{
|
|
name: "New priority queue by user-defined value of podMaxInUnschedulablePodsDuration",
|
|
podMaxInUnschedulablePodsDuration: 30 * time.Second,
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
addPodUnschedulablePods,
|
|
flushUnscheduledQ,
|
|
},
|
|
operands: []*framework.QueuedPodInfo{pInfo1, pInfo2, nil},
|
|
expected: []*framework.QueuedPodInfo{pInfo2, pInfo1},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
tCtx := ktesting.Init(t)
|
|
logger := klog.FromContext(tCtx)
|
|
var queue *PriorityQueue
|
|
if test.podMaxInUnschedulablePodsDuration > 0 {
|
|
queue = NewTestQueue(tCtx, newDefaultQueueSort(),
|
|
WithClock(testingclock.NewFakeClock(timestamp)),
|
|
WithPodMaxInUnschedulablePodsDuration(test.podMaxInUnschedulablePodsDuration))
|
|
} else {
|
|
queue = NewTestQueue(tCtx, newDefaultQueueSort(),
|
|
WithClock(testingclock.NewFakeClock(timestamp)))
|
|
}
|
|
|
|
var podInfoList []*framework.QueuedPodInfo
|
|
|
|
for i, op := range test.operations {
|
|
op(tCtx, queue, test.operands[i])
|
|
}
|
|
|
|
expectedLen := len(test.expected)
|
|
if queue.activeQ.len() != expectedLen {
|
|
t.Fatalf("Expected %v items to be in activeQ, but got: %v", expectedLen, queue.activeQ.len())
|
|
}
|
|
|
|
for i := 0; i < expectedLen; i++ {
|
|
entity, err := queue.activeQ.pop(logger)
|
|
if err != nil {
|
|
t.Errorf("Error while popping the head of the queue: %v", err)
|
|
} else {
|
|
pInfo := entity.(*framework.QueuedPodInfo)
|
|
podInfoList = append(podInfoList, pInfo)
|
|
// Cleanup attempts counter incremented in activeQ.pop()
|
|
pInfo.Attempts = 0
|
|
}
|
|
}
|
|
|
|
if diff := cmp.Diff(test.expected, podInfoList, cmpopts.IgnoreUnexported(framework.PodInfo{})); diff != "" {
|
|
t.Errorf("Unexpected QueuedPodInfo list (-want, +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type operation func(tCtx ktesting.TContext, queue *PriorityQueue, pInfo *framework.QueuedPodInfo)
|
|
|
|
var (
|
|
add = func(tCtx ktesting.TContext, queue *PriorityQueue, pInfo *framework.QueuedPodInfo) {
|
|
queue.Add(tCtx, pInfo.Pod)
|
|
}
|
|
pop = func(tCtx ktesting.TContext, queue *PriorityQueue, _ *framework.QueuedPodInfo) {
|
|
_, err := queue.Pop(klog.FromContext(tCtx))
|
|
if err != nil {
|
|
tCtx.Fatalf("Unexpected error during Pop: %v", err)
|
|
}
|
|
}
|
|
popAndRequeueAsUnschedulable = func(tCtx ktesting.TContext, queue *PriorityQueue, pInfo *framework.QueuedPodInfo) {
|
|
// To simulate the pod is failed in scheduling in the real world, Pop() the pod from activeQ before AddUnschedulablePodIfNotPresent() below.
|
|
// UnschedulablePlugins and PendingPlugins will get cleared by Pop, so make a copy first.
|
|
logger := klog.FromContext(tCtx)
|
|
unschedulablePlugins := pInfo.UnschedulablePlugins.Clone()
|
|
pendingPlugins := pInfo.PendingPlugins.Clone()
|
|
queue.Add(tCtx, pInfo.Pod)
|
|
entity, err := queue.Pop(logger)
|
|
p := entity.(*framework.QueuedPodInfo)
|
|
if err != nil {
|
|
tCtx.Fatalf("Unexpected error during Pop: %v", err)
|
|
} else if diff := cmp.Diff(pInfo.Pod, p.Pod); diff != "" {
|
|
tCtx.Fatalf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
// Simulate plugins that are waiting for some events.
|
|
p.UnschedulablePlugins = unschedulablePlugins
|
|
p.PendingPlugins = pendingPlugins
|
|
if err := queue.AddUnschedulablePodIfNotPresent(logger, p, 1); err != nil {
|
|
tCtx.Fatalf("Unexpected error during AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
}
|
|
popAndRequeueAsBackoff = func(tCtx ktesting.TContext, queue *PriorityQueue, pInfo *framework.QueuedPodInfo) {
|
|
// To simulate the pod is failed in scheduling in the real world, Pop() the pod from activeQ before AddUnschedulablePodIfNotPresent() below.
|
|
logger := klog.FromContext(tCtx)
|
|
queue.Add(tCtx, pInfo.Pod)
|
|
entity, err := queue.Pop(logger)
|
|
p := entity.(*framework.QueuedPodInfo)
|
|
if err != nil {
|
|
tCtx.Fatalf("Unexpected error during Pop: %v", err)
|
|
} else if diff := cmp.Diff(pInfo.Pod, p.Pod); diff != "" {
|
|
tCtx.Fatalf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
// needs to increment it to make it backoff
|
|
p.UnschedulableCount++
|
|
// When there is no known unschedulable plugin, pods always go to the backoff queue.
|
|
if err := queue.AddUnschedulablePodIfNotPresent(logger, p, 1); err != nil {
|
|
tCtx.Fatalf("Unexpected error during AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
}
|
|
addPodActiveQ = func(tCtx ktesting.TContext, queue *PriorityQueue, pInfo *framework.QueuedPodInfo) {
|
|
queue.Add(tCtx, pInfo.Pod)
|
|
}
|
|
addPodActiveQDirectly = func(tCtx ktesting.TContext, queue *PriorityQueue, pInfo *framework.QueuedPodInfo) {
|
|
queue.activeQ.add(klog.FromContext(tCtx), pInfo, framework.EventUnscheduledPodAdd.Label())
|
|
}
|
|
addPodUnschedulablePods = func(tCtx ktesting.TContext, queue *PriorityQueue, pInfo *framework.QueuedPodInfo) {
|
|
if !pInfo.Gated() {
|
|
// Update pod condition to unschedulable.
|
|
podutil.UpdatePodCondition(&pInfo.Pod.Status, &v1.PodCondition{
|
|
Type: v1.PodScheduled,
|
|
Status: v1.ConditionFalse,
|
|
Reason: v1.PodReasonUnschedulable,
|
|
Message: "fake scheduling failure",
|
|
})
|
|
// needs to increment it to make it backoff
|
|
pInfo.UnschedulableCount++
|
|
}
|
|
queue.unschedulableEntities.addOrUpdate(pInfo, false, framework.EventUnscheduledPodAdd.Label())
|
|
}
|
|
deletePod = func(tCtx ktesting.TContext, queue *PriorityQueue, pInfo *framework.QueuedPodInfo) {
|
|
queue.Delete(tCtx.Logger(), pInfo.Pod)
|
|
}
|
|
updatePodQueueable = func(tCtx ktesting.TContext, queue *PriorityQueue, pInfo *framework.QueuedPodInfo) {
|
|
newPod := pInfo.Pod.DeepCopy()
|
|
newPod.Labels = map[string]string{"queueable": ""}
|
|
queue.Update(tCtx, pInfo.Pod, newPod)
|
|
}
|
|
addPodBackoffQ = func(tCtx ktesting.TContext, queue *PriorityQueue, pInfo *framework.QueuedPodInfo) {
|
|
queue.backoffQ.add(klog.FromContext(tCtx), pInfo, framework.EventUnscheduledPodAdd.Label())
|
|
}
|
|
moveAllToActiveOrBackoffQ = func(tCtx ktesting.TContext, queue *PriorityQueue, _ *framework.QueuedPodInfo) {
|
|
queue.MoveAllToActiveOrBackoffQueue(klog.FromContext(tCtx), framework.EventUnschedulableTimeout, nil, nil, nil)
|
|
}
|
|
flushBackoffQ = func(tCtx ktesting.TContext, queue *PriorityQueue, _ *framework.QueuedPodInfo) {
|
|
queue.clock.(*testingclock.FakeClock).Step(3 * time.Second)
|
|
queue.flushBackoffQCompleted(klog.FromContext(tCtx))
|
|
}
|
|
moveClockForward = func(tCtx ktesting.TContext, queue *PriorityQueue, _ *framework.QueuedPodInfo) {
|
|
queue.clock.(*testingclock.FakeClock).Step(3 * time.Second)
|
|
}
|
|
flushUnscheduledQ = func(tCtx ktesting.TContext, queue *PriorityQueue, _ *framework.QueuedPodInfo) {
|
|
queue.clock.(*testingclock.FakeClock).Step(queue.podMaxInUnschedulablePodsDuration)
|
|
queue.flushUnschedulableEntitiesLeftover(klog.FromContext(tCtx))
|
|
}
|
|
updatePluginToGateAllPods = func(tCtx ktesting.TContext, queue *PriorityQueue, _ *framework.QueuedPodInfo) {
|
|
queue.preEnqueuePluginMap[""]["preEnqueuePlugin"] = &preEnqueuePlugin{allowlists: []string{""}}
|
|
}
|
|
updatePluginToUngateAllPods = func(tCtx ktesting.TContext, queue *PriorityQueue, _ *framework.QueuedPodInfo) {
|
|
queue.preEnqueuePluginMap[""]["preEnqueuePlugin"] = &preEnqueuePlugin{allowlists: []string{"queueable"}}
|
|
}
|
|
)
|
|
|
|
// TestPodTimestamp tests the operations related to QueuedPodInfo.
|
|
func TestPodTimestamp(t *testing.T) {
|
|
pod1 := st.MakePod().Name("test-pod-1").Namespace("ns1").UID("tp-1").NominatedNodeName("node1").Obj()
|
|
pod2 := st.MakePod().Name("test-pod-2").Namespace("ns2").UID("tp-2").NominatedNodeName("node2").Obj()
|
|
|
|
var timestamp = time.Now()
|
|
pInfo1 := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewTestPodInfo(t, pod1),
|
|
QueueingParams: framework.QueueingParams{
|
|
Timestamp: timestamp,
|
|
},
|
|
}
|
|
pInfo2 := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewTestPodInfo(t, pod2),
|
|
QueueingParams: framework.QueueingParams{
|
|
Timestamp: timestamp.Add(time.Second),
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
operations []operation
|
|
operands []*framework.QueuedPodInfo
|
|
expected []*framework.QueuedPodInfo
|
|
}{
|
|
{
|
|
name: "add two pod to activeQ and sort them by the timestamp",
|
|
operations: []operation{
|
|
// Need to add the pods directly to the activeQ to override the timestamps.
|
|
addPodActiveQDirectly,
|
|
addPodActiveQDirectly,
|
|
},
|
|
operands: []*framework.QueuedPodInfo{pInfo2, pInfo1},
|
|
expected: []*framework.QueuedPodInfo{pInfo1, pInfo2},
|
|
},
|
|
{
|
|
name: "add two pod to unschedulableEntities then move them to activeQ and sort them by the timestamp",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
addPodUnschedulablePods,
|
|
moveClockForward,
|
|
moveAllToActiveOrBackoffQ,
|
|
},
|
|
operands: []*framework.QueuedPodInfo{pInfo2, pInfo1, nil, nil},
|
|
expected: []*framework.QueuedPodInfo{pInfo1, pInfo2},
|
|
},
|
|
{
|
|
name: "add one pod to BackoffQ and move it to activeQ",
|
|
operations: []operation{
|
|
// Need to add the pods directly to activeQ to override the timestamps.
|
|
addPodActiveQDirectly,
|
|
addPodBackoffQ,
|
|
flushBackoffQ,
|
|
moveAllToActiveOrBackoffQ,
|
|
},
|
|
operands: []*framework.QueuedPodInfo{pInfo2, pInfo1, nil, nil},
|
|
expected: []*framework.QueuedPodInfo{pInfo1, pInfo2},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
tCtx := ktesting.Init(t)
|
|
logger := klog.FromContext(tCtx)
|
|
queue := NewTestQueue(tCtx, newDefaultQueueSort(), WithClock(testingclock.NewFakeClock(timestamp)))
|
|
var podInfoList []*framework.QueuedPodInfo
|
|
|
|
for i, op := range test.operations {
|
|
op(tCtx, queue, test.operands[i])
|
|
}
|
|
|
|
expectedLen := len(test.expected)
|
|
if queue.activeQ.len() != expectedLen {
|
|
t.Fatalf("Expected %v items to be in activeQ, but got: %v", expectedLen, queue.activeQ.len())
|
|
}
|
|
|
|
for i := 0; i < expectedLen; i++ {
|
|
entity, err := queue.activeQ.pop(logger)
|
|
if err != nil {
|
|
t.Errorf("Error while popping the head of the queue: %v", err)
|
|
} else {
|
|
pInfo := entity.(*framework.QueuedPodInfo)
|
|
podInfoList = append(podInfoList, pInfo)
|
|
// Cleanup attempts counter incremented in activeQ.pop()
|
|
pInfo.Attempts = 0
|
|
}
|
|
}
|
|
|
|
if diff := cmp.Diff(test.expected, podInfoList, cmpopts.IgnoreUnexported(framework.PodInfo{})); diff != "" {
|
|
t.Errorf("Unexpected QueuedPodInfo list (-want, +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSchedulerPodsMetric tests Prometheus metrics
|
|
func TestSchedulerPodsMetric(t *testing.T) {
|
|
timestamp := time.Now()
|
|
preenqueuePluginName := "preEnqueuePlugin"
|
|
metrics.Register()
|
|
total := 60
|
|
queueableNum := 50
|
|
queueable, failme := "queueable", "failme"
|
|
// First 50 Pods are queueable.
|
|
pInfos := makeQueuedPodInfos(queueableNum, "x", queueable, timestamp)
|
|
// The last 10 Pods are not queueable.
|
|
gated := makeQueuedPodInfos(total-queueableNum, "y", failme, timestamp)
|
|
// Manually mark them as gated=true.
|
|
for _, pInfo := range gated {
|
|
setQueuedPodInfoGated(pInfo, preenqueuePluginName, []fwk.ClusterEvent{framework.EventUnscheduledPodUpdate})
|
|
}
|
|
pInfos = append(pInfos, gated...)
|
|
totalWithDelay := 20
|
|
pInfosWithDelay := makeQueuedPodInfos(totalWithDelay, "z", queueable, timestamp.Add(2*time.Second))
|
|
|
|
resetPodInfos := func() {
|
|
// reset PodInfo's Attempts because they influence the backoff time calculation.
|
|
for i := range pInfos {
|
|
pInfos[i].Attempts = 0
|
|
pInfos[i].UnschedulableCount = 0
|
|
}
|
|
for i := range pInfosWithDelay {
|
|
pInfosWithDelay[i].Attempts = 0
|
|
pInfosWithDelay[i].UnschedulableCount = 0
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
operations []operation
|
|
operands [][]*framework.QueuedPodInfo
|
|
metricsName string
|
|
pluginMetricsSamplePercent int
|
|
disablePopFromBackoffQ bool
|
|
wants string
|
|
}{
|
|
{
|
|
name: "add pods to activeQ and unschedulableEntities",
|
|
operations: []operation{
|
|
addPodActiveQ,
|
|
addPodUnschedulablePods,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:30],
|
|
pInfos[30:],
|
|
},
|
|
metricsName: "scheduler_pending_pods",
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 30
|
|
scheduler_pending_pods{queue="backoff"} 0
|
|
scheduler_pending_pods{queue="gated"} 10
|
|
scheduler_pending_pods{queue="unschedulable"} 20
|
|
`,
|
|
},
|
|
{
|
|
name: "add pods to all kinds of queues",
|
|
operations: []operation{
|
|
addPodActiveQ,
|
|
addPodBackoffQ,
|
|
addPodUnschedulablePods,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:15],
|
|
pInfos[15:40],
|
|
pInfos[40:],
|
|
},
|
|
metricsName: "scheduler_pending_pods",
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 15
|
|
scheduler_pending_pods{queue="backoff"} 25
|
|
scheduler_pending_pods{queue="gated"} 10
|
|
scheduler_pending_pods{queue="unschedulable"} 10
|
|
`,
|
|
},
|
|
{
|
|
name: "add pods to unschedulableEntities and then move all to activeQ",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
moveClockForward,
|
|
moveAllToActiveOrBackoffQ,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:total],
|
|
{nil},
|
|
{nil},
|
|
},
|
|
metricsName: "scheduler_pending_pods",
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 50
|
|
scheduler_pending_pods{queue="backoff"} 0
|
|
scheduler_pending_pods{queue="gated"} 10
|
|
scheduler_pending_pods{queue="unschedulable"} 0
|
|
`,
|
|
},
|
|
{
|
|
name: "make some pods subject to backoff, add pods to unschedulableEntities, and then move all to activeQ",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
moveClockForward,
|
|
addPodUnschedulablePods,
|
|
moveAllToActiveOrBackoffQ,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[20:total],
|
|
{nil},
|
|
pInfosWithDelay[:20],
|
|
{nil},
|
|
},
|
|
metricsName: "scheduler_pending_pods",
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 30
|
|
scheduler_pending_pods{queue="backoff"} 20
|
|
scheduler_pending_pods{queue="gated"} 10
|
|
scheduler_pending_pods{queue="unschedulable"} 0
|
|
`,
|
|
},
|
|
{
|
|
name: "make some pods subject to backoff, add pods to unschedulableEntities/activeQ, move all to activeQ, and finally flush backoffQ",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
addPodActiveQ,
|
|
moveAllToActiveOrBackoffQ,
|
|
flushBackoffQ,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:40],
|
|
pInfos[40:50],
|
|
{nil},
|
|
{nil},
|
|
},
|
|
metricsName: "scheduler_pending_pods",
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 50
|
|
scheduler_pending_pods{queue="backoff"} 0
|
|
scheduler_pending_pods{queue="gated"} 0
|
|
scheduler_pending_pods{queue="unschedulable"} 0
|
|
`,
|
|
},
|
|
{
|
|
name: "add pods to activeQ/unschedulableEntities and then delete some Pods",
|
|
operations: []operation{
|
|
addPodActiveQ,
|
|
addPodUnschedulablePods,
|
|
deletePod,
|
|
deletePod,
|
|
deletePod,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:30],
|
|
pInfos[30:],
|
|
pInfos[:2],
|
|
pInfos[30:33],
|
|
pInfos[50:54],
|
|
},
|
|
metricsName: "scheduler_pending_pods",
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 28
|
|
scheduler_pending_pods{queue="backoff"} 0
|
|
scheduler_pending_pods{queue="gated"} 6
|
|
scheduler_pending_pods{queue="unschedulable"} 17
|
|
`,
|
|
},
|
|
{
|
|
name: "add pods to activeQ/unschedulableEntities and then update some Pods as queueable",
|
|
operations: []operation{
|
|
addPodActiveQ,
|
|
addPodUnschedulablePods,
|
|
updatePodQueueable,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:30],
|
|
pInfos[30:],
|
|
pInfos[50:55],
|
|
},
|
|
metricsName: "scheduler_pending_pods",
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 35
|
|
scheduler_pending_pods{queue="backoff"} 0
|
|
scheduler_pending_pods{queue="gated"} 5
|
|
scheduler_pending_pods{queue="unschedulable"} 20
|
|
`,
|
|
},
|
|
{
|
|
name: "the metrics should not be recorded (pluginMetricsSamplePercent=0)",
|
|
operations: []operation{
|
|
add,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:1],
|
|
},
|
|
metricsName: "scheduler_plugin_execution_duration_seconds",
|
|
pluginMetricsSamplePercent: 0,
|
|
wants: `
|
|
# HELP scheduler_plugin_execution_duration_seconds [ALPHA] Duration for running a plugin at a specific extension point.
|
|
# TYPE scheduler_plugin_execution_duration_seconds histogram
|
|
`, // the observed value will always be 0, because we don't proceed the fake clock.
|
|
},
|
|
{
|
|
name: "the metrics should be recorded (pluginMetricsSamplePercent=100)",
|
|
operations: []operation{
|
|
add,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:1],
|
|
},
|
|
metricsName: "scheduler_plugin_execution_duration_seconds",
|
|
pluginMetricsSamplePercent: 100,
|
|
wants: `
|
|
# HELP scheduler_plugin_execution_duration_seconds [ALPHA] Duration for running a plugin at a specific extension point.
|
|
# TYPE scheduler_plugin_execution_duration_seconds histogram
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="1e-05"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="1.5000000000000002e-05"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="2.2500000000000005e-05"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="3.375000000000001e-05"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="5.062500000000001e-05"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="7.593750000000002e-05"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.00011390625000000003"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.00017085937500000006"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.0002562890625000001"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.00038443359375000017"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.0005766503906250003"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.0008649755859375004"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.0012974633789062506"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.0019461950683593758"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.0029192926025390638"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.004378938903808595"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.006568408355712893"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.009852612533569338"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.014778918800354007"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="0.02216837820053101"} 1
|
|
scheduler_plugin_execution_duration_seconds_bucket{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success",le="+Inf"} 1
|
|
scheduler_plugin_execution_duration_seconds_sum{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success"} 0
|
|
scheduler_plugin_execution_duration_seconds_count{extension_point="PreEnqueue",plugin="preEnqueuePlugin",status="Success"} 1
|
|
`, // the observed value will always be 0, because we don't proceed the fake clock.
|
|
},
|
|
{
|
|
name: "Gated metric should be 1 when Ungated to Gated transition into moveToActiveQ",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
moveClockForward,
|
|
updatePluginToGateAllPods,
|
|
updatePodQueueable,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:1],
|
|
{nil},
|
|
{nil},
|
|
pInfos[:1],
|
|
},
|
|
metricsName: "scheduler_pending_pods",
|
|
pluginMetricsSamplePercent: 100,
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 0
|
|
scheduler_pending_pods{queue="backoff"} 0
|
|
scheduler_pending_pods{queue="gated"} 1
|
|
scheduler_pending_pods{queue="unschedulable"} 0
|
|
`,
|
|
},
|
|
{
|
|
name: "Gated metric should be 1 when Ungated to Gated transition into moveToBackoffQ",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
updatePluginToGateAllPods,
|
|
updatePodQueueable,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:1],
|
|
{nil},
|
|
pInfos[:1],
|
|
},
|
|
metricsName: "scheduler_pending_pods",
|
|
pluginMetricsSamplePercent: 100,
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 0
|
|
scheduler_pending_pods{queue="backoff"} 0
|
|
scheduler_pending_pods{queue="gated"} 1
|
|
scheduler_pending_pods{queue="unschedulable"} 0
|
|
`,
|
|
},
|
|
{
|
|
name: "Gated metric should be 1 when Ungated to Gated transition when popFromBackoffQ is disabled",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
moveClockForward,
|
|
updatePluginToGateAllPods,
|
|
updatePodQueueable,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:1],
|
|
{nil},
|
|
{nil},
|
|
pInfos[:1],
|
|
},
|
|
metricsName: "scheduler_pending_pods",
|
|
pluginMetricsSamplePercent: 100,
|
|
disablePopFromBackoffQ: true,
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 0
|
|
scheduler_pending_pods{queue="backoff"} 0
|
|
scheduler_pending_pods{queue="gated"} 1
|
|
scheduler_pending_pods{queue="unschedulable"} 0
|
|
`,
|
|
},
|
|
{
|
|
name: "Gated metric should be 0 when Ungated -> Gated -> Ungated (ActiveQ) transition",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
moveClockForward,
|
|
updatePluginToGateAllPods,
|
|
updatePodQueueable,
|
|
updatePluginToUngateAllPods,
|
|
updatePodQueueable,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:1],
|
|
{nil},
|
|
{nil},
|
|
pInfos[:1],
|
|
{nil},
|
|
pInfos[:1],
|
|
},
|
|
pluginMetricsSamplePercent: 100,
|
|
metricsName: "scheduler_pending_pods",
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 1
|
|
scheduler_pending_pods{queue="backoff"} 0
|
|
scheduler_pending_pods{queue="gated"} 0
|
|
scheduler_pending_pods{queue="unschedulable"} 0
|
|
`,
|
|
},
|
|
{
|
|
name: "Gated metric should be 0 when Ungated -> Gated -> Ungated (BackoffQ) transition",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
updatePluginToGateAllPods,
|
|
updatePodQueueable,
|
|
updatePluginToUngateAllPods,
|
|
updatePodQueueable,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:1],
|
|
{nil},
|
|
pInfos[:1],
|
|
{nil},
|
|
pInfos[:1],
|
|
},
|
|
pluginMetricsSamplePercent: 100,
|
|
metricsName: "scheduler_pending_pods",
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 0
|
|
scheduler_pending_pods{queue="backoff"} 1
|
|
scheduler_pending_pods{queue="gated"} 0
|
|
scheduler_pending_pods{queue="unschedulable"} 0
|
|
`,
|
|
},
|
|
{
|
|
name: "Gated metric should be 0 when Ungated -> Gated -> Ungated transition, when popFromBackoffQ is disabled",
|
|
operations: []operation{
|
|
addPodUnschedulablePods,
|
|
moveClockForward,
|
|
updatePluginToGateAllPods,
|
|
updatePodQueueable,
|
|
updatePluginToUngateAllPods,
|
|
updatePodQueueable,
|
|
},
|
|
operands: [][]*framework.QueuedPodInfo{
|
|
pInfos[:1],
|
|
{nil},
|
|
{nil},
|
|
pInfos[:1],
|
|
{nil},
|
|
pInfos[:1],
|
|
},
|
|
pluginMetricsSamplePercent: 100,
|
|
disablePopFromBackoffQ: true,
|
|
wants: `
|
|
# HELP scheduler_pending_pods [STABLE] Number of pending pods, by the queue type. 'active' means number of pods in activeQ; 'backoff' means number of pods in backoffQ; 'unschedulable' means number of pods in unschedulableEntities that the scheduler attempted to schedule and failed; 'gated' is the number of unschedulable pods that the scheduler never attempted to schedule because they are gated.
|
|
# TYPE scheduler_pending_pods gauge
|
|
scheduler_pending_pods{queue="active"} 1
|
|
scheduler_pending_pods{queue="backoff"} 0
|
|
scheduler_pending_pods{queue="gated"} 0
|
|
scheduler_pending_pods{queue="unschedulable"} 0
|
|
`,
|
|
},
|
|
}
|
|
|
|
resetMetrics := func() {
|
|
metrics.ActivePods().Set(0)
|
|
metrics.BackoffPods().Set(0)
|
|
metrics.UnschedulablePods().Set(0)
|
|
metrics.GatedPods().Set(0)
|
|
metrics.PluginExecutionDuration.Reset()
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
tCtx := ktesting.Init(t)
|
|
resetMetrics()
|
|
resetPodInfos()
|
|
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
m[""][framework.EventTargetPodUpdate] = []*QueueingHintFunction{
|
|
{
|
|
PluginName: preenqueuePluginName,
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
}
|
|
preenq := map[string]map[string]fwk.PreEnqueuePlugin{"": {(&preEnqueuePlugin{}).Name(): &preEnqueuePlugin{allowlists: []string{queueable}}}}
|
|
recorder := metrics.NewMetricsAsyncRecorder(3, 20*time.Microsecond, tCtx.Done())
|
|
queue := NewTestQueue(tCtx, newDefaultQueueSort(), WithClock(testingclock.NewFakeClock(timestamp)), WithPreEnqueuePluginMap(preenq), WithPluginMetricsSamplePercent(test.pluginMetricsSamplePercent), WithMetricsRecorder(recorder), WithQueueingHintMapPerProfile(m))
|
|
queue.isPopFromBackoffQEnabled = !test.disablePopFromBackoffQ
|
|
for i, op := range test.operations {
|
|
for _, pInfo := range test.operands[i] {
|
|
op(tCtx, queue, pInfo)
|
|
}
|
|
}
|
|
|
|
recorder.FlushMetrics()
|
|
|
|
if err := testutil.GatherAndCompare(metrics.GetGather(), strings.NewReader(test.wants), test.metricsName); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPerPodSchedulingMetrics makes sure pod schedule attempts is updated correctly while
|
|
// initialAttemptTimestamp stays the same during multiple add/pop operations.
|
|
func TestPerPodSchedulingMetrics(t *testing.T) {
|
|
timestamp := time.Now()
|
|
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
perPodSchedulingMetricsScenario func(*testingclock.FakeClock, *PriorityQueue, *v1.Pod)
|
|
wantAttempts int
|
|
wantInitialAttemptTs time.Time
|
|
}{
|
|
{
|
|
// The queue operations are Add -> Pop.
|
|
name: "pod is created and scheduled after 1 attempt",
|
|
perPodSchedulingMetricsScenario: func(c *testingclock.FakeClock, queue *PriorityQueue, pod *v1.Pod) {
|
|
queue.Add(ctx, pod)
|
|
},
|
|
wantAttempts: 1,
|
|
wantInitialAttemptTs: timestamp,
|
|
},
|
|
{
|
|
// The queue operations are Add -> Pop -> AddUnschedulablePodIfNotPresent -> flushUnschedulableEntitiesLeftover -> Pop.
|
|
name: "pod is created and scheduled after 2 attempts",
|
|
perPodSchedulingMetricsScenario: func(c *testingclock.FakeClock, queue *PriorityQueue, pod *v1.Pod) {
|
|
queue.Add(ctx, pod)
|
|
entity, err := queue.Pop(logger)
|
|
pInfo := entity.(*framework.QueuedPodInfo)
|
|
if err != nil {
|
|
t.Fatalf("Failed to pop a pod %v", err)
|
|
}
|
|
|
|
pInfo.UnschedulablePlugins = sets.New("plugin")
|
|
err = queue.AddUnschedulablePodIfNotPresent(logger, pInfo, 1)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add unschedulable pod %v", err)
|
|
}
|
|
// Override clock to exceed the DefaultPodMaxInUnschedulablePodsDuration so that unschedulable pods
|
|
// will be moved to activeQ
|
|
c.SetTime(timestamp.Add(DefaultPodMaxInUnschedulablePodsDuration + 1))
|
|
queue.flushUnschedulableEntitiesLeftover(logger)
|
|
},
|
|
wantAttempts: 2,
|
|
wantInitialAttemptTs: timestamp,
|
|
},
|
|
{
|
|
// The queue operations are Add -> Pop -> AddUnschedulablePodIfNotPresent -> flushUnschedulableEntitiesLeftover -> Update -> Pop.
|
|
name: "pod is created and scheduled after 2 attempts but before the second pop, call update",
|
|
perPodSchedulingMetricsScenario: func(c *testingclock.FakeClock, queue *PriorityQueue, pod *v1.Pod) {
|
|
queue.Add(ctx, pod)
|
|
entity, err := queue.Pop(logger)
|
|
pInfo := entity.(*framework.QueuedPodInfo)
|
|
if err != nil {
|
|
t.Fatalf("Failed to pop a pod %v", err)
|
|
}
|
|
|
|
pInfo.UnschedulablePlugins = sets.New("plugin")
|
|
err = queue.AddUnschedulablePodIfNotPresent(logger, pInfo, 1)
|
|
if err != nil {
|
|
t.Fatalf("Failed to add unschedulable pod %v", err)
|
|
}
|
|
// Override clock to exceed the DefaultPodMaxInUnschedulablePodsDuration so that unschedulable pods
|
|
// will be moved to activeQ
|
|
updatedTimestamp := timestamp
|
|
c.SetTime(updatedTimestamp.Add(DefaultPodMaxInUnschedulablePodsDuration + 1))
|
|
queue.flushUnschedulableEntitiesLeftover(logger)
|
|
newPod := pod.DeepCopy()
|
|
newPod.Generation = 1
|
|
queue.Update(ctx, pod, newPod)
|
|
},
|
|
wantAttempts: 2,
|
|
wantInitialAttemptTs: timestamp,
|
|
},
|
|
{
|
|
// The queue operations are Add gated pod -> check unschedulableEntities -> lift gate & update pod -> Pop.
|
|
name: "A gated pod is created and scheduled after lifting gate",
|
|
perPodSchedulingMetricsScenario: func(c *testingclock.FakeClock, queue *PriorityQueue, pod *v1.Pod) {
|
|
// Create a queue with PreEnqueuePlugin
|
|
queue.preEnqueuePluginMap = map[string]map[string]fwk.PreEnqueuePlugin{"": {(&preEnqueuePlugin{}).Name(): &preEnqueuePlugin{allowlists: []string{"foo"}}}}
|
|
queue.pluginMetricsSamplePercent = 0
|
|
queue.Add(ctx, pod)
|
|
// Check pod is added to the unschedulableEntities queue.
|
|
if diff := cmp.Diff(pod, getUnschedulablePod(queue, pod)); diff != "" {
|
|
t.Errorf("Unexpected pod in unschedulableEntities (-want, +got):\n%s", diff)
|
|
}
|
|
// Override clock to get different InitialAttemptTimestamp
|
|
c.Step(1 * time.Minute)
|
|
|
|
// Update pod with the required label to get it out of unschedulableEntities queue.
|
|
updateGatedPod := pod.DeepCopy()
|
|
updateGatedPod.Labels = map[string]string{"foo": ""}
|
|
queue.Update(ctx, pod, updateGatedPod)
|
|
},
|
|
wantAttempts: 1,
|
|
wantInitialAttemptTs: timestamp.Add(1 * time.Minute),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
c := testingclock.NewFakeClock(timestamp)
|
|
pod := st.MakePod().Name("test-pod").Namespace("test-ns").UID("test-uid").Obj()
|
|
queue := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c))
|
|
|
|
test.perPodSchedulingMetricsScenario(c, queue, pod)
|
|
entity, err := queue.Pop(logger)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if entity.GetAttempts() != test.wantAttempts {
|
|
t.Errorf("Pod schedule attempt unexpected, got %v, want %v", entity.GetAttempts(), test.wantAttempts)
|
|
}
|
|
if *entity.GetInitialAttemptTimestamp() != test.wantInitialAttemptTs {
|
|
t.Errorf("Pod initial schedule attempt timestamp unexpected, got %v, want %v", *entity.GetInitialAttemptTimestamp(), test.wantInitialAttemptTs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIncomingPodsMetrics(t *testing.T) {
|
|
timestamp := time.Now()
|
|
unschedulablePlg := "unschedulable_plugin"
|
|
var pInfos = make([]*framework.QueuedPodInfo, 0, 3)
|
|
|
|
for i := 1; i <= 3; i++ {
|
|
p := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewTestPodInfo(t,
|
|
st.MakePod().Name(fmt.Sprintf("test-pod-%d", i)).Namespace(fmt.Sprintf("ns%d", i)).UID(fmt.Sprintf("tp-%d", i)).Obj()),
|
|
QueueingParams: framework.QueueingParams{
|
|
Timestamp: timestamp,
|
|
UnschedulablePlugins: sets.New(unschedulablePlg),
|
|
},
|
|
}
|
|
pInfos = append(pInfos, p)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
operations []operation
|
|
want string
|
|
}{
|
|
{
|
|
name: "add pods to activeQ",
|
|
operations: []operation{
|
|
add,
|
|
},
|
|
want: `
|
|
scheduler_queue_incoming_pods_total{event="UnscheduledPodAdd",queue="active"} 3
|
|
`,
|
|
},
|
|
{
|
|
name: "add unscheduled pods then make them unschedulable",
|
|
operations: []operation{
|
|
popAndRequeueAsUnschedulable,
|
|
},
|
|
want: `scheduler_queue_incoming_pods_total{event="UnscheduledPodAdd",queue="active"} 3
|
|
scheduler_queue_incoming_pods_total{event="ScheduleAttemptFailure",queue="unschedulable"} 3
|
|
`,
|
|
},
|
|
{
|
|
name: "add unscheduled pods, make them unschedulable, and move them to backoffQ",
|
|
operations: []operation{
|
|
popAndRequeueAsUnschedulable,
|
|
moveAllToActiveOrBackoffQ,
|
|
},
|
|
want: `scheduler_queue_incoming_pods_total{event="UnscheduledPodAdd",queue="active"} 3
|
|
scheduler_queue_incoming_pods_total{event="ScheduleAttemptFailure",queue="unschedulable"} 3
|
|
scheduler_queue_incoming_pods_total{event="UnschedulableTimeout",queue="backoff"} 3
|
|
`,
|
|
},
|
|
{
|
|
name: "add unscheduled pods, make them unschedulable, and move them to activeQ",
|
|
operations: []operation{
|
|
popAndRequeueAsUnschedulable,
|
|
moveClockForward,
|
|
moveAllToActiveOrBackoffQ,
|
|
},
|
|
want: `scheduler_queue_incoming_pods_total{event="UnscheduledPodAdd",queue="active"} 3
|
|
scheduler_queue_incoming_pods_total{event="ScheduleAttemptFailure",queue="unschedulable"} 3
|
|
scheduler_queue_incoming_pods_total{event="UnschedulableTimeout",queue="active"} 3
|
|
`,
|
|
},
|
|
{
|
|
name: "make some pods subject to backoff and add them to backoffQ, then flush backoffQ",
|
|
operations: []operation{
|
|
popAndRequeueAsBackoff,
|
|
moveClockForward,
|
|
flushBackoffQ,
|
|
},
|
|
want: `scheduler_queue_incoming_pods_total{event="UnscheduledPodAdd",queue="active"} 3
|
|
scheduler_queue_incoming_pods_total{event="BackoffComplete",queue="active"} 3
|
|
scheduler_queue_incoming_pods_total{event="ScheduleAttemptFailure",queue="backoff"} 3
|
|
`,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
tCtx := ktesting.Init(t)
|
|
metrics.SchedulerQueueIncomingPods.Reset()
|
|
queue := NewTestQueue(tCtx, newDefaultQueueSort(), WithClock(testingclock.NewFakeClock(timestamp)))
|
|
for _, op := range test.operations {
|
|
for _, pInfo := range pInfos {
|
|
op(tCtx, queue, pInfo)
|
|
}
|
|
}
|
|
metricName := metrics.SchedulerSubsystem + "_" + metrics.SchedulerQueueIncomingPods.Name
|
|
if err := testutil.CollectAndCompare(metrics.SchedulerQueueIncomingPods, strings.NewReader(queueMetricMetadata+test.want), metricName); err != nil {
|
|
t.Errorf("unexpected collecting result:\n%s", err)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBackOffFlow(t *testing.T) {
|
|
cl := testingclock.NewFakeClock(time.Now())
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
steps := []struct {
|
|
wantBackoff time.Duration
|
|
}{
|
|
{wantBackoff: time.Second},
|
|
{wantBackoff: 2 * time.Second},
|
|
{wantBackoff: 4 * time.Second},
|
|
{wantBackoff: 8 * time.Second},
|
|
{wantBackoff: 10 * time.Second},
|
|
{wantBackoff: 10 * time.Second},
|
|
{wantBackoff: 10 * time.Second},
|
|
}
|
|
for _, popFromBackoffQEnabled := range []bool{true, false} {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SchedulerPopFromBackoffQ, popFromBackoffQEnabled)
|
|
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(cl))
|
|
|
|
pod := st.MakePod().Name("test-pod").Namespace("test-ns").UID("test-uid").Obj()
|
|
podID := types.NamespacedName{
|
|
Namespace: pod.Namespace,
|
|
Name: pod.Name,
|
|
}
|
|
q.Add(ctx, pod)
|
|
|
|
for i, step := range steps {
|
|
t.Run(fmt.Sprintf("step %d popFromBackoffQEnabled(%v)", i, popFromBackoffQEnabled), func(t *testing.T) {
|
|
timestamp := cl.Now()
|
|
// Simulate schedule attempt.
|
|
entity, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
podInfo := entity.(*framework.QueuedPodInfo)
|
|
if podInfo.GetAttempts() != i+1 {
|
|
t.Errorf("got attempts %d, want %d", podInfo.GetAttempts(), i+1)
|
|
}
|
|
podInfo.GetUnschedulablePlugins().Insert("unsched-plugin")
|
|
err = q.AddUnschedulablePodIfNotPresent(logger, podInfo, int64(i))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
|
|
// An event happens.
|
|
q.MoveAllToActiveOrBackoffQueue(logger, framework.EventUnschedulableTimeout, nil, nil, nil)
|
|
|
|
if !q.backoffQ.has(podInfo) {
|
|
t.Errorf("pod %v is not in the backoff queue", podID)
|
|
}
|
|
|
|
// Check backoff duration.
|
|
deadline := podInfo.BackoffExpiration
|
|
backoff := deadline.Sub(timestamp)
|
|
if popFromBackoffQEnabled {
|
|
// If popFromBackoffQEnabled, the actual backoff can be calculated by rounding up to the ordering window duration.
|
|
backoff = backoff.Truncate(backoffQOrderingWindowDuration) + backoffQOrderingWindowDuration
|
|
}
|
|
if backoff != step.wantBackoff {
|
|
t.Errorf("got backoff %s, want %s", backoff, step.wantBackoff)
|
|
}
|
|
|
|
// Simulate routine that continuously flushes the backoff queue.
|
|
cl.Step(backoffQOrderingWindowDuration)
|
|
q.flushBackoffQCompleted(logger)
|
|
// Still in backoff queue after an early flush.
|
|
if !q.backoffQ.has(podInfo) {
|
|
t.Errorf("pod %v is not in the backoff queue", podID)
|
|
}
|
|
// Moved out of the backoff queue after timeout.
|
|
cl.Step(backoff)
|
|
q.flushBackoffQCompleted(logger)
|
|
if q.backoffQ.has(podInfo) {
|
|
t.Errorf("pod %v is still in the backoff queue", podID)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMoveAllToActiveOrBackoffQueue_PreEnqueueChecks(t *testing.T) {
|
|
var podInfos []*framework.QueuedPodInfo
|
|
for i := 0; i < 5; i++ {
|
|
pInfo := newQueuedPodInfoForLookup(
|
|
st.MakePod().Name(fmt.Sprintf("p%d", i)).Priority(int32(i)).Obj(),
|
|
)
|
|
podInfos = append(podInfos, pInfo)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
preEnqueueCheck PreEnqueueCheck
|
|
podInfos []*framework.QueuedPodInfo
|
|
event fwk.ClusterEvent
|
|
want sets.Set[string]
|
|
}{
|
|
{
|
|
name: "nil PreEnqueueCheck",
|
|
podInfos: podInfos,
|
|
event: framework.EventUnschedulableTimeout,
|
|
want: sets.New("p0", "p1", "p2", "p3", "p4"),
|
|
},
|
|
{
|
|
name: "move Pods with priority greater than 2",
|
|
podInfos: podInfos,
|
|
event: framework.EventUnschedulableTimeout,
|
|
preEnqueueCheck: func(pod *v1.Pod) bool { return *pod.Spec.Priority >= 2 },
|
|
want: sets.New("p2", "p3", "p4"),
|
|
},
|
|
{
|
|
name: "move Pods with even priority and greater than 2",
|
|
podInfos: podInfos,
|
|
event: framework.EventUnschedulableTimeout,
|
|
preEnqueueCheck: func(pod *v1.Pod) bool {
|
|
return *pod.Spec.Priority%2 == 0 && *pod.Spec.Priority >= 2
|
|
},
|
|
want: sets.New("p2", "p4"),
|
|
},
|
|
{
|
|
name: "move Pods with even and negative priority",
|
|
podInfos: podInfos,
|
|
event: framework.EventUnschedulableTimeout,
|
|
preEnqueueCheck: func(pod *v1.Pod) bool {
|
|
return *pod.Spec.Priority%2 == 0 && *pod.Spec.Priority < 0
|
|
},
|
|
},
|
|
{
|
|
name: "preCheck isn't called if the event is not interested by any plugins",
|
|
podInfos: podInfos,
|
|
event: pvAdd, // No plugin is interested in this event.
|
|
preEnqueueCheck: func(pod *v1.Pod) bool {
|
|
panic("preCheck shouldn't be called")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := testingclock.NewFakeClock(time.Now().Truncate(backoffQOrderingWindowDuration))
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithClock(c))
|
|
for _, podInfo := range tt.podInfos {
|
|
// To simulate the pod is failed in scheduling in the real world, Pop() the pod from activeQ before AddUnschedulablePodIfNotPresent() below.
|
|
q.Add(ctx, podInfo.Pod)
|
|
if p, err := q.Pop(logger); err != nil {
|
|
t.Errorf("Pop failed: %v", err)
|
|
} else if diff := cmp.Diff(podInfo.Pod, p.(*framework.QueuedPodInfo).Pod); diff != "" {
|
|
t.Errorf("Unexpected pod after Pop (-want, +got):\n%s", diff)
|
|
}
|
|
podInfo.UnschedulablePlugins = sets.New("plugin")
|
|
err := q.AddUnschedulablePodIfNotPresent(logger, podInfo, q.activeQ.schedulingCycle())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from AddUnschedulablePodIfNotPresent: %v", err)
|
|
}
|
|
}
|
|
q.MoveAllToActiveOrBackoffQueue(logger, tt.event, nil, nil, tt.preEnqueueCheck)
|
|
got := sets.New[string]()
|
|
c.Step(2 * q.backoffQ.podMaxBackoffDuration())
|
|
gotPodInfos := q.backoffQ.popAllBackoffCompleted(logger)
|
|
for _, pInfo := range gotPodInfos {
|
|
got.Insert(pInfo.(*framework.QueuedPodInfo).Pod.Name)
|
|
}
|
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
|
t.Errorf("Unexpected diff (-want, +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func makeQueuedPodInfos(num int, namePrefix, label string, timestamp time.Time) []*framework.QueuedPodInfo {
|
|
var pInfos = make([]*framework.QueuedPodInfo, 0, num)
|
|
for i := 1; i <= num; i++ {
|
|
p := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(
|
|
st.MakePod().Name(fmt.Sprintf("%v-%d", namePrefix, i)).Namespace(fmt.Sprintf("ns%d", i)).Label(label, "").UID(fmt.Sprintf("tp-%d", i)).Obj()),
|
|
QueueingParams: framework.QueueingParams{
|
|
Timestamp: timestamp,
|
|
UnschedulablePlugins: sets.New[string](),
|
|
},
|
|
}
|
|
pInfos = append(pInfos, p)
|
|
}
|
|
return pInfos
|
|
}
|
|
|
|
func mustNewTestPodInfo(t *testing.T, pod *v1.Pod) *framework.PodInfo {
|
|
podInfo, err := framework.NewPodInfo(pod)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return podInfo
|
|
}
|
|
|
|
func mustNewPodInfo(pod *v1.Pod) *framework.PodInfo {
|
|
podInfo, err := framework.NewPodInfo(pod)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return podInfo
|
|
}
|
|
|
|
// Test_isPodWorthRequeuing tests isPodWorthRequeuing function.
|
|
func Test_isPodWorthRequeuing(t *testing.T) {
|
|
metrics.Register()
|
|
count := 0
|
|
queueHintReturnQueue := func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
|
|
count++
|
|
return fwk.Queue, nil
|
|
}
|
|
queueHintReturnSkip := func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
|
|
count++
|
|
return fwk.QueueSkip, nil
|
|
}
|
|
queueHintReturnErr := func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
|
|
count++
|
|
return fwk.QueueSkip, fmt.Errorf("unexpected error")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
podInfo *framework.QueuedPodInfo
|
|
event fwk.ClusterEvent
|
|
oldObj interface{}
|
|
newObj interface{}
|
|
expected queueingStrategy
|
|
expectedExecutionCount int // expected total execution count of queueing hint function
|
|
queueingHintMap QueueingHintMapPerProfile
|
|
}{
|
|
{
|
|
name: "return Queue when no queueing hint function is registered for the event",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: nodeAdd,
|
|
oldObj: nil,
|
|
newObj: st.MakeNode().Obj(),
|
|
expected: queueSkip,
|
|
expectedExecutionCount: 0,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
// no queueing hint function for NodeAdd.
|
|
framework.EventAssignedPodAdd: {
|
|
{
|
|
// It will be ignored because the event is not NodeAdd.
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Treat the event as Queue when QueueHintFn returns error",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: nodeAdd,
|
|
oldObj: nil,
|
|
newObj: st.MakeNode().Obj(),
|
|
expected: queueAfterBackoff,
|
|
expectedExecutionCount: 1,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
nodeAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnErr,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "return Queue when the event is wildcard",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: framework.EventUnschedulableTimeout,
|
|
oldObj: nil,
|
|
newObj: nil,
|
|
expected: queueAfterBackoff,
|
|
expectedExecutionCount: 0,
|
|
queueingHintMap: QueueingHintMapPerProfile{},
|
|
},
|
|
{
|
|
name: "return Queue when the event is wildcard and the wildcard targets the pod to be requeued right now",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: framework.EventForceActivate,
|
|
oldObj: nil,
|
|
newObj: st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj(),
|
|
expected: queueAfterBackoff,
|
|
expectedExecutionCount: 0,
|
|
queueingHintMap: QueueingHintMapPerProfile{},
|
|
},
|
|
{
|
|
name: "return Skip when the event is wildcard, but the wildcard targets a different pod",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: framework.EventForceActivate,
|
|
oldObj: nil,
|
|
newObj: st.MakePod().Name("pod-different").Namespace("ns2").UID("2").Obj(),
|
|
expected: queueSkip,
|
|
expectedExecutionCount: 0,
|
|
queueingHintMap: QueueingHintMapPerProfile{},
|
|
},
|
|
{
|
|
name: "interprets Queue from the Pending plugin as queueImmediately",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1", "fooPlugin3"),
|
|
PendingPlugins: sets.New("fooPlugin2"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: nodeAdd,
|
|
oldObj: nil,
|
|
newObj: st.MakeNode().Node,
|
|
expected: queueImmediately,
|
|
expectedExecutionCount: 2,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
nodeAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
// It returns Queue and it's interpreted as queueAfterBackoff.
|
|
// But, the function continues to run other hints because the Pod has PendingPlugins, which can result in queueImmediately.
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
PluginName: "fooPlugin2",
|
|
// It's interpreted as queueImmediately.
|
|
// The function doesn't run other hints because queueImmediately is the highest priority.
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
PluginName: "fooPlugin3",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
PluginName: "fooPlugin4",
|
|
QueueingHintFn: queueHintReturnErr,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "interprets Queue from the Unschedulable plugin as queueAfterBackoff",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1", "fooPlugin2"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: nodeAdd,
|
|
oldObj: nil,
|
|
newObj: st.MakeNode().Obj(),
|
|
expected: queueAfterBackoff,
|
|
expectedExecutionCount: 2,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
nodeAdd: {
|
|
{
|
|
// Skip will be ignored
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnSkip,
|
|
},
|
|
{
|
|
// Skip will be ignored
|
|
PluginName: "fooPlugin2",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Queueing hint function that isn't from the plugin in UnschedulablePlugins/PendingPlugins is ignored",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1", "fooPlugin2"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: nodeAdd,
|
|
oldObj: nil,
|
|
newObj: st.MakeNode().Node,
|
|
expected: queueSkip,
|
|
expectedExecutionCount: 2,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
nodeAdd: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnSkip,
|
|
},
|
|
{
|
|
PluginName: "fooPlugin2",
|
|
QueueingHintFn: queueHintReturnSkip,
|
|
},
|
|
{
|
|
PluginName: "fooPlugin3",
|
|
QueueingHintFn: queueHintReturnQueue, // It'll be ignored.
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "If event is specific Node update event, queueing hint function for NodeUpdate/UpdateNodeLabel is also executed",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1", "fooPlugin2"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.UpdateNodeLabel},
|
|
oldObj: nil,
|
|
newObj: st.MakeNode().Obj(),
|
|
expected: queueAfterBackoff,
|
|
expectedExecutionCount: 1,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.UpdateNodeLabel}: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
// It's only executed and interpreted as queueAfterBackoff.
|
|
// The function doesn't run other hints because this Pod doesn't have PendingPlugins.
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
PluginName: "fooPlugin2",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Update}: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
nodeAdd: { // not executed because NodeAdd is unrelated.
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "If event with '*' Resource, queueing hint function for specified Resource is also executed",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Add},
|
|
oldObj: nil,
|
|
newObj: st.MakeNode().Obj(),
|
|
expected: queueAfterBackoff,
|
|
expectedExecutionCount: 1,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
fwk.ClusterEvent{Resource: fwk.WildCard, ActionType: fwk.Add}: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "If event is a wildcard one, queueing hint function for all kinds of events is executed",
|
|
podInfo: &framework.QueuedPodInfo{
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin1"),
|
|
},
|
|
PodInfo: mustNewPodInfo(st.MakePod().Name("pod1").Namespace("ns1").UID("1").Obj()),
|
|
},
|
|
event: fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.UpdateNodeLabel | fwk.UpdateNodeTaint},
|
|
oldObj: nil,
|
|
newObj: st.MakeNode().Obj(),
|
|
expected: queueAfterBackoff,
|
|
expectedExecutionCount: 1,
|
|
queueingHintMap: QueueingHintMapPerProfile{
|
|
"": {
|
|
fwk.ClusterEvent{Resource: fwk.WildCard, ActionType: fwk.All}: {
|
|
{
|
|
PluginName: "fooPlugin1",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
count = 0 // reset count every time
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithQueueingHintMapPerProfile(test.queueingHintMap))
|
|
actual := q.isPodWorthRequeuing(logger, test.podInfo, test.event, test.oldObj, test.newObj)
|
|
if actual != test.expected {
|
|
t.Errorf("isPodWorthRequeuing() = %v, want %v", actual, test.expected)
|
|
}
|
|
if count != test.expectedExecutionCount {
|
|
t.Errorf("isPodWorthRequeuing() executed queueing hint functions %v times, expected: %v", count, test.expectedExecutionCount)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_queuedPodInfo_gatedSetUponCreationAndUnsetUponUpdate(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
plugin, _ := schedulinggates.New(ctx, nil, nil, plfeature.Features{})
|
|
m := map[string]map[string]fwk.PreEnqueuePlugin{"": {names.SchedulingGates: plugin.(fwk.PreEnqueuePlugin)}}
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithPreEnqueuePluginMap(m))
|
|
|
|
gatedPod := st.MakePod().SchedulingGates([]string{"hello world"}).Obj()
|
|
q.Add(ctx, gatedPod)
|
|
|
|
if !q.unschedulableEntities.get(newQueuedPodInfoForLookup(gatedPod)).Gated() {
|
|
t.Error("Expected pod to be gated")
|
|
}
|
|
|
|
ungatedPod := gatedPod.DeepCopy()
|
|
ungatedPod.Spec.SchedulingGates = nil
|
|
q.Update(ctx, gatedPod, ungatedPod)
|
|
|
|
ungatedPodInfo, _ := q.Pop(logger)
|
|
if ungatedPodInfo.Gated() {
|
|
t.Error("Expected pod to be ungated")
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_GetPod(t *testing.T) {
|
|
activeQPod := &v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pod1",
|
|
Namespace: "default",
|
|
},
|
|
}
|
|
backoffQPod := &v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pod2",
|
|
Namespace: "default",
|
|
},
|
|
}
|
|
unschedPod := &v1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pod3",
|
|
Namespace: "default",
|
|
},
|
|
}
|
|
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
q := NewTestQueue(ctx, newDefaultQueueSort())
|
|
q.activeQ.add(logger, newQueuedPodInfoForLookup(activeQPod), framework.EventUnscheduledPodAdd.Label())
|
|
q.backoffQ.add(logger, newQueuedPodInfoForLookup(backoffQPod), framework.EventUnscheduledPodAdd.Label())
|
|
q.unschedulableEntities.addOrUpdate(newQueuedPodInfoForLookup(unschedPod), false, framework.EventUnscheduledPodAdd.Label())
|
|
|
|
tests := []struct {
|
|
name string
|
|
podName string
|
|
namespace string
|
|
expectedPod *v1.Pod
|
|
expectedOK bool
|
|
}{
|
|
{
|
|
name: "pod is found in activeQ",
|
|
podName: "pod1",
|
|
namespace: "default",
|
|
expectedPod: activeQPod,
|
|
expectedOK: true,
|
|
},
|
|
{
|
|
name: "pod is found in backoffQ",
|
|
podName: "pod2",
|
|
namespace: "default",
|
|
expectedPod: backoffQPod,
|
|
expectedOK: true,
|
|
},
|
|
{
|
|
name: "pod is found in unschedulableEntities",
|
|
podName: "pod3",
|
|
namespace: "default",
|
|
expectedPod: unschedPod,
|
|
expectedOK: true,
|
|
},
|
|
{
|
|
name: "pod is not found",
|
|
podName: "pod4",
|
|
namespace: "default",
|
|
expectedPod: nil,
|
|
expectedOK: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pInfo, ok := q.GetPod(tt.podName, tt.namespace, nil)
|
|
if ok != tt.expectedOK {
|
|
t.Errorf("Expected ok=%v, but got ok=%v", tt.expectedOK, ok)
|
|
}
|
|
|
|
if tt.expectedPod == nil {
|
|
if pInfo == nil {
|
|
return
|
|
}
|
|
t.Fatalf("Expected pod is empty, but got pod=%v", pInfo.Pod)
|
|
}
|
|
|
|
if !cmp.Equal(pInfo.Pod, tt.expectedPod) {
|
|
t.Errorf("Expected pod=%v, but got pod=%v", tt.expectedPod, pInfo.Pod)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnschedulablePodsMetric(t *testing.T) {
|
|
type step func(tCtx ktesting.TContext, q *PriorityQueue)
|
|
|
|
addPod := func(pInfo *framework.QueuedPodInfo) step {
|
|
return func(tCtx ktesting.TContext, q *PriorityQueue) {
|
|
add(tCtx, q, pInfo)
|
|
}
|
|
}
|
|
deletePod := func(pInfo *framework.QueuedPodInfo) step {
|
|
return func(tCtx ktesting.TContext, q *PriorityQueue) {
|
|
deletePod(tCtx, q, pInfo)
|
|
}
|
|
}
|
|
popPod := func() step {
|
|
return func(tCtx ktesting.TContext, q *PriorityQueue) {
|
|
pop(tCtx, q, nil)
|
|
}
|
|
}
|
|
moveAllToActiveOrBackoffQ := func() step {
|
|
return func(tCtx ktesting.TContext, q *PriorityQueue) {
|
|
moveAllToActiveOrBackoffQ(tCtx, q, nil)
|
|
}
|
|
}
|
|
updatePluginAllowList := func(pluginName string, list []string) step {
|
|
return func(tCtx ktesting.TContext, q *PriorityQueue) {
|
|
q.preEnqueuePluginMap[""][pluginName].(*preEnqueuePlugin).allowlists = list
|
|
}
|
|
}
|
|
|
|
pluginName1 := "plugin1"
|
|
pluginName2 := "plugin2"
|
|
queueable := "queueable"
|
|
timestamp := time.Now()
|
|
pod := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(
|
|
st.MakePod().Name("podA").Namespace("namespaceA").Label(queueable, "").UID("someUid").Obj()),
|
|
QueueingParams: framework.QueueingParams{
|
|
Timestamp: timestamp,
|
|
UnschedulablePlugins: sets.New[string](),
|
|
},
|
|
}
|
|
|
|
resetMetrics := func() {
|
|
metrics.UnschedulableReason(pluginName1, "").Set(0)
|
|
metrics.UnschedulableReason(pluginName2, "").Set(0)
|
|
}
|
|
|
|
makeGated := func(pInfo *framework.QueuedPodInfo) *framework.QueuedPodInfo {
|
|
return setQueuedPodInfoGated(pInfo.DeepCopy(), pluginName1, []fwk.ClusterEvent{framework.EventUnschedulableTimeout})
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
steps []step
|
|
expectedMetrics []int
|
|
}{
|
|
{
|
|
name: "Unschedulable pods metric must be 0 after a pod is gated, ungated, re-queued, and eventually popped from the scheduling queue",
|
|
steps: []step{
|
|
updatePluginAllowList(pluginName1, []string{}),
|
|
addPod(pod),
|
|
moveAllToActiveOrBackoffQ(),
|
|
updatePluginAllowList(pluginName1, []string{queueable}),
|
|
moveAllToActiveOrBackoffQ(),
|
|
popPod(),
|
|
},
|
|
expectedMetrics: []int{0, 0},
|
|
},
|
|
{
|
|
name: "Unschedulable pods metric must be 0 after pod is gated and then deleted",
|
|
steps: []step{
|
|
updatePluginAllowList(pluginName1, []string{}),
|
|
addPod(pod),
|
|
moveAllToActiveOrBackoffQ(),
|
|
deletePod(pod),
|
|
},
|
|
expectedMetrics: []int{0, 0},
|
|
},
|
|
{
|
|
name: "Unschedulable pods metric must be 1 after pod is gated multiple time by the same plugin",
|
|
steps: []step{
|
|
updatePluginAllowList(pluginName1, []string{}),
|
|
addPod(pod),
|
|
moveAllToActiveOrBackoffQ(),
|
|
},
|
|
expectedMetrics: []int{1, 0},
|
|
},
|
|
{
|
|
name: "Unschedulable pods metric must be 0 after non gated pods is added and then deleted",
|
|
steps: []step{
|
|
addPod(pod),
|
|
deletePod(pod),
|
|
},
|
|
expectedMetrics: []int{0, 0},
|
|
},
|
|
{
|
|
name: "Unschedulable pods metric should not be duplicate if gated pods added and then gated with the same plugin again",
|
|
steps: []step{
|
|
updatePluginAllowList(pluginName1, []string{}),
|
|
addPod(makeGated(pod)),
|
|
moveAllToActiveOrBackoffQ(),
|
|
},
|
|
expectedMetrics: []int{1, 0},
|
|
},
|
|
{
|
|
name: "Unschedulable pods metric should be 0 if pod was gated by two plugins sequentially and then ungated and popped",
|
|
steps: []step{
|
|
updatePluginAllowList(pluginName1, []string{}),
|
|
addPod(pod),
|
|
updatePluginAllowList(pluginName1, []string{queueable}),
|
|
updatePluginAllowList(pluginName2, []string{}),
|
|
moveAllToActiveOrBackoffQ(),
|
|
updatePluginAllowList(pluginName2, []string{queueable}),
|
|
moveAllToActiveOrBackoffQ(),
|
|
popPod(),
|
|
},
|
|
expectedMetrics: []int{0, 0},
|
|
},
|
|
{
|
|
name: "Unschedulable pods metric should be 0 if pod was gated by two plugins sequentially and then deleted",
|
|
steps: []step{
|
|
updatePluginAllowList(pluginName1, []string{}),
|
|
addPod(pod),
|
|
updatePluginAllowList(pluginName1, []string{queueable}),
|
|
updatePluginAllowList(pluginName2, []string{}),
|
|
moveAllToActiveOrBackoffQ(),
|
|
deletePod(pod),
|
|
},
|
|
expectedMetrics: []int{0, 0},
|
|
},
|
|
{
|
|
name: "Unschedulable pods metric should be 1 for both plugins if pod was gated by two plugins sequentially",
|
|
steps: []step{
|
|
updatePluginAllowList(pluginName1, []string{}),
|
|
addPod(pod),
|
|
updatePluginAllowList(pluginName1, []string{queueable}),
|
|
updatePluginAllowList(pluginName2, []string{}),
|
|
moveAllToActiveOrBackoffQ(),
|
|
},
|
|
expectedMetrics: []int{1, 1},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tCtx := ktesting.Init(t)
|
|
resetMetrics()
|
|
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
m[""][framework.EventUnschedulableTimeout] = []*QueueingHintFunction{
|
|
{
|
|
PluginName: pluginName1,
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
{
|
|
PluginName: pluginName2,
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
}
|
|
|
|
plugin1 := preEnqueuePlugin{name: pluginName1, allowlists: []string{queueable}}
|
|
plugin2 := preEnqueuePlugin{name: pluginName2, allowlists: []string{queueable}}
|
|
|
|
preenq := map[string]map[string]fwk.PreEnqueuePlugin{"": {pluginName1: &plugin1, pluginName2: &plugin2}}
|
|
recorder := metrics.NewMetricsAsyncRecorder(3, 20*time.Microsecond, tCtx.Done())
|
|
q := NewTestQueue(tCtx, newDefaultQueueSort(), WithClock(testingclock.NewFakeClock(timestamp)), WithPreEnqueuePluginMap(preenq), WithMetricsRecorder(recorder), WithQueueingHintMapPerProfile(m))
|
|
|
|
for _, step := range tt.steps {
|
|
step(tCtx, q)
|
|
}
|
|
|
|
for i, pluginName := range []string{pluginName1, pluginName2} {
|
|
val, err := testutil.GetGaugeMetricValue(metrics.UnschedulableReason(pluginName, ""))
|
|
|
|
if err != nil {
|
|
t.Errorf("Error while collection metric value:\n%s", err)
|
|
}
|
|
if int(val) != tt.expectedMetrics[i] {
|
|
t.Errorf("Unexpected metric for plugin %s result expected %d, actual %d", pluginName, tt.expectedMetrics[i], int(val))
|
|
}
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_signPod(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
enableFeatureGate bool
|
|
signers map[string]PodSigner
|
|
pod *v1.Pod
|
|
expectedSignature fwk.PodSignature
|
|
}{
|
|
{
|
|
name: "Feature gate disabled",
|
|
enableFeatureGate: false,
|
|
signers: map[string]PodSigner{
|
|
"default-scheduler": func(ctx context.Context, pod *v1.Pod) fwk.PodSignature {
|
|
return fwk.PodSignature("sig-1")
|
|
},
|
|
},
|
|
pod: st.MakePod().Name("pod1").SchedulerName("default-scheduler").Obj(),
|
|
},
|
|
{
|
|
name: "No signers configured",
|
|
enableFeatureGate: true,
|
|
signers: nil,
|
|
pod: st.MakePod().Name("pod1").SchedulerName("default-scheduler").Obj(),
|
|
},
|
|
{
|
|
name: "Signer not found for scheduler",
|
|
enableFeatureGate: true,
|
|
signers: map[string]PodSigner{
|
|
"default-scheduler": func(ctx context.Context, pod *v1.Pod) fwk.PodSignature {
|
|
return fwk.PodSignature("sig-1")
|
|
},
|
|
},
|
|
pod: st.MakePod().Name("pod1").SchedulerName("custom-scheduler").Obj(),
|
|
},
|
|
{
|
|
name: "Successful signature computation",
|
|
enableFeatureGate: true,
|
|
signers: map[string]PodSigner{
|
|
"default-scheduler": func(ctx context.Context, pod *v1.Pod) fwk.PodSignature {
|
|
return fwk.PodSignature("sig-1")
|
|
},
|
|
},
|
|
pod: st.MakePod().Name("pod1").SchedulerName("default-scheduler").Obj(),
|
|
expectedSignature: fwk.PodSignature("sig-1"),
|
|
},
|
|
{
|
|
name: "Signer returns nil (unsignable pod)",
|
|
enableFeatureGate: true,
|
|
signers: map[string]PodSigner{
|
|
"default-scheduler": func(ctx context.Context, pod *v1.Pod) fwk.PodSignature {
|
|
return nil
|
|
},
|
|
},
|
|
pod: st.MakePod().Name("pod1").SchedulerName("default-scheduler").Obj(),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.OpportunisticBatching, tt.enableFeatureGate)
|
|
|
|
tCtx := ktesting.Init(t)
|
|
q := NewTestQueue(tCtx, newDefaultQueueSort(), WithPodSigners(tt.signers))
|
|
|
|
signature := q.signPod(tCtx, tt.pod)
|
|
|
|
if !bytes.Equal(signature, tt.expectedSignature) {
|
|
t.Errorf("Expected signature '%s', got '%s'", string(tt.expectedSignature), string(signature))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_AddComputesSignature(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.OpportunisticBatching, true)
|
|
|
|
signers := map[string]PodSigner{
|
|
"default-scheduler": func(ctx context.Context, pod *v1.Pod) fwk.PodSignature {
|
|
if val, ok := pod.Labels["key"]; ok {
|
|
return fwk.PodSignature(fmt.Sprintf("sig-%s", val))
|
|
}
|
|
return fwk.PodSignature("sig-default")
|
|
},
|
|
}
|
|
|
|
tCtx := ktesting.Init(t)
|
|
q := NewTestQueue(tCtx, newDefaultQueueSort(), WithPodSigners(signers))
|
|
|
|
pod := st.MakePod().Name("pod1").SchedulerName("default-scheduler").Label("key", "value1").Obj()
|
|
q.Add(tCtx, pod)
|
|
|
|
pInfo, exists := q.GetPod(pod.Name, pod.Namespace, nil)
|
|
if !exists {
|
|
t.Fatal("Pod not found in queue after Add")
|
|
}
|
|
if !bytes.Equal(pInfo.PodSignature, fwk.PodSignature("sig-value1")) {
|
|
t.Errorf("Expected signature 'sig-value1', got '%s'", string(pInfo.PodSignature))
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_UpdateRecomputesSignature(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.OpportunisticBatching, true)
|
|
|
|
signers := map[string]PodSigner{
|
|
"default-scheduler": func(ctx context.Context, pod *v1.Pod) fwk.PodSignature {
|
|
if val, ok := pod.Labels["key"]; ok {
|
|
return fwk.PodSignature(fmt.Sprintf("sig-%s", val))
|
|
}
|
|
return fwk.PodSignature("sig-default")
|
|
},
|
|
}
|
|
|
|
pod1 := st.MakePod().Name("pod1").SchedulerName("default-scheduler").Label("key", "value1").Obj()
|
|
pod2 := pod1.DeepCopy()
|
|
pod2.Labels["key"] = "value2"
|
|
|
|
tests := []struct {
|
|
name string
|
|
prepareFunc func(tCtx ktesting.TContext, q *PriorityQueue)
|
|
}{
|
|
{
|
|
name: "pod in activeQ",
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) {
|
|
q.Add(tCtx, pod1)
|
|
},
|
|
},
|
|
{
|
|
name: "pod in backoffQ",
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) {
|
|
pInfo := q.newQueuedPodInfo(tCtx, pod1)
|
|
q.backoffQ.add(klog.FromContext(tCtx), pInfo, framework.EventUnscheduledPodAdd.Label())
|
|
},
|
|
},
|
|
{
|
|
name: "pod in unschedulableEntities",
|
|
prepareFunc: func(tCtx ktesting.TContext, q *PriorityQueue) {
|
|
pInfo := q.newQueuedPodInfo(tCtx, pod1)
|
|
q.unschedulableEntities.addOrUpdate(pInfo, false, framework.EventUnscheduledPodAdd.Label())
|
|
},
|
|
},
|
|
{
|
|
name: "pod not in any queue",
|
|
prepareFunc: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tCtx := ktesting.Init(t)
|
|
q := NewTestQueue(tCtx, newDefaultQueueSort(), WithPodSigners(signers))
|
|
|
|
if tt.prepareFunc != nil {
|
|
tt.prepareFunc(tCtx, q)
|
|
}
|
|
|
|
// Update pod with different label
|
|
q.Update(tCtx, pod1, pod2)
|
|
|
|
// Check signature was recomputed
|
|
pInfo, exists := q.GetPod(pod2.Name, pod2.Namespace, nil)
|
|
if !exists {
|
|
t.Fatal("Pod not found in queue after update")
|
|
}
|
|
if !bytes.Equal(pInfo.PodSignature, fwk.PodSignature("sig-value2")) {
|
|
t.Errorf("Expected signature 'sig-value2', got '%s'", string(pInfo.PodSignature))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPriorityQueue_MultipleProfiles(t *testing.T) {
|
|
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.OpportunisticBatching, true)
|
|
|
|
_, ctx := ktesting.NewTestContext(t)
|
|
|
|
signers := map[string]PodSigner{
|
|
"scheduler-1": func(ctx context.Context, pod *v1.Pod) fwk.PodSignature {
|
|
return fwk.PodSignature("sig-scheduler-1")
|
|
},
|
|
"scheduler-2": func(ctx context.Context, pod *v1.Pod) fwk.PodSignature {
|
|
return fwk.PodSignature("sig-scheduler-2")
|
|
},
|
|
}
|
|
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithPodSigners(signers))
|
|
|
|
pod1 := st.MakePod().Name("pod1").SchedulerName("scheduler-1").Obj()
|
|
pod2 := st.MakePod().Name("pod2").SchedulerName("scheduler-2").Obj()
|
|
pod3 := st.MakePod().Name("pod3").SchedulerName("scheduler-3").Obj() // No signer
|
|
|
|
q.Add(ctx, pod1)
|
|
q.Add(ctx, pod2)
|
|
q.Add(ctx, pod3)
|
|
|
|
pInfo1, _ := q.GetPod(pod1.Name, pod1.Namespace, nil)
|
|
pInfo2, _ := q.GetPod(pod2.Name, pod2.Namespace, nil)
|
|
pInfo3, _ := q.GetPod(pod3.Name, pod3.Namespace, nil)
|
|
|
|
if !bytes.Equal(pInfo1.PodSignature, fwk.PodSignature("sig-scheduler-1")) {
|
|
t.Errorf("Pod1: expected 'sig-scheduler-1', got '%s'", string(pInfo1.PodSignature))
|
|
}
|
|
if !bytes.Equal(pInfo2.PodSignature, fwk.PodSignature("sig-scheduler-2")) {
|
|
t.Errorf("Pod2: expected 'sig-scheduler-2', got '%s'", string(pInfo2.PodSignature))
|
|
}
|
|
if pInfo3.PodSignature != nil {
|
|
t.Errorf("Pod3: expected nil signature (no signer), got '%s'", string(pInfo3.PodSignature))
|
|
}
|
|
|
|
}
|
|
|
|
func TestConcurrentUpdateAndPop(t *testing.T) {
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
|
|
q := NewTestQueue(ctx, newDefaultQueueSort())
|
|
|
|
podName := "test-pod"
|
|
// Create a pod with high priority to ensure it's at the front
|
|
pod := st.MakePod().Name(podName).Namespace("default").UID("uid-1").Priority(100).Obj()
|
|
q.Add(ctx, pod)
|
|
|
|
var wg sync.WaitGroup
|
|
start := time.Now()
|
|
testDuration := 3 * time.Second
|
|
|
|
// Goroutine 1: Continuously Pop and re-Add
|
|
wg.Go(func() {
|
|
for time.Since(start) < testDuration {
|
|
// Pop blocks if empty, but we verify we don't block forever or panic
|
|
entity, err := q.Pop(logger)
|
|
if err != nil {
|
|
t.Errorf("Unexpected error during Pop: %v", err)
|
|
return
|
|
}
|
|
if entity == nil {
|
|
t.Errorf("Unexpected nil QueuedEntityInfo during Pop")
|
|
return
|
|
}
|
|
pInfo := entity.(*framework.QueuedPodInfo)
|
|
if pInfo.Pod.UID != pod.UID {
|
|
t.Errorf("Expected pod UID %v, got %v", pod.UID, pInfo.Pod.UID)
|
|
}
|
|
// Simulate some work to widen the race window
|
|
time.Sleep(100 * time.Microsecond)
|
|
q.Done(pInfo.Pod.UID)
|
|
// Re-add to queue to keep the cycle going
|
|
q.Add(ctx, pInfo.Pod)
|
|
}
|
|
})
|
|
|
|
// Goroutine 2: Continuously Update the pod
|
|
wg.Go(func() {
|
|
iter := 0
|
|
currentPod := pod
|
|
for time.Since(start) < testDuration {
|
|
iter++
|
|
newPod := currentPod.DeepCopy()
|
|
newPod.Annotations = map[string]string{"ver": fmt.Sprintf("%d", iter)}
|
|
// Update is atomic
|
|
q.Update(ctx, currentPod, newPod)
|
|
currentPod = newPod
|
|
time.Sleep(50 * time.Microsecond)
|
|
}
|
|
})
|
|
|
|
wg.Wait()
|
|
q.Close()
|
|
}
|
|
|
|
type initialQueueState int
|
|
|
|
const (
|
|
stateActive initialQueueState = iota
|
|
statePopped
|
|
stateBackoff
|
|
stateUnschedulable
|
|
stateGated
|
|
)
|
|
|
|
func setupInitialPodGroupState(t *testing.T, ctx context.Context, q *PriorityQueue, initialPods []*v1.Pod, initialState initialQueueState) {
|
|
t.Helper()
|
|
if len(initialPods) == 0 {
|
|
return
|
|
}
|
|
logger := klog.FromContext(ctx)
|
|
|
|
for _, pod := range initialPods {
|
|
q.Add(ctx, pod)
|
|
}
|
|
|
|
pgLookup := newQueuedPodGroupInfoForLookup(initialPods[0])
|
|
switch initialState {
|
|
case statePopped:
|
|
if _, err := q.Pop(logger); err != nil {
|
|
t.Fatalf("Unexpected error from Pop: %v", err)
|
|
}
|
|
case stateBackoff:
|
|
entity := q.activeQ.delete(pgLookup)
|
|
if entity != nil {
|
|
if pgInfo, ok := entity.(*framework.QueuedPodGroupInfo); ok {
|
|
pgInfo.UnschedulableCount = 1
|
|
pgInfo.Timestamp = q.clock.Now()
|
|
pgInfo.UnschedulablePlugins = sets.New("fooPlugin")
|
|
} else if pInfo, ok := entity.(*framework.QueuedPodInfo); ok {
|
|
pInfo.UnschedulableCount = 1
|
|
pInfo.Timestamp = q.clock.Now()
|
|
pInfo.UnschedulablePlugins = sets.New("fooPlugin")
|
|
}
|
|
q.backoffQ.add(logger, entity, framework.EventUnscheduledPodAdd.Label())
|
|
}
|
|
case stateUnschedulable:
|
|
entity := q.activeQ.delete(pgLookup)
|
|
if entity != nil {
|
|
q.unschedulableEntities.addOrUpdate(entity, false, framework.EventUnscheduledPodAdd.Label())
|
|
}
|
|
case stateGated:
|
|
entity := q.activeQ.delete(pgLookup)
|
|
if entity != nil {
|
|
entity.SetGatingPlugin("preEnqueuePlugin", []fwk.ClusterEvent{pvAdd})
|
|
q.unschedulableEntities.addOrUpdate(entity, false, framework.EventUnscheduledPodAdd.Label())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddPodGroupMember(t *testing.T) {
|
|
pgName := "pg-test"
|
|
p1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Label("allow", "").PodGroupName(pgName).Obj()
|
|
p2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Label("allow", "").PodGroupName(pgName).Obj()
|
|
gatedP1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").PodGroupName(pgName).Obj()
|
|
gatedP2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").PodGroupName(pgName).Obj()
|
|
|
|
tests := []struct {
|
|
name string
|
|
initialPod *v1.Pod
|
|
initialState initialQueueState
|
|
incomingPod *v1.Pod
|
|
expectedInActiveQ bool
|
|
expectedInUnschedulable bool
|
|
expectedInBackoffQ bool
|
|
expectedInPendingPodGroupPods bool
|
|
expectedGated bool
|
|
expectedGroupSize int
|
|
}{
|
|
{
|
|
name: "first member added (ungated), creates new pod group in activeQ",
|
|
incomingPod: p1,
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 1,
|
|
},
|
|
{
|
|
name: "first member added (gated), creates new pod group directly in unschedulableEntities",
|
|
incomingPod: gatedP1,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 1,
|
|
},
|
|
{
|
|
name: "matching pod group is currently in flight (last popped), pod (ungated) goes to pendingPodGroupPods",
|
|
initialPod: p1,
|
|
initialState: statePopped,
|
|
incomingPod: p2,
|
|
expectedInPendingPodGroupPods: true,
|
|
},
|
|
{
|
|
name: "matching pod group is currently in flight (last popped), pod (gated) goes to pendingPodGroupPods",
|
|
initialPod: p1,
|
|
initialState: statePopped,
|
|
incomingPod: gatedP2,
|
|
expectedInPendingPodGroupPods: true,
|
|
},
|
|
{
|
|
name: "existing pod group in activeQ (ungated addition), stays in activeQ",
|
|
initialPod: p1,
|
|
initialState: stateActive,
|
|
incomingPod: p2,
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "existing pod group in activeQ (gated addition), becomes gated and moves to unschedulableEntities",
|
|
initialPod: p1,
|
|
initialState: stateActive,
|
|
incomingPod: gatedP2,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "existing pod group in backoffQ (ungated addition), stays in backoffQ",
|
|
initialPod: p1,
|
|
initialState: stateBackoff,
|
|
incomingPod: p2,
|
|
expectedInBackoffQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "existing pod group in backoffQ (gated addition), becomes gated and moves to unschedulableEntities",
|
|
initialPod: p1,
|
|
initialState: stateBackoff,
|
|
incomingPod: gatedP2,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "existing pod group in unschedulableEntities (ungated addition), stays in unschedulableEntities as ungated",
|
|
initialPod: p1,
|
|
initialState: stateUnschedulable,
|
|
incomingPod: p2,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: false,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "existing pod group in unschedulableEntities (gated addition), stays in unschedulableEntities as ungated",
|
|
initialPod: p1,
|
|
initialState: stateUnschedulable,
|
|
incomingPod: gatedP2,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: false,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "existing gated pod group in unschedulableEntities (ungated addition), stays in unschedulableEntities as gated",
|
|
initialPod: gatedP1,
|
|
initialState: stateGated,
|
|
incomingPod: p2,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
|
features.GenericWorkload: true,
|
|
})
|
|
|
|
_, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
// Configure a PreEnqueue plugin that allows pods with label "allow", but gates others.
|
|
preEnqueueMap := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
"preEnqueuePlugin": &preEnqueuePlugin{allowlists: []string{"allow"}},
|
|
},
|
|
}
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithPreEnqueuePluginMap(preEnqueueMap))
|
|
|
|
var initialPods []*v1.Pod
|
|
if tt.initialPod != nil {
|
|
initialPods = []*v1.Pod{tt.initialPod}
|
|
}
|
|
setupInitialPodGroupState(t, ctx, q, initialPods, tt.initialState)
|
|
|
|
// Add incoming pod
|
|
q.Add(ctx, tt.incomingPod)
|
|
|
|
// Verify conditions
|
|
pgLookup := newQueuedPodGroupInfoForLookup(tt.incomingPod)
|
|
|
|
if inActive := q.activeQ.has(pgLookup); inActive != tt.expectedInActiveQ {
|
|
t.Errorf("Expected incoming pod in activeQ: %v, got %v", tt.expectedInActiveQ, inActive)
|
|
}
|
|
var entity framework.QueuedEntityInfo
|
|
if tt.expectedInActiveQ {
|
|
entity, _ = q.activeQ.get(pgLookup)
|
|
}
|
|
|
|
if inBackoff := q.backoffQ.has(pgLookup); inBackoff != tt.expectedInBackoffQ {
|
|
t.Errorf("Expected incoming pod in backoffQ: %v, got %v", tt.expectedInBackoffQ, inBackoff)
|
|
}
|
|
if tt.expectedInBackoffQ {
|
|
entity, _ = q.backoffQ.get(pgLookup)
|
|
}
|
|
|
|
unschedulableEntity := q.unschedulableEntities.get(pgLookup)
|
|
inUnschedulable := unschedulableEntity != nil
|
|
if inUnschedulable != tt.expectedInUnschedulable {
|
|
t.Errorf("Expected incoming pod in unschedulableEntities: %v, got %v", tt.expectedInUnschedulable, inUnschedulable)
|
|
}
|
|
if tt.expectedInUnschedulable {
|
|
entity = unschedulableEntity
|
|
}
|
|
|
|
if entity != nil {
|
|
if isGated := entity.Gated(); isGated != tt.expectedGated {
|
|
t.Errorf("Expected pod group to be gated: %v, got %v", tt.expectedGated, isGated)
|
|
}
|
|
if size := entity.Size(); size != tt.expectedGroupSize {
|
|
t.Errorf("Expected pod group to be of size: %d, got %d", tt.expectedGroupSize, size)
|
|
}
|
|
|
|
// Verify effective addition of the incoming pod
|
|
foundMember := false
|
|
for _, pInfo := range entity.(*framework.QueuedPodGroupInfo).QueuedPodInfos {
|
|
if pInfo.Pod.Name == tt.incomingPod.Name {
|
|
foundMember = true
|
|
break
|
|
}
|
|
}
|
|
if !foundMember {
|
|
t.Errorf("Incoming pod %s was not found in the pod group members", tt.incomingPod.Name)
|
|
}
|
|
}
|
|
|
|
inPending := false
|
|
for _, pInfo := range q.pendingPodGroupPods.get(pgLookup) {
|
|
if pInfo.Pod.Name == tt.incomingPod.Name {
|
|
inPending = true
|
|
break
|
|
}
|
|
}
|
|
if inPending != tt.expectedInPendingPodGroupPods {
|
|
t.Errorf("Expected incoming pod in pendingPodGroupPods: %v, got %v", tt.expectedInPendingPodGroupPods, inPending)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeletePodGroupMember(t *testing.T) {
|
|
pgName := "pg-test"
|
|
p1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Label("allow", "").PodGroupName(pgName).Obj()
|
|
p2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Label("allow", "").PodGroupName(pgName).Obj()
|
|
p3 := st.MakePod().Name("pod3").Namespace("ns1").UID("pod3").Label("allow", "").PodGroupName(pgName).Obj()
|
|
gatedP2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").PodGroupName(pgName).Obj()
|
|
gatedP3 := st.MakePod().Name("pod3").Namespace("ns1").UID("pod3").PodGroupName(pgName).Obj()
|
|
notFoundPod := st.MakePod().Name("pod-not-found").Namespace("ns1").UID("pod-not-found").Label("allow", "").PodGroupName(pgName).Obj()
|
|
|
|
tests := []struct {
|
|
name string
|
|
initialPods []*v1.Pod
|
|
initialState initialQueueState
|
|
pendingPods []*v1.Pod
|
|
podToDelete *v1.Pod
|
|
expectedInActiveQ bool
|
|
expectedInUnschedulable bool
|
|
expectedInBackoffQ bool
|
|
expectedPodsInPending int
|
|
expectedGated bool
|
|
expectedGroupSize int
|
|
}{
|
|
{
|
|
name: "delete from pendingPodGroupPods (group in flight)",
|
|
initialPods: []*v1.Pod{p1},
|
|
initialState: statePopped,
|
|
pendingPods: []*v1.Pod{p2},
|
|
podToDelete: p2,
|
|
expectedPodsInPending: 0,
|
|
},
|
|
{
|
|
name: "delete one pending pod where there are more pending pods for a group in flight",
|
|
initialPods: []*v1.Pod{p1, p2, p3},
|
|
initialState: statePopped,
|
|
pendingPods: []*v1.Pod{p2, p3},
|
|
podToDelete: p2,
|
|
expectedPodsInPending: 1,
|
|
},
|
|
{
|
|
name: "delete the only pod of a group in activeQ, completely removes group",
|
|
initialPods: []*v1.Pod{p1},
|
|
initialState: stateActive,
|
|
podToDelete: p1,
|
|
expectedInActiveQ: false,
|
|
},
|
|
{
|
|
name: "delete one pod of a size-2 group in activeQ, decreases size and keeps in activeQ",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateActive,
|
|
podToDelete: p2,
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 1,
|
|
},
|
|
{
|
|
name: "delete the only pod of a group in backoffQ, completely removes group",
|
|
initialPods: []*v1.Pod{p1},
|
|
initialState: stateBackoff,
|
|
podToDelete: p1,
|
|
expectedInBackoffQ: false,
|
|
},
|
|
{
|
|
name: "delete one pod of a size-2 group in backoffQ, keeps group in backoffQ",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateBackoff,
|
|
podToDelete: p2,
|
|
expectedInBackoffQ: true,
|
|
expectedGroupSize: 1,
|
|
},
|
|
{
|
|
name: "delete the only pod of a group in unschedulableEntities, completely removes group",
|
|
initialPods: []*v1.Pod{p1},
|
|
initialState: stateUnschedulable,
|
|
podToDelete: p1,
|
|
expectedInUnschedulable: false,
|
|
},
|
|
{
|
|
name: "delete one pod of a size-2 group in unschedulableEntities, decreases size and keeps in unschedulableEntities",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateUnschedulable,
|
|
podToDelete: p2,
|
|
expectedInUnschedulable: true,
|
|
expectedGroupSize: 1,
|
|
},
|
|
{
|
|
name: "delete a pod that doesn't exist in any queue or pending map",
|
|
initialPods: []*v1.Pod{p1},
|
|
initialState: stateActive,
|
|
podToDelete: notFoundPod,
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 1,
|
|
},
|
|
{
|
|
name: "delete the only gated pod from a group, ungates the pod group",
|
|
initialPods: []*v1.Pod{p1, gatedP2},
|
|
initialState: stateGated,
|
|
podToDelete: gatedP2,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: false,
|
|
expectedGroupSize: 1,
|
|
},
|
|
{
|
|
name: "delete one gated pod from a group where another gated pod remains, group stays gated",
|
|
initialPods: []*v1.Pod{p1, gatedP2, gatedP3},
|
|
initialState: stateGated,
|
|
podToDelete: gatedP2,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
|
features.GenericWorkload: true,
|
|
})
|
|
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
preEnqueueMap := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
"preEnqueuePlugin": &preEnqueuePlugin{allowlists: []string{"allow"}},
|
|
},
|
|
}
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithPreEnqueuePluginMap(preEnqueueMap))
|
|
|
|
setupInitialPodGroupState(t, ctx, q, tt.initialPods, tt.initialState)
|
|
// Add pending pods
|
|
for _, pod := range tt.pendingPods {
|
|
q.Add(ctx, pod)
|
|
}
|
|
|
|
// Delete the target pod
|
|
q.Delete(logger, tt.podToDelete)
|
|
|
|
// Verify conditions
|
|
pgLookup := newQueuedPodGroupInfoForLookup(tt.podToDelete)
|
|
|
|
if inActive := q.activeQ.has(pgLookup); inActive != tt.expectedInActiveQ {
|
|
t.Errorf("Expected target pod group in activeQ: %v, got %v", tt.expectedInActiveQ, inActive)
|
|
}
|
|
var entity framework.QueuedEntityInfo
|
|
if tt.expectedInActiveQ {
|
|
entity, _ = q.activeQ.get(pgLookup)
|
|
}
|
|
|
|
if inBackoff := q.backoffQ.has(pgLookup); inBackoff != tt.expectedInBackoffQ {
|
|
t.Errorf("Expected target pod group in backoffQ: %v, got %v", tt.expectedInBackoffQ, inBackoff)
|
|
}
|
|
if tt.expectedInBackoffQ {
|
|
entity, _ = q.backoffQ.get(pgLookup)
|
|
}
|
|
|
|
unschedulableEntity := q.unschedulableEntities.get(pgLookup)
|
|
inUnschedulable := unschedulableEntity != nil
|
|
if inUnschedulable != tt.expectedInUnschedulable {
|
|
t.Errorf("Expected target pod group in unschedulableEntities: %v, got %v", tt.expectedInUnschedulable, inUnschedulable)
|
|
}
|
|
if tt.expectedInUnschedulable {
|
|
entity = unschedulableEntity
|
|
}
|
|
|
|
if entity != nil {
|
|
if size := entity.Size(); size != tt.expectedGroupSize {
|
|
t.Errorf("Expected pod group to be of size: %d, got %d", tt.expectedGroupSize, size)
|
|
}
|
|
|
|
// Verify effective removal of the deleted pod
|
|
for _, pInfo := range entity.(*framework.QueuedPodGroupInfo).QueuedPodInfos {
|
|
if pInfo.Pod.Name == tt.podToDelete.Name {
|
|
t.Errorf("Deleted pod %s is still present in the pod group members", tt.podToDelete.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
pendingPods := q.pendingPodGroupPods.get(pgLookup)
|
|
if pendingLen := len(pendingPods); pendingLen != tt.expectedPodsInPending {
|
|
t.Errorf("Expected pod group to have %d pods in pendingPodGroupPods, got %d", tt.expectedPodsInPending, pendingLen)
|
|
}
|
|
for _, pInfo := range pendingPods {
|
|
if pInfo.Pod.Name == tt.podToDelete.Name {
|
|
t.Errorf("Deleted pod %s is still present in pendingPodGroupPods map", tt.podToDelete.Name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdatePodGroupMember(t *testing.T) {
|
|
pgName := "pg-test"
|
|
p1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Label("allow", "").PodGroupName(pgName).Obj()
|
|
p2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Label("allow", "").PodGroupName(pgName).Obj()
|
|
updatedP2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Label("allow", "").Label("update", "true").PodGroupName(pgName).Obj()
|
|
gatedP1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").PodGroupName(pgName).Obj()
|
|
ungatedP1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Label("allow", "").PodGroupName(pgName).Obj()
|
|
updatedGatedP1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Label("updated", "true").PodGroupName(pgName).Obj()
|
|
gatedP2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").PodGroupName(pgName).Obj()
|
|
notFoundPod := st.MakePod().Name("pod-not-found").Namespace("ns1").UID("pod-not-found").Label("allow", "").PodGroupName(pgName).Obj()
|
|
updatedNotFoundPod := st.MakePod().Name("pod-not-found").Namespace("ns1").UID("pod-not-found").Label("allow", "").Label("updated", "true").PodGroupName(pgName).Obj()
|
|
|
|
tests := []struct {
|
|
name string
|
|
initialPods []*v1.Pod
|
|
initialState initialQueueState
|
|
pendingPods []*v1.Pod
|
|
oldPod *v1.Pod
|
|
newPod *v1.Pod
|
|
expectedInActiveQ bool
|
|
expectedInUnschedulable bool
|
|
expectedInBackoffQ bool
|
|
expectedPodsInPending int
|
|
expectedGated bool
|
|
expectedGroupSize int
|
|
}{
|
|
{
|
|
name: "update pod in activeQ, keeps pod group in activeQ",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateActive,
|
|
oldPod: p2,
|
|
newPod: updatedP2,
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "update pod in backoffQ, keeps pod group in backoffQ",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateBackoff,
|
|
oldPod: p2,
|
|
newPod: updatedP2,
|
|
expectedInBackoffQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "update pod in unschedulableEntities moves it to activeQ (ungated pod group)",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateUnschedulable,
|
|
oldPod: p2,
|
|
newPod: updatedP2,
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "update pod in pendingPodGroupPods (group in flight), updates pod in pending buffer",
|
|
initialPods: []*v1.Pod{p1},
|
|
initialState: statePopped,
|
|
pendingPods: []*v1.Pod{p2},
|
|
oldPod: p2,
|
|
newPod: updatedP2,
|
|
expectedPodsInPending: 1,
|
|
},
|
|
{
|
|
name: "gated pod update (removal of gate), ungates and moves pod group to activeQ",
|
|
initialPods: []*v1.Pod{gatedP1},
|
|
initialState: stateGated,
|
|
oldPod: gatedP1,
|
|
newPod: ungatedP1,
|
|
expectedInActiveQ: true,
|
|
expectedGated: false,
|
|
expectedGroupSize: 1,
|
|
},
|
|
{
|
|
name: "gated pod update without removal of gate, keeps pod group in unschedulableEntities as gated",
|
|
initialPods: []*v1.Pod{gatedP1},
|
|
initialState: stateGated,
|
|
oldPod: gatedP1,
|
|
newPod: updatedGatedP1,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 1,
|
|
},
|
|
{
|
|
name: "ungate one pod but another remains gated, keeps pod group in unschedulableEntities as gated",
|
|
initialPods: []*v1.Pod{gatedP1, gatedP2},
|
|
initialState: stateGated,
|
|
oldPod: gatedP1,
|
|
newPod: ungatedP1,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "updating pod that doesn't exist in any queue, creates new pod group in activeQ",
|
|
initialPods: nil,
|
|
initialState: stateActive,
|
|
oldPod: notFoundPod,
|
|
newPod: updatedNotFoundPod,
|
|
expectedInActiveQ: true,
|
|
expectedGated: false,
|
|
expectedGroupSize: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
|
features.GenericWorkload: true,
|
|
})
|
|
|
|
_, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
preEnqueueMap := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
"preEnqueuePlugin": &preEnqueuePlugin{allowlists: []string{"allow"}},
|
|
},
|
|
}
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithPreEnqueuePluginMap(preEnqueueMap))
|
|
|
|
setupInitialPodGroupState(t, ctx, q, tt.initialPods, tt.initialState)
|
|
// Add pending pods
|
|
for _, pod := range tt.pendingPods {
|
|
q.Add(ctx, pod)
|
|
}
|
|
|
|
// Perform the update
|
|
q.Update(ctx, tt.oldPod, tt.newPod)
|
|
|
|
// Verify conditions
|
|
pgLookup := newQueuedPodGroupInfoForLookup(tt.newPod)
|
|
|
|
if inActive := q.activeQ.has(pgLookup); inActive != tt.expectedInActiveQ {
|
|
t.Errorf("Expected target pod group in activeQ: %v, got %v", tt.expectedInActiveQ, inActive)
|
|
}
|
|
var entity framework.QueuedEntityInfo
|
|
if tt.expectedInActiveQ {
|
|
entity, _ = q.activeQ.get(pgLookup)
|
|
}
|
|
|
|
if inBackoff := q.backoffQ.has(pgLookup); inBackoff != tt.expectedInBackoffQ {
|
|
t.Errorf("Expected target pod group in backoffQ: %v, got %v", tt.expectedInBackoffQ, inBackoff)
|
|
}
|
|
if tt.expectedInBackoffQ {
|
|
entity, _ = q.backoffQ.get(pgLookup)
|
|
}
|
|
|
|
unschedulableEntity := q.unschedulableEntities.get(pgLookup)
|
|
inUnschedulable := unschedulableEntity != nil
|
|
if inUnschedulable != tt.expectedInUnschedulable {
|
|
t.Errorf("Expected target pod group in unschedulableEntities: %v, got %v", tt.expectedInUnschedulable, inUnschedulable)
|
|
}
|
|
if tt.expectedInUnschedulable {
|
|
entity = unschedulableEntity
|
|
}
|
|
|
|
if entity != nil {
|
|
if isGated := entity.Gated(); isGated != tt.expectedGated {
|
|
t.Errorf("Expected pod group to be gated: %v, got %v", tt.expectedGated, isGated)
|
|
}
|
|
if size := entity.Size(); size != tt.expectedGroupSize {
|
|
t.Errorf("Expected pod group to be of size: %d, got %d", tt.expectedGroupSize, size)
|
|
}
|
|
|
|
// Verify effective update of the updated pod
|
|
foundUpdated := false
|
|
for _, pInfo := range entity.(*framework.QueuedPodGroupInfo).QueuedPodInfos {
|
|
if pInfo.Pod.Name == tt.newPod.Name {
|
|
foundUpdated = true
|
|
if diff := cmp.Diff(tt.newPod, pInfo.Pod); diff != "" {
|
|
t.Errorf("Queued member pod differs from newPod (-want +got):\n%s", diff)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if !foundUpdated {
|
|
t.Errorf("Updated pod %s was not found in the pod group members", tt.newPod.Name)
|
|
}
|
|
}
|
|
|
|
pendingPods := q.pendingPodGroupPods.get(pgLookup)
|
|
if pendingLen := len(pendingPods); pendingLen != tt.expectedPodsInPending {
|
|
t.Errorf("Expected pod group to have %d pods in pendingPodGroupPods, got %d", tt.expectedPodsInPending, pendingLen)
|
|
}
|
|
if tt.expectedPodsInPending > 0 {
|
|
inPending := false
|
|
for _, pInfo := range pendingPods {
|
|
if pInfo.Pod.Name == tt.newPod.Name {
|
|
inPending = true
|
|
if diff := cmp.Diff(tt.newPod, pInfo.Pod); diff != "" {
|
|
t.Errorf("Pending member pod differs from newPod (-want +got):\n%s", diff)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if !inPending {
|
|
t.Errorf("Updated pod %s was not found in pendingPodGroupPods map", tt.newPod.Name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActivatePodGroupMember(t *testing.T) {
|
|
pgName := "pg-test"
|
|
p1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Label("allow", "").PodGroupName(pgName).Obj()
|
|
p2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Label("allow", "").PodGroupName(pgName).Obj()
|
|
gatedP1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").PodGroupName(pgName).Obj()
|
|
notFoundPod := st.MakePod().Name("pod-not-found").Namespace("ns1").UID("pod-not-found").Label("allow", "").PodGroupName(pgName).Obj()
|
|
|
|
tests := []struct {
|
|
name string
|
|
initialPods []*v1.Pod
|
|
initialState initialQueueState
|
|
pendingPods []*v1.Pod
|
|
podToActivate *v1.Pod
|
|
expectedInActiveQ bool
|
|
expectedInUnschedulable bool
|
|
expectedPodsInPending int
|
|
expectedGated bool
|
|
expectedForceActivateEvent bool
|
|
}{
|
|
{
|
|
name: "activate pod group in unschedulableEntities (ungated), moves pod group to activeQ",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateUnschedulable,
|
|
podToActivate: p1,
|
|
expectedInActiveQ: true,
|
|
},
|
|
{
|
|
name: "activate pod group in unschedulableEntities (gated), keeps pod group in unschedulableEntities as gated",
|
|
initialPods: []*v1.Pod{gatedP1},
|
|
initialState: stateGated,
|
|
podToActivate: gatedP1,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
},
|
|
{
|
|
name: "activate pod group in backoffQ, moves pod group to activeQ",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateBackoff,
|
|
podToActivate: p1,
|
|
expectedInActiveQ: true,
|
|
},
|
|
{
|
|
name: "activate pod group in flight (popped), pod group remains in flight but tracks event",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: statePopped,
|
|
podToActivate: p1,
|
|
expectedForceActivateEvent: true,
|
|
},
|
|
{
|
|
name: "activating pod that does not exist in any queue or in flight is ignored",
|
|
podToActivate: notFoundPod,
|
|
expectedInActiveQ: false,
|
|
},
|
|
// {
|
|
// name: "activate pending pod when pod group is in flight, pods remain pending in pendingPodGroupPods map, but track event",
|
|
// initialPods: []*v1.Pod{p1},
|
|
// initialState: statePopped,
|
|
// pendingPods: []*v1.Pod{p2},
|
|
// podToActivate: p2,
|
|
// expectedForceActivateEvent: true,
|
|
// expectedPodsInPending: 1,
|
|
// },
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
|
features.GenericWorkload: true,
|
|
})
|
|
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
preEnqueueMap := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
"preEnqueuePlugin": &preEnqueuePlugin{allowlists: []string{"allow"}},
|
|
},
|
|
}
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithPreEnqueuePluginMap(preEnqueueMap))
|
|
|
|
setupInitialPodGroupState(t, ctx, q, tt.initialPods, tt.initialState)
|
|
// Add pending pods
|
|
for _, pod := range tt.pendingPods {
|
|
q.Add(ctx, pod)
|
|
}
|
|
|
|
// Activate the pod
|
|
q.Activate(logger, map[string]*v1.Pod{string(tt.podToActivate.UID): tt.podToActivate})
|
|
|
|
// Verify conditions
|
|
pgLookup := newQueuedPodGroupInfoForLookup(tt.podToActivate)
|
|
|
|
if inActive := q.activeQ.has(pgLookup); inActive != tt.expectedInActiveQ {
|
|
t.Errorf("Expected target pod group in activeQ: %v, got %v", tt.expectedInActiveQ, inActive)
|
|
}
|
|
var entity framework.QueuedEntityInfo
|
|
if tt.expectedInActiveQ {
|
|
entity, _ = q.activeQ.get(pgLookup)
|
|
}
|
|
|
|
if q.backoffQ.has(pgLookup) {
|
|
t.Errorf("Expected target pod group not to be present in backoffQ")
|
|
}
|
|
|
|
unschedulableEntity := q.unschedulableEntities.get(pgLookup)
|
|
inUnschedulable := unschedulableEntity != nil
|
|
if inUnschedulable != tt.expectedInUnschedulable {
|
|
t.Errorf("Expected target pod group in unschedulableEntities: %v, got %v", tt.expectedInUnschedulable, inUnschedulable)
|
|
}
|
|
if tt.expectedInUnschedulable {
|
|
entity = unschedulableEntity
|
|
}
|
|
|
|
if entity != nil {
|
|
if isGated := entity.Gated(); isGated != tt.expectedGated {
|
|
t.Errorf("Expected pod group to be gated: %v, got %v", tt.expectedGated, isGated)
|
|
}
|
|
}
|
|
|
|
pendingPods := q.pendingPodGroupPods.get(pgLookup)
|
|
if pendingLen := len(pendingPods); pendingLen != tt.expectedPodsInPending {
|
|
t.Errorf("Expected pod group to have %d pods in pendingPodGroupPods, got %d", tt.expectedPodsInPending, pendingLen)
|
|
}
|
|
if tt.expectedPodsInPending > 0 {
|
|
inPending := false
|
|
for _, pInfo := range pendingPods {
|
|
if pInfo.Pod.Name == tt.podToActivate.Name {
|
|
inPending = true
|
|
break
|
|
}
|
|
}
|
|
if !inPending {
|
|
t.Errorf("Pod %s was not found in pendingPodGroupPods map", tt.podToActivate.Name)
|
|
}
|
|
}
|
|
|
|
if tt.expectedForceActivateEvent {
|
|
foundEvent := false
|
|
for _, ev := range q.activeQ.listInFlightEvents() {
|
|
clusterEvent, ok := ev.(*clusterEvent)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if clusterEvent.event.Label() == framework.EventForceActivate.Label() {
|
|
foundEvent = true
|
|
break
|
|
}
|
|
}
|
|
if !foundEvent {
|
|
t.Errorf("Expected ForceActivate in-flight event to be tracked, but it wasn't")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMoveAllToActiveOrBackoffQueuePodGroupMember(t *testing.T) {
|
|
pgName := "pg-test"
|
|
p1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Label("allow", "").PodGroupName(pgName).Obj()
|
|
p2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Label("allow", "").PodGroupName(pgName).Obj()
|
|
gatedP1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").PodGroupName(pgName).Obj()
|
|
|
|
tests := []struct {
|
|
name string
|
|
initialPods []*v1.Pod
|
|
initialState initialQueueState
|
|
event fwk.ClusterEvent
|
|
preCheck PreEnqueueCheck
|
|
expectedInActiveQ bool
|
|
expectedInUnschedulable bool
|
|
expectedGated bool
|
|
expectedGroupSize int
|
|
expectedInFlightEvent bool
|
|
}{
|
|
{
|
|
name: "event of interest moves ungated pod group from unschedulableEntities to activeQ",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateUnschedulable,
|
|
event: fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Add},
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "event not of interest keeps pod group in unschedulableEntities",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateUnschedulable,
|
|
event: fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Delete},
|
|
expectedInUnschedulable: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
// {
|
|
// name: "gated pod group matching gating event moves the pod group to activeQ",
|
|
// initialPods: []*v1.Pod{gatedP1, p2},
|
|
// initialState: stateGated,
|
|
// event: fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.Add},
|
|
// expectedInActiveQ: true,
|
|
// expectedGroupSize: 2,
|
|
// },
|
|
{
|
|
name: "gated pod group not matching gating event stays in unschedulableEntities as gated",
|
|
initialPods: []*v1.Pod{gatedP1, p2},
|
|
initialState: stateGated,
|
|
event: fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.Delete},
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "gated pod group matching event for ungated pod stays in unschedulableEntities as gated",
|
|
initialPods: []*v1.Pod{gatedP1, p2},
|
|
initialState: stateGated,
|
|
event: fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Add},
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "event received while pod group is in flight is tracked in inFlightEvents",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: statePopped,
|
|
event: fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Add},
|
|
expectedInFlightEvent: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
|
features.GenericWorkload: true,
|
|
})
|
|
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
preEnqueueMap := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
"preEnqueuePlugin": &preEnqueuePlugin{allowlists: []string{"allow"}},
|
|
},
|
|
}
|
|
|
|
m := makeEmptyQueueingHintMapPerProfile()
|
|
m[""] = map[fwk.ClusterEvent][]*QueueingHintFunction{
|
|
{Resource: fwk.Pod, ActionType: fwk.Add}: {
|
|
{
|
|
PluginName: "preEnqueuePlugin",
|
|
QueueingHintFn: queueHintReturnQueue,
|
|
},
|
|
},
|
|
{Resource: fwk.Node, ActionType: fwk.Add}: {
|
|
{
|
|
PluginName: "otherPlugin",
|
|
QueueingHintFn: func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
|
|
if pod.Name == "pod1" {
|
|
return fwk.Queue, nil
|
|
}
|
|
return fwk.QueueSkip, nil
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithPreEnqueuePluginMap(preEnqueueMap), WithQueueingHintMapPerProfile(m))
|
|
|
|
setupInitialPodGroupState(t, ctx, q, tt.initialPods, tt.initialState)
|
|
|
|
// MoveAllToActiveOrBackoffQueue with the given event
|
|
q.MoveAllToActiveOrBackoffQueue(logger, tt.event, nil, nil, tt.preCheck)
|
|
|
|
// Verify conditions
|
|
pgLookup := newQueuedPodGroupInfoForLookup(tt.initialPods[0])
|
|
|
|
if inActive := q.activeQ.has(pgLookup); inActive != tt.expectedInActiveQ {
|
|
t.Errorf("Expected target pod group in activeQ: %v, got %v", tt.expectedInActiveQ, inActive)
|
|
}
|
|
var entity framework.QueuedEntityInfo
|
|
if tt.expectedInActiveQ {
|
|
entity, _ = q.activeQ.get(pgLookup)
|
|
}
|
|
|
|
if q.backoffQ.has(pgLookup) {
|
|
t.Errorf("Expected target pod group not to be present in backoffQ")
|
|
}
|
|
|
|
unschedulableEntity := q.unschedulableEntities.get(pgLookup)
|
|
inUnschedulable := unschedulableEntity != nil
|
|
if inUnschedulable != tt.expectedInUnschedulable {
|
|
t.Errorf("Expected target pod group in unschedulableEntities: %v, got %v", tt.expectedInUnschedulable, inUnschedulable)
|
|
}
|
|
if tt.expectedInUnschedulable {
|
|
entity = unschedulableEntity
|
|
}
|
|
|
|
if entity != nil {
|
|
if isGated := entity.Gated(); isGated != tt.expectedGated {
|
|
t.Errorf("Expected pod group to be gated: %v, got %v", tt.expectedGated, isGated)
|
|
}
|
|
if size := entity.Size(); size != tt.expectedGroupSize {
|
|
t.Errorf("Expected pod group to be of size: %d, got %d", tt.expectedGroupSize, size)
|
|
}
|
|
|
|
if tt.expectedGroupSize > 0 {
|
|
foundPod := false
|
|
for _, pInfo := range entity.(*framework.QueuedPodGroupInfo).QueuedPodInfos {
|
|
if pInfo.Pod.Name == tt.initialPods[0].Name {
|
|
foundPod = true
|
|
break
|
|
}
|
|
}
|
|
if !foundPod {
|
|
t.Errorf("Pod %s was not found in the pod group members", tt.initialPods[0].Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
if tt.expectedInFlightEvent {
|
|
foundEvent := false
|
|
for _, ev := range q.activeQ.listInFlightEvents() {
|
|
clusterEvent, ok := ev.(*clusterEvent)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if clusterEvent.event.Label() == tt.event.Label() {
|
|
foundEvent = true
|
|
break
|
|
}
|
|
}
|
|
if !foundEvent {
|
|
t.Errorf("Expected in-flight event %s to be tracked, but it wasn't", tt.event.Label())
|
|
}
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFlushBackoffQCompletedPodGroupMember(t *testing.T) {
|
|
pgName := "pg-test"
|
|
p1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Label("allow", "").PodGroupName(pgName).Obj()
|
|
p2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Label("allow", "").PodGroupName(pgName).Obj()
|
|
|
|
tests := []struct {
|
|
name string
|
|
initialPods []*v1.Pod
|
|
initialState initialQueueState
|
|
advanceClock time.Duration
|
|
expectedInActiveQ bool
|
|
expectedInBackoffQ bool
|
|
expectedGroupSize int
|
|
}{
|
|
{
|
|
name: "flushing backoffQ after backoff completes moves pod group to activeQ",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateBackoff,
|
|
advanceClock: 10 * time.Second,
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "flushing backoffQ before backoff completes keeps pod group in backoffQ",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateBackoff,
|
|
advanceClock: 0,
|
|
expectedInBackoffQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
|
features.GenericWorkload: true,
|
|
})
|
|
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
preEnqueueMap := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
"preEnqueuePlugin": &preEnqueuePlugin{allowlists: []string{"allow"}},
|
|
},
|
|
}
|
|
|
|
fakeClock := testingclock.NewFakeClock(time.Now())
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithPreEnqueuePluginMap(preEnqueueMap), WithClock(fakeClock))
|
|
|
|
setupInitialPodGroupState(t, ctx, q, tt.initialPods, tt.initialState)
|
|
|
|
if tt.advanceClock > 0 {
|
|
fakeClock.Step(tt.advanceClock)
|
|
}
|
|
|
|
// Flush the backoff queue
|
|
q.flushBackoffQCompleted(logger)
|
|
|
|
// Verify conditions
|
|
pgLookup := newQueuedPodGroupInfoForLookup(tt.initialPods[0])
|
|
|
|
if inActive := q.activeQ.has(pgLookup); inActive != tt.expectedInActiveQ {
|
|
t.Errorf("Expected target pod group in activeQ: %v, got %v", tt.expectedInActiveQ, inActive)
|
|
}
|
|
var entity framework.QueuedEntityInfo
|
|
if tt.expectedInActiveQ {
|
|
entity, _ = q.activeQ.get(pgLookup)
|
|
}
|
|
|
|
if inBackoff := q.backoffQ.has(pgLookup); inBackoff != tt.expectedInBackoffQ {
|
|
t.Errorf("Expected target pod group in backoffQ: %v, got %v", tt.expectedInBackoffQ, inBackoff)
|
|
}
|
|
if tt.expectedInBackoffQ {
|
|
entity, _ = q.backoffQ.get(pgLookup)
|
|
}
|
|
|
|
if q.unschedulableEntities.get(pgLookup) != nil {
|
|
t.Errorf("Expected target pod group not to be present in unschedulableEntities")
|
|
}
|
|
|
|
if entity != nil {
|
|
if size := entity.Size(); size != tt.expectedGroupSize {
|
|
t.Errorf("Expected pod group to be of size: %d, got %d", tt.expectedGroupSize, size)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFlushUnschedulableEntitiesLeftoverPodGroupMember(t *testing.T) {
|
|
pgName := "pg-test"
|
|
p1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Label("allow", "").PodGroupName(pgName).Obj()
|
|
p2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Label("allow", "").PodGroupName(pgName).Obj()
|
|
gatedP1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").PodGroupName(pgName).Obj()
|
|
|
|
tests := []struct {
|
|
name string
|
|
initialPods []*v1.Pod
|
|
initialState initialQueueState
|
|
advanceClock time.Duration
|
|
expectedInActiveQ bool
|
|
expectedInUnschedulable bool
|
|
expectedGated bool
|
|
expectedGroupSize int
|
|
}{
|
|
{
|
|
name: "flushing unschedulable leftover after max duration moves ungated pod group to activeQ",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateUnschedulable,
|
|
advanceClock: DefaultPodMaxInUnschedulablePodsDuration + time.Second,
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "flushing unschedulable leftover before max duration keeps pod group in unschedulableEntities",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: stateUnschedulable,
|
|
advanceClock: 0,
|
|
expectedInUnschedulable: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "flushing unschedulable leftover for gated pod group after max duration keeps it in unschedulableEntities as gated",
|
|
initialPods: []*v1.Pod{gatedP1, p2},
|
|
initialState: stateGated,
|
|
advanceClock: DefaultPodMaxInUnschedulablePodsDuration + time.Second,
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
|
features.GenericWorkload: true,
|
|
})
|
|
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
preEnqueueMap := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
"preEnqueuePlugin": &preEnqueuePlugin{allowlists: []string{"allow"}},
|
|
},
|
|
}
|
|
|
|
fakeClock := testingclock.NewFakeClock(time.Now())
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithPreEnqueuePluginMap(preEnqueueMap), WithClock(fakeClock))
|
|
|
|
setupInitialPodGroupState(t, ctx, q, tt.initialPods, tt.initialState)
|
|
|
|
if tt.advanceClock > 0 {
|
|
fakeClock.Step(tt.advanceClock)
|
|
}
|
|
|
|
// Flush unschedulable entities
|
|
q.flushUnschedulableEntitiesLeftover(logger)
|
|
|
|
// Verify conditions
|
|
pgLookup := newQueuedPodGroupInfoForLookup(tt.initialPods[0])
|
|
|
|
if inActive := q.activeQ.has(pgLookup); inActive != tt.expectedInActiveQ {
|
|
t.Errorf("Expected target pod group in activeQ: %v, got %v", tt.expectedInActiveQ, inActive)
|
|
}
|
|
var entity framework.QueuedEntityInfo
|
|
if tt.expectedInActiveQ {
|
|
entity, _ = q.activeQ.get(pgLookup)
|
|
}
|
|
|
|
if q.backoffQ.has(pgLookup) {
|
|
t.Errorf("Expected target pod group not to be present in backoffQ")
|
|
}
|
|
|
|
unschedulableEntity := q.unschedulableEntities.get(pgLookup)
|
|
inUnschedulable := unschedulableEntity != nil
|
|
if inUnschedulable != tt.expectedInUnschedulable {
|
|
t.Errorf("Expected target pod group in unschedulableEntities: %v, got %v", tt.expectedInUnschedulable, inUnschedulable)
|
|
}
|
|
if tt.expectedInUnschedulable {
|
|
entity = unschedulableEntity
|
|
}
|
|
|
|
if entity != nil {
|
|
if isGated := entity.Gated(); isGated != tt.expectedGated {
|
|
t.Errorf("Expected pod group to be gated: %v, got %v", tt.expectedGated, isGated)
|
|
}
|
|
if size := entity.Size(); size != tt.expectedGroupSize {
|
|
t.Errorf("Expected pod group to be of size: %d, got %d", tt.expectedGroupSize, size)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddUnschedulablePodIfNotPresentPodGroupMember(t *testing.T) {
|
|
pgName := "pg-test"
|
|
p1 := st.MakePod().Name("pod1").Namespace("ns1").UID("pod1").Label("allow", "").PodGroupName(pgName).Obj()
|
|
p2 := st.MakePod().Name("pod2").Namespace("ns1").UID("pod2").Label("allow", "").PodGroupName(pgName).Obj()
|
|
p3 := st.MakePod().Name("pod3").Namespace("ns1").UID("pod3").Label("allow", "").PodGroupName(pgName).Obj()
|
|
gatedP3 := st.MakePod().Name("pod3").Namespace("ns1").UID("pod3").PodGroupName(pgName).Obj()
|
|
|
|
pInfo1 := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(p1),
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("fooPlugin"),
|
|
},
|
|
}
|
|
pInfo1NoPlugins := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(p1),
|
|
}
|
|
pInfo2 := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(p2),
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("barPlugin"),
|
|
},
|
|
}
|
|
gatedPInfo3 := &framework.QueuedPodInfo{
|
|
PodInfo: mustNewPodInfo(gatedP3),
|
|
QueueingParams: framework.QueueingParams{
|
|
UnschedulablePlugins: sets.New("bazPlugin"),
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
initialPods []*v1.Pod
|
|
initialState initialQueueState
|
|
clearLastPopped bool
|
|
podsToAdd []*framework.QueuedPodInfo
|
|
expectedPodsInPending int
|
|
expectedInActiveQ bool
|
|
expectedInUnschedulable bool
|
|
expectedInBackoffQ bool
|
|
expectedGated bool
|
|
expectedGroupSize int
|
|
}{
|
|
{
|
|
name: "single pod group member added with plugin to pending when group is in flight",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: statePopped,
|
|
podsToAdd: []*framework.QueuedPodInfo{pInfo1},
|
|
expectedPodsInPending: 1,
|
|
},
|
|
{
|
|
name: "single pod group member added with no plugins (consecutive error) to pending",
|
|
initialPods: []*v1.Pod{p1, p2},
|
|
initialState: statePopped,
|
|
podsToAdd: []*framework.QueuedPodInfo{pInfo1NoPlugins},
|
|
expectedPodsInPending: 1,
|
|
},
|
|
{
|
|
name: "multiple pod group members, including gated added with plugins sequentially to pending when group is in flight",
|
|
initialPods: []*v1.Pod{p1, p2, p3},
|
|
initialState: statePopped,
|
|
podsToAdd: []*framework.QueuedPodInfo{pInfo1, pInfo2, gatedPInfo3},
|
|
expectedPodsInPending: 3,
|
|
},
|
|
{
|
|
name: "pod group is not last popped entity and no group exists, a new group is created and added to activeQ",
|
|
initialPods: nil,
|
|
podsToAdd: []*framework.QueuedPodInfo{pInfo1},
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 1,
|
|
},
|
|
{
|
|
name: "pod group exists in unschedulableEntities, pod is added and group remains in unschedulableEntities",
|
|
initialPods: []*v1.Pod{p2},
|
|
initialState: stateUnschedulable,
|
|
clearLastPopped: true,
|
|
podsToAdd: []*framework.QueuedPodInfo{pInfo1},
|
|
expectedInUnschedulable: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "pod group exists in activeQ, pod is added and group remains in activeQ",
|
|
initialPods: []*v1.Pod{p2},
|
|
initialState: stateActive,
|
|
clearLastPopped: true,
|
|
podsToAdd: []*framework.QueuedPodInfo{pInfo1},
|
|
expectedInActiveQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "pod group exists in backoffQ, pod is added and group remains in backoffQ",
|
|
initialPods: []*v1.Pod{p2},
|
|
initialState: stateBackoff,
|
|
clearLastPopped: true,
|
|
podsToAdd: []*framework.QueuedPodInfo{pInfo1},
|
|
expectedInBackoffQ: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
{
|
|
name: "pod group exists in unschedulableEntities (gated), ungated pod is added and group remains gated in unschedulableEntities",
|
|
initialPods: []*v1.Pod{gatedP3},
|
|
initialState: stateGated,
|
|
clearLastPopped: true,
|
|
podsToAdd: []*framework.QueuedPodInfo{pInfo1},
|
|
expectedInUnschedulable: true,
|
|
expectedGated: true,
|
|
expectedGroupSize: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
|
features.GenericWorkload: true,
|
|
})
|
|
|
|
logger, ctx := ktesting.NewTestContext(t)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
preEnqueueMap := map[string]map[string]fwk.PreEnqueuePlugin{
|
|
"": {
|
|
"preEnqueuePlugin": &preEnqueuePlugin{allowlists: []string{"allow"}},
|
|
},
|
|
}
|
|
|
|
q := NewTestQueue(ctx, newDefaultQueueSort(), WithPreEnqueuePluginMap(preEnqueueMap))
|
|
|
|
setupInitialPodGroupState(t, ctx, q, tt.initialPods, tt.initialState)
|
|
|
|
if tt.clearLastPopped {
|
|
q.activeQ.clearPoppedEntity()
|
|
}
|
|
|
|
// Add unschedulable pods
|
|
for _, pInfo := range tt.podsToAdd {
|
|
pInfoCloned := pInfo.DeepCopy()
|
|
if err := q.AddUnschedulablePodIfNotPresent(logger, pInfoCloned, q.SchedulingCycle()); err != nil {
|
|
t.Errorf("Failed to add unschedulable pods %s: %v", pInfoCloned.Pod.Name, err)
|
|
}
|
|
}
|
|
|
|
// Verify conditions
|
|
pgLookup := newQueuedPodGroupInfoForLookup(tt.podsToAdd[0].Pod)
|
|
|
|
if inActive := q.activeQ.has(pgLookup); inActive != tt.expectedInActiveQ {
|
|
t.Errorf("Expected pod group in activeQ: %v, got %v", tt.expectedInActiveQ, inActive)
|
|
}
|
|
var entity framework.QueuedEntityInfo
|
|
if tt.expectedInActiveQ {
|
|
entity, _ = q.activeQ.get(pgLookup)
|
|
}
|
|
|
|
if inBackoff := q.backoffQ.has(pgLookup); inBackoff != tt.expectedInBackoffQ {
|
|
t.Errorf("Expected pod group in backoffQ: %v, got %v", tt.expectedInBackoffQ, inBackoff)
|
|
}
|
|
if tt.expectedInBackoffQ {
|
|
entity, _ = q.backoffQ.get(pgLookup)
|
|
}
|
|
|
|
unschedulableEntity := q.unschedulableEntities.get(pgLookup)
|
|
inUnschedulable := unschedulableEntity != nil
|
|
if inUnschedulable != tt.expectedInUnschedulable {
|
|
t.Errorf("Expected pod group in unschedulableEntities: %v, got %v", tt.expectedInUnschedulable, inUnschedulable)
|
|
}
|
|
if tt.expectedInUnschedulable {
|
|
entity = unschedulableEntity
|
|
}
|
|
|
|
if entity != nil {
|
|
if isGated := entity.Gated(); isGated != tt.expectedGated {
|
|
t.Errorf("Expected pod group to be gated: %v, got %v", tt.expectedGated, isGated)
|
|
}
|
|
if size := entity.Size(); size != tt.expectedGroupSize {
|
|
t.Errorf("Expected pod group to be of size: %d, got %d", tt.expectedGroupSize, size)
|
|
}
|
|
}
|
|
|
|
pendingPods := q.pendingPodGroupPods.get(pgLookup)
|
|
if pendingLen := len(pendingPods); pendingLen != tt.expectedPodsInPending {
|
|
t.Errorf("Expected pod group to have %d pods in pendingPodGroupPods, got %d", tt.expectedPodsInPending, pendingLen)
|
|
}
|
|
})
|
|
}
|
|
}
|