github.com/xmidt-org/webpa-common@v1.11.9/store/cache.go (about) 1 package store 2 3 import ( 4 "errors" 5 "sync" 6 "sync/atomic" 7 "time" 8 ) 9 10 var ( 11 NegativeCachePeriod = errors.New("The cache period must be positive") 12 ) 13 14 // cacheEntry is an internal type that holds a value and a timestamp. 15 type cacheEntry struct { 16 value interface{} 17 expiry time.Time 18 } 19 20 // Cache is an on-demand cache for an arbitrary value. This type implements 21 // the Value interface. 22 // 23 // Cache management is done inline with the Load() method. No other goroutine 24 // is necessary to monitor instances of this type. 25 type Cache struct { 26 updateLock sync.Mutex 27 cache atomic.Value 28 source Value 29 period CachePeriod 30 } 31 32 // NewCache constructs a new Cache that uses the given source. The period 33 // parameter must be positive, so special values such as CachePeriodForever are 34 // not supported by this function. 35 // 36 // To dynamically create a Value based on a CachePeriod, use NewValue() instead. 37 func NewCache(source Value, period CachePeriod) (*Cache, error) { 38 if period <= 0 { 39 return nil, NegativeCachePeriod 40 } 41 42 return &Cache{ 43 source: source, 44 period: period, 45 }, nil 46 } 47 48 // Refresh forces this cache to update itself. Unlike Load(), this method will 49 // return errors that occur when consulting the source for a new value. It will 50 // not update its cache with the old value and a new expiry when the source value 51 // returns an error. 52 func (c *Cache) Refresh() (interface{}, error) { 53 now := time.Now() 54 c.updateLock.Lock() 55 defer c.updateLock.Unlock() 56 57 if newValue, err := c.source.Load(); err != nil { 58 // Unlike Load(), we don't want to update if there's an error. 59 // Just let Load() keep returning the old value. 60 return nil, err 61 } else { 62 entry := &cacheEntry{ 63 value: newValue, 64 expiry: c.period.Next(now), 65 } 66 67 c.cache.Store(entry) 68 return entry.value, nil 69 } 70 } 71 72 // Load will return the currently cached value if there is one and it hasn't expired. 73 // If the cache has no value yet or if the existing value has expired, this method 74 // consults the source Value to update its cache and returns the new value. 75 // 76 // In the event that a cache has an expired value and cannot get a fresh one from the source, 77 // this method uses the old cache value and sets a new expiry time. This allows the application 78 // to continue using the old value in the event of intermediate I/O problems. No error is returned 79 // in that case. 80 func (c *Cache) Load() (interface{}, error) { 81 now := time.Now() 82 entry, ok := c.cache.Load().(*cacheEntry) 83 if !ok || entry.expiry.Before(now) { 84 c.updateLock.Lock() 85 defer c.updateLock.Unlock() 86 87 entry, ok = c.cache.Load().(*cacheEntry) 88 if !ok || entry.expiry.Before(now) { 89 if newValue, err := c.source.Load(); err != nil { 90 if !ok { 91 // we couldn't even get the initial value for the cache .... 92 return nil, err 93 } 94 95 // hang on to the old cached value in the event of an error 96 // this prevents this code from hammering external resources 97 // if there are I/O errors 98 // TODO: record the error somehow 99 entry = &cacheEntry{ 100 value: entry.value, 101 expiry: c.period.Next(now), 102 } 103 104 c.cache.Store(entry) 105 } else { 106 entry = &cacheEntry{ 107 value: newValue, 108 expiry: c.period.Next(now), 109 } 110 111 c.cache.Store(entry) 112 } 113 } 114 } 115 116 return entry.value, nil 117 }