github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/disk_md_cache.go (about) 1 // Copyright 2018 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 libkbfs 6 7 import ( 8 "context" 9 "io" 10 "path/filepath" 11 "sync" 12 "time" 13 14 "github.com/keybase/client/go/kbfs/kbfsmd" 15 "github.com/keybase/client/go/kbfs/ldbutils" 16 "github.com/keybase/client/go/kbfs/tlf" 17 "github.com/keybase/client/go/logger" 18 "github.com/pkg/errors" 19 ldberrors "github.com/syndtr/goleveldb/leveldb/errors" 20 "github.com/syndtr/goleveldb/leveldb/filter" 21 "github.com/syndtr/goleveldb/leveldb/opt" 22 "github.com/syndtr/goleveldb/leveldb/storage" 23 ) 24 25 const ( 26 headsDbFilename string = "diskCacheMDHeads.leveldb" 27 initialDiskMDCacheVersion uint64 = 1 28 currentDiskMDCacheVersion uint64 = initialDiskMDCacheVersion 29 defaultMDCacheTableSize int = 50 * opt.MiB 30 mdCacheFolderName string = "kbfs_md_cache" 31 ) 32 33 // diskMDCacheConfig specifies the interfaces that a DiskMDCacheStandard 34 // needs to perform its functions. This adheres to the standard libkbfs Config 35 // API. 36 type diskMDCacheConfig interface { 37 codecGetter 38 logMaker 39 } 40 41 type diskMDBlock struct { 42 // Exported only for serialization. 43 Buf []byte 44 Ver kbfsmd.MetadataVer 45 Time time.Time 46 Revision kbfsmd.Revision 47 } 48 49 // DiskMDCacheLocal is the standard implementation for DiskMDCache. 50 type DiskMDCacheLocal struct { 51 config diskMDCacheConfig 52 log logger.Logger 53 54 // Track the cache hit rate and eviction rate 55 hitMeter *ldbutils.CountMeter 56 missMeter *ldbutils.CountMeter 57 putMeter *ldbutils.CountMeter 58 // Protect the disk caches from being shutdown while they're being 59 // accessed, and mutable data. 60 lock sync.RWMutex 61 headsDb *ldbutils.LevelDb // tlfID -> metadata block 62 tlfsCached map[tlf.ID]kbfsmd.Revision 63 tlfsStaged map[tlf.ID][]diskMDBlock 64 65 startedCh chan struct{} 66 startErrCh chan struct{} 67 shutdownCh chan struct{} 68 69 closer func() 70 } 71 72 var _ DiskMDCache = (*DiskMDCacheLocal)(nil) 73 74 // DiskMDCacheStartState represents whether this disk MD cache has 75 // started or failed. 76 type DiskMDCacheStartState int 77 78 // String allows DiskMDCacheStartState to be output as a string. 79 func (s DiskMDCacheStartState) String() string { 80 switch s { 81 case DiskMDCacheStartStateStarting: 82 return "starting" 83 case DiskMDCacheStartStateStarted: 84 return "started" 85 case DiskMDCacheStartStateFailed: 86 return "failed" 87 default: 88 return "unknown" 89 } 90 } 91 92 const ( 93 // DiskMDCacheStartStateStarting represents when the cache is starting. 94 DiskMDCacheStartStateStarting DiskMDCacheStartState = iota 95 // DiskMDCacheStartStateStarted represents when the cache has started. 96 DiskMDCacheStartStateStarted 97 // DiskMDCacheStartStateFailed represents when the cache has failed to 98 // start. 99 DiskMDCacheStartStateFailed 100 ) 101 102 // DiskMDCacheStatus represents the status of the MD cache. 103 type DiskMDCacheStatus struct { 104 StartState DiskMDCacheStartState 105 NumMDs uint64 106 NumStaged uint64 107 Hits ldbutils.MeterStatus 108 Misses ldbutils.MeterStatus 109 Puts ldbutils.MeterStatus 110 DBStats []string `json:",omitempty"` 111 } 112 113 // newDiskMDCacheLocalFromStorage creates a new *DiskMDCacheLocal 114 // with the passed-in storage.Storage interfaces as storage layers for each 115 // cache. 116 func newDiskMDCacheLocalFromStorage( 117 config diskMDCacheConfig, headsStorage storage.Storage, mode InitMode) ( 118 cache *DiskMDCacheLocal, err error) { 119 log := config.MakeLogger("DMC") 120 closers := make([]io.Closer, 0, 1) 121 closer := func() { 122 for _, c := range closers { 123 closeErr := c.Close() 124 if closeErr != nil { 125 log.Warning("Error closing leveldb or storage: %+v", closeErr) 126 } 127 } 128 } 129 defer func() { 130 if err != nil { 131 err = errors.WithStack(err) 132 closer() 133 } 134 }() 135 mdDbOptions := ldbutils.LeveldbOptions(mode) 136 mdDbOptions.CompactionTableSize = defaultMDCacheTableSize 137 mdDbOptions.Filter = filter.NewBloomFilter(16) 138 headsDb, err := ldbutils.OpenLevelDbWithOptions(headsStorage, mdDbOptions) 139 if err != nil { 140 return nil, err 141 } 142 closers = append(closers, headsDb) 143 144 startedCh := make(chan struct{}) 145 startErrCh := make(chan struct{}) 146 cache = &DiskMDCacheLocal{ 147 config: config, 148 hitMeter: ldbutils.NewCountMeter(), 149 missMeter: ldbutils.NewCountMeter(), 150 putMeter: ldbutils.NewCountMeter(), 151 log: log, 152 headsDb: headsDb, 153 tlfsStaged: make(map[tlf.ID][]diskMDBlock), 154 startedCh: startedCh, 155 startErrCh: startErrCh, 156 shutdownCh: make(chan struct{}), 157 closer: closer, 158 } 159 // Sync the MD counts asynchronously so syncing doesn't block init. 160 // Since this method blocks, any Get or Put requests to the disk MD 161 // cache will block until this is done. The log will contain the beginning 162 // and end of this sync. 163 go func() { 164 err := cache.syncMDCountsFromDb() 165 if err != nil { 166 close(startErrCh) 167 closer() 168 log.Warning("Disabling disk MD cache due to error syncing the "+ 169 "MD counts from DB: %+v", err) 170 return 171 } 172 close(startedCh) 173 }() 174 return cache, nil 175 } 176 177 // newDiskMDCacheLocal creates a new *DiskMDCacheLocal with a 178 // specified directory on the filesystem as storage. 179 func newDiskMDCacheLocal( 180 config diskBlockCacheConfig, dirPath string, mode InitMode) ( 181 cache *DiskMDCacheLocal, err error) { 182 log := config.MakeLogger("DMC") 183 defer func() { 184 if err != nil { 185 log.Error("Error initializing MD cache: %+v", err) 186 } 187 }() 188 cachePath := filepath.Join(dirPath, mdCacheFolderName) 189 versionPath, err := ldbutils.GetVersionedPathForDb( 190 log, cachePath, "disk md cache", currentDiskMDCacheVersion) 191 if err != nil { 192 return nil, err 193 } 194 headsDbPath := filepath.Join(versionPath, headsDbFilename) 195 headsStorage, err := storage.OpenFile(headsDbPath, false) 196 if err != nil { 197 return nil, err 198 } 199 defer func() { 200 if err != nil { 201 headsStorage.Close() 202 } 203 }() 204 return newDiskMDCacheLocalFromStorage(config, headsStorage, mode) 205 } 206 207 // WaitUntilStarted waits until this cache has started. 208 func (cache *DiskMDCacheLocal) WaitUntilStarted() error { 209 select { 210 case <-cache.startedCh: 211 return nil 212 case <-cache.startErrCh: 213 return DiskMDCacheError{"error starting channel"} 214 } 215 } 216 217 func (cache *DiskMDCacheLocal) syncMDCountsFromDb() error { 218 cache.log.Debug("+ syncMDCountsFromDb begin") 219 defer cache.log.Debug("- syncMDCountsFromDb end") 220 // We take a write lock for this to prevent any reads from happening while 221 // we're syncing the MD counts. 222 cache.lock.Lock() 223 defer cache.lock.Unlock() 224 225 tlfsCached := make(map[tlf.ID]kbfsmd.Revision) 226 iter := cache.headsDb.NewIterator(nil, nil) 227 defer iter.Release() 228 for iter.Next() { 229 var tlfID tlf.ID 230 err := tlfID.UnmarshalBinary(iter.Key()) 231 if err != nil { 232 return err 233 } 234 235 var md diskMDBlock 236 err = cache.config.Codec().Decode(iter.Value(), &md) 237 if err != nil { 238 return err 239 } 240 241 tlfsCached[tlfID] = md.Revision 242 } 243 cache.tlfsCached = tlfsCached 244 return nil 245 } 246 247 // getMetadataLocked retrieves the metadata for a block in the cache, or 248 // returns leveldb.ErrNotFound and a zero-valued metadata otherwise. 249 func (cache *DiskMDCacheLocal) getMetadataLocked( 250 tlfID tlf.ID, metered bool) (metadata diskMDBlock, err error) { 251 var hitMeter, missMeter *ldbutils.CountMeter 252 if ldbutils.Metered { 253 hitMeter = cache.hitMeter 254 missMeter = cache.missMeter 255 } 256 257 metadataBytes, err := cache.headsDb.GetWithMeter( 258 tlfID.Bytes(), hitMeter, missMeter) 259 if err != nil { 260 return diskMDBlock{}, err 261 } 262 err = cache.config.Codec().Decode(metadataBytes, &metadata) 263 if err != nil { 264 return diskMDBlock{}, err 265 } 266 return metadata, nil 267 } 268 269 // checkAndLockCache checks whether the cache is started. 270 func (cache *DiskMDCacheLocal) checkCacheLocked( 271 ctx context.Context, method string) error { 272 // First see if the context has expired since we began. 273 select { 274 case <-ctx.Done(): 275 return ctx.Err() 276 default: 277 } 278 279 select { 280 case <-cache.startedCh: 281 case <-cache.startErrCh: 282 // The cache will never be started. No need for a stack here since this 283 // could happen anywhere. 284 return DiskCacheStartingError{method} 285 default: 286 // If the cache hasn't started yet, return an error. No need for a 287 // stack here since this could happen anywhere. 288 return DiskCacheStartingError{method} 289 } 290 // shutdownCh has to be checked under lock, otherwise we can race. 291 select { 292 case <-cache.shutdownCh: 293 return errors.WithStack(DiskCacheClosedError{method}) 294 default: 295 } 296 if cache.headsDb == nil { 297 return errors.WithStack(DiskCacheClosedError{method}) 298 } 299 return nil 300 } 301 302 // Get implements the DiskMDCache interface for DiskMDCacheLocal. 303 func (cache *DiskMDCacheLocal) Get( 304 ctx context.Context, tlfID tlf.ID) ( 305 buf []byte, ver kbfsmd.MetadataVer, timestamp time.Time, err error) { 306 cache.lock.RLock() 307 defer cache.lock.RUnlock() 308 err = cache.checkCacheLocked(ctx, "MD(Get)") 309 if err != nil { 310 return nil, -1, time.Time{}, err 311 } 312 313 if _, ok := cache.tlfsCached[tlfID]; !ok { 314 cache.missMeter.Mark(1) 315 return nil, -1, time.Time{}, errors.WithStack(ldberrors.ErrNotFound) 316 } 317 318 md, err := cache.getMetadataLocked(tlfID, ldbutils.Metered) 319 if err != nil { 320 return nil, -1, time.Time{}, err 321 } 322 return md.Buf, md.Ver, md.Time, nil 323 } 324 325 // Stage implements the DiskMDCache interface for DiskMDCacheLocal. 326 func (cache *DiskMDCacheLocal) Stage( 327 ctx context.Context, tlfID tlf.ID, rev kbfsmd.Revision, buf []byte, 328 ver kbfsmd.MetadataVer, timestamp time.Time) error { 329 cache.lock.Lock() 330 defer cache.lock.Unlock() 331 err := cache.checkCacheLocked(ctx, "MD(Stage)") 332 if err != nil { 333 return err 334 } 335 336 if cachedRev, ok := cache.tlfsCached[tlfID]; ok && cachedRev >= rev { 337 // Ignore stages for older revisions 338 return nil 339 } 340 341 md := diskMDBlock{ 342 Buf: buf, 343 Ver: ver, 344 Time: timestamp, 345 Revision: rev, 346 } 347 348 cache.tlfsStaged[tlfID] = append(cache.tlfsStaged[tlfID], md) 349 return nil 350 } 351 352 // Commit implements the DiskMDCache interface for DiskMDCacheLocal. 353 func (cache *DiskMDCacheLocal) Commit( 354 ctx context.Context, tlfID tlf.ID, rev kbfsmd.Revision) error { 355 cache.lock.Lock() 356 defer cache.lock.Unlock() 357 err := cache.checkCacheLocked(ctx, "MD(Commit)") 358 if err != nil { 359 return err 360 } 361 362 stagedMDs := cache.tlfsStaged[tlfID] 363 if len(stagedMDs) == 0 { 364 // Nothing to do. 365 return nil 366 } 367 newStagedMDs := make([]diskMDBlock, 0, len(stagedMDs)-1) 368 foundMD := false 369 // The staged MDs list is unordered, so iterate through the whole 370 // thing to find what should remain after commiting `rev`. 371 for _, md := range stagedMDs { 372 switch { 373 case md.Revision > rev: 374 newStagedMDs = append(newStagedMDs, md) 375 continue 376 case md.Revision < rev: 377 continue 378 case foundMD: 379 // Duplicate. 380 continue 381 } 382 foundMD = true 383 384 encodedMetadata, err := cache.config.Codec().Encode(&md) 385 if err != nil { 386 return err 387 } 388 389 err = cache.headsDb.PutWithMeter( 390 tlfID.Bytes(), encodedMetadata, cache.putMeter) 391 if err != nil { 392 return err 393 } 394 } 395 396 if !foundMD { 397 // Nothing to do. 398 return nil 399 } 400 401 cache.tlfsCached[tlfID] = rev 402 if len(newStagedMDs) == 0 { 403 delete(cache.tlfsStaged, tlfID) 404 } else { 405 cache.tlfsStaged[tlfID] = newStagedMDs 406 } 407 return nil 408 } 409 410 // Unstage implements the DiskMDCache interface for DiskMDCacheLocal. 411 func (cache *DiskMDCacheLocal) Unstage( 412 ctx context.Context, tlfID tlf.ID, rev kbfsmd.Revision) error { 413 cache.lock.Lock() 414 defer cache.lock.Unlock() 415 err := cache.checkCacheLocked(ctx, "MD(Unstage)") 416 if err != nil { 417 return err 418 } 419 420 // Just remove the first one matching `rev`. 421 stagedMDs := cache.tlfsStaged[tlfID] 422 for i, md := range stagedMDs { 423 if md.Revision == rev { 424 if len(stagedMDs) == 1 { 425 delete(cache.tlfsStaged, tlfID) 426 } else { 427 cache.tlfsStaged[tlfID] = append( 428 stagedMDs[:i], stagedMDs[i+1:]...) 429 } 430 return nil 431 } 432 } 433 434 return nil 435 } 436 437 // Status implements the DiskMDCache interface for DiskMDCacheLocal. 438 func (cache *DiskMDCacheLocal) Status(ctx context.Context) DiskMDCacheStatus { 439 select { 440 case <-cache.startedCh: 441 case <-cache.startErrCh: 442 return DiskMDCacheStatus{StartState: DiskMDCacheStartStateFailed} 443 default: 444 return DiskMDCacheStatus{StartState: DiskMDCacheStartStateStarting} 445 } 446 447 cache.lock.RLock() 448 defer cache.lock.RUnlock() 449 numStaged := uint64(0) 450 for _, mds := range cache.tlfsStaged { 451 numStaged += uint64(len(mds)) 452 } 453 454 var dbStats []string 455 if err := cache.checkCacheLocked(ctx, "MD(Status)"); err == nil { 456 dbStats, err = cache.headsDb.StatStrings() 457 if err != nil { 458 cache.log.CDebugf(ctx, "Couldn't get db stats: %+v", err) 459 } 460 } 461 462 return DiskMDCacheStatus{ 463 StartState: DiskMDCacheStartStateStarted, 464 NumMDs: uint64(len(cache.tlfsCached)), 465 NumStaged: numStaged, 466 Hits: ldbutils.RateMeterToStatus(cache.hitMeter), 467 Misses: ldbutils.RateMeterToStatus(cache.missMeter), 468 Puts: ldbutils.RateMeterToStatus(cache.putMeter), 469 DBStats: dbStats, 470 } 471 } 472 473 // Shutdown implements the DiskMDCache interface for DiskMDCacheLocal. 474 func (cache *DiskMDCacheLocal) Shutdown(ctx context.Context) { 475 // Wait for the cache to either finish starting or error. 476 select { 477 case <-cache.startedCh: 478 case <-cache.startErrCh: 479 return 480 } 481 cache.lock.Lock() 482 defer cache.lock.Unlock() 483 // shutdownCh has to be checked under lock, otherwise we can race. 484 select { 485 case <-cache.shutdownCh: 486 cache.log.CWarningf(ctx, "Shutdown called more than once") 487 return 488 default: 489 } 490 close(cache.shutdownCh) 491 if cache.headsDb == nil { 492 return 493 } 494 cache.closer() 495 cache.headsDb = nil 496 cache.hitMeter.Shutdown() 497 cache.missMeter.Shutdown() 498 cache.putMeter.Shutdown() 499 }