dubbo.apache.org/dubbo-go/v3@v3.1.1/xds/client/load/store_test.go (about)

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