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 }