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