github.com/koko1123/flow-go-1@v0.29.6/module/mempool/herocache/backdata/cache_test.go (about) 1 package herocache 2 3 import ( 4 "fmt" 5 "math/rand" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/require" 10 11 "github.com/koko1123/flow-go-1/model/flow" 12 "github.com/koko1123/flow-go-1/module/mempool/herocache/backdata/heropool" 13 "github.com/koko1123/flow-go-1/module/metrics" 14 "github.com/koko1123/flow-go-1/utils/unittest" 15 ) 16 17 // TestArrayBackData_SingleBucket evaluates health of state transition for storing 10 entities in a Cache with only 18 // a single bucket (of 16). It also evaluates all stored items are retrievable. 19 func TestArrayBackData_SingleBucket(t *testing.T) { 20 limit := 10 21 22 bd := NewCache(uint32(limit), 23 1, 24 heropool.LRUEjection, 25 unittest.Logger(), 26 metrics.NewNoopCollector()) 27 28 entities := unittest.EntityListFixture(uint(limit)) 29 30 // adds all entities to backdata 31 testAddEntities(t, bd, entities, heropool.LRUEjection) 32 33 // sanity checks 34 for i := heropool.EIndex(0); i < heropool.EIndex(len(entities)); i++ { 35 // since we are below limit, elements should be added sequentially at bucket 0. 36 // the ith added element has a key index of i+1, 37 // since 0 means unused key index in implementation. 38 require.Equal(t, bd.buckets[0].slots[i].slotAge, uint64(i+1)) 39 // also, since we have not yet over-limited, 40 // entities are assigned their entityIndex in the same order they are added. 41 require.Equal(t, bd.buckets[0].slots[i].entityIndex, i) 42 _, _, owner := bd.entities.Get(i) 43 require.Equal(t, owner, uint64(i)) 44 } 45 46 // all stored items must be retrievable 47 testRetrievableFrom(t, bd, entities, 0) 48 } 49 50 // TestArrayBackData_Adjust evaluates that Adjust method correctly updates the value of 51 // the desired entity while preserving the integrity of BackData. 52 func TestArrayBackData_Adjust(t *testing.T) { 53 limit := 100_000 54 55 bd := NewCache(uint32(limit), 56 8, 57 heropool.LRUEjection, 58 unittest.Logger(), 59 metrics.NewNoopCollector()) 60 61 entities := unittest.EntityListFixture(uint(limit)) 62 63 // adds all entities to backdata 64 testAddEntities(t, bd, entities, heropool.LRUEjection) 65 66 // picks a random entity from BackData and adjusts its identifier to a new one. 67 entityIndex := rand.Int() % limit 68 // checking integrity of retrieving entity 69 oldEntity, ok := bd.ByID(entities[entityIndex].ID()) 70 require.True(t, ok) 71 oldEntityID := oldEntity.ID() 72 require.Equal(t, entities[entityIndex].ID(), oldEntityID) 73 require.Equal(t, entities[entityIndex], oldEntity) 74 75 // picks a new identifier for the entity and makes sure it is different than its current one. 76 newEntityID := unittest.IdentifierFixture() 77 require.NotEqual(t, oldEntityID, newEntityID) 78 79 // adjusts old entity to a new entity with a new identifier 80 newEntity, ok := bd.Adjust(oldEntity.ID(), func(entity flow.Entity) flow.Entity { 81 mockEntity, ok := entity.(*unittest.MockEntity) 82 require.True(t, ok) 83 // oldEntity must be passed to func parameter of adjust. 84 require.Equal(t, oldEntityID, mockEntity.ID()) 85 require.Equal(t, oldEntity, mockEntity) 86 87 return &unittest.MockEntity{Identifier: newEntityID} 88 }) 89 90 // adjustment must be successful, and identifier must be updated. 91 require.True(t, ok) 92 require.Equal(t, newEntityID, newEntity.ID()) 93 newMockEntity, ok := newEntity.(*unittest.MockEntity) 94 require.True(t, ok) 95 96 // replaces new entity in the original reference list and 97 // retrieves all. 98 entities[entityIndex] = newMockEntity 99 testRetrievableFrom(t, bd, entities, 0) 100 101 // re-adjusting old entity must fail, since its identifier must no longer exist 102 entity, ok := bd.Adjust(oldEntityID, func(entity flow.Entity) flow.Entity { 103 require.Fail(t, "function must not be invoked on a non-existing entity") 104 return entity 105 }) 106 require.False(t, ok) 107 require.Nil(t, entity) 108 109 // similarly, retrieving old entity must fail 110 entity, ok = bd.ByID(oldEntityID) 111 require.False(t, ok) 112 require.Nil(t, entity) 113 114 ok = bd.Has(oldEntityID) 115 require.False(t, ok) 116 117 // adjusting any random non-existing identifier must fail 118 entity, ok = bd.Adjust(unittest.IdentifierFixture(), func(entity flow.Entity) flow.Entity { 119 require.Fail(t, "function must not be invoked on a non-existing entity") 120 return entity 121 }) 122 require.False(t, ok) 123 require.Nil(t, entity) 124 125 // adjustment must be idempotent for size 126 require.Equal(t, bd.Size(), uint(limit)) 127 } 128 129 // TestArrayBackData_WriteHeavy evaluates correctness of Cache under the writing and retrieving 130 // a heavy load of entities up to its limit. All data must be written successfully and then retrievable. 131 func TestArrayBackData_WriteHeavy(t *testing.T) { 132 limit := 100_000 133 134 bd := NewCache(uint32(limit), 135 8, 136 heropool.LRUEjection, 137 unittest.Logger(), 138 metrics.NewNoopCollector()) 139 140 entities := unittest.EntityListFixture(uint(limit)) 141 142 // adds all entities to backdata 143 testAddEntities(t, bd, entities, heropool.LRUEjection) 144 145 // retrieves all entities from backdata 146 testRetrievableFrom(t, bd, entities, 0) 147 } 148 149 // TestArrayBackData_LRU_Ejection evaluates correctness of Cache under the writing and retrieving 150 // a heavy load of entities beyond its limit. With LRU ejection, only most recently written data must be maintained 151 // by mempool. 152 func TestArrayBackData_LRU_Ejection(t *testing.T) { 153 // mempool has the limit of 100K, but we put 1M 154 // (10 time more than its capacity) 155 limit := 100_000 156 items := uint(1_000_000) 157 158 bd := NewCache(uint32(limit), 159 8, 160 heropool.LRUEjection, 161 unittest.Logger(), 162 metrics.NewNoopCollector()) 163 164 entities := unittest.EntityListFixture(items) 165 166 // adds all entities to backdata 167 testAddEntities(t, bd, entities, heropool.LRUEjection) 168 169 // only last 100K (i.e., 900Kth forward) items must be retrievable, and 170 // the rest must be ejected. 171 testRetrievableFrom(t, bd, entities, 900_000) 172 } 173 174 // TestArrayBackData_No_Ejection evaluates correctness of Cache under the writing and retrieving 175 // a heavy load of entities beyond its limit. With NoEjection mode, the cache should refuse to add extra entities beyond 176 // its limit. 177 func TestArrayBackData_No_Ejection(t *testing.T) { 178 // mempool has the limit of 100K, but we put 1M 179 // (10 time more than its capacity) 180 limit := 100_000 181 items := uint(1_000_000) 182 183 bd := NewCache(uint32(limit), 184 8, 185 heropool.NoEjection, 186 unittest.Logger(), 187 metrics.NewNoopCollector()) 188 189 entities := unittest.EntityListFixture(items) 190 191 // adds all entities to backdata 192 testAddEntities(t, bd, entities, heropool.NoEjection) 193 194 // only last 100K (i.e., 900Kth forward) items must be retrievable, and 195 // the rest must be ejected. 196 testRetrievableInRange(t, bd, entities, 0, limit) 197 } 198 199 // TestArrayBackData_Random_Ejection evaluates correctness of Cache under the writing and retrieving 200 // a heavy load of entities beyond its limit. With random ejection, only as many entities as capacity of 201 // Cache must be retrievable. 202 func TestArrayBackData_Random_Ejection(t *testing.T) { 203 // mempool has the limit of 100K, but we put 1M 204 // (10 time more than its capacity) 205 limit := 100_000 206 items := uint(1_000_000) 207 208 bd := NewCache(uint32(limit), 209 8, 210 heropool.RandomEjection, 211 unittest.Logger(), 212 metrics.NewNoopCollector()) 213 214 entities := unittest.EntityListFixture(items) 215 216 // adds all entities to backdata 217 testAddEntities(t, bd, entities, heropool.RandomEjection) 218 219 // only 100K (random) items must be retrievable, as the rest 220 // are randomly ejected to make room. 221 testRetrievableCount(t, bd, entities, 100_000) 222 } 223 224 // TestArrayBackData_AddDuplicate evaluates that adding duplicate entity to Cache will fail without 225 // altering the internal state of it. 226 func TestArrayBackData_AddDuplicate(t *testing.T) { 227 limit := 100 228 229 bd := NewCache(uint32(limit), 230 8, 231 heropool.LRUEjection, 232 unittest.Logger(), 233 metrics.NewNoopCollector()) 234 235 entities := unittest.EntityListFixture(uint(limit)) 236 237 // adds all entities to backdata 238 testAddEntities(t, bd, entities, heropool.LRUEjection) 239 240 // adding duplicate entity should fail 241 for _, entity := range entities { 242 require.False(t, bd.Add(entity.ID(), entity)) 243 } 244 245 // still all entities must be retrievable from Cache. 246 testRetrievableFrom(t, bd, entities, 0) 247 } 248 249 // TestArrayBackData_Clear evaluates that calling Clear method removes all entities stored in BackData. 250 func TestArrayBackData_Clear(t *testing.T) { 251 limit := 100 252 253 bd := NewCache(uint32(limit), 254 8, 255 heropool.LRUEjection, 256 unittest.Logger(), 257 metrics.NewNoopCollector()) 258 259 entities := unittest.EntityListFixture(uint(limit)) 260 261 // adds all entities to backdata 262 testAddEntities(t, bd, entities, heropool.LRUEjection) 263 264 // still all must be retrievable from backdata 265 testRetrievableFrom(t, bd, entities, 0) 266 require.Equal(t, bd.Size(), uint(limit)) 267 require.Len(t, bd.All(), limit) 268 269 // calling clear must shrink size of BackData to zero 270 bd.Clear() 271 require.Equal(t, bd.Size(), uint(0)) 272 require.Len(t, bd.All(), 0) 273 274 // none of stored elements must be retrievable any longer 275 testRetrievableCount(t, bd, entities, 0) 276 } 277 278 // TestArrayBackData_All checks correctness of All method in returning all stored entities in it. 279 func TestArrayBackData_All(t *testing.T) { 280 tt := []struct { 281 limit uint32 282 items uint32 283 ejectionMode heropool.EjectionMode 284 }{ 285 { // mempool has the limit of 1000, but we put 100. 286 limit: 1000, 287 items: 100, 288 ejectionMode: heropool.LRUEjection, 289 }, 290 { // mempool has the limit of 1000, and we put exactly 1000 items. 291 limit: 1000, 292 items: 1000, 293 ejectionMode: heropool.LRUEjection, 294 }, 295 { // mempool has the limit of 1000, and we put 10K items with LRU ejection. 296 limit: 1000, 297 items: 10_000, 298 ejectionMode: heropool.LRUEjection, 299 }, 300 { // mempool has the limit of 1000, and we put 10K items with random ejection. 301 limit: 1000, 302 items: 10_000, 303 ejectionMode: heropool.RandomEjection, 304 }, 305 } 306 307 for _, tc := range tt { 308 t.Run(fmt.Sprintf("%d-limit-%d-items-%s-ejection", tc.limit, tc.items, tc.ejectionMode), func(t *testing.T) { 309 bd := NewCache(tc.limit, 310 8, 311 tc.ejectionMode, 312 unittest.Logger(), 313 metrics.NewNoopCollector()) 314 entities := unittest.EntityListFixture(uint(tc.items)) 315 316 testAddEntities(t, bd, entities, tc.ejectionMode) 317 318 if tc.ejectionMode == heropool.RandomEjection { 319 // in random ejection mode we count total number of matched entities 320 // with All map. 321 testMapMatchCount(t, bd.All(), entities, int(tc.limit)) 322 testEntitiesMatchCount(t, bd.Entities(), entities, int(tc.limit)) 323 testIdentifiersMatchCount(t, bd.Identifiers(), entities, int(tc.limit)) 324 } else { 325 // in LRU ejection mode we match All items based on a from index (i.e., last "from" items). 326 from := int(tc.items) - int(tc.limit) 327 if from < 0 { 328 // we are below limit, hence we start matching from index 0 329 from = 0 330 } 331 testMapMatchFrom(t, bd.All(), entities, from) 332 testEntitiesMatchFrom(t, bd.Entities(), entities, from) 333 testIdentifiersMatchFrom(t, bd.Identifiers(), entities, from) 334 } 335 }) 336 } 337 } 338 339 // TestArrayBackData_Remove checks correctness of removing elements from Cache. 340 func TestArrayBackData_Remove(t *testing.T) { 341 tt := []struct { 342 limit uint32 343 items uint32 344 from int // index start to be removed (set -1 to remove randomly) 345 count int // total elements to be removed 346 }{ 347 { // removing range with total items below the limit 348 limit: 100_000, 349 items: 10_000, 350 from: 188, 351 count: 2012, 352 }, 353 { // removing range from full Cache 354 limit: 100_000, 355 items: 100_000, 356 from: 50_333, 357 count: 6667, 358 }, 359 { // removing random from Cache with total items below its limit 360 limit: 100_000, 361 items: 10_000, 362 from: -1, 363 count: 6888, 364 }, 365 { // removing random from full Cache 366 limit: 100_000, 367 items: 10_000, 368 from: -1, 369 count: 7328, 370 }, 371 } 372 373 for _, tc := range tt { 374 t.Run(fmt.Sprintf("%d-limit-%d-items-%dfrom-%dcount", tc.limit, tc.items, tc.from, tc.count), func(t *testing.T) { 375 bd := NewCache( 376 tc.limit, 377 8, 378 heropool.RandomEjection, 379 unittest.Logger(), 380 metrics.NewNoopCollector()) 381 entities := unittest.EntityListFixture(uint(tc.items)) 382 383 testAddEntities(t, bd, entities, heropool.RandomEjection) 384 385 if tc.from == -1 { 386 // random removal 387 testRemoveAtRandom(t, bd, entities, tc.count) 388 // except removed ones, the rest must be retrievable 389 testRetrievableCount(t, bd, entities, uint64(int(tc.items)-tc.count)) 390 } else { 391 // removing a range 392 testRemoveRange(t, bd, entities, tc.from, tc.from+tc.count) 393 testCheckRangeRemoved(t, bd, entities, tc.from, tc.from+tc.count) 394 } 395 }) 396 } 397 } 398 399 // testAddEntities is a test helper that checks entities are added successfully to the Cache. 400 // and each entity is retrievable right after it is written to backdata. 401 func testAddEntities(t *testing.T, bd *Cache, entities []*unittest.MockEntity, ejection heropool.EjectionMode) { 402 // initially, head should be undefined 403 e, ok := bd.Head() 404 require.False(t, ok) 405 require.Nil(t, e) 406 407 // adding elements 408 for i, e := range entities { 409 if ejection == heropool.NoEjection && uint32(i) >= bd.sizeLimit { 410 // with no ejection when it goes beyond limit, the writes should be unsuccessful. 411 require.False(t, bd.Add(e.ID(), e)) 412 413 // the head should retrieve the first added entity. 414 headEntity, headExists := bd.Head() 415 require.True(t, headExists) 416 require.Equal(t, headEntity.ID(), entities[0].ID()) 417 } else { 418 // adding each element must be successful. 419 require.True(t, bd.Add(e.ID(), e)) 420 421 if uint32(i) < bd.sizeLimit { 422 // when we are below limit the size of 423 // Cache should be incremented by each addition. 424 require.Equal(t, bd.Size(), uint(i+1)) 425 426 // in case cache is not full, the head should retrieve the first added entity. 427 headEntity, headExists := bd.Head() 428 require.True(t, headExists) 429 require.Equal(t, headEntity.ID(), entities[0].ID()) 430 } else { 431 // when we cross the limit, the ejection kicks in, and 432 // size must be steady at the limit. 433 require.Equal(t, uint32(bd.Size()), bd.sizeLimit) 434 } 435 436 // entity should be immediately retrievable 437 actual, ok := bd.ByID(e.ID()) 438 require.True(t, ok) 439 require.Equal(t, e, actual) 440 } 441 } 442 } 443 444 // testRetrievableInRange is a test helper that evaluates that all entities starting from given index are retrievable from Cache. 445 func testRetrievableFrom(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int) { 446 testRetrievableInRange(t, bd, entities, from, len(entities)) 447 } 448 449 // testRetrievableInRange is a test helper that evaluates within given range [from, to) are retrievable from Cache. 450 func testRetrievableInRange(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int, to int) { 451 for i := range entities { 452 expected := entities[i] 453 actual, ok := bd.ByID(expected.ID()) 454 if i < from || i >= to { 455 require.False(t, ok, i) 456 require.Nil(t, actual) 457 } else { 458 require.True(t, ok) 459 require.Equal(t, expected, actual) 460 } 461 } 462 } 463 464 // testRemoveAtRandom is a test helper removes specified number of entities from Cache at random. 465 func testRemoveAtRandom(t *testing.T, bd *Cache, entities []*unittest.MockEntity, count int) { 466 for removedCount := 0; removedCount < count; { 467 unittest.RequireReturnsBefore(t, func() { 468 index := rand.Int() % len(entities) 469 expected, removed := bd.Remove(entities[index].ID()) 470 if !removed { 471 return 472 } 473 require.Equal(t, entities[index], expected) 474 removedCount++ 475 // size sanity check after removal 476 require.Equal(t, bd.Size(), uint(len(entities)-removedCount)) 477 }, 100*time.Millisecond, "could not find element to remove") 478 } 479 } 480 481 // testRemoveRange is a test helper that removes specified range of entities from Cache. 482 func testRemoveRange(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int, to int) { 483 for i := from; i < to; i++ { 484 expected, removed := bd.Remove(entities[i].ID()) 485 require.True(t, removed) 486 require.Equal(t, entities[i], expected) 487 // size sanity check after removal 488 require.Equal(t, bd.Size(), uint(len(entities)-(i-from)-1)) 489 } 490 } 491 492 // testCheckRangeRemoved is a test helper that evaluates the specified range of entities have been removed from Cache. 493 func testCheckRangeRemoved(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int, to int) { 494 for i := from; i < to; i++ { 495 // both removal and retrieval must fail 496 expected, removed := bd.Remove(entities[i].ID()) 497 require.False(t, removed) 498 require.Nil(t, expected) 499 500 expected, exists := bd.ByID(entities[i].ID()) 501 require.False(t, exists) 502 require.Nil(t, expected) 503 } 504 } 505 506 // testMapMatchFrom is a test helper that checks entities are retrievable from entitiesMap starting specified index. 507 func testMapMatchFrom(t *testing.T, entitiesMap map[flow.Identifier]flow.Entity, entities []*unittest.MockEntity, from int) { 508 require.Len(t, entitiesMap, len(entities)-from) 509 510 for i := range entities { 511 expected := entities[i] 512 actual, ok := entitiesMap[expected.ID()] 513 if i < from { 514 require.False(t, ok, i) 515 require.Nil(t, actual) 516 } else { 517 require.True(t, ok) 518 require.Equal(t, expected, actual) 519 } 520 } 521 } 522 523 // testEntitiesMatchFrom is a test helper that checks entities are retrievable from given list starting specified index. 524 func testEntitiesMatchFrom(t *testing.T, expectedEntities []flow.Entity, actualEntities []*unittest.MockEntity, from int) { 525 require.Len(t, expectedEntities, len(actualEntities)-from) 526 527 for i, actual := range actualEntities { 528 if i < from { 529 require.NotContains(t, expectedEntities, actual) 530 } else { 531 require.Contains(t, expectedEntities, actual) 532 } 533 } 534 } 535 536 // testIdentifiersMatchFrom is a test helper that checks identifiers of entities are retrievable from given list starting specified index. 537 func testIdentifiersMatchFrom(t *testing.T, expectedIdentifiers flow.IdentifierList, actualEntities []*unittest.MockEntity, from int) { 538 require.Len(t, expectedIdentifiers, len(actualEntities)-from) 539 540 for i, actual := range actualEntities { 541 if i < from { 542 require.NotContains(t, expectedIdentifiers, actual.ID()) 543 } else { 544 require.Contains(t, expectedIdentifiers, actual.ID()) 545 } 546 } 547 } 548 549 // testMapMatchFrom is a test helper that checks specified number of entities are retrievable from entitiesMap. 550 func testMapMatchCount(t *testing.T, entitiesMap map[flow.Identifier]flow.Entity, entities []*unittest.MockEntity, count int) { 551 require.Len(t, entitiesMap, count) 552 actualCount := 0 553 for i := range entities { 554 expected := entities[i] 555 actual, ok := entitiesMap[expected.ID()] 556 if !ok { 557 continue 558 } 559 require.Equal(t, expected, actual) 560 actualCount++ 561 } 562 require.Equal(t, count, actualCount) 563 } 564 565 // testEntitiesMatchCount is a test helper that checks specified number of entities are retrievable from given list. 566 func testEntitiesMatchCount(t *testing.T, expectedEntities []flow.Entity, actualEntities []*unittest.MockEntity, count int) { 567 entitiesMap := make(map[flow.Identifier]flow.Entity) 568 569 // converts expected entities list to a map in order to utilize a test helper. 570 for _, expected := range expectedEntities { 571 entitiesMap[expected.ID()] = expected 572 } 573 574 testMapMatchCount(t, entitiesMap, actualEntities, count) 575 } 576 577 // testIdentifiersMatchCount is a test helper that checks specified number of entities are retrievable from given list. 578 func testIdentifiersMatchCount(t *testing.T, expectedIdentifiers flow.IdentifierList, actualEntities []*unittest.MockEntity, count int) { 579 idMap := make(map[flow.Identifier]struct{}) 580 581 // converts expected identifiers to a map. 582 for _, expectedId := range expectedIdentifiers { 583 idMap[expectedId] = struct{}{} 584 } 585 586 require.Len(t, idMap, count) 587 actualCount := 0 588 for _, e := range actualEntities { 589 _, ok := idMap[e.ID()] 590 if !ok { 591 continue 592 } 593 actualCount++ 594 } 595 require.Equal(t, count, actualCount) 596 } 597 598 // testRetrievableCount is a test helper that checks the number of retrievable entities from backdata exactly matches 599 // the expectedCount. 600 func testRetrievableCount(t *testing.T, bd *Cache, entities []*unittest.MockEntity, expectedCount uint64) { 601 actualCount := 0 602 603 for i := range entities { 604 expected := entities[i] 605 actual, ok := bd.ByID(expected.ID()) 606 if !ok { 607 continue 608 } 609 require.Equal(t, expected, actual) 610 actualCount++ 611 } 612 613 require.Equal(t, int(expectedCount), actualCount) 614 }