github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/storage/badger/cache.go (about)

     1  package badger
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/dgraph-io/badger/v2"
     8  	lru "github.com/hashicorp/golang-lru/v2"
     9  
    10  	"github.com/onflow/flow-go/module"
    11  	"github.com/onflow/flow-go/storage"
    12  	"github.com/onflow/flow-go/storage/badger/transaction"
    13  )
    14  
    15  func withLimit[K comparable, V any](limit uint) func(*Cache[K, V]) {
    16  	return func(c *Cache[K, V]) {
    17  		c.limit = limit
    18  	}
    19  }
    20  
    21  type storeFunc[K comparable, V any] func(key K, val V) func(*transaction.Tx) error
    22  
    23  const DefaultCacheSize = uint(1000)
    24  
    25  func withStore[K comparable, V any](store storeFunc[K, V]) func(*Cache[K, V]) {
    26  	return func(c *Cache[K, V]) {
    27  		c.store = store
    28  	}
    29  }
    30  
    31  func noStore[K comparable, V any](_ K, _ V) func(*transaction.Tx) error {
    32  	return func(tx *transaction.Tx) error {
    33  		return fmt.Errorf("no store function for cache put available")
    34  	}
    35  }
    36  
    37  func noopStore[K comparable, V any](_ K, _ V) func(*transaction.Tx) error {
    38  	return func(tx *transaction.Tx) error {
    39  		return nil
    40  	}
    41  }
    42  
    43  type retrieveFunc[K comparable, V any] func(key K) func(*badger.Txn) (V, error)
    44  
    45  func withRetrieve[K comparable, V any](retrieve retrieveFunc[K, V]) func(*Cache[K, V]) {
    46  	return func(c *Cache[K, V]) {
    47  		c.retrieve = retrieve
    48  	}
    49  }
    50  
    51  func noRetrieve[K comparable, V any](_ K) func(*badger.Txn) (V, error) {
    52  	return func(tx *badger.Txn) (V, error) {
    53  		var nullV V
    54  		return nullV, fmt.Errorf("no retrieve function for cache get available")
    55  	}
    56  }
    57  
    58  type Cache[K comparable, V any] struct {
    59  	metrics  module.CacheMetrics
    60  	limit    uint
    61  	store    storeFunc[K, V]
    62  	retrieve retrieveFunc[K, V]
    63  	resource string
    64  	cache    *lru.Cache[K, V]
    65  }
    66  
    67  func newCache[K comparable, V any](collector module.CacheMetrics, resourceName string, options ...func(*Cache[K, V])) *Cache[K, V] {
    68  	c := Cache[K, V]{
    69  		metrics:  collector,
    70  		limit:    1000,
    71  		store:    noStore[K, V],
    72  		retrieve: noRetrieve[K, V],
    73  		resource: resourceName,
    74  	}
    75  	for _, option := range options {
    76  		option(&c)
    77  	}
    78  	c.cache, _ = lru.New[K, V](int(c.limit))
    79  	c.metrics.CacheEntries(c.resource, uint(c.cache.Len()))
    80  	return &c
    81  }
    82  
    83  // IsCached returns true if the key exists in the cache.
    84  // It DOES NOT check whether the key exists in the underlying data store.
    85  func (c *Cache[K, V]) IsCached(key K) bool {
    86  	return c.cache.Contains(key)
    87  }
    88  
    89  // Get will try to retrieve the resource from cache first, and then from the
    90  // injected. During normal operations, the following error returns are expected:
    91  //   - `storage.ErrNotFound` if key is unknown.
    92  func (c *Cache[K, V]) Get(key K) func(*badger.Txn) (V, error) {
    93  	return func(tx *badger.Txn) (V, error) {
    94  
    95  		// check if we have it in the cache
    96  		resource, cached := c.cache.Get(key)
    97  		if cached {
    98  			c.metrics.CacheHit(c.resource)
    99  			return resource, nil
   100  		}
   101  
   102  		// get it from the database
   103  		resource, err := c.retrieve(key)(tx)
   104  		if err != nil {
   105  			if errors.Is(err, storage.ErrNotFound) {
   106  				c.metrics.CacheNotFound(c.resource)
   107  			}
   108  			var nullV V
   109  			return nullV, fmt.Errorf("could not retrieve resource: %w", err)
   110  		}
   111  
   112  		c.metrics.CacheMiss(c.resource)
   113  
   114  		// cache the resource and eject least recently used one if we reached limit
   115  		evicted := c.cache.Add(key, resource)
   116  		if !evicted {
   117  			c.metrics.CacheEntries(c.resource, uint(c.cache.Len()))
   118  		}
   119  
   120  		return resource, nil
   121  	}
   122  }
   123  
   124  func (c *Cache[K, V]) Remove(key K) {
   125  	c.cache.Remove(key)
   126  }
   127  
   128  // Insert will add a resource directly to the cache with the given ID
   129  func (c *Cache[K, V]) Insert(key K, resource V) {
   130  	// cache the resource and eject least recently used one if we reached limit
   131  	evicted := c.cache.Add(key, resource)
   132  	if !evicted {
   133  		c.metrics.CacheEntries(c.resource, uint(c.cache.Len()))
   134  	}
   135  }
   136  
   137  // PutTx will return tx which adds a resource to the cache with the given ID.
   138  func (c *Cache[K, V]) PutTx(key K, resource V) func(*transaction.Tx) error {
   139  	storeOps := c.store(key, resource) // assemble DB operations to store resource (no execution)
   140  
   141  	return func(tx *transaction.Tx) error {
   142  		err := storeOps(tx) // execute operations to store resource
   143  		if err != nil {
   144  			return fmt.Errorf("could not store resource: %w", err)
   145  		}
   146  
   147  		tx.OnSucceed(func() {
   148  			c.Insert(key, resource)
   149  		})
   150  
   151  		return nil
   152  	}
   153  }