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 }