github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/sync/loadingcache/value.go (about) 1 package loadingcache 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "runtime/debug" 8 "sync" 9 "time" 10 11 "github.com/Schaudge/grailbase/must" 12 ) 13 14 type ( 15 // Value manages the loading (calculation) and storing of a cache value. It's designed for use 16 // cases where loading is slow. Concurrency is well-supported: 17 // 1. Only one load is in progress at a time, even if concurrent callers request the value. 18 // 2. Cancellation is respected for loading: a caller's load function is invoked with their 19 // context. If it respects cancellation and returns an error immediately, the caller's 20 // GetOrLoad does, too. 21 // 3. Cancellation is respected for waiting: if a caller's context is canceled while they're 22 // waiting for another in-progress load (not their own), the caller's GetOrLoad returns 23 // immediately with the cancellation error. 24 // Simpler mechanisms (like just locking a sync.Mutex when starting computation) don't achieve 25 // all of these (in the mutex example, cancellation is not respected while waiting on Lock()). 26 // 27 // The original use case was reading rarely-changing data via RPC while letting users 28 // cancel the operation (Ctrl-C in their terminal). Very different uses (very fast computations 29 // or extremely high concurrency) may not work as well; they're at least not tested. 30 // Memory overhead may be quite large if small values are cached. 31 // 32 // Value{} is ready to use. (*Value)(nil) is valid and just never caches or shares a result 33 // (every get loads). Value must not be copied. 34 // 35 // Time-based expiration is optional. See LoadFunc and LoadOpts. 36 Value struct { 37 // init supports at-most-once initialization of subsequent fields. 38 init sync.Once 39 // c is both a semaphore (limit 1) and storage for cache state. 40 c chan state 41 // now is used for faking time in tests. 42 now func() time.Time 43 } 44 state struct { 45 // dataPtr is non-zero if there's a previously-computed value (which may be expired). 46 dataPtr reflect.Value 47 // expiresAt is the time of expiration (according to now) when dataPtr is non-zero. 48 // expiresAt.IsZero() means no expiration (infinite caching). 49 expiresAt time.Time 50 } 51 // LoadFunc computes a value. It should respect cancellation (return with cancellation error). 52 LoadFunc func(context.Context, *LoadOpts) error 53 // LoadOpts configures how long a LoadFunc result should be cached. 54 // Cache settings overwrite each other; last write wins. Default is don't cache at all. 55 // Callers should synchronize their calls themselves if using multiple goroutines (this is 56 // not expected). 57 LoadOpts struct { 58 // validFor is cache time if > 0, disables cache if == 0, infinite cache time if < 0. 59 validFor time.Duration 60 } 61 ) 62 63 // GetOrLoad either copies a cached value to dataPtr or runs load and then copies dataPtr's value 64 // into the cache. A properly-written load writes dataPtr's value. Example: 65 // 66 // var result string 67 // err := value.GetOrLoad(ctx, &result, func(ctx context.Context, opts *loadingcache.LoadOpts) error { 68 // var err error 69 // result, err = doExpensiveThing(ctx) 70 // opts.CacheFor(time.Hour) 71 // return err 72 // }) 73 // 74 // dataPtr must be a pointer to a copyable value (slice, int, struct without Mutex, etc.). 75 // 76 // Value does not cache errors. Consider caching a value containing an error, like 77 // struct{result int; err error} if desired. 78 func (v *Value) GetOrLoad(ctx context.Context, dataPtr interface{}, load LoadFunc) error { 79 ptrVal := reflect.ValueOf(dataPtr) 80 must.True(ptrVal.Kind() == reflect.Ptr, "%v", dataPtr) 81 // TODO: Check copyable? 82 83 if v == nil { 84 return runNoPanic(func() error { 85 var opts LoadOpts 86 return load(ctx, &opts) 87 }) 88 } 89 90 v.init.Do(func() { 91 if v.c == nil { 92 v.c = make(chan state, 1) 93 v.c <- state{} 94 } 95 if v.now == nil { 96 v.now = time.Now 97 } 98 }) 99 100 var state state 101 select { 102 case <-ctx.Done(): 103 return ctx.Err() 104 case state = <-v.c: 105 } 106 defer func() { v.c <- state }() 107 108 if state.dataPtr.IsValid() { 109 if state.expiresAt.IsZero() || v.now().Before(state.expiresAt) { 110 ptrVal.Elem().Set(state.dataPtr.Elem()) 111 return nil 112 } 113 state.dataPtr = reflect.Value{} 114 } 115 116 var opts LoadOpts 117 // TODO: Consider calling load() directly rather than via runNoPanic(). 118 // A previous implementation needed to intercept panics to handle internal state correctly. 119 // That's no longer true, so we can avoid tampering with callers' panic traces. 120 err := runNoPanic(func() error { return load(ctx, &opts) }) 121 if err == nil && opts.validFor != 0 { 122 state.dataPtr = ptrVal 123 if opts.validFor > 0 { 124 state.expiresAt = v.now().Add(opts.validFor) 125 } else { 126 state.expiresAt = time.Time{} 127 } 128 } 129 return err 130 } 131 132 func runNoPanic(f func() error) (err error) { 133 defer func() { 134 if r := recover(); r != nil { 135 err = fmt.Errorf("cache: recovered panic: %v, stack:\n%v", r, string(debug.Stack())) 136 } 137 }() 138 return f() 139 } 140 141 // setClock is for testing. It must be called before any GetOrLoad and is not concurrency-safe. 142 func (v *Value) setClock(now func() time.Time) { 143 if v == nil { 144 return 145 } 146 v.now = now 147 } 148 149 func (o *LoadOpts) CacheFor(d time.Duration) { o.validFor = d } 150 func (o *LoadOpts) CacheForever() { o.validFor = -1 }