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 }