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  }