github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/objstorage/objstorageprovider/sharedcache/shared_cache.go (about)

     1  // Copyright 2023 The LevelDB-Go and Pebble Authors. All rights reserved. Use
     2  // of this source code is governed by a BSD-style license that can be found in
     3  // the LICENSE file.
     4  
     5  package sharedcache
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"math/bits"
    12  	"sync"
    13  	"sync/atomic"
    14  	"time"
    15  
    16  	"github.com/cockroachdb/errors"
    17  	"github.com/cockroachdb/pebble/internal/base"
    18  	"github.com/cockroachdb/pebble/internal/invariants"
    19  	"github.com/cockroachdb/pebble/objstorage/remote"
    20  	"github.com/cockroachdb/pebble/vfs"
    21  	"github.com/prometheus/client_golang/prometheus"
    22  )
    23  
    24  // Exported to enable exporting from package pebble to enable
    25  // exporting metrics with below buckets in CRDB.
    26  var (
    27  	IOBuckets           = prometheus.ExponentialBucketsRange(float64(time.Millisecond*1), float64(10*time.Second), 50)
    28  	ChannelWriteBuckets = prometheus.ExponentialBucketsRange(float64(time.Microsecond*1), float64(10*time.Second), 50)
    29  )
    30  
    31  // Cache is a persistent cache backed by a local filesystem. It is intended
    32  // to cache data that is in slower shared storage (e.g. S3), hence the
    33  // package name 'sharedcache'.
    34  type Cache struct {
    35  	shards       []shard
    36  	writeWorkers writeWorkers
    37  
    38  	bm                blockMath
    39  	shardingBlockSize int64
    40  
    41  	logger  base.Logger
    42  	metrics internalMetrics
    43  }
    44  
    45  // Metrics is a struct containing metrics exported by the secondary cache.
    46  // TODO(josh): Reconsider the set of metrics exported by the secondary cache
    47  // before we release the secondary cache to users. We choose to export many metrics
    48  // right now, so we learn a lot from the benchmarking we are doing over the 23.2
    49  // cycle.
    50  type Metrics struct {
    51  	// The number of sstable bytes stored in the cache.
    52  	Size int64
    53  	// The count of cache blocks in the cache (not sstable blocks).
    54  	Count int64
    55  
    56  	// The number of calls to ReadAt.
    57  	TotalReads int64
    58  	// The number of calls to ReadAt that require reading data from 2+ shards.
    59  	MultiShardReads int64
    60  	// The number of calls to ReadAt that require reading data from 2+ cache blocks.
    61  	MultiBlockReads int64
    62  	// The number of calls to ReadAt where all data returned was read from the cache.
    63  	ReadsWithFullHit int64
    64  	// The number of calls to ReadAt where some data returned was read from the cache.
    65  	ReadsWithPartialHit int64
    66  	// The number of calls to ReadAt where no data returned was read from the cache.
    67  	ReadsWithNoHit int64
    68  
    69  	// The number of times a cache block was evicted from the cache.
    70  	Evictions int64
    71  	// The number of times writing a cache block to the cache failed.
    72  	WriteBackFailures int64
    73  
    74  	// The latency of calls to get some data from the cache.
    75  	GetLatency prometheus.Histogram
    76  	// The latency of reads of a single cache block from disk.
    77  	DiskReadLatency prometheus.Histogram
    78  	// The latency of writing data to write back to the cache to a channel.
    79  	// Generally should be low, but if the channel is full, could be high.
    80  	QueuePutLatency prometheus.Histogram
    81  	// The latency of calls to put some data read from block storage into the cache.
    82  	PutLatency prometheus.Histogram
    83  	// The latency of writes of a single cache block to disk.
    84  	DiskWriteLatency prometheus.Histogram
    85  }
    86  
    87  // See docs at Metrics.
    88  type internalMetrics struct {
    89  	count atomic.Int64
    90  
    91  	totalReads          atomic.Int64
    92  	multiShardReads     atomic.Int64
    93  	multiBlockReads     atomic.Int64
    94  	readsWithFullHit    atomic.Int64
    95  	readsWithPartialHit atomic.Int64
    96  	readsWithNoHit      atomic.Int64
    97  
    98  	evictions         atomic.Int64
    99  	writeBackFailures atomic.Int64
   100  
   101  	getLatency       prometheus.Histogram
   102  	diskReadLatency  prometheus.Histogram
   103  	queuePutLatency  prometheus.Histogram
   104  	putLatency       prometheus.Histogram
   105  	diskWriteLatency prometheus.Histogram
   106  }
   107  
   108  const (
   109  	// writeWorkersPerShard is used to establish the number of worker goroutines
   110  	// that perform writes to the cache.
   111  	writeWorkersPerShard = 4
   112  	// writeTaskPerWorker is used to establish how many tasks can be queued up
   113  	// until we have to block.
   114  	writeTasksPerWorker = 4
   115  )
   116  
   117  // Open opens a cache. If there is no existing cache at fsDir, a new one
   118  // is created.
   119  func Open(
   120  	fs vfs.FS,
   121  	logger base.Logger,
   122  	fsDir string,
   123  	blockSize int,
   124  	// shardingBlockSize is the size of a shard block. The cache is split into contiguous
   125  	// shardingBlockSize units. The units are distributed across multiple independent shards
   126  	// of the cache, via a hash(offset) modulo num shards operation. The cache replacement
   127  	// policies operate at the level of shard, not whole cache. This is done to reduce lock
   128  	// contention.
   129  	shardingBlockSize int64,
   130  	sizeBytes int64,
   131  	numShards int,
   132  ) (*Cache, error) {
   133  	if minSize := shardingBlockSize * int64(numShards); sizeBytes < minSize {
   134  		// Up the size so that we have one block per shard. In practice, this should
   135  		// only happen in tests.
   136  		sizeBytes = minSize
   137  	}
   138  
   139  	c := &Cache{
   140  		logger:            logger,
   141  		bm:                makeBlockMath(blockSize),
   142  		shardingBlockSize: shardingBlockSize,
   143  	}
   144  	c.shards = make([]shard, numShards)
   145  	blocksPerShard := sizeBytes / int64(numShards) / int64(blockSize)
   146  	for i := range c.shards {
   147  		if err := c.shards[i].init(c, fs, fsDir, i, blocksPerShard, blockSize, shardingBlockSize); err != nil {
   148  			return nil, err
   149  		}
   150  	}
   151  
   152  	c.writeWorkers.Start(c, numShards*writeWorkersPerShard)
   153  
   154  	c.metrics.getLatency = prometheus.NewHistogram(prometheus.HistogramOpts{Buckets: IOBuckets})
   155  	c.metrics.diskReadLatency = prometheus.NewHistogram(prometheus.HistogramOpts{Buckets: IOBuckets})
   156  	c.metrics.putLatency = prometheus.NewHistogram(prometheus.HistogramOpts{Buckets: IOBuckets})
   157  	c.metrics.diskWriteLatency = prometheus.NewHistogram(prometheus.HistogramOpts{Buckets: IOBuckets})
   158  
   159  	// Measures a channel write, so lower min.
   160  	c.metrics.queuePutLatency = prometheus.NewHistogram(prometheus.HistogramOpts{Buckets: ChannelWriteBuckets})
   161  
   162  	return c, nil
   163  }
   164  
   165  // Close closes the cache. Methods such as ReadAt should not be called after Close is
   166  // called.
   167  func (c *Cache) Close() error {
   168  	c.writeWorkers.Stop()
   169  
   170  	var retErr error
   171  	for i := range c.shards {
   172  		if err := c.shards[i].close(); err != nil && retErr == nil {
   173  			retErr = err
   174  		}
   175  	}
   176  	c.shards = nil
   177  	return retErr
   178  }
   179  
   180  // Metrics return metrics for the cache. Callers should not mutate
   181  // the returned histograms, which are pointer types.
   182  func (c *Cache) Metrics() Metrics {
   183  	return Metrics{
   184  		Count:               c.metrics.count.Load(),
   185  		Size:                c.metrics.count.Load() * int64(c.bm.BlockSize()),
   186  		TotalReads:          c.metrics.totalReads.Load(),
   187  		MultiShardReads:     c.metrics.multiShardReads.Load(),
   188  		MultiBlockReads:     c.metrics.multiBlockReads.Load(),
   189  		ReadsWithFullHit:    c.metrics.readsWithFullHit.Load(),
   190  		ReadsWithPartialHit: c.metrics.readsWithPartialHit.Load(),
   191  		ReadsWithNoHit:      c.metrics.readsWithNoHit.Load(),
   192  		Evictions:           c.metrics.evictions.Load(),
   193  		WriteBackFailures:   c.metrics.writeBackFailures.Load(),
   194  		GetLatency:          c.metrics.getLatency,
   195  		DiskReadLatency:     c.metrics.diskReadLatency,
   196  		QueuePutLatency:     c.metrics.queuePutLatency,
   197  		PutLatency:          c.metrics.putLatency,
   198  		DiskWriteLatency:    c.metrics.diskWriteLatency,
   199  	}
   200  }
   201  
   202  // ReadFlags contains options for Cache.ReadAt.
   203  type ReadFlags struct {
   204  	// ReadOnly instructs ReadAt to not write any new data into the cache; it is
   205  	// used when the data is unlikely to be used again.
   206  	ReadOnly bool
   207  }
   208  
   209  // ReadAt performs a read form an object, attempting to use cached data when
   210  // possible.
   211  func (c *Cache) ReadAt(
   212  	ctx context.Context,
   213  	fileNum base.DiskFileNum,
   214  	p []byte,
   215  	ofs int64,
   216  	objReader remote.ObjectReader,
   217  	objSize int64,
   218  	flags ReadFlags,
   219  ) error {
   220  	c.metrics.totalReads.Add(1)
   221  	if ofs >= objSize {
   222  		if invariants.Enabled {
   223  			panic(fmt.Sprintf("invalid ReadAt offset %v %v", ofs, objSize))
   224  		}
   225  		return io.EOF
   226  	}
   227  	// TODO(radu): for compaction reads, we may not want to read from the cache at
   228  	// all.
   229  	{
   230  		start := time.Now()
   231  		n, err := c.get(fileNum, p, ofs)
   232  		c.metrics.getLatency.Observe(float64(time.Since(start)))
   233  		if err != nil {
   234  			return err
   235  		}
   236  		if n == len(p) {
   237  			// Everything was in cache!
   238  			c.metrics.readsWithFullHit.Add(1)
   239  			return nil
   240  		}
   241  		if n == 0 {
   242  			c.metrics.readsWithNoHit.Add(1)
   243  		} else {
   244  			c.metrics.readsWithPartialHit.Add(1)
   245  		}
   246  
   247  		// Note this. The below code does not need the original ofs, as with the earlier
   248  		// reading from the cache done, the relevant offset is ofs + int64(n). Same with p.
   249  		ofs += int64(n)
   250  		p = p[n:]
   251  
   252  		if invariants.Enabled {
   253  			if n != 0 && c.bm.Remainder(ofs) != 0 {
   254  				panic(fmt.Sprintf("after non-zero read from cache, ofs is not block-aligned: %v %v", ofs, n))
   255  			}
   256  		}
   257  	}
   258  
   259  	if flags.ReadOnly {
   260  		return objReader.ReadAt(ctx, p, ofs)
   261  	}
   262  
   263  	// We must do reads with offset & size that are multiples of the block size. Else
   264  	// later cache hits may return incorrect zeroed results from the cache.
   265  	firstBlockInd := c.bm.Block(ofs)
   266  	adjustedOfs := c.bm.BlockOffset(firstBlockInd)
   267  
   268  	// Take the length of what is left to read plus the length of the adjustment of
   269  	// the offset plus the size of a block minus one and divide by the size of a block
   270  	// to get the number of blocks to read from the object.
   271  	sizeOfOffAdjustment := int(ofs - adjustedOfs)
   272  	adjustedLen := int(c.bm.RoundUp(int64(len(p) + sizeOfOffAdjustment)))
   273  	adjustedP := make([]byte, adjustedLen)
   274  
   275  	// Read the rest from the object. We may need to cap the length to avoid past EOF reads.
   276  	eofCap := int64(adjustedLen)
   277  	if adjustedOfs+eofCap > objSize {
   278  		eofCap = objSize - adjustedOfs
   279  	}
   280  	if err := objReader.ReadAt(ctx, adjustedP[:eofCap], adjustedOfs); err != nil {
   281  		return err
   282  	}
   283  	copy(p, adjustedP[sizeOfOffAdjustment:])
   284  
   285  	start := time.Now()
   286  	c.writeWorkers.QueueWrite(fileNum, adjustedP, adjustedOfs)
   287  	c.metrics.queuePutLatency.Observe(float64(time.Since(start)))
   288  
   289  	return nil
   290  }
   291  
   292  // get attempts to read the requested data from the cache, if it is already
   293  // there.
   294  //
   295  // If all data is available, returns n = len(p).
   296  //
   297  // If data is partially available, a prefix of the data is read; returns n < len(p)
   298  // and no error. If no prefix is available, returns n = 0 and no error.
   299  func (c *Cache) get(fileNum base.DiskFileNum, p []byte, ofs int64) (n int, _ error) {
   300  	// The data extent might cross shard boundaries, hence the loop. In the hot
   301  	// path, max two iterations of this loop will be executed, since reads are sized
   302  	// in units of sstable block size.
   303  	var multiShard bool
   304  	for {
   305  		shard := c.getShard(fileNum, ofs+int64(n))
   306  		cappedLen := len(p[n:])
   307  		if toBoundary := int(c.shardingBlockSize - ((ofs + int64(n)) % c.shardingBlockSize)); cappedLen > toBoundary {
   308  			cappedLen = toBoundary
   309  		}
   310  		numRead, err := shard.get(fileNum, p[n:n+cappedLen], ofs+int64(n))
   311  		if err != nil {
   312  			return n, err
   313  		}
   314  		n += numRead
   315  		if numRead < cappedLen {
   316  			// We only read a prefix from this shard.
   317  			return n, nil
   318  		}
   319  		if n == len(p) {
   320  			// We are done.
   321  			return n, nil
   322  		}
   323  		// Data extent crosses shard boundary, continue with next shard.
   324  		if !multiShard {
   325  			c.metrics.multiShardReads.Add(1)
   326  			multiShard = true
   327  		}
   328  	}
   329  }
   330  
   331  // set attempts to write the requested data to the cache. Both ofs & len(p) must
   332  // be multiples of the block size.
   333  //
   334  // If all of p is not written to the shard, set returns a non-nil error.
   335  func (c *Cache) set(fileNum base.DiskFileNum, p []byte, ofs int64) error {
   336  	if invariants.Enabled {
   337  		if c.bm.Remainder(ofs) != 0 || c.bm.Remainder(int64(len(p))) != 0 {
   338  			panic(fmt.Sprintf("set with ofs & len not multiples of block size: %v %v", ofs, len(p)))
   339  		}
   340  	}
   341  
   342  	// The data extent might cross shard boundaries, hence the loop. In the hot
   343  	// path, max two iterations of this loop will be executed, since reads are sized
   344  	// in units of sstable block size.
   345  	n := 0
   346  	for {
   347  		shard := c.getShard(fileNum, ofs+int64(n))
   348  		cappedLen := len(p[n:])
   349  		if toBoundary := int(c.shardingBlockSize - ((ofs + int64(n)) % c.shardingBlockSize)); cappedLen > toBoundary {
   350  			cappedLen = toBoundary
   351  		}
   352  		err := shard.set(fileNum, p[n:n+cappedLen], ofs+int64(n))
   353  		if err != nil {
   354  			return err
   355  		}
   356  		// set returns an error if cappedLen bytes aren't written to the shard.
   357  		n += cappedLen
   358  		if n == len(p) {
   359  			// We are done.
   360  			return nil
   361  		}
   362  		// Data extent crosses shard boundary, continue with next shard.
   363  	}
   364  }
   365  
   366  func (c *Cache) getShard(fileNum base.DiskFileNum, ofs int64) *shard {
   367  	const prime64 = 1099511628211
   368  	hash := uint64(fileNum.FileNum())*prime64 + uint64(ofs/c.shardingBlockSize)
   369  	// TODO(josh): Instance change ops are often run in production. Such an operation
   370  	// updates len(c.shards); see openSharedCache. As a result, the behavior of this
   371  	// function changes, and the cache empties out at restart time. We may want a better
   372  	// story here eventually.
   373  	return &c.shards[hash%uint64(len(c.shards))]
   374  }
   375  
   376  type shard struct {
   377  	cache             *Cache
   378  	file              vfs.File
   379  	sizeInBlocks      int64
   380  	bm                blockMath
   381  	shardingBlockSize int64
   382  	mu                struct {
   383  		sync.Mutex
   384  		// TODO(josh): None of these datastructures are space-efficient.
   385  		// Focusing on correctness to start.
   386  		where  whereMap
   387  		blocks []cacheBlockState
   388  		// Head of LRU list (doubly-linked circular).
   389  		lruHead cacheBlockIndex
   390  		// Head of free list (singly-linked chain).
   391  		freeHead cacheBlockIndex
   392  	}
   393  }
   394  
   395  type cacheBlockState struct {
   396  	lock    lockState
   397  	logical logicalBlockID
   398  
   399  	// next is the next block in the LRU or free list (or invalidBlockIndex if it
   400  	// is the last block in the free list).
   401  	next cacheBlockIndex
   402  
   403  	// prev is the previous block in the LRU list. It is not used when the block
   404  	// is in the free list.
   405  	prev cacheBlockIndex
   406  }
   407  
   408  // Maps a logical block in an SST to an index of the cache block with the
   409  // file contents (to the "cache block index").
   410  type whereMap map[logicalBlockID]cacheBlockIndex
   411  
   412  type logicalBlockID struct {
   413  	filenum       base.DiskFileNum
   414  	cacheBlockIdx cacheBlockIndex
   415  }
   416  
   417  type lockState int64
   418  
   419  const (
   420  	unlocked lockState = 0
   421  	// >0 lockState tracks the number of distinct readers of some cache block / logical block
   422  	// which is in the secondary cache. It is used to ensure that a cache block is not evicted
   423  	// and overwritten, while there are active readers.
   424  	readLockTakenInc = 1
   425  	// -1 lockState indicates that some cache block is currently being populated with data from
   426  	// blob storage. It is used to ensure that a cache block is not read or evicted again, while
   427  	// it is being populated.
   428  	writeLockTaken = -1
   429  )
   430  
   431  func (s *shard) init(
   432  	cache *Cache,
   433  	fs vfs.FS,
   434  	fsDir string,
   435  	shardIdx int,
   436  	sizeInBlocks int64,
   437  	blockSize int,
   438  	shardingBlockSize int64,
   439  ) error {
   440  	*s = shard{
   441  		cache:        cache,
   442  		sizeInBlocks: sizeInBlocks,
   443  	}
   444  	if blockSize < 1024 || shardingBlockSize%int64(blockSize) != 0 {
   445  		return errors.Newf("invalid block size %d (must divide %d)", blockSize, shardingBlockSize)
   446  	}
   447  	s.bm = makeBlockMath(blockSize)
   448  	s.shardingBlockSize = shardingBlockSize
   449  	file, err := fs.OpenReadWrite(fs.PathJoin(fsDir, fmt.Sprintf("SHARED-CACHE-%03d", shardIdx)))
   450  	if err != nil {
   451  		return err
   452  	}
   453  	// TODO(radu): truncate file if necessary (especially important if we restart
   454  	// with more shards).
   455  	if err := file.Preallocate(0, int64(blockSize)*sizeInBlocks); err != nil {
   456  		return err
   457  	}
   458  	s.file = file
   459  
   460  	// TODO(josh): Right now, the secondary cache is not persistent. All existing
   461  	// cache contents will be over-written, since all metadata is only stored in
   462  	// memory.
   463  	s.mu.where = make(whereMap)
   464  	s.mu.blocks = make([]cacheBlockState, sizeInBlocks)
   465  	s.mu.lruHead = invalidBlockIndex
   466  	s.mu.freeHead = invalidBlockIndex
   467  	for i := range s.mu.blocks {
   468  		s.freePush(cacheBlockIndex(i))
   469  	}
   470  
   471  	return nil
   472  }
   473  
   474  func (s *shard) close() error {
   475  	defer func() {
   476  		s.file = nil
   477  	}()
   478  	return s.file.Close()
   479  }
   480  
   481  // freePush pushes a block to the front of the free list.
   482  func (s *shard) freePush(index cacheBlockIndex) {
   483  	s.mu.blocks[index].next = s.mu.freeHead
   484  	s.mu.freeHead = index
   485  }
   486  
   487  // freePop removes the block from the front of the free list. Must not be called
   488  // if the list is empty (i.e. freeHead = invalidBlockIndex).
   489  func (s *shard) freePop() cacheBlockIndex {
   490  	index := s.mu.freeHead
   491  	s.mu.freeHead = s.mu.blocks[index].next
   492  	return index
   493  }
   494  
   495  // lruInsertFront inserts a block at the front of the LRU list.
   496  func (s *shard) lruInsertFront(index cacheBlockIndex) {
   497  	b := &s.mu.blocks[index]
   498  	if s.mu.lruHead == invalidBlockIndex {
   499  		b.next = index
   500  		b.prev = index
   501  	} else {
   502  		b.next = s.mu.lruHead
   503  		h := &s.mu.blocks[s.mu.lruHead]
   504  		b.prev = h.prev
   505  		s.mu.blocks[h.prev].next = index
   506  		h.prev = index
   507  	}
   508  	s.mu.lruHead = index
   509  }
   510  
   511  func (s *shard) lruNext(index cacheBlockIndex) cacheBlockIndex {
   512  	return s.mu.blocks[index].next
   513  }
   514  
   515  func (s *shard) lruPrev(index cacheBlockIndex) cacheBlockIndex {
   516  	return s.mu.blocks[index].prev
   517  }
   518  
   519  // lruUnlink removes a block from the LRU list.
   520  func (s *shard) lruUnlink(index cacheBlockIndex) {
   521  	b := &s.mu.blocks[index]
   522  	if b.next == index {
   523  		s.mu.lruHead = invalidBlockIndex
   524  	} else {
   525  		s.mu.blocks[b.prev].next = b.next
   526  		s.mu.blocks[b.next].prev = b.prev
   527  		if s.mu.lruHead == index {
   528  			s.mu.lruHead = b.next
   529  		}
   530  	}
   531  	b.next, b.prev = invalidBlockIndex, invalidBlockIndex
   532  }
   533  
   534  // get attempts to read the requested data from the shard. The data must not
   535  // cross a shard boundary.
   536  //
   537  // If all data is available, returns n = len(p).
   538  //
   539  // If data is partially available, a prefix of the data is read; returns n < len(p)
   540  // and no error. If no prefix is available, returns n = 0 and no error.
   541  //
   542  // TODO(josh): Today, if there are two cache blocks needed to satisfy a read, and the
   543  // first block is not in the cache and the second one is, we will read both from
   544  // blob storage. We should fix this. This is not an unlikely scenario if we are doing
   545  // a reverse scan, since those iterate over sstable blocks in reverse order and due to
   546  // cache block aligned reads will have read the suffix of the sstable block that will
   547  // be needed next.
   548  func (s *shard) get(fileNum base.DiskFileNum, p []byte, ofs int64) (n int, _ error) {
   549  	if invariants.Enabled {
   550  		if ofs/s.shardingBlockSize != (ofs+int64(len(p))-1)/s.shardingBlockSize {
   551  			panic(fmt.Sprintf("get crosses shard boundary: %v %v", ofs, len(p)))
   552  		}
   553  		s.assertShardStateIsConsistent()
   554  	}
   555  
   556  	// The data extent might cross cache block boundaries, hence the loop. In the hot
   557  	// path, max two iterations of this loop will be executed, since reads are sized
   558  	// in units of sstable block size.
   559  	var multiBlock bool
   560  	for {
   561  		k := logicalBlockID{
   562  			filenum:       fileNum,
   563  			cacheBlockIdx: s.bm.Block(ofs + int64(n)),
   564  		}
   565  		s.mu.Lock()
   566  		cacheBlockIdx, ok := s.mu.where[k]
   567  		// TODO(josh): Multiple reads within the same few milliseconds (anything that is smaller
   568  		// than blob storage read latency) that miss on the same logical block ID will not necessarily
   569  		// be rare. We may want to do only one read, with the later readers blocking on the first read
   570  		// completing. This could be implemented either here or in the primary block cache. See
   571  		// https://github.com/cockroachdb/pebble/pull/2586 for additional discussion.
   572  		if !ok {
   573  			s.mu.Unlock()
   574  			return n, nil
   575  		}
   576  		if s.mu.blocks[cacheBlockIdx].lock == writeLockTaken {
   577  			// In practice, if we have two reads of the same SST block in close succession, we
   578  			// would expect the second to hit in the in-memory block cache. So it's not worth
   579  			// optimizing this case here.
   580  			s.mu.Unlock()
   581  			return n, nil
   582  		}
   583  		s.mu.blocks[cacheBlockIdx].lock += readLockTakenInc
   584  		// Move to front of the LRU list.
   585  		s.lruUnlink(cacheBlockIdx)
   586  		s.lruInsertFront(cacheBlockIdx)
   587  		s.mu.Unlock()
   588  
   589  		readAt := s.bm.BlockOffset(cacheBlockIdx)
   590  		readSize := s.bm.BlockSize()
   591  		if n == 0 { // if first read
   592  			rem := s.bm.Remainder(ofs)
   593  			readAt += rem
   594  			readSize -= int(rem)
   595  		}
   596  
   597  		if len(p[n:]) <= readSize {
   598  			start := time.Now()
   599  			numRead, err := s.file.ReadAt(p[n:], readAt)
   600  			s.cache.metrics.diskReadLatency.Observe(float64(time.Since(start)))
   601  			s.dropReadLock(cacheBlockIdx)
   602  			return n + numRead, err
   603  		}
   604  		start := time.Now()
   605  		numRead, err := s.file.ReadAt(p[n:n+readSize], readAt)
   606  		s.cache.metrics.diskReadLatency.Observe(float64(time.Since(start)))
   607  		s.dropReadLock(cacheBlockIdx)
   608  		if err != nil {
   609  			return 0, err
   610  		}
   611  
   612  		// Note that numRead == readSize, since we checked for an error above.
   613  		n += numRead
   614  
   615  		if !multiBlock {
   616  			s.cache.metrics.multiBlockReads.Add(1)
   617  			multiBlock = true
   618  		}
   619  	}
   620  }
   621  
   622  // set attempts to write the requested data to the shard. The data must not
   623  // cross a shard boundary, and both ofs & len(p) must be multiples of the
   624  // block size.
   625  //
   626  // If all of p is not written to the shard, set returns a non-nil error.
   627  func (s *shard) set(fileNum base.DiskFileNum, p []byte, ofs int64) error {
   628  	if invariants.Enabled {
   629  		if ofs/s.shardingBlockSize != (ofs+int64(len(p))-1)/s.shardingBlockSize {
   630  			panic(fmt.Sprintf("set crosses shard boundary: %v %v", ofs, len(p)))
   631  		}
   632  		if s.bm.Remainder(ofs) != 0 || s.bm.Remainder(int64(len(p))) != 0 {
   633  			panic(fmt.Sprintf("set with ofs & len not multiples of block size: %v %v", ofs, len(p)))
   634  		}
   635  		s.assertShardStateIsConsistent()
   636  	}
   637  
   638  	// The data extent might cross cache block boundaries, hence the loop. In the hot
   639  	// path, max two iterations of this loop will be executed, since reads are sized
   640  	// in units of sstable block size.
   641  	n := 0
   642  	for {
   643  		if n == len(p) {
   644  			return nil
   645  		}
   646  		if invariants.Enabled {
   647  			if n > len(p) {
   648  				panic(fmt.Sprintf("set with n greater than len(p): %v %v", n, len(p)))
   649  			}
   650  		}
   651  
   652  		// If the logical block is already in the cache, we should skip doing a set.
   653  		k := logicalBlockID{
   654  			filenum:       fileNum,
   655  			cacheBlockIdx: s.bm.Block(ofs + int64(n)),
   656  		}
   657  		s.mu.Lock()
   658  		if _, ok := s.mu.where[k]; ok {
   659  			s.mu.Unlock()
   660  			n += s.bm.BlockSize()
   661  			continue
   662  		}
   663  
   664  		var cacheBlockIdx cacheBlockIndex
   665  		if s.mu.freeHead == invalidBlockIndex {
   666  			if invariants.Enabled && s.mu.lruHead == invalidBlockIndex {
   667  				panic("both LRU and free lists empty")
   668  			}
   669  
   670  			// Find the last element in the LRU list which is not locked.
   671  			for idx := s.lruPrev(s.mu.lruHead); ; idx = s.lruPrev(idx) {
   672  				if lock := s.mu.blocks[idx].lock; lock == unlocked {
   673  					cacheBlockIdx = idx
   674  					break
   675  				}
   676  				if idx == s.mu.lruHead {
   677  					// No unlocked block to evict.
   678  					//
   679  					// TODO(josh): We may want to block until a block frees up, instead of returning
   680  					// an error here. But I think we can do that later on, e.g. after running some production
   681  					// experiments.
   682  					s.mu.Unlock()
   683  					return errors.New("no block to evict so skipping write to cache")
   684  				}
   685  			}
   686  			s.cache.metrics.evictions.Add(1)
   687  			s.lruUnlink(cacheBlockIdx)
   688  			delete(s.mu.where, s.mu.blocks[cacheBlockIdx].logical)
   689  		} else {
   690  			s.cache.metrics.count.Add(1)
   691  			cacheBlockIdx = s.freePop()
   692  		}
   693  
   694  		s.lruInsertFront(cacheBlockIdx)
   695  		s.mu.where[k] = cacheBlockIdx
   696  		s.mu.blocks[cacheBlockIdx].logical = k
   697  		s.mu.blocks[cacheBlockIdx].lock = writeLockTaken
   698  		s.mu.Unlock()
   699  
   700  		writeAt := s.bm.BlockOffset(cacheBlockIdx)
   701  
   702  		writeSize := s.bm.BlockSize()
   703  		if len(p[n:]) <= writeSize {
   704  			writeSize = len(p[n:])
   705  		}
   706  
   707  		start := time.Now()
   708  		_, err := s.file.WriteAt(p[n:n+writeSize], writeAt)
   709  		s.cache.metrics.diskWriteLatency.Observe(float64(time.Since(start)))
   710  		if err != nil {
   711  			// Free the block.
   712  			s.mu.Lock()
   713  			defer s.mu.Unlock()
   714  
   715  			delete(s.mu.where, k)
   716  			s.lruUnlink(cacheBlockIdx)
   717  			s.freePush(cacheBlockIdx)
   718  			return err
   719  		}
   720  		s.dropWriteLock(cacheBlockIdx)
   721  		n += writeSize
   722  	}
   723  }
   724  
   725  // Doesn't inline currently. This might be okay, but something to keep in mind.
   726  func (s *shard) dropReadLock(cacheBlockInd cacheBlockIndex) {
   727  	s.mu.Lock()
   728  	s.mu.blocks[cacheBlockInd].lock -= readLockTakenInc
   729  	if invariants.Enabled && s.mu.blocks[cacheBlockInd].lock < 0 {
   730  		panic(fmt.Sprintf("unexpected lock state %v in dropReadLock", s.mu.blocks[cacheBlockInd].lock))
   731  	}
   732  	s.mu.Unlock()
   733  }
   734  
   735  // Doesn't inline currently. This might be okay, but something to keep in mind.
   736  func (s *shard) dropWriteLock(cacheBlockInd cacheBlockIndex) {
   737  	s.mu.Lock()
   738  	if invariants.Enabled && s.mu.blocks[cacheBlockInd].lock != writeLockTaken {
   739  		panic(fmt.Sprintf("unexpected lock state %v in dropWriteLock", s.mu.blocks[cacheBlockInd].lock))
   740  	}
   741  	s.mu.blocks[cacheBlockInd].lock = unlocked
   742  	s.mu.Unlock()
   743  }
   744  
   745  func (s *shard) assertShardStateIsConsistent() {
   746  	s.mu.Lock()
   747  	defer s.mu.Unlock()
   748  
   749  	lruLen := 0
   750  	if s.mu.lruHead != invalidBlockIndex {
   751  		for b := s.mu.lruHead; ; {
   752  			lruLen++
   753  			if idx, ok := s.mu.where[s.mu.blocks[b].logical]; !ok || idx != b {
   754  				panic("block in LRU list with no entry in where map")
   755  			}
   756  			b = s.lruNext(b)
   757  			if b == s.mu.lruHead {
   758  				break
   759  			}
   760  		}
   761  	}
   762  	if lruLen != len(s.mu.where) {
   763  		panic(fmt.Sprintf("lru list len is %d but where map has %d entries", lruLen, len(s.mu.where)))
   764  	}
   765  	freeLen := 0
   766  	for n := s.mu.freeHead; n != invalidBlockIndex; n = s.mu.blocks[n].next {
   767  		freeLen++
   768  	}
   769  
   770  	if lruLen+freeLen != int(s.sizeInBlocks) {
   771  		panic(fmt.Sprintf("%d lru blocks and %d free blocks don't add up to %d", lruLen, freeLen, s.sizeInBlocks))
   772  	}
   773  	for i := range s.mu.blocks {
   774  		if state := s.mu.blocks[i].lock; state < writeLockTaken {
   775  			panic(fmt.Sprintf("lock state %v is not allowed", state))
   776  		}
   777  	}
   778  }
   779  
   780  // cacheBlockIndex is the index of a blockSize-aligned cache block.
   781  type cacheBlockIndex int64
   782  
   783  // invalidBlockIndex is used for the head of a list when the list is empty.
   784  const invalidBlockIndex cacheBlockIndex = -1
   785  
   786  // blockMath is a helper type for performing conversions between offsets and
   787  // block indexes.
   788  type blockMath struct {
   789  	blockSizeBits int8
   790  }
   791  
   792  func makeBlockMath(blockSize int) blockMath {
   793  	bm := blockMath{
   794  		blockSizeBits: int8(bits.Len64(uint64(blockSize)) - 1),
   795  	}
   796  	if blockSize != (1 << bm.blockSizeBits) {
   797  		panic(fmt.Sprintf("blockSize %d is not a power of 2", blockSize))
   798  	}
   799  	return bm
   800  }
   801  
   802  func (bm blockMath) mask() int64 {
   803  	return (1 << bm.blockSizeBits) - 1
   804  }
   805  
   806  // BlockSize returns the block size.
   807  func (bm blockMath) BlockSize() int {
   808  	return 1 << bm.blockSizeBits
   809  }
   810  
   811  // Block returns the block index containing the given offset.
   812  func (bm blockMath) Block(offset int64) cacheBlockIndex {
   813  	return cacheBlockIndex(offset >> bm.blockSizeBits)
   814  }
   815  
   816  // Remainder returns the offset relative to the start of the cache block.
   817  func (bm blockMath) Remainder(offset int64) int64 {
   818  	return offset & bm.mask()
   819  }
   820  
   821  // BlockOffset returns the object offset where the given block starts.
   822  func (bm blockMath) BlockOffset(block cacheBlockIndex) int64 {
   823  	return int64(block) << bm.blockSizeBits
   824  }
   825  
   826  // RoundUp rounds up the given value to the closest multiple of block size.
   827  func (bm blockMath) RoundUp(x int64) int64 {
   828  	return (x + bm.mask()) & ^(bm.mask())
   829  }
   830  
   831  type writeWorkers struct {
   832  	doneCh        chan struct{}
   833  	doneWaitGroup sync.WaitGroup
   834  
   835  	numWorkers int
   836  	tasksCh    chan writeTask
   837  }
   838  
   839  type writeTask struct {
   840  	fileNum base.DiskFileNum
   841  	p       []byte
   842  	offset  int64
   843  }
   844  
   845  // Start starts the worker goroutines.
   846  func (w *writeWorkers) Start(c *Cache, numWorkers int) {
   847  	doneCh := make(chan struct{})
   848  	tasksCh := make(chan writeTask, numWorkers*writeTasksPerWorker)
   849  
   850  	w.numWorkers = numWorkers
   851  	w.doneCh = doneCh
   852  	w.tasksCh = tasksCh
   853  	w.doneWaitGroup.Add(numWorkers)
   854  	for i := 0; i < numWorkers; i++ {
   855  		go func() {
   856  			defer w.doneWaitGroup.Done()
   857  			for {
   858  				select {
   859  				case <-doneCh:
   860  					return
   861  				case task, ok := <-tasksCh:
   862  					if !ok {
   863  						// The tasks channel was closed; this is used in testing code to
   864  						// ensure all writes are completed.
   865  						return
   866  					}
   867  					// TODO(radu): set() can perform multiple writes; perhaps each one
   868  					// should be its own task.
   869  					start := time.Now()
   870  					err := c.set(task.fileNum, task.p, task.offset)
   871  					c.metrics.putLatency.Observe(float64(time.Since(start)))
   872  					if err != nil {
   873  						c.metrics.writeBackFailures.Add(1)
   874  						// TODO(radu): throttle logs.
   875  						c.logger.Infof("writing back to cache after miss failed: %v", err)
   876  					}
   877  				}
   878  			}
   879  		}()
   880  	}
   881  }
   882  
   883  // Stop waits for any in-progress writes to complete and stops the worker
   884  // goroutines and waits for any in-pro. Any queued writes not yet started are
   885  // discarded.
   886  func (w *writeWorkers) Stop() {
   887  	close(w.doneCh)
   888  	w.doneCh = nil
   889  	w.tasksCh = nil
   890  	w.doneWaitGroup.Wait()
   891  }
   892  
   893  // QueueWrite adds a write task to the queue. Can block if the queue is full.
   894  func (w *writeWorkers) QueueWrite(fileNum base.DiskFileNum, p []byte, offset int64) {
   895  	w.tasksCh <- writeTask{
   896  		fileNum: fileNum,
   897  		p:       p,
   898  		offset:  offset,
   899  	}
   900  }