github.com/celestiaorg/celestia-node@v0.15.0-beta.1/share/eds/cache/accessor_cache.go (about) 1 package cache 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "sync" 9 "sync/atomic" 10 "time" 11 12 "github.com/filecoin-project/dagstore" 13 "github.com/filecoin-project/dagstore/shard" 14 lru "github.com/hashicorp/golang-lru/v2" 15 ) 16 17 const defaultCloseTimeout = time.Minute 18 19 var _ Cache = (*AccessorCache)(nil) 20 21 // AccessorCache implements the Cache interface using an LRU cache backend. 22 type AccessorCache struct { 23 // The name is a prefix that will be used for cache metrics if they are enabled. 24 name string 25 // stripedLocks prevents simultaneous RW access to the blockstore cache for a shard. Instead 26 // of using only one lock or one lock per key, we stripe the shard keys across 256 locks. 256 is 27 // chosen because it 0-255 is the range of values we get looking at the last byte of the key. 28 stripedLocks [256]sync.Mutex 29 // Caches the blockstore for a given shard for shard read affinity, i.e., further reads will likely 30 // be from the same shard. Maps (shard key -> blockstore). 31 cache *lru.Cache[shard.Key, *accessorWithBlockstore] 32 33 metrics *metrics 34 } 35 36 // accessorWithBlockstore is the value that we store in the blockstore Cache. It implements the 37 // Accessor interface. 38 type accessorWithBlockstore struct { 39 sync.RWMutex 40 shardAccessor Accessor 41 // The blockstore is stored separately because each access to the blockstore over the shard 42 // accessor reopens the underlying CAR. 43 bs dagstore.ReadBlockstore 44 45 done chan struct{} 46 refs atomic.Int32 47 isClosed bool 48 } 49 50 // Blockstore implements the Blockstore of the Accessor interface. It creates the blockstore on the 51 // first request and reuses the created instance for all subsequent requests. 52 func (s *accessorWithBlockstore) Blockstore() (dagstore.ReadBlockstore, error) { 53 s.Lock() 54 defer s.Unlock() 55 var err error 56 if s.bs == nil { 57 s.bs, err = s.shardAccessor.Blockstore() 58 } 59 return s.bs, err 60 } 61 62 // Reader returns a new copy of the reader to read data. 63 func (s *accessorWithBlockstore) Reader() io.Reader { 64 return s.shardAccessor.Reader() 65 } 66 67 func (s *accessorWithBlockstore) addRef() error { 68 s.Lock() 69 defer s.Unlock() 70 if s.isClosed { 71 // item is already closed and soon will be removed after all refs are released 72 return errCacheMiss 73 } 74 if s.refs.Add(1) == 1 { 75 // there were no refs previously and done channel was closed, reopen it by recreating 76 s.done = make(chan struct{}) 77 } 78 return nil 79 } 80 81 func (s *accessorWithBlockstore) removeRef() { 82 s.Lock() 83 defer s.Unlock() 84 if s.refs.Add(-1) <= 0 { 85 close(s.done) 86 } 87 } 88 89 func (s *accessorWithBlockstore) close() error { 90 s.Lock() 91 if s.isClosed { 92 s.Unlock() 93 // accessor will be closed by another goroutine 94 return nil 95 } 96 s.isClosed = true 97 done := s.done 98 s.Unlock() 99 100 select { 101 case <-done: 102 case <-time.After(defaultCloseTimeout): 103 return fmt.Errorf("closing accessor, some readers didn't close the accessor within timeout,"+ 104 " amount left: %v", s.refs.Load()) 105 } 106 if err := s.shardAccessor.Close(); err != nil { 107 return fmt.Errorf("closing accessor: %w", err) 108 } 109 return nil 110 } 111 112 func NewAccessorCache(name string, cacheSize int) (*AccessorCache, error) { 113 bc := &AccessorCache{ 114 name: name, 115 } 116 // Instantiate the blockstore Cache. 117 bslru, err := lru.NewWithEvict[shard.Key, *accessorWithBlockstore](cacheSize, bc.evictFn()) 118 if err != nil { 119 return nil, fmt.Errorf("failed to instantiate blockstore cache: %w", err) 120 } 121 bc.cache = bslru 122 return bc, nil 123 } 124 125 // evictFn will be invoked when an item is evicted from the cache. 126 func (bc *AccessorCache) evictFn() func(shard.Key, *accessorWithBlockstore) { 127 return func(_ shard.Key, abs *accessorWithBlockstore) { 128 // we can release accessor from cache early, while it is being closed in parallel routine 129 go func() { 130 err := abs.close() 131 if err != nil { 132 bc.metrics.observeEvicted(true) 133 log.Errorf("couldn't close accessor after cache eviction: %s", err) 134 return 135 } 136 bc.metrics.observeEvicted(false) 137 }() 138 } 139 } 140 141 // Get retrieves the Accessor for a given shard key from the Cache. If the Accessor is not in 142 // the Cache, it returns an errCacheMiss. 143 func (bc *AccessorCache) Get(key shard.Key) (Accessor, error) { 144 lk := &bc.stripedLocks[shardKeyToStriped(key)] 145 lk.Lock() 146 defer lk.Unlock() 147 148 accessor, err := bc.get(key) 149 if err != nil { 150 bc.metrics.observeGet(false) 151 return nil, err 152 } 153 bc.metrics.observeGet(true) 154 return newRefCloser(accessor) 155 } 156 157 func (bc *AccessorCache) get(key shard.Key) (*accessorWithBlockstore, error) { 158 abs, ok := bc.cache.Get(key) 159 if !ok { 160 return nil, errCacheMiss 161 } 162 return abs, nil 163 } 164 165 // GetOrLoad attempts to get an item from the cache, and if not found, invokes 166 // the provided loader function to load it. 167 func (bc *AccessorCache) GetOrLoad( 168 ctx context.Context, 169 key shard.Key, 170 loader func(context.Context, shard.Key) (Accessor, error), 171 ) (Accessor, error) { 172 lk := &bc.stripedLocks[shardKeyToStriped(key)] 173 lk.Lock() 174 defer lk.Unlock() 175 176 abs, err := bc.get(key) 177 if err == nil { 178 // return accessor, only of it is not closed yet 179 accessorWithRef, err := newRefCloser(abs) 180 if err == nil { 181 bc.metrics.observeGet(true) 182 return accessorWithRef, nil 183 } 184 } 185 186 // accessor not found in cache, so load new one using loader 187 accessor, err := loader(ctx, key) 188 if err != nil { 189 return nil, fmt.Errorf("unable to load accessor: %w", err) 190 } 191 192 abs = &accessorWithBlockstore{ 193 shardAccessor: accessor, 194 } 195 196 // Create a new accessor first to increment the reference count in it, so it cannot get evicted 197 // from the inner lru cache before it is used. 198 accessorWithRef, err := newRefCloser(abs) 199 if err != nil { 200 return nil, err 201 } 202 bc.cache.Add(key, abs) 203 return accessorWithRef, nil 204 } 205 206 // Remove removes the Accessor for a given key from the cache. 207 func (bc *AccessorCache) Remove(key shard.Key) error { 208 lk := &bc.stripedLocks[shardKeyToStriped(key)] 209 lk.Lock() 210 accessor, err := bc.get(key) 211 lk.Unlock() 212 if errors.Is(err, errCacheMiss) { 213 // item is not in cache 214 return nil 215 } 216 if err = accessor.close(); err != nil { 217 return err 218 } 219 // The cache will call evictFn on removal, where accessor close will be called. 220 bc.cache.Remove(key) 221 return nil 222 } 223 224 // EnableMetrics enables metrics for the cache. 225 func (bc *AccessorCache) EnableMetrics() error { 226 var err error 227 bc.metrics, err = newMetrics(bc) 228 return err 229 } 230 231 // refCloser manages references to accessor from provided reader and removes the ref, when the 232 // Close is called 233 type refCloser struct { 234 *accessorWithBlockstore 235 closeFn func() 236 } 237 238 // newRefCloser creates new refCloser 239 func newRefCloser(abs *accessorWithBlockstore) (*refCloser, error) { 240 if err := abs.addRef(); err != nil { 241 return nil, err 242 } 243 244 var closeOnce sync.Once 245 return &refCloser{ 246 accessorWithBlockstore: abs, 247 closeFn: func() { 248 closeOnce.Do(abs.removeRef) 249 }, 250 }, nil 251 } 252 253 func (c *refCloser) Close() error { 254 c.closeFn() 255 return nil 256 } 257 258 // shardKeyToStriped returns the index of the lock to use for a given shard key. We use the last 259 // byte of the shard key as the pseudo-random index. 260 func shardKeyToStriped(sk shard.Key) byte { 261 return sk.String()[len(sk.String())-1] 262 }