github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/data/bcache.go (about)

     1  // Copyright 2016 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package data
     6  
     7  import (
     8  	"fmt"
     9  	"sync"
    10  	"sync/atomic"
    11  
    12  	lru "github.com/hashicorp/golang-lru"
    13  	"github.com/keybase/client/go/kbfs/kbfsblock"
    14  	"github.com/keybase/client/go/kbfs/kbfshash"
    15  	"github.com/keybase/client/go/kbfs/tlf"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  type idCacheKey struct {
    20  	tlf           tlf.ID
    21  	plaintextHash kbfshash.RawDefaultHash
    22  }
    23  
    24  // BlockCacheStandard implements the BlockCache interface by storing
    25  // blocks in an in-memory LRU cache.  Clean blocks are identified
    26  // internally by just their block ID (since blocks are immutable and
    27  // content-addressable).
    28  type BlockCacheStandard struct {
    29  	cleanBytesCapacity uint64
    30  
    31  	ids *lru.Cache
    32  
    33  	cleanTransient *lru.Cache
    34  
    35  	cleanLock      sync.RWMutex
    36  	cleanPermanent map[kbfsblock.ID]Block
    37  
    38  	bytesLock       sync.Mutex
    39  	cleanTotalBytes uint64
    40  }
    41  
    42  // NewBlockCacheStandard constructs a new BlockCacheStandard instance
    43  // with the given transient capacity (in number of entries) and the
    44  // clean bytes capacity, which is the total of number of bytes allowed
    45  // between the transient and permanent clean caches.  If putting a
    46  // block will exceed this bytes capacity, transient entries are
    47  // evicted until the block will fit in capacity.
    48  func NewBlockCacheStandard(transientCapacity int,
    49  	cleanBytesCapacity uint64) *BlockCacheStandard {
    50  	b := &BlockCacheStandard{
    51  		cleanBytesCapacity: cleanBytesCapacity,
    52  		cleanPermanent:     make(map[kbfsblock.ID]Block),
    53  	}
    54  
    55  	if transientCapacity > 0 {
    56  		var err error
    57  		// TODO: Plumb error up.
    58  		b.ids, err = lru.New(transientCapacity)
    59  		if err != nil {
    60  			return nil
    61  		}
    62  
    63  		b.cleanTransient, err = lru.NewWithEvict(transientCapacity, b.onEvict)
    64  		if err != nil {
    65  			return nil
    66  		}
    67  	}
    68  	return b
    69  }
    70  
    71  // GetWithLifetime implements the BlockCache interface for BlockCacheStandard.
    72  func (b *BlockCacheStandard) GetWithLifetime(ptr BlockPointer) (
    73  	Block, BlockCacheLifetime, error) {
    74  	if b.cleanTransient != nil {
    75  		if tmp, ok := b.cleanTransient.Get(ptr.ID); ok {
    76  			block, ok := tmp.(Block)
    77  			if !ok {
    78  				return nil, NoCacheEntry, BadDataError{ptr.ID}
    79  			}
    80  			return block, TransientEntry, nil
    81  		}
    82  	}
    83  
    84  	block := func() Block {
    85  		b.cleanLock.RLock()
    86  		defer b.cleanLock.RUnlock()
    87  		return b.cleanPermanent[ptr.ID]
    88  	}()
    89  	if block != nil {
    90  		return block, PermanentEntry, nil
    91  	}
    92  
    93  	return nil, NoCacheEntry, NoSuchBlockError{ptr.ID}
    94  }
    95  
    96  // Get implements the BlockCache interface for BlockCacheStandard.
    97  func (b *BlockCacheStandard) Get(ptr BlockPointer) (Block, error) {
    98  	block, _, err := b.GetWithLifetime(ptr)
    99  	return block, err
   100  }
   101  
   102  func getCachedBlockSize(block Block) uint32 {
   103  	// Get the size of the block.  For direct file blocks, use the
   104  	// length of the plaintext contents.  For everything else, just
   105  	// approximate the plaintext size using the encoding size.
   106  	switch b := block.(type) {
   107  	case *FileBlock:
   108  		if b.IsInd {
   109  			return b.GetEncodedSize()
   110  		}
   111  		return uint32(len(b.Contents))
   112  	default:
   113  		return block.GetEncodedSize()
   114  	}
   115  }
   116  
   117  func (b *BlockCacheStandard) subtractBlockBytes(block Block) {
   118  	size := uint64(getCachedBlockSize(block))
   119  	b.bytesLock.Lock()
   120  	defer b.bytesLock.Unlock()
   121  	if b.cleanTotalBytes >= size {
   122  		b.cleanTotalBytes -= size
   123  	} else {
   124  		// In case the race mentioned in `PutWithPrefetch` causes us
   125  		// to undercut the byte count.
   126  		b.cleanTotalBytes = 0
   127  	}
   128  }
   129  
   130  func (b *BlockCacheStandard) onEvict(key interface{}, value interface{}) {
   131  	block, ok := value.(Block)
   132  	if !ok {
   133  		return
   134  	}
   135  	b.subtractBlockBytes(block)
   136  }
   137  
   138  // CheckForKnownPtr implements the BlockCache interface for BlockCacheStandard.
   139  func (b *BlockCacheStandard) CheckForKnownPtr(
   140  	tlf tlf.ID, block *FileBlock, hashBehavior BlockCacheHashBehavior) (
   141  	BlockPointer, error) {
   142  	if hashBehavior == SkipCacheHash {
   143  		// Avoid hashing if we're not caching the hashes.
   144  		return BlockPointer{}, nil
   145  	}
   146  
   147  	if block.IsInd {
   148  		return BlockPointer{}, NotDirectFileBlockError{}
   149  	}
   150  
   151  	if b.ids == nil {
   152  		return BlockPointer{}, nil
   153  	}
   154  
   155  	key := idCacheKey{tlf, block.GetHash()}
   156  	tmp, ok := b.ids.Get(key)
   157  	if !ok {
   158  		return BlockPointer{}, nil
   159  	}
   160  
   161  	ptr, ok := tmp.(BlockPointer)
   162  	if !ok {
   163  		return BlockPointer{}, fmt.Errorf("Unexpected cached id: %v", tmp)
   164  	}
   165  	return ptr, nil
   166  }
   167  
   168  // SetCleanBytesCapacity implements the BlockCache interface for
   169  // BlockCacheStandard.
   170  func (b *BlockCacheStandard) SetCleanBytesCapacity(capacity uint64) {
   171  	atomic.StoreUint64(&b.cleanBytesCapacity, capacity)
   172  }
   173  
   174  // GetCleanBytesCapacity implements the BlockCache interface for
   175  // BlockCacheStandard.
   176  func (b *BlockCacheStandard) GetCleanBytesCapacity() (capacity uint64) {
   177  	return atomic.LoadUint64(&b.cleanBytesCapacity)
   178  }
   179  
   180  func (b *BlockCacheStandard) makeRoomForSize(size uint64, lifetime BlockCacheLifetime) bool {
   181  	if b.cleanTransient == nil {
   182  		return false
   183  	}
   184  
   185  	oldLen := b.cleanTransient.Len() + 1
   186  	doUnlock := true
   187  	b.bytesLock.Lock()
   188  	defer func() {
   189  		if doUnlock {
   190  			b.bytesLock.Unlock()
   191  		}
   192  	}()
   193  
   194  	cleanBytesCapacity := b.GetCleanBytesCapacity()
   195  
   196  	// Evict items from the cache until the bytes capacity is lower
   197  	// than the total capacity (or until no items are removed).
   198  	for b.cleanTotalBytes+size > cleanBytesCapacity {
   199  		// Unlock while removing, since onEvict needs the lock and
   200  		// cleanTransient.Len() takes the LRU mutex (which could lead
   201  		// to a deadlock with onEvict).  TODO: either change
   202  		// `cleanTransient` into an `lru.SimpleLRU` and protect it
   203  		// with our own lock, or build our own LRU that can evict
   204  		// based on total bytes.  See #250 and KBFS-1404 for a longer
   205  		// discussion.
   206  		b.bytesLock.Unlock()
   207  		doUnlock = false
   208  		if oldLen == b.cleanTransient.Len() {
   209  			doUnlock = true
   210  			b.bytesLock.Lock()
   211  			break
   212  		}
   213  		oldLen = b.cleanTransient.Len()
   214  		b.cleanTransient.RemoveOldest()
   215  		doUnlock = true
   216  		b.bytesLock.Lock()
   217  	}
   218  
   219  	if b.cleanTotalBytes+size > cleanBytesCapacity {
   220  		// There must be too many permanent clean blocks, so we
   221  		// couldn't make room.
   222  		if lifetime == PermanentEntry {
   223  			// Permanent entries will be added no matter what, so we have to
   224  			// account for them.
   225  			b.cleanTotalBytes += size
   226  		}
   227  		return false
   228  	}
   229  	// Only count clean bytes if we actually have a transient cache.
   230  	b.cleanTotalBytes += size
   231  	return true
   232  }
   233  
   234  // Put implements the BlockCache interface for BlockCacheStandard.
   235  // This method is idempotent for a given ptr, but that invariant is
   236  // not currently goroutine-safe, and it does not hold if a block size
   237  // changes between Puts. That is, we assume that a cached block
   238  // associated with a given pointer will never change its size, even
   239  // when it gets Put into the cache again.
   240  func (b *BlockCacheStandard) Put(
   241  	ptr BlockPointer, tlf tlf.ID, block Block,
   242  	lifetime BlockCacheLifetime, hashBehavior BlockCacheHashBehavior) error {
   243  	// We first check if the block shouldn't be cached, since CommonBlocks can
   244  	// take this path.
   245  	if lifetime == NoCacheEntry {
   246  		return nil
   247  	}
   248  	// Just in case we tried to cache a block type that shouldn't be cached,
   249  	// return an error. This is an insurance check. That said, this got rid of
   250  	// a flake in TestSBSConflicts, so we should still look for the underlying
   251  	// error.
   252  	switch block.(type) {
   253  	case *DirBlock:
   254  	case *FileBlock:
   255  	case *CommonBlock:
   256  		return errors.New("attempted to Put a common block")
   257  	default:
   258  		return errors.Errorf("attempted to Put an unknown block type %T", block)
   259  	}
   260  
   261  	var wasInCache bool
   262  
   263  	switch lifetime {
   264  	case TransientEntry:
   265  		// If it's the right type of block, store the hash -> ID mapping.
   266  		if fBlock, isFileBlock := block.(*FileBlock); b.ids != nil &&
   267  			isFileBlock && !fBlock.IsInd && hashBehavior == DoCacheHash {
   268  			key := idCacheKey{tlf, fBlock.GetHash()}
   269  			// zero out the refnonce, it doesn't matter
   270  			ptr.RefNonce = kbfsblock.ZeroRefNonce
   271  			b.ids.Add(key, ptr)
   272  		}
   273  		if b.cleanTransient == nil {
   274  			return nil
   275  		}
   276  		// We could use `cleanTransient.Contains()`, but that wouldn't update
   277  		// the LRU time. By using `Get`, we make it less likely that another
   278  		// goroutine will evict this block before we can `Put` it again.
   279  		_, wasInCache = b.cleanTransient.Get(ptr.ID)
   280  		// Cache it later, once we know there's room
   281  
   282  	case PermanentEntry:
   283  		if hashBehavior != SkipCacheHash {
   284  			return errors.New("Must skip cache hash for permanent entries")
   285  		}
   286  		func() {
   287  			b.cleanLock.Lock()
   288  			defer b.cleanLock.Unlock()
   289  			_, wasInCache = b.cleanPermanent[ptr.ID]
   290  			b.cleanPermanent[ptr.ID] = block
   291  		}()
   292  
   293  	default:
   294  		return fmt.Errorf("Unknown lifetime %v", lifetime)
   295  	}
   296  
   297  	transientCacheHasRoom := true
   298  	// We must make room whether the cache is transient or permanent, but only
   299  	// if it wasn't already in the cache.
   300  	// TODO: This is racy, where another goroutine can evict or add this block
   301  	// between our check above and our attempt to make room. If the other
   302  	// goroutine evicts this block, we under-count its size as 0. If the other
   303  	// goroutine inserts this block, we double-count it.
   304  	if !wasInCache {
   305  		size := uint64(getCachedBlockSize(block))
   306  		transientCacheHasRoom = b.makeRoomForSize(size, lifetime)
   307  	}
   308  	if lifetime == TransientEntry {
   309  		if !transientCacheHasRoom {
   310  			return CachePutCacheFullError{ptr.ID}
   311  		}
   312  		b.cleanTransient.Add(ptr.ID, block)
   313  	}
   314  
   315  	return nil
   316  }
   317  
   318  // DeletePermanent implements the BlockCache interface for
   319  // BlockCacheStandard.
   320  func (b *BlockCacheStandard) DeletePermanent(id kbfsblock.ID) error {
   321  	b.cleanLock.Lock()
   322  	defer b.cleanLock.Unlock()
   323  	block, ok := b.cleanPermanent[id]
   324  	if ok {
   325  		delete(b.cleanPermanent, id)
   326  		b.subtractBlockBytes(block)
   327  	}
   328  	return nil
   329  }
   330  
   331  // DeleteTransient implements the BlockCache interface for BlockCacheStandard.
   332  func (b *BlockCacheStandard) DeleteTransient(
   333  	id kbfsblock.ID, tlf tlf.ID) error {
   334  	if b.cleanTransient == nil {
   335  		return nil
   336  	}
   337  
   338  	// If the block is cached and a file block, delete the known
   339  	// pointer as well.
   340  	if tmp, ok := b.cleanTransient.Get(id); ok {
   341  		block, ok := tmp.(Block)
   342  		if !ok {
   343  			return BadDataError{id}
   344  		}
   345  
   346  		// Remove the key if it exists
   347  		if fBlock, ok := block.(*FileBlock); b.ids != nil && ok &&
   348  			!fBlock.IsInd {
   349  			_, hash := kbfshash.DoRawDefaultHash(fBlock.Contents)
   350  			key := idCacheKey{tlf, hash}
   351  			b.ids.Remove(key)
   352  		}
   353  
   354  		b.cleanTransient.Remove(id)
   355  	}
   356  	return nil
   357  }
   358  
   359  // DeleteKnownPtr implements the BlockCache interface for BlockCacheStandard.
   360  func (b *BlockCacheStandard) DeleteKnownPtr(tlf tlf.ID, block *FileBlock) error {
   361  	if block.IsInd {
   362  		return NotDirectFileBlockError{}
   363  	}
   364  
   365  	if b.ids == nil {
   366  		return nil
   367  	}
   368  
   369  	_, hash := kbfshash.DoRawDefaultHash(block.Contents)
   370  	key := idCacheKey{tlf, hash}
   371  	b.ids.Remove(key)
   372  	return nil
   373  }
   374  
   375  // NumCleanTransientBlocks returns the number of blocks in the cache
   376  // with transient lifetimes.
   377  func (b *BlockCacheStandard) NumCleanTransientBlocks() int {
   378  	return b.cleanTransient.Len()
   379  }