sigs.k8s.io/kueue@v0.6.2/pkg/cache/clusterqueue_test.go (about)

     1  /*
     2  Copyright 2023 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 cache
    18  
    19  import (
    20  	"testing"
    21  
    22  	"github.com/google/go-cmp/cmp"
    23  	corev1 "k8s.io/api/core/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  
    26  	kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
    27  	"sigs.k8s.io/kueue/pkg/features"
    28  	"sigs.k8s.io/kueue/pkg/metrics"
    29  	utiltesting "sigs.k8s.io/kueue/pkg/util/testing"
    30  )
    31  
    32  func TestClusterQueueUpdateWithFlavors(t *testing.T) {
    33  	rf := utiltesting.MakeResourceFlavor("x86").Obj()
    34  	cq := utiltesting.MakeClusterQueue("cq").
    35  		ResourceGroup(*utiltesting.MakeFlavorQuotas("x86").Resource("cpu", "5").Obj()).
    36  		Obj()
    37  
    38  	testcases := []struct {
    39  		name       string
    40  		curStatus  metrics.ClusterQueueStatus
    41  		flavors    map[kueue.ResourceFlavorReference]*kueue.ResourceFlavor
    42  		wantStatus metrics.ClusterQueueStatus
    43  	}{
    44  		{
    45  			name:      "Pending clusterQueue updated existent flavors",
    46  			curStatus: pending,
    47  			flavors: map[kueue.ResourceFlavorReference]*kueue.ResourceFlavor{
    48  				kueue.ResourceFlavorReference(rf.Name): rf,
    49  			},
    50  			wantStatus: active,
    51  		},
    52  		{
    53  			name:       "Active clusterQueue updated with not found flavors",
    54  			curStatus:  active,
    55  			flavors:    map[kueue.ResourceFlavorReference]*kueue.ResourceFlavor{},
    56  			wantStatus: pending,
    57  		},
    58  		{
    59  			name:      "Terminating clusterQueue updated with existent flavors",
    60  			curStatus: terminating,
    61  			flavors: map[kueue.ResourceFlavorReference]*kueue.ResourceFlavor{
    62  				kueue.ResourceFlavorReference(rf.Name): rf,
    63  			},
    64  			wantStatus: terminating,
    65  		},
    66  		{
    67  			name:       "Terminating clusterQueue updated with not found flavors",
    68  			curStatus:  terminating,
    69  			wantStatus: terminating,
    70  		},
    71  	}
    72  
    73  	for _, tc := range testcases {
    74  		t.Run(tc.name, func(t *testing.T) {
    75  			cache := New(utiltesting.NewFakeClient())
    76  			cq, err := cache.newClusterQueue(cq)
    77  			if err != nil {
    78  				t.Fatalf("failed to new clusterQueue %v", err)
    79  			}
    80  
    81  			cq.Status = tc.curStatus
    82  			cq.UpdateWithFlavors(tc.flavors)
    83  
    84  			if cq.Status != tc.wantStatus {
    85  				t.Fatalf("got different status, want: %v, got: %v", tc.wantStatus, cq.Status)
    86  			}
    87  		})
    88  	}
    89  }
    90  
    91  func TestFitInCohort(t *testing.T) {
    92  	cases := map[string]struct {
    93  		request            FlavorResourceQuantities
    94  		wantFit            bool
    95  		cq                 *ClusterQueue
    96  		enableLendingLimit bool
    97  	}{
    98  		"full cohort, empty request": {
    99  			request: FlavorResourceQuantities{},
   100  			wantFit: true,
   101  			cq: &ClusterQueue{
   102  				Name: "CQ",
   103  				Cohort: &Cohort{
   104  					Name: "C",
   105  					RequestableResources: FlavorResourceQuantities{
   106  						"f1": map[corev1.ResourceName]int64{
   107  							corev1.ResourceCPU:    5,
   108  							corev1.ResourceMemory: 5,
   109  						},
   110  						"f2": map[corev1.ResourceName]int64{
   111  							corev1.ResourceCPU:    5,
   112  							corev1.ResourceMemory: 5,
   113  						},
   114  					},
   115  					Usage: FlavorResourceQuantities{
   116  						"f1": map[corev1.ResourceName]int64{
   117  							corev1.ResourceCPU:    5,
   118  							corev1.ResourceMemory: 5,
   119  						},
   120  						"f2": map[corev1.ResourceName]int64{
   121  							corev1.ResourceCPU:    5,
   122  							corev1.ResourceMemory: 5,
   123  						},
   124  					},
   125  				},
   126  				ResourceGroups: nil,
   127  			},
   128  		},
   129  		"can fit": {
   130  			request: FlavorResourceQuantities{
   131  				"f2": map[corev1.ResourceName]int64{
   132  					corev1.ResourceCPU:    1,
   133  					corev1.ResourceMemory: 1,
   134  				},
   135  			},
   136  			wantFit: true,
   137  			cq: &ClusterQueue{
   138  				Name: "CQ",
   139  				Cohort: &Cohort{
   140  					Name: "C",
   141  					RequestableResources: FlavorResourceQuantities{
   142  						"f1": map[corev1.ResourceName]int64{
   143  							corev1.ResourceCPU:    5,
   144  							corev1.ResourceMemory: 5,
   145  						},
   146  						"f2": map[corev1.ResourceName]int64{
   147  							corev1.ResourceCPU:    5,
   148  							corev1.ResourceMemory: 5,
   149  						},
   150  					},
   151  					Usage: FlavorResourceQuantities{
   152  						"f1": map[corev1.ResourceName]int64{
   153  							corev1.ResourceCPU:    5,
   154  							corev1.ResourceMemory: 5,
   155  						},
   156  						"f2": map[corev1.ResourceName]int64{
   157  							corev1.ResourceCPU:    4,
   158  							corev1.ResourceMemory: 4,
   159  						},
   160  					},
   161  				},
   162  				ResourceGroups: nil,
   163  			},
   164  		},
   165  		"full cohort, none fit": {
   166  			request: FlavorResourceQuantities{
   167  				"f1": map[corev1.ResourceName]int64{
   168  					corev1.ResourceCPU:    1,
   169  					corev1.ResourceMemory: 1,
   170  				},
   171  				"f2": map[corev1.ResourceName]int64{
   172  					corev1.ResourceCPU:    1,
   173  					corev1.ResourceMemory: 1,
   174  				},
   175  			},
   176  			wantFit: false,
   177  			cq: &ClusterQueue{
   178  				Name: "CQ",
   179  				Cohort: &Cohort{
   180  					Name: "C",
   181  					RequestableResources: FlavorResourceQuantities{
   182  						"f1": map[corev1.ResourceName]int64{
   183  							corev1.ResourceCPU:    5,
   184  							corev1.ResourceMemory: 5,
   185  						},
   186  						"f2": map[corev1.ResourceName]int64{
   187  							corev1.ResourceCPU:    5,
   188  							corev1.ResourceMemory: 5,
   189  						},
   190  					},
   191  					Usage: FlavorResourceQuantities{
   192  						"f1": map[corev1.ResourceName]int64{
   193  							corev1.ResourceCPU:    5,
   194  							corev1.ResourceMemory: 5,
   195  						},
   196  						"f2": map[corev1.ResourceName]int64{
   197  							corev1.ResourceCPU:    5,
   198  							corev1.ResourceMemory: 5,
   199  						},
   200  					},
   201  				},
   202  				ResourceGroups: nil,
   203  			},
   204  		},
   205  		"one cannot fit": {
   206  			request: FlavorResourceQuantities{
   207  				"f1": map[corev1.ResourceName]int64{
   208  					corev1.ResourceCPU:    1,
   209  					corev1.ResourceMemory: 1,
   210  				},
   211  				"f2": map[corev1.ResourceName]int64{
   212  					corev1.ResourceCPU:    2,
   213  					corev1.ResourceMemory: 1,
   214  				},
   215  			},
   216  			wantFit: false,
   217  			cq: &ClusterQueue{
   218  				Name: "CQ",
   219  				Cohort: &Cohort{
   220  					Name: "C",
   221  					RequestableResources: FlavorResourceQuantities{
   222  						"f1": map[corev1.ResourceName]int64{
   223  							corev1.ResourceCPU:    5,
   224  							corev1.ResourceMemory: 5,
   225  						},
   226  						"f2": map[corev1.ResourceName]int64{
   227  							corev1.ResourceCPU:    5,
   228  							corev1.ResourceMemory: 5,
   229  						},
   230  					},
   231  					Usage: FlavorResourceQuantities{
   232  						"f1": map[corev1.ResourceName]int64{
   233  							corev1.ResourceCPU:    4,
   234  							corev1.ResourceMemory: 4,
   235  						},
   236  						"f2": map[corev1.ResourceName]int64{
   237  							corev1.ResourceCPU:    4,
   238  							corev1.ResourceMemory: 4,
   239  						},
   240  					},
   241  				},
   242  				ResourceGroups: nil,
   243  			},
   244  		},
   245  		"missing flavor": {
   246  			request: FlavorResourceQuantities{
   247  				"f2": map[corev1.ResourceName]int64{
   248  					corev1.ResourceCPU:    1,
   249  					corev1.ResourceMemory: 1,
   250  				},
   251  			},
   252  			wantFit: false,
   253  			cq: &ClusterQueue{
   254  				Name: "CQ",
   255  				Cohort: &Cohort{
   256  					Name: "C",
   257  					RequestableResources: FlavorResourceQuantities{
   258  						"f1": map[corev1.ResourceName]int64{
   259  							corev1.ResourceCPU:    5,
   260  							corev1.ResourceMemory: 5,
   261  						},
   262  					},
   263  					Usage: FlavorResourceQuantities{
   264  						"f1": map[corev1.ResourceName]int64{
   265  							corev1.ResourceCPU:    5,
   266  							corev1.ResourceMemory: 5,
   267  						},
   268  					},
   269  				},
   270  				ResourceGroups: nil,
   271  			},
   272  		},
   273  		"missing resource": {
   274  			request: FlavorResourceQuantities{
   275  				"f1": map[corev1.ResourceName]int64{
   276  					corev1.ResourceCPU:    1,
   277  					corev1.ResourceMemory: 1,
   278  				},
   279  			},
   280  			wantFit: false,
   281  			cq: &ClusterQueue{
   282  				Name: "CQ",
   283  				Cohort: &Cohort{
   284  					Name: "C",
   285  					RequestableResources: FlavorResourceQuantities{
   286  						"f1": map[corev1.ResourceName]int64{
   287  							corev1.ResourceCPU: 5,
   288  						},
   289  					},
   290  					Usage: FlavorResourceQuantities{
   291  						"f1": map[corev1.ResourceName]int64{
   292  							corev1.ResourceCPU: 3,
   293  						},
   294  					},
   295  				},
   296  				ResourceGroups: nil,
   297  			},
   298  		},
   299  		"lendingLimit enabled can't fit": {
   300  			request: FlavorResourceQuantities{
   301  				"f1": map[corev1.ResourceName]int64{
   302  					corev1.ResourceCPU: 3,
   303  				},
   304  			},
   305  			wantFit: false,
   306  			cq: &ClusterQueue{
   307  				Name: "CQ-A",
   308  				Cohort: &Cohort{
   309  					Name: "C",
   310  					RequestableResources: FlavorResourceQuantities{
   311  						"f1": map[corev1.ResourceName]int64{
   312  							// CQ-A has 2 nominal cpu, CQ-B has 3 nominal cpu and 2 lendingLimit,
   313  							// so when lendingLimit enabled, the cohort's RequestableResources is 4 cpu.
   314  							corev1.ResourceCPU: 4,
   315  						},
   316  					},
   317  					Usage: FlavorResourceQuantities{
   318  						"f1": map[corev1.ResourceName]int64{
   319  							corev1.ResourceCPU: 2,
   320  						},
   321  					},
   322  				},
   323  				GuaranteedQuota: FlavorResourceQuantities{
   324  					"f1": {
   325  						corev1.ResourceCPU: 0,
   326  					},
   327  				},
   328  			},
   329  			enableLendingLimit: true,
   330  		},
   331  		"lendingLimit enabled can fit": {
   332  			request: FlavorResourceQuantities{
   333  				"f1": map[corev1.ResourceName]int64{
   334  					corev1.ResourceCPU: 3,
   335  				},
   336  			},
   337  			wantFit: true,
   338  			cq: &ClusterQueue{
   339  				Name: "CQ-A",
   340  				Cohort: &Cohort{
   341  					Name: "C",
   342  					RequestableResources: FlavorResourceQuantities{
   343  						"f1": map[corev1.ResourceName]int64{
   344  							// CQ-A has 2 nominal cpu, CQ-B has 3 nominal cpu and 2 lendingLimit,
   345  							// so when lendingLimit enabled, the cohort's RequestableResources is 4 cpu.
   346  							corev1.ResourceCPU: 4,
   347  						},
   348  					},
   349  					Usage: FlavorResourceQuantities{
   350  						"f1": map[corev1.ResourceName]int64{
   351  							// CQ-B has admitted a workload with 2 cpus, but with 1 GuaranteedQuota,
   352  							// so when lendingLimit enabled, Cohort.Usage should be 2 - 1 = 1.
   353  							corev1.ResourceCPU: 1,
   354  						},
   355  					},
   356  				},
   357  				GuaranteedQuota: FlavorResourceQuantities{
   358  					"f1": {
   359  						corev1.ResourceCPU: 2,
   360  					},
   361  				},
   362  			},
   363  			enableLendingLimit: true,
   364  		},
   365  	}
   366  
   367  	for name, tc := range cases {
   368  		t.Run(name, func(t *testing.T) {
   369  			defer features.SetFeatureGateDuringTest(t, features.LendingLimit, tc.enableLendingLimit)()
   370  			got := tc.cq.FitInCohort(tc.request)
   371  			if got != tc.wantFit {
   372  				t.Errorf("Unexpected result, %v", got)
   373  			}
   374  
   375  		})
   376  	}
   377  
   378  }
   379  
   380  func TestClusterQueueUpdate(t *testing.T) {
   381  	resourceFlavors := []*kueue.ResourceFlavor{
   382  		{ObjectMeta: metav1.ObjectMeta{Name: "on-demand"}},
   383  		{ObjectMeta: metav1.ObjectMeta{Name: "spot"}},
   384  	}
   385  	clusterQueue :=
   386  		*utiltesting.MakeClusterQueue("eng-alpha").
   387  			QueueingStrategy(kueue.StrictFIFO).
   388  			Preemption(kueue.ClusterQueuePreemption{
   389  				WithinClusterQueue: kueue.PreemptionPolicyLowerPriority,
   390  			}).
   391  			FlavorFungibility(kueue.FlavorFungibility{
   392  				WhenCanPreempt: kueue.Preempt,
   393  			}).
   394  			ResourceGroup(
   395  				*utiltesting.MakeFlavorQuotas("on-demand").
   396  					Resource(corev1.ResourceCPU, "50", "50").Obj(),
   397  				*utiltesting.MakeFlavorQuotas("spot").
   398  					Resource(corev1.ResourceCPU, "100", "0").Obj(),
   399  			).Obj()
   400  	newClusterQueue :=
   401  		*utiltesting.MakeClusterQueue("eng-alpha").
   402  			QueueingStrategy(kueue.StrictFIFO).
   403  			Preemption(kueue.ClusterQueuePreemption{
   404  				WithinClusterQueue: kueue.PreemptionPolicyLowerPriority,
   405  			}).
   406  			FlavorFungibility(kueue.FlavorFungibility{
   407  				WhenCanPreempt: kueue.Preempt,
   408  			}).
   409  			ResourceGroup(
   410  				*utiltesting.MakeFlavorQuotas("on-demand").
   411  					Resource(corev1.ResourceCPU, "100", "50").Obj(),
   412  				*utiltesting.MakeFlavorQuotas("spot").
   413  					Resource(corev1.ResourceCPU, "100", "0").Obj(),
   414  			).Obj()
   415  	cases := []struct {
   416  		name                         string
   417  		cq                           *kueue.ClusterQueue
   418  		newcq                        *kueue.ClusterQueue
   419  		wantLastAssignmentGeneration int64
   420  	}{
   421  		{
   422  			name:                         "RGs not change",
   423  			cq:                           &clusterQueue,
   424  			newcq:                        clusterQueue.DeepCopy(),
   425  			wantLastAssignmentGeneration: 1,
   426  		},
   427  		{
   428  			name:                         "RGs changed",
   429  			cq:                           &clusterQueue,
   430  			newcq:                        &newClusterQueue,
   431  			wantLastAssignmentGeneration: 2,
   432  		},
   433  	}
   434  	for _, tc := range cases {
   435  		t.Run(tc.name, func(t *testing.T) {
   436  			ctx, _ := utiltesting.ContextWithLog(t)
   437  			clientBuilder := utiltesting.NewClientBuilder().
   438  				WithObjects(
   439  					&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}},
   440  					tc.cq,
   441  				)
   442  			cl := clientBuilder.Build()
   443  			cqCache := New(cl)
   444  			// Workloads are loaded into queues or clusterQueues as we add them.
   445  			for _, rf := range resourceFlavors {
   446  				cqCache.AddOrUpdateResourceFlavor(rf)
   447  			}
   448  			if err := cqCache.AddClusterQueue(ctx, tc.cq); err != nil {
   449  				t.Fatalf("Inserting clusterQueue %s in cache: %v", tc.cq.Name, err)
   450  			}
   451  			if err := cqCache.UpdateClusterQueue(tc.newcq); err != nil {
   452  				t.Fatalf("Updating clusterQueue %s in cache: %v", tc.newcq.Name, err)
   453  			}
   454  			snapshot := cqCache.Snapshot()
   455  			if diff := cmp.Diff(
   456  				tc.wantLastAssignmentGeneration,
   457  				snapshot.ClusterQueues["eng-alpha"].AllocatableResourceGeneration); diff != "" {
   458  				t.Errorf("Unexpected assigned clusterQueues in cache (-want,+got):\n%s", diff)
   459  			}
   460  		})
   461  	}
   462  }