github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/mempool/herocache/backdata/heropool/pool_test.go (about) 1 package heropool 2 3 import ( 4 "fmt" 5 "math" 6 "testing" 7 8 "github.com/onflow/flow-go/utils/rand" 9 10 "github.com/stretchr/testify/require" 11 12 "github.com/onflow/flow-go/model/flow" 13 "github.com/onflow/flow-go/utils/unittest" 14 ) 15 16 // TestStoreAndRetrieval_BelowLimit checks health of heroPool for storing and retrieval scenarios that 17 // do not involve ejection. 18 // The test involves cases for testing the pool below its limit, and also up to its limit. However, it never gets beyond 19 // the limit, so no ejection will kick-in. 20 func TestStoreAndRetrieval_BelowLimit(t *testing.T) { 21 for _, tc := range []struct { 22 limit uint32 // capacity of entity list 23 entityCount uint32 // total entities to be stored 24 }{ 25 { 26 limit: 30, 27 entityCount: 10, 28 }, 29 { 30 limit: 30, 31 entityCount: 30, 32 }, 33 { 34 limit: 2000, 35 entityCount: 1000, 36 }, 37 { 38 limit: 1000, 39 entityCount: 1000, 40 }, 41 } { 42 t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) { 43 withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){ 44 func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 45 testInitialization(t, pool, entities) 46 }, 47 func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 48 testAddingEntities(t, pool, entities, LRUEjection) 49 }, 50 func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 51 testRetrievingEntitiesFrom(t, pool, entities, 0) 52 }, 53 }..., 54 ) 55 }) 56 } 57 } 58 59 // TestStoreAndRetrieval_With_No_Ejection checks health of heroPool for storing and retrieval scenarios that involves the NoEjection mode. 60 func TestStoreAndRetrieval_With_No_Ejection(t *testing.T) { 61 for _, tc := range []struct { 62 limit uint32 // capacity of pool 63 entityCount uint32 // total entities to be stored 64 }{ 65 { 66 limit: 30, 67 entityCount: 31, 68 }, 69 { 70 limit: 30, 71 entityCount: 100, 72 }, 73 { 74 limit: 1000, 75 entityCount: 2000, 76 }, 77 } { 78 t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) { 79 withTestScenario(t, tc.limit, tc.entityCount, NoEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){ 80 func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 81 testAddingEntities(t, pool, entities, NoEjection) 82 }, 83 func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 84 // with the NoEjection mode, only the first "limit" entities must be retrievable. 85 testRetrievingEntitiesInRange(t, pool, entities, 0, EIndex(tc.limit)) 86 }, 87 }..., 88 ) 89 }) 90 } 91 } 92 93 // TestStoreAndRetrieval_With_LRU_Ejection checks health of heroPool for storing and retrieval scenarios that involves the LRU ejection. 94 // The test involves cases for testing the pool beyond its limit, so the LRU ejection will kick-in. 95 func TestStoreAndRetrieval_With_LRU_Ejection(t *testing.T) { 96 for _, tc := range []struct { 97 limit uint32 // capacity of pool 98 entityCount uint32 // total entities to be stored 99 }{ 100 { 101 limit: 30, 102 entityCount: 31, 103 }, 104 { 105 limit: 30, 106 entityCount: 100, 107 }, 108 { 109 limit: 1000, 110 entityCount: 2000, 111 }, 112 } { 113 t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) { 114 withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){ 115 func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 116 testAddingEntities(t, pool, entities, LRUEjection) 117 }, 118 func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 119 // with a limit of tc.limit, storing a total of tc.entityCount (> tc.limit) entities, results 120 // in ejection of the first tc.entityCount - tc.limit entities. 121 // Hence, we check retrieval of the last tc.limit entities, which start from index 122 // tc.entityCount - tc.limit entities. 123 testRetrievingEntitiesFrom(t, pool, entities, EIndex(tc.entityCount-tc.limit)) 124 }, 125 }..., 126 ) 127 }) 128 } 129 } 130 131 // TestStoreAndRetrieval_With_Random_Ejection checks health of heroPool for storing and retrieval scenarios that involves the LRU ejection. 132 func TestStoreAndRetrieval_With_Random_Ejection(t *testing.T) { 133 for _, tc := range []struct { 134 limit uint32 // capacity of pool 135 entityCount uint32 // total entities to be stored 136 }{ 137 { 138 limit: 30, 139 entityCount: 31, 140 }, 141 { 142 limit: 30, 143 entityCount: 100, 144 }, 145 } { 146 t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) { 147 withTestScenario(t, tc.limit, tc.entityCount, RandomEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){ 148 func(t *testing.T, backData *Pool, entities []*unittest.MockEntity) { 149 testAddingEntities(t, backData, entities, RandomEjection) 150 }, 151 func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 152 // with a limit of tc.limit, storing a total of tc.entityCount (> tc.limit) entities, results 153 // in ejection of "tc.entityCount - tc.limit" entities at random. 154 // Hence, we check retrieval any successful total of "tc.limit" entities. 155 testRetrievingCount(t, pool, entities, int(tc.limit)) 156 }, 157 }..., 158 ) 159 }) 160 } 161 } 162 163 // TestInvalidateEntity checks the health of heroPool for invalidating entities under random, LRU, and LIFO scenarios. 164 // Invalidating an entity removes it from the used state and moves its node to the free state. 165 func TestInvalidateEntity(t *testing.T) { 166 for _, tc := range []struct { 167 limit uint32 // capacity of entity pool 168 entityCount uint32 // total entities to be stored 169 }{ 170 { 171 limit: 30, 172 entityCount: 0, 173 }, 174 { 175 limit: 30, 176 entityCount: 1, 177 }, 178 { 179 limit: 30, 180 entityCount: 10, 181 }, 182 { 183 limit: 30, 184 entityCount: 30, 185 }, 186 { 187 limit: 100, 188 entityCount: 10, 189 }, 190 { 191 limit: 100, 192 entityCount: 100, 193 }, 194 } { 195 // head invalidation test (LRU) 196 t.Run(fmt.Sprintf("head-invalidation-%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) { 197 withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){ 198 func(t *testing.T, backData *Pool, entities []*unittest.MockEntity) { 199 testAddingEntities(t, backData, entities, LRUEjection) 200 }, 201 func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 202 testInvalidatingHead(t, pool, entities) 203 }, 204 }...) 205 }) 206 207 // tail invalidation test (LIFO) 208 t.Run(fmt.Sprintf("tail-invalidation-%d-limit-%d-entities-", tc.limit, tc.entityCount), func(t *testing.T) { 209 withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){ 210 func(t *testing.T, backData *Pool, entities []*unittest.MockEntity) { 211 testAddingEntities(t, backData, entities, LRUEjection) 212 }, 213 func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 214 testInvalidatingTail(t, pool, entities) 215 }, 216 }...) 217 }) 218 } 219 } 220 221 // TestAddAndRemoveEntities checks health of heroPool for scenario where entitites are stored and removed in a predetermined order. 222 // LRUEjection, NoEjection and RandomEjection are tested. RandomEjection doesn't allow to provide a final state of the pool to check. 223 func TestAddAndRemoveEntities(t *testing.T) { 224 for _, tc := range []struct { 225 limit uint32 // capacity of the pool 226 entityCount uint32 // total entities to be stored 227 ejectionMode EjectionMode // ejection mode 228 numberOfOperations int 229 probabilityOfAdding float32 230 }{ 231 { 232 limit: 500, 233 entityCount: 1000, 234 ejectionMode: LRUEjection, 235 numberOfOperations: 1000, 236 probabilityOfAdding: 0.8, 237 }, 238 { 239 limit: 500, 240 entityCount: 1000, 241 ejectionMode: NoEjection, 242 numberOfOperations: 1000, 243 probabilityOfAdding: 0.8, 244 }, 245 { 246 limit: 500, 247 entityCount: 1000, 248 ejectionMode: RandomEjection, 249 numberOfOperations: 1000, 250 probabilityOfAdding: 0.8, 251 }, 252 } { 253 t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) { 254 testAddRemoveEntities(t, tc.limit, tc.entityCount, tc.ejectionMode, tc.numberOfOperations, tc.probabilityOfAdding) 255 }) 256 } 257 } 258 259 // testAddRemoveEntities adds and removes randomly elements in the pool, probabilityOfAdding and its counterpart 1-probabilityOfAdding are probabilities 260 // for an operation to be add or remove. Current timestamp is taken as a seed for the random number generator. 261 func testAddRemoveEntities(t *testing.T, limit uint32, entityCount uint32, ejectionMode EjectionMode, numberOfOperations int, probabilityOfAdding float32) { 262 263 require.GreaterOrEqual(t, entityCount, 2*limit, "entityCount must be greater or equal to 2*limit to test add/remove operations") 264 265 randomIntN := func(length int) int { 266 random, err := rand.Uintn(uint(length)) 267 require.NoError(t, err) 268 return int(random) 269 } 270 271 pool := NewHeroPool(limit, ejectionMode, unittest.Logger()) 272 entities := unittest.EntityListFixture(uint(entityCount)) 273 // retryLimit is the max number of retries to find an entity that is not already in the pool to add it. 274 // The test fails if it reaches this limit. 275 retryLimit := 100 276 // an array of random owner Ids. 277 ownerIds := make([]uint64, entityCount) 278 // generate ownerId to index in the entities array. 279 for i := 0; i < int(entityCount); i++ { 280 randomOwnerId, err := rand.Uint64() 281 require.Nil(t, err) 282 ownerIds[i] = randomOwnerId 283 } 284 // this map maintains entities currently stored in the pool. 285 addedEntities := make(map[flow.Identifier]int) 286 addedEntitiesInPool := make(map[flow.Identifier]EIndex) 287 for i := 0; i < numberOfOperations; i++ { 288 // choose between Add and Remove with a probability of probabilityOfAdding and 1-probabilityOfAdding respectively. 289 if float32(randomIntN(math.MaxInt32))/math.MaxInt32 < probabilityOfAdding || len(addedEntities) == 0 { 290 // keeps finding an entity to add until it finds one that is not already in the pool. 291 found := false 292 for retryTime := 0; retryTime < retryLimit; retryTime++ { 293 toAddIndex := randomIntN(int(entityCount)) 294 _, found = addedEntities[entities[toAddIndex].ID()] 295 if !found { 296 // found an entity that is not in the pool, add it. 297 indexInThePool, _, ejectedEntity := pool.Add(entities[toAddIndex].ID(), entities[toAddIndex], ownerIds[toAddIndex]) 298 if ejectionMode != NoEjection || len(addedEntities) < int(limit) { 299 // when there is an ejection mode in place, or the pool is not full, the index should be valid. 300 require.NotEqual(t, InvalidIndex, indexInThePool) 301 } 302 require.LessOrEqual(t, len(addedEntities), int(limit), "pool should not contain more elements than its limit") 303 if ejectionMode != NoEjection && len(addedEntities) == int(limit) { 304 // when there is an ejection mode in place, the ejected entity should be valid. 305 require.NotNil(t, ejectedEntity) 306 } 307 if ejectionMode != NoEjection && len(addedEntities) >= int(limit) { 308 // when there is an ejection mode in place, the ejected entity should be valid. 309 require.NotNil(t, ejectedEntity) 310 } 311 if indexInThePool != InvalidIndex { 312 entityId := entities[toAddIndex].ID() 313 // tracks the index of the entity in the pool and the index of the entity in the entities array. 314 addedEntities[entityId] = int(toAddIndex) 315 addedEntitiesInPool[entityId] = indexInThePool 316 // any entity added to the pool should be in the pool, and must be retrievable. 317 actualFlowId, actualEntity, actualOwnerId := pool.Get(indexInThePool) 318 require.Equal(t, entityId, actualFlowId) 319 require.Equal(t, entities[toAddIndex], actualEntity, "pool returned a different entity than the one added") 320 require.Equal(t, ownerIds[toAddIndex], actualOwnerId, "pool returned a different owner than the one added") 321 } 322 if ejectedEntity != nil { 323 require.Contains(t, addedEntities, ejectedEntity.ID(), "pool ejected an entity that was not added before") 324 delete(addedEntities, ejectedEntity.ID()) 325 delete(addedEntitiesInPool, ejectedEntity.ID()) 326 } 327 break 328 } 329 } 330 require.Falsef(t, found, "could not find an entity to add after %d retries", retryLimit) 331 } else { 332 // randomly select an index of an entity to remove. 333 entityToRemove := randomIntN(len(addedEntities)) 334 i := 0 335 var indexInPoolToRemove EIndex = 0 336 var indexInEntitiesArray int = 0 337 for k, v := range addedEntities { 338 if i == entityToRemove { 339 indexInPoolToRemove = addedEntitiesInPool[k] 340 indexInEntitiesArray = v 341 break 342 } 343 i++ 344 } 345 // remove the selected entity from the pool. 346 removedEntity := pool.Remove(indexInPoolToRemove) 347 expectedRemovedEntityId := entities[indexInEntitiesArray].ID() 348 require.Equal(t, expectedRemovedEntityId, removedEntity.ID(), "removed wrong entity") 349 delete(addedEntities, expectedRemovedEntityId) 350 delete(addedEntitiesInPool, expectedRemovedEntityId) 351 actualFlowId, actualEntity, _ := pool.Get(indexInPoolToRemove) 352 require.Equal(t, flow.ZeroID, actualFlowId) 353 require.Equal(t, nil, actualEntity) 354 } 355 } 356 for k, v := range addedEntities { 357 indexInPool := addedEntitiesInPool[k] 358 actualFlowId, actualEntity, actualOwnerId := pool.Get(indexInPool) 359 require.Equal(t, entities[v].ID(), actualFlowId) 360 require.Equal(t, entities[v], actualEntity) 361 require.Equal(t, ownerIds[v], actualOwnerId) 362 } 363 require.Equalf(t, len(addedEntities), int(pool.Size()), "pool size is not correct, expected %d, actual %d", len(addedEntities), pool.Size()) 364 } 365 366 // testInvalidatingHead keeps invalidating the head and evaluates the linked-list keeps updating its head 367 // and remains connected. 368 func testInvalidatingHead(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 369 // total number of entities to store 370 totalEntitiesStored := len(entities) 371 // freeListInitialSize is total number of empty nodes after 372 // storing all items in the list 373 freeListInitialSize := len(pool.poolEntities) - totalEntitiesStored 374 375 // (i+1) keeps total invalidated (head) entities. 376 for i := 0; i < totalEntitiesStored; i++ { 377 headIndex := pool.invalidateUsedHead() 378 // head index should be moved to the next index after each head invalidation. 379 require.Equal(t, entities[i], headIndex) 380 // size of list should be decremented after each invalidation. 381 require.Equal(t, uint32(totalEntitiesStored-i-1), pool.Size()) 382 // invalidated head should be appended to free entities 383 require.Equal(t, pool.states[stateFree].tail, EIndex(i)) 384 385 if freeListInitialSize != 0 { 386 // number of entities is below limit, hence free list is not empty. 387 // invalidating used head must not change the free head. 388 require.Equal(t, EIndex(totalEntitiesStored), pool.states[stateFree].head) 389 } else { 390 // number of entities is greater than or equal to limit, hence free list is empty. 391 // free head must be updated to the first invalidated head (index 0), 392 // and must be kept there for entire test (as we invalidate head not tail). 393 require.Equal(t, EIndex(0), pool.states[stateFree].head) 394 } 395 396 // except when the list is empty, head must be updated after invalidation, 397 // except when the list is empty, head and tail must be accessible after each invalidation` 398 // i.e., the linked list remains connected despite invalidation. 399 if i != totalEntitiesStored-1 { 400 // used linked-list 401 tailAccessibleFromHead(t, 402 pool.states[stateUsed].head, 403 pool.states[stateUsed].tail, 404 pool, 405 pool.Size()) 406 407 headAccessibleFromTail(t, 408 pool.states[stateUsed].head, 409 pool.states[stateUsed].tail, 410 pool, 411 pool.Size()) 412 413 // free lined-list 414 // 415 // after invalidating each item, size of free linked-list is incremented by one. 416 tailAccessibleFromHead(t, 417 pool.states[stateFree].head, 418 pool.states[stateFree].tail, 419 pool, 420 uint32(i+1+freeListInitialSize)) 421 422 headAccessibleFromTail(t, 423 pool.states[stateFree].head, 424 pool.states[stateFree].tail, 425 pool, 426 uint32(i+1+freeListInitialSize)) 427 } 428 429 // checking the status of head and tail in used linked-list after each head invalidation. 430 usedTail, _ := pool.getTails() 431 usedHead, _ := pool.getHeads() 432 if i != totalEntitiesStored-1 { 433 // pool is not empty yet, we still have entities to invalidate. 434 // 435 // used tail should point to the last element in pool, since we are 436 // invalidating head. 437 require.Equal(t, entities[totalEntitiesStored-1].ID(), usedTail.id) 438 require.Equal(t, EIndex(totalEntitiesStored-1), pool.states[stateUsed].tail) 439 440 // used head must point to the next element in the pool, 441 // i.e., invalidating head moves it forward. 442 require.Equal(t, entities[i+1].ID(), usedHead.id) 443 require.Equal(t, EIndex(i+1), pool.states[stateUsed].head) 444 } else { 445 // pool is empty 446 // used head and tail must be nil and their corresponding 447 // pointer indices must be undefined. 448 require.Nil(t, usedHead) 449 require.Nil(t, usedTail) 450 require.True(t, pool.states[stateUsed].size == 0) 451 require.Equal(t, pool.states[stateUsed].tail, InvalidIndex) 452 require.Equal(t, pool.states[stateUsed].head, InvalidIndex) 453 } 454 checkEachEntityIsInFreeOrUsedState(t, pool) 455 } 456 } 457 458 // testInvalidatingHead keeps invalidating the tail and evaluates the underlying free and used linked-lists keep updating its tail and remains connected. 459 func testInvalidatingTail(t *testing.T, pool *Pool, entities []*unittest.MockEntity) { 460 size := len(entities) 461 offset := len(pool.poolEntities) - size 462 for i := 0; i < size; i++ { 463 // invalidates tail index 464 tailIndex := pool.states[stateUsed].tail 465 require.Equal(t, EIndex(size-1-i), tailIndex) 466 467 pool.invalidateEntityAtIndex(tailIndex) 468 // old head index must be invalidated 469 require.True(t, pool.isInvalidated(tailIndex)) 470 // unclaimed head should be appended to free entities 471 require.Equal(t, pool.states[stateFree].tail, tailIndex) 472 473 if offset != 0 { 474 // number of entities is below limit 475 // free must head keeps pointing to first empty index after 476 // adding all entities. 477 require.Equal(t, EIndex(size), pool.states[stateFree].head) 478 } else { 479 // number of entities is greater than or equal to limit 480 // free head must be updated to last element in the pool (size - 1), 481 // and must be kept there for entire test (as we invalidate tail not head). 482 require.Equal(t, EIndex(size-1), pool.states[stateFree].head) 483 } 484 485 // size of pool should be shrunk after each invalidation. 486 require.Equal(t, uint32(size-i-1), pool.Size()) 487 488 // except when the pool is empty, tail must be updated after invalidation, 489 // and also head and tail must be accessible after each invalidation 490 // i.e., the linked-list remains connected despite invalidation. 491 if i != size-1 { 492 493 // used linked-list 494 tailAccessibleFromHead(t, 495 pool.states[stateUsed].head, 496 pool.states[stateUsed].tail, 497 pool, 498 pool.Size()) 499 500 headAccessibleFromTail(t, 501 pool.states[stateUsed].head, 502 pool.states[stateUsed].tail, 503 pool, 504 pool.Size()) 505 506 // free linked-list 507 tailAccessibleFromHead(t, 508 pool.states[stateFree].head, 509 pool.states[stateFree].tail, 510 pool, 511 uint32(i+1+offset)) 512 513 headAccessibleFromTail(t, 514 pool.states[stateFree].head, 515 pool.states[stateFree].tail, 516 pool, 517 uint32(i+1+offset)) 518 } 519 520 usedTail, _ := pool.getTails() 521 usedHead, _ := pool.getHeads() 522 if i != size-1 { 523 // pool is not empty yet 524 // 525 // used tail should move backward after each invalidation 526 require.Equal(t, entities[size-i-2].ID(), usedTail.id) 527 require.Equal(t, EIndex(size-i-2), pool.states[stateUsed].tail) 528 529 // used head must point to the first element in the pool, 530 require.Equal(t, entities[0].ID(), usedHead.id) 531 require.Equal(t, EIndex(0), pool.states[stateUsed].head) 532 } else { 533 // pool is empty 534 // used head and tail must be nil and their corresponding 535 // pointer indices must be undefined. 536 require.Nil(t, usedHead) 537 require.Nil(t, usedTail) 538 require.True(t, pool.states[stateUsed].size == 0) 539 require.Equal(t, pool.states[stateUsed].head, InvalidIndex) 540 require.Equal(t, pool.states[stateUsed].tail, InvalidIndex) 541 } 542 checkEachEntityIsInFreeOrUsedState(t, pool) 543 } 544 } 545 546 // testInitialization evaluates the state of an initialized pool before adding any element to it. 547 func testInitialization(t *testing.T, pool *Pool, _ []*unittest.MockEntity) { 548 // "used" linked-list must have a zero size, since we have no elements in the list. 549 require.True(t, pool.states[stateUsed].size == 0) 550 require.Equal(t, pool.states[stateUsed].head, InvalidIndex) 551 require.Equal(t, pool.states[stateUsed].tail, InvalidIndex) 552 553 for i := 0; i < len(pool.poolEntities); i++ { 554 if i == 0 { 555 // head of "free" linked-list should point to InvalidIndex of entities slice. 556 require.Equal(t, EIndex(i), pool.states[stateFree].head) 557 // previous element of head must be undefined (linked-list head feature). 558 require.Equal(t, pool.poolEntities[i].node.prev, InvalidIndex) 559 } 560 561 if i != 0 { 562 // except head, any element should point back to its previous index in slice. 563 require.Equal(t, EIndex(i-1), pool.poolEntities[i].node.prev) 564 } 565 566 if i != len(pool.poolEntities)-1 { 567 // except tail, any element should point forward to its next index in slice. 568 require.Equal(t, EIndex(i+1), pool.poolEntities[i].node.next) 569 } 570 571 if i == len(pool.poolEntities)-1 { 572 // tail of "free" linked-list should point to the last index in entities slice. 573 require.Equal(t, EIndex(i), pool.states[stateFree].tail) 574 // next element of tail must be undefined. 575 require.Equal(t, pool.poolEntities[i].node.next, InvalidIndex) 576 } 577 } 578 } 579 580 // testAddingEntities evaluates health of pool for storing new elements. 581 func testAddingEntities(t *testing.T, pool *Pool, entitiesToBeAdded []*unittest.MockEntity, ejectionMode EjectionMode) { 582 // initially head must be empty 583 e, ok := pool.Head() 584 require.False(t, ok) 585 require.Nil(t, e) 586 587 var uniqueEntities map[flow.Identifier]struct{} 588 if ejectionMode != NoEjection { 589 uniqueEntities = make(map[flow.Identifier]struct{}) 590 for _, entity := range entitiesToBeAdded { 591 uniqueEntities[entity.ID()] = struct{}{} 592 } 593 require.Equalf(t, len(uniqueEntities), len(entitiesToBeAdded), "entitesToBeAdded must be constructed of unique entities") 594 } 595 596 // adding elements 597 lruEjectedIndex := 0 598 for i, e := range entitiesToBeAdded { 599 // adding each element must be successful. 600 entityIndex, slotAvailable, ejectedEntity := pool.Add(e.ID(), e, uint64(i)) 601 602 if i < len(pool.poolEntities) { 603 // in case of no over limit, size of entities linked list should be incremented by each addition. 604 require.Equal(t, pool.Size(), uint32(i+1)) 605 606 require.True(t, slotAvailable) 607 require.Nil(t, ejectedEntity) 608 require.Equal(t, entityIndex, EIndex(i)) 609 610 // in case pool is not full, the head should retrieve the first added entity. 611 headEntity, headExists := pool.Head() 612 require.True(t, headExists) 613 require.Equal(t, headEntity.ID(), entitiesToBeAdded[0].ID()) 614 } 615 616 if ejectionMode == LRUEjection { 617 // under LRU ejection mode, new entity should be placed at index i in back data 618 _, entity, _ := pool.Get(EIndex(i % len(pool.poolEntities))) 619 require.Equal(t, e, entity) 620 621 if i >= len(pool.poolEntities) { 622 require.True(t, slotAvailable) 623 require.NotNil(t, ejectedEntity) 624 // confirm that ejected entity is the oldest entity 625 require.Equal(t, entitiesToBeAdded[lruEjectedIndex], ejectedEntity) 626 lruEjectedIndex++ 627 // when pool is full and with LRU ejection, the head should move forward with each element added. 628 headEntity, headExists := pool.Head() 629 require.True(t, headExists) 630 require.Equal(t, headEntity.ID(), entitiesToBeAdded[i+1-len(pool.poolEntities)].ID()) 631 } 632 } 633 634 if ejectionMode == RandomEjection { 635 if i >= len(pool.poolEntities) { 636 require.True(t, slotAvailable) 637 require.NotNil(t, ejectedEntity) 638 // confirm that ejected entity is from list of entitiesToBeAdded 639 _, ok := uniqueEntities[ejectedEntity.ID()] 640 require.True(t, ok) 641 } 642 } 643 644 if ejectionMode == NoEjection { 645 if i >= len(pool.poolEntities) { 646 require.False(t, slotAvailable) 647 require.Nil(t, ejectedEntity) 648 require.Equal(t, entityIndex, InvalidIndex) 649 650 // when pool is full and with NoEjection, the head must keep pointing to the first added element. 651 headEntity, headExists := pool.Head() 652 require.True(t, headExists) 653 require.Equal(t, headEntity.ID(), entitiesToBeAdded[0].ID()) 654 } 655 } 656 657 // underlying linked-lists sanity check 658 // first insertion forward, head of used list should always point to first entity in the list. 659 usedHead, freeHead := pool.getHeads() 660 usedTail, freeTail := pool.getTails() 661 662 if ejectionMode == LRUEjection { 663 expectedUsedHead := 0 664 if i >= len(pool.poolEntities) { 665 // we are beyond limit, so LRU ejection must happen and used head must 666 // be moved. 667 expectedUsedHead = (i + 1) % len(pool.poolEntities) 668 } 669 require.Equal(t, pool.poolEntities[expectedUsedHead].entity, usedHead.entity) 670 // head must be healthy and point back to undefined. 671 require.Equal(t, usedHead.node.prev, InvalidIndex) 672 } 673 674 if ejectionMode != NoEjection || i < len(pool.poolEntities) { 675 // new entity must be successfully added to tail of used linked-list 676 require.Equal(t, entitiesToBeAdded[i], usedTail.entity) 677 // used tail must be healthy and point back to undefined. 678 require.Equal(t, usedTail.node.next, InvalidIndex) 679 } 680 681 if ejectionMode == NoEjection && i >= len(pool.poolEntities) { 682 // used tail must not move 683 require.Equal(t, entitiesToBeAdded[len(pool.poolEntities)-1], usedTail.entity) 684 // used tail must be healthy and point back to undefined. 685 // This is not needed anymore as tail's next is now ignored 686 require.Equal(t, usedTail.node.next, InvalidIndex) 687 } 688 689 // free head 690 if i < len(pool.poolEntities)-1 { 691 // as long as we are below limit, after adding i element, free head 692 // should move to i+1 element. 693 require.Equal(t, EIndex(i+1), pool.states[stateFree].head) 694 // head must be healthy and point back to undefined. 695 require.Equal(t, freeHead.node.prev, InvalidIndex) 696 } else { 697 // once we go beyond limit, 698 // we run out of free slots, 699 // and free head must be kept at undefined. 700 require.Nil(t, freeHead) 701 } 702 703 // free tail 704 if i < len(pool.poolEntities)-1 { 705 // as long as we are below limit, after adding i element, free tail 706 // must keep pointing to last index of the array-based linked-list. In other 707 // words, adding element must not change free tail (since only free head is 708 // updated). 709 require.Equal(t, EIndex(len(pool.poolEntities)-1), pool.states[stateFree].tail) 710 // head tail be healthy and point next to undefined. 711 require.Equal(t, freeTail.node.next, InvalidIndex) 712 } else { 713 // once we go beyond limit, we run out of free slots, and 714 // free tail must be kept at undefined. 715 require.Nil(t, freeTail) 716 } 717 718 // used linked-list 719 // if we are still below limit, head to tail of used linked-list 720 // must be reachable within i + 1 steps. 721 // +1 is since we start from index 0 not 1. 722 usedTraverseStep := uint32(i + 1) 723 if i >= len(pool.poolEntities) { 724 // if we are above the limit, head to tail of used linked-list 725 // must be reachable within as many steps as the actual capacity of pool. 726 usedTraverseStep = uint32(len(pool.poolEntities)) 727 } 728 tailAccessibleFromHead(t, 729 pool.states[stateUsed].head, 730 pool.states[stateUsed].tail, 731 pool, 732 usedTraverseStep) 733 headAccessibleFromTail(t, 734 pool.states[stateUsed].head, 735 pool.states[stateUsed].tail, 736 pool, 737 usedTraverseStep) 738 739 // free linked-list 740 // if we are still below limit, head to tail of used linked-list 741 // must be reachable within "limit - i - 1" steps. "limit - i" part is since 742 // when we have i elements in pool, we have "limit - i" free slots, and -1 is 743 // since we start from index 0 not 1. 744 freeTraverseStep := uint32(len(pool.poolEntities) - i - 1) 745 if i >= len(pool.poolEntities) { 746 // if we are above the limit, head and tail of free linked-list must be reachable 747 // within 0 steps. 748 // The reason is linked-list is full and adding new elements is done 749 // by ejecting existing ones, remaining no free slot. 750 freeTraverseStep = uint32(0) 751 } 752 tailAccessibleFromHead(t, 753 pool.states[stateFree].head, 754 pool.states[stateFree].tail, 755 pool, 756 freeTraverseStep) 757 headAccessibleFromTail(t, 758 pool.states[stateFree].head, 759 pool.states[stateFree].tail, 760 pool, 761 freeTraverseStep) 762 763 checkEachEntityIsInFreeOrUsedState(t, pool) 764 } 765 } 766 767 // testRetrievingEntitiesFrom evaluates that all entities starting from given index are retrievable from pool. 768 func testRetrievingEntitiesFrom(t *testing.T, pool *Pool, entities []*unittest.MockEntity, from EIndex) { 769 testRetrievingEntitiesInRange(t, pool, entities, from, EIndex(len(entities))) 770 } 771 772 // testRetrievingEntitiesInRange evaluates that all entities in the given range are retrievable from pool. 773 func testRetrievingEntitiesInRange(t *testing.T, pool *Pool, entities []*unittest.MockEntity, from EIndex, to EIndex) { 774 for i := from; i < to; i++ { 775 actualID, actual, _ := pool.Get(i % EIndex(len(pool.poolEntities))) 776 require.Equal(t, entities[i].ID(), actualID, i) 777 require.Equal(t, entities[i], actual, i) 778 } 779 } 780 781 // testRetrievingCount evaluates that exactly expected number of entities are retrievable from underlying pool. 782 func testRetrievingCount(t *testing.T, pool *Pool, entities []*unittest.MockEntity, expected int) { 783 actualRetrievable := 0 784 785 for i := EIndex(0); i < EIndex(len(entities)); i++ { 786 for j := EIndex(0); j < EIndex(len(pool.poolEntities)); j++ { 787 actualID, actual, _ := pool.Get(j % EIndex(len(pool.poolEntities))) 788 if entities[i].ID() == actualID && entities[i] == actual { 789 actualRetrievable++ 790 } 791 } 792 } 793 794 require.Equal(t, expected, actualRetrievable) 795 } 796 797 // withTestScenario creates a new pool, and then runs helpers on it sequentially. 798 func withTestScenario(t *testing.T, 799 limit uint32, 800 entityCount uint32, 801 ejectionMode EjectionMode, 802 helpers ...func(*testing.T, *Pool, []*unittest.MockEntity)) { 803 804 pool := NewHeroPool(limit, ejectionMode, unittest.Logger()) 805 806 // head on underlying linked-list value should be uninitialized 807 require.True(t, pool.states[stateUsed].size == 0) 808 require.Equal(t, pool.Size(), uint32(0)) 809 810 entities := unittest.EntityListFixture(uint(entityCount)) 811 812 for _, helper := range helpers { 813 helper(t, pool, entities) 814 } 815 } 816 817 // tailAccessibleFromHead checks tail of given entities linked-list is reachable from its head by traversing expected number of steps. 818 func tailAccessibleFromHead(t *testing.T, headSliceIndex EIndex, tailSliceIndex EIndex, pool *Pool, steps uint32) { 819 seen := make(map[EIndex]struct{}) 820 821 index := headSliceIndex 822 for i := uint32(0); i < steps; i++ { 823 if i == steps-1 { 824 require.Equal(t, tailSliceIndex, index, "tail not reachable after steps steps") 825 return 826 } 827 828 require.NotEqual(t, tailSliceIndex, index, "tail visited in less expected steps (potential inconsistency)", i, steps) 829 _, ok := seen[index] 830 require.False(t, ok, "duplicate identifiers found") 831 832 require.NotEqual(t, pool.poolEntities[index].node.next, InvalidIndex, "tail not found, and reached end of list") 833 index = pool.poolEntities[index].node.next 834 } 835 } 836 837 // headAccessibleFromTail checks head of given entities linked list is reachable from its tail by traversing expected number of steps. 838 func headAccessibleFromTail(t *testing.T, headSliceIndex EIndex, tailSliceIndex EIndex, pool *Pool, total uint32) { 839 seen := make(map[EIndex]struct{}) 840 841 index := tailSliceIndex 842 for i := uint32(0); i < total; i++ { 843 if i == total-1 { 844 require.Equal(t, headSliceIndex, index, "head not reachable after total steps") 845 return 846 } 847 848 require.NotEqual(t, headSliceIndex, index, "head visited in less expected steps (potential inconsistency)", i, total) 849 _, ok := seen[index] 850 require.False(t, ok, "duplicate identifiers found") 851 852 index = pool.poolEntities[index].node.prev 853 } 854 } 855 856 // checkEachEntityIsInFreeOrUsedState checks if each entity in the pool belongs exactly to one of the state lists. 857 func checkEachEntityIsInFreeOrUsedState(t *testing.T, pool *Pool) { 858 pool_capacity := len(pool.poolEntities) 859 // check size 860 require.Equal(t, int(pool.states[stateFree].size+pool.states[stateUsed].size), pool_capacity, "Pool capacity is not equal to the sum of used and free sizes") 861 // check elelments 862 nodesInFree := discoverEntitiesBelongingToStateList(t, pool, stateFree) 863 nodesInUsed := discoverEntitiesBelongingToStateList(t, pool, stateUsed) 864 for i := 0; i < pool_capacity; i++ { 865 require.False(t, !nodesInFree[i] && !nodesInUsed[i], "Node is not in any state list") 866 require.False(t, nodesInFree[i] && nodesInUsed[i], "Node is in two state lists at the same time") 867 } 868 } 869 870 // discoverEntitiesBelongingToStateList discovers all entities in the pool that belong to the given list. 871 func discoverEntitiesBelongingToStateList(t *testing.T, pool *Pool, stateType StateIndex) []bool { 872 result := make([]bool, len(pool.poolEntities)) 873 for node_index := pool.states[stateType].head; node_index != InvalidIndex; { 874 require.False(t, result[node_index], "A node is present two times in the same state list") 875 result[node_index] = true 876 node_index = pool.poolEntities[node_index].node.next 877 } 878 return result 879 }