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 }