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 }