github.com/mailgun/holster/v4@v4.20.0/collections/expire_cache.go (about)

     1  /*
     2  Copyright 2017 Mailgun Technologies Inc
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  package collections
    17  
    18  import (
    19  	"sync"
    20  
    21  	"github.com/mailgun/holster/v4/clock"
    22  	"github.com/mailgun/holster/v4/errors"
    23  	"github.com/mailgun/holster/v4/syncutil"
    24  )
    25  
    26  type ExpireCacheStats struct {
    27  	Size int64
    28  	Miss int64
    29  	Hit  int64
    30  }
    31  
    32  // ExpireCache is a cache which expires entries only after 2 conditions are met
    33  // 1. The Specified TTL has expired
    34  // 2. The item has been processed with ExpireCache.Each()
    35  //
    36  // This is an unbounded cache which guaranties each item in the cache
    37  // has been processed before removal. This is different from a LRU
    38  // cache, as the cache might decide an item needs to be removed
    39  // (because we hit the cache limit) before the item has been processed.
    40  //
    41  // Every time an item is touched by `Get()` or `Add()` the duration is
    42  // updated which ensures items in frequent use stay in the cache
    43  //
    44  // Processing can modify the item in the cache without updating the
    45  // expiration time by using the `Update()` method
    46  //
    47  // The cache can also return statistics which can be used to graph track
    48  // the size of the cache
    49  //
    50  // NOTE: Because this is an unbounded cache, the user MUST process the cache
    51  // with `Each()` regularly! Else the cache items will never expire and the cache
    52  // will eventually eat all the memory on the system
    53  type ExpireCache struct {
    54  	cache map[interface{}]*expireRecord
    55  	mutex sync.Mutex
    56  	ttl   clock.Duration
    57  	stats ExpireCacheStats
    58  }
    59  
    60  type expireRecord struct {
    61  	Value    interface{}
    62  	ExpireAt clock.Time
    63  }
    64  
    65  // New creates a new ExpireCache.
    66  func NewExpireCache(ttl clock.Duration) *ExpireCache {
    67  	return &ExpireCache{
    68  		cache: make(map[interface{}]*expireRecord),
    69  		ttl:   ttl,
    70  	}
    71  }
    72  
    73  // Retrieves a key's value from the cache
    74  func (c *ExpireCache) Get(key interface{}) (interface{}, bool) {
    75  	c.mutex.Lock()
    76  	defer c.mutex.Unlock()
    77  
    78  	record, ok := c.cache[key]
    79  	if !ok {
    80  		c.stats.Miss++
    81  		return nil, ok
    82  	}
    83  
    84  	// Since this was recently accessed, keep it in
    85  	// the cache by resetting the expire time
    86  	record.ExpireAt = clock.Now().UTC().Add(c.ttl)
    87  
    88  	c.stats.Hit++
    89  	return record.Value, ok
    90  }
    91  
    92  // Put the key, value and TTL in the cache
    93  func (c *ExpireCache) Add(key, value interface{}) {
    94  	c.mutex.Lock()
    95  	defer c.mutex.Unlock()
    96  
    97  	record := expireRecord{
    98  		Value:    value,
    99  		ExpireAt: clock.Now().UTC().Add(c.ttl),
   100  	}
   101  	// Add the record to the cache
   102  	c.cache[key] = &record
   103  }
   104  
   105  // Update the value in the cache without updating the TTL
   106  func (c *ExpireCache) Update(key, value interface{}) error {
   107  	c.mutex.Lock()
   108  	defer c.mutex.Unlock()
   109  
   110  	record, ok := c.cache[key]
   111  	if !ok {
   112  		return errors.Errorf("ExpoireCache() - No record found for '%+v'", key)
   113  	}
   114  	record.Value = value
   115  	return nil
   116  }
   117  
   118  // Get a list of keys at this point in time
   119  func (c *ExpireCache) Keys() (keys []interface{}) {
   120  	defer c.mutex.Unlock()
   121  	c.mutex.Lock()
   122  
   123  	for key := range c.cache {
   124  		keys = append(keys, key)
   125  	}
   126  	return
   127  }
   128  
   129  // Get the value without updating the expiration
   130  func (c *ExpireCache) Peek(key interface{}) (value interface{}, ok bool) {
   131  	defer c.mutex.Unlock()
   132  	c.mutex.Lock()
   133  
   134  	if record, hit := c.cache[key]; hit {
   135  		return record.Value, true
   136  	}
   137  	return nil, false
   138  }
   139  
   140  // Processes each item in the cache in a thread safe way, such that the cache can be in use
   141  // while processing items in the cache
   142  func (c *ExpireCache) Each(concurrent int, callBack func(key interface{}, value interface{}) error) []error {
   143  	fanOut := syncutil.NewFanOut(concurrent)
   144  	keys := c.Keys()
   145  
   146  	for _, key := range keys {
   147  		fanOut.Run(func(key interface{}) error {
   148  			c.mutex.Lock()
   149  			record, ok := c.cache[key]
   150  			c.mutex.Unlock()
   151  			if !ok {
   152  				return errors.Errorf("Each() - key '%+v' disapeared "+
   153  					"from cache during iteration", key)
   154  			}
   155  
   156  			err := callBack(key, record.Value)
   157  			if err != nil {
   158  				return err
   159  			}
   160  
   161  			c.mutex.Lock()
   162  			if record.ExpireAt.Before(clock.Now().UTC()) {
   163  				delete(c.cache, key)
   164  			}
   165  			c.mutex.Unlock()
   166  			return nil
   167  		}, key)
   168  	}
   169  
   170  	// Wait for all the routines to complete
   171  	errs := fanOut.Wait()
   172  	if errs != nil {
   173  		return errs
   174  	}
   175  
   176  	return nil
   177  }
   178  
   179  // Retrieve stats about the cache
   180  func (c *ExpireCache) GetStats() ExpireCacheStats {
   181  	c.mutex.Lock()
   182  	c.stats.Size = int64(len(c.cache))
   183  	defer func() {
   184  		c.stats = ExpireCacheStats{}
   185  		c.mutex.Unlock()
   186  	}()
   187  	return c.stats
   188  }
   189  
   190  // Returns the number of items in the cache.
   191  func (c *ExpireCache) Size() int64 {
   192  	defer c.mutex.Unlock()
   193  	c.mutex.Lock()
   194  	return int64(len(c.cache))
   195  }