github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/inspector/internal/cache/cache_test.go (about) 1 package cache 2 3 import ( 4 "fmt" 5 "sync" 6 "testing" 7 "time" 8 9 "github.com/libp2p/go-libp2p/core/peer" 10 "github.com/rs/zerolog" 11 "github.com/stretchr/testify/require" 12 13 "github.com/onflow/flow-go/model/flow" 14 "github.com/onflow/flow-go/module" 15 "github.com/onflow/flow-go/module/metrics" 16 "github.com/onflow/flow-go/utils/unittest" 17 ) 18 19 const defaultDecay = 0.99 20 21 // TestRecordCache_Init tests the Init method of the RecordCache. 22 // It ensures that the method returns true when a new record is initialized 23 // and false when an existing record is initialized. 24 func TestRecordCache_Init(t *testing.T) { 25 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 26 27 peerID1 := unittest.PeerIdFixture(t) 28 peerID2 := unittest.PeerIdFixture(t) 29 30 // test initializing a record for an node ID that doesn't exist in the cache 31 gauge, ok, err := cache.GetWithInit(peerID1) 32 require.NoError(t, err) 33 require.True(t, ok, "expected record to exist") 34 require.Zerof(t, gauge, "expected gauge to be 0") 35 require.Equal(t, uint(1), cache.Size(), "expected cache to have one additional record") 36 37 // test initializing a record for an node ID that already exists in the cache 38 gaugeAgain, ok, err := cache.GetWithInit(peerID1) 39 require.NoError(t, err) 40 require.True(t, ok, "expected record to still exist") 41 require.Zerof(t, gaugeAgain, "expected same gauge to be 0") 42 require.Equal(t, gauge, gaugeAgain, "expected records to be the same") 43 require.Equal(t, uint(1), cache.Size(), "expected cache to still have one additional record") 44 45 // test initializing a record for another node ID 46 gauge2, ok, err := cache.GetWithInit(peerID2) 47 require.NoError(t, err) 48 require.True(t, ok, "expected record to exist") 49 require.Zerof(t, gauge2, "expected second gauge to be 0") 50 require.Equal(t, uint(2), cache.Size(), "expected cache to have two additional records") 51 } 52 53 // TestRecordCache_ConcurrentInit tests the concurrent initialization of records. 54 // The test covers the following scenarios: 55 // 1. Multiple goroutines initializing records for different node IDs. 56 // 2. Ensuring that all records are correctly initialized. 57 func TestRecordCache_ConcurrentInit(t *testing.T) { 58 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 59 60 pids := unittest.PeerIdFixtures(t, 10) 61 62 var wg sync.WaitGroup 63 wg.Add(len(pids)) 64 65 for _, pid := range pids { 66 go func(id peer.ID) { 67 defer wg.Done() 68 gauge, found, err := cache.GetWithInit(id) 69 require.NoError(t, err) 70 require.True(t, found) 71 require.Zerof(t, gauge, "expected all gauge values to be initialized to 0") 72 }(pid) 73 } 74 75 unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish") 76 } 77 78 // TestRecordCache_ConcurrentSameRecordInit tests the concurrent initialization of the same record. 79 // The test covers the following scenarios: 80 // 1. Multiple goroutines attempting to initialize the same record concurrently. 81 // 2. Only one goroutine successfully initializes the record, and others receive false on initialization. 82 // 3. The record is correctly initialized in the cache and can be retrieved using the GetWithInit method. 83 func TestRecordCache_ConcurrentSameRecordInit(t *testing.T) { 84 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 85 86 nodeID := unittest.PeerIdFixture(t) 87 const concurrentAttempts = 10 88 89 var wg sync.WaitGroup 90 wg.Add(concurrentAttempts) 91 92 for i := 0; i < concurrentAttempts; i++ { 93 go func() { 94 defer wg.Done() 95 gauge, found, err := cache.GetWithInit(nodeID) 96 require.NoError(t, err) 97 require.True(t, found) 98 require.Zero(t, gauge) 99 }() 100 } 101 102 unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish") 103 104 // ensure that only one goroutine successfully initialized the record 105 require.Equal(t, uint(1), cache.Size()) 106 } 107 108 // TestRecordCache_ReceivedClusterPrefixedMessage tests the ReceivedClusterPrefixedMessage method of the RecordCache. 109 // The test covers the following scenarios: 110 // 1. Updating a record gauge for an existing node ID. 111 // 2. Attempting to update a record gauge for a non-existing node ID should not result in error. ReceivedClusterPrefixedMessage should always attempt to initialize the gauge. 112 // 3. Multiple updates on the same record only initialize the record once. 113 func TestRecordCache_ReceivedClusterPrefixedMessage(t *testing.T) { 114 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 115 116 peerID1 := unittest.PeerIdFixture(t) 117 peerID2 := unittest.PeerIdFixture(t) 118 119 gauge, err := cache.ReceivedClusterPrefixedMessage(peerID1) 120 require.NoError(t, err) 121 require.Equal(t, float64(1), gauge) 122 123 // get will apply a slightl decay resulting 124 // in a gauge value less than gauge which is 1 but greater than 0.9 125 currentGauge, ok, err := cache.GetWithInit(peerID1) 126 require.NoError(t, err) 127 require.True(t, ok) 128 require.LessOrEqual(t, currentGauge, gauge) 129 require.Greater(t, currentGauge, 0.9) 130 131 _, ok, err = cache.GetWithInit(peerID2) 132 require.NoError(t, err) 133 require.True(t, ok) 134 135 // test adjusting the spam record for a non-existing node ID 136 peerID3 := unittest.PeerIdFixture(t) 137 gauge3, err := cache.ReceivedClusterPrefixedMessage(peerID3) 138 require.NoError(t, err) 139 require.Equal(t, float64(1), gauge3) 140 141 // when updated the value should be incremented from 1 -> 2 and slightly decayed resulting 142 // in a gauge value less than 2 but greater than 1.9 143 gauge3, err = cache.ReceivedClusterPrefixedMessage(peerID3) 144 require.NoError(t, err) 145 require.LessOrEqual(t, gauge3, 2.0) 146 require.Greater(t, gauge3, 1.9) 147 } 148 149 // TestRecordCache_UpdateDecay ensures that a gauge in the record cache is eventually decayed back to 0 after some time. 150 func TestRecordCache_Decay(t *testing.T) { 151 cache := cacheFixture(t, 100, 0.09, zerolog.Nop(), metrics.NewNoopCollector()) 152 153 peerID1 := unittest.PeerIdFixture(t) 154 155 // initialize spam records for peerID1 and peerID2 156 gauge, err := cache.ReceivedClusterPrefixedMessage(peerID1) 157 require.Equal(t, float64(1), gauge) 158 require.NoError(t, err) 159 gauge, ok, err := cache.GetWithInit(peerID1) 160 require.True(t, ok) 161 require.NoError(t, err) 162 // gauge should have been delayed slightly 163 require.True(t, gauge < float64(1)) 164 165 time.Sleep(time.Second) 166 167 gauge, ok, err = cache.GetWithInit(peerID1) 168 require.True(t, ok) 169 require.NoError(t, err) 170 // gauge should have been delayed slightly, but closer to 0 171 require.Less(t, gauge, 0.1) 172 } 173 174 // TestRecordCache_Identities tests the NodeIDs method of the RecordCache. 175 // The test covers the following scenarios: 176 // 1. Initializing the cache with multiple records. 177 // 2. Checking if the NodeIDs method returns the correct set of node IDs. 178 func TestRecordCache_Identities(t *testing.T) { 179 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 180 181 // initialize spam records for a few node IDs 182 peerID1 := unittest.PeerIdFixture(t) 183 peerID2 := unittest.PeerIdFixture(t) 184 peerID3 := unittest.PeerIdFixture(t) 185 186 _, ok, err := cache.GetWithInit(peerID1) 187 require.NoError(t, err) 188 require.True(t, ok) 189 _, ok, err = cache.GetWithInit(peerID2) 190 require.NoError(t, err) 191 require.True(t, ok) 192 _, ok, err = cache.GetWithInit(peerID3) 193 require.NoError(t, err) 194 require.True(t, ok) 195 196 // check if the NodeIDs method returns the correct set of node IDs 197 identities := cache.NodeIDs() 198 require.Equal(t, 3, len(identities)) 199 require.ElementsMatch(t, identities, []flow.Identifier{cache.MakeId(peerID1), cache.MakeId(peerID2), cache.MakeId(peerID3)}) 200 } 201 202 // TestRecordCache_Remove tests the Remove method of the RecordCache. 203 // The test covers the following scenarios: 204 // 1. Initializing the cache with multiple records. 205 // 2. Removing a record and checking if it is removed correctly. 206 // 3. Ensuring the other records are still in the cache after removal. 207 // 4. Attempting to remove a non-existent node ID. 208 func TestRecordCache_Remove(t *testing.T) { 209 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 210 211 // initialize spam records for a few node IDs 212 peerID1 := unittest.PeerIdFixture(t) 213 peerID2 := unittest.PeerIdFixture(t) 214 peerID3 := unittest.PeerIdFixture(t) 215 216 _, ok, err := cache.GetWithInit(peerID1) 217 require.NoError(t, err) 218 require.True(t, ok) 219 _, ok, err = cache.GetWithInit(peerID2) 220 require.NoError(t, err) 221 require.True(t, ok) 222 _, ok, err = cache.GetWithInit(peerID3) 223 require.NoError(t, err) 224 require.True(t, ok) 225 226 numOfIds := uint(3) 227 require.Equal(t, numOfIds, cache.Size(), fmt.Sprintf("expected size of the cache to be %d", numOfIds)) 228 // remove peerID1 and check if the record is removed 229 require.True(t, cache.Remove(peerID1)) 230 require.NotContains(t, peerID1, cache.NodeIDs()) 231 232 // check if the other node IDs are still in the cache 233 _, exists, err := cache.GetWithInit(peerID2) 234 require.NoError(t, err) 235 require.True(t, exists) 236 _, exists, err = cache.GetWithInit(peerID3) 237 require.NoError(t, err) 238 require.True(t, exists) 239 240 // attempt to remove a non-existent node ID 241 nodeID4 := unittest.PeerIdFixture(t) 242 require.False(t, cache.Remove(nodeID4)) 243 } 244 245 // TestRecordCache_ConcurrentRemove tests the concurrent removal of records for different node IDs. 246 // The test covers the following scenarios: 247 // 1. Multiple goroutines removing records for different node IDs concurrently. 248 // 2. The records are correctly removed from the cache. 249 func TestRecordCache_ConcurrentRemove(t *testing.T) { 250 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 251 252 peerIds := unittest.PeerIdFixtures(t, 10) 253 for _, pid := range peerIds { 254 _, ok, err := cache.GetWithInit(pid) 255 require.NoError(t, err) 256 require.True(t, ok) 257 } 258 259 var wg sync.WaitGroup 260 wg.Add(len(peerIds)) 261 262 for _, pid := range peerIds { 263 go func(id peer.ID) { 264 defer wg.Done() 265 removed := cache.Remove(id) 266 require.True(t, removed) 267 require.NotContains(t, id, cache.NodeIDs()) 268 }(pid) 269 } 270 271 unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish") 272 273 require.Equal(t, uint(0), cache.Size()) 274 } 275 276 // TestRecordCache_ConcurrentUpdatesAndReads tests the concurrent adjustments and reads of records for different 277 // node IDs. The test covers the following scenarios: 278 // 1. Multiple goroutines adjusting records for different node IDs concurrently. 279 // 2. Multiple goroutines getting records for different node IDs concurrently. 280 // 3. The adjusted records are correctly updated in the cache. 281 func TestRecordCache_ConcurrentUpdatesAndReads(t *testing.T) { 282 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 283 284 peerIds := unittest.PeerIdFixtures(t, 10) 285 for _, pid := range peerIds { 286 _, ok, err := cache.GetWithInit(pid) 287 require.NoError(t, err) 288 require.True(t, ok) 289 } 290 291 var wg sync.WaitGroup 292 wg.Add(len(peerIds) * 2) 293 294 for _, pid := range peerIds { 295 // adjust spam records concurrently 296 go func(id peer.ID) { 297 defer wg.Done() 298 _, err := cache.ReceivedClusterPrefixedMessage(id) 299 require.NoError(t, err) 300 }(pid) 301 302 // get spam records concurrently 303 go func(id peer.ID) { 304 defer wg.Done() 305 _, found, err := cache.GetWithInit(id) 306 require.NoError(t, err) 307 require.True(t, found) 308 }(pid) 309 } 310 311 unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish") 312 313 // ensure that the records are correctly updated in the cache 314 for _, pid := range peerIds { 315 gauge, found, err := cache.GetWithInit(pid) 316 require.NoError(t, err) 317 require.True(t, found) 318 // slight decay will result in 0.9 < gauge < 1 319 require.LessOrEqual(t, gauge, 1.0) 320 require.Greater(t, gauge, 0.9) 321 } 322 } 323 324 // TestRecordCache_ConcurrentInitAndRemove tests the concurrent initialization and removal of records for different 325 // node IDs. The test covers the following scenarios: 326 // 1. Multiple goroutines initializing records for different node IDs concurrently. 327 // 2. Multiple goroutines removing records for different node IDs concurrently. 328 // 3. The initialized records are correctly added to the cache. 329 // 4. The removed records are correctly removed from the cache. 330 func TestRecordCache_ConcurrentInitAndRemove(t *testing.T) { 331 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 332 333 peerIds := unittest.PeerIdFixtures(t, 20) 334 peerIdsToAdd := peerIds[:10] 335 peerIdsToRemove := peerIds[10:] 336 337 for _, pid := range peerIdsToRemove { 338 _, ok, err := cache.GetWithInit(pid) 339 require.NoError(t, err) 340 require.True(t, ok) 341 } 342 343 var wg sync.WaitGroup 344 wg.Add(len(peerIds)) 345 346 // initialize spam records concurrently 347 for _, pid := range peerIdsToAdd { 348 go func(id peer.ID) { 349 defer wg.Done() 350 _, ok, err := cache.GetWithInit(id) 351 require.NoError(t, err) 352 require.True(t, ok) 353 }(pid) 354 } 355 356 // remove spam records concurrently 357 for _, pid := range peerIdsToRemove { 358 go func(id peer.ID) { 359 defer wg.Done() 360 cache.Remove(id) 361 require.NotContains(t, id, cache.NodeIDs()) 362 }(pid) 363 } 364 365 unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish") 366 367 // ensure that the initialized records are correctly added to the cache 368 // and removed records are correctly removed from the cache 369 expectedIds := make([]flow.Identifier, len(peerIdsToAdd)) 370 for i, pid := range peerIdsToAdd { 371 expectedIds[i] = cache.MakeId(pid) 372 } 373 require.ElementsMatch(t, expectedIds, cache.NodeIDs()) 374 } 375 376 // TestRecordCache_ConcurrentInitRemoveUpdate tests the concurrent initialization, removal, and adjustment of 377 // records for different node IDs. The test covers the following scenarios: 378 // 1. Multiple goroutines initializing records for different node IDs concurrently. 379 // 2. Multiple goroutines removing records for different node IDs concurrently. 380 // 3. Multiple goroutines adjusting records for different node IDs concurrently. 381 func TestRecordCache_ConcurrentInitRemoveUpdate(t *testing.T) { 382 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 383 384 peerIds := unittest.PeerIdFixtures(t, 30) 385 peerIdsToAdd := peerIds[:10] 386 peerIdsToRemove := peerIds[10:20] 387 peerIdsToAdjust := peerIds[20:] 388 389 for _, pid := range peerIdsToRemove { 390 _, ok, err := cache.GetWithInit(pid) 391 require.NoError(t, err) 392 require.True(t, ok) 393 } 394 395 var wg sync.WaitGroup 396 wg.Add(len(peerIds)) 397 398 // Initialize spam records concurrently 399 for _, pid := range peerIdsToAdd { 400 go func(id peer.ID) { 401 defer wg.Done() 402 _, ok, err := cache.GetWithInit(id) 403 require.NoError(t, err) 404 require.True(t, ok) 405 }(pid) 406 } 407 408 // Remove spam records concurrently 409 for _, pid := range peerIdsToRemove { 410 go func(id peer.ID) { 411 defer wg.Done() 412 cache.Remove(id) 413 require.NotContains(t, id, cache.NodeIDs()) 414 }(pid) 415 } 416 417 // Adjust spam records concurrently 418 for _, pid := range peerIdsToAdjust { 419 go func(id peer.ID) { 420 defer wg.Done() 421 _, _ = cache.ReceivedClusterPrefixedMessage(id) 422 }(pid) 423 } 424 425 expectedPeerIds := append(peerIdsToAdd, peerIdsToAdjust...) 426 expectedIds := make([]flow.Identifier, len(expectedPeerIds)) 427 for i, pid := range expectedPeerIds { 428 expectedIds[i] = cache.MakeId(pid) 429 } 430 unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish") 431 require.ElementsMatch(t, expectedIds, cache.NodeIDs()) 432 } 433 434 // TestRecordCache_EdgeCasesAndInvalidInputs tests the edge cases and invalid inputs for RecordCache methods. 435 // The test covers the following scenarios: 436 // 1. Initializing a record multiple times. 437 // 2. Adjusting a non-existent record. 438 // 3. Removing a record multiple times. 439 func TestRecordCache_EdgeCasesAndInvalidInputs(t *testing.T) { 440 cache := cacheFixture(t, 100, defaultDecay, zerolog.Nop(), metrics.NewNoopCollector()) 441 442 peerIds := unittest.PeerIdFixtures(t, 20) 443 peerIdsToAdd := peerIds[:10] 444 peerIdsToRemove := peerIds[10:20] 445 446 for _, pid := range peerIdsToRemove { 447 _, ok, err := cache.GetWithInit(pid) 448 require.NoError(t, err) 449 require.True(t, ok) 450 } 451 452 var wg sync.WaitGroup 453 wg.Add(len(peerIds) + 10) 454 455 // initialize spam records concurrently 456 for _, pid := range peerIdsToAdd { 457 go func(id peer.ID) { 458 defer wg.Done() 459 retrieved, ok, err := cache.GetWithInit(id) 460 require.NoError(t, err) 461 require.True(t, ok) 462 require.Zero(t, retrieved) 463 }(pid) 464 } 465 466 // remove spam records concurrently 467 for _, pid := range peerIdsToRemove { 468 go func(id peer.ID) { 469 defer wg.Done() 470 require.True(t, cache.Remove(id)) 471 require.NotContains(t, cache.MakeId(id), cache.NodeIDs()) 472 }(pid) 473 } 474 475 expectedIds := make([]flow.Identifier, len(peerIds)) 476 for i, pid := range peerIds { 477 expectedIds[i] = cache.MakeId(pid) 478 } 479 // call NodeIDs method concurrently 480 for i := 0; i < 10; i++ { 481 go func() { 482 defer wg.Done() 483 ids := cache.NodeIDs() 484 // the number of returned IDs should be less than or equal to the number of node IDs 485 require.True(t, len(ids) <= len(peerIds)) 486 // the returned IDs should be a subset of the node IDs 487 for _, id := range ids { 488 require.Contains(t, expectedIds, id) 489 } 490 }() 491 } 492 unittest.RequireReturnsBefore(t, wg.Wait, 1*time.Second, "timed out waiting for goroutines to finish") 493 } 494 495 // recordFixture creates a new record entity with the given node id. 496 // Args: 497 // - id: the node id of the record. 498 // Returns: 499 // - RecordEntity: the created record entity. 500 func recordEntityFixture(id flow.Identifier) ClusterPrefixedMessagesReceivedRecord { 501 return ClusterPrefixedMessagesReceivedRecord{NodeID: id, Gauge: 0.0, lastUpdated: time.Now()} 502 } 503 504 // cacheFixture returns a new *RecordCache. 505 func cacheFixture(t *testing.T, sizeLimit uint32, recordDecay float64, logger zerolog.Logger, collector module.HeroCacheMetrics) *RecordCache { 506 recordFactory := func(id flow.Identifier) ClusterPrefixedMessagesReceivedRecord { 507 return recordEntityFixture(id) 508 } 509 config := &RecordCacheConfig{ 510 sizeLimit: sizeLimit, 511 logger: logger, 512 collector: collector, 513 recordDecay: recordDecay, 514 } 515 r, err := NewRecordCache(config, recordFactory) 516 require.NoError(t, err) 517 // expect cache to be empty 518 require.Equalf(t, uint(0), r.Size(), "cache size must be 0") 519 require.NotNil(t, r) 520 return r 521 }