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  }