/* * * Copyright 2020 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package adaptive import ( "sync" "testing" "time" ) // stats returns a tuple with accepts, throttles for the current time. func (th *Throttler) stats() (int64, int64) { now := timeNowFunc() th.mu.Lock() a, t := th.accepts.sum(now), th.throttles.sum(now) th.mu.Unlock() return a, t } // Enums for responses. const ( E = iota // No response A // Accepted T // Throttled ) func TestRegisterBackendResponse(t *testing.T) { testcases := []struct { desc string bins int64 ticks []int64 responses []int64 wantAccepts []int64 wantThrottled []int64 }{ { "Accumulate", 3, []int64{0, 1, 2}, // Ticks []int64{A, T, E}, // Responses []int64{1, 1, 1}, // Accepts []int64{0, 1, 1}, // Throttled }, { "LightTimeTravel", 3, []int64{1, 0, 2}, // Ticks []int64{A, T, E}, // Response []int64{1, 1, 1}, // Accepts []int64{0, 1, 1}, // Throttled }, { "HeavyTimeTravel", 3, []int64{8, 0, 9}, // Ticks []int64{A, A, A}, // Response []int64{1, 1, 2}, // Accepts []int64{0, 0, 0}, // Throttled }, { "Rollover", 1, []int64{0, 1, 2}, // Ticks []int64{A, T, E}, // Responses []int64{1, 0, 0}, // Accepts []int64{0, 1, 0}, // Throttled }, } m := mockClock{} oldTimeNowFunc := timeNowFunc timeNowFunc = m.Now defer func() { timeNowFunc = oldTimeNowFunc }() for _, test := range testcases { t.Run(test.desc, func(t *testing.T) { th := newWithArgs(time.Duration(test.bins), test.bins, 2.0, 8) for i, tick := range test.ticks { m.SetNanos(tick) if test.responses[i] != E { th.RegisterBackendResponse(test.responses[i] == T) } if gotAccepts, gotThrottled := th.stats(); gotAccepts != test.wantAccepts[i] || gotThrottled != test.wantThrottled[i] { t.Errorf("th.stats() = {%d, %d} for index %d, want {%d, %d}", i, gotAccepts, gotThrottled, test.wantAccepts[i], test.wantThrottled[i]) } } }) } } func TestShouldThrottleOptions(t *testing.T) { // ShouldThrottle should return true iff // (requests - RatioForAccepts * accepts) / (requests + RequestsPadding) <= p // where p is a random number. For the purposes of this test it's fixed // to 0.5. responses := []int64{T, T, T, T, T, T, T, T, T, A, A, A, A, A, A, T, T, T, T} n := false y := true testcases := []struct { desc string ratioForAccepts float64 requestsPadding float64 want []bool }{ { "Baseline", 1.1, 8, []bool{n, n, n, n, n, n, n, n, y, y, y, y, y, n, n, n, y, y, y}, }, { "ChangePadding", 1.1, 7, []bool{n, n, n, n, n, n, n, y, y, y, y, y, y, y, y, y, y, y, y}, }, { "ChangeRatioForAccepts", 1.4, 8, []bool{n, n, n, n, n, n, n, n, y, y, n, n, n, n, n, n, n, n, n}, }, } m := mockClock{} oldTimeNowFunc := timeNowFunc timeNowFunc = m.Now oldRandFunc := randFunc randFunc = func() float64 { return 0.5 } defer func() { timeNowFunc = oldTimeNowFunc randFunc = oldRandFunc }() for _, test := range testcases { t.Run(test.desc, func(t *testing.T) { m.SetNanos(0) th := newWithArgs(time.Duration(time.Nanosecond), 1, test.ratioForAccepts, test.requestsPadding) for i, response := range responses { if response != E { th.RegisterBackendResponse(response == T) } if got := th.ShouldThrottle(); got != test.want[i] { t.Errorf("ShouldThrottle for index %d: got %v, want %v", i, got, test.want[i]) } } }) } } func TestParallel(t *testing.T) { // Uses all the defaults which comes with a 30 second duration. th := New() testDuration := 2 * time.Second numRoutines := 10 accepts := make([]int64, numRoutines) throttles := make([]int64, numRoutines) var wg sync.WaitGroup for i := 0; i < numRoutines; i++ { wg.Add(1) go func(num int) { defer wg.Done() ticker := time.NewTicker(testDuration) var accept int64 var throttle int64 for i := 0; ; i++ { select { case <-ticker.C: ticker.Stop() accepts[num] = accept throttles[num] = throttle return default: if i%2 == 0 { th.RegisterBackendResponse(true) throttle++ } else { th.RegisterBackendResponse(false) accept++ } } } }(i) } wg.Wait() var wantAccepts, wantThrottles int64 for i := 0; i < numRoutines; i++ { wantAccepts += accepts[i] wantThrottles += throttles[i] } if gotAccepts, gotThrottles := th.stats(); gotAccepts != wantAccepts || gotThrottles != wantThrottles { t.Errorf("th.stats() = {%d, %d}, want {%d, %d}", gotAccepts, gotThrottles, wantAccepts, wantThrottles) } } type mockClock struct { t time.Time } func (m *mockClock) Now() time.Time { return m.t } func (m *mockClock) SetNanos(n int64) { m.t = time.Unix(0, n) }