mirror of https://github.com/grpc/grpc-go.git
447 lines
13 KiB
Go
447 lines
13 KiB
Go
/*
|
|
*
|
|
* 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 load
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
)
|
|
|
|
var (
|
|
dropCategories = []string{"drop_for_real", "drop_for_fun"}
|
|
localities = []string{"locality-A", "locality-B"}
|
|
errTest = fmt.Errorf("test error")
|
|
)
|
|
|
|
// rpcData wraps the rpc counts and load data to be pushed to the store.
|
|
type rpcData struct {
|
|
start, success, failure int
|
|
serverData map[string]float64 // Will be reported with successful RPCs.
|
|
}
|
|
|
|
// TestDrops spawns a bunch of goroutines which report drop data. After the
|
|
// goroutines have exited, the test dumps the stats from the Store and makes
|
|
// sure they are as expected.
|
|
func TestDrops(t *testing.T) {
|
|
var (
|
|
drops = map[string]int{
|
|
dropCategories[0]: 30,
|
|
dropCategories[1]: 40,
|
|
"": 10,
|
|
}
|
|
wantStoreData = &Data{
|
|
TotalDrops: 80,
|
|
Drops: map[string]uint64{
|
|
dropCategories[0]: 30,
|
|
dropCategories[1]: 40,
|
|
},
|
|
}
|
|
)
|
|
|
|
ls := perClusterStore{}
|
|
var wg sync.WaitGroup
|
|
for category, count := range drops {
|
|
for i := 0; i < count; i++ {
|
|
wg.Add(1)
|
|
go func(c string) {
|
|
ls.CallDropped(c)
|
|
wg.Done()
|
|
}(category)
|
|
}
|
|
}
|
|
wg.Wait()
|
|
|
|
gotStoreData := ls.stats()
|
|
if diff := cmp.Diff(wantStoreData, gotStoreData, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval")); diff != "" {
|
|
t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// TestLocalityStats spawns a bunch of goroutines which report rpc and load
|
|
// data. After the goroutines have exited, the test dumps the stats from the
|
|
// Store and makes sure they are as expected.
|
|
func TestLocalityStats(t *testing.T) {
|
|
var (
|
|
localityData = map[string]rpcData{
|
|
localities[0]: {
|
|
start: 40,
|
|
success: 20,
|
|
failure: 10,
|
|
serverData: map[string]float64{"net": 1, "disk": 2, "cpu": 3, "mem": 4},
|
|
},
|
|
localities[1]: {
|
|
start: 80,
|
|
success: 40,
|
|
failure: 20,
|
|
serverData: map[string]float64{"net": 1, "disk": 2, "cpu": 3, "mem": 4},
|
|
},
|
|
}
|
|
wantStoreData = &Data{
|
|
LocalityStats: map[string]LocalityData{
|
|
localities[0]: {
|
|
RequestStats: RequestData{Succeeded: 20, Errored: 10, InProgress: 10},
|
|
LoadStats: map[string]ServerLoadData{
|
|
"net": {Count: 20, Sum: 20},
|
|
"disk": {Count: 20, Sum: 40},
|
|
"cpu": {Count: 20, Sum: 60},
|
|
"mem": {Count: 20, Sum: 80},
|
|
},
|
|
},
|
|
localities[1]: {
|
|
RequestStats: RequestData{Succeeded: 40, Errored: 20, InProgress: 20},
|
|
LoadStats: map[string]ServerLoadData{
|
|
"net": {Count: 40, Sum: 40},
|
|
"disk": {Count: 40, Sum: 80},
|
|
"cpu": {Count: 40, Sum: 120},
|
|
"mem": {Count: 40, Sum: 160},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
ls := perClusterStore{}
|
|
var wg sync.WaitGroup
|
|
for locality, data := range localityData {
|
|
wg.Add(data.start)
|
|
for i := 0; i < data.start; i++ {
|
|
go func(l string) {
|
|
ls.CallStarted(l)
|
|
wg.Done()
|
|
}(locality)
|
|
}
|
|
// The calls to callStarted() need to happen before the other calls are
|
|
// made. Hence the wait here.
|
|
wg.Wait()
|
|
|
|
wg.Add(data.success)
|
|
for i := 0; i < data.success; i++ {
|
|
go func(l string, serverData map[string]float64) {
|
|
ls.CallFinished(l, nil)
|
|
for n, d := range serverData {
|
|
ls.CallServerLoad(l, n, d)
|
|
}
|
|
wg.Done()
|
|
}(locality, data.serverData)
|
|
}
|
|
wg.Add(data.failure)
|
|
for i := 0; i < data.failure; i++ {
|
|
go func(l string) {
|
|
ls.CallFinished(l, errTest)
|
|
wg.Done()
|
|
}(locality)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
gotStoreData := ls.stats()
|
|
if diff := cmp.Diff(wantStoreData, gotStoreData, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval")); diff != "" {
|
|
t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestResetAfterStats(t *testing.T) {
|
|
// Push a bunch of drops, call stats and load stats, and leave inProgress to be non-zero.
|
|
// Dump the stats. Verify expected
|
|
// Push the same set of loads as before
|
|
// Now dump and verify the newly expected ones.
|
|
var (
|
|
drops = map[string]int{
|
|
dropCategories[0]: 30,
|
|
dropCategories[1]: 40,
|
|
}
|
|
localityData = map[string]rpcData{
|
|
localities[0]: {
|
|
start: 40,
|
|
success: 20,
|
|
failure: 10,
|
|
serverData: map[string]float64{"net": 1, "disk": 2, "cpu": 3, "mem": 4},
|
|
},
|
|
localities[1]: {
|
|
start: 80,
|
|
success: 40,
|
|
failure: 20,
|
|
serverData: map[string]float64{"net": 1, "disk": 2, "cpu": 3, "mem": 4},
|
|
},
|
|
}
|
|
wantStoreData = &Data{
|
|
TotalDrops: 70,
|
|
Drops: map[string]uint64{
|
|
dropCategories[0]: 30,
|
|
dropCategories[1]: 40,
|
|
},
|
|
LocalityStats: map[string]LocalityData{
|
|
localities[0]: {
|
|
RequestStats: RequestData{Succeeded: 20, Errored: 10, InProgress: 10},
|
|
LoadStats: map[string]ServerLoadData{
|
|
"net": {Count: 20, Sum: 20},
|
|
"disk": {Count: 20, Sum: 40},
|
|
"cpu": {Count: 20, Sum: 60},
|
|
"mem": {Count: 20, Sum: 80},
|
|
},
|
|
},
|
|
localities[1]: {
|
|
RequestStats: RequestData{Succeeded: 40, Errored: 20, InProgress: 20},
|
|
LoadStats: map[string]ServerLoadData{
|
|
"net": {Count: 40, Sum: 40},
|
|
"disk": {Count: 40, Sum: 80},
|
|
"cpu": {Count: 40, Sum: 120},
|
|
"mem": {Count: 40, Sum: 160},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
reportLoad := func(ls *perClusterStore) {
|
|
for category, count := range drops {
|
|
for i := 0; i < count; i++ {
|
|
ls.CallDropped(category)
|
|
}
|
|
}
|
|
for locality, data := range localityData {
|
|
for i := 0; i < data.start; i++ {
|
|
ls.CallStarted(locality)
|
|
}
|
|
for i := 0; i < data.success; i++ {
|
|
ls.CallFinished(locality, nil)
|
|
for n, d := range data.serverData {
|
|
ls.CallServerLoad(locality, n, d)
|
|
}
|
|
}
|
|
for i := 0; i < data.failure; i++ {
|
|
ls.CallFinished(locality, errTest)
|
|
}
|
|
}
|
|
}
|
|
|
|
ls := perClusterStore{}
|
|
reportLoad(&ls)
|
|
gotStoreData := ls.stats()
|
|
if diff := cmp.Diff(wantStoreData, gotStoreData, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval")); diff != "" {
|
|
t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
|
|
}
|
|
|
|
// The above call to stats() should have reset all load reports except the
|
|
// inProgress rpc count. We are now going to push the same load data into
|
|
// the store. So, we should expect to see twice the count for inProgress.
|
|
for _, l := range localities {
|
|
ls := wantStoreData.LocalityStats[l]
|
|
ls.RequestStats.InProgress *= 2
|
|
wantStoreData.LocalityStats[l] = ls
|
|
}
|
|
reportLoad(&ls)
|
|
gotStoreData = ls.stats()
|
|
if diff := cmp.Diff(wantStoreData, gotStoreData, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval")); diff != "" {
|
|
t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
var sortDataSlice = cmp.Transformer("SortDataSlice", func(in []*Data) []*Data {
|
|
out := append([]*Data(nil), in...) // Copy input to avoid mutating it
|
|
sort.Slice(out,
|
|
func(i, j int) bool {
|
|
if out[i].Cluster < out[j].Cluster {
|
|
return true
|
|
}
|
|
if out[i].Cluster == out[j].Cluster {
|
|
return out[i].Service < out[j].Service
|
|
}
|
|
return false
|
|
},
|
|
)
|
|
return out
|
|
})
|
|
|
|
// Test all load are returned for the given clusters, and all clusters are
|
|
// reported if no cluster is specified.
|
|
func TestStoreStats(t *testing.T) {
|
|
var (
|
|
testClusters = []string{"c0", "c1", "c2"}
|
|
testServices = []string{"s0", "s1"}
|
|
testLocality = "test-locality"
|
|
)
|
|
|
|
store := NewStore()
|
|
for _, c := range testClusters {
|
|
for _, s := range testServices {
|
|
store.PerCluster(c, s).CallStarted(testLocality)
|
|
store.PerCluster(c, s).CallServerLoad(testLocality, "abc", 123)
|
|
store.PerCluster(c, s).CallDropped("dropped")
|
|
store.PerCluster(c, s).CallFinished(testLocality, nil)
|
|
}
|
|
}
|
|
|
|
wantC0 := []*Data{
|
|
{
|
|
Cluster: "c0", Service: "s0",
|
|
TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {
|
|
RequestStats: RequestData{Succeeded: 1},
|
|
LoadStats: map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Cluster: "c0", Service: "s1",
|
|
TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {
|
|
RequestStats: RequestData{Succeeded: 1},
|
|
LoadStats: map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
// Call Stats with just "c0", this should return data for "c0", and not
|
|
// touch data for other clusters.
|
|
gotC0 := store.Stats([]string{"c0"})
|
|
if diff := cmp.Diff(wantC0, gotC0, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval"), sortDataSlice); diff != "" {
|
|
t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
|
|
}
|
|
|
|
wantOther := []*Data{
|
|
{
|
|
Cluster: "c1", Service: "s0",
|
|
TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {
|
|
RequestStats: RequestData{Succeeded: 1},
|
|
LoadStats: map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Cluster: "c1", Service: "s1",
|
|
TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {
|
|
RequestStats: RequestData{Succeeded: 1},
|
|
LoadStats: map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Cluster: "c2", Service: "s0",
|
|
TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {
|
|
RequestStats: RequestData{Succeeded: 1},
|
|
LoadStats: map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Cluster: "c2", Service: "s1",
|
|
TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {
|
|
RequestStats: RequestData{Succeeded: 1},
|
|
LoadStats: map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
// Call Stats with empty slice, this should return data for all the
|
|
// remaining clusters, and not include c0 (because c0 data was cleared).
|
|
gotOther := store.Stats(nil)
|
|
if diff := cmp.Diff(wantOther, gotOther, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval"), sortDataSlice); diff != "" {
|
|
t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// Test the cases that if a cluster doesn't have load to report, its data is not
|
|
// appended to the slice returned by Stats().
|
|
func TestStoreStatsEmptyDataNotReported(t *testing.T) {
|
|
var (
|
|
testServices = []string{"s0", "s1"}
|
|
testLocality = "test-locality"
|
|
)
|
|
|
|
store := NewStore()
|
|
// "c0"'s RPCs all finish with success.
|
|
for _, s := range testServices {
|
|
store.PerCluster("c0", s).CallStarted(testLocality)
|
|
store.PerCluster("c0", s).CallFinished(testLocality, nil)
|
|
}
|
|
// "c1"'s RPCs never finish (always inprocess).
|
|
for _, s := range testServices {
|
|
store.PerCluster("c1", s).CallStarted(testLocality)
|
|
}
|
|
|
|
want0 := []*Data{
|
|
{
|
|
Cluster: "c0", Service: "s0",
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {RequestStats: RequestData{Succeeded: 1}},
|
|
},
|
|
},
|
|
{
|
|
Cluster: "c0", Service: "s1",
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {RequestStats: RequestData{Succeeded: 1}},
|
|
},
|
|
},
|
|
{
|
|
Cluster: "c1", Service: "s0",
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {RequestStats: RequestData{InProgress: 1}},
|
|
},
|
|
},
|
|
{
|
|
Cluster: "c1", Service: "s1",
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {RequestStats: RequestData{InProgress: 1}},
|
|
},
|
|
},
|
|
}
|
|
// Call Stats with empty slice, this should return data for all the
|
|
// clusters.
|
|
got0 := store.Stats(nil)
|
|
if diff := cmp.Diff(want0, got0, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval"), sortDataSlice); diff != "" {
|
|
t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
|
|
}
|
|
|
|
want1 := []*Data{
|
|
{
|
|
Cluster: "c1", Service: "s0",
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {RequestStats: RequestData{InProgress: 1}},
|
|
},
|
|
},
|
|
{
|
|
Cluster: "c1", Service: "s1",
|
|
LocalityStats: map[string]LocalityData{
|
|
"test-locality": {RequestStats: RequestData{InProgress: 1}},
|
|
},
|
|
},
|
|
}
|
|
// Call Stats with empty slice again, this should return data only for "c1",
|
|
// because "c0" data was cleared, but "c1" has in-progress RPCs.
|
|
got1 := store.Stats(nil)
|
|
if diff := cmp.Diff(want1, got1, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval"), sortDataSlice); diff != "" {
|
|
t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
|
|
}
|
|
}
|