github.com/grailbio/base@v0.0.11/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/grailbio/base/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 }