google.golang.org/grpc@v1.72.2/xds/internal/xdsclient/load/store_test.go (about)

     1  /*
     2   *
     3   * Copyright 2020 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package load
    19  
    20  import (
    21  	"fmt"
    22  	"sort"
    23  	"sync"
    24  	"testing"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"github.com/google/go-cmp/cmp/cmpopts"
    28  )
    29  
    30  var (
    31  	dropCategories = []string{"drop_for_real", "drop_for_fun"}
    32  	localities     = []string{"locality-A", "locality-B"}
    33  	errTest        = fmt.Errorf("test error")
    34  )
    35  
    36  // rpcData wraps the rpc counts and load data to be pushed to the store.
    37  type rpcData struct {
    38  	start, success, failure int
    39  	serverData              map[string]float64 // Will be reported with successful RPCs.
    40  }
    41  
    42  // TestDrops spawns a bunch of goroutines which report drop data. After the
    43  // goroutines have exited, the test dumps the stats from the Store and makes
    44  // sure they are as expected.
    45  func TestDrops(t *testing.T) {
    46  	var (
    47  		drops = map[string]int{
    48  			dropCategories[0]: 30,
    49  			dropCategories[1]: 40,
    50  			"":                10,
    51  		}
    52  		wantStoreData = &Data{
    53  			TotalDrops: 80,
    54  			Drops: map[string]uint64{
    55  				dropCategories[0]: 30,
    56  				dropCategories[1]: 40,
    57  			},
    58  		}
    59  	)
    60  
    61  	ls := perClusterStore{}
    62  	var wg sync.WaitGroup
    63  	for category, count := range drops {
    64  		for i := 0; i < count; i++ {
    65  			wg.Add(1)
    66  			go func(c string) {
    67  				ls.CallDropped(c)
    68  				wg.Done()
    69  			}(category)
    70  		}
    71  	}
    72  	wg.Wait()
    73  
    74  	gotStoreData := ls.stats()
    75  	if diff := cmp.Diff(wantStoreData, gotStoreData, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval")); diff != "" {
    76  		t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
    77  	}
    78  }
    79  
    80  // TestLocalityStats spawns a bunch of goroutines which report rpc and load
    81  // data. After the goroutines have exited, the test dumps the stats from the
    82  // Store and makes sure they are as expected.
    83  func TestLocalityStats(t *testing.T) {
    84  	var (
    85  		localityData = map[string]rpcData{
    86  			localities[0]: {
    87  				start:      40,
    88  				success:    20,
    89  				failure:    10,
    90  				serverData: map[string]float64{"net": 1, "disk": 2, "cpu": 3, "mem": 4},
    91  			},
    92  			localities[1]: {
    93  				start:      80,
    94  				success:    40,
    95  				failure:    20,
    96  				serverData: map[string]float64{"net": 1, "disk": 2, "cpu": 3, "mem": 4},
    97  			},
    98  		}
    99  		wantStoreData = &Data{
   100  			LocalityStats: map[string]LocalityData{
   101  				localities[0]: {
   102  					RequestStats: RequestData{
   103  						Succeeded:  20,
   104  						Errored:    10,
   105  						InProgress: 10,
   106  						Issued:     40,
   107  					},
   108  					LoadStats: map[string]ServerLoadData{
   109  						"net":  {Count: 20, Sum: 20},
   110  						"disk": {Count: 20, Sum: 40},
   111  						"cpu":  {Count: 20, Sum: 60},
   112  						"mem":  {Count: 20, Sum: 80},
   113  					},
   114  				},
   115  				localities[1]: {
   116  					RequestStats: RequestData{
   117  						Succeeded:  40,
   118  						Errored:    20,
   119  						InProgress: 20,
   120  						Issued:     80,
   121  					},
   122  					LoadStats: map[string]ServerLoadData{
   123  						"net":  {Count: 40, Sum: 40},
   124  						"disk": {Count: 40, Sum: 80},
   125  						"cpu":  {Count: 40, Sum: 120},
   126  						"mem":  {Count: 40, Sum: 160},
   127  					},
   128  				},
   129  			},
   130  		}
   131  	)
   132  
   133  	ls := perClusterStore{}
   134  	var wg sync.WaitGroup
   135  	for locality, data := range localityData {
   136  		wg.Add(data.start)
   137  		for i := 0; i < data.start; i++ {
   138  			go func(l string) {
   139  				ls.CallStarted(l)
   140  				wg.Done()
   141  			}(locality)
   142  		}
   143  		// The calls to callStarted() need to happen before the other calls are
   144  		// made. Hence the wait here.
   145  		wg.Wait()
   146  
   147  		wg.Add(data.success)
   148  		for i := 0; i < data.success; i++ {
   149  			go func(l string, serverData map[string]float64) {
   150  				ls.CallFinished(l, nil)
   151  				for n, d := range serverData {
   152  					ls.CallServerLoad(l, n, d)
   153  				}
   154  				wg.Done()
   155  			}(locality, data.serverData)
   156  		}
   157  		wg.Add(data.failure)
   158  		for i := 0; i < data.failure; i++ {
   159  			go func(l string) {
   160  				ls.CallFinished(l, errTest)
   161  				wg.Done()
   162  			}(locality)
   163  		}
   164  		wg.Wait()
   165  	}
   166  
   167  	gotStoreData := ls.stats()
   168  	if diff := cmp.Diff(wantStoreData, gotStoreData, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval")); diff != "" {
   169  		t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
   170  	}
   171  }
   172  
   173  func TestResetAfterStats(t *testing.T) {
   174  	// Push a bunch of drops, call stats and load stats, and leave inProgress to be non-zero.
   175  	// Dump the stats. Verify expected
   176  	// Push the same set of loads as before
   177  	// Now dump and verify the newly expected ones.
   178  	var (
   179  		drops = map[string]int{
   180  			dropCategories[0]: 30,
   181  			dropCategories[1]: 40,
   182  		}
   183  		localityData = map[string]rpcData{
   184  			localities[0]: {
   185  				start:      40,
   186  				success:    20,
   187  				failure:    10,
   188  				serverData: map[string]float64{"net": 1, "disk": 2, "cpu": 3, "mem": 4},
   189  			},
   190  			localities[1]: {
   191  				start:      80,
   192  				success:    40,
   193  				failure:    20,
   194  				serverData: map[string]float64{"net": 1, "disk": 2, "cpu": 3, "mem": 4},
   195  			},
   196  		}
   197  		wantStoreData = &Data{
   198  			TotalDrops: 70,
   199  			Drops: map[string]uint64{
   200  				dropCategories[0]: 30,
   201  				dropCategories[1]: 40,
   202  			},
   203  			LocalityStats: map[string]LocalityData{
   204  				localities[0]: {
   205  					RequestStats: RequestData{
   206  						Succeeded:  20,
   207  						Errored:    10,
   208  						InProgress: 10,
   209  						Issued:     40,
   210  					},
   211  
   212  					LoadStats: map[string]ServerLoadData{
   213  						"net":  {Count: 20, Sum: 20},
   214  						"disk": {Count: 20, Sum: 40},
   215  						"cpu":  {Count: 20, Sum: 60},
   216  						"mem":  {Count: 20, Sum: 80},
   217  					},
   218  				},
   219  				localities[1]: {
   220  					RequestStats: RequestData{
   221  						Succeeded:  40,
   222  						Errored:    20,
   223  						InProgress: 20,
   224  						Issued:     80,
   225  					},
   226  
   227  					LoadStats: map[string]ServerLoadData{
   228  						"net":  {Count: 40, Sum: 40},
   229  						"disk": {Count: 40, Sum: 80},
   230  						"cpu":  {Count: 40, Sum: 120},
   231  						"mem":  {Count: 40, Sum: 160},
   232  					},
   233  				},
   234  			},
   235  		}
   236  	)
   237  
   238  	reportLoad := func(ls *perClusterStore) {
   239  		for category, count := range drops {
   240  			for i := 0; i < count; i++ {
   241  				ls.CallDropped(category)
   242  			}
   243  		}
   244  		for locality, data := range localityData {
   245  			for i := 0; i < data.start; i++ {
   246  				ls.CallStarted(locality)
   247  			}
   248  			for i := 0; i < data.success; i++ {
   249  				ls.CallFinished(locality, nil)
   250  				for n, d := range data.serverData {
   251  					ls.CallServerLoad(locality, n, d)
   252  				}
   253  			}
   254  			for i := 0; i < data.failure; i++ {
   255  				ls.CallFinished(locality, errTest)
   256  			}
   257  		}
   258  	}
   259  
   260  	ls := perClusterStore{}
   261  	reportLoad(&ls)
   262  	gotStoreData := ls.stats()
   263  	if diff := cmp.Diff(wantStoreData, gotStoreData, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval")); diff != "" {
   264  		t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
   265  	}
   266  
   267  	// The above call to stats() should have reset all load reports except the
   268  	// inProgress rpc count. We are now going to push the same load data into
   269  	// the store. So, we should expect to see twice the count for inProgress.
   270  	for _, l := range localities {
   271  		ls := wantStoreData.LocalityStats[l]
   272  		ls.RequestStats.InProgress *= 2
   273  		wantStoreData.LocalityStats[l] = ls
   274  	}
   275  	reportLoad(&ls)
   276  	gotStoreData = ls.stats()
   277  	if diff := cmp.Diff(wantStoreData, gotStoreData, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval")); diff != "" {
   278  		t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
   279  	}
   280  }
   281  
   282  var sortDataSlice = cmp.Transformer("SortDataSlice", func(in []*Data) []*Data {
   283  	out := append([]*Data(nil), in...) // Copy input to avoid mutating it
   284  	sort.Slice(out,
   285  		func(i, j int) bool {
   286  			if out[i].Cluster < out[j].Cluster {
   287  				return true
   288  			}
   289  			if out[i].Cluster == out[j].Cluster {
   290  				return out[i].Service < out[j].Service
   291  			}
   292  			return false
   293  		},
   294  	)
   295  	return out
   296  })
   297  
   298  // Test all load are returned for the given clusters, and all clusters are
   299  // reported if no cluster is specified.
   300  func TestStoreStats(t *testing.T) {
   301  	var (
   302  		testClusters = []string{"c0", "c1", "c2"}
   303  		testServices = []string{"s0", "s1"}
   304  		testLocality = "test-locality"
   305  	)
   306  
   307  	store := NewStore()
   308  	for _, c := range testClusters {
   309  		for _, s := range testServices {
   310  			store.PerCluster(c, s).CallStarted(testLocality)
   311  			store.PerCluster(c, s).CallServerLoad(testLocality, "abc", 123)
   312  			store.PerCluster(c, s).CallDropped("dropped")
   313  			store.PerCluster(c, s).CallFinished(testLocality, nil)
   314  		}
   315  	}
   316  
   317  	wantC0 := []*Data{
   318  		{
   319  			Cluster: "c0", Service: "s0",
   320  			TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
   321  			LocalityStats: map[string]LocalityData{
   322  				"test-locality": {
   323  					RequestStats: RequestData{Succeeded: 1, Issued: 1},
   324  					LoadStats:    map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
   325  				},
   326  			},
   327  		},
   328  		{
   329  			Cluster: "c0", Service: "s1",
   330  			TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
   331  			LocalityStats: map[string]LocalityData{
   332  				"test-locality": {
   333  					RequestStats: RequestData{Succeeded: 1, Issued: 1},
   334  					LoadStats:    map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
   335  				},
   336  			},
   337  		},
   338  	}
   339  	// Call Stats with just "c0", this should return data for "c0", and not
   340  	// touch data for other clusters.
   341  	gotC0 := store.Stats([]string{"c0"})
   342  	if diff := cmp.Diff(wantC0, gotC0, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval"), sortDataSlice); diff != "" {
   343  		t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
   344  	}
   345  
   346  	wantOther := []*Data{
   347  		{
   348  			Cluster: "c1", Service: "s0",
   349  			TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
   350  			LocalityStats: map[string]LocalityData{
   351  				"test-locality": {
   352  					RequestStats: RequestData{Succeeded: 1, Issued: 1},
   353  					LoadStats:    map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
   354  				},
   355  			},
   356  		},
   357  		{
   358  			Cluster: "c1", Service: "s1",
   359  			TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
   360  			LocalityStats: map[string]LocalityData{
   361  				"test-locality": {
   362  					RequestStats: RequestData{Succeeded: 1, Issued: 1},
   363  					LoadStats:    map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
   364  				},
   365  			},
   366  		},
   367  		{
   368  			Cluster: "c2", Service: "s0",
   369  			TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
   370  			LocalityStats: map[string]LocalityData{
   371  				"test-locality": {
   372  					RequestStats: RequestData{Succeeded: 1, Issued: 1},
   373  					LoadStats:    map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
   374  				},
   375  			},
   376  		},
   377  		{
   378  			Cluster: "c2", Service: "s1",
   379  			TotalDrops: 1, Drops: map[string]uint64{"dropped": 1},
   380  			LocalityStats: map[string]LocalityData{
   381  				"test-locality": {
   382  					RequestStats: RequestData{Succeeded: 1, Issued: 1},
   383  					LoadStats:    map[string]ServerLoadData{"abc": {Count: 1, Sum: 123}},
   384  				},
   385  			},
   386  		},
   387  	}
   388  	// Call Stats with empty slice, this should return data for all the
   389  	// remaining clusters, and not include c0 (because c0 data was cleared).
   390  	gotOther := store.Stats(nil)
   391  	if diff := cmp.Diff(wantOther, gotOther, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval"), sortDataSlice); diff != "" {
   392  		t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
   393  	}
   394  }
   395  
   396  // Test the cases that if a cluster doesn't have load to report, its data is not
   397  // appended to the slice returned by Stats().
   398  func TestStoreStatsEmptyDataNotReported(t *testing.T) {
   399  	var (
   400  		testServices = []string{"s0", "s1"}
   401  		testLocality = "test-locality"
   402  	)
   403  
   404  	store := NewStore()
   405  	// "c0"'s RPCs all finish with success.
   406  	for _, s := range testServices {
   407  		store.PerCluster("c0", s).CallStarted(testLocality)
   408  		store.PerCluster("c0", s).CallFinished(testLocality, nil)
   409  	}
   410  	// "c1"'s RPCs never finish (always inprocess).
   411  	for _, s := range testServices {
   412  		store.PerCluster("c1", s).CallStarted(testLocality)
   413  	}
   414  
   415  	want0 := []*Data{
   416  		{
   417  			Cluster: "c0", Service: "s0",
   418  			LocalityStats: map[string]LocalityData{
   419  				"test-locality": {RequestStats: RequestData{Succeeded: 1, Issued: 1}},
   420  			},
   421  		},
   422  		{
   423  			Cluster: "c0", Service: "s1",
   424  			LocalityStats: map[string]LocalityData{
   425  				"test-locality": {RequestStats: RequestData{Succeeded: 1, Issued: 1}},
   426  			},
   427  		},
   428  		{
   429  			Cluster: "c1", Service: "s0",
   430  			LocalityStats: map[string]LocalityData{
   431  				"test-locality": {RequestStats: RequestData{InProgress: 1, Issued: 1}},
   432  			},
   433  		},
   434  		{
   435  			Cluster: "c1", Service: "s1",
   436  			LocalityStats: map[string]LocalityData{
   437  				"test-locality": {RequestStats: RequestData{InProgress: 1, Issued: 1}},
   438  			},
   439  		},
   440  	}
   441  	// Call Stats with empty slice, this should return data for all the
   442  	// clusters.
   443  	got0 := store.Stats(nil)
   444  	if diff := cmp.Diff(want0, got0, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval"), sortDataSlice); diff != "" {
   445  		t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
   446  	}
   447  
   448  	want1 := []*Data{
   449  		{
   450  			Cluster: "c1", Service: "s0",
   451  			LocalityStats: map[string]LocalityData{
   452  				"test-locality": {RequestStats: RequestData{InProgress: 1}},
   453  			},
   454  		},
   455  		{
   456  			Cluster: "c1", Service: "s1",
   457  			LocalityStats: map[string]LocalityData{
   458  				"test-locality": {RequestStats: RequestData{InProgress: 1}},
   459  			},
   460  		},
   461  	}
   462  	// Call Stats with empty slice again, this should return data only for "c1",
   463  	// because "c0" data was cleared, but "c1" has in-progress RPCs.
   464  	got1 := store.Stats(nil)
   465  	if diff := cmp.Diff(want1, got1, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Data{}, "ReportInterval"), sortDataSlice); diff != "" {
   466  		t.Errorf("store.stats() returned unexpected diff (-want +got):\n%s", diff)
   467  	}
   468  }