github.com/dominant-strategies/go-quai@v0.28.2/common/timedcache/timedcache.go (about)

     1  package timedcache
     2  
     3  import (
     4  	"sync"
     5  	"time"
     6  
     7  	lru "github.com/hashicorp/golang-lru"
     8  )
     9  
    10  // timedEntry provides a wrapper to store an entry in an LRU cache, with a
    11  // specified expiration time
    12  type timedEntry struct {
    13  	expiresAt int64
    14  	value     interface{}
    15  }
    16  
    17  // expired returns whether or not the given entry has expired
    18  func (te *timedEntry) expired() bool {
    19  	return te.expiresAt < time.Now().Unix()
    20  }
    21  
    22  // TimedCache defines a new cache, where entries are removed after exceeding
    23  // their ttl. The entry is not guaranteed to live this long (i.e. if it gets
    24  // evicted when the cache fills up). Conversely, the entry also isn't guaranteed
    25  // to expire at exactly the ttl time. The expiration mechanism is 'lazy', and
    26  // will only remove expired objects at next access.
    27  type TimedCache struct {
    28  	ttl   int64        // Time to live in seconds
    29  	cache *lru.Cache   // Underlying size-limited LRU cache
    30  	lock  sync.RWMutex
    31  
    32  	evictedKeys, evictedVals []interface{}
    33  	onEvictedCB              func(k, v interface{})
    34  }
    35  
    36  // New creates a new cache with a given size and ttl. TTL defines the time in
    37  // seconds an entry shall live, before being expired.
    38  func New(size int, ttl int) (*TimedCache, error) {
    39  	return NewWithEvict(size, ttl, nil)
    40  }
    41  
    42  // NewWithEvict constructs a fixed size cache with the given ttl & eviction
    43  // callback.
    44  func NewWithEvict(size int, ttl int, onEvicted func(key, value interface{})) (*TimedCache, error) {
    45  	tc := &TimedCache{
    46  		ttl:         int64(ttl),
    47  		onEvictedCB: onEvicted,
    48  	}
    49  	if onEvicted != nil {
    50  		tc.initEvictBuffers()
    51  		onEvicted = tc.onEvictedCB
    52  	}
    53  	cache, err := lru.NewWithEvict(size, onEvicted)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  	tc.cache = cache
    58  	return tc, nil
    59  }
    60  
    61  func (tc *TimedCache) initEvictBuffers() {
    62  	tc.evictedKeys = make([]interface{}, 0, lru.DefaultEvictedBufferSize)
    63  	tc.evictedVals = make([]interface{}, 0, lru.DefaultEvictedBufferSize)
    64  }
    65  
    66  // onEvicted save evicted key/val and sent in externally registered callback
    67  // outside of critical section
    68  func (tc *TimedCache) onEvicted(k, v interface{}) {
    69  	tc.evictedKeys = append(tc.evictedKeys, k)
    70  	tc.evictedVals = append(tc.evictedVals, v)
    71  }
    72  
    73  // calcExpireTime calculates the expiration time given a TTL relative to now.
    74  func calcExpireTime(ttl int64) int64 {
    75  	t := time.Now().Unix() + ttl
    76  	return t
    77  }
    78  
    79  // removeExpired removes any expired entries from the cache
    80  func (tc *TimedCache) removeExpired() {
    81  	for k := range tc.cache.Keys() {
    82  		if val, ok := tc.cache.Peek(k); ok {
    83  			if v := val.(timedEntry); v.expired() {
    84  				tc.cache.Remove(k)
    85  			}
    86  		}
    87  	}
    88  }
    89  
    90  // Purge is used to completely clear the cache.
    91  func (tc *TimedCache) Purge() {
    92  	var ks, vs []interface{}
    93  	tc.lock.Lock()
    94  	tc.cache.Purge()
    95  	if tc.onEvictedCB != nil && len(tc.evictedKeys) > 0 {
    96  		ks, vs = tc.evictedKeys, tc.evictedVals
    97  		tc.initEvictBuffers()
    98  	}
    99  	tc.lock.Unlock()
   100  	// invoke callback outside of critical section
   101  	if tc.onEvictedCB != nil {
   102  		for i := 0; i < len(ks); i++ {
   103  			tc.onEvictedCB(ks[i], vs[i])
   104  		}
   105  	}
   106  }
   107  
   108  // Add adds a value to the cache. Returns true if an eviction occurred.
   109  func (tc *TimedCache) Add(key, value interface{}) (evicted bool) {
   110  	var k, v interface{}
   111  	tc.lock.Lock()
   112  	// First remove expired entries, so that LRU cache doesn't evict more than
   113  	// necessary, if there is not enough room to add this entry.
   114  	tc.removeExpired()
   115  	// Wrap the entry and add it to the cache
   116  	evicted = tc.cache.Add(key, timedEntry{expiresAt: calcExpireTime(tc.ttl), value: value})
   117  	tc.lock.Unlock()
   118  	// invoke callback outside of critical section
   119  	if tc.onEvictedCB != nil {
   120  		tc.onEvictedCB(k, v)
   121  	}
   122  	return
   123  }
   124  
   125  // Get looks up a key's value from the cache, removing it if it has expired.
   126  func (tc *TimedCache) Get(key interface{}) (value interface{}, ok bool) {
   127  	tc.lock.Lock()
   128  	defer tc.lock.Unlock()
   129  	val, ok := tc.cache.Get(key)
   130  	if ok {
   131  		v := val.(timedEntry)
   132  		if v.expired() {
   133  			tc.cache.Remove(key)
   134  			return nil, false
   135  		} else {
   136  			return v.value, true
   137  		}
   138  	} else {
   139  		return nil, false
   140  	}
   141  }
   142  
   143  // Contains checks if a key is in the cache, without updating the
   144  // recent-ness or deleting it for being stale.
   145  func (tc *TimedCache) Contains(key interface{}) bool {
   146  	_, ok := tc.Peek(key)
   147  	return ok
   148  }
   149  
   150  // Peek returns the key value (or undefined if not found) without updating
   151  // the "recently used"-ness or ttl of the key.
   152  func (tc *TimedCache) Peek(key interface{}) (value interface{}, ok bool) {
   153  	tc.lock.Lock()
   154  	defer tc.lock.Unlock()
   155  	val, ok := tc.cache.Peek(key)
   156  	if ok {
   157  		v := val.(timedEntry)
   158  		if v.expired() {
   159  			tc.cache.Remove(key)
   160  			return nil, false
   161  		} else {
   162  			return v.value, ok
   163  		}
   164  	} else {
   165  		return nil, false
   166  	}
   167  }
   168  
   169  // ContainsOrAdd checks if a key is in the cache without updating the
   170  // recent-ness, ttl, or deleting it for being stale, and if not, adds the value.
   171  // Returns whether found and whether an eviction occurred.
   172  func (tc *TimedCache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) {
   173  	var k, v interface{}
   174  	tc.lock.Lock()
   175  	// First remove expired entries, so that LRU cache doesn't evict more than
   176  	// necessary, if there is not enough room to add this entry.
   177  	tc.removeExpired()
   178  	// Wrap the entry and add it to the cache
   179  	ok, evicted = tc.cache.ContainsOrAdd(key, timedEntry{expiresAt: calcExpireTime(tc.ttl), value: value})
   180  	tc.lock.Unlock()
   181  	// invoke callback outside of critical section
   182  	if tc.onEvictedCB != nil {
   183  		tc.onEvictedCB(k, v)
   184  	}
   185  	return
   186  }
   187  
   188  // PeekOrAdd checks if a key is in the cache without updating the
   189  // recent-ness, ttl, or deleting it for being stale, and if not, adds the value.
   190  // Returns whether found and whether an eviction occurred.
   191  func (tc *TimedCache) PeekOrAdd(key, value interface{}) (previous interface{}, ok, evicted bool) {
   192  	var k, v interface{}
   193  	tc.lock.Lock()
   194  	// First remove expired entries, so that LRU cache doesn't evict more than
   195  	// necessary, if there is not enough room to add this entry.
   196  	tc.removeExpired()
   197  	// Wrap the entry and add it to the cache
   198  	previous, ok, evicted = tc.cache.PeekOrAdd(key, timedEntry{expiresAt: calcExpireTime(tc.ttl), value: value})
   199  	tc.lock.Unlock()
   200  	// invoke callback outside of critical section
   201  	if tc.onEvictedCB != nil {
   202  		tc.onEvictedCB(k, v)
   203  	}
   204  	return
   205  }
   206  
   207  // Remove removes the provided key from the cache.
   208  func (tc *TimedCache) Remove(key interface{}) (present bool) {
   209  	var k, v interface{}
   210  	tc.lock.Lock()
   211  	tc.removeExpired()
   212  	present = tc.cache.Remove(key)
   213  	tc.lock.Unlock()
   214  	// invoke callback outside of critical section
   215  	if tc.onEvictedCB != nil {
   216  		tc.onEvictedCB(k, v)
   217  	}
   218  	return
   219  }
   220  
   221  // Resize changes the cache size.
   222  func (tc *TimedCache) Resize(size int) (evicted int) {
   223  	var k, v interface{}
   224  	tc.lock.Lock()
   225  	tc.removeExpired()
   226  	evicted = tc.cache.Resize(size)
   227  	tc.lock.Unlock()
   228  	// invoke callback outside of critical section
   229  	if tc.onEvictedCB != nil {
   230  		tc.onEvictedCB(k, v)
   231  	}
   232  	return
   233  }
   234  
   235  // RemoveOldest removes the oldest item from the cache.
   236  func (tc *TimedCache) RemoveOldest() (key, value interface{}, ok bool) {
   237  	var k, v interface{}
   238  	tc.lock.Lock()
   239  	tc.removeExpired()
   240  	key, value, ok = tc.cache.RemoveOldest()
   241  	if ok {
   242  		value = value.(timedEntry).value
   243  	}
   244  	tc.lock.Unlock()
   245  	// invoke callback outside of critical section
   246  	if tc.onEvictedCB != nil {
   247  		tc.onEvictedCB(k, v)
   248  	}
   249  	return
   250  }
   251  
   252  // GetOldest returns the oldest entry
   253  func (tc *TimedCache) GetOldest() (key, value interface{}, ok bool) {
   254  	tc.lock.Lock()
   255  	defer tc.lock.Unlock()
   256  	tc.removeExpired()
   257  	key, value, ok = tc.cache.GetOldest()
   258  	if ok {
   259  		value = value.(timedEntry).value
   260  	}
   261  	return
   262  }
   263  
   264  // Keys returns a slice of the keys in the cache, from oldest to newest.
   265  func (tc *TimedCache) Keys() []interface{} {
   266  	tc.lock.Lock()
   267  	defer tc.lock.Unlock()
   268  	tc.removeExpired()
   269  	return tc.cache.Keys()
   270  }
   271  
   272  // Len returns the number of items in the cache.
   273  func (tc *TimedCache) Len() int {
   274  	tc.lock.Lock()
   275  	defer tc.lock.Unlock()
   276  	tc.removeExpired()
   277  	return tc.cache.Len()
   278  }
   279  
   280  // Ttl returns the number of seconds each item is allowed to live (except if
   281  // evicted to free up space)
   282  func (tc *TimedCache) Ttl() int64 {
   283  	tc.lock.RLock()
   284  	defer tc.lock.RUnlock()
   285  	return tc.ttl
   286  }