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  }