k8s.io/apiserver@v0.31.1/pkg/util/flowcontrol/conc_alloc_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 flowcontrol
    18  
    19  import (
    20  	"math"
    21  	"math/rand"
    22  	"sort"
    23  	"testing"
    24  )
    25  
    26  // floating-point imprecision
    27  const fpSlack = 1e-10
    28  
    29  // TestConcAlloc tests computeConcurrencyAllocation with a bunch of randomly generated cases.
    30  func TestConcAlloc(t *testing.T) {
    31  	rands := rand.New(rand.NewSource(1234567890))
    32  	for i := 0; i < 10000; i++ {
    33  		test1ConcAlloc(t, rands)
    34  	}
    35  }
    36  
    37  func test1ConcAlloc(t *testing.T, rands *rand.Rand) {
    38  	probLen := ([]int{0, 1, 2, 3, 4, 6, 9})[rands.Intn(7)]
    39  	classes := make([]allocProblemItem, probLen)
    40  	var lowSum, highSum float64
    41  	var requiredSum int
    42  	var requiredSumF float64
    43  	style := "empty"
    44  	if probLen > 0 {
    45  		switch rands.Intn(20) {
    46  		case 0:
    47  			style = "bound from below"
    48  			requiredSum = rands.Intn(probLen * 3)
    49  			requiredSumF = float64(requiredSum)
    50  			partition64(rands, probLen, requiredSumF, func(j int, x float64) {
    51  				classes[j].lowerBound = x
    52  				classes[j].target = x + 2*rands.Float64()
    53  				classes[j].upperBound = x + 3*rands.Float64()
    54  				lowSum += classes[j].lowerBound
    55  				highSum += classes[j].upperBound
    56  			})
    57  		case 1:
    58  			style = "bound from above"
    59  			requiredSum = rands.Intn(probLen*3) + 1
    60  			requiredSumF = float64(requiredSum)
    61  			partition64(rands, probLen, requiredSumF, func(j int, x float64) {
    62  				classes[j].upperBound = x
    63  				classes[j].lowerBound = x * math.Max(0, 1.25*rands.Float64()-1)
    64  				classes[j].target = classes[j].lowerBound + rands.Float64()
    65  				lowSum += classes[j].lowerBound
    66  				highSum += classes[j].upperBound
    67  			})
    68  		default:
    69  			style = "not-set-by-bounds"
    70  			for j := 0; j < probLen; j++ {
    71  				x := math.Max(0, rands.Float64()*5-1)
    72  				classes[j].lowerBound = x
    73  				classes[j].target = x + 2*rands.Float64()
    74  				classes[j].upperBound = x + 3*rands.Float64()
    75  				lowSum += classes[j].lowerBound
    76  				highSum += classes[j].upperBound
    77  			}
    78  			requiredSumF = math.Round(float64(lowSum + (highSum-lowSum)*rands.Float64()))
    79  			requiredSum = int(requiredSumF)
    80  		}
    81  	}
    82  	for rands.Float64() < 0.25 {
    83  		// Add a class with a target of zero
    84  		classes = append(classes, allocProblemItem{target: 0, upperBound: rands.Float64() + 0.00001})
    85  		highSum += classes[probLen].upperBound
    86  		if probLen > 1 {
    87  			m := rands.Intn(probLen)
    88  			classes[m], classes[probLen] = classes[probLen], classes[m]
    89  		}
    90  		probLen = len(classes)
    91  	}
    92  	allocs, fairProp, err := computeConcurrencyAllocation(requiredSum, classes)
    93  	var actualSumF float64
    94  	for _, item := range allocs {
    95  		actualSumF += item
    96  	}
    97  	expectErr := lowSum-requiredSumF > fpSlack || requiredSumF-highSum > fpSlack
    98  	if err != nil {
    99  		if expectErr {
   100  			t.Logf("For requiredSum=%v, %s classes=%#+v expected error and got %#+v", requiredSum, style, classes, err)
   101  			return
   102  		}
   103  		t.Fatalf("For requiredSum=%v, %s classes=%#+v got unexpected error %#+v", requiredSum, style, classes, err)
   104  	}
   105  	if expectErr {
   106  		t.Fatalf("Expected error from requiredSum=%v, %s classes=%#+v but got solution %v, %v instead", requiredSum, style, classes, allocs, fairProp)
   107  	}
   108  	rd := f64RelDiff(requiredSumF, actualSumF)
   109  	if rd > fpSlack {
   110  		t.Fatalf("For requiredSum=%v, %s classes=%#+v got solution %v, %v which has sum %v", requiredSum, style, classes, allocs, fairProp, actualSumF)
   111  	}
   112  	for idx, item := range classes {
   113  		target := math.Max(item.target, MinTarget)
   114  		alloc := fairProp * target
   115  		if alloc <= item.lowerBound {
   116  			if allocs[idx] != item.lowerBound {
   117  				t.Fatalf("For requiredSum=%v, %s classes=%#+v got solution %v, %v in which item %d should be its lower bound but is not", requiredSum, style, classes, allocs, fairProp, idx)
   118  			}
   119  		} else if alloc >= item.upperBound {
   120  			if allocs[idx] != item.upperBound {
   121  				t.Fatalf("For requiredSum=%v, %s classes=%#+v got solution %v, %v in which item %d should be its upper bound but is not", requiredSum, style, classes, allocs, fairProp, idx)
   122  			}
   123  		} else if f64RelDiff(alloc, allocs[idx]) > fpSlack {
   124  			t.Fatalf("For requiredSum=%v, %s classes=%#+v got solution %v, %v in which item %d got alloc %v should be %v (which is proportional to its target) but is not", requiredSum, style, classes, allocs, fairProp, idx, allocs[idx], alloc)
   125  		}
   126  	}
   127  	t.Logf("For requiredSum=%v, %s classes=%#+v got solution %v, %v", requiredSum, style, classes, allocs, fairProp)
   128  }
   129  
   130  // partition64 calls consume n times, passing ints [0,n) and floats that sum to x
   131  func partition64(rands *rand.Rand, n int, x float64, consume func(int, float64)) {
   132  	if n <= 0 {
   133  		return
   134  	}
   135  	divs := make([]float64, n-1)
   136  	for idx := range divs {
   137  		divs[idx] = float64(rands.Float64())
   138  	}
   139  	sort.Float64s(divs)
   140  	var last float64
   141  	for idx, div := range divs {
   142  		div32 := float64(div)
   143  		delta := div32 - last
   144  		consume(idx, delta*x)
   145  		last = div32
   146  	}
   147  	consume(n-1, (1-last)*x)
   148  }
   149  
   150  func f64RelDiff(a, b float64) float64 {
   151  	den := math.Max(math.Abs(a), math.Abs(b))
   152  	if den == 0 {
   153  		return 0
   154  	}
   155  	return math.Abs(a-b) / den
   156  }