github.com/onflow/flow-go@v0.33.17/module/mempool/stdmap/eject.go (about) 1 // (c) 2019 Dapper Labs - ALL RIGHTS RESERVED 2 3 package stdmap 4 5 import ( 6 "fmt" 7 "math" 8 "sort" 9 "sync" 10 11 "github.com/onflow/flow-go/model/flow" 12 "github.com/onflow/flow-go/utils/rand" 13 ) 14 15 // this is the threshold for how much over the guaranteed capacity the 16 // collection should be before performing a batch ejection 17 const overCapacityThreshold = 128 18 19 // BatchEjectFunc implements an ejection policy to remove elements when the mempool 20 // exceeds its specified capacity. A custom ejection policy can be injected 21 // into the memory pool upon creation to change the strategy of eviction. 22 // The ejection policy is executed from within the thread that serves the 23 // mempool. Implementations should adhere to the following convention: 24 // - The ejector function has the freedom to eject _multiple_ elements. 25 // - In a single `eject` call, it must eject as many elements to statistically 26 // keep the mempool size within the desired limit. 27 // - The ejector _might_ (for performance reasons) retain more elements in the 28 // mempool than the targeted capacity. 29 // - The ejector _must_ notify the `Backend.ejectionCallbacks` for _each_ 30 // element it removes from the mempool. 31 // - Implementations do _not_ need to be concurrency safe. The Backend handles 32 // concurrency (specifically, it locks the mempool during ejection). 33 // - The implementation should be non-blocking (though, it is allowed to 34 // take a bit of time; the mempool will just be locked during this time). 35 type BatchEjectFunc func(b *Backend) (bool, error) 36 type EjectFunc func(b *Backend) (flow.Identifier, flow.Entity, bool) 37 38 // EjectRandomFast checks if the map size is beyond the 39 // threshold size, and will iterate through them and eject unneeded 40 // entries if that is the case. Return values are unused 41 func EjectRandomFast(b *Backend) (bool, error) { 42 currentSize := b.backData.Size() 43 44 if b.guaranteedCapacity >= currentSize { 45 return false, nil 46 } 47 // At this point, we know that currentSize > b.guaranteedCapacity. As 48 // currentSize fits into an int, b.guaranteedCapacity must also fit. 49 overcapacity := currentSize - b.guaranteedCapacity 50 if overcapacity <= overCapacityThreshold { 51 return false, nil 52 } 53 54 // Randomly select indices of elements to remove: 55 mapIndices := make([]int, 0, overcapacity) 56 for i := overcapacity; i > 0; i-- { 57 rand, err := rand.Uintn(currentSize) 58 if err != nil { 59 return false, fmt.Errorf("random generation failed: %w", err) 60 } 61 mapIndices = append(mapIndices, int(rand)) 62 } 63 sort.Ints(mapIndices) // inplace 64 65 // Now, mapIndices is a sequentially sorted list of indices to remove. 66 // Remove them in a loop. Repeated indices are idempotent (subsequent 67 // ejection calls will make up for it). 68 idx := 0 // index into mapIndices 69 next2Remove := mapIndices[0] // index of the element to be removed next 70 i := 0 // index into the entities map 71 for entityID, entity := range b.backData.All() { 72 if i == next2Remove { 73 b.backData.Remove(entityID) // remove entity 74 for _, callback := range b.ejectionCallbacks { 75 callback(entity) // notify callback 76 } 77 78 idx++ 79 80 // There is a (1 in b.guaranteedCapacity) chance that the 81 // next value in mapIndices is a duplicate. If a duplicate is 82 // found, skip it by incrementing 'idx' 83 for ; idx < int(overcapacity) && next2Remove == mapIndices[idx]; idx++ { 84 } 85 86 if idx == int(overcapacity) { 87 return true, nil 88 } 89 next2Remove = mapIndices[idx] 90 } 91 i++ 92 } 93 return true, nil 94 } 95 96 // EjectPanic simply panics, crashing the program. Useful when cache is not expected 97 // to grow beyond certain limits, but ejecting is not applicable 98 func EjectPanic(b *Backend) (flow.Identifier, flow.Entity, bool) { 99 panic("unexpected: mempool size over the limit") 100 } 101 102 // LRUEjector provides a swift FIFO ejection functionality 103 type LRUEjector struct { 104 sync.Mutex 105 table map[flow.Identifier]uint64 // keeps sequence number of entities it tracks 106 seqNum uint64 // keeps the most recent sequence number 107 } 108 109 func NewLRUEjector() *LRUEjector { 110 return &LRUEjector{ 111 table: make(map[flow.Identifier]uint64), 112 seqNum: 0, 113 } 114 } 115 116 // Track should be called every time a new entity is added to the mempool. 117 // It tracks the entity for later ejection. 118 func (q *LRUEjector) Track(entityID flow.Identifier) { 119 q.Lock() 120 defer q.Unlock() 121 122 if _, ok := q.table[entityID]; ok { 123 // skips adding duplicate item 124 return 125 } 126 127 // TODO current table structure provides O(1) track and untrack features 128 // however, the Eject functionality is asymptotically O(n). 129 // With proper resource cleanups by the mempools, the Eject is supposed 130 // as a very infrequent operation. However, further optimizations on 131 // Eject efficiency is needed. 132 q.table[entityID] = q.seqNum 133 q.seqNum++ 134 } 135 136 // Untrack simply removes the tracker of the ejector off the entityID 137 func (q *LRUEjector) Untrack(entityID flow.Identifier) { 138 q.Lock() 139 defer q.Unlock() 140 141 delete(q.table, entityID) 142 } 143 144 // Eject implements EjectFunc for LRUEjector. It finds the entity with the lowest sequence number (i.e., 145 // the oldest entity). It also untracks. This is using a linear search 146 func (q *LRUEjector) Eject(b *Backend) flow.Identifier { 147 q.Lock() 148 defer q.Unlock() 149 150 // finds the oldest entity 151 oldestSQ := uint64(math.MaxUint64) 152 var oldestID flow.Identifier 153 for _, id := range b.backData.Identifiers() { 154 if sq, ok := q.table[id]; ok { 155 if sq < oldestSQ { 156 oldestID = id 157 oldestSQ = sq 158 } 159 } 160 } 161 162 // untracks the oldest id as it is supposed to be ejected 163 delete(q.table, oldestID) 164 165 return oldestID 166 }