k8s.io/apiserver@v0.31.1/pkg/util/flowcontrol/fairqueuing/queueset/queueset_test.go (about) 1 /* 2 Copyright 2019 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 queueset 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "math" 24 "os" 25 "reflect" 26 "sort" 27 "strings" 28 "sync" 29 "sync/atomic" 30 "testing" 31 "time" 32 33 "k8s.io/apimachinery/pkg/util/sets" 34 "k8s.io/utils/clock" 35 36 "k8s.io/apiserver/pkg/authentication/user" 37 genericrequest "k8s.io/apiserver/pkg/endpoints/request" 38 "k8s.io/apiserver/pkg/util/flowcontrol/counter" 39 "k8s.io/apiserver/pkg/util/flowcontrol/debug" 40 fq "k8s.io/apiserver/pkg/util/flowcontrol/fairqueuing" 41 "k8s.io/apiserver/pkg/util/flowcontrol/fairqueuing/promise" 42 test "k8s.io/apiserver/pkg/util/flowcontrol/fairqueuing/testing" 43 testeventclock "k8s.io/apiserver/pkg/util/flowcontrol/fairqueuing/testing/eventclock" 44 testpromise "k8s.io/apiserver/pkg/util/flowcontrol/fairqueuing/testing/promise" 45 "k8s.io/apiserver/pkg/util/flowcontrol/metrics" 46 fcrequest "k8s.io/apiserver/pkg/util/flowcontrol/request" 47 "k8s.io/klog/v2" 48 ) 49 50 // fairAlloc computes the max-min fair allocation of the given 51 // capacity to the given demands (which slice is not side-effected). 52 func fairAlloc(demands []float64, capacity float64) []float64 { 53 count := len(demands) 54 indices := make([]int, count) 55 for i := 0; i < count; i++ { 56 indices[i] = i 57 } 58 sort.Slice(indices, func(i, j int) bool { return demands[indices[i]] < demands[indices[j]] }) 59 alloc := make([]float64, count) 60 var next int 61 var prevAlloc float64 62 for ; next < count; next++ { 63 // `capacity` is how much remains assuming that 64 // all unvisited items get `prevAlloc`. 65 idx := indices[next] 66 demand := demands[idx] 67 if demand <= 0 { 68 continue 69 } 70 // `fullCapacityBite` is how much more capacity would be used 71 // if this and all following items get as much as this one 72 // is demanding. 73 fullCapacityBite := float64(count-next) * (demand - prevAlloc) 74 if fullCapacityBite > capacity { 75 break 76 } 77 prevAlloc = demand 78 alloc[idx] = demand 79 capacity -= fullCapacityBite 80 } 81 for j := next; j < count; j++ { 82 alloc[indices[j]] = prevAlloc + capacity/float64(count-next) 83 } 84 return alloc 85 } 86 87 func TestFairAlloc(t *testing.T) { 88 if e, a := []float64{0, 0}, fairAlloc([]float64{0, 0}, 42); !reflect.DeepEqual(e, a) { 89 t.Errorf("Expected %#+v, got #%+v", e, a) 90 } 91 if e, a := []float64{42, 0}, fairAlloc([]float64{47, 0}, 42); !reflect.DeepEqual(e, a) { 92 t.Errorf("Expected %#+v, got #%+v", e, a) 93 } 94 if e, a := []float64{1, 41}, fairAlloc([]float64{1, 47}, 42); !reflect.DeepEqual(e, a) { 95 t.Errorf("Expected %#+v, got #%+v", e, a) 96 } 97 if e, a := []float64{3, 5, 5, 1}, fairAlloc([]float64{3, 7, 9, 1}, 14); !reflect.DeepEqual(e, a) { 98 t.Errorf("Expected %#+v, got #%+v", e, a) 99 } 100 if e, a := []float64{1, 9, 7, 3}, fairAlloc([]float64{1, 9, 7, 3}, 21); !reflect.DeepEqual(e, a) { 101 t.Errorf("Expected %#+v, got #%+v", e, a) 102 } 103 } 104 105 type uniformClient struct { 106 hash uint64 107 nThreads int 108 nCalls int 109 // duration for a simulated synchronous call 110 execDuration time.Duration 111 // duration for simulated "other work". This can be negative, 112 // causing a request to be launched a certain amount of time 113 // before the previous one finishes. 114 thinkDuration time.Duration 115 // padDuration is additional time during which this request occupies its seats. 116 // This comes at the end of execution, after the reply has been released toward 117 // the client. 118 // The evaluation code below does not take this into account. 119 // In cases where `padDuration` makes a difference, 120 // set the `expectedAverages` field of `uniformScenario`. 121 padDuration time.Duration 122 // When true indicates that only half the specified number of 123 // threads should run during the first half of the evaluation 124 // period 125 split bool 126 // initialSeats is the number of seats this request occupies in the first phase of execution 127 initialSeats uint64 128 // finalSeats is the number occupied during the second phase of execution 129 finalSeats uint64 130 } 131 132 func newUniformClient(hash uint64, nThreads, nCalls int, execDuration, thinkDuration time.Duration) uniformClient { 133 return uniformClient{ 134 hash: hash, 135 nThreads: nThreads, 136 nCalls: nCalls, 137 execDuration: execDuration, 138 thinkDuration: thinkDuration, 139 initialSeats: 1, 140 finalSeats: 1, 141 } 142 } 143 144 func (uc uniformClient) setSplit() uniformClient { 145 uc.split = true 146 return uc 147 } 148 149 func (uc uniformClient) setInitWidth(seats uint64) uniformClient { 150 uc.initialSeats = seats 151 return uc 152 } 153 154 func (uc uniformClient) pad(finalSeats int, duration time.Duration) uniformClient { 155 uc.finalSeats = uint64(finalSeats) 156 uc.padDuration = duration 157 return uc 158 } 159 160 // uniformScenario describes a scenario based on the given set of uniform clients. 161 // Each uniform client specifies a number of threads, each of which alternates between thinking 162 // and making a synchronous request through the QueueSet. 163 // The test measures how much concurrency each client got, on average, over 164 // the initial evalDuration and tests to see whether they all got about the fair amount. 165 // Each client needs to be demanding enough to use more than its fair share, 166 // or overall care needs to be taken about timing so that scheduling details 167 // do not cause any client to actually request a significantly smaller share 168 // than it theoretically should. 169 // expectFair indicate whether the QueueSet is expected to be 170 // fair in the respective halves of a split scenario; 171 // in a non-split scenario this is a singleton with one expectation. 172 // expectAllRequests indicates whether all requests are expected to get dispatched. 173 // expectedAverages, if provided, replaces the normal calculation of expected results. 174 type uniformScenario struct { 175 name string 176 qs fq.QueueSet 177 clients []uniformClient 178 concurrencyLimit int 179 evalDuration time.Duration 180 expectedFair []bool 181 expectedFairnessMargin []float64 182 expectAllRequests bool 183 evalInqueueMetrics, evalExecutingMetrics bool 184 rejectReason string 185 clk *testeventclock.Fake 186 counter counter.GoRoutineCounter 187 expectedAverages []float64 188 expectedEpochAdvances int 189 seatDemandIntegratorSubject fq.Integrator 190 dontDump bool 191 } 192 193 func (us uniformScenario) exercise(t *testing.T) { 194 uss := uniformScenarioState{ 195 t: t, 196 uniformScenario: us, 197 startTime: us.clk.Now(), 198 execSeatsIntegrators: make([]fq.Integrator, len(us.clients)), 199 seatDemandIntegratorCheck: fq.NewNamedIntegrator(us.clk, us.name+"-seatDemandCheck"), 200 executions: make([]int32, len(us.clients)), 201 rejects: make([]int32, len(us.clients)), 202 } 203 for _, uc := range us.clients { 204 uss.doSplit = uss.doSplit || uc.split 205 } 206 uss.exercise() 207 } 208 209 type uniformScenarioState struct { 210 t *testing.T 211 uniformScenario 212 startTime time.Time 213 doSplit bool 214 execSeatsIntegrators []fq.Integrator 215 seatDemandIntegratorCheck fq.Integrator 216 failedCount uint64 217 expectedInqueueReqs, expectedInqueueSeats, expectedExecuting, expectedConcurrencyInUse string 218 executions, rejects []int32 219 } 220 221 func (uss *uniformScenarioState) exercise() { 222 uss.t.Logf("%s: Start %s, doSplit=%v, clk=%p, grc=%p", uss.startTime.Format(nsTimeFmt), uss.name, uss.doSplit, uss.clk, uss.counter) 223 if uss.evalInqueueMetrics || uss.evalExecutingMetrics { 224 metrics.Reset() 225 } 226 for i, uc := range uss.clients { 227 uss.execSeatsIntegrators[i] = fq.NewNamedIntegrator(uss.clk, fmt.Sprintf("%s client %d execSeats", uss.name, i)) 228 fsName := fmt.Sprintf("client%d", i) 229 uss.expectedInqueueReqs = uss.expectedInqueueReqs + fmt.Sprintf(` apiserver_flowcontrol_current_inqueue_requests{flow_schema=%q,priority_level=%q} 0%s`, fsName, uss.name, "\n") 230 uss.expectedInqueueSeats = uss.expectedInqueueSeats + fmt.Sprintf(` apiserver_flowcontrol_current_inqueue_seats{flow_schema=%q,priority_level=%q} 0%s`, fsName, uss.name, "\n") 231 for j := 0; j < uc.nThreads; j++ { 232 ust := uniformScenarioThread{ 233 uss: uss, 234 i: i, 235 j: j, 236 nCalls: uc.nCalls, 237 uc: uc, 238 execSeatsIntegrator: uss.execSeatsIntegrators[i], 239 fsName: fsName, 240 } 241 ust.start() 242 } 243 } 244 if uss.doSplit { 245 uss.evalTo(uss.startTime.Add(uss.evalDuration/2), false, uss.expectedFair[0], uss.expectedFairnessMargin[0]) 246 } 247 uss.evalTo(uss.startTime.Add(uss.evalDuration), true, uss.expectedFair[len(uss.expectedFair)-1], uss.expectedFairnessMargin[len(uss.expectedFairnessMargin)-1]) 248 uss.clk.Run(nil) 249 uss.finalReview() 250 } 251 252 type uniformScenarioThread struct { 253 uss *uniformScenarioState 254 i, j int 255 nCalls int 256 uc uniformClient 257 execSeatsIntegrator fq.Integrator 258 fsName string 259 } 260 261 func (ust *uniformScenarioThread) start() { 262 initialDelay := time.Duration(90*ust.j + 20*ust.i) 263 if ust.uc.split && ust.j >= ust.uc.nThreads/2 { 264 initialDelay += ust.uss.evalDuration / 2 265 ust.nCalls = ust.nCalls / 2 266 } 267 ust.uss.clk.EventAfterDuration(ust.genCallK(0), initialDelay) 268 } 269 270 // generates an EventFunc that does call k 271 func (ust *uniformScenarioThread) genCallK(k int) func(time.Time) { 272 return func(time.Time) { 273 ust.callK(k) 274 } 275 } 276 277 func (ust *uniformScenarioThread) callK(k int) { 278 if k >= ust.nCalls { 279 return 280 } 281 maxWidth := float64(uint64max(ust.uc.initialSeats, ust.uc.finalSeats)) 282 ust.uss.seatDemandIntegratorCheck.Add(maxWidth) 283 returnSeatDemand := func(time.Time) { ust.uss.seatDemandIntegratorCheck.Add(-maxWidth) } 284 ctx := context.Background() 285 username := fmt.Sprintf("%d:%d:%d", ust.i, ust.j, k) 286 ctx = genericrequest.WithUser(ctx, &user.DefaultInfo{Name: username}) 287 req, idle := ust.uss.qs.StartRequest(ctx, &fcrequest.WorkEstimate{InitialSeats: ust.uc.initialSeats, FinalSeats: ust.uc.finalSeats, AdditionalLatency: ust.uc.padDuration}, ust.uc.hash, "", ust.fsName, ust.uss.name, []int{ust.i, ust.j, k}, nil) 288 ust.uss.t.Logf("%s: %d, %d, %d got req=%p, idle=%v", ust.uss.clk.Now().Format(nsTimeFmt), ust.i, ust.j, k, req, idle) 289 if req == nil { 290 atomic.AddUint64(&ust.uss.failedCount, 1) 291 atomic.AddInt32(&ust.uss.rejects[ust.i], 1) 292 returnSeatDemand(ust.uss.clk.Now()) 293 return 294 } 295 if idle { 296 ust.uss.t.Error("got request but QueueSet reported idle") 297 } 298 if (!ust.uss.dontDump) && k%100 == 0 { 299 insistRequestFromUser(ust.uss.t, ust.uss.qs, username) 300 } 301 var executed bool 302 var returnTime time.Time 303 idle2 := req.Finish(func() { 304 executed = true 305 execStart := ust.uss.clk.Now() 306 atomic.AddInt32(&ust.uss.executions[ust.i], 1) 307 ust.execSeatsIntegrator.Add(float64(ust.uc.initialSeats)) 308 ust.uss.t.Logf("%s: %d, %d, %d executing; width1=%d", execStart.Format(nsTimeFmt), ust.i, ust.j, k, ust.uc.initialSeats) 309 ust.uss.clk.EventAfterDuration(ust.genCallK(k+1), ust.uc.execDuration+ust.uc.thinkDuration) 310 ust.uss.clk.Sleep(ust.uc.execDuration) 311 ust.execSeatsIntegrator.Add(-float64(ust.uc.initialSeats)) 312 ust.uss.clk.EventAfterDuration(returnSeatDemand, ust.uc.padDuration) 313 returnTime = ust.uss.clk.Now() 314 }) 315 now := ust.uss.clk.Now() 316 ust.uss.t.Logf("%s: %d, %d, %d got executed=%v, idle2=%v", now.Format(nsTimeFmt), ust.i, ust.j, k, executed, idle2) 317 if !executed { 318 atomic.AddUint64(&ust.uss.failedCount, 1) 319 atomic.AddInt32(&ust.uss.rejects[ust.i], 1) 320 returnSeatDemand(ust.uss.clk.Now()) 321 } else if now != returnTime { 322 ust.uss.t.Errorf("%s: %d, %d, %d returnTime=%s", now.Format(nsTimeFmt), ust.i, ust.j, k, returnTime.Format(nsTimeFmt)) 323 } 324 } 325 326 func insistRequestFromUser(t *testing.T, qs fq.QueueSet, username string) { 327 qsd := qs.Dump(true) 328 goodRequest := func(rd debug.RequestDump) bool { 329 return rd.UserName == username 330 } 331 goodSliceOfRequests := SliceMapReduce(goodRequest, or) 332 if goodSliceOfRequests(qsd.QueuelessExecutingRequests) { 333 t.Logf("Found user %s among queueless requests", username) 334 return 335 } 336 goodQueueDump := func(qd debug.QueueDump) bool { 337 return goodSliceOfRequests(qd.Requests) || goodSliceOfRequests(qd.RequestsExecuting) 338 } 339 if SliceMapReduce(goodQueueDump, or)(qsd.Queues) { 340 t.Logf("Found user %s among queued requests", username) 341 return 342 } 343 t.Errorf("Failed to find request from user %s", username) 344 } 345 346 func (uss *uniformScenarioState) evalTo(lim time.Time, last, expectFair bool, margin float64) { 347 uss.clk.Run(&lim) 348 uss.clk.SetTime(lim) 349 if uss.doSplit && !last { 350 uss.t.Logf("%s: End of first half of scenario %q", uss.clk.Now().Format(nsTimeFmt), uss.name) 351 } else { 352 uss.t.Logf("%s: End of scenario %q", uss.clk.Now().Format(nsTimeFmt), uss.name) 353 } 354 demands := make([]float64, len(uss.clients)) 355 averages := make([]float64, len(uss.clients)) 356 for i, uc := range uss.clients { 357 nThreads := uc.nThreads 358 if uc.split && !last { 359 nThreads = nThreads / 2 360 } 361 sep := uc.thinkDuration 362 demands[i] = float64(nThreads) * float64(uc.initialSeats) * float64(uc.execDuration) / float64(sep+uc.execDuration) 363 averages[i] = uss.execSeatsIntegrators[i].Reset().Average 364 } 365 fairAverages := uss.expectedAverages 366 if fairAverages == nil { 367 fairAverages = fairAlloc(demands, float64(uss.concurrencyLimit)) 368 } 369 for i := range uss.clients { 370 expectedAverage := fairAverages[i] 371 var gotFair bool 372 if expectedAverage > 0 { 373 relDiff := (averages[i] - expectedAverage) / expectedAverage 374 gotFair = math.Abs(relDiff) <= margin 375 } else { 376 gotFair = math.Abs(averages[i]) <= margin 377 } 378 379 if gotFair != expectFair { 380 uss.t.Errorf("%s client %d last=%v expectFair=%v margin=%v got an Average of %v but the expected average was %v", uss.name, i, last, expectFair, margin, averages[i], expectedAverage) 381 } else { 382 uss.t.Logf("%s client %d last=%v expectFair=%v margin=%v got an Average of %v and the expected average was %v", uss.name, i, last, expectFair, margin, averages[i], expectedAverage) 383 } 384 } 385 if uss.seatDemandIntegratorSubject != nil { 386 checkResults := uss.seatDemandIntegratorCheck.GetResults() 387 subjectResults := uss.seatDemandIntegratorSubject.GetResults() 388 if float64close(subjectResults.Duration, checkResults.Duration) { 389 uss.t.Logf("%s last=%v got duration of %v and expected %v", uss.name, last, subjectResults.Duration, checkResults.Duration) 390 } else { 391 uss.t.Errorf("%s last=%v got duration of %v but expected %v", uss.name, last, subjectResults.Duration, checkResults.Duration) 392 } 393 if got, expected := float64NaNTo0(subjectResults.Average), float64NaNTo0(checkResults.Average); float64close(got, expected) { 394 uss.t.Logf("%s last=%v got SeatDemand average of %v and expected %v", uss.name, last, got, expected) 395 } else { 396 uss.t.Errorf("%s last=%v got SeatDemand average of %v but expected %v", uss.name, last, got, expected) 397 } 398 if got, expected := float64NaNTo0(subjectResults.Deviation), float64NaNTo0(checkResults.Deviation); float64close(got, expected) { 399 uss.t.Logf("%s last=%v got SeatDemand standard deviation of %v and expected %v", uss.name, last, got, expected) 400 } else { 401 uss.t.Errorf("%s last=%v got SeatDemand standard deviation of %v but expected %v", uss.name, last, got, expected) 402 } 403 } 404 } 405 406 func (uss *uniformScenarioState) finalReview() { 407 if uss.expectAllRequests && uss.failedCount > 0 { 408 uss.t.Errorf("Expected all requests to be successful but got %v failed requests", uss.failedCount) 409 } else if !uss.expectAllRequests && uss.failedCount == 0 { 410 uss.t.Errorf("Expected failed requests but all requests succeeded") 411 } 412 if uss.evalInqueueMetrics { 413 e := ` 414 # HELP apiserver_flowcontrol_current_inqueue_requests [BETA] Number of requests currently pending in queues of the API Priority and Fairness subsystem 415 # TYPE apiserver_flowcontrol_current_inqueue_requests gauge 416 ` + uss.expectedInqueueReqs 417 err := metrics.GatherAndCompare(e, "apiserver_flowcontrol_current_inqueue_requests") 418 if err != nil { 419 uss.t.Error(err) 420 } else { 421 uss.t.Log("Success with" + e) 422 } 423 424 e = ` 425 # HELP apiserver_flowcontrol_current_inqueue_seats [ALPHA] Number of seats currently pending in queues of the API Priority and Fairness subsystem 426 # TYPE apiserver_flowcontrol_current_inqueue_seats gauge 427 ` + uss.expectedInqueueSeats 428 err = metrics.GatherAndCompare(e, "apiserver_flowcontrol_current_inqueue_seats") 429 if err != nil { 430 uss.t.Error(err) 431 } else { 432 uss.t.Log("Success with" + e) 433 } 434 } 435 expectedRejects := "" 436 for i := range uss.clients { 437 fsName := fmt.Sprintf("client%d", i) 438 if atomic.LoadInt32(&uss.executions[i]) > 0 { 439 uss.expectedExecuting = uss.expectedExecuting + fmt.Sprintf(` apiserver_flowcontrol_current_executing_requests{flow_schema=%q,priority_level=%q} 0%s`, fsName, uss.name, "\n") 440 uss.expectedConcurrencyInUse = uss.expectedConcurrencyInUse + fmt.Sprintf(` apiserver_flowcontrol_request_concurrency_in_use{flow_schema=%q,priority_level=%q} 0%s`, fsName, uss.name, "\n") 441 } 442 if atomic.LoadInt32(&uss.rejects[i]) > 0 { 443 expectedRejects = expectedRejects + fmt.Sprintf(` apiserver_flowcontrol_rejected_requests_total{flow_schema=%q,priority_level=%q,reason=%q} %d%s`, fsName, uss.name, uss.rejectReason, uss.rejects[i], "\n") 444 } 445 } 446 if uss.evalExecutingMetrics && len(uss.expectedExecuting) > 0 { 447 e := ` 448 # HELP apiserver_flowcontrol_current_executing_requests [BETA] Number of requests in initial (for a WATCH) or any (for a non-WATCH) execution stage in the API Priority and Fairness subsystem 449 # TYPE apiserver_flowcontrol_current_executing_requests gauge 450 ` + uss.expectedExecuting 451 err := metrics.GatherAndCompare(e, "apiserver_flowcontrol_current_executing_requests") 452 if err != nil { 453 uss.t.Error(err) 454 } else { 455 uss.t.Log("Success with" + e) 456 } 457 } 458 if uss.evalExecutingMetrics && len(uss.expectedConcurrencyInUse) > 0 { 459 e := ` 460 # HELP apiserver_flowcontrol_request_concurrency_in_use [ALPHA] Concurrency (number of seats) occupied by the currently executing (initial stage for a WATCH, any stage otherwise) requests in the API Priority and Fairness subsystem 461 # TYPE apiserver_flowcontrol_request_concurrency_in_use gauge 462 ` + uss.expectedConcurrencyInUse 463 err := metrics.GatherAndCompare(e, "apiserver_flowcontrol_request_concurrency_in_use") 464 if err != nil { 465 uss.t.Error(err) 466 } else { 467 uss.t.Log("Success with" + e) 468 } 469 } 470 if uss.evalExecutingMetrics && len(expectedRejects) > 0 { 471 e := ` 472 # HELP apiserver_flowcontrol_rejected_requests_total [BETA] Number of requests rejected by API Priority and Fairness subsystem 473 # TYPE apiserver_flowcontrol_rejected_requests_total counter 474 ` + expectedRejects 475 err := metrics.GatherAndCompare(e, "apiserver_flowcontrol_rejected_requests_total") 476 if err != nil { 477 uss.t.Error(err) 478 } else { 479 uss.t.Log("Success with" + e) 480 } 481 } 482 e := "" 483 if uss.expectedEpochAdvances > 0 { 484 e = fmt.Sprintf(` # HELP apiserver_flowcontrol_epoch_advance_total [ALPHA] Number of times the queueset's progress meter jumped backward 485 # TYPE apiserver_flowcontrol_epoch_advance_total counter 486 apiserver_flowcontrol_epoch_advance_total{priority_level=%q,success=%q} %d%s`, uss.name, "true", uss.expectedEpochAdvances, "\n") 487 } 488 err := metrics.GatherAndCompare(e, "apiserver_flowcontrol_epoch_advance_total") 489 if err != nil { 490 uss.t.Error(err) 491 } else { 492 uss.t.Logf("Success with apiserver_flowcontrol_epoch_advance_total = %d", uss.expectedEpochAdvances) 493 } 494 } 495 496 func TestMain(m *testing.M) { 497 klog.InitFlags(nil) 498 os.Exit(m.Run()) 499 } 500 501 // TestNoRestraint tests whether the no-restraint factory gives every client what it asks for 502 // even though that is unfair. 503 // Expects fairness when there is no competition, unfairness when there is competition. 504 func TestNoRestraint(t *testing.T) { 505 metrics.Register() 506 testCases := []struct { 507 concurrency int 508 margin float64 509 fair bool 510 name string 511 }{ 512 {concurrency: 10, margin: 0.001, fair: true, name: "no-competition"}, 513 {concurrency: 2, margin: 0.25, fair: false, name: "with-competition"}, 514 } 515 for _, testCase := range testCases { 516 t.Run(testCase.name, func(t *testing.T) { 517 now := time.Now() 518 clk, counter := testeventclock.NewFake(now, 0, nil) 519 nrc, err := test.NewNoRestraintFactory().BeginConstruction(fq.QueuingConfig{}, newGaugePair(clk), newExecSeatsGauge(clk), fq.NewNamedIntegrator(clk, "TestNoRestraint")) 520 if err != nil { 521 t.Fatal(err) 522 } 523 nr := nrc.Complete(fq.DispatchingConfig{}) 524 uniformScenario{name: "NoRestraint/" + testCase.name, 525 qs: nr, 526 clients: []uniformClient{ 527 newUniformClient(1001001001, 5, 15, time.Second, time.Second), 528 newUniformClient(2002002002, 2, 15, time.Second, time.Second/2), 529 }, 530 concurrencyLimit: testCase.concurrency, 531 evalDuration: time.Second * 18, 532 expectedFair: []bool{testCase.fair}, 533 expectedFairnessMargin: []float64{testCase.margin}, 534 expectAllRequests: true, 535 clk: clk, 536 counter: counter, 537 dontDump: true, 538 }.exercise(t) 539 }) 540 } 541 } 542 543 func TestBaseline(t *testing.T) { 544 metrics.Register() 545 now := time.Now() 546 547 clk, counter := testeventclock.NewFake(now, 0, nil) 548 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 549 qCfg := fq.QueuingConfig{ 550 Name: "TestBaseline", 551 DesiredNumQueues: 9, 552 QueueLengthLimit: 8, 553 HandSize: 3, 554 } 555 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, "seatDemandSubject") 556 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 557 if err != nil { 558 t.Fatal(err) 559 } 560 qs := qsComplete(qsc, 1) 561 562 uniformScenario{name: qCfg.Name, 563 qs: qs, 564 clients: []uniformClient{ 565 newUniformClient(1001001001, 1, 21, time.Second, 0), 566 }, 567 concurrencyLimit: 1, 568 evalDuration: time.Second * 20, 569 expectedFair: []bool{true}, 570 expectedFairnessMargin: []float64{0}, 571 expectAllRequests: true, 572 evalInqueueMetrics: true, 573 evalExecutingMetrics: true, 574 clk: clk, 575 counter: counter, 576 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 577 }.exercise(t) 578 } 579 580 func TestExampt(t *testing.T) { 581 metrics.Register() 582 for concurrencyLimit := 0; concurrencyLimit <= 2; concurrencyLimit += 2 { 583 t.Run(fmt.Sprintf("concurrency=%d", concurrencyLimit), func(t *testing.T) { 584 now := time.Now() 585 clk, counter := testeventclock.NewFake(now, 0, nil) 586 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 587 qCfg := fq.QueuingConfig{ 588 Name: "TestBaseline", 589 DesiredNumQueues: -1, 590 QueueLengthLimit: 2, 591 HandSize: 3, 592 } 593 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, "seatDemandSubject") 594 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 595 if err != nil { 596 t.Fatal(err) 597 } 598 qs := qsComplete(qsc, concurrencyLimit) 599 uniformScenario{name: qCfg.Name, 600 qs: qs, 601 clients: []uniformClient{ 602 newUniformClient(1001001001, 5, 20, time.Second, time.Second).setInitWidth(3), 603 }, 604 concurrencyLimit: 1, 605 evalDuration: time.Second * 40, 606 expectedFair: []bool{true}, // "fair" is a bit odd-sounding here, but it "expectFair" here means expect `expectedAverages` 607 expectedAverages: []float64{7.5}, 608 expectedFairnessMargin: []float64{0.00000001}, 609 expectAllRequests: true, 610 evalInqueueMetrics: false, 611 evalExecutingMetrics: true, 612 clk: clk, 613 counter: counter, 614 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 615 }.exercise(t) 616 }) 617 } 618 } 619 620 func TestSeparations(t *testing.T) { 621 flts := func(avgs ...float64) []float64 { return avgs } 622 for _, seps := range []struct { 623 think, pad time.Duration 624 finalSeats, conc, nClients int 625 exp []float64 // override expected results 626 }{ 627 {think: time.Second, pad: 0, finalSeats: 1, conc: 1, nClients: 1}, 628 {think: time.Second, pad: 0, finalSeats: 1, conc: 2, nClients: 1}, 629 {think: time.Second, pad: 0, finalSeats: 2, conc: 2, nClients: 1}, 630 {think: time.Second, pad: 0, finalSeats: 1, conc: 1, nClients: 2}, 631 {think: time.Second, pad: 0, finalSeats: 1, conc: 2, nClients: 2}, 632 {think: time.Second, pad: 0, finalSeats: 2, conc: 2, nClients: 2}, 633 {think: 0, pad: time.Second, finalSeats: 1, conc: 1, nClients: 1, exp: flts(0.5)}, 634 {think: 0, pad: time.Second, finalSeats: 1, conc: 2, nClients: 1}, 635 {think: 0, pad: time.Second, finalSeats: 2, conc: 2, nClients: 1, exp: flts(0.5)}, 636 {think: 0, pad: time.Second, finalSeats: 1, conc: 1, nClients: 2, exp: flts(0.25, 0.25)}, 637 {think: 0, pad: time.Second, finalSeats: 1, conc: 2, nClients: 2, exp: flts(0.5, 0.5)}, 638 {think: 0, pad: time.Second, finalSeats: 2, conc: 2, nClients: 2, exp: flts(0.25, 0.25)}, 639 {think: time.Second, pad: time.Second / 2, finalSeats: 1, conc: 1, nClients: 1}, 640 {think: time.Second, pad: time.Second / 2, finalSeats: 1, conc: 2, nClients: 1}, 641 {think: time.Second, pad: time.Second / 2, finalSeats: 2, conc: 2, nClients: 1}, 642 {think: time.Second, pad: time.Second / 2, finalSeats: 1, conc: 1, nClients: 2, exp: flts(1.0/3, 1.0/3)}, 643 {think: time.Second, pad: time.Second / 2, finalSeats: 1, conc: 2, nClients: 2}, 644 {think: time.Second, pad: time.Second / 2, finalSeats: 2, conc: 2, nClients: 2, exp: flts(1.0/3, 1.0/3)}, 645 {think: time.Second / 2, pad: time.Second, finalSeats: 1, conc: 1, nClients: 1, exp: flts(0.5)}, 646 {think: time.Second / 2, pad: time.Second, finalSeats: 1, conc: 2, nClients: 1}, 647 {think: time.Second / 2, pad: time.Second, finalSeats: 2, conc: 2, nClients: 1, exp: flts(0.5)}, 648 {think: time.Second / 2, pad: time.Second, finalSeats: 1, conc: 1, nClients: 2, exp: flts(0.25, 0.25)}, 649 {think: time.Second / 2, pad: time.Second, finalSeats: 1, conc: 2, nClients: 2, exp: flts(0.5, 0.5)}, 650 {think: time.Second / 2, pad: time.Second, finalSeats: 2, conc: 2, nClients: 2, exp: flts(0.25, 0.25)}, 651 } { 652 caseName := fmt.Sprintf("think=%v,finalSeats=%d,pad=%v,nClients=%d,conc=%d", seps.think, seps.finalSeats, seps.pad, seps.nClients, seps.conc) 653 t.Run(caseName, func(t *testing.T) { 654 metrics.Register() 655 now := time.Now() 656 657 clk, counter := testeventclock.NewFake(now, 0, nil) 658 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 659 qCfg := fq.QueuingConfig{ 660 Name: "TestSeparations/" + caseName, 661 DesiredNumQueues: 9, 662 QueueLengthLimit: 8, 663 HandSize: 3, 664 } 665 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, caseName+" seatDemandSubject") 666 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 667 if err != nil { 668 t.Fatal(err) 669 } 670 qs := qsComplete(qsc, seps.conc) 671 uniformScenario{name: qCfg.Name, 672 qs: qs, 673 clients: []uniformClient{ 674 newUniformClient(1001001001, 1, 25, time.Second, seps.think).pad(seps.finalSeats, seps.pad), 675 newUniformClient(2002002002, 1, 25, time.Second, seps.think).pad(seps.finalSeats, seps.pad), 676 }[:seps.nClients], 677 concurrencyLimit: seps.conc, 678 evalDuration: time.Second * 24, // multiple of every period involved, so that margin can be 0 below 679 expectedFair: []bool{true}, 680 expectedFairnessMargin: []float64{0}, 681 expectAllRequests: true, 682 evalInqueueMetrics: true, 683 evalExecutingMetrics: true, 684 clk: clk, 685 counter: counter, 686 expectedAverages: seps.exp, 687 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 688 }.exercise(t) 689 }) 690 } 691 } 692 693 func TestUniformFlowsHandSize1(t *testing.T) { 694 metrics.Register() 695 now := time.Now() 696 697 clk, counter := testeventclock.NewFake(now, 0, nil) 698 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 699 qCfg := fq.QueuingConfig{ 700 Name: "TestUniformFlowsHandSize1", 701 DesiredNumQueues: 9, 702 QueueLengthLimit: 8, 703 HandSize: 1, 704 } 705 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, "seatDemandSubject") 706 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 707 if err != nil { 708 t.Fatal(err) 709 } 710 qs := qsComplete(qsc, 4) 711 712 uniformScenario{name: qCfg.Name, 713 qs: qs, 714 clients: []uniformClient{ 715 newUniformClient(1001001001, 8, 20, time.Second, time.Second-1), 716 newUniformClient(2002002002, 8, 20, time.Second, time.Second-1), 717 }, 718 concurrencyLimit: 4, 719 evalDuration: time.Second * 50, 720 expectedFair: []bool{true}, 721 expectedFairnessMargin: []float64{0.01}, 722 expectAllRequests: true, 723 evalInqueueMetrics: true, 724 evalExecutingMetrics: true, 725 clk: clk, 726 counter: counter, 727 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 728 }.exercise(t) 729 } 730 731 func TestUniformFlowsHandSize3(t *testing.T) { 732 metrics.Register() 733 now := time.Now() 734 735 clk, counter := testeventclock.NewFake(now, 0, nil) 736 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 737 qCfg := fq.QueuingConfig{ 738 Name: "TestUniformFlowsHandSize3", 739 DesiredNumQueues: 8, 740 QueueLengthLimit: 16, 741 HandSize: 3, 742 } 743 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, qCfg.Name) 744 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 745 if err != nil { 746 t.Fatal(err) 747 } 748 qs := qsComplete(qsc, 4) 749 uniformScenario{name: qCfg.Name, 750 qs: qs, 751 clients: []uniformClient{ 752 newUniformClient(400900100100, 8, 30, time.Second, time.Second-1), 753 newUniformClient(300900200200, 8, 30, time.Second, time.Second-1), 754 }, 755 concurrencyLimit: 4, 756 evalDuration: time.Second * 60, 757 expectedFair: []bool{true}, 758 expectedFairnessMargin: []float64{0.03}, 759 expectAllRequests: true, 760 evalInqueueMetrics: true, 761 evalExecutingMetrics: true, 762 clk: clk, 763 counter: counter, 764 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 765 }.exercise(t) 766 } 767 768 func TestDifferentFlowsExpectEqual(t *testing.T) { 769 metrics.Register() 770 now := time.Now() 771 772 clk, counter := testeventclock.NewFake(now, 0, nil) 773 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 774 qCfg := fq.QueuingConfig{ 775 Name: "DiffFlowsExpectEqual", 776 DesiredNumQueues: 9, 777 QueueLengthLimit: 8, 778 HandSize: 1, 779 } 780 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, qCfg.Name) 781 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 782 if err != nil { 783 t.Fatal(err) 784 } 785 qs := qsComplete(qsc, 4) 786 787 uniformScenario{name: qCfg.Name, 788 qs: qs, 789 clients: []uniformClient{ 790 newUniformClient(1001001001, 8, 20, time.Second, time.Second), 791 newUniformClient(2002002002, 7, 30, time.Second, time.Second/2), 792 }, 793 concurrencyLimit: 4, 794 evalDuration: time.Second * 40, 795 expectedFair: []bool{true}, 796 expectedFairnessMargin: []float64{0.01}, 797 expectAllRequests: true, 798 evalInqueueMetrics: true, 799 evalExecutingMetrics: true, 800 clk: clk, 801 counter: counter, 802 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 803 }.exercise(t) 804 } 805 806 // TestSeatSecondsRollover checks that there is not a problem with SeatSeconds overflow. 807 func TestSeatSecondsRollover(t *testing.T) { 808 metrics.Register() 809 now := time.Now() 810 811 const Quarter = 91 * 24 * time.Hour 812 813 clk, counter := testeventclock.NewFake(now, 0, nil) 814 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 815 qCfg := fq.QueuingConfig{ 816 Name: "TestSeatSecondsRollover", 817 DesiredNumQueues: 9, 818 QueueLengthLimit: 8, 819 HandSize: 1, 820 } 821 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, qCfg.Name) 822 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 823 if err != nil { 824 t.Fatal(err) 825 } 826 qs := qsComplete(qsc, 2000) 827 828 uniformScenario{name: qCfg.Name, 829 qs: qs, 830 clients: []uniformClient{ 831 newUniformClient(1001001001, 8, 20, Quarter, Quarter).setInitWidth(500), 832 newUniformClient(2002002002, 7, 30, Quarter, Quarter/2).setInitWidth(500), 833 }, 834 concurrencyLimit: 2000, 835 evalDuration: Quarter * 40, 836 expectedFair: []bool{true}, 837 expectedFairnessMargin: []float64{0.01}, 838 expectAllRequests: true, 839 evalInqueueMetrics: true, 840 evalExecutingMetrics: true, 841 clk: clk, 842 counter: counter, 843 expectedEpochAdvances: 8, 844 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 845 }.exercise(t) 846 } 847 848 func TestDifferentFlowsExpectUnequal(t *testing.T) { 849 metrics.Register() 850 now := time.Now() 851 852 clk, counter := testeventclock.NewFake(now, 0, nil) 853 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 854 qCfg := fq.QueuingConfig{ 855 Name: "DiffFlowsExpectUnequal", 856 DesiredNumQueues: 9, 857 QueueLengthLimit: 6, 858 HandSize: 1, 859 } 860 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, qCfg.Name) 861 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 862 if err != nil { 863 t.Fatal(err) 864 } 865 qs := qsComplete(qsc, 3) 866 867 uniformScenario{name: qCfg.Name, 868 qs: qs, 869 clients: []uniformClient{ 870 newUniformClient(1001001001, 4, 20, time.Second, time.Second-1), 871 newUniformClient(2002002002, 2, 20, time.Second, time.Second-1), 872 }, 873 concurrencyLimit: 3, 874 evalDuration: time.Second * 20, 875 expectedFair: []bool{true}, 876 expectedFairnessMargin: []float64{0.01}, 877 expectAllRequests: true, 878 evalInqueueMetrics: true, 879 evalExecutingMetrics: true, 880 clk: clk, 881 counter: counter, 882 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 883 }.exercise(t) 884 } 885 886 func TestDifferentWidths(t *testing.T) { 887 metrics.Register() 888 now := time.Now() 889 890 clk, counter := testeventclock.NewFake(now, 0, nil) 891 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 892 qCfg := fq.QueuingConfig{ 893 Name: "TestDifferentWidths", 894 DesiredNumQueues: 64, 895 QueueLengthLimit: 13, 896 HandSize: 7, 897 } 898 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, qCfg.Name) 899 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 900 if err != nil { 901 t.Fatal(err) 902 } 903 qs := qsComplete(qsc, 6) 904 uniformScenario{name: qCfg.Name, 905 qs: qs, 906 clients: []uniformClient{ 907 newUniformClient(10010010010010, 13, 10, time.Second, time.Second-1), 908 newUniformClient(20020020020020, 7, 10, time.Second, time.Second-1).setInitWidth(2), 909 }, 910 concurrencyLimit: 6, 911 evalDuration: time.Second * 20, 912 expectedFair: []bool{true}, 913 expectedFairnessMargin: []float64{0.155}, 914 expectAllRequests: true, 915 evalInqueueMetrics: true, 916 evalExecutingMetrics: true, 917 clk: clk, 918 counter: counter, 919 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 920 }.exercise(t) 921 } 922 923 func TestTooWide(t *testing.T) { 924 metrics.Register() 925 now := time.Now() 926 927 clk, counter := testeventclock.NewFake(now, 0, nil) 928 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 929 qCfg := fq.QueuingConfig{ 930 Name: "TestTooWide", 931 DesiredNumQueues: 64, 932 QueueLengthLimit: 35, 933 HandSize: 7, 934 } 935 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, qCfg.Name) 936 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 937 if err != nil { 938 t.Fatal(err) 939 } 940 qs := qsComplete(qsc, 6) 941 uniformScenario{name: qCfg.Name, 942 qs: qs, 943 clients: []uniformClient{ 944 newUniformClient(40040040040040, 15, 21, time.Second, time.Second-1).setInitWidth(2), 945 newUniformClient(50050050050050, 15, 21, time.Second, time.Second-1).setInitWidth(2), 946 newUniformClient(60060060060060, 15, 21, time.Second, time.Second-1).setInitWidth(2), 947 newUniformClient(70070070070070, 15, 21, time.Second, time.Second-1).setInitWidth(2), 948 newUniformClient(90090090090090, 15, 21, time.Second, time.Second-1).setInitWidth(7), 949 }, 950 concurrencyLimit: 6, 951 evalDuration: time.Second * 225, 952 expectedFair: []bool{true}, 953 expectedFairnessMargin: []float64{0.33}, 954 expectAllRequests: true, 955 evalInqueueMetrics: true, 956 evalExecutingMetrics: true, 957 clk: clk, 958 counter: counter, 959 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 960 }.exercise(t) 961 } 962 963 // TestWindup exercises a scenario with the windup problem. 964 // That is, a flow that can not use all the seats that it is allocated 965 // for a while. During that time, the queues that serve that flow 966 // advance their `virtualStart` (that is, R(next dispatch in virtual world)) 967 // more slowly than the other queues (which are using more seats than they 968 // are allocated). The implementation has a hack that addresses part of 969 // this imbalance but not all of it. In this test, flow 1 can not use all 970 // of its allocation during the first half, and *can* (and does) use all of 971 // its allocation and more during the second half. 972 // Thus we expect the fair (not equal) result 973 // in the first half and an unfair result in the second half. 974 // This func has two test cases, bounding the amount of unfairness 975 // in the second half. 976 func TestWindup(t *testing.T) { 977 metrics.Register() 978 testCases := []struct { 979 margin2 float64 980 expectFair2 bool 981 name string 982 }{ 983 {margin2: 0.26, expectFair2: true, name: "upper-bound"}, 984 {margin2: 0.1, expectFair2: false, name: "lower-bound"}, 985 } 986 for _, testCase := range testCases { 987 t.Run(testCase.name, func(t *testing.T) { 988 now := time.Now() 989 clk, counter := testeventclock.NewFake(now, 0, nil) 990 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 991 qCfg := fq.QueuingConfig{ 992 Name: "TestWindup/" + testCase.name, 993 DesiredNumQueues: 9, 994 QueueLengthLimit: 6, 995 HandSize: 1, 996 } 997 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, qCfg.Name) 998 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 999 if err != nil { 1000 t.Fatal(err) 1001 } 1002 qs := qsComplete(qsc, 3) 1003 1004 uniformScenario{name: qCfg.Name, qs: qs, 1005 clients: []uniformClient{ 1006 newUniformClient(1001001001, 2, 40, time.Second, -1), 1007 newUniformClient(2002002002, 2, 40, time.Second, -1).setSplit(), 1008 }, 1009 concurrencyLimit: 3, 1010 evalDuration: time.Second * 40, 1011 expectedFair: []bool{true, testCase.expectFair2}, 1012 expectedFairnessMargin: []float64{0.01, testCase.margin2}, 1013 expectAllRequests: true, 1014 evalInqueueMetrics: true, 1015 evalExecutingMetrics: true, 1016 clk: clk, 1017 counter: counter, 1018 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 1019 }.exercise(t) 1020 }) 1021 } 1022 } 1023 1024 func TestDifferentFlowsWithoutQueuing(t *testing.T) { 1025 metrics.Register() 1026 now := time.Now() 1027 1028 clk, counter := testeventclock.NewFake(now, 0, nil) 1029 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 1030 qCfg := fq.QueuingConfig{ 1031 Name: "TestDifferentFlowsWithoutQueuing", 1032 DesiredNumQueues: 0, 1033 } 1034 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, "seatDemandSubject") 1035 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 1036 if err != nil { 1037 t.Fatal(err) 1038 } 1039 qs := qsComplete(qsc, 4) 1040 1041 uniformScenario{name: qCfg.Name, 1042 qs: qs, 1043 clients: []uniformClient{ 1044 newUniformClient(1001001001, 6, 10, time.Second, 57*time.Millisecond), 1045 newUniformClient(2002002002, 4, 15, time.Second, 750*time.Millisecond), 1046 }, 1047 concurrencyLimit: 4, 1048 evalDuration: time.Second * 13, 1049 expectedFair: []bool{false}, 1050 expectedFairnessMargin: []float64{0.20}, 1051 evalExecutingMetrics: true, 1052 rejectReason: "concurrency-limit", 1053 clk: clk, 1054 counter: counter, 1055 seatDemandIntegratorSubject: seatDemandIntegratorSubject, 1056 }.exercise(t) 1057 } 1058 1059 // TestContextCancel tests cancellation of a request's context. 1060 // The outline is: 1061 // 1. Use a concurrency limit of 1. 1062 // 2. Start request 1. 1063 // 3. Use a fake clock for the following logic, to insulate from scheduler noise. 1064 // 4. The exec fn of request 1 starts request 2, which should wait 1065 // in its queue. 1066 // 5. The exec fn of request 1 also forks a goroutine that waits 1 second 1067 // and then cancels the context of request 2. 1068 // 6. The exec fn of request 1, if StartRequest 2 returns a req2 (which is the normal case), 1069 // calls `req2.Finish`, which is expected to return after the context cancel. 1070 // 7. The queueset interface allows StartRequest 2 to return `nil` in this situation, 1071 // if the scheduler gets the cancel done before StartRequest finishes; 1072 // the test handles this without regard to whether the implementation will ever do that. 1073 // 8. Check that the above took exactly 1 second. 1074 func TestContextCancel(t *testing.T) { 1075 metrics.Register() 1076 metrics.Reset() 1077 now := time.Now() 1078 clk, counter := testeventclock.NewFake(now, 0, nil) 1079 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 1080 qCfg := fq.QueuingConfig{ 1081 Name: "TestContextCancel", 1082 DesiredNumQueues: 11, 1083 QueueLengthLimit: 11, 1084 HandSize: 1, 1085 } 1086 seatDemandIntegratorSubject := fq.NewNamedIntegrator(clk, qCfg.Name) 1087 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), seatDemandIntegratorSubject) 1088 if err != nil { 1089 t.Fatal(err) 1090 } 1091 qs := qsComplete(qsc, 1) 1092 counter.Add(1) // account for main activity of the goroutine running this test 1093 ctx1 := context.Background() 1094 pZero := func() *int32 { var zero int32; return &zero } 1095 // counts of calls to the QueueNoteFns 1096 queueNoteCounts := map[int]map[bool]*int32{ 1097 1: {false: pZero(), true: pZero()}, 1098 2: {false: pZero(), true: pZero()}, 1099 } 1100 queueNoteFn := func(fn int) func(inQueue bool) { 1101 return func(inQueue bool) { atomic.AddInt32(queueNoteCounts[fn][inQueue], 1) } 1102 } 1103 fatalErrs := []string{} 1104 var errsLock sync.Mutex 1105 expectQNCount := func(fn int, inQueue bool, expect int32) { 1106 if a := atomic.LoadInt32(queueNoteCounts[fn][inQueue]); a != expect { 1107 errsLock.Lock() 1108 defer errsLock.Unlock() 1109 fatalErrs = append(fatalErrs, fmt.Sprintf("Got %d calls to queueNoteFn%d(%v), expected %d", a, fn, inQueue, expect)) 1110 } 1111 } 1112 expectQNCounts := func(fn int, expectF, expectT int32) { 1113 expectQNCount(fn, false, expectF) 1114 expectQNCount(fn, true, expectT) 1115 } 1116 req1, _ := qs.StartRequest(ctx1, &fcrequest.WorkEstimate{InitialSeats: 1}, 1, "", "fs1", "test", "one", queueNoteFn(1)) 1117 if req1 == nil { 1118 t.Error("Request rejected") 1119 return 1120 } 1121 expectQNCounts(1, 1, 1) 1122 var executed1, idle1 bool 1123 counter.Add(1) // account for the following goroutine 1124 go func() { 1125 defer counter.Add(-1) // account completion of this goroutine 1126 idle1 = req1.Finish(func() { 1127 executed1 = true 1128 ctx2, cancel2 := context.WithCancel(context.Background()) 1129 tBefore := clk.Now() 1130 counter.Add(1) // account for the following goroutine 1131 go func() { 1132 defer counter.Add(-1) // account completion of this goroutine 1133 clk.Sleep(time.Second) 1134 expectQNCounts(2, 0, 1) 1135 // account for unblocking the goroutine that waits on cancelation 1136 counter.Add(1) 1137 cancel2() 1138 }() 1139 req2, idle2a := qs.StartRequest(ctx2, &fcrequest.WorkEstimate{InitialSeats: 1}, 2, "", "fs2", "test", "two", queueNoteFn(2)) 1140 if idle2a { 1141 t.Error("2nd StartRequest returned idle") 1142 } 1143 if req2 != nil { 1144 idle2b := req2.Finish(func() { 1145 t.Error("Executing req2") 1146 }) 1147 if idle2b { 1148 t.Error("2nd Finish returned idle") 1149 } 1150 expectQNCounts(2, 1, 1) 1151 } 1152 tAfter := clk.Now() 1153 dt := tAfter.Sub(tBefore) 1154 if dt != time.Second { 1155 t.Errorf("Unexpected: dt=%d", dt) 1156 } 1157 }) 1158 }() 1159 counter.Add(-1) // completion of main activity of goroutine running this test 1160 clk.Run(nil) 1161 errsLock.Lock() 1162 defer errsLock.Unlock() 1163 if len(fatalErrs) > 0 { 1164 t.Error(strings.Join(fatalErrs, "; ")) 1165 } 1166 if !executed1 { 1167 t.Errorf("Unexpected: executed1=%v", executed1) 1168 } 1169 if !idle1 { 1170 t.Error("Not idle at the end") 1171 } 1172 } 1173 1174 func countingPromiseFactoryFactory(activeCounter counter.GoRoutineCounter) promiseFactoryFactory { 1175 return func(qs *queueSet) promiseFactory { 1176 return func(initial interface{}, doneCtx context.Context, doneVal interface{}) promise.WriteOnce { 1177 return testpromise.NewCountingWriteOnce(activeCounter, &qs.lock, initial, doneCtx.Done(), doneVal) 1178 } 1179 } 1180 } 1181 1182 func TestTotalRequestsExecutingWithPanic(t *testing.T) { 1183 metrics.Register() 1184 metrics.Reset() 1185 now := time.Now() 1186 clk, counter := testeventclock.NewFake(now, 0, nil) 1187 qsf := newTestableQueueSetFactory(clk, countingPromiseFactoryFactory(counter)) 1188 qCfg := fq.QueuingConfig{ 1189 Name: "TestTotalRequestsExecutingWithPanic", 1190 DesiredNumQueues: 0, 1191 } 1192 qsc, err := qsf.BeginConstruction(qCfg, newGaugePair(clk), newExecSeatsGauge(clk), fq.NewNamedIntegrator(clk, qCfg.Name)) 1193 if err != nil { 1194 t.Fatal(err) 1195 } 1196 qs := qsComplete(qsc, 1) 1197 counter.Add(1) // account for the goroutine running this test 1198 1199 queue, ok := qs.(*queueSet) 1200 if !ok { 1201 t.Fatalf("expected a QueueSet of type: %T but got: %T", &queueSet{}, qs) 1202 } 1203 if queue.totRequestsExecuting != 0 { 1204 t.Fatalf("precondition: expected total requests currently executing of the QueueSet to be 0, but got: %d", queue.totRequestsExecuting) 1205 } 1206 if queue.dCfg.ConcurrencyLimit != 1 { 1207 t.Fatalf("precondition: expected concurrency limit of the QueueSet to be 1, but got: %d", queue.dCfg.ConcurrencyLimit) 1208 } 1209 1210 ctx := context.Background() 1211 req, _ := qs.StartRequest(ctx, &fcrequest.WorkEstimate{InitialSeats: 1}, 1, "", "fs", "test", "one", func(inQueue bool) {}) 1212 if req == nil { 1213 t.Fatal("expected a Request object from StartRequest, but got nil") 1214 } 1215 1216 panicErrExpected := errors.New("apiserver panic'd") 1217 var panicErrGot interface{} 1218 func() { 1219 defer func() { 1220 panicErrGot = recover() 1221 }() 1222 1223 req.Finish(func() { 1224 // verify that total requests executing goes up by 1 since the request is executing. 1225 if queue.totRequestsExecuting != 1 { 1226 t.Fatalf("expected total requests currently executing of the QueueSet to be 1, but got: %d", queue.totRequestsExecuting) 1227 } 1228 1229 panic(panicErrExpected) 1230 }) 1231 }() 1232 1233 // verify that the panic was from us (above) 1234 if panicErrExpected != panicErrGot { 1235 t.Errorf("expected panic error: %#v, but got: %#v", panicErrExpected, panicErrGot) 1236 } 1237 if queue.totRequestsExecuting != 0 { 1238 t.Errorf("expected total requests currently executing of the QueueSet to be 0, but got: %d", queue.totRequestsExecuting) 1239 } 1240 } 1241 1242 func TestFindDispatchQueueLocked(t *testing.T) { 1243 const G = 3 * time.Millisecond 1244 qs0 := &queueSet{estimatedServiceDuration: G} 1245 tests := []struct { 1246 name string 1247 robinIndex int 1248 concurrencyLimit int 1249 totSeatsInUse int 1250 queues []*queue 1251 attempts int 1252 beforeSelectQueueLocked func(attempt int, qs *queueSet) 1253 minQueueIndexExpected []int 1254 robinIndexExpected []int 1255 }{ 1256 { 1257 name: "width1=1, seats are available, the queue with less virtual start time wins", 1258 concurrencyLimit: 1, 1259 totSeatsInUse: 0, 1260 robinIndex: -1, 1261 queues: []*queue{ 1262 { 1263 nextDispatchR: fcrequest.SeatsTimesDuration(1, 200*time.Second), 1264 requestsWaiting: newFIFO( 1265 &request{workEstimate: qs0.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 1})}, 1266 ), 1267 requestsExecuting: sets.New[*request](), 1268 }, 1269 { 1270 nextDispatchR: fcrequest.SeatsTimesDuration(1, 100*time.Second), 1271 requestsWaiting: newFIFO( 1272 &request{workEstimate: qs0.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 1})}, 1273 ), 1274 }, 1275 }, 1276 attempts: 1, 1277 minQueueIndexExpected: []int{1}, 1278 robinIndexExpected: []int{1}, 1279 }, 1280 { 1281 name: "width1=1, all seats are occupied, no queue is picked", 1282 concurrencyLimit: 1, 1283 totSeatsInUse: 1, 1284 robinIndex: -1, 1285 queues: []*queue{ 1286 { 1287 nextDispatchR: fcrequest.SeatsTimesDuration(1, 200*time.Second), 1288 requestsWaiting: newFIFO( 1289 &request{workEstimate: qs0.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 1})}, 1290 ), 1291 requestsExecuting: sets.New[*request](), 1292 }, 1293 }, 1294 attempts: 1, 1295 minQueueIndexExpected: []int{-1}, 1296 robinIndexExpected: []int{0}, 1297 }, 1298 { 1299 name: "width1 > 1, seats are available for request with the least finish R, queue is picked", 1300 concurrencyLimit: 50, 1301 totSeatsInUse: 25, 1302 robinIndex: -1, 1303 queues: []*queue{ 1304 { 1305 nextDispatchR: fcrequest.SeatsTimesDuration(1, 200*time.Second), 1306 requestsWaiting: newFIFO( 1307 &request{workEstimate: qs0.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 50})}, 1308 ), 1309 requestsExecuting: sets.New[*request](), 1310 }, 1311 { 1312 nextDispatchR: fcrequest.SeatsTimesDuration(1, 100*time.Second), 1313 requestsWaiting: newFIFO( 1314 &request{workEstimate: qs0.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 25})}, 1315 ), 1316 requestsExecuting: sets.New[*request](), 1317 }, 1318 }, 1319 attempts: 1, 1320 minQueueIndexExpected: []int{1}, 1321 robinIndexExpected: []int{1}, 1322 }, 1323 { 1324 name: "width1 > 1, seats are not available for request with the least finish R, queue is not picked", 1325 concurrencyLimit: 50, 1326 totSeatsInUse: 26, 1327 robinIndex: -1, 1328 queues: []*queue{ 1329 { 1330 nextDispatchR: fcrequest.SeatsTimesDuration(1, 200*time.Second), 1331 requestsWaiting: newFIFO( 1332 &request{workEstimate: qs0.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 10})}, 1333 ), 1334 requestsExecuting: sets.New[*request](), 1335 }, 1336 { 1337 nextDispatchR: fcrequest.SeatsTimesDuration(1, 100*time.Second), 1338 requestsWaiting: newFIFO( 1339 &request{workEstimate: qs0.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 25})}, 1340 ), 1341 requestsExecuting: sets.New[*request](), 1342 }, 1343 }, 1344 attempts: 3, 1345 minQueueIndexExpected: []int{-1, -1, -1}, 1346 robinIndexExpected: []int{1, 1, 1}, 1347 }, 1348 { 1349 name: "width1 > 1, seats become available before 3rd attempt, queue is picked", 1350 concurrencyLimit: 50, 1351 totSeatsInUse: 26, 1352 robinIndex: -1, 1353 queues: []*queue{ 1354 { 1355 nextDispatchR: fcrequest.SeatsTimesDuration(1, 200*time.Second), 1356 requestsWaiting: newFIFO( 1357 &request{workEstimate: qs0.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 10})}, 1358 ), 1359 requestsExecuting: sets.New[*request](), 1360 }, 1361 { 1362 nextDispatchR: fcrequest.SeatsTimesDuration(1, 100*time.Second), 1363 requestsWaiting: newFIFO( 1364 &request{workEstimate: qs0.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 25})}, 1365 ), 1366 requestsExecuting: sets.New[*request](), 1367 }, 1368 }, 1369 beforeSelectQueueLocked: func(attempt int, qs *queueSet) { 1370 if attempt == 3 { 1371 qs.totSeatsInUse = 25 1372 } 1373 }, 1374 attempts: 3, 1375 minQueueIndexExpected: []int{-1, -1, 1}, 1376 robinIndexExpected: []int{1, 1, 1}, 1377 }, 1378 } 1379 1380 for _, test := range tests { 1381 t.Run(test.name, func(t *testing.T) { 1382 qs := &queueSet{ 1383 estimatedServiceDuration: G, 1384 seatDemandIntegrator: fq.NewNamedIntegrator(clock.RealClock{}, "seatDemandSubject"), 1385 robinIndex: test.robinIndex, 1386 totSeatsInUse: test.totSeatsInUse, 1387 qCfg: fq.QueuingConfig{Name: "TestSelectQueueLocked/" + test.name}, 1388 dCfg: fq.DispatchingConfig{ 1389 ConcurrencyLimit: test.concurrencyLimit, 1390 }, 1391 queues: test.queues, 1392 } 1393 1394 t.Logf("QS: robin index=%d, seats in use=%d limit=%d", qs.robinIndex, qs.totSeatsInUse, qs.dCfg.ConcurrencyLimit) 1395 1396 for i := 0; i < test.attempts; i++ { 1397 attempt := i + 1 1398 if test.beforeSelectQueueLocked != nil { 1399 test.beforeSelectQueueLocked(attempt, qs) 1400 } 1401 1402 var minQueueExpected *queue 1403 if queueIdx := test.minQueueIndexExpected[i]; queueIdx >= 0 { 1404 minQueueExpected = test.queues[queueIdx] 1405 } 1406 1407 minQueueGot, reqGot := qs.findDispatchQueueToBoundLocked() 1408 if minQueueExpected != minQueueGot { 1409 t.Errorf("Expected queue: %#v, but got: %#v", minQueueExpected, minQueueGot) 1410 } 1411 1412 robinIndexExpected := test.robinIndexExpected[i] 1413 if robinIndexExpected != qs.robinIndex { 1414 t.Errorf("Expected robin index: %d for attempt: %d, but got: %d", robinIndexExpected, attempt, qs.robinIndex) 1415 } 1416 1417 if (reqGot == nil) != (minQueueGot == nil) { 1418 t.Errorf("reqGot=%p but minQueueGot=%p", reqGot, minQueueGot) 1419 } 1420 } 1421 }) 1422 } 1423 } 1424 1425 func TestFinishRequestLocked(t *testing.T) { 1426 tests := []struct { 1427 name string 1428 workEstimate fcrequest.WorkEstimate 1429 }{ 1430 { 1431 name: "request has additional latency", 1432 workEstimate: fcrequest.WorkEstimate{ 1433 InitialSeats: 1, 1434 FinalSeats: 10, 1435 AdditionalLatency: time.Minute, 1436 }, 1437 }, 1438 { 1439 name: "request has no additional latency", 1440 workEstimate: fcrequest.WorkEstimate{ 1441 InitialSeats: 10, 1442 }, 1443 }, 1444 } 1445 1446 metrics.Register() 1447 for _, test := range tests { 1448 t.Run(test.name, func(t *testing.T) { 1449 metrics.Reset() 1450 1451 now := time.Now() 1452 clk, _ := testeventclock.NewFake(now, 0, nil) 1453 qs := &queueSet{ 1454 clock: clk, 1455 estimatedServiceDuration: time.Second, 1456 reqsGaugePair: newGaugePair(clk), 1457 execSeatsGauge: newExecSeatsGauge(clk), 1458 seatDemandIntegrator: fq.NewNamedIntegrator(clk, "seatDemandSubject"), 1459 } 1460 queue := &queue{ 1461 requestsWaiting: newRequestFIFO(), 1462 requestsExecuting: sets.New[*request](), 1463 } 1464 r := &request{ 1465 qs: qs, 1466 queue: queue, 1467 workEstimate: qs.completeWorkEstimate(&test.workEstimate), 1468 } 1469 rOther := &request{qs: qs, queue: queue} 1470 1471 qs.totRequestsExecuting = 111 1472 qs.totSeatsInUse = 222 1473 queue.requestsExecuting = sets.New(r, rOther) 1474 queue.seatsInUse = 22 1475 1476 var ( 1477 queuesetTotalRequestsExecutingExpected = qs.totRequestsExecuting - 1 1478 queuesetTotalSeatsInUseExpected = qs.totSeatsInUse - test.workEstimate.MaxSeats() 1479 queueRequestsExecutingExpected = sets.New(rOther) 1480 queueSeatsInUseExpected = queue.seatsInUse - test.workEstimate.MaxSeats() 1481 ) 1482 1483 qs.finishRequestLocked(r) 1484 1485 // as soon as AdditionalLatency elapses we expect the seats to be released 1486 clk.SetTime(now.Add(test.workEstimate.AdditionalLatency)) 1487 1488 if queuesetTotalRequestsExecutingExpected != qs.totRequestsExecuting { 1489 t.Errorf("Expected total requests executing: %d, but got: %d", queuesetTotalRequestsExecutingExpected, qs.totRequestsExecuting) 1490 } 1491 if queuesetTotalSeatsInUseExpected != qs.totSeatsInUse { 1492 t.Errorf("Expected total seats in use: %d, but got: %d", queuesetTotalSeatsInUseExpected, qs.totSeatsInUse) 1493 } 1494 if !queueRequestsExecutingExpected.Equal(queue.requestsExecuting) { 1495 t.Errorf("Expected requests executing for queue: %v, but got: %v", queueRequestsExecutingExpected, queue.requestsExecuting) 1496 } 1497 if queueSeatsInUseExpected != queue.seatsInUse { 1498 t.Errorf("Expected seats in use for queue: %d, but got: %d", queueSeatsInUseExpected, queue.seatsInUse) 1499 } 1500 }) 1501 } 1502 } 1503 1504 func TestRequestSeats(t *testing.T) { 1505 qs := &queueSet{estimatedServiceDuration: time.Second} 1506 tests := []struct { 1507 name string 1508 request *request 1509 expected int 1510 }{ 1511 { 1512 name: "", 1513 request: &request{workEstimate: qs.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 3, FinalSeats: 3})}, 1514 expected: 3, 1515 }, 1516 { 1517 name: "", 1518 request: &request{workEstimate: qs.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 1, FinalSeats: 3})}, 1519 expected: 3, 1520 }, 1521 { 1522 name: "", 1523 request: &request{workEstimate: qs.completeWorkEstimate(&fcrequest.WorkEstimate{InitialSeats: 3, FinalSeats: 1})}, 1524 expected: 3, 1525 }, 1526 } 1527 1528 for _, test := range tests { 1529 t.Run(test.name, func(t *testing.T) { 1530 seatsGot := test.request.MaxSeats() 1531 if test.expected != seatsGot { 1532 t.Errorf("Expected seats: %d, got %d", test.expected, seatsGot) 1533 } 1534 }) 1535 } 1536 } 1537 1538 func TestRequestWork(t *testing.T) { 1539 qs := &queueSet{estimatedServiceDuration: 2 * time.Second} 1540 request := &request{ 1541 workEstimate: qs.completeWorkEstimate(&fcrequest.WorkEstimate{ 1542 InitialSeats: 3, 1543 FinalSeats: 50, 1544 AdditionalLatency: 70 * time.Second, 1545 }), 1546 } 1547 1548 got := request.totalWork() 1549 want := fcrequest.SeatsTimesDuration(3, 2*time.Second) + fcrequest.SeatsTimesDuration(50, 70*time.Second) 1550 if want != got { 1551 t.Errorf("Expected totalWork: %v, but got: %v", want, got) 1552 } 1553 } 1554 1555 func newFIFO(requests ...*request) fifo { 1556 l := newRequestFIFO() 1557 for i := range requests { 1558 requests[i].removeFromQueueLocked = l.Enqueue(requests[i]) 1559 } 1560 return l 1561 } 1562 1563 func newGaugePair(clk clock.PassiveClock) metrics.RatioedGaugePair { 1564 return metrics.RatioedGaugeVecPhasedElementPair(metrics.PriorityLevelConcurrencyGaugeVec, 1, 1, []string{"test"}) 1565 } 1566 1567 func newExecSeatsGauge(clk clock.PassiveClock) metrics.RatioedGauge { 1568 return metrics.PriorityLevelExecutionSeatsGaugeVec.NewForLabelValuesSafe(0, 1, []string{"test"}) 1569 } 1570 1571 func float64close(x, y float64) bool { 1572 x0 := float64NaNTo0(x) 1573 y0 := float64NaNTo0(y) 1574 diff := math.Abs(x0 - y0) 1575 den := math.Max(math.Abs(x0), math.Abs(y0)) 1576 return den == 0 || diff/den < 1e-10 1577 } 1578 1579 func uint64max(a, b uint64) uint64 { 1580 if b > a { 1581 return b 1582 } 1583 return a 1584 } 1585 1586 func float64NaNTo0(x float64) float64 { 1587 if math.IsNaN(x) { 1588 return 0 1589 } 1590 return x 1591 } 1592 1593 func qsComplete(qsc fq.QueueSetCompleter, concurrencyLimit int) fq.QueueSet { 1594 concurrencyDenominator := concurrencyLimit 1595 if concurrencyDenominator <= 0 { 1596 concurrencyDenominator = 1 1597 } 1598 return qsc.Complete(fq.DispatchingConfig{ConcurrencyLimit: concurrencyLimit, ConcurrencyDenominator: concurrencyDenominator}) 1599 }