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 }