github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/stellar/time_cache.go (about)

     1  package stellar
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"time"
     7  
     8  	lru "github.com/hashicorp/golang-lru"
     9  	"github.com/keybase/client/go/libkb"
    10  )
    11  
    12  // Threadsafe cache with expiration and singleflighting.
    13  type TimeCache struct {
    14  	name    string // name for logging
    15  	maxAge  time.Duration
    16  	lockTab *libkb.LockTable
    17  	cache   *lru.Cache
    18  }
    19  
    20  type timeCacheEntry struct {
    21  	val  interface{}
    22  	time time.Time
    23  }
    24  
    25  func NewTimeCache(name string, size int, maxAge time.Duration) *TimeCache {
    26  	if size <= 0 {
    27  		size = 10
    28  	}
    29  	cache, err := lru.New(size)
    30  	if err != nil {
    31  		panic(err)
    32  	}
    33  	return &TimeCache{
    34  		name:    name,
    35  		maxAge:  maxAge,
    36  		cache:   cache,
    37  		lockTab: libkb.NewLockTable(),
    38  	}
    39  }
    40  
    41  type cacheFillFunc = func() (interface{}, error)
    42  
    43  func (c *TimeCache) Get(mctx libkb.MetaContext, key string, into interface{}) (ok bool) {
    44  	return c.getHelper(mctx, key, into, nil) == nil
    45  }
    46  
    47  // GetWithFill is prefereable to Get because it holds a locktab lock during the fill.
    48  // Which prevents concurrent accesses from doing the extra work of running fill at the same time.
    49  func (c *TimeCache) GetWithFill(mctx libkb.MetaContext, key string, into interface{}, fill cacheFillFunc) error {
    50  	return c.getHelper(mctx, key, into, fill)
    51  }
    52  
    53  func (c *TimeCache) getHelper(mctx libkb.MetaContext, key string, into interface{}, fill cacheFillFunc) error {
    54  	lock, err := c.lockTab.AcquireOnNameWithContextAndTimeout(mctx.Ctx(), mctx.G(), key, time.Minute)
    55  	if err != nil {
    56  		return fmt.Errorf("could not acquire cache lock for key %v", key)
    57  	}
    58  	defer lock.Release(mctx.Ctx())
    59  	if val, ok := c.cache.Get(key); ok {
    60  		if entry, ok := val.(timeCacheEntry); ok {
    61  			if c.maxAge <= 0 || mctx.G().GetClock().Now().Sub(entry.time) <= c.maxAge {
    62  				// Cache hit
    63  				mctx.Debug("TimeCache %v cache hit", c.name)
    64  				if c.storeResult(entry.val, into) {
    65  					return nil
    66  				}
    67  				mctx.Debug("TimeCache %v target does not match", c.name)
    68  			}
    69  		} else {
    70  			// Would indicate a bug in TimeCache.
    71  			mctx.Debug("TimeCache %v bad cached type: %T", c.name, val)
    72  		}
    73  		// Remove the expired or corrupt entry.
    74  		c.cache.Remove(key)
    75  	}
    76  	if fill != nil {
    77  		val, err := fill()
    78  		if err != nil {
    79  			return err
    80  		}
    81  		c.Put(mctx, key, val)
    82  		if c.storeResult(val, into) {
    83  			return nil
    84  		}
    85  		return fmt.Errorf("value cannot be stored")
    86  	}
    87  	return fmt.Errorf("value not found for '%v'", key)
    88  }
    89  
    90  func (c *TimeCache) storeResult(val interface{}, into interface{}) (ok bool) {
    91  	target := reflect.Indirect(reflect.ValueOf(into))
    92  	if target.CanSet() && reflect.TypeOf(val).AssignableTo(reflect.TypeOf(target.Interface())) {
    93  		target.Set(reflect.ValueOf(val))
    94  		return true
    95  	}
    96  	// Would indicate the caller used the wrong types.
    97  	return false
    98  }
    99  
   100  func (c *TimeCache) Put(mctx libkb.MetaContext, key string, val interface{}) {
   101  	c.cache.Add(key, timeCacheEntry{
   102  		val:  val,
   103  		time: time.Now().Round(0),
   104  	})
   105  }
   106  
   107  func (c *TimeCache) Clear() {
   108  	c.cache.Purge()
   109  }