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  }