sigs.k8s.io/kueue@v0.6.2/pkg/queue/cluster_queue_impl_test.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package queue 18 19 import ( 20 "context" 21 "testing" 22 "time" 23 24 "github.com/google/go-cmp/cmp" 25 "github.com/google/go-cmp/cmp/cmpopts" 26 corev1 "k8s.io/api/core/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/labels" 29 testingclock "k8s.io/utils/clock/testing" 30 "k8s.io/utils/ptr" 31 32 config "sigs.k8s.io/kueue/apis/config/v1beta1" 33 kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1" 34 utiltesting "sigs.k8s.io/kueue/pkg/util/testing" 35 "sigs.k8s.io/kueue/pkg/workload" 36 ) 37 38 const ( 39 defaultNamespace = "default" 40 ) 41 42 var ( 43 defaultQueueOrderingFunc = queueOrderingFunc(workload.Ordering{ 44 PodsReadyRequeuingTimestamp: config.EvictionTimestamp, 45 }) 46 ) 47 48 func Test_PushOrUpdate(t *testing.T) { 49 now := time.Now() 50 minuteLater := now.Add(time.Minute) 51 fakeClock := testingclock.NewFakeClock(now) 52 cmpOpts := []cmp.Option{ 53 cmpopts.EquateEmpty(), 54 cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime"), 55 } 56 wlBase := utiltesting.MakeWorkload("workload-1", defaultNamespace).Clone() 57 58 cases := map[string]struct { 59 workload *utiltesting.WorkloadWrapper 60 wantWorkload *workload.Info 61 wantInAdmissibleWorkloads map[string]*workload.Info 62 }{ 63 "workload doesn't have re-queue state": { 64 workload: wlBase.Clone(), 65 wantWorkload: workload.NewInfo(wlBase.Clone().ResourceVersion("1").Obj()), 66 }, 67 "workload is still under the backoff waiting time": { 68 workload: wlBase.Clone(). 69 RequeueState(ptr.To[int32](10), ptr.To(metav1.NewTime(minuteLater))). 70 Condition(metav1.Condition{ 71 Type: kueue.WorkloadEvicted, 72 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 73 Status: metav1.ConditionTrue, 74 }), 75 wantInAdmissibleWorkloads: map[string]*workload.Info{ 76 "default/workload-1": workload.NewInfo(wlBase.Clone(). 77 ResourceVersion("1"). 78 RequeueState(ptr.To[int32](10), ptr.To(metav1.NewTime(minuteLater))). 79 Condition(metav1.Condition{ 80 Type: kueue.WorkloadEvicted, 81 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 82 Status: metav1.ConditionTrue, 83 }). 84 Obj()), 85 }, 86 }, 87 } 88 for name, tc := range cases { 89 t.Run(name, func(t *testing.T) { 90 cq := newClusterQueueImpl(keyFunc, defaultQueueOrderingFunc, fakeClock) 91 92 if cq.Pending() != 0 { 93 t.Error("ClusterQueue should be empty") 94 } 95 cq.PushOrUpdate(workload.NewInfo(tc.workload.Clone().Obj())) 96 if cq.Pending() != 1 { 97 t.Error("ClusterQueue should have one workload") 98 } 99 100 // Just used to validate the update operation. 101 updatedWl := tc.workload.Clone().ResourceVersion("1").Obj() 102 cq.PushOrUpdate(workload.NewInfo(updatedWl)) 103 newWl := cq.Pop() 104 if newWl != nil && cq.Pending() != 0 { 105 t.Error("failed to update a workload in ClusterQueue") 106 } 107 if diff := cmp.Diff(tc.wantWorkload, newWl, cmpOpts...); len(diff) != 0 { 108 t.Errorf("Unexpectd workloads in heap (-want,+got):\n%s", diff) 109 } 110 if diff := cmp.Diff(tc.wantInAdmissibleWorkloads, cq.inadmissibleWorkloads, cmpOpts...); len(diff) != 0 { 111 t.Errorf("Unexpectd inadmissibleWorkloads (-want,+got):\n%s", diff) 112 } 113 }) 114 } 115 } 116 117 func Test_Pop(t *testing.T) { 118 now := time.Now() 119 cq := newClusterQueueImpl(keyFunc, defaultQueueOrderingFunc, testingclock.NewFakeClock(now)) 120 wl1 := workload.NewInfo(utiltesting.MakeWorkload("workload-1", defaultNamespace).Creation(now).Obj()) 121 wl2 := workload.NewInfo(utiltesting.MakeWorkload("workload-2", defaultNamespace).Creation(now.Add(time.Second)).Obj()) 122 if cq.Pop() != nil { 123 t.Error("ClusterQueue should be empty") 124 } 125 cq.PushOrUpdate(wl1) 126 cq.PushOrUpdate(wl2) 127 newWl := cq.Pop() 128 if newWl == nil || newWl.Obj.Name != "workload-1" { 129 t.Error("failed to Pop workload") 130 } 131 newWl = cq.Pop() 132 if newWl == nil || newWl.Obj.Name != "workload-2" { 133 t.Error("failed to Pop workload") 134 } 135 if cq.Pop() != nil { 136 t.Error("ClusterQueue should be empty") 137 } 138 } 139 140 func Test_Delete(t *testing.T) { 141 cq := newClusterQueueImpl(keyFunc, defaultQueueOrderingFunc, testingclock.NewFakeClock(time.Now())) 142 wl1 := utiltesting.MakeWorkload("workload-1", defaultNamespace).Obj() 143 wl2 := utiltesting.MakeWorkload("workload-2", defaultNamespace).Obj() 144 cq.PushOrUpdate(workload.NewInfo(wl1)) 145 cq.PushOrUpdate(workload.NewInfo(wl2)) 146 if cq.Pending() != 2 { 147 t.Error("ClusterQueue should have two workload") 148 } 149 cq.Delete(wl1) 150 if cq.Pending() != 1 { 151 t.Error("ClusterQueue should have only one workload") 152 } 153 // Change workload item, ClusterQueue.Delete should only care about the namespace and name. 154 wl2.Spec = kueue.WorkloadSpec{QueueName: "default"} 155 cq.Delete(wl2) 156 if cq.Pending() != 0 { 157 t.Error("ClusterQueue should have be empty") 158 } 159 } 160 161 func Test_Info(t *testing.T) { 162 cq := newClusterQueueImpl(keyFunc, defaultQueueOrderingFunc, testingclock.NewFakeClock(time.Now())) 163 wl := utiltesting.MakeWorkload("workload-1", defaultNamespace).Obj() 164 if info := cq.Info(keyFunc(workload.NewInfo(wl))); info != nil { 165 t.Error("workload doesn't exist") 166 } 167 cq.PushOrUpdate(workload.NewInfo(wl)) 168 if info := cq.Info(keyFunc(workload.NewInfo(wl))); info == nil { 169 t.Error("expected workload to exist") 170 } 171 } 172 173 func Test_AddFromLocalQueue(t *testing.T) { 174 cq := newClusterQueueImpl(keyFunc, defaultQueueOrderingFunc, testingclock.NewFakeClock(time.Now())) 175 wl := utiltesting.MakeWorkload("workload-1", defaultNamespace).Obj() 176 queue := &LocalQueue{ 177 items: map[string]*workload.Info{ 178 wl.Name: workload.NewInfo(wl), 179 }, 180 } 181 cq.PushOrUpdate(workload.NewInfo(wl)) 182 if added := cq.AddFromLocalQueue(queue); added { 183 t.Error("expected workload not to be added") 184 } 185 cq.Delete(wl) 186 if added := cq.AddFromLocalQueue(queue); !added { 187 t.Error("workload should be added to the ClusterQueue") 188 } 189 } 190 191 func Test_DeleteFromLocalQueue(t *testing.T) { 192 cq := newClusterQueueImpl(keyFunc, defaultQueueOrderingFunc, testingclock.NewFakeClock(time.Now())) 193 q := utiltesting.MakeLocalQueue("foo", "").ClusterQueue("cq").Obj() 194 qImpl := newLocalQueue(q) 195 wl1 := utiltesting.MakeWorkload("wl1", "").Queue(q.Name).Obj() 196 wl2 := utiltesting.MakeWorkload("wl2", "").Queue(q.Name).Obj() 197 wl3 := utiltesting.MakeWorkload("wl3", "").Queue(q.Name).Obj() 198 wl4 := utiltesting.MakeWorkload("wl4", "").Queue(q.Name).Obj() 199 admissibleworkloads := []*kueue.Workload{wl1, wl2} 200 inadmissibleWorkloads := []*kueue.Workload{wl3, wl4} 201 202 for _, w := range admissibleworkloads { 203 wInfo := workload.NewInfo(w) 204 cq.PushOrUpdate(wInfo) 205 qImpl.AddOrUpdate(wInfo) 206 } 207 208 for _, w := range inadmissibleWorkloads { 209 wInfo := workload.NewInfo(w) 210 cq.requeueIfNotPresent(wInfo, false) 211 qImpl.AddOrUpdate(wInfo) 212 } 213 214 wantPending := len(admissibleworkloads) + len(inadmissibleWorkloads) 215 if pending := cq.Pending(); pending != wantPending { 216 t.Errorf("clusterQueue's workload number not right, want %v, got %v", wantPending, pending) 217 } 218 if len(cq.inadmissibleWorkloads) != len(inadmissibleWorkloads) { 219 t.Errorf("clusterQueue's workload number in inadmissibleWorkloads not right, want %v, got %v", len(inadmissibleWorkloads), len(cq.inadmissibleWorkloads)) 220 } 221 222 cq.DeleteFromLocalQueue(qImpl) 223 if cq.Pending() != 0 { 224 t.Error("clusterQueue should be empty") 225 } 226 } 227 228 func TestClusterQueueImpl(t *testing.T) { 229 cl := utiltesting.NewFakeClient( 230 &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns1", Labels: map[string]string{"dep": "eng"}}}, 231 &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns2", Labels: map[string]string{"dep": "sales"}}}, 232 &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns3", Labels: map[string]string{"dep": "marketing"}}}, 233 ) 234 235 now := time.Now() 236 minuteLater := now.Add(time.Minute) 237 fakeClock := testingclock.NewFakeClock(now) 238 239 var workloads = []*kueue.Workload{ 240 utiltesting.MakeWorkload("w1", "ns1").Queue("q1").Obj(), 241 utiltesting.MakeWorkload("w2", "ns2").Queue("q2").Obj(), 242 utiltesting.MakeWorkload("w3", "ns3").Queue("q3").Obj(), 243 utiltesting.MakeWorkload("w4-requeue-state", "ns1"). 244 RequeueState(ptr.To[int32](1), ptr.To(metav1.NewTime(minuteLater))). 245 Queue("q1"). 246 Condition(metav1.Condition{ 247 Type: kueue.WorkloadEvicted, 248 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 249 Status: metav1.ConditionTrue, 250 }). 251 Obj(), 252 } 253 var updatedWorkloads = make([]*kueue.Workload, len(workloads)) 254 255 updatedWorkloads[0] = workloads[0].DeepCopy() 256 updatedWorkloads[0].Spec.QueueName = "q2" 257 updatedWorkloads[1] = workloads[1].DeepCopy() 258 updatedWorkloads[1].Spec.QueueName = "q1" 259 260 tests := map[string]struct { 261 workloadsToAdd []*kueue.Workload 262 inadmissibleWorkloadsToRequeue []*workload.Info 263 admissibleWorkloadsToRequeue []*workload.Info 264 workloadsToUpdate []*kueue.Workload 265 workloadsToDelete []*kueue.Workload 266 queueInadmissibleWorkloads bool 267 wantActiveWorkloads []string 268 wantPending int 269 wantInadmissibleWorkloadsRequeued bool 270 }{ 271 "add, update, delete workload": { 272 workloadsToAdd: []*kueue.Workload{workloads[0], workloads[1], workloads[3]}, 273 inadmissibleWorkloadsToRequeue: []*workload.Info{}, 274 workloadsToUpdate: []*kueue.Workload{updatedWorkloads[0]}, 275 workloadsToDelete: []*kueue.Workload{workloads[0]}, 276 wantActiveWorkloads: []string{workload.Key(workloads[1])}, 277 wantPending: 2, 278 }, 279 "re-queue inadmissible workload; workloads with requeueState can't re-queue": { 280 workloadsToAdd: []*kueue.Workload{workloads[0]}, 281 inadmissibleWorkloadsToRequeue: []*workload.Info{workload.NewInfo(workloads[1]), workload.NewInfo(workloads[3])}, 282 wantActiveWorkloads: []string{workload.Key(workloads[0])}, 283 wantPending: 3, 284 }, 285 "re-queue admissible workload that was inadmissible": { 286 workloadsToAdd: []*kueue.Workload{workloads[0]}, 287 inadmissibleWorkloadsToRequeue: []*workload.Info{workload.NewInfo(workloads[1]), workload.NewInfo(workloads[3])}, 288 admissibleWorkloadsToRequeue: []*workload.Info{workload.NewInfo(workloads[1]), workload.NewInfo(workloads[3])}, 289 wantActiveWorkloads: []string{workload.Key(workloads[0]), workload.Key(workloads[1])}, 290 wantPending: 3, 291 }, 292 "re-queue inadmissible workload and flush": { 293 workloadsToAdd: []*kueue.Workload{workloads[0]}, 294 inadmissibleWorkloadsToRequeue: []*workload.Info{workload.NewInfo(workloads[1]), workload.NewInfo(workloads[3])}, 295 queueInadmissibleWorkloads: true, 296 wantActiveWorkloads: []string{workload.Key(workloads[0]), workload.Key(workloads[1])}, 297 wantPending: 3, 298 wantInadmissibleWorkloadsRequeued: true, 299 }, 300 "avoid re-queueing inadmissible workloads not matching namespace selector": { 301 workloadsToAdd: []*kueue.Workload{workloads[0]}, 302 inadmissibleWorkloadsToRequeue: []*workload.Info{workload.NewInfo(workloads[2])}, 303 queueInadmissibleWorkloads: true, 304 wantActiveWorkloads: []string{workload.Key(workloads[0])}, 305 wantPending: 2, 306 }, 307 "update inadmissible workload": { 308 workloadsToAdd: []*kueue.Workload{workloads[0]}, 309 inadmissibleWorkloadsToRequeue: []*workload.Info{workload.NewInfo(workloads[1])}, 310 workloadsToUpdate: []*kueue.Workload{updatedWorkloads[1]}, 311 wantActiveWorkloads: []string{workload.Key(workloads[0]), workload.Key(workloads[1])}, 312 wantPending: 2, 313 }, 314 "delete inadmissible workload": { 315 workloadsToAdd: []*kueue.Workload{workloads[0]}, 316 inadmissibleWorkloadsToRequeue: []*workload.Info{workload.NewInfo(workloads[1])}, 317 workloadsToDelete: []*kueue.Workload{workloads[1]}, 318 queueInadmissibleWorkloads: true, 319 wantActiveWorkloads: []string{workload.Key(workloads[0])}, 320 wantPending: 1, 321 }, 322 "update inadmissible workload without changes": { 323 inadmissibleWorkloadsToRequeue: []*workload.Info{workload.NewInfo(workloads[1])}, 324 workloadsToUpdate: []*kueue.Workload{workloads[1]}, 325 wantPending: 1, 326 }, 327 "requeue inadmissible workload twice": { 328 inadmissibleWorkloadsToRequeue: []*workload.Info{workload.NewInfo(workloads[1]), workload.NewInfo(workloads[1])}, 329 wantPending: 1, 330 }, 331 "update reclaimable pods in inadmissible": { 332 inadmissibleWorkloadsToRequeue: []*workload.Info{ 333 workload.NewInfo(utiltesting.MakeWorkload("w", "").PodSets(*utiltesting.MakePodSet("main", 1).Request(corev1.ResourceCPU, "1").Obj()).Obj()), 334 }, 335 workloadsToUpdate: []*kueue.Workload{ 336 utiltesting.MakeWorkload("w", "").PodSets(*utiltesting.MakePodSet("main", 2).Request(corev1.ResourceCPU, "1").Obj()). 337 ReclaimablePods(kueue.ReclaimablePod{Name: "main", Count: 1}). 338 Obj(), 339 }, 340 wantActiveWorkloads: []string{"/w"}, 341 wantPending: 1, 342 }, 343 } 344 345 for name, test := range tests { 346 t.Run(name, func(t *testing.T) { 347 cq := newClusterQueueImpl(keyFunc, defaultQueueOrderingFunc, fakeClock) 348 err := cq.Update(utiltesting.MakeClusterQueue("cq"). 349 NamespaceSelector(&metav1.LabelSelector{ 350 MatchExpressions: []metav1.LabelSelectorRequirement{ 351 { 352 Key: "dep", 353 Operator: metav1.LabelSelectorOpIn, 354 Values: []string{"eng", "sales"}, 355 }, 356 }, 357 }).Obj()) 358 if err != nil { 359 t.Fatalf("Failed updating clusterQueue: %v", err) 360 } 361 362 for _, w := range test.workloadsToAdd { 363 cq.PushOrUpdate(workload.NewInfo(w)) 364 } 365 366 for _, w := range test.inadmissibleWorkloadsToRequeue { 367 cq.requeueIfNotPresent(w, false) 368 } 369 for _, w := range test.admissibleWorkloadsToRequeue { 370 cq.requeueIfNotPresent(w, true) 371 } 372 373 for _, w := range test.workloadsToUpdate { 374 cq.PushOrUpdate(workload.NewInfo(w)) 375 } 376 377 for _, w := range test.workloadsToDelete { 378 cq.Delete(w) 379 } 380 381 if test.queueInadmissibleWorkloads { 382 if diff := cmp.Diff(test.wantInadmissibleWorkloadsRequeued, 383 cq.QueueInadmissibleWorkloads(context.Background(), cl)); diff != "" { 384 t.Errorf("Unexpected requeuing of inadmissible workloads (-want,+got):\n%s", diff) 385 } 386 } 387 388 gotWorkloads, _ := cq.Dump() 389 if diff := cmp.Diff(test.wantActiveWorkloads, gotWorkloads, cmpDump...); diff != "" { 390 t.Errorf("Unexpected active workloads in cluster foo (-want,+got):\n%s", diff) 391 } 392 if got := cq.Pending(); got != test.wantPending { 393 t.Errorf("Got %d pending workloads, want %d", got, test.wantPending) 394 } 395 }) 396 } 397 } 398 399 func TestQueueInadmissibleWorkloadsDuringScheduling(t *testing.T) { 400 cq := newClusterQueueImpl(keyFunc, defaultQueueOrderingFunc, testingclock.NewFakeClock(time.Now())) 401 cq.namespaceSelector = labels.Everything() 402 wl := utiltesting.MakeWorkload("workload-1", defaultNamespace).Obj() 403 cl := utiltesting.NewFakeClient( 404 wl, 405 &corev1.Namespace{ 406 ObjectMeta: metav1.ObjectMeta{Name: defaultNamespace}, 407 }, 408 ) 409 ctx := context.Background() 410 cq.PushOrUpdate(workload.NewInfo(wl)) 411 412 wantActiveWorkloads := []string{workload.Key(wl)} 413 414 activeWorkloads, _ := cq.Dump() 415 if diff := cmp.Diff(wantActiveWorkloads, activeWorkloads, cmpDump...); diff != "" { 416 t.Errorf("Unexpected active workloads before events (-want,+got):\n%s", diff) 417 } 418 419 // Simulate requeuing during scheduling attempt. 420 head := cq.Pop() 421 cq.QueueInadmissibleWorkloads(ctx, cl) 422 cq.requeueIfNotPresent(head, false) 423 424 activeWorkloads, _ = cq.Dump() 425 wantActiveWorkloads = []string{workload.Key(wl)} 426 if diff := cmp.Diff(wantActiveWorkloads, activeWorkloads, cmpDump...); diff != "" { 427 t.Errorf("Unexpected active workloads after scheduling with requeuing (-want,+got):\n%s", diff) 428 } 429 430 // Simulating scheduling again without requeuing. 431 head = cq.Pop() 432 cq.requeueIfNotPresent(head, false) 433 activeWorkloads, _ = cq.Dump() 434 wantActiveWorkloads = nil 435 if diff := cmp.Diff(wantActiveWorkloads, activeWorkloads, cmpDump...); diff != "" { 436 t.Errorf("Unexpected active workloads after scheduling (-want,+got):\n%s", diff) 437 } 438 } 439 440 func TestBackoffWaitingTimeExpired(t *testing.T) { 441 now := time.Now() 442 minuteLater := now.Add(time.Minute) 443 minuteAgo := now.Add(-time.Minute) 444 fakeClock := testingclock.NewFakeClock(now) 445 446 cases := map[string]struct { 447 workloadInfo *workload.Info 448 want bool 449 }{ 450 "workload doesn't have requeueState": { 451 workloadInfo: workload.NewInfo(utiltesting.MakeWorkload("wl", "ns").Obj()), 452 want: true, 453 }, 454 "workload doesn't have an evicted condition with reason=PodsReadyTimeout": { 455 workloadInfo: workload.NewInfo(utiltesting.MakeWorkload("wl", "ns"). 456 RequeueState(ptr.To[int32](10), nil).Obj()), 457 want: true, 458 }, 459 "now already has exceeded requeueAt": { 460 workloadInfo: workload.NewInfo(utiltesting.MakeWorkload("wl", "ns"). 461 RequeueState(ptr.To[int32](10), ptr.To(metav1.NewTime(minuteAgo))). 462 Condition(metav1.Condition{ 463 Type: kueue.WorkloadEvicted, 464 Status: metav1.ConditionTrue, 465 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 466 }).Obj()), 467 want: true, 468 }, 469 "now hasn't yet exceeded requeueAt": { 470 workloadInfo: workload.NewInfo(utiltesting.MakeWorkload("wl", "ns"). 471 RequeueState(ptr.To[int32](10), ptr.To(metav1.NewTime(minuteLater))). 472 Condition(metav1.Condition{ 473 Type: kueue.WorkloadEvicted, 474 Status: metav1.ConditionTrue, 475 Reason: kueue.WorkloadEvictedByPodsReadyTimeout, 476 }).Obj()), 477 want: false, 478 }, 479 } 480 for name, tc := range cases { 481 t.Run(name, func(t *testing.T) { 482 cq := newClusterQueueImpl(keyFunc, defaultQueueOrderingFunc, fakeClock) 483 got := cq.backoffWaitingTimeExpired(tc.workloadInfo) 484 if tc.want != got { 485 t.Errorf("Unexpected result from backoffWaitingTimeExpired\nwant: %v\ngot: %v\n", tc.want, got) 486 } 487 }) 488 } 489 }