Codebase list golang-github-renekroon-ttlcache / upstream/2.0.0+ds
New upstream version 2.0.0+ds Sascha Steinbiss 3 years ago
6 changed file(s) with 419 addition(s) and 129 deletion(s). Raw diff Collapse all Expand all
0 # 2.0.0 (July 2020)
1
2 ## Fixes #29, #30, #31
3
4 ## Behavioural changes
5
6 * `Remove(key)` now also calls the expiration callback when it's set
7 * `Count()` returns zero when the cache is closed
8
9 ## API changes
10
11 * `SetLoaderFunction` allows you to provide a function to retrieve data on missing cache keys.
12 * Operations that affect item behaviour such as `Close`, `Set`, `SetWithTTL`, `Get`, `Remove`, `Purge` now return an error with standard errors `ErrClosed` an `ErrNotFound` instead of a bool or nothing
13 * `SkipTTLExtensionOnHit` replaces `SkipTtlExtensionOnHit` to satisfy golint
14 * The callback types are now exported
0 ## TTLCache - an in-memory cache with expiration
0 # TTLCache - an in-memory cache with expiration
11
22 TTLCache is a simple key/value cache in golang with the following functions:
33
4 1. Thread-safe
5 2. Individual expiring time or global expiring time, you can choose
6 3. Auto-Extending expiration on `Get` -or- DNS style TTL, see `SkipTtlExtensionOnHit(bool)`
7 4. Fast and memory efficient
4 1. Expiration of items based on time, or custom function
5 2. Loader function to retrieve missing keys can be provided. Additional `Get` calls on the same key block while fetching is in progress (groupcache style).
6 3. Individual expiring time or global expiring time, you can choose
7 4. Auto-Extending expiration on `Get` -or- DNS style TTL, see `SkipTTLExtensionOnHit(bool)`
88 5. Can trigger callback on key expiration
99 6. Cleanup resources by calling `Close()` at end of lifecycle.
10 7. Thread-safe with comprehensive testing suite. This code is in production at bol.com on critical systems.
1011
1112 Note (issue #25): by default, due to historic reasons, the TTL will be reset on each cache hit and you need to explicitly configure the cache to use a TTL that will not get extended.
1213
1314 [![Build Status](https://travis-ci.org/ReneKroon/ttlcache.svg?branch=master)](https://travis-ci.org/ReneKroon/ttlcache)
1415
15 #### Usage
16 ## Usage
17
1618 ```go
1719 import (
1820 "time"
2325
2426 func main () {
2527 newItemCallback := func(key string, value interface{}) {
26 fmt.Printf("New key(%s) added\n", key)
28 fmt.Printf("New key(%s) added\n", key)
2729 }
2830 checkExpirationCallback := func(key string, value interface{}) bool {
29 if key == "key1" {
30 // if the key equals "key1", the value
31 // will not be allowed to expire
32 return false
33 }
34 // all other values are allowed to expire
35 return true
36 }
31 if key == "key1" {
32 // if the key equals "key1", the value
33 // will not be allowed to expire
34 return false
35 }
36 // all other values are allowed to expire
37 return true
38 }
3739 expirationCallback := func(key string, value interface{}) {
38 fmt.Printf("This key(%s) has expired\n", key)
39 }
40 fmt.Printf("This key(%s) has expired\n", key)
41 }
42
43 loaderFunction := func(key string) (data interface{}, ttl time.Duration, err error) {
44 ttl = time.Second * 300
45 data, err = getFromNetwork(key)
46
47 return data, ttl, err
48 }
4049
4150 cache := ttlcache.NewCache()
4251 defer cache.Close()
4352 cache.SetTTL(time.Duration(10 * time.Second))
4453 cache.SetExpirationCallback(expirationCallback)
54 cache.SetLoaderFunction(loaderFunction)
4555
4656 cache.Set("key", "value")
4757 cache.SetWithTTL("keyWithTTL", "value", 10 * time.Second)
5262 }
5363 ```
5464
55 #### TTLCache - Some design considerations
65 ### TTLCache - Some design considerations
5666
57 1. The complexity of the current cache is already quite high. Therefore i will not add 'convenience' features like an interface to supply a function to get missing keys.
58 2. The locking should be done only in the functions of the Cache struct. Else data races can occur or recursive locks are needed, which are both unwanted.
67 1. The complexity of the current cache is already quite high. Therefore not all requests can be implemented in a straight-forward manner.
68 2. The locking should be done only in the exported functions and `startExpirationProcessing` of the Cache struct. Else data races can occur or recursive locks are needed, which are both unwanted.
5969 3. I prefer correct functionality over fast tests. It's ok for new tests to take seconds to proof something.
6070
61 #### Original Project
71 ### Original Project
6272
6373 TTLCache was forked from [wunderlist/ttlcache](https://github.com/wunderlist/ttlcache) to add extra functions not avaiable in the original scope.
6474 The main differences are:
6676 1. A item can store any kind of object, previously, only strings could be saved
6777 2. Optionally, you can add callbacks too: check if a value should expire, be notified if a value expires, and be notified when new values are added to the cache
6878 3. The expiration can be either global or per item
69 4. Can exist items without expiration time
79 4. Items can exist without expiration time (time.Zero)
7080 5. Expirations and callbacks are realtime. Don't have a pooling time to check anymore, now it's done with a heap.
00 package ttlcache
11
22 import (
3 "errors"
34 "sync"
45 "time"
56 )
67
78 // CheckExpireCallback is used as a callback for an external check on item expiration
8 type checkExpireCallback func(key string, value interface{}) bool
9 type CheckExpireCallback func(key string, value interface{}) bool
910
1011 // ExpireCallback is used as a callback on item expiration or when notifying of an item new to the cache
11 type expireCallback func(key string, value interface{})
12 type ExpireCallback func(key string, value interface{})
13
14 // LoaderFunction can be supplied to retrieve an item where a cache miss occurs. Supply an item specific ttl or Duration.Zero
15 type LoaderFunction func(key string) (data interface{}, ttl time.Duration, err error)
1216
1317 // Cache is a synchronized map of items that can auto-expire once stale
1418 type Cache struct {
1519 mutex sync.Mutex
1620 ttl time.Duration
1721 items map[string]*item
18 expireCallback expireCallback
19 checkExpireCallback checkExpireCallback
20 newItemCallback expireCallback
22 loaderLock map[string]*sync.Cond
23 expireCallback ExpireCallback
24 checkExpireCallback CheckExpireCallback
25 newItemCallback ExpireCallback
2126 priorityQueue *priorityQueue
2227 expirationNotification chan bool
2328 expirationTime time.Time
2429 skipTTLExtension bool
2530 shutdownSignal chan (chan struct{})
2631 isShutDown bool
27 }
32 loaderFunction LoaderFunction
33 }
34
35 var (
36 // ErrClosed is raised when operating on a cache where Close() has already been called.
37 ErrClosed = errors.New("cache already closed")
38 // ErrNotFound indicates that the requested key is not present in the cache
39 ErrNotFound = errors.New("key not found")
40 )
2841
2942 func (cache *Cache) getItem(key string) (*item, bool, bool) {
3043 item, exists := cache.items[key]
104117 }
105118 }
106119
120 func (cache *Cache) removeItem(item *item) {
121 if cache.expireCallback != nil {
122 go cache.expireCallback(item.key, item.data)
123 }
124 cache.priorityQueue.remove(item)
125 delete(cache.items, item.key)
126
127 }
128
107129 func (cache *Cache) evictjob() {
108130 // index will only be advanced if the current entry will not be evicted
109131 i := 0
110132 for item := cache.priorityQueue.items[i]; ; item = cache.priorityQueue.items[i] {
111133
112 cache.priorityQueue.remove(item)
113 delete(cache.items, item.key)
114 if cache.expireCallback != nil {
115 go cache.expireCallback(item.key, item.data)
116 }
134 cache.removeItem(item)
117135 if cache.priorityQueue.Len() == 0 {
118136 return
119137 }
137155 }
138156 }
139157
140 cache.priorityQueue.remove(item)
141 delete(cache.items, item.key)
142 if cache.expireCallback != nil {
143 go cache.expireCallback(item.key, item.data)
144 }
158 cache.removeItem(item)
145159 if cache.priorityQueue.Len() == 0 {
146160 return
147161 }
148162 }
149163 }
150164
151 // Close calls Purge, and then stops the goroutine that does ttl checking, for a clean shutdown.
152 // The cache is no longer cleaning up after the first call to Close, repeated calls are safe though.
153 func (cache *Cache) Close() {
165 // Close calls Purge after stopping the goroutine that does ttl checking, for a clean shutdown.
166 // The cache is no longer cleaning up after the first call to Close, repeated calls are safe and return ErrClosed.
167 func (cache *Cache) Close() error {
154168
155169 cache.mutex.Lock()
156170 if !cache.isShutDown {
162176 close(cache.shutdownSignal)
163177 } else {
164178 cache.mutex.Unlock()
179 return ErrClosed
165180 }
166181 cache.Purge()
167 }
168
169 // Set is a thread-safe way to add new items to the map
170 func (cache *Cache) Set(key string, data interface{}) {
171 cache.SetWithTTL(key, data, ItemExpireWithGlobalTTL)
172 }
173
174 // SetWithTTL is a thread-safe way to add new items to the map with individual ttl
175 func (cache *Cache) SetWithTTL(key string, data interface{}, ttl time.Duration) {
176 cache.mutex.Lock()
182 return nil
183 }
184
185 // Set is a thread-safe way to add new items to the map.
186 func (cache *Cache) Set(key string, data interface{}) error {
187 return cache.SetWithTTL(key, data, ItemExpireWithGlobalTTL)
188 }
189
190 // SetWithTTL is a thread-safe way to add new items to the map with individual ttl.
191 func (cache *Cache) SetWithTTL(key string, data interface{}, ttl time.Duration) error {
192 cache.mutex.Lock()
193 if cache.isShutDown {
194 cache.mutex.Unlock()
195 return ErrClosed
196 }
177197 item, exists, _ := cache.getItem(key)
178198
179199 if exists {
202222 cache.newItemCallback(key, data)
203223 }
204224 cache.expirationNotification <- true
225 return nil
205226 }
206227
207228 // Get is a thread-safe way to lookup items
208229 // Every lookup, also touches the item, hence extending it's life
209 func (cache *Cache) Get(key string) (interface{}, bool) {
210 cache.mutex.Lock()
230 func (cache *Cache) Get(key string) (interface{}, error) {
231 cache.mutex.Lock()
232 if cache.isShutDown {
233 cache.mutex.Unlock()
234 return nil, ErrClosed
235 }
211236 item, exists, triggerExpirationNotification := cache.getItem(key)
212237
213238 var dataToReturn interface{}
214239 if exists {
215240 dataToReturn = item.data
216241 }
217 cache.mutex.Unlock()
242
243 var err error = nil
244 if !exists {
245 err = ErrNotFound
246 }
247 if cache.loaderFunction == nil || exists {
248 cache.mutex.Unlock()
249 }
250
251 if cache.loaderFunction != nil && !exists {
252 if lock, ok := cache.loaderLock[key]; ok {
253 // if a lock is present then a fetch is in progress and we wait.
254 cache.mutex.Unlock()
255 lock.L.Lock()
256 lock.Wait()
257 lock.L.Unlock()
258 cache.mutex.Lock()
259 item, exists, triggerExpirationNotification = cache.getItem(key)
260 if exists {
261 dataToReturn = item.data
262 err = nil
263 }
264 cache.mutex.Unlock()
265 } else {
266 // if no lock is present we are the leader and should set the lock and fetch.
267 m := sync.NewCond(&sync.Mutex{})
268 cache.loaderLock[key] = m
269 cache.mutex.Unlock()
270 // cache is not blocked during IO
271 dataToReturn, err = cache.invokeLoader(key)
272 cache.mutex.Lock()
273 m.Broadcast()
274 // cleanup so that we don't block consecutive access.
275 delete(cache.loaderLock, key)
276 cache.mutex.Unlock()
277 }
278
279 }
280
218281 if triggerExpirationNotification {
219282 cache.expirationNotification <- true
220283 }
221 return dataToReturn, exists
222 }
223
224 func (cache *Cache) Remove(key string) bool {
225 cache.mutex.Lock()
284
285 return dataToReturn, err
286 }
287
288 func (cache *Cache) invokeLoader(key string) (dataToReturn interface{}, err error) {
289 var ttl time.Duration
290
291 dataToReturn, ttl, err = cache.loaderFunction(key)
292 if err == nil {
293 err = cache.SetWithTTL(key, dataToReturn, ttl)
294 if err != nil {
295 dataToReturn = nil
296 }
297 }
298 return dataToReturn, err
299 }
300
301 // Remove removes an item from the cache if it exists, triggers expiration callback when set. Can return ErrNotFound if the entry was not present.
302 func (cache *Cache) Remove(key string) error {
303 cache.mutex.Lock()
304 defer cache.mutex.Unlock()
305 if cache.isShutDown {
306 return ErrClosed
307 }
308
226309 object, exists := cache.items[key]
227310 if !exists {
228 cache.mutex.Unlock()
229 return false
230 }
231 delete(cache.items, object.key)
232 cache.priorityQueue.remove(object)
233 cache.mutex.Unlock()
234
235 return true
236 }
237
238 // Count returns the number of items in the cache
311 return ErrNotFound
312 }
313 cache.removeItem(object)
314
315 return nil
316 }
317
318 // Count returns the number of items in the cache. Returns zero when the cache has been closed.
239319 func (cache *Cache) Count() int {
240320 cache.mutex.Lock()
321 defer cache.mutex.Unlock()
322
323 if cache.isShutDown {
324 return 0
325 }
241326 length := len(cache.items)
242 cache.mutex.Unlock()
243327 return length
244328 }
245329
246 func (cache *Cache) SetTTL(ttl time.Duration) {
247 cache.mutex.Lock()
330 // SetTTL sets the global TTL value for items in the cache, which can be overridden at the item level.
331 func (cache *Cache) SetTTL(ttl time.Duration) error {
332 cache.mutex.Lock()
333
334 if cache.isShutDown {
335 cache.mutex.Unlock()
336 return ErrClosed
337 }
248338 cache.ttl = ttl
249339 cache.mutex.Unlock()
250340 cache.expirationNotification <- true
341 return nil
251342 }
252343
253344 // SetExpirationCallback sets a callback that will be called when an item expires
254 func (cache *Cache) SetExpirationCallback(callback expireCallback) {
345 func (cache *Cache) SetExpirationCallback(callback ExpireCallback) {
255346 cache.expireCallback = callback
256347 }
257348
258349 // SetCheckExpirationCallback sets a callback that will be called when an item is about to expire
259350 // in order to allow external code to decide whether the item expires or remains for another TTL cycle
260 func (cache *Cache) SetCheckExpirationCallback(callback checkExpireCallback) {
351 func (cache *Cache) SetCheckExpirationCallback(callback CheckExpireCallback) {
261352 cache.checkExpireCallback = callback
262353 }
263354
264355 // SetNewItemCallback sets a callback that will be called when a new item is added to the cache
265 func (cache *Cache) SetNewItemCallback(callback expireCallback) {
356 func (cache *Cache) SetNewItemCallback(callback ExpireCallback) {
266357 cache.newItemCallback = callback
267358 }
268359
269 // SkipTtlExtensionOnHit allows the user to change the cache behaviour. When this flag is set to true it will
360 // SkipTTLExtensionOnHit allows the user to change the cache behaviour. When this flag is set to true it will
270361 // no longer extend TTL of items when they are retrieved using Get, or when their expiration condition is evaluated
271362 // using SetCheckExpirationCallback.
272 func (cache *Cache) SkipTtlExtensionOnHit(value bool) {
363 func (cache *Cache) SkipTTLExtensionOnHit(value bool) {
273364 cache.skipTTLExtension = value
274365 }
275366
367 // SetLoaderFunction allows you to set a function to retrieve cache misses. The signature matches that of the Get function.
368 // Additional Get calls on the same key block while fetching is in progress (groupcache style).
369 func (cache *Cache) SetLoaderFunction(loader LoaderFunction) {
370 cache.loaderFunction = loader
371 }
372
276373 // Purge will remove all entries
277 func (cache *Cache) Purge() {
278 cache.mutex.Lock()
374 func (cache *Cache) Purge() error {
375 cache.mutex.Lock()
376 defer cache.mutex.Unlock()
377 if cache.isShutDown {
378 return ErrClosed
379 }
279380 cache.items = make(map[string]*item)
280381 cache.priorityQueue = newPriorityQueue()
281 cache.mutex.Unlock()
382 return nil
282383 }
283384
284385 // NewCache is a helper to create instance of the Cache struct
288389
289390 cache := &Cache{
290391 items: make(map[string]*item),
392 loaderLock: make(map[string]*sync.Cond),
291393 priorityQueue: newPriorityQueue(),
292394 expirationNotification: make(chan bool),
293395 expirationTime: time.Now(),
294396 shutdownSignal: shutdownChan,
295397 isShutDown: false,
398 loaderFunction: nil,
296399 }
297400 go cache.startExpirationProcessing()
298401 return cache
0 package ttlcache
0 package ttlcache_test
11
22 import (
33 "math/rand"
4 "sync/atomic"
45 "testing"
56 "time"
67
910 "fmt"
1011 "sync"
1112
13 . "github.com/ReneKroon/ttlcache/v2"
1214 "github.com/stretchr/testify/assert"
1315 )
1416
1618 goleak.VerifyTestMain(m)
1719 }
1820
21 // Issue #31: Test that a single fetch is executed with the loader function
22 func TestCache_TestSingleFetch(t *testing.T) {
23 t.Parallel()
24 cache := NewCache()
25 defer cache.Close()
26
27 var calls int32
28
29 loader := func(key string) (data interface{}, ttl time.Duration, err error) {
30 time.Sleep(time.Millisecond * 100)
31 atomic.AddInt32(&calls, 1)
32 return "data", 0, nil
33
34 }
35
36 cache.SetLoaderFunction(loader)
37 wg := sync.WaitGroup{}
38
39 for i := 0; i < 1000; i++ {
40 wg.Add(1)
41 go func() {
42 cache.Get("1")
43 wg.Done()
44 }()
45 }
46 wg.Wait()
47
48 assert.Equal(t, int32(1), calls)
49 }
50
51 // Issue #30: Removal does not use expiration callback.
52 func TestCache_TestRemovalTriggersCallback(t *testing.T) {
53 t.Parallel()
54 cache := NewCache()
55 defer cache.Close()
56
57 var sync = make(chan struct{})
58 expiration := func(key string, data interface{}) {
59
60 sync <- struct{}{}
61 }
62 cache.SetExpirationCallback(expiration)
63
64 cache.Set("1", "barf")
65 cache.Remove("1")
66
67 <-sync
68 }
69
70 // Issue #31: loader function
71 func TestCache_TestLoaderFunction(t *testing.T) {
72 t.Parallel()
73 cache := NewCache()
74
75 cache.SetLoaderFunction(func(key string) (data interface{}, ttl time.Duration, err error) {
76 return nil, 0, ErrNotFound
77 })
78
79 _, err := cache.Get("1")
80 assert.Equal(t, ErrNotFound, err)
81
82 cache.SetLoaderFunction(func(key string) (data interface{}, ttl time.Duration, err error) {
83 return "1", 0, nil
84 })
85
86 value, found := cache.Get("1")
87 assert.Equal(t, nil, found)
88 assert.Equal(t, "1", value)
89
90 cache.Close()
91
92 value, found = cache.Get("1")
93 assert.Equal(t, ErrClosed, found)
94 assert.Equal(t, nil, value)
95 }
96
97 // Issue #31: edge case where cache is closed when loader function has completed
98 func TestCache_TestLoaderFunctionDuringClose(t *testing.T) {
99 t.Parallel()
100 cache := NewCache()
101
102 cache.SetLoaderFunction(func(key string) (data interface{}, ttl time.Duration, err error) {
103 cache.Close()
104 return "1", 0, nil
105 })
106
107 value, found := cache.Get("1")
108 assert.Equal(t, ErrClosed, found)
109 assert.Equal(t, nil, value)
110
111 cache.Close()
112
113 }
114
19115 // Issue #28: call expirationCallback automatically on cache.Close()
20116 func TestCache_ExpirationOnClose(t *testing.T) {
21
117 t.Parallel()
22118 cache := NewCache()
23119
24120 success := make(chan struct{})
48144 }
49145
50146 // # Issue 29: After Close() the behaviour of Get, Set, Remove is not defined.
51 /*
147
52148 func TestCache_ModifyAfterClose(t *testing.T) {
149 t.Parallel()
53150 cache := NewCache()
54151
55152 cache.SetTTL(time.Hour * 100)
60157 cache.Set("2", 1)
61158 cache.Set("3", 1)
62159
160 _, findErr := cache.Get("1")
161 assert.Equal(t, nil, findErr)
162 assert.Equal(t, nil, cache.Set("broken", 1))
163 assert.Equal(t, ErrNotFound, cache.Remove("broken2"))
164 assert.Equal(t, nil, cache.Purge())
165 assert.Equal(t, nil, cache.SetWithTTL("broken", 2, time.Minute))
166 assert.Equal(t, nil, cache.SetTTL(time.Hour))
167
63168 cache.Close()
64169
65 cache.Get("broken3")
66 cache.Set("broken", 1)
67 cache.Remove("broken2")
68
69 wait := time.NewTimer(time.Millisecond * 100)
70
71 select {
72 case <-wait.C:
73 t.Fail()
74 }
75
76 }*/
170 _, getErr := cache.Get("broken3")
171 assert.Equal(t, ErrClosed, getErr)
172 assert.Equal(t, ErrClosed, cache.Set("broken", 1))
173 assert.Equal(t, ErrClosed, cache.Remove("broken2"))
174 assert.Equal(t, ErrClosed, cache.Purge())
175 assert.Equal(t, ErrClosed, cache.SetWithTTL("broken", 2, time.Minute))
176 assert.Equal(t, ErrClosed, cache.SetTTL(time.Hour))
177 assert.Equal(t, 0, cache.Count())
178
179 }
77180
78181 // Issue #23: Goroutine leak on closing. When adding a close method i would like to see
79182 // that it can be called in a repeated way without problems.
80183 func TestCache_MultipleCloseCalls(t *testing.T) {
184 t.Parallel()
81185 cache := NewCache()
82186
83187 cache.SetTTL(time.Millisecond * 100)
84188
85 cache.SkipTtlExtensionOnHit(false)
189 cache.SkipTTLExtensionOnHit(false)
86190 cache.Set("test", "!")
87191 startTime := time.Now()
88192 for now := time.Now(); now.Before(startTime.Add(time.Second * 3)); now = time.Now() {
89 if _, found := cache.Get("test"); !found {
193 if _, err := cache.Get("test"); err != nil {
90194 t.Errorf("Item was not found, even though it should not expire.")
91195 }
92196
93197 }
94198
95199 cache.Close()
96 cache.Close()
97 cache.Close()
98 cache.Close()
200 assert.Equal(t, ErrClosed, cache.Close())
99201 }
100202
101203 // test for Feature request in issue #12
102204 //
103205 func TestCache_SkipTtlExtensionOnHit(t *testing.T) {
206 t.Parallel()
207
104208 cache := NewCache()
105209 defer cache.Close()
106210
107211 cache.SetTTL(time.Millisecond * 100)
108212
109 cache.SkipTtlExtensionOnHit(false)
213 cache.SkipTTLExtensionOnHit(false)
110214 cache.Set("test", "!")
111215 startTime := time.Now()
112216 for now := time.Now(); now.Before(startTime.Add(time.Second * 3)); now = time.Now() {
113 if _, found := cache.Get("test"); !found {
217 if _, err := cache.Get("test"); err != nil {
114218 t.Errorf("Item was not found, even though it should not expire.")
115219 }
116220
117221 }
118222
119 cache.SkipTtlExtensionOnHit(true)
223 cache.SkipTTLExtensionOnHit(true)
120224 cache.Set("expireTest", "!")
121225 // will loop if item does not expire
122 for _, found := cache.Get("expireTest"); found; _, found = cache.Get("expireTest") {
226 for _, err := cache.Get("expireTest"); err == nil; _, err = cache.Get("expireTest") {
123227 }
124228 }
125229
126230 func TestCache_ForRacesAcrossGoroutines(t *testing.T) {
231 t.Parallel()
232
127233 cache := NewCache()
128234 defer cache.Close()
129235
130236 cache.SetTTL(time.Minute * 1)
131 cache.SkipTtlExtensionOnHit(false)
237 cache.SkipTTLExtensionOnHit(false)
132238
133239 var wgSet sync.WaitGroup
134240 var wgGet sync.WaitGroup
174280 defer cache.Close()
175281
176282 cache.SetTTL(time.Minute * 1)
177 cache.SkipTtlExtensionOnHit(true)
283 cache.SkipTTLExtensionOnHit(true)
178284
179285 var wgSet sync.WaitGroup
180286 var wgGet sync.WaitGroup
218324 // test github issue #14
219325 // Testing expiration callback would continue with the next item in list, even when it exceeds list lengths
220326 func TestCache_SetCheckExpirationCallback(t *testing.T) {
327 t.Parallel()
328
221329 iterated := 0
222330 ch := make(chan struct{})
223331
249357 // Which is not right when we become negative due to scheduling.
250358 // This test could use improvement as it's not requiring a lot of time to trigger.
251359 func TestCache_SetExpirationCallback(t *testing.T) {
360 t.Parallel()
252361
253362 type A struct {
254363 }
274383
275384 // test github issue #4
276385 func TestRemovalAndCountDoesNotPanic(t *testing.T) {
386 t.Parallel()
387
277388 cache := NewCache()
278389 defer cache.Close()
279390
285396
286397 // test github issue #3
287398 func TestRemovalWithTtlDoesNotPanic(t *testing.T) {
399 t.Parallel()
400
288401 cache := NewCache()
289402 defer cache.Close()
290403
296409 cache.Set("key", "value")
297410 cache.Remove("key")
298411
299 value, exists := cache.Get("keyWithTTL")
300 if exists {
412 value, err := cache.Get("keyWithTTL")
413 if err == nil {
301414 t.Logf("got %s for keyWithTTL\n", value)
302415 }
303416 count := cache.Count()
305418
306419 <-time.After(3 * time.Second)
307420
308 value, exists = cache.Get("keyWithTTL")
309 if exists {
421 value, err = cache.Get("keyWithTTL")
422 if err != nil {
310423 t.Logf("got %s for keyWithTTL\n", value)
311424 } else {
312425 t.Logf("keyWithTTL has gone")
316429 }
317430
318431 func TestCacheIndividualExpirationBiggerThanGlobal(t *testing.T) {
432 t.Parallel()
433
319434 cache := NewCache()
320435 defer cache.Close()
321436
323438 cache.SetWithTTL("key", "value", time.Duration(100*time.Millisecond))
324439 <-time.After(150 * time.Millisecond)
325440 data, exists := cache.Get("key")
326 assert.Equal(t, exists, false, "Expected item to not exist")
441 assert.Equal(t, exists, ErrNotFound, "Expected item to not exist")
327442 assert.Nil(t, data, "Expected item to be nil")
328443 }
329444
330445 func TestCacheGlobalExpirationByGlobal(t *testing.T) {
446 t.Parallel()
447
331448 cache := NewCache()
332449 defer cache.Close()
333450
334451 cache.Set("key", "value")
335452 <-time.After(50 * time.Millisecond)
336453 data, exists := cache.Get("key")
337 assert.Equal(t, exists, true, "Expected item to exist in cache")
454 assert.Equal(t, exists, nil, "Expected item to exist in cache")
338455 assert.Equal(t, data.(string), "value", "Expected item to have 'value' in value")
339456
340457 cache.SetTTL(time.Duration(50 * time.Millisecond))
341458 data, exists = cache.Get("key")
342 assert.Equal(t, exists, true, "Expected item to exist in cache")
459 assert.Equal(t, exists, nil, "Expected item to exist in cache")
343460 assert.Equal(t, data.(string), "value", "Expected item to have 'value' in value")
344461
345462 <-time.After(100 * time.Millisecond)
346463 data, exists = cache.Get("key")
347 assert.Equal(t, exists, false, "Expected item to not exist")
464 assert.Equal(t, exists, ErrNotFound, "Expected item to not exist")
348465 assert.Nil(t, data, "Expected item to be nil")
349466 }
350467
351468 func TestCacheGlobalExpiration(t *testing.T) {
469 t.Parallel()
470
352471 cache := NewCache()
353472 defer cache.Close()
354473
357476 cache.Set("key_2", "value")
358477 <-time.After(200 * time.Millisecond)
359478 assert.Equal(t, 0, cache.Count(), "Cache should be empty")
360 assert.Equal(t, 0, cache.priorityQueue.Len(), "PriorityQueue should be empty")
479
361480 }
362481
363482 func TestCacheMixedExpirations(t *testing.T) {
483 t.Parallel()
484
364485 cache := NewCache()
365486 defer cache.Close()
366487
375496 }
376497
377498 func TestCacheIndividualExpiration(t *testing.T) {
499 t.Parallel()
500
378501 cache := NewCache()
379502 defer cache.Close()
380503
393516 }
394517
395518 func TestCacheGet(t *testing.T) {
519 t.Parallel()
520
396521 cache := NewCache()
397522 defer cache.Close()
398523
399524 data, exists := cache.Get("hello")
400 assert.Equal(t, exists, false, "Expected empty cache to return no data")
525 assert.Equal(t, exists, ErrNotFound, "Expected empty cache to return no data")
401526 assert.Nil(t, data, "Expected data to be empty")
402527
403528 cache.Set("hello", "world")
404529 data, exists = cache.Get("hello")
405530 assert.NotNil(t, data, "Expected data to be not nil")
406 assert.Equal(t, true, exists, "Expected data to exist")
531 assert.Equal(t, nil, exists, "Expected data to exist")
407532 assert.Equal(t, "world", (data.(string)), "Expected data content to be 'world'")
408533 }
409534
410535 func TestCacheExpirationCallbackFunction(t *testing.T) {
536 t.Parallel()
537
411538 expiredCount := 0
412539 var lock sync.Mutex
413540
432559 // TestCacheCheckExpirationCallbackFunction should consider that the next entry in the queue
433560 // needs to be considered for eviction even if the callback returns no eviction for the current item
434561 func TestCacheCheckExpirationCallbackFunction(t *testing.T) {
562 t.Parallel()
563
435564 expiredCount := 0
436565 var lock sync.Mutex
437566
438567 cache := NewCache()
439568 defer cache.Close()
440569
441 cache.SkipTtlExtensionOnHit(true)
570 cache.SkipTTLExtensionOnHit(true)
442571 cache.SetTTL(time.Duration(50 * time.Millisecond))
443572 cache.SetCheckExpirationCallback(func(key string, value interface{}) bool {
444573 if key == "key2" || key == "key4" {
463592 }
464593
465594 func TestCacheNewItemCallbackFunction(t *testing.T) {
595 t.Parallel()
596
466597 newItemCount := 0
467598 cache := NewCache()
468599 defer cache.Close()
479610 }
480611
481612 func TestCacheRemove(t *testing.T) {
613 t.Parallel()
614
482615 cache := NewCache()
483616 defer cache.Close()
484617
488621 <-time.After(70 * time.Millisecond)
489622 removeKey := cache.Remove("key")
490623 removeKey2 := cache.Remove("key_2")
491 assert.Equal(t, true, removeKey, "Expected 'key' to be removed from cache")
492 assert.Equal(t, false, removeKey2, "Expected 'key_2' to already be expired from cache")
624 assert.Equal(t, nil, removeKey, "Expected 'key' to be removed from cache")
625 assert.Equal(t, ErrNotFound, removeKey2, "Expected 'key_2' to already be expired from cache")
493626 }
494627
495628 func TestCacheSetWithTTLExistItem(t *testing.T) {
629 t.Parallel()
630
496631 cache := NewCache()
497632 defer cache.Close()
498633
501636 <-time.After(30 * time.Millisecond)
502637 cache.SetWithTTL("key", "value2", time.Duration(50*time.Millisecond))
503638 data, exists := cache.Get("key")
504 assert.Equal(t, true, exists, "Expected 'key' to exist")
639 assert.Equal(t, nil, exists, "Expected 'key' to exist")
505640 assert.Equal(t, "value2", data.(string), "Expected 'data' to have value 'value2'")
506641 }
507642
508643 func TestCache_Purge(t *testing.T) {
644 t.Parallel()
645
509646 cache := NewCache()
510647 defer cache.Close()
511648
0 module github.com/ReneKroon/ttlcache
0 module github.com/ReneKroon/ttlcache/v2
11
22 go 1.14
33
44 require (
55 github.com/davecgh/go-spew v1.1.1 // indirect
6 github.com/stretchr/testify v1.3.0
7 go.uber.org/goleak v0.10.0
6 github.com/stretchr/testify v1.6.1
7 go.uber.org/goleak v1.1.10
88 )
0 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
01 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
23 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
5 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
6 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
7 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
8 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
39 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
410 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
511 github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
612 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
8 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
9 go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
10 go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
13 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
14 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
15 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
16 go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
17 go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
18 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
19 golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
20 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
21 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
22 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
23 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
24 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
25 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
26 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
27 golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E=
28 golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
29 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
30 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
31 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
32 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
33 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
34 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
35 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=