blob: 40a846a388a4c6c8eca854d52305ecec933374e7 [file] [log] [blame]
/*
*
* 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)
}