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

     1  // Copyright 2017 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  	"fmt"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/keybase/client/go/kbfs/kbfsblock"
    13  	"github.com/keybase/client/go/libkb"
    14  	"github.com/keybase/client/go/logger"
    15  	"github.com/keybase/client/go/protocol/keybase1"
    16  	"github.com/pkg/errors"
    17  	"golang.org/x/net/context"
    18  )
    19  
    20  const (
    21  	// quotaServerTimeoutWhenCached defines how long we wait for the
    22  	// quota usage to return from the server, when we've already read
    23  	// it from the disk cache.
    24  	quotaServerTimeoutWhenCached = 500 * time.Millisecond
    25  )
    26  
    27  // ECQUCtxTagKey is the type for unique ECQU background operation IDs.
    28  type ECQUCtxTagKey struct{}
    29  
    30  // ECQUID is used in EventuallyConsistentQuotaUsage for only background RPCs.
    31  // More specifically, when we need to spawn a background goroutine for
    32  // GetUserQuotaInfo, a new context with this tag is created and used. This is
    33  // also used as a prefix for the logger module name in
    34  // EventuallyConsistentQuotaUsage.
    35  const ECQUID = "ECQU"
    36  
    37  type cachedQuotaUsage struct {
    38  	timestamp       time.Time
    39  	usageBytes      int64
    40  	archiveBytes    int64
    41  	limitBytes      int64
    42  	gitUsageBytes   int64
    43  	gitArchiveBytes int64
    44  	gitLimitBytes   int64
    45  }
    46  
    47  // EventuallyConsistentQuotaUsage keeps tracks of quota usage, in a way user of
    48  // which can choose to accept stale data to reduce calls into block servers.
    49  type EventuallyConsistentQuotaUsage struct {
    50  	config  Config
    51  	log     logger.Logger
    52  	tid     keybase1.TeamID
    53  	fetcher *fetchDecider
    54  
    55  	mu      sync.RWMutex
    56  	cached  cachedQuotaUsage
    57  	bgFetch bool
    58  }
    59  
    60  // QuotaUsageLogModule makes a log module for a quota usage log.
    61  func QuotaUsageLogModule(suffix string) string {
    62  	return fmt.Sprintf("%s - %s", ECQUID, suffix)
    63  }
    64  
    65  // NewEventuallyConsistentQuotaUsage creates a new
    66  // EventuallyConsistentQuotaUsage object.
    67  func NewEventuallyConsistentQuotaUsage(
    68  	config Config, log logger.Logger,
    69  	vlog *libkb.VDebugLog) *EventuallyConsistentQuotaUsage {
    70  	q := &EventuallyConsistentQuotaUsage{
    71  		config: config,
    72  		log:    log,
    73  	}
    74  	q.fetcher = newFetchDecider(
    75  		q.log, vlog, q.getAndCache, ECQUCtxTagKey{}, ECQUID, q.config)
    76  	return q
    77  }
    78  
    79  // NewEventuallyConsistentTeamQuotaUsage creates a new
    80  // EventuallyConsistentQuotaUsage object.
    81  func NewEventuallyConsistentTeamQuotaUsage(
    82  	config Config, tid keybase1.TeamID,
    83  	log logger.Logger, vlog *libkb.VDebugLog) *EventuallyConsistentQuotaUsage {
    84  	q := NewEventuallyConsistentQuotaUsage(config, log, vlog)
    85  	q.tid = tid
    86  	return q
    87  }
    88  
    89  func (q *EventuallyConsistentQuotaUsage) getCached() cachedQuotaUsage {
    90  	q.mu.RLock()
    91  	defer q.mu.RUnlock()
    92  	return q.cached
    93  }
    94  
    95  func (q *EventuallyConsistentQuotaUsage) getID(
    96  	ctx context.Context) (keybase1.UserOrTeamID, error) {
    97  	if q.tid.IsNil() {
    98  		session, err := q.config.KBPKI().GetCurrentSession(ctx)
    99  		if err != nil {
   100  			return keybase1.UserOrTeamID(""), err
   101  		}
   102  		return session.UID.AsUserOrTeam(), nil
   103  	}
   104  	return q.tid.AsUserOrTeam(), nil
   105  }
   106  
   107  func (q *EventuallyConsistentQuotaUsage) cache(
   108  	ctx context.Context, quotaInfo *kbfsblock.QuotaInfo, doCacheToDisk bool) {
   109  	id, err := q.getID(ctx)
   110  	if err != nil {
   111  		q.log.CDebugf(ctx, "Can't get ID: %+v", err)
   112  		return
   113  	}
   114  
   115  	q.mu.Lock()
   116  	defer q.mu.Unlock()
   117  	q.cached.limitBytes = quotaInfo.Limit
   118  	q.cached.gitLimitBytes = quotaInfo.GitLimit
   119  	if quotaInfo.Total != nil {
   120  		q.cached.usageBytes = quotaInfo.Total.Bytes[kbfsblock.UsageWrite]
   121  		q.cached.archiveBytes = quotaInfo.Total.Bytes[kbfsblock.UsageArchive]
   122  		q.cached.gitUsageBytes = quotaInfo.Total.Bytes[kbfsblock.UsageGitWrite]
   123  		q.cached.gitArchiveBytes =
   124  			quotaInfo.Total.Bytes[kbfsblock.UsageGitArchive]
   125  	} else {
   126  		q.cached.usageBytes = 0
   127  	}
   128  	q.cached.timestamp = q.config.Clock().Now()
   129  
   130  	dqc := q.config.DiskQuotaCache()
   131  	if !doCacheToDisk || dqc == nil {
   132  		return
   133  	}
   134  
   135  	err = dqc.Put(ctx, id, *quotaInfo)
   136  	if err != nil {
   137  		q.log.CDebugf(ctx, "Can't cache quota for %s: %+v", id, err)
   138  	}
   139  }
   140  
   141  func (q *EventuallyConsistentQuotaUsage) fetch(ctx context.Context) (
   142  	quotaInfo *kbfsblock.QuotaInfo, err error) {
   143  	bserver := q.config.BlockServer()
   144  	for i := 0; bserver == nil; i++ {
   145  		// This is possible if a login event comes in during
   146  		// initialization.
   147  		if i == 0 {
   148  			q.log.CDebugf(ctx, "Waiting for bserver")
   149  		}
   150  		time.Sleep(100 * time.Millisecond)
   151  		bserver = q.config.BlockServer()
   152  	}
   153  	if q.tid.IsNil() {
   154  		return bserver.GetUserQuotaInfo(ctx)
   155  	}
   156  	return bserver.GetTeamQuotaInfo(ctx, q.tid)
   157  }
   158  
   159  func (q *EventuallyConsistentQuotaUsage) doBackgroundFetch() {
   160  	doFetch := func() bool {
   161  		q.mu.Lock()
   162  		defer q.mu.Unlock()
   163  		if q.bgFetch {
   164  			return false
   165  		}
   166  		q.bgFetch = true
   167  		return true
   168  	}()
   169  	if !doFetch {
   170  		return
   171  	}
   172  
   173  	defer func() {
   174  		q.mu.Lock()
   175  		defer q.mu.Unlock()
   176  		q.bgFetch = false
   177  	}()
   178  
   179  	ctx := CtxWithRandomIDReplayable(
   180  		context.Background(), ECQUCtxTagKey{}, ECQUID, q.log)
   181  	q.log.CDebugf(ctx, "Running background quota fetch, without a timeout")
   182  
   183  	quotaInfo, err := q.fetch(ctx)
   184  	if err != nil {
   185  		q.log.CDebugf(ctx, "Unable to fetch quota in background: %+v", err)
   186  		return
   187  	}
   188  	q.cache(ctx, quotaInfo, true)
   189  }
   190  
   191  func (q *EventuallyConsistentQuotaUsage) getAndCache(
   192  	ctx context.Context) (err error) {
   193  	defer func() {
   194  		q.log.CDebugf(ctx, "getAndCache: error=%v", err)
   195  	}()
   196  
   197  	// Try pulling the quota from the disk cache.  If it exists, still
   198  	// try the servers, but give it a short timeout.
   199  	var quotaInfoFromCache *kbfsblock.QuotaInfo
   200  	id, err := q.getID(ctx)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	getCtx := ctx
   205  	dqc := q.config.DiskQuotaCache()
   206  	if dqc != nil {
   207  		qi, err := dqc.Get(ctx, id)
   208  		if err == nil {
   209  			q.log.CDebugf(ctx, "Read quota for %s from disk cache", id)
   210  			quotaInfoFromCache = &qi
   211  			var cancel context.CancelFunc
   212  			getCtx, cancel = context.WithTimeout(
   213  				ctx, quotaServerTimeoutWhenCached)
   214  			defer cancel()
   215  		}
   216  	}
   217  
   218  	quotaInfo, err := q.fetch(getCtx)
   219  	doCacheToDisk := dqc != nil
   220  	switch err {
   221  	case nil:
   222  	case context.DeadlineExceeded:
   223  		go q.doBackgroundFetch()
   224  		if quotaInfoFromCache != nil {
   225  			q.log.CDebugf(ctx, "Can't contact server; using cached quota")
   226  			quotaInfo = quotaInfoFromCache
   227  			doCacheToDisk = false
   228  		} else {
   229  			return err
   230  		}
   231  	default:
   232  		return err
   233  	}
   234  
   235  	q.cache(ctx, quotaInfo, doCacheToDisk)
   236  	return nil
   237  }
   238  
   239  // Get returns KBFS bytes used and limit for user, for the current
   240  // default block type. To help avoid having too frequent calls into
   241  // bserver, caller can provide a positive tolerance, to accept stale
   242  // LimitBytes and UsageBytes data. If tolerance is 0 or negative, this
   243  // always makes a blocking RPC to bserver and return latest quota
   244  // usage.
   245  //
   246  // 1) If the age of cached data is more than blockTolerance, a blocking RPC is
   247  // issued and the function only returns after RPC finishes, with the newest
   248  // data from RPC. The RPC causes cached data to be refreshed as well.
   249  // 2) Otherwise, if the age of cached data is more than bgTolerance,
   250  // a background RPC is spawned to refresh cached data, and the stale
   251  // data is returned immediately.
   252  // 3) Otherwise, the cached stale data is returned immediately.
   253  func (q *EventuallyConsistentQuotaUsage) Get(
   254  	ctx context.Context, bgTolerance, blockTolerance time.Duration) (
   255  	timestamp time.Time, usageBytes, archiveBytes, limitBytes int64,
   256  	err error) {
   257  	c := q.getCached()
   258  	err = q.fetcher.Do(ctx, bgTolerance, blockTolerance, c.timestamp)
   259  	if err != nil {
   260  		return time.Time{}, -1, -1, -1, err
   261  	}
   262  
   263  	c = q.getCached()
   264  	switch q.config.DefaultBlockType() {
   265  	case keybase1.BlockType_DATA:
   266  		return c.timestamp, c.usageBytes, c.archiveBytes, c.limitBytes, nil
   267  	case keybase1.BlockType_GIT:
   268  		return c.timestamp, c.gitUsageBytes, c.gitArchiveBytes,
   269  			c.gitLimitBytes, nil
   270  	default:
   271  		return time.Time{}, -1, -1, -1, errors.Errorf(
   272  			"Unknown default block type: %d", q.config.DefaultBlockType())
   273  	}
   274  }
   275  
   276  // GetAllTypes is the same as Get, except it returns usage and limits
   277  // for all block types.
   278  func (q *EventuallyConsistentQuotaUsage) GetAllTypes(
   279  	ctx context.Context, bgTolerance, blockTolerance time.Duration) (
   280  	timestamp time.Time,
   281  	usageBytes, archiveBytes, limitBytes,
   282  	gitUsageBytes, gitArchiveBytes, gitLimitBytes int64, err error) {
   283  	c := q.getCached()
   284  	err = q.fetcher.Do(ctx, bgTolerance, blockTolerance, c.timestamp)
   285  	if err != nil {
   286  		return time.Time{}, -1, -1, -1, -1, -1, -1, err
   287  	}
   288  
   289  	c = q.getCached()
   290  	return c.timestamp,
   291  		c.usageBytes, c.archiveBytes, c.limitBytes,
   292  		c.gitUsageBytes, c.gitArchiveBytes, c.gitLimitBytes, nil
   293  }