github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/disk_quota_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 13 "github.com/keybase/client/go/kbfs/kbfsblock" 14 "github.com/keybase/client/go/kbfs/ldbutils" 15 "github.com/keybase/client/go/logger" 16 "github.com/keybase/client/go/protocol/keybase1" 17 "github.com/pkg/errors" 18 "github.com/syndtr/goleveldb/leveldb/filter" 19 "github.com/syndtr/goleveldb/leveldb/opt" 20 "github.com/syndtr/goleveldb/leveldb/storage" 21 ) 22 23 const ( 24 quotaDbFilename string = "diskCacheQuota.leveldb" 25 initialDiskQuotaCacheVersion uint64 = 1 26 currentDiskQuotaCacheVersion uint64 = initialDiskQuotaCacheVersion 27 defaultQuotaCacheTableSize int = 50 * opt.MiB 28 quotaCacheFolderName string = "kbfs_quota_cache" 29 ) 30 31 // diskQuotaCacheConfig specifies the interfaces that a DiskQuotaCacheLocal 32 // needs to perform its functions. This adheres to the standard libkbfs Config 33 // API. 34 type diskQuotaCacheConfig interface { 35 codecGetter 36 logMaker 37 } 38 39 // DiskQuotaCacheLocal is the standard implementation for DiskQuotaCache. 40 type DiskQuotaCacheLocal struct { 41 config diskQuotaCacheConfig 42 log logger.Logger 43 44 // Track the cache hit rate and eviction rate 45 hitMeter *ldbutils.CountMeter 46 missMeter *ldbutils.CountMeter 47 putMeter *ldbutils.CountMeter 48 // Protect the disk caches from being shutdown while they're being 49 // accessed, and mutable data. 50 lock sync.RWMutex 51 db *ldbutils.LevelDb // id -> quota info 52 quotasCached map[keybase1.UserOrTeamID]bool 53 54 startedCh chan struct{} 55 startErrCh chan struct{} 56 shutdownCh chan struct{} 57 58 closer func() 59 } 60 61 var _ DiskQuotaCache = (*DiskQuotaCacheLocal)(nil) 62 63 // DiskQuotaCacheStartState represents whether this disk Quota cache has 64 // started or failed. 65 type DiskQuotaCacheStartState int 66 67 // String allows DiskQuotaCacheStartState to be output as a string. 68 func (s DiskQuotaCacheStartState) String() string { 69 switch s { 70 case DiskQuotaCacheStartStateStarting: 71 return "starting" 72 case DiskQuotaCacheStartStateStarted: 73 return "started" 74 case DiskQuotaCacheStartStateFailed: 75 return "failed" 76 default: 77 return "unknown" 78 } 79 } 80 81 const ( 82 // DiskQuotaCacheStartStateStarting represents when the cache is starting. 83 DiskQuotaCacheStartStateStarting DiskQuotaCacheStartState = iota 84 // DiskQuotaCacheStartStateStarted represents when the cache has started. 85 DiskQuotaCacheStartStateStarted 86 // DiskQuotaCacheStartStateFailed represents when the cache has failed to 87 // start. 88 DiskQuotaCacheStartStateFailed 89 ) 90 91 // DiskQuotaCacheStatus represents the status of the Quota cache. 92 type DiskQuotaCacheStatus struct { 93 StartState DiskQuotaCacheStartState 94 NumQuotas uint64 95 Hits ldbutils.MeterStatus 96 Misses ldbutils.MeterStatus 97 Puts ldbutils.MeterStatus 98 DBStats []string `json:",omitempty"` 99 } 100 101 // newDiskQuotaCacheLocalFromStorage creates a new *DiskQuotaCacheLocal 102 // with the passed-in storage.Storage interfaces as storage layers for each 103 // cache. 104 func newDiskQuotaCacheLocalFromStorage( 105 config diskQuotaCacheConfig, quotaStorage storage.Storage, mode InitMode) ( 106 cache *DiskQuotaCacheLocal, err error) { 107 log := config.MakeLogger("DQC") 108 closers := make([]io.Closer, 0, 1) 109 closer := func() { 110 for _, c := range closers { 111 closeErr := c.Close() 112 if closeErr != nil { 113 log.Warning("Error closing leveldb or storage: %+v", closeErr) 114 } 115 } 116 } 117 defer func() { 118 if err != nil { 119 err = errors.WithStack(err) 120 closer() 121 } 122 }() 123 quotaDbOptions := ldbutils.LeveldbOptions(mode) 124 quotaDbOptions.CompactionTableSize = defaultQuotaCacheTableSize 125 quotaDbOptions.Filter = filter.NewBloomFilter(16) 126 db, err := ldbutils.OpenLevelDbWithOptions(quotaStorage, quotaDbOptions) 127 if err != nil { 128 return nil, err 129 } 130 closers = append(closers, db) 131 132 startedCh := make(chan struct{}) 133 startErrCh := make(chan struct{}) 134 cache = &DiskQuotaCacheLocal{ 135 config: config, 136 hitMeter: ldbutils.NewCountMeter(), 137 missMeter: ldbutils.NewCountMeter(), 138 putMeter: ldbutils.NewCountMeter(), 139 log: log, 140 db: db, 141 quotasCached: make(map[keybase1.UserOrTeamID]bool), 142 startedCh: startedCh, 143 startErrCh: startErrCh, 144 shutdownCh: make(chan struct{}), 145 closer: closer, 146 } 147 // Sync the quota counts asynchronously so syncing doesn't block init. 148 // Since this method blocks, any Get or Put requests to the disk Quota 149 // cache will block until this is done. The log will contain the beginning 150 // and end of this sync. 151 go func() { 152 err := cache.syncQuotaCountsFromDb() 153 if err != nil { 154 close(startErrCh) 155 closer() 156 log.Warning("Disabling disk quota cache due to error syncing the "+ 157 "quota counts from DB: %+v", err) 158 return 159 } 160 close(startedCh) 161 }() 162 return cache, nil 163 } 164 165 // newDiskQuotaCacheLocal creates a new *DiskQuotaCacheLocal with a 166 // specified directory on the filesystem as storage. 167 func newDiskQuotaCacheLocal( 168 config diskBlockCacheConfig, dirPath string, mode InitMode) ( 169 cache *DiskQuotaCacheLocal, err error) { 170 log := config.MakeLogger("DQC") 171 defer func() { 172 if err != nil { 173 log.Error("Error initializing quota cache: %+v", err) 174 } 175 }() 176 cachePath := filepath.Join(dirPath, quotaCacheFolderName) 177 versionPath, err := ldbutils.GetVersionedPathForDb( 178 log, cachePath, "disk quota cache", currentDiskQuotaCacheVersion) 179 if err != nil { 180 return nil, err 181 } 182 dbPath := filepath.Join(versionPath, quotaDbFilename) 183 quotaStorage, err := storage.OpenFile(dbPath, false) 184 if err != nil { 185 return nil, err 186 } 187 defer func() { 188 if err != nil { 189 quotaStorage.Close() 190 } 191 }() 192 return newDiskQuotaCacheLocalFromStorage(config, quotaStorage, mode) 193 } 194 195 // WaitUntilStarted waits until this cache has started. 196 func (cache *DiskQuotaCacheLocal) WaitUntilStarted() error { 197 select { 198 case <-cache.startedCh: 199 return nil 200 case <-cache.startErrCh: 201 return DiskQuotaCacheError{"error starting channel"} 202 } 203 } 204 205 func (cache *DiskQuotaCacheLocal) syncQuotaCountsFromDb() error { 206 cache.log.Debug("+ syncQuotaCountsFromDb begin") 207 defer cache.log.Debug("- syncQuotaCountsFromDb end") 208 // We take a write lock for this to prevent any reads from happening while 209 // we're syncing the Quota counts. 210 cache.lock.Lock() 211 defer cache.lock.Unlock() 212 213 quotasCached := make(map[keybase1.UserOrTeamID]bool) 214 iter := cache.db.NewIterator(nil, nil) 215 defer iter.Release() 216 for iter.Next() { 217 var id keybase1.UserOrTeamID 218 id, err := keybase1.UserOrTeamIDFromString(string(iter.Key())) 219 if err != nil { 220 return err 221 } 222 223 quotasCached[id] = true 224 } 225 cache.quotasCached = quotasCached 226 return nil 227 } 228 229 // getQuotaLocked retrieves the quota info for a block in the cache, 230 // or returns leveldb.ErrNotFound and a zero-valued metadata 231 // otherwise. 232 func (cache *DiskQuotaCacheLocal) getQuotaLocked( 233 id keybase1.UserOrTeamID, metered bool) ( 234 info kbfsblock.QuotaInfo, err error) { 235 var hitMeter, missMeter *ldbutils.CountMeter 236 if ldbutils.Metered { 237 hitMeter = cache.hitMeter 238 missMeter = cache.missMeter 239 } 240 241 quotaBytes, err := cache.db.GetWithMeter( 242 []byte(id.String()), hitMeter, missMeter) 243 if err != nil { 244 return kbfsblock.QuotaInfo{}, err 245 } 246 err = cache.config.Codec().Decode(quotaBytes, &info) 247 if err != nil { 248 return kbfsblock.QuotaInfo{}, err 249 } 250 return info, nil 251 } 252 253 // checkAndLockCache checks whether the cache is started. 254 func (cache *DiskQuotaCacheLocal) checkCacheLocked( 255 ctx context.Context, method string) error { 256 // First see if the context has expired since we began. 257 select { 258 case <-ctx.Done(): 259 return ctx.Err() 260 default: 261 } 262 263 select { 264 case <-cache.startedCh: 265 case <-cache.startErrCh: 266 // The cache will never be started. No need for a stack here since this 267 // could happen anywhere. 268 return DiskCacheStartingError{method} 269 default: 270 // If the cache hasn't started yet, return an error. No need for a 271 // stack here since this could happen anywhere. 272 return DiskCacheStartingError{method} 273 } 274 // shutdownCh has to be checked under lock, otherwise we can race. 275 select { 276 case <-cache.shutdownCh: 277 return errors.WithStack(DiskCacheClosedError{method}) 278 default: 279 } 280 if cache.db == nil { 281 return errors.WithStack(DiskCacheClosedError{method}) 282 } 283 return nil 284 } 285 286 // Get implements the DiskQuotaCache interface for DiskQuotaCacheLocal. 287 func (cache *DiskQuotaCacheLocal) Get( 288 ctx context.Context, id keybase1.UserOrTeamID) ( 289 info kbfsblock.QuotaInfo, err error) { 290 cache.lock.RLock() 291 defer cache.lock.RUnlock() 292 err = cache.checkCacheLocked(ctx, "Quota(Get)") 293 if err != nil { 294 return kbfsblock.QuotaInfo{}, err 295 } 296 297 return cache.getQuotaLocked(id, ldbutils.Metered) 298 } 299 300 // Put implements the DiskQuotaCache interface for DiskQuotaCacheLocal. 301 func (cache *DiskQuotaCacheLocal) Put( 302 ctx context.Context, id keybase1.UserOrTeamID, 303 info kbfsblock.QuotaInfo) (err error) { 304 cache.lock.Lock() 305 defer cache.lock.Unlock() 306 err = cache.checkCacheLocked(ctx, "Quota(Put)") 307 if err != nil { 308 return err 309 } 310 311 encodedInfo, err := cache.config.Codec().Encode(&info) 312 if err != nil { 313 return err 314 } 315 316 err = cache.db.PutWithMeter( 317 []byte(id.String()), encodedInfo, cache.putMeter) 318 if err != nil { 319 return err 320 } 321 322 cache.quotasCached[id] = true 323 return nil 324 } 325 326 // Status implements the DiskQuotaCache interface for DiskQuotaCacheLocal. 327 func (cache *DiskQuotaCacheLocal) Status( 328 ctx context.Context) DiskQuotaCacheStatus { 329 select { 330 case <-cache.startedCh: 331 case <-cache.startErrCh: 332 return DiskQuotaCacheStatus{StartState: DiskQuotaCacheStartStateFailed} 333 default: 334 return DiskQuotaCacheStatus{StartState: DiskQuotaCacheStartStateStarting} 335 } 336 337 cache.lock.RLock() 338 defer cache.lock.RUnlock() 339 340 var dbStats []string 341 if err := cache.checkCacheLocked(ctx, "Quota(Status)"); err == nil { 342 dbStats, err = cache.db.StatStrings() 343 if err != nil { 344 cache.log.CDebugf(ctx, "Couldn't get db stats: %+v", err) 345 } 346 } 347 348 return DiskQuotaCacheStatus{ 349 StartState: DiskQuotaCacheStartStateStarted, 350 NumQuotas: uint64(len(cache.quotasCached)), 351 Hits: ldbutils.RateMeterToStatus(cache.hitMeter), 352 Misses: ldbutils.RateMeterToStatus(cache.missMeter), 353 Puts: ldbutils.RateMeterToStatus(cache.putMeter), 354 DBStats: dbStats, 355 } 356 } 357 358 // Shutdown implements the DiskQuotaCache interface for DiskQuotaCacheLocal. 359 func (cache *DiskQuotaCacheLocal) Shutdown(ctx context.Context) { 360 // Wait for the cache to either finish starting or error. 361 select { 362 case <-cache.startedCh: 363 case <-cache.startErrCh: 364 return 365 } 366 cache.lock.Lock() 367 defer cache.lock.Unlock() 368 // shutdownCh has to be checked under lock, otherwise we can race. 369 select { 370 case <-cache.shutdownCh: 371 cache.log.CWarningf(ctx, "Shutdown called more than once") 372 return 373 default: 374 } 375 close(cache.shutdownCh) 376 if cache.db == nil { 377 return 378 } 379 cache.closer() 380 cache.db = nil 381 cache.hitMeter.Shutdown() 382 cache.missMeter.Shutdown() 383 cache.putMeter.Shutdown() 384 }