replica_calculator_test.go 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635
  1. /*
  2. Copyright 2016 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package podautoscaler
  14. import (
  15. "fmt"
  16. "math"
  17. "testing"
  18. "time"
  19. autoscalingv2 "k8s.io/api/autoscaling/v2beta2"
  20. v1 "k8s.io/api/core/v1"
  21. "k8s.io/apimachinery/pkg/api/meta/testrestmapper"
  22. "k8s.io/apimachinery/pkg/api/resource"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. "k8s.io/apimachinery/pkg/labels"
  25. "k8s.io/apimachinery/pkg/runtime"
  26. "k8s.io/apimachinery/pkg/runtime/schema"
  27. "k8s.io/apimachinery/pkg/util/sets"
  28. "k8s.io/client-go/informers"
  29. "k8s.io/client-go/kubernetes/fake"
  30. core "k8s.io/client-go/testing"
  31. "k8s.io/kubernetes/pkg/api/legacyscheme"
  32. "k8s.io/kubernetes/pkg/controller"
  33. metricsclient "k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
  34. cmapi "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2"
  35. emapi "k8s.io/metrics/pkg/apis/external_metrics/v1beta1"
  36. metricsapi "k8s.io/metrics/pkg/apis/metrics/v1beta1"
  37. metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake"
  38. cmfake "k8s.io/metrics/pkg/client/custom_metrics/fake"
  39. emfake "k8s.io/metrics/pkg/client/external_metrics/fake"
  40. "github.com/stretchr/testify/assert"
  41. "github.com/stretchr/testify/require"
  42. )
  43. type resourceInfo struct {
  44. name v1.ResourceName
  45. requests []resource.Quantity
  46. levels []int64
  47. // only applies to pod names returned from "heapster"
  48. podNames []string
  49. targetUtilization int32
  50. expectedUtilization int32
  51. expectedValue int64
  52. }
  53. type metricType int
  54. const (
  55. objectMetric metricType = iota
  56. objectPerPodMetric
  57. externalMetric
  58. externalPerPodMetric
  59. podMetric
  60. )
  61. type metricInfo struct {
  62. name string
  63. levels []int64
  64. singleObject *autoscalingv2.CrossVersionObjectReference
  65. selector *metav1.LabelSelector
  66. metricType metricType
  67. targetUtilization int64
  68. perPodTargetUtilization int64
  69. expectedUtilization int64
  70. }
  71. type replicaCalcTestCase struct {
  72. currentReplicas int32
  73. expectedReplicas int32
  74. expectedError error
  75. timestamp time.Time
  76. resource *resourceInfo
  77. metric *metricInfo
  78. metricLabelSelector labels.Selector
  79. podReadiness []v1.ConditionStatus
  80. podStartTime []metav1.Time
  81. podPhase []v1.PodPhase
  82. podDeletionTimestamp []bool
  83. }
  84. const (
  85. testNamespace = "test-namespace"
  86. podNamePrefix = "test-pod"
  87. numContainersPerPod = 2
  88. )
  89. func (tc *replicaCalcTestCase) prepareTestClientSet() *fake.Clientset {
  90. fakeClient := &fake.Clientset{}
  91. fakeClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
  92. obj := &v1.PodList{}
  93. podsCount := int(tc.currentReplicas)
  94. // Failed pods are not included in tc.currentReplicas
  95. if tc.podPhase != nil && len(tc.podPhase) > podsCount {
  96. podsCount = len(tc.podPhase)
  97. }
  98. for i := 0; i < podsCount; i++ {
  99. podReadiness := v1.ConditionTrue
  100. if tc.podReadiness != nil && i < len(tc.podReadiness) {
  101. podReadiness = tc.podReadiness[i]
  102. }
  103. var podStartTime metav1.Time
  104. if tc.podStartTime != nil {
  105. podStartTime = tc.podStartTime[i]
  106. }
  107. podPhase := v1.PodRunning
  108. if tc.podPhase != nil {
  109. podPhase = tc.podPhase[i]
  110. }
  111. podDeletionTimestamp := false
  112. if tc.podDeletionTimestamp != nil {
  113. podDeletionTimestamp = tc.podDeletionTimestamp[i]
  114. }
  115. podName := fmt.Sprintf("%s-%d", podNamePrefix, i)
  116. pod := v1.Pod{
  117. Status: v1.PodStatus{
  118. Phase: podPhase,
  119. StartTime: &podStartTime,
  120. Conditions: []v1.PodCondition{
  121. {
  122. Type: v1.PodReady,
  123. Status: podReadiness,
  124. },
  125. },
  126. },
  127. ObjectMeta: metav1.ObjectMeta{
  128. Name: podName,
  129. Namespace: testNamespace,
  130. Labels: map[string]string{
  131. "name": podNamePrefix,
  132. },
  133. },
  134. Spec: v1.PodSpec{
  135. Containers: []v1.Container{{}, {}},
  136. },
  137. }
  138. if podDeletionTimestamp {
  139. pod.DeletionTimestamp = &metav1.Time{Time: time.Now()}
  140. }
  141. if tc.resource != nil && i < len(tc.resource.requests) {
  142. pod.Spec.Containers[0].Resources = v1.ResourceRequirements{
  143. Requests: v1.ResourceList{
  144. tc.resource.name: tc.resource.requests[i],
  145. },
  146. }
  147. pod.Spec.Containers[1].Resources = v1.ResourceRequirements{
  148. Requests: v1.ResourceList{
  149. tc.resource.name: tc.resource.requests[i],
  150. },
  151. }
  152. }
  153. obj.Items = append(obj.Items, pod)
  154. }
  155. return true, obj, nil
  156. })
  157. return fakeClient
  158. }
  159. func (tc *replicaCalcTestCase) prepareTestMetricsClient() *metricsfake.Clientset {
  160. fakeMetricsClient := &metricsfake.Clientset{}
  161. // NB: we have to sound like Gollum due to gengo's inability to handle already-plural resource names
  162. fakeMetricsClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
  163. if tc.resource != nil {
  164. metrics := &metricsapi.PodMetricsList{}
  165. for i, resValue := range tc.resource.levels {
  166. podName := fmt.Sprintf("%s-%d", podNamePrefix, i)
  167. if len(tc.resource.podNames) > i {
  168. podName = tc.resource.podNames[i]
  169. }
  170. // NB: the list reactor actually does label selector filtering for us,
  171. // so we have to make sure our results match the label selector
  172. podMetric := metricsapi.PodMetrics{
  173. ObjectMeta: metav1.ObjectMeta{
  174. Name: podName,
  175. Namespace: testNamespace,
  176. Labels: map[string]string{"name": podNamePrefix},
  177. },
  178. Timestamp: metav1.Time{Time: tc.timestamp},
  179. Window: metav1.Duration{Duration: time.Minute},
  180. Containers: make([]metricsapi.ContainerMetrics, numContainersPerPod),
  181. }
  182. for i := 0; i < numContainersPerPod; i++ {
  183. podMetric.Containers[i] = metricsapi.ContainerMetrics{
  184. Name: fmt.Sprintf("container%v", i),
  185. Usage: v1.ResourceList{
  186. v1.ResourceName(tc.resource.name): *resource.NewMilliQuantity(
  187. int64(resValue),
  188. resource.DecimalSI),
  189. },
  190. }
  191. }
  192. metrics.Items = append(metrics.Items, podMetric)
  193. }
  194. return true, metrics, nil
  195. }
  196. return true, nil, fmt.Errorf("no pod resource metrics specified in test client")
  197. })
  198. return fakeMetricsClient
  199. }
  200. func (tc *replicaCalcTestCase) prepareTestCMClient(t *testing.T) *cmfake.FakeCustomMetricsClient {
  201. fakeCMClient := &cmfake.FakeCustomMetricsClient{}
  202. fakeCMClient.AddReactor("get", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
  203. getForAction, wasGetFor := action.(cmfake.GetForAction)
  204. if !wasGetFor {
  205. return true, nil, fmt.Errorf("expected a get-for action, got %v instead", action)
  206. }
  207. if tc.metric == nil {
  208. return true, nil, fmt.Errorf("no custom metrics specified in test client")
  209. }
  210. assert.Equal(t, tc.metric.name, getForAction.GetMetricName(), "the metric requested should have matched the one specified")
  211. if getForAction.GetName() == "*" {
  212. metrics := cmapi.MetricValueList{}
  213. // multiple objects
  214. assert.Equal(t, "pods", getForAction.GetResource().Resource, "the type of object that we requested multiple metrics for should have been pods")
  215. for i, level := range tc.metric.levels {
  216. podMetric := cmapi.MetricValue{
  217. DescribedObject: v1.ObjectReference{
  218. Kind: "Pod",
  219. Name: fmt.Sprintf("%s-%d", podNamePrefix, i),
  220. Namespace: testNamespace,
  221. },
  222. Timestamp: metav1.Time{Time: tc.timestamp},
  223. Metric: cmapi.MetricIdentifier{
  224. Name: tc.metric.name,
  225. },
  226. Value: *resource.NewMilliQuantity(level, resource.DecimalSI),
  227. }
  228. metrics.Items = append(metrics.Items, podMetric)
  229. }
  230. return true, &metrics, nil
  231. }
  232. name := getForAction.GetName()
  233. mapper := testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme)
  234. metrics := &cmapi.MetricValueList{}
  235. assert.NotNil(t, tc.metric.singleObject, "should have only requested a single-object metric when calling GetObjectMetricReplicas")
  236. gk := schema.FromAPIVersionAndKind(tc.metric.singleObject.APIVersion, tc.metric.singleObject.Kind).GroupKind()
  237. mapping, err := mapper.RESTMapping(gk)
  238. if err != nil {
  239. return true, nil, fmt.Errorf("unable to get mapping for %s: %v", gk.String(), err)
  240. }
  241. groupResource := mapping.Resource.GroupResource()
  242. assert.Equal(t, groupResource.String(), getForAction.GetResource().Resource, "should have requested metrics for the resource matching the GroupKind passed in")
  243. assert.Equal(t, tc.metric.singleObject.Name, name, "should have requested metrics for the object matching the name passed in")
  244. metrics.Items = []cmapi.MetricValue{
  245. {
  246. DescribedObject: v1.ObjectReference{
  247. Kind: tc.metric.singleObject.Kind,
  248. APIVersion: tc.metric.singleObject.APIVersion,
  249. Name: name,
  250. },
  251. Timestamp: metav1.Time{Time: tc.timestamp},
  252. Metric: cmapi.MetricIdentifier{
  253. Name: tc.metric.name,
  254. },
  255. Value: *resource.NewMilliQuantity(int64(tc.metric.levels[0]), resource.DecimalSI),
  256. },
  257. }
  258. return true, metrics, nil
  259. })
  260. return fakeCMClient
  261. }
  262. func (tc *replicaCalcTestCase) prepareTestEMClient(t *testing.T) *emfake.FakeExternalMetricsClient {
  263. fakeEMClient := &emfake.FakeExternalMetricsClient{}
  264. fakeEMClient.AddReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
  265. listAction, wasList := action.(core.ListAction)
  266. if !wasList {
  267. return true, nil, fmt.Errorf("expected a list-for action, got %v instead", action)
  268. }
  269. if tc.metric == nil {
  270. return true, nil, fmt.Errorf("no external metrics specified in test client")
  271. }
  272. assert.Equal(t, tc.metric.name, listAction.GetResource().Resource, "the metric requested should have matched the one specified")
  273. selector, err := metav1.LabelSelectorAsSelector(tc.metric.selector)
  274. if err != nil {
  275. return true, nil, fmt.Errorf("failed to convert label selector specified in test client")
  276. }
  277. assert.Equal(t, selector, listAction.GetListRestrictions().Labels, "the metric selector should have matched the one specified")
  278. metrics := emapi.ExternalMetricValueList{}
  279. for _, level := range tc.metric.levels {
  280. metric := emapi.ExternalMetricValue{
  281. Timestamp: metav1.Time{Time: tc.timestamp},
  282. MetricName: tc.metric.name,
  283. Value: *resource.NewMilliQuantity(level, resource.DecimalSI),
  284. }
  285. metrics.Items = append(metrics.Items, metric)
  286. }
  287. return true, &metrics, nil
  288. })
  289. return fakeEMClient
  290. }
  291. func (tc *replicaCalcTestCase) prepareTestClient(t *testing.T) (*fake.Clientset, *metricsfake.Clientset, *cmfake.FakeCustomMetricsClient, *emfake.FakeExternalMetricsClient) {
  292. fakeClient := tc.prepareTestClientSet()
  293. fakeMetricsClient := tc.prepareTestMetricsClient()
  294. fakeCMClient := tc.prepareTestCMClient(t)
  295. fakeEMClient := tc.prepareTestEMClient(t)
  296. return fakeClient, fakeMetricsClient, fakeCMClient, fakeEMClient
  297. }
  298. func (tc *replicaCalcTestCase) runTest(t *testing.T) {
  299. testClient, testMetricsClient, testCMClient, testEMClient := tc.prepareTestClient(t)
  300. metricsClient := metricsclient.NewRESTMetricsClient(testMetricsClient.MetricsV1beta1(), testCMClient, testEMClient)
  301. informerFactory := informers.NewSharedInformerFactory(testClient, controller.NoResyncPeriodFunc())
  302. informer := informerFactory.Core().V1().Pods()
  303. replicaCalc := NewReplicaCalculator(metricsClient, informer.Lister(), defaultTestingTolerance, defaultTestingCpuInitializationPeriod, defaultTestingDelayOfInitialReadinessStatus)
  304. stop := make(chan struct{})
  305. defer close(stop)
  306. informerFactory.Start(stop)
  307. if !controller.WaitForCacheSync("HPA", stop, informer.Informer().HasSynced) {
  308. return
  309. }
  310. selector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
  311. MatchLabels: map[string]string{"name": podNamePrefix},
  312. })
  313. if err != nil {
  314. require.Nil(t, err, "something went horribly wrong...")
  315. }
  316. if tc.resource != nil {
  317. outReplicas, outUtilization, outRawValue, outTimestamp, err := replicaCalc.GetResourceReplicas(tc.currentReplicas, tc.resource.targetUtilization, tc.resource.name, testNamespace, selector)
  318. if tc.expectedError != nil {
  319. require.Error(t, err, "there should be an error calculating the replica count")
  320. assert.Contains(t, err.Error(), tc.expectedError.Error(), "the error message should have contained the expected error message")
  321. return
  322. }
  323. require.NoError(t, err, "there should not have been an error calculating the replica count")
  324. assert.Equal(t, tc.expectedReplicas, outReplicas, "replicas should be as expected")
  325. assert.Equal(t, tc.resource.expectedUtilization, outUtilization, "utilization should be as expected")
  326. assert.Equal(t, tc.resource.expectedValue, outRawValue, "raw value should be as expected")
  327. assert.True(t, tc.timestamp.Equal(outTimestamp), "timestamp should be as expected")
  328. return
  329. }
  330. var outReplicas int32
  331. var outUtilization int64
  332. var outTimestamp time.Time
  333. switch tc.metric.metricType {
  334. case objectMetric:
  335. if tc.metric.singleObject == nil {
  336. t.Fatal("Metric specified as objectMetric but metric.singleObject is nil.")
  337. }
  338. outReplicas, outUtilization, outTimestamp, err = replicaCalc.GetObjectMetricReplicas(tc.currentReplicas, tc.metric.targetUtilization, tc.metric.name, testNamespace, tc.metric.singleObject, selector, nil)
  339. case objectPerPodMetric:
  340. if tc.metric.singleObject == nil {
  341. t.Fatal("Metric specified as objectMetric but metric.singleObject is nil.")
  342. }
  343. outReplicas, outUtilization, outTimestamp, err = replicaCalc.GetObjectPerPodMetricReplicas(tc.currentReplicas, tc.metric.perPodTargetUtilization, tc.metric.name, testNamespace, tc.metric.singleObject, nil)
  344. case externalMetric:
  345. if tc.metric.selector == nil {
  346. t.Fatal("Metric specified as externalMetric but metric.selector is nil.")
  347. }
  348. if tc.metric.targetUtilization <= 0 {
  349. t.Fatalf("Metric specified as externalMetric but metric.targetUtilization is %d which is <=0.", tc.metric.targetUtilization)
  350. }
  351. outReplicas, outUtilization, outTimestamp, err = replicaCalc.GetExternalMetricReplicas(tc.currentReplicas, tc.metric.targetUtilization, tc.metric.name, testNamespace, tc.metric.selector, selector)
  352. case externalPerPodMetric:
  353. if tc.metric.selector == nil {
  354. t.Fatal("Metric specified as externalPerPodMetric but metric.selector is nil.")
  355. }
  356. if tc.metric.perPodTargetUtilization <= 0 {
  357. t.Fatalf("Metric specified as externalPerPodMetric but metric.perPodTargetUtilization is %d which is <=0.", tc.metric.perPodTargetUtilization)
  358. }
  359. outReplicas, outUtilization, outTimestamp, err = replicaCalc.GetExternalPerPodMetricReplicas(tc.currentReplicas, tc.metric.perPodTargetUtilization, tc.metric.name, testNamespace, tc.metric.selector)
  360. case podMetric:
  361. outReplicas, outUtilization, outTimestamp, err = replicaCalc.GetMetricReplicas(tc.currentReplicas, tc.metric.targetUtilization, tc.metric.name, testNamespace, selector, nil)
  362. default:
  363. t.Fatalf("Unknown metric type: %d", tc.metric.metricType)
  364. }
  365. if tc.expectedError != nil {
  366. require.Error(t, err, "there should be an error calculating the replica count")
  367. assert.Contains(t, err.Error(), tc.expectedError.Error(), "the error message should have contained the expected error message")
  368. return
  369. }
  370. require.NoError(t, err, "there should not have been an error calculating the replica count")
  371. assert.Equal(t, tc.expectedReplicas, outReplicas, "replicas should be as expected")
  372. assert.Equal(t, tc.metric.expectedUtilization, outUtilization, "utilization should be as expected")
  373. assert.True(t, tc.timestamp.Equal(outTimestamp), "timestamp should be as expected")
  374. }
  375. func TestReplicaCalcDisjointResourcesMetrics(t *testing.T) {
  376. tc := replicaCalcTestCase{
  377. currentReplicas: 1,
  378. expectedError: fmt.Errorf("no metrics returned matched known pods"),
  379. resource: &resourceInfo{
  380. name: v1.ResourceCPU,
  381. requests: []resource.Quantity{resource.MustParse("1.0")},
  382. levels: []int64{100},
  383. podNames: []string{"an-older-pod-name"},
  384. targetUtilization: 100,
  385. },
  386. }
  387. tc.runTest(t)
  388. }
  389. func TestReplicaCalcScaleUp(t *testing.T) {
  390. tc := replicaCalcTestCase{
  391. currentReplicas: 3,
  392. expectedReplicas: 5,
  393. resource: &resourceInfo{
  394. name: v1.ResourceCPU,
  395. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  396. levels: []int64{300, 500, 700},
  397. targetUtilization: 30,
  398. expectedUtilization: 50,
  399. expectedValue: numContainersPerPod * 500,
  400. },
  401. }
  402. tc.runTest(t)
  403. }
  404. func TestReplicaCalcScaleUpUnreadyLessScale(t *testing.T) {
  405. tc := replicaCalcTestCase{
  406. currentReplicas: 3,
  407. expectedReplicas: 4,
  408. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
  409. resource: &resourceInfo{
  410. name: v1.ResourceCPU,
  411. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  412. levels: []int64{300, 500, 700},
  413. targetUtilization: 30,
  414. expectedUtilization: 60,
  415. expectedValue: numContainersPerPod * 600,
  416. },
  417. }
  418. tc.runTest(t)
  419. }
  420. func TestReplicaCalcScaleUpHotCpuLessScale(t *testing.T) {
  421. tc := replicaCalcTestCase{
  422. currentReplicas: 3,
  423. expectedReplicas: 4,
  424. podStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), coolCpuCreationTime()},
  425. resource: &resourceInfo{
  426. name: v1.ResourceCPU,
  427. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  428. levels: []int64{300, 500, 700},
  429. targetUtilization: 30,
  430. expectedUtilization: 60,
  431. expectedValue: numContainersPerPod * 600,
  432. },
  433. }
  434. tc.runTest(t)
  435. }
  436. func TestReplicaCalcScaleUpUnreadyNoScale(t *testing.T) {
  437. tc := replicaCalcTestCase{
  438. currentReplicas: 3,
  439. expectedReplicas: 3,
  440. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  441. resource: &resourceInfo{
  442. name: v1.ResourceCPU,
  443. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  444. levels: []int64{400, 500, 700},
  445. targetUtilization: 30,
  446. expectedUtilization: 40,
  447. expectedValue: numContainersPerPod * 400,
  448. },
  449. }
  450. tc.runTest(t)
  451. }
  452. func TestReplicaCalcScaleHotCpuNoScale(t *testing.T) {
  453. tc := replicaCalcTestCase{
  454. currentReplicas: 3,
  455. expectedReplicas: 3,
  456. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  457. podStartTime: []metav1.Time{coolCpuCreationTime(), hotCpuCreationTime(), hotCpuCreationTime()},
  458. resource: &resourceInfo{
  459. name: v1.ResourceCPU,
  460. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  461. levels: []int64{400, 500, 700},
  462. targetUtilization: 30,
  463. expectedUtilization: 40,
  464. expectedValue: numContainersPerPod * 400,
  465. },
  466. }
  467. tc.runTest(t)
  468. }
  469. func TestReplicaCalcScaleUpIgnoresFailedPods(t *testing.T) {
  470. tc := replicaCalcTestCase{
  471. currentReplicas: 2,
  472. expectedReplicas: 4,
  473. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  474. podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed},
  475. resource: &resourceInfo{
  476. name: v1.ResourceCPU,
  477. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  478. levels: []int64{500, 700},
  479. targetUtilization: 30,
  480. expectedUtilization: 60,
  481. expectedValue: numContainersPerPod * 600,
  482. },
  483. }
  484. tc.runTest(t)
  485. }
  486. func TestReplicaCalcScaleUpIgnoresDeletionPods(t *testing.T) {
  487. tc := replicaCalcTestCase{
  488. currentReplicas: 2,
  489. expectedReplicas: 4,
  490. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  491. podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning},
  492. podDeletionTimestamp: []bool{false, false, true, true},
  493. resource: &resourceInfo{
  494. name: v1.ResourceCPU,
  495. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  496. levels: []int64{500, 700},
  497. targetUtilization: 30,
  498. expectedUtilization: 60,
  499. expectedValue: numContainersPerPod * 600,
  500. },
  501. }
  502. tc.runTest(t)
  503. }
  504. func TestReplicaCalcScaleUpCM(t *testing.T) {
  505. tc := replicaCalcTestCase{
  506. currentReplicas: 3,
  507. expectedReplicas: 4,
  508. metric: &metricInfo{
  509. name: "qps",
  510. levels: []int64{20000, 10000, 30000},
  511. targetUtilization: 15000,
  512. expectedUtilization: 20000,
  513. metricType: podMetric,
  514. },
  515. }
  516. tc.runTest(t)
  517. }
  518. func TestReplicaCalcScaleUpCMUnreadyHotCpuNoLessScale(t *testing.T) {
  519. tc := replicaCalcTestCase{
  520. currentReplicas: 3,
  521. expectedReplicas: 6,
  522. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse},
  523. podStartTime: []metav1.Time{coolCpuCreationTime(), coolCpuCreationTime(), hotCpuCreationTime()},
  524. metric: &metricInfo{
  525. name: "qps",
  526. levels: []int64{50000, 10000, 30000},
  527. targetUtilization: 15000,
  528. expectedUtilization: 30000,
  529. metricType: podMetric,
  530. },
  531. }
  532. tc.runTest(t)
  533. }
  534. func TestReplicaCalcScaleUpCMUnreadyHotCpuScaleWouldScaleDown(t *testing.T) {
  535. tc := replicaCalcTestCase{
  536. currentReplicas: 3,
  537. expectedReplicas: 7,
  538. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse},
  539. podStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), hotCpuCreationTime()},
  540. metric: &metricInfo{
  541. name: "qps",
  542. levels: []int64{50000, 15000, 30000},
  543. targetUtilization: 15000,
  544. expectedUtilization: 31666,
  545. metricType: podMetric,
  546. },
  547. }
  548. tc.runTest(t)
  549. }
  550. func TestReplicaCalcScaleUpCMObject(t *testing.T) {
  551. tc := replicaCalcTestCase{
  552. currentReplicas: 3,
  553. expectedReplicas: 4,
  554. metric: &metricInfo{
  555. name: "qps",
  556. levels: []int64{20000},
  557. targetUtilization: 15000,
  558. expectedUtilization: 20000,
  559. singleObject: &autoscalingv2.CrossVersionObjectReference{
  560. Kind: "Deployment",
  561. APIVersion: "apps/v1",
  562. Name: "some-deployment",
  563. },
  564. },
  565. }
  566. tc.runTest(t)
  567. }
  568. func TestReplicaCalcScaleUpCMPerPodObject(t *testing.T) {
  569. tc := replicaCalcTestCase{
  570. currentReplicas: 3,
  571. expectedReplicas: 4,
  572. metric: &metricInfo{
  573. metricType: objectPerPodMetric,
  574. name: "qps",
  575. levels: []int64{20000},
  576. perPodTargetUtilization: 5000,
  577. expectedUtilization: 6667,
  578. singleObject: &autoscalingv2.CrossVersionObjectReference{
  579. Kind: "Deployment",
  580. APIVersion: "apps/v1",
  581. Name: "some-deployment",
  582. },
  583. },
  584. }
  585. tc.runTest(t)
  586. }
  587. func TestReplicaCalcScaleUpCMObjectIgnoresUnreadyPods(t *testing.T) {
  588. tc := replicaCalcTestCase{
  589. currentReplicas: 3,
  590. expectedReplicas: 5, // If we did not ignore unready pods, we'd expect 15 replicas.
  591. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse},
  592. metric: &metricInfo{
  593. name: "qps",
  594. levels: []int64{50000},
  595. targetUtilization: 10000,
  596. expectedUtilization: 50000,
  597. singleObject: &autoscalingv2.CrossVersionObjectReference{
  598. Kind: "Deployment",
  599. APIVersion: "apps/v1",
  600. Name: "some-deployment",
  601. },
  602. },
  603. }
  604. tc.runTest(t)
  605. }
  606. func TestReplicaCalcScaleUpCMExternal(t *testing.T) {
  607. tc := replicaCalcTestCase{
  608. currentReplicas: 1,
  609. expectedReplicas: 2,
  610. metric: &metricInfo{
  611. name: "qps",
  612. levels: []int64{8600},
  613. targetUtilization: 4400,
  614. expectedUtilization: 8600,
  615. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  616. metricType: podMetric,
  617. },
  618. }
  619. tc.runTest(t)
  620. }
  621. func TestReplicaCalcScaleUpCMExternalIgnoresUnreadyPods(t *testing.T) {
  622. tc := replicaCalcTestCase{
  623. currentReplicas: 3,
  624. expectedReplicas: 2, // Would expect 6 if we didn't ignore unready pods
  625. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse},
  626. metric: &metricInfo{
  627. name: "qps",
  628. levels: []int64{8600},
  629. targetUtilization: 4400,
  630. expectedUtilization: 8600,
  631. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  632. metricType: externalMetric,
  633. },
  634. }
  635. tc.runTest(t)
  636. }
  637. func TestReplicaCalcScaleUpCMExternalNoLabels(t *testing.T) {
  638. tc := replicaCalcTestCase{
  639. currentReplicas: 1,
  640. expectedReplicas: 2,
  641. metric: &metricInfo{
  642. name: "qps",
  643. levels: []int64{8600},
  644. targetUtilization: 4400,
  645. expectedUtilization: 8600,
  646. metricType: podMetric,
  647. },
  648. }
  649. tc.runTest(t)
  650. }
  651. func TestReplicaCalcScaleUpPerPodCMExternal(t *testing.T) {
  652. tc := replicaCalcTestCase{
  653. currentReplicas: 3,
  654. expectedReplicas: 4,
  655. metric: &metricInfo{
  656. name: "qps",
  657. levels: []int64{8600},
  658. perPodTargetUtilization: 2150,
  659. expectedUtilization: 2867,
  660. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  661. metricType: externalPerPodMetric,
  662. },
  663. }
  664. tc.runTest(t)
  665. }
  666. func TestReplicaCalcScaleDown(t *testing.T) {
  667. tc := replicaCalcTestCase{
  668. currentReplicas: 5,
  669. expectedReplicas: 3,
  670. resource: &resourceInfo{
  671. name: v1.ResourceCPU,
  672. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  673. levels: []int64{100, 300, 500, 250, 250},
  674. targetUtilization: 50,
  675. expectedUtilization: 28,
  676. expectedValue: numContainersPerPod * 280,
  677. },
  678. }
  679. tc.runTest(t)
  680. }
  681. func TestReplicaCalcScaleDownCM(t *testing.T) {
  682. tc := replicaCalcTestCase{
  683. currentReplicas: 5,
  684. expectedReplicas: 3,
  685. metric: &metricInfo{
  686. name: "qps",
  687. levels: []int64{12000, 12000, 12000, 12000, 12000},
  688. targetUtilization: 20000,
  689. expectedUtilization: 12000,
  690. metricType: podMetric,
  691. },
  692. }
  693. tc.runTest(t)
  694. }
  695. func TestReplicaCalcScaleDownPerPodCMObject(t *testing.T) {
  696. tc := replicaCalcTestCase{
  697. currentReplicas: 5,
  698. expectedReplicas: 3,
  699. metric: &metricInfo{
  700. name: "qps",
  701. levels: []int64{6000},
  702. perPodTargetUtilization: 2000,
  703. expectedUtilization: 1200,
  704. singleObject: &autoscalingv2.CrossVersionObjectReference{
  705. Kind: "Deployment",
  706. APIVersion: "apps/v1",
  707. Name: "some-deployment",
  708. },
  709. metricType: objectPerPodMetric,
  710. },
  711. }
  712. tc.runTest(t)
  713. }
  714. func TestReplicaCalcScaleDownCMObject(t *testing.T) {
  715. tc := replicaCalcTestCase{
  716. currentReplicas: 5,
  717. expectedReplicas: 3,
  718. metric: &metricInfo{
  719. name: "qps",
  720. levels: []int64{12000},
  721. targetUtilization: 20000,
  722. expectedUtilization: 12000,
  723. singleObject: &autoscalingv2.CrossVersionObjectReference{
  724. Kind: "Deployment",
  725. APIVersion: "apps/v1",
  726. Name: "some-deployment",
  727. },
  728. },
  729. }
  730. tc.runTest(t)
  731. }
  732. func TestReplicaCalcScaleDownCMExternal(t *testing.T) {
  733. tc := replicaCalcTestCase{
  734. currentReplicas: 5,
  735. expectedReplicas: 3,
  736. metric: &metricInfo{
  737. name: "qps",
  738. levels: []int64{8600},
  739. targetUtilization: 14334,
  740. expectedUtilization: 8600,
  741. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  742. metricType: externalMetric,
  743. },
  744. }
  745. tc.runTest(t)
  746. }
  747. func TestReplicaCalcScaleDownPerPodCMExternal(t *testing.T) {
  748. tc := replicaCalcTestCase{
  749. currentReplicas: 5,
  750. expectedReplicas: 3,
  751. metric: &metricInfo{
  752. name: "qps",
  753. levels: []int64{8600},
  754. perPodTargetUtilization: 2867,
  755. expectedUtilization: 1720,
  756. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  757. metricType: externalPerPodMetric,
  758. },
  759. }
  760. tc.runTest(t)
  761. }
  762. func TestReplicaCalcScaleDownIncludeUnreadyPods(t *testing.T) {
  763. tc := replicaCalcTestCase{
  764. currentReplicas: 5,
  765. expectedReplicas: 2,
  766. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  767. resource: &resourceInfo{
  768. name: v1.ResourceCPU,
  769. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  770. levels: []int64{100, 300, 500, 250, 250},
  771. targetUtilization: 50,
  772. expectedUtilization: 30,
  773. expectedValue: numContainersPerPod * 300,
  774. },
  775. }
  776. tc.runTest(t)
  777. }
  778. func TestReplicaCalcScaleDownIgnoreHotCpuPods(t *testing.T) {
  779. tc := replicaCalcTestCase{
  780. currentReplicas: 5,
  781. expectedReplicas: 2,
  782. podStartTime: []metav1.Time{coolCpuCreationTime(), coolCpuCreationTime(), coolCpuCreationTime(), hotCpuCreationTime(), hotCpuCreationTime()},
  783. resource: &resourceInfo{
  784. name: v1.ResourceCPU,
  785. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  786. levels: []int64{100, 300, 500, 250, 250},
  787. targetUtilization: 50,
  788. expectedUtilization: 30,
  789. expectedValue: numContainersPerPod * 300,
  790. },
  791. }
  792. tc.runTest(t)
  793. }
  794. func TestReplicaCalcScaleDownIgnoresFailedPods(t *testing.T) {
  795. tc := replicaCalcTestCase{
  796. currentReplicas: 5,
  797. expectedReplicas: 3,
  798. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  799. podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed},
  800. resource: &resourceInfo{
  801. name: v1.ResourceCPU,
  802. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  803. levels: []int64{100, 300, 500, 250, 250},
  804. targetUtilization: 50,
  805. expectedUtilization: 28,
  806. expectedValue: numContainersPerPod * 280,
  807. },
  808. }
  809. tc.runTest(t)
  810. }
  811. func TestReplicaCalcScaleDownIgnoresDeletionPods(t *testing.T) {
  812. tc := replicaCalcTestCase{
  813. currentReplicas: 5,
  814. expectedReplicas: 3,
  815. podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
  816. podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning},
  817. podDeletionTimestamp: []bool{false, false, false, false, false, true, true},
  818. resource: &resourceInfo{
  819. name: v1.ResourceCPU,
  820. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  821. levels: []int64{100, 300, 500, 250, 250},
  822. targetUtilization: 50,
  823. expectedUtilization: 28,
  824. expectedValue: numContainersPerPod * 280,
  825. },
  826. }
  827. tc.runTest(t)
  828. }
  829. func TestReplicaCalcTolerance(t *testing.T) {
  830. tc := replicaCalcTestCase{
  831. currentReplicas: 3,
  832. expectedReplicas: 3,
  833. resource: &resourceInfo{
  834. name: v1.ResourceCPU,
  835. requests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
  836. levels: []int64{1010, 1030, 1020},
  837. targetUtilization: 100,
  838. expectedUtilization: 102,
  839. expectedValue: numContainersPerPod * 1020,
  840. },
  841. }
  842. tc.runTest(t)
  843. }
  844. func TestReplicaCalcToleranceCM(t *testing.T) {
  845. tc := replicaCalcTestCase{
  846. currentReplicas: 3,
  847. expectedReplicas: 3,
  848. metric: &metricInfo{
  849. name: "qps",
  850. levels: []int64{20000, 21000, 21000},
  851. targetUtilization: 20000,
  852. expectedUtilization: 20666,
  853. metricType: podMetric,
  854. },
  855. }
  856. tc.runTest(t)
  857. }
  858. func TestReplicaCalcToleranceCMObject(t *testing.T) {
  859. tc := replicaCalcTestCase{
  860. currentReplicas: 3,
  861. expectedReplicas: 3,
  862. metric: &metricInfo{
  863. name: "qps",
  864. levels: []int64{20666},
  865. targetUtilization: 20000,
  866. expectedUtilization: 20666,
  867. singleObject: &autoscalingv2.CrossVersionObjectReference{
  868. Kind: "Deployment",
  869. APIVersion: "apps/v1",
  870. Name: "some-deployment",
  871. },
  872. },
  873. }
  874. tc.runTest(t)
  875. }
  876. func TestReplicaCalcTolerancePerPodCMObject(t *testing.T) {
  877. tc := replicaCalcTestCase{
  878. currentReplicas: 4,
  879. expectedReplicas: 4,
  880. metric: &metricInfo{
  881. metricType: objectPerPodMetric,
  882. name: "qps",
  883. levels: []int64{20166},
  884. perPodTargetUtilization: 5000,
  885. expectedUtilization: 5042,
  886. singleObject: &autoscalingv2.CrossVersionObjectReference{
  887. Kind: "Deployment",
  888. APIVersion: "apps/v1",
  889. Name: "some-deployment",
  890. },
  891. },
  892. }
  893. tc.runTest(t)
  894. }
  895. func TestReplicaCalcToleranceCMExternal(t *testing.T) {
  896. tc := replicaCalcTestCase{
  897. currentReplicas: 3,
  898. expectedReplicas: 3,
  899. metric: &metricInfo{
  900. name: "qps",
  901. levels: []int64{8600},
  902. targetUtilization: 8888,
  903. expectedUtilization: 8600,
  904. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  905. metricType: externalMetric,
  906. },
  907. }
  908. tc.runTest(t)
  909. }
  910. func TestReplicaCalcTolerancePerPodCMExternal(t *testing.T) {
  911. tc := replicaCalcTestCase{
  912. currentReplicas: 3,
  913. expectedReplicas: 3,
  914. metric: &metricInfo{
  915. name: "qps",
  916. levels: []int64{8600},
  917. perPodTargetUtilization: 2900,
  918. expectedUtilization: 2867,
  919. selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
  920. metricType: externalPerPodMetric,
  921. },
  922. }
  923. tc.runTest(t)
  924. }
  925. func TestReplicaCalcSuperfluousMetrics(t *testing.T) {
  926. tc := replicaCalcTestCase{
  927. currentReplicas: 4,
  928. expectedReplicas: 24,
  929. resource: &resourceInfo{
  930. name: v1.ResourceCPU,
  931. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  932. levels: []int64{4000, 9500, 3000, 7000, 3200, 2000},
  933. targetUtilization: 100,
  934. expectedUtilization: 587,
  935. expectedValue: numContainersPerPod * 5875,
  936. },
  937. }
  938. tc.runTest(t)
  939. }
  940. func TestReplicaCalcMissingMetrics(t *testing.T) {
  941. tc := replicaCalcTestCase{
  942. currentReplicas: 4,
  943. expectedReplicas: 3,
  944. resource: &resourceInfo{
  945. name: v1.ResourceCPU,
  946. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  947. levels: []int64{400, 95},
  948. targetUtilization: 100,
  949. expectedUtilization: 24,
  950. expectedValue: 495, // numContainersPerPod * 247, for sufficiently large values of 247
  951. },
  952. }
  953. tc.runTest(t)
  954. }
  955. func TestReplicaCalcEmptyMetrics(t *testing.T) {
  956. tc := replicaCalcTestCase{
  957. currentReplicas: 4,
  958. expectedError: fmt.Errorf("unable to get metrics for resource cpu: no metrics returned from resource metrics API"),
  959. resource: &resourceInfo{
  960. name: v1.ResourceCPU,
  961. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  962. levels: []int64{},
  963. targetUtilization: 100,
  964. },
  965. }
  966. tc.runTest(t)
  967. }
  968. func TestReplicaCalcEmptyCPURequest(t *testing.T) {
  969. tc := replicaCalcTestCase{
  970. currentReplicas: 1,
  971. expectedError: fmt.Errorf("missing request for"),
  972. resource: &resourceInfo{
  973. name: v1.ResourceCPU,
  974. requests: []resource.Quantity{},
  975. levels: []int64{200},
  976. targetUtilization: 100,
  977. },
  978. }
  979. tc.runTest(t)
  980. }
  981. func TestReplicaCalcMissingMetricsNoChangeEq(t *testing.T) {
  982. tc := replicaCalcTestCase{
  983. currentReplicas: 2,
  984. expectedReplicas: 2,
  985. resource: &resourceInfo{
  986. name: v1.ResourceCPU,
  987. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0")},
  988. levels: []int64{1000},
  989. targetUtilization: 100,
  990. expectedUtilization: 100,
  991. expectedValue: numContainersPerPod * 1000,
  992. },
  993. }
  994. tc.runTest(t)
  995. }
  996. func TestReplicaCalcMissingMetricsNoChangeGt(t *testing.T) {
  997. tc := replicaCalcTestCase{
  998. currentReplicas: 2,
  999. expectedReplicas: 2,
  1000. resource: &resourceInfo{
  1001. name: v1.ResourceCPU,
  1002. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0")},
  1003. levels: []int64{1900},
  1004. targetUtilization: 100,
  1005. expectedUtilization: 190,
  1006. expectedValue: numContainersPerPod * 1900,
  1007. },
  1008. }
  1009. tc.runTest(t)
  1010. }
  1011. func TestReplicaCalcMissingMetricsNoChangeLt(t *testing.T) {
  1012. tc := replicaCalcTestCase{
  1013. currentReplicas: 2,
  1014. expectedReplicas: 2,
  1015. resource: &resourceInfo{
  1016. name: v1.ResourceCPU,
  1017. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0")},
  1018. levels: []int64{600},
  1019. targetUtilization: 100,
  1020. expectedUtilization: 60,
  1021. expectedValue: numContainersPerPod * 600,
  1022. },
  1023. }
  1024. tc.runTest(t)
  1025. }
  1026. func TestReplicaCalcMissingMetricsUnreadyChange(t *testing.T) {
  1027. tc := replicaCalcTestCase{
  1028. currentReplicas: 3,
  1029. expectedReplicas: 3,
  1030. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
  1031. resource: &resourceInfo{
  1032. name: v1.ResourceCPU,
  1033. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  1034. levels: []int64{100, 450},
  1035. targetUtilization: 50,
  1036. expectedUtilization: 45,
  1037. expectedValue: numContainersPerPod * 450,
  1038. },
  1039. }
  1040. tc.runTest(t)
  1041. }
  1042. func TestReplicaCalcMissingMetricsHotCpuNoChange(t *testing.T) {
  1043. tc := replicaCalcTestCase{
  1044. currentReplicas: 3,
  1045. expectedReplicas: 3,
  1046. podStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), coolCpuCreationTime()},
  1047. resource: &resourceInfo{
  1048. name: v1.ResourceCPU,
  1049. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  1050. levels: []int64{100, 450},
  1051. targetUtilization: 50,
  1052. expectedUtilization: 45,
  1053. expectedValue: numContainersPerPod * 450,
  1054. },
  1055. }
  1056. tc.runTest(t)
  1057. }
  1058. func TestReplicaCalcMissingMetricsUnreadyScaleUp(t *testing.T) {
  1059. tc := replicaCalcTestCase{
  1060. currentReplicas: 3,
  1061. expectedReplicas: 4,
  1062. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
  1063. resource: &resourceInfo{
  1064. name: v1.ResourceCPU,
  1065. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  1066. levels: []int64{100, 2000},
  1067. targetUtilization: 50,
  1068. expectedUtilization: 200,
  1069. expectedValue: numContainersPerPod * 2000,
  1070. },
  1071. }
  1072. tc.runTest(t)
  1073. }
  1074. func TestReplicaCalcMissingMetricsHotCpuScaleUp(t *testing.T) {
  1075. tc := replicaCalcTestCase{
  1076. currentReplicas: 3,
  1077. expectedReplicas: 4,
  1078. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
  1079. podStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), coolCpuCreationTime()},
  1080. resource: &resourceInfo{
  1081. name: v1.ResourceCPU,
  1082. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  1083. levels: []int64{100, 2000},
  1084. targetUtilization: 50,
  1085. expectedUtilization: 200,
  1086. expectedValue: numContainersPerPod * 2000,
  1087. },
  1088. }
  1089. tc.runTest(t)
  1090. }
  1091. func TestReplicaCalcMissingMetricsUnreadyScaleDown(t *testing.T) {
  1092. tc := replicaCalcTestCase{
  1093. currentReplicas: 4,
  1094. expectedReplicas: 3,
  1095. podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue},
  1096. resource: &resourceInfo{
  1097. name: v1.ResourceCPU,
  1098. requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
  1099. levels: []int64{100, 100, 100},
  1100. targetUtilization: 50,
  1101. expectedUtilization: 10,
  1102. expectedValue: numContainersPerPod * 100,
  1103. },
  1104. }
  1105. tc.runTest(t)
  1106. }
  1107. // TestComputedToleranceAlgImplementation is a regression test which
  1108. // back-calculates a minimal percentage for downscaling based on a small percentage
  1109. // increase in pod utilization which is calibrated against the tolerance value.
  1110. func TestReplicaCalcComputedToleranceAlgImplementation(t *testing.T) {
  1111. startPods := int32(10)
  1112. // 150 mCPU per pod.
  1113. totalUsedCPUOfAllPods := int64(startPods * 150)
  1114. // Each pod starts out asking for 2X what is really needed.
  1115. // This means we will have a 50% ratio of used/requested
  1116. totalRequestedCPUOfAllPods := int32(2 * totalUsedCPUOfAllPods)
  1117. requestedToUsed := float64(totalRequestedCPUOfAllPods / int32(totalUsedCPUOfAllPods))
  1118. // Spread the amount we ask over 10 pods. We can add some jitter later in reportedLevels.
  1119. perPodRequested := totalRequestedCPUOfAllPods / startPods
  1120. // Force a minimal scaling event by satisfying (tolerance < 1 - resourcesUsedRatio).
  1121. target := math.Abs(1/(requestedToUsed*(1-defaultTestingTolerance))) + .01
  1122. finalCPUPercentTarget := int32(target * 100)
  1123. resourcesUsedRatio := float64(totalUsedCPUOfAllPods) / float64(float64(totalRequestedCPUOfAllPods)*target)
  1124. // i.e. .60 * 20 -> scaled down expectation.
  1125. finalPods := int32(math.Ceil(resourcesUsedRatio * float64(startPods)))
  1126. // To breach tolerance we will create a utilization ratio difference of tolerance to usageRatioToleranceValue)
  1127. tc := replicaCalcTestCase{
  1128. currentReplicas: startPods,
  1129. expectedReplicas: finalPods,
  1130. resource: &resourceInfo{
  1131. name: v1.ResourceCPU,
  1132. levels: []int64{
  1133. totalUsedCPUOfAllPods / 10,
  1134. totalUsedCPUOfAllPods / 10,
  1135. totalUsedCPUOfAllPods / 10,
  1136. totalUsedCPUOfAllPods / 10,
  1137. totalUsedCPUOfAllPods / 10,
  1138. totalUsedCPUOfAllPods / 10,
  1139. totalUsedCPUOfAllPods / 10,
  1140. totalUsedCPUOfAllPods / 10,
  1141. totalUsedCPUOfAllPods / 10,
  1142. totalUsedCPUOfAllPods / 10,
  1143. },
  1144. requests: []resource.Quantity{
  1145. resource.MustParse(fmt.Sprint(perPodRequested+100) + "m"),
  1146. resource.MustParse(fmt.Sprint(perPodRequested-100) + "m"),
  1147. resource.MustParse(fmt.Sprint(perPodRequested+10) + "m"),
  1148. resource.MustParse(fmt.Sprint(perPodRequested-10) + "m"),
  1149. resource.MustParse(fmt.Sprint(perPodRequested+2) + "m"),
  1150. resource.MustParse(fmt.Sprint(perPodRequested-2) + "m"),
  1151. resource.MustParse(fmt.Sprint(perPodRequested+1) + "m"),
  1152. resource.MustParse(fmt.Sprint(perPodRequested-1) + "m"),
  1153. resource.MustParse(fmt.Sprint(perPodRequested) + "m"),
  1154. resource.MustParse(fmt.Sprint(perPodRequested) + "m"),
  1155. },
  1156. targetUtilization: finalCPUPercentTarget,
  1157. expectedUtilization: int32(totalUsedCPUOfAllPods*100) / totalRequestedCPUOfAllPods,
  1158. expectedValue: numContainersPerPod * totalUsedCPUOfAllPods / 10,
  1159. },
  1160. }
  1161. tc.runTest(t)
  1162. // Reuse the data structure above, now testing "unscaling".
  1163. // Now, we test that no scaling happens if we are in a very close margin to the tolerance
  1164. target = math.Abs(1/(requestedToUsed*(1-defaultTestingTolerance))) + .004
  1165. finalCPUPercentTarget = int32(target * 100)
  1166. tc.resource.targetUtilization = finalCPUPercentTarget
  1167. tc.currentReplicas = startPods
  1168. tc.expectedReplicas = startPods
  1169. tc.runTest(t)
  1170. }
  1171. func TestGroupPods(t *testing.T) {
  1172. tests := []struct {
  1173. name string
  1174. pods []*v1.Pod
  1175. metrics metricsclient.PodMetricsInfo
  1176. resource v1.ResourceName
  1177. expectReadyPodCount int
  1178. expectIgnoredPods sets.String
  1179. expectMissingPods sets.String
  1180. }{
  1181. {
  1182. "void",
  1183. []*v1.Pod{},
  1184. metricsclient.PodMetricsInfo{},
  1185. v1.ResourceCPU,
  1186. 0,
  1187. sets.NewString(),
  1188. sets.NewString(),
  1189. },
  1190. {
  1191. "count in a ready pod - memory",
  1192. []*v1.Pod{
  1193. {
  1194. ObjectMeta: metav1.ObjectMeta{
  1195. Name: "bentham",
  1196. },
  1197. Status: v1.PodStatus{
  1198. Phase: v1.PodSucceeded,
  1199. },
  1200. },
  1201. },
  1202. metricsclient.PodMetricsInfo{
  1203. "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now(), Window: time.Minute},
  1204. },
  1205. v1.ResourceMemory,
  1206. 1,
  1207. sets.NewString(),
  1208. sets.NewString(),
  1209. },
  1210. {
  1211. "ignore a pod without ready condition - CPU",
  1212. []*v1.Pod{
  1213. {
  1214. ObjectMeta: metav1.ObjectMeta{
  1215. Name: "lucretius",
  1216. },
  1217. Status: v1.PodStatus{
  1218. Phase: v1.PodSucceeded,
  1219. StartTime: &metav1.Time{
  1220. Time: time.Now(),
  1221. },
  1222. },
  1223. },
  1224. },
  1225. metricsclient.PodMetricsInfo{
  1226. "lucretius": metricsclient.PodMetric{Value: 1},
  1227. },
  1228. v1.ResourceCPU,
  1229. 0,
  1230. sets.NewString("lucretius"),
  1231. sets.NewString(),
  1232. },
  1233. {
  1234. "count in a ready pod with fresh metrics during initialization period - CPU",
  1235. []*v1.Pod{
  1236. {
  1237. ObjectMeta: metav1.ObjectMeta{
  1238. Name: "bentham",
  1239. },
  1240. Status: v1.PodStatus{
  1241. Phase: v1.PodSucceeded,
  1242. StartTime: &metav1.Time{
  1243. Time: time.Now().Add(-1 * time.Minute),
  1244. },
  1245. Conditions: []v1.PodCondition{
  1246. {
  1247. Type: v1.PodReady,
  1248. LastTransitionTime: metav1.Time{Time: time.Now().Add(-30 * time.Second)},
  1249. Status: v1.ConditionTrue,
  1250. },
  1251. },
  1252. },
  1253. },
  1254. },
  1255. metricsclient.PodMetricsInfo{
  1256. "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now(), Window: 30 * time.Second},
  1257. },
  1258. v1.ResourceCPU,
  1259. 1,
  1260. sets.NewString(),
  1261. sets.NewString(),
  1262. },
  1263. {
  1264. "ignore a ready pod without fresh metrics during initialization period - CPU",
  1265. []*v1.Pod{
  1266. {
  1267. ObjectMeta: metav1.ObjectMeta{
  1268. Name: "bentham",
  1269. },
  1270. Status: v1.PodStatus{
  1271. Phase: v1.PodSucceeded,
  1272. StartTime: &metav1.Time{
  1273. Time: time.Now().Add(-1 * time.Minute),
  1274. },
  1275. Conditions: []v1.PodCondition{
  1276. {
  1277. Type: v1.PodReady,
  1278. LastTransitionTime: metav1.Time{Time: time.Now().Add(-30 * time.Second)},
  1279. Status: v1.ConditionTrue,
  1280. },
  1281. },
  1282. },
  1283. },
  1284. },
  1285. metricsclient.PodMetricsInfo{
  1286. "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now(), Window: 60 * time.Second},
  1287. },
  1288. v1.ResourceCPU,
  1289. 0,
  1290. sets.NewString("bentham"),
  1291. sets.NewString(),
  1292. },
  1293. {
  1294. "ignore an unready pod during initialization period - CPU",
  1295. []*v1.Pod{
  1296. {
  1297. ObjectMeta: metav1.ObjectMeta{
  1298. Name: "lucretius",
  1299. },
  1300. Status: v1.PodStatus{
  1301. Phase: v1.PodSucceeded,
  1302. StartTime: &metav1.Time{
  1303. Time: time.Now().Add(-10 * time.Minute),
  1304. },
  1305. Conditions: []v1.PodCondition{
  1306. {
  1307. Type: v1.PodReady,
  1308. LastTransitionTime: metav1.Time{Time: time.Now().Add(-9*time.Minute - 54*time.Second)},
  1309. Status: v1.ConditionFalse,
  1310. },
  1311. },
  1312. },
  1313. },
  1314. },
  1315. metricsclient.PodMetricsInfo{
  1316. "lucretius": metricsclient.PodMetric{Value: 1},
  1317. },
  1318. v1.ResourceCPU,
  1319. 0,
  1320. sets.NewString("lucretius"),
  1321. sets.NewString(),
  1322. },
  1323. {
  1324. "count in a ready pod without fresh metrics after initialization period - CPU",
  1325. []*v1.Pod{
  1326. {
  1327. ObjectMeta: metav1.ObjectMeta{
  1328. Name: "bentham",
  1329. },
  1330. Status: v1.PodStatus{
  1331. Phase: v1.PodSucceeded,
  1332. StartTime: &metav1.Time{
  1333. Time: time.Now().Add(-3 * time.Minute),
  1334. },
  1335. Conditions: []v1.PodCondition{
  1336. {
  1337. Type: v1.PodReady,
  1338. LastTransitionTime: metav1.Time{Time: time.Now().Add(-3 * time.Minute)},
  1339. Status: v1.ConditionTrue,
  1340. },
  1341. },
  1342. },
  1343. },
  1344. },
  1345. metricsclient.PodMetricsInfo{
  1346. "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now().Add(-2 * time.Minute), Window: time.Minute},
  1347. },
  1348. v1.ResourceCPU,
  1349. 1,
  1350. sets.NewString(),
  1351. sets.NewString(),
  1352. },
  1353. {
  1354. "count in an unready pod that was ready after initialization period - CPU",
  1355. []*v1.Pod{
  1356. {
  1357. ObjectMeta: metav1.ObjectMeta{
  1358. Name: "lucretius",
  1359. },
  1360. Status: v1.PodStatus{
  1361. Phase: v1.PodSucceeded,
  1362. StartTime: &metav1.Time{
  1363. Time: time.Now().Add(-10 * time.Minute),
  1364. },
  1365. Conditions: []v1.PodCondition{
  1366. {
  1367. Type: v1.PodReady,
  1368. LastTransitionTime: metav1.Time{Time: time.Now().Add(-9 * time.Minute)},
  1369. Status: v1.ConditionFalse,
  1370. },
  1371. },
  1372. },
  1373. },
  1374. },
  1375. metricsclient.PodMetricsInfo{
  1376. "lucretius": metricsclient.PodMetric{Value: 1},
  1377. },
  1378. v1.ResourceCPU,
  1379. 1,
  1380. sets.NewString(),
  1381. sets.NewString(),
  1382. },
  1383. {
  1384. "ignore pod that has never been ready after initialization period - CPU",
  1385. []*v1.Pod{
  1386. {
  1387. ObjectMeta: metav1.ObjectMeta{
  1388. Name: "lucretius",
  1389. },
  1390. Status: v1.PodStatus{
  1391. Phase: v1.PodSucceeded,
  1392. StartTime: &metav1.Time{
  1393. Time: time.Now().Add(-10 * time.Minute),
  1394. },
  1395. Conditions: []v1.PodCondition{
  1396. {
  1397. Type: v1.PodReady,
  1398. LastTransitionTime: metav1.Time{Time: time.Now().Add(-9*time.Minute - 50*time.Second)},
  1399. Status: v1.ConditionFalse,
  1400. },
  1401. },
  1402. },
  1403. },
  1404. },
  1405. metricsclient.PodMetricsInfo{
  1406. "lucretius": metricsclient.PodMetric{Value: 1},
  1407. },
  1408. v1.ResourceCPU,
  1409. 1,
  1410. sets.NewString(),
  1411. sets.NewString(),
  1412. },
  1413. {
  1414. "a missing pod",
  1415. []*v1.Pod{
  1416. {
  1417. ObjectMeta: metav1.ObjectMeta{
  1418. Name: "epicurus",
  1419. },
  1420. Status: v1.PodStatus{
  1421. Phase: v1.PodSucceeded,
  1422. StartTime: &metav1.Time{
  1423. Time: time.Now().Add(-3 * time.Minute),
  1424. },
  1425. },
  1426. },
  1427. },
  1428. metricsclient.PodMetricsInfo{},
  1429. v1.ResourceCPU,
  1430. 0,
  1431. sets.NewString(),
  1432. sets.NewString("epicurus"),
  1433. },
  1434. {
  1435. "several pods",
  1436. []*v1.Pod{
  1437. {
  1438. ObjectMeta: metav1.ObjectMeta{
  1439. Name: "lucretius",
  1440. },
  1441. Status: v1.PodStatus{
  1442. Phase: v1.PodSucceeded,
  1443. StartTime: &metav1.Time{
  1444. Time: time.Now(),
  1445. },
  1446. },
  1447. },
  1448. {
  1449. ObjectMeta: metav1.ObjectMeta{
  1450. Name: "niccolo",
  1451. },
  1452. Status: v1.PodStatus{
  1453. Phase: v1.PodSucceeded,
  1454. StartTime: &metav1.Time{
  1455. Time: time.Now().Add(-3 * time.Minute),
  1456. },
  1457. Conditions: []v1.PodCondition{
  1458. {
  1459. Type: v1.PodReady,
  1460. LastTransitionTime: metav1.Time{Time: time.Now().Add(-3 * time.Minute)},
  1461. Status: v1.ConditionTrue,
  1462. },
  1463. },
  1464. },
  1465. },
  1466. {
  1467. ObjectMeta: metav1.ObjectMeta{
  1468. Name: "epicurus",
  1469. },
  1470. Status: v1.PodStatus{
  1471. Phase: v1.PodSucceeded,
  1472. StartTime: &metav1.Time{
  1473. Time: time.Now().Add(-3 * time.Minute),
  1474. },
  1475. },
  1476. },
  1477. },
  1478. metricsclient.PodMetricsInfo{
  1479. "lucretius": metricsclient.PodMetric{Value: 1},
  1480. "niccolo": metricsclient.PodMetric{Value: 1},
  1481. },
  1482. v1.ResourceCPU,
  1483. 1,
  1484. sets.NewString("lucretius"),
  1485. sets.NewString("epicurus"),
  1486. },
  1487. }
  1488. for _, tc := range tests {
  1489. readyPodCount, ignoredPods, missingPods := groupPods(tc.pods, tc.metrics, tc.resource, defaultTestingCpuInitializationPeriod, defaultTestingDelayOfInitialReadinessStatus)
  1490. if readyPodCount != tc.expectReadyPodCount {
  1491. t.Errorf("%s got readyPodCount %d, expected %d", tc.name, readyPodCount, tc.expectReadyPodCount)
  1492. }
  1493. if !ignoredPods.Equal(tc.expectIgnoredPods) {
  1494. t.Errorf("%s got unreadyPods %v, expected %v", tc.name, ignoredPods, tc.expectIgnoredPods)
  1495. }
  1496. if !missingPods.Equal(tc.expectMissingPods) {
  1497. t.Errorf("%s got missingPods %v, expected %v", tc.name, missingPods, tc.expectMissingPods)
  1498. }
  1499. }
  1500. }
  1501. // TODO: add more tests