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  }