Codebase list golang-github-renekroon-ttlcache / 11ad8c0
New upstream version 2.9.0+ds Sascha Steinbiss 2 years ago
3 changed file(s) with 103 addition(s) and 44 deletion(s). Raw diff Collapse all Expand all
0 # 2.9.0 (October 2021)
1
2 #55,#56,#57 : @chenyahui was on fire and greatly improved the peformance of the library. He also got rid of the blocking call to expirationNotification, making the code run twice as fast in the benchmarks!
3
4 # 2.8.1 (September 2021)
5
6 #53 : Avoids recalculation of TTL value returned in API when TTL is extended. by @iczc
7
8 # 2.8.0 (August 2021)
9
10 #51 : The call GetWithTTL(key string) (interface{}, time.Duration, error) is added so that you can retrieve an item, and also know the remaining TTL. Thanks to @asgarciap for contributing.
11
012 # 2.7.0 (June 2021)
113
214 #46 : got panic
1010 type CheckExpireCallback func(key string, value interface{}) bool
1111
1212 // ExpireCallback is used as a callback on item expiration or when notifying of an item new to the cache
13 // Note that ExpireReasonCallback will be the succesor of this function in the next major release.
13 // Note that ExpireReasonCallback will be the successor of this function in the next major release.
1414 type ExpireCallback func(key string, value interface{})
1515
1616 // ExpireReasonCallback is used as a callback on item expiration with extra information why the item expired.
3333
3434 // Cache is a synchronized map of items that can auto-expire once stale
3535 type Cache struct {
36 mutex sync.Mutex
37 ttl time.Duration
38 items map[string]*item
39 loaderLock *singleflight.Group
40 expireCallback ExpireCallback
41 expireReasonCallback ExpireReasonCallback
42 checkExpireCallback CheckExpireCallback
43 newItemCallback ExpireCallback
36 // mutex is shared for all operations that need to be safe
37 mutex sync.Mutex
38 // ttl is the global ttl for the cache, can be zero (is infinite)
39 ttl time.Duration
40 // actual item storage
41 items map[string]*item
42 // lock used to avoid fetching a remote item multiple times
43 loaderLock *singleflight.Group
44 expireCallback ExpireCallback
45 expireReasonCallback ExpireReasonCallback
46 checkExpireCallback CheckExpireCallback
47 newItemCallback ExpireCallback
48 // the queue is used to have an ordered structure to use for expiration and cleanup.
4449 priorityQueue *priorityQueue
4550 expirationNotification chan bool
46 expirationTime time.Time
47 skipTTLExtension bool
48 shutdownSignal chan (chan struct{})
49 isShutDown bool
50 loaderFunction LoaderFunction
51 sizeLimit int
52 metrics Metrics
51 // hasNotified is used to not schedule new expiration processing when an request is already pending.
52 hasNotified bool
53 expirationTime time.Time
54 skipTTLExtension bool
55 shutdownSignal chan (chan struct{})
56 isShutDown bool
57 loaderFunction LoaderFunction
58 sizeLimit int
59 metrics Metrics
5360 }
5461
5562 // EvictionReason is an enum that explains why an item was evicted
8592 return nil, false, false
8693 }
8794
88 if item.ttl >= 0 && (item.ttl > 0 || cache.ttl > 0) {
89 if cache.ttl > 0 && item.ttl == 0 {
90 item.ttl = cache.ttl
91 }
92
93 if !cache.skipTTLExtension {
94 item.touch()
95 }
96 cache.priorityQueue.update(item)
97 }
95 // no need to change priority queue when skipTTLExtension is true or the item will not expire
96 if cache.skipTTLExtension || (item.ttl == 0 && cache.ttl == 0) {
97 return item, true, false
98 }
99
100 if item.ttl == 0 {
101 item.ttl = cache.ttl
102 }
103
104 item.touch()
105
106 oldExpireTime := cache.priorityQueue.root().expireAt
107 cache.priorityQueue.update(item)
108 nowExpireTime := cache.priorityQueue.root().expireAt
98109
99110 expirationNotification := false
100 if cache.expirationTime.After(time.Now().Add(item.ttl)) {
111
112 // notify expiration only if the latest expire time is changed
113 if (oldExpireTime.IsZero() && !nowExpireTime.IsZero()) || oldExpireTime.After(nowExpireTime) {
101114 expirationNotification = true
102115 }
103116 return item, exists, expirationNotification
108121 for {
109122 var sleepTime time.Duration
110123 cache.mutex.Lock()
124 cache.hasNotified = false
111125 if cache.priorityQueue.Len() > 0 {
112 sleepTime = time.Until(cache.priorityQueue.items[0].expireAt)
113 if sleepTime < 0 && cache.priorityQueue.items[0].expireAt.IsZero() {
126 sleepTime = time.Until(cache.priorityQueue.root().expireAt)
127 if sleepTime < 0 && cache.priorityQueue.root().expireAt.IsZero() {
114128 sleepTime = time.Hour
115129 } else if sleepTime < 0 {
116130 sleepTime = time.Microsecond
171185 cache.checkExpirationCallback(item, reason)
172186 cache.priorityQueue.remove(item)
173187 delete(cache.items, item.key)
174
175188 }
176189
177190 func (cache *Cache) evictjob(reason EvictionReason) {
243256 }
244257 item, exists, _ := cache.getItem(key)
245258
259 oldExpireTime := time.Time{}
260 if !cache.priorityQueue.isEmpty() {
261 oldExpireTime = cache.priorityQueue.root().expireAt
262 }
263
246264 if exists {
247265 item.data = data
248266 item.ttl = ttl
255273 }
256274 cache.metrics.Inserted++
257275
258 if item.ttl >= 0 && (item.ttl > 0 || cache.ttl > 0) {
259 if cache.ttl > 0 && item.ttl == 0 {
260 item.ttl = cache.ttl
261 }
262 item.touch()
263 }
276 if item.ttl == 0 {
277 item.ttl = cache.ttl
278 }
279
280 item.touch()
264281
265282 if exists {
266283 cache.priorityQueue.update(item)
268285 cache.priorityQueue.push(item)
269286 }
270287
288 nowExpireTime := cache.priorityQueue.root().expireAt
289
271290 cache.mutex.Unlock()
272291 if !exists && cache.newItemCallback != nil {
273292 cache.newItemCallback(key, data)
274293 }
275 cache.expirationNotification <- true
294
295 // notify expiration only if the latest expire time is changed
296 if (oldExpireTime.IsZero() && !nowExpireTime.IsZero()) || oldExpireTime.After(nowExpireTime) {
297 cache.notifyExpiration()
298 }
276299 return nil
277300 }
278301
279302 // Get is a thread-safe way to lookup items
280 // Every lookup, also touches the item, hence extending it's life
303 // Every lookup, also touches the item, hence extending its life
281304 func (cache *Cache) Get(key string) (interface{}, error) {
282305 return cache.GetByLoader(key, nil)
283306 }
284307
285308 // GetWithTTL has exactly the same behaviour as Get but also returns
286 // the remaining TTL for an specific item at the moment it its retrieved
309 // the remaining TTL for a specific item at the moment its retrieved
287310 func (cache *Cache) GetWithTTL(key string) (interface{}, time.Duration, error) {
288311 return cache.GetByLoaderWithTtl(key, nil)
289312 }
290313
291 // GetByLoader can take a per key loader function (ie. to propagate context)
314 // GetByLoader can take a per key loader function (i.e. to propagate context)
292315 func (cache *Cache) GetByLoader(key string, customLoaderFunction LoaderFunction) (interface{}, error) {
293316 dataToReturn, _, err := cache.GetByLoaderWithTtl(key, customLoaderFunction)
294317
295318 return dataToReturn, err
296319 }
297320
298 // GetByLoaderWithTtl can take a per key loader function (ie. to propagate context)
321 // GetByLoaderWithTtl can take a per key loader function (i.e. to propagate context)
299322 func (cache *Cache) GetByLoaderWithTtl(key string, customLoaderFunction LoaderFunction) (interface{}, time.Duration, error) {
300323 cache.mutex.Lock()
301324 if cache.isShutDown {
358381 }
359382
360383 if triggerExpirationNotification {
361 cache.expirationNotification <- true
384 cache.notifyExpiration()
362385 }
363386
364387 return dataToReturn, ttlToReturn, err
388 }
389
390 func (cache *Cache) notifyExpiration() {
391 cache.mutex.Lock()
392 if cache.hasNotified {
393 cache.mutex.Unlock()
394 return
395 }
396 cache.hasNotified = true
397 cache.mutex.Unlock()
398
399 cache.expirationNotification <- true
365400 }
366401
367402 func (cache *Cache) invokeLoader(key string, loaderFunction LoaderFunction) (dataToReturn interface{}, ttl time.Duration, err error) {
431466 }
432467 cache.ttl = ttl
433468 cache.mutex.Unlock()
434 cache.expirationNotification <- true
469 cache.notifyExpiration()
435470 return nil
436471 }
437472
512547 items: make(map[string]*item),
513548 loaderLock: &singleflight.Group{},
514549 priorityQueue: newPriorityQueue(),
515 expirationNotification: make(chan bool),
550 expirationNotification: make(chan bool, 1),
516551 expirationTime: time.Now(),
517552 shutdownSignal: shutdownChan,
518553 isShutDown: false,
1111
1212 type priorityQueue struct {
1313 items []*item
14 }
15
16 func (pq *priorityQueue) isEmpty() bool {
17 return len(pq.items) == 0
18 }
19
20 func (pq *priorityQueue) root() *item {
21 if len(pq.items) == 0 {
22 return nil
23 }
24
25 return pq.items[0]
1426 }
1527
1628 func (pq *priorityQueue) update(item *item) {