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 }