@@ -4803,6 +4803,229 @@ var _ = ginkgo.Describe("Topology Aware Scheduling", ginkgo.Ordered, func() {
48034803 })
48044804 })
48054805
4806+ ginkgo .When ("PrioritizePreemptorWorkloads is enabled with multiple evictions within Cohort" , func () {
4807+ var (
4808+ nodes []corev1.Node
4809+ localQueueB * kueue.LocalQueue
4810+ clusterQueueB * kueue.ClusterQueue
4811+ localQueueC * kueue.LocalQueue
4812+ clusterQueueC * kueue.ClusterQueue
4813+ )
4814+
4815+ ginkgo .BeforeEach (func () {
4816+ features .SetFeatureGateDuringTest (ginkgo .GinkgoTB (), features .PrioritizePreemptorWorkloads , true )
4817+
4818+ // b1
4819+ // / | \
4820+ // r1 r2 r3
4821+ // | | |
4822+ // x2 x1 x3
4823+ // (5) (5) (2)
4824+ nodes = []corev1.Node {
4825+ * testingnode .MakeNode ("x2" ).
4826+ Label ("node-group" , "tas" ).
4827+ Label (utiltesting .DefaultBlockTopologyLevel , "b1" ).
4828+ Label (utiltesting .DefaultRackTopologyLevel , "r1" ).
4829+ Label (corev1 .LabelHostname , "x2" ).
4830+ StatusAllocatable (corev1.ResourceList {
4831+ corev1 .ResourceCPU : resource .MustParse ("5" ),
4832+ corev1 .ResourceMemory : resource .MustParse ("5Gi" ),
4833+ corev1 .ResourcePods : resource .MustParse ("10" ),
4834+ }).
4835+ Ready ().
4836+ Obj (),
4837+ * testingnode .MakeNode ("x1" ).
4838+ Label ("node-group" , "tas" ).
4839+ Label (utiltesting .DefaultBlockTopologyLevel , "b1" ).
4840+ Label (utiltesting .DefaultRackTopologyLevel , "r2" ).
4841+ Label (corev1 .LabelHostname , "x1" ).
4842+ StatusAllocatable (corev1.ResourceList {
4843+ corev1 .ResourceCPU : resource .MustParse ("5" ),
4844+ corev1 .ResourceMemory : resource .MustParse ("5Gi" ),
4845+ corev1 .ResourcePods : resource .MustParse ("10" ),
4846+ }).
4847+ Ready ().
4848+ Obj (),
4849+ * testingnode .MakeNode ("x3" ).
4850+ Label ("node-group" , "tas" ).
4851+ Label (utiltesting .DefaultBlockTopologyLevel , "b1" ).
4852+ Label (utiltesting .DefaultRackTopologyLevel , "r3" ).
4853+ Label (corev1 .LabelHostname , "x3" ).
4854+ StatusAllocatable (corev1.ResourceList {
4855+ corev1 .ResourceCPU : resource .MustParse ("2" ),
4856+ corev1 .ResourceMemory : resource .MustParse ("5Gi" ),
4857+ corev1 .ResourcePods : resource .MustParse ("10" ),
4858+ }).
4859+ Ready ().
4860+ Obj (),
4861+ }
4862+ util .CreateNodesWithStatus (ctx , k8sClient , nodes )
4863+
4864+ topology = utiltestingapi .MakeDefaultThreeLevelTopology ("default" )
4865+ util .MustCreate (ctx , k8sClient , topology )
4866+
4867+ tasFlavor = utiltestingapi .MakeResourceFlavor ("tas-flavor" ).
4868+ NodeLabel ("node-group" , "tas" ).
4869+ TopologyName ("default" ).Obj ()
4870+ util .MustCreate (ctx , k8sClient , tasFlavor )
4871+
4872+ clusterQueue = utiltestingapi .MakeClusterQueue ("cluster-queue" ).
4873+ Cohort ("cohort" ).
4874+ Preemption (kueue.ClusterQueuePreemption {
4875+ WithinClusterQueue : kueue .PreemptionPolicyLowerPriority ,
4876+ ReclaimWithinCohort : kueue .PreemptionPolicyAny ,
4877+ BorrowWithinCohort : & kueue.BorrowWithinCohort {
4878+ Policy : kueue .BorrowWithinCohortPolicyLowerPriority ,
4879+ },
4880+ }).
4881+ ResourceGroup (* utiltestingapi .MakeFlavorQuotas (tasFlavor .Name ).
4882+ Resource (corev1 .ResourceCPU , "4" ).
4883+ Resource (corev1 .ResourceMemory , "5Gi" ).Obj ()).
4884+ Obj ()
4885+ util .MustCreate (ctx , k8sClient , clusterQueue )
4886+ util .ExpectClusterQueuesToBeActive (ctx , k8sClient , clusterQueue )
4887+
4888+ localQueue = utiltestingapi .MakeLocalQueue ("local-queue" , ns .Name ).ClusterQueue (clusterQueue .Name ).Obj ()
4889+ util .MustCreate (ctx , k8sClient , localQueue )
4890+
4891+ clusterQueueB = utiltestingapi .MakeClusterQueue ("cluster-queue-b" ).
4892+ Cohort ("cohort" ).
4893+ Preemption (kueue.ClusterQueuePreemption {
4894+ WithinClusterQueue : kueue .PreemptionPolicyLowerPriority ,
4895+ ReclaimWithinCohort : kueue .PreemptionPolicyLowerPriority ,
4896+ BorrowWithinCohort : & kueue.BorrowWithinCohort {
4897+ Policy : kueue .BorrowWithinCohortPolicyLowerPriority ,
4898+ },
4899+ }).
4900+ ResourceGroup (* utiltestingapi .MakeFlavorQuotas (tasFlavor .Name ).
4901+ Resource (corev1 .ResourceCPU , "3" ).
4902+ Resource (corev1 .ResourceMemory , "5Gi" ).Obj ()).
4903+ Obj ()
4904+ util .MustCreate (ctx , k8sClient , clusterQueueB )
4905+ util .ExpectClusterQueuesToBeActive (ctx , k8sClient , clusterQueueB )
4906+
4907+ localQueueB = utiltestingapi .MakeLocalQueue ("local-queue-b" , ns .Name ).ClusterQueue (clusterQueueB .Name ).Obj ()
4908+ util .MustCreate (ctx , k8sClient , localQueueB )
4909+
4910+ clusterQueueC = utiltestingapi .MakeClusterQueue ("cluster-queue-c" ).
4911+ Cohort ("cohort" ).
4912+ Preemption (kueue.ClusterQueuePreemption {
4913+ WithinClusterQueue : kueue .PreemptionPolicyLowerPriority ,
4914+ ReclaimWithinCohort : kueue .PreemptionPolicyLowerPriority ,
4915+ BorrowWithinCohort : & kueue.BorrowWithinCohort {
4916+ Policy : kueue .BorrowWithinCohortPolicyLowerPriority ,
4917+ },
4918+ }).
4919+ ResourceGroup (* utiltestingapi .MakeFlavorQuotas (tasFlavor .Name ).
4920+ Resource (corev1 .ResourceCPU , "5" ).
4921+ Resource (corev1 .ResourceMemory , "5Gi" ).Obj ()).
4922+ Obj ()
4923+ util .MustCreate (ctx , k8sClient , clusterQueueC )
4924+ util .ExpectClusterQueuesToBeActive (ctx , k8sClient , clusterQueueC )
4925+
4926+ localQueueC = utiltestingapi .MakeLocalQueue ("local-queue-c" , ns .Name ).ClusterQueue (clusterQueueC .Name ).Obj ()
4927+ util .MustCreate (ctx , k8sClient , localQueueC )
4928+ })
4929+
4930+ ginkgo .AfterEach (func () {
4931+ gomega .Expect (util .DeleteAllJobsInNamespace (ctx , k8sClient , ns )).Should (gomega .Succeed ())
4932+ gomega .Expect (util .DeleteWorkloadsInNamespace (ctx , k8sClient , ns )).Should (gomega .Succeed ())
4933+ gomega .Expect (util .DeleteObject (ctx , k8sClient , localQueue )).Should (gomega .Succeed ())
4934+ gomega .Expect (util .DeleteObject (ctx , k8sClient , localQueueB )).Should (gomega .Succeed ())
4935+ gomega .Expect (util .DeleteObject (ctx , k8sClient , localQueueC )).Should (gomega .Succeed ())
4936+
4937+ util .ExpectObjectToBeDeleted (ctx , k8sClient , clusterQueue , true )
4938+ util .ExpectObjectToBeDeleted (ctx , k8sClient , clusterQueueB , true )
4939+ util .ExpectObjectToBeDeleted (ctx , k8sClient , clusterQueueC , true )
4940+ util .ExpectObjectToBeDeleted (ctx , k8sClient , tasFlavor , true )
4941+ util .ExpectObjectToBeDeleted (ctx , k8sClient , topology , true )
4942+ for _ , node := range nodes {
4943+ util .ExpectObjectToBeDeleted (ctx , k8sClient , & node , true )
4944+ }
4945+ })
4946+
4947+ ginkgo .It ("should prevent other workloads from stealing topology when a preemptor is waiting for multiple evictions" , func () {
4948+ var wlA , wlB , wlPending , preemptor * kueue.Workload
4949+ ginkgo .By ("creating initial workload in clusterQueue consuming 4 CPU on first node" , func () {
4950+ wlA = utiltestingapi .MakeWorkload ("wl-a" , ns .Name ).
4951+ Priority (1 ).
4952+ PodSets (* utiltestingapi .MakePodSet ("worker" , 1 ).
4953+ PreferredTopologyRequest (utiltesting .DefaultBlockTopologyLevel ).
4954+ Obj ()).
4955+ Queue (kueue .LocalQueueName (localQueue .Name )).Request (corev1 .ResourceCPU , "4" ).Obj ()
4956+ util .MustCreate (ctx , k8sClient , wlA )
4957+ })
4958+
4959+ ginkgo .By ("creating initial workload in clusterQueueB consuming 5 CPU on second node (borrowing 2 CPU)" , func () {
4960+ wlB = utiltestingapi .MakeWorkload ("wl-b" , ns .Name ).
4961+ Priority (2 ).
4962+ PodSets (* utiltestingapi .MakePodSet ("worker" , 1 ).
4963+ PreferredTopologyRequest (utiltesting .DefaultBlockTopologyLevel ).
4964+ Obj ()).
4965+ Queue (kueue .LocalQueueName (localQueueB .Name )).Request (corev1 .ResourceCPU , "5" ).Obj ()
4966+ util .MustCreate (ctx , k8sClient , wlB )
4967+ })
4968+
4969+ ginkgo .By ("verify both workloads are admitted" , func () {
4970+ util .ExpectWorkloadsToBeAdmitted (ctx , k8sClient , wlA , wlB )
4971+ })
4972+
4973+ ginkgo .By ("creating a pending workload in clusterQueueC within its nominal quota (3 CPU) that cannot fit due to fragmented node capacity" , func () {
4974+ wlPending = utiltestingapi .MakeWorkload ("wl-pending" , ns .Name ).
4975+ Priority (1 ).
4976+ PodSets (* utiltestingapi .MakePodSet ("worker" , 1 ).
4977+ PreferredTopologyRequest (utiltesting .DefaultBlockTopologyLevel ).
4978+ Obj ()).
4979+ Queue (kueue .LocalQueueName (localQueueC .Name )).Request (corev1 .ResourceCPU , "3" ).Obj ()
4980+ util .MustCreate (ctx , k8sClient , wlPending )
4981+ util .ExpectWorkloadsToBePending (ctx , k8sClient , wlPending )
4982+ })
4983+
4984+ ginkgo .By ("creating a high priority preemptor in clusterQueue requesting 2 pods of 4 CPU (borrowing from cohort)" , func () {
4985+ preemptor = utiltestingapi .MakeWorkload ("preemptor" , ns .Name ).
4986+ Priority (3 ).
4987+ PodSets (* utiltestingapi .MakePodSet ("worker" , 2 ).
4988+ PreferredTopologyRequest (utiltesting .DefaultBlockTopologyLevel ).
4989+ Obj ()).
4990+ Queue (kueue .LocalQueueName (localQueue .Name )).Request (corev1 .ResourceCPU , "4" ).Obj ()
4991+ util .MustCreate (ctx , k8sClient , preemptor )
4992+ })
4993+
4994+ ginkgo .By ("verifying both wl-a and wl-b are marked for preemption" , func () {
4995+ util .ExpectWorkloadsToBePreempted (ctx , k8sClient , wlA , wlB )
4996+ })
4997+
4998+ ginkgo .By ("finishing eviction for wl-a only" , func () {
4999+ util .FinishEvictionForWorkloads (ctx , k8sClient , wlA )
5000+ })
5001+
5002+ ginkgo .By ("ensuring the pending workload wl-pending in clusterQueueC is not admitted in the interim across scheduling cycles" , func () {
5003+ gomega .Consistently (func (g gomega.Gomega ) {
5004+ g .Expect (k8sClient .Get (ctx , client .ObjectKeyFromObject (wlPending ), wlPending )).To (gomega .Succeed ())
5005+ g .Expect (workload .HasQuotaReservation (wlPending )).To (gomega .BeFalse ())
5006+ }, util .ConsistentDuration , util .ShortInterval ).Should (gomega .Succeed ())
5007+ })
5008+
5009+ ginkgo .By ("finishing eviction for wl-b" , func () {
5010+ util .FinishEvictionForWorkloads (ctx , k8sClient , wlB )
5011+ })
5012+
5013+ ginkgo .By ("verifying preemptor is admitted and wl-pending remains pending with available quota but fragmented topology" , func () {
5014+ util .ExpectWorkloadsToBeAdmitted (ctx , k8sClient , preemptor )
5015+ util .ExpectWorkloadsToBePending (ctx , k8sClient , wlPending )
5016+
5017+ gomega .Eventually (func (g gomega.Gomega ) {
5018+ g .Expect (k8sClient .Get (ctx , client .ObjectKeyFromObject (wlPending ), wlPending )).To (gomega .Succeed ())
5019+ cond := apimeta .FindStatusCondition (wlPending .Status .Conditions , kueue .WorkloadQuotaReserved )
5020+ g .Expect (cond ).ToNot (gomega .BeNil ())
5021+ g .Expect (cond .Status ).To (gomega .Equal (metav1 .ConditionFalse ))
5022+ g .Expect (cond .Reason ).To (gomega .Equal (kueue .WorkloadQuotaReservedReasonWaitingForQuota ))
5023+ g .Expect (cond .Message ).To (gomega .ContainSubstring (`topology "default" doesn't allow to fit any of 1 pod(s)` ))
5024+ }, util .Timeout , util .Interval ).Should (gomega .Succeed ())
5025+ })
5026+ })
5027+ })
5028+
48065029 ginkgo .When ("Node structure is mutated during test cases" , func () {
48075030 var (
48085031 nodes []corev1.Node
0 commit comments