github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/fetch_decider.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  	"sync"
     9  	"time"
    10  
    11  	"github.com/keybase/client/go/libkb"
    12  	"github.com/keybase/client/go/logger"
    13  	"golang.org/x/net/context"
    14  )
    15  
    16  const (
    17  	fetchDeciderBackgroundTimeout = 10 * time.Second
    18  )
    19  
    20  // fetchDecider is a struct that helps avoid having too frequent calls
    21  // into a remote server.
    22  type fetchDecider struct {
    23  	clockGetter
    24  
    25  	log     logger.Logger
    26  	vlog    *libkb.VDebugLog
    27  	fetcher func(ctx context.Context) error
    28  	tagKey  interface{}
    29  	tagName string
    30  
    31  	blockingForTest chan<- struct{}
    32  
    33  	lock    sync.Mutex
    34  	readyCh chan struct{}
    35  	errPtr  *error
    36  }
    37  
    38  func newFetchDecider(
    39  	log logger.Logger, vlog *libkb.VDebugLog,
    40  	fetcher func(ctx context.Context) error, tagKey interface{}, tagName string,
    41  	clock clockGetter) *fetchDecider {
    42  	return &fetchDecider{
    43  		log:         log,
    44  		vlog:        vlog,
    45  		fetcher:     fetcher,
    46  		tagKey:      tagKey,
    47  		tagName:     tagName,
    48  		clockGetter: clock,
    49  	}
    50  }
    51  
    52  func (fd *fetchDecider) launchBackgroundFetch(ctx context.Context) (
    53  	readyCh <-chan struct{}, errPtr *error) {
    54  	fd.lock.Lock()
    55  	defer fd.lock.Unlock()
    56  
    57  	if fd.readyCh != nil {
    58  		fd.vlog.CLogf(ctx, libkb.VLog1, "Waiting on existing fetch")
    59  		// There's already a fetch in progress.
    60  		return fd.readyCh, fd.errPtr
    61  	}
    62  
    63  	fd.readyCh = make(chan struct{})
    64  	fd.errPtr = new(error)
    65  
    66  	id, err := MakeRandomRequestID()
    67  	if err != nil {
    68  		fd.log.Warning("Couldn't generate a random request ID: %v", err)
    69  	}
    70  	fd.vlog.CLogf(
    71  		ctx, libkb.VLog1, "Spawning fetch in background with tag:%s=%v",
    72  		fd.tagName, id)
    73  	go func() {
    74  		// Make a new context so that it doesn't get canceled
    75  		// when returned.
    76  		logTags := make(logger.CtxLogTags)
    77  		logTags[fd.tagKey] = fd.tagName
    78  		bgCtx := logger.NewContextWithLogTags(
    79  			context.Background(), logTags)
    80  		bgCtx = context.WithValue(bgCtx, fd.tagKey, id)
    81  		// Make sure a timeout is on the context, in case the
    82  		// RPC blocks forever somehow, where we'd end up with
    83  		// never resetting backgroundInProcess flag again.
    84  		bgCtx, cancel := context.WithTimeout(
    85  			bgCtx, fetchDeciderBackgroundTimeout)
    86  		defer cancel()
    87  		err := fd.fetcher(bgCtx)
    88  
    89  		// Notify everyone we're done fetching.
    90  		fd.lock.Lock()
    91  		defer fd.lock.Unlock()
    92  		fd.vlog.CLogf(bgCtx, libkb.VLog1, "Finished fetch: %+v", err)
    93  		*fd.errPtr = err
    94  		close(fd.readyCh)
    95  		fd.readyCh = nil
    96  		fd.errPtr = nil
    97  	}()
    98  	return fd.readyCh, fd.errPtr
    99  }
   100  
   101  // Do decides whether to block on a fetch, launch a background fetch
   102  // and use existing cached value, or simply use the existing cached
   103  // value with no more fetching. The caller can provide a positive
   104  // tolerance, to accept stale LimitBytes and UsageBytes data. If
   105  // tolerance is 0 or negative, this always makes a blocking call using
   106  // `fd.fetcher`.
   107  //
   108  // 1) If the age of cached data is more than blockTolerance, it blocks
   109  // until a new value is fetched and ready in the caller's cache.
   110  // 2) Otherwise, if the age of cached data is more than bgTolerance,
   111  // a background RPC is spawned to refresh cached data using `fd.fetcher`,
   112  // but returns immediately to let the caller use stale data.
   113  // 3) Otherwise, it returns immediately
   114  func (fd *fetchDecider) Do(
   115  	ctx context.Context, bgTolerance, blockTolerance time.Duration,
   116  	cachedTimestamp time.Time) (err error) {
   117  	past := fd.Clock().Now().Sub(cachedTimestamp)
   118  	switch {
   119  	case past > blockTolerance || cachedTimestamp.IsZero():
   120  		fd.vlog.CLogf(
   121  			ctx, libkb.VLog1, "Blocking on fetch; cached data is %s old", past)
   122  		readyCh, errPtr := fd.launchBackgroundFetch(ctx)
   123  
   124  		if fd.blockingForTest != nil {
   125  			fd.blockingForTest <- struct{}{}
   126  		}
   127  
   128  		select {
   129  		case <-readyCh:
   130  			return *errPtr
   131  		case <-ctx.Done():
   132  			return ctx.Err()
   133  		}
   134  	case past > bgTolerance:
   135  		fd.vlog.CLogf(ctx, libkb.VLog1, "Cached data is %s old", past)
   136  		_, _ = fd.launchBackgroundFetch(ctx)
   137  		// Return immediately, with no error, since the caller can
   138  		// just use the existing cache value.
   139  		return nil
   140  	default:
   141  		fd.vlog.CLogf(ctx, libkb.VLog1, "Using cached data from %s ago", past)
   142  		return nil
   143  	}
   144  }