Update upstream source from tag 'upstream/2.0.0+ds'
Update to upstream version '2.0.0+ds'
with Debian dir 928e4c7eed42eb7f67641b7b22437ab48921e038
Sascha Steinbiss
3 years ago
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 | |
1 | 1 | |
2 | 2 | TTLCache is a simple key/value cache in golang with the following functions: |
3 | 3 | |
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)` | |
8 | 8 | 5. Can trigger callback on key expiration |
9 | 9 | 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. | |
10 | 11 | |
11 | 12 | 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. |
12 | 13 | |
13 | 14 | [](https://travis-ci.org/ReneKroon/ttlcache) |
14 | 15 | |
15 | #### Usage | |
16 | ## Usage | |
17 | ||
16 | 18 | ```go |
17 | 19 | import ( |
18 | 20 | "time" |
23 | 25 | |
24 | 26 | func main () { |
25 | 27 | newItemCallback := func(key string, value interface{}) { |
26 | fmt.Printf("New key(%s) added\n", key) | |
28 | fmt.Printf("New key(%s) added\n", key) | |
27 | 29 | } |
28 | 30 | 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 | } | |
37 | 39 | 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 | } | |
40 | 49 | |
41 | 50 | cache := ttlcache.NewCache() |
42 | 51 | defer cache.Close() |
43 | 52 | cache.SetTTL(time.Duration(10 * time.Second)) |
44 | 53 | cache.SetExpirationCallback(expirationCallback) |
54 | cache.SetLoaderFunction(loaderFunction) | |
45 | 55 | |
46 | 56 | cache.Set("key", "value") |
47 | 57 | cache.SetWithTTL("keyWithTTL", "value", 10 * time.Second) |
52 | 62 | } |
53 | 63 | ``` |
54 | 64 | |
55 | #### TTLCache - Some design considerations | |
65 | ### TTLCache - Some design considerations | |
56 | 66 | |
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. | |
59 | 69 | 3. I prefer correct functionality over fast tests. It's ok for new tests to take seconds to proof something. |
60 | 70 | |
61 | #### Original Project | |
71 | ### Original Project | |
62 | 72 | |
63 | 73 | TTLCache was forked from [wunderlist/ttlcache](https://github.com/wunderlist/ttlcache) to add extra functions not avaiable in the original scope. |
64 | 74 | The main differences are: |
66 | 76 | 1. A item can store any kind of object, previously, only strings could be saved |
67 | 77 | 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 |
68 | 78 | 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) | |
70 | 80 | 5. Expirations and callbacks are realtime. Don't have a pooling time to check anymore, now it's done with a heap. |
0 | 0 | package ttlcache |
1 | 1 | |
2 | 2 | import ( |
3 | "errors" | |
3 | 4 | "sync" |
4 | 5 | "time" |
5 | 6 | ) |
6 | 7 | |
7 | 8 | // 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 | |
9 | 10 | |
10 | 11 | // 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) | |
12 | 16 | |
13 | 17 | // Cache is a synchronized map of items that can auto-expire once stale |
14 | 18 | type Cache struct { |
15 | 19 | mutex sync.Mutex |
16 | 20 | ttl time.Duration |
17 | 21 | 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 | |
21 | 26 | priorityQueue *priorityQueue |
22 | 27 | expirationNotification chan bool |
23 | 28 | expirationTime time.Time |
24 | 29 | skipTTLExtension bool |
25 | 30 | shutdownSignal chan (chan struct{}) |
26 | 31 | 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 | ) | |
28 | 41 | |
29 | 42 | func (cache *Cache) getItem(key string) (*item, bool, bool) { |
30 | 43 | item, exists := cache.items[key] |
104 | 117 | } |
105 | 118 | } |
106 | 119 | |
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 | ||
107 | 129 | func (cache *Cache) evictjob() { |
108 | 130 | // index will only be advanced if the current entry will not be evicted |
109 | 131 | i := 0 |
110 | 132 | for item := cache.priorityQueue.items[i]; ; item = cache.priorityQueue.items[i] { |
111 | 133 | |
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) | |
117 | 135 | if cache.priorityQueue.Len() == 0 { |
118 | 136 | return |
119 | 137 | } |
137 | 155 | } |
138 | 156 | } |
139 | 157 | |
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) | |
145 | 159 | if cache.priorityQueue.Len() == 0 { |
146 | 160 | return |
147 | 161 | } |
148 | 162 | } |
149 | 163 | } |
150 | 164 | |
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 { | |
154 | 168 | |
155 | 169 | cache.mutex.Lock() |
156 | 170 | if !cache.isShutDown { |
162 | 176 | close(cache.shutdownSignal) |
163 | 177 | } else { |
164 | 178 | cache.mutex.Unlock() |
179 | return ErrClosed | |
165 | 180 | } |
166 | 181 | 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 | } | |
177 | 197 | item, exists, _ := cache.getItem(key) |
178 | 198 | |
179 | 199 | if exists { |
202 | 222 | cache.newItemCallback(key, data) |
203 | 223 | } |
204 | 224 | cache.expirationNotification <- true |
225 | return nil | |
205 | 226 | } |
206 | 227 | |
207 | 228 | // Get is a thread-safe way to lookup items |
208 | 229 | // 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 | } | |
211 | 236 | item, exists, triggerExpirationNotification := cache.getItem(key) |
212 | 237 | |
213 | 238 | var dataToReturn interface{} |
214 | 239 | if exists { |
215 | 240 | dataToReturn = item.data |
216 | 241 | } |
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 | ||
218 | 281 | if triggerExpirationNotification { |
219 | 282 | cache.expirationNotification <- true |
220 | 283 | } |
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 | ||
226 | 309 | object, exists := cache.items[key] |
227 | 310 | 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. | |
239 | 319 | func (cache *Cache) Count() int { |
240 | 320 | cache.mutex.Lock() |
321 | defer cache.mutex.Unlock() | |
322 | ||
323 | if cache.isShutDown { | |
324 | return 0 | |
325 | } | |
241 | 326 | length := len(cache.items) |
242 | cache.mutex.Unlock() | |
243 | 327 | return length |
244 | 328 | } |
245 | 329 | |
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 | } | |
248 | 338 | cache.ttl = ttl |
249 | 339 | cache.mutex.Unlock() |
250 | 340 | cache.expirationNotification <- true |
341 | return nil | |
251 | 342 | } |
252 | 343 | |
253 | 344 | // 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) { | |
255 | 346 | cache.expireCallback = callback |
256 | 347 | } |
257 | 348 | |
258 | 349 | // SetCheckExpirationCallback sets a callback that will be called when an item is about to expire |
259 | 350 | // 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) { | |
261 | 352 | cache.checkExpireCallback = callback |
262 | 353 | } |
263 | 354 | |
264 | 355 | // 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) { | |
266 | 357 | cache.newItemCallback = callback |
267 | 358 | } |
268 | 359 | |
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 | |
270 | 361 | // no longer extend TTL of items when they are retrieved using Get, or when their expiration condition is evaluated |
271 | 362 | // using SetCheckExpirationCallback. |
272 | func (cache *Cache) SkipTtlExtensionOnHit(value bool) { | |
363 | func (cache *Cache) SkipTTLExtensionOnHit(value bool) { | |
273 | 364 | cache.skipTTLExtension = value |
274 | 365 | } |
275 | 366 | |
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 | ||
276 | 373 | // 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 | } | |
279 | 380 | cache.items = make(map[string]*item) |
280 | 381 | cache.priorityQueue = newPriorityQueue() |
281 | cache.mutex.Unlock() | |
382 | return nil | |
282 | 383 | } |
283 | 384 | |
284 | 385 | // NewCache is a helper to create instance of the Cache struct |
288 | 389 | |
289 | 390 | cache := &Cache{ |
290 | 391 | items: make(map[string]*item), |
392 | loaderLock: make(map[string]*sync.Cond), | |
291 | 393 | priorityQueue: newPriorityQueue(), |
292 | 394 | expirationNotification: make(chan bool), |
293 | 395 | expirationTime: time.Now(), |
294 | 396 | shutdownSignal: shutdownChan, |
295 | 397 | isShutDown: false, |
398 | loaderFunction: nil, | |
296 | 399 | } |
297 | 400 | go cache.startExpirationProcessing() |
298 | 401 | return cache |
0 | package ttlcache | |
0 | package ttlcache_test | |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "math/rand" |
4 | "sync/atomic" | |
4 | 5 | "testing" |
5 | 6 | "time" |
6 | 7 | |
9 | 10 | "fmt" |
10 | 11 | "sync" |
11 | 12 | |
13 | . "github.com/ReneKroon/ttlcache/v2" | |
12 | 14 | "github.com/stretchr/testify/assert" |
13 | 15 | ) |
14 | 16 | |
16 | 18 | goleak.VerifyTestMain(m) |
17 | 19 | } |
18 | 20 | |
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 | ||
19 | 115 | // Issue #28: call expirationCallback automatically on cache.Close() |
20 | 116 | func TestCache_ExpirationOnClose(t *testing.T) { |
21 | ||
117 | t.Parallel() | |
22 | 118 | cache := NewCache() |
23 | 119 | |
24 | 120 | success := make(chan struct{}) |
48 | 144 | } |
49 | 145 | |
50 | 146 | // # Issue 29: After Close() the behaviour of Get, Set, Remove is not defined. |
51 | /* | |
147 | ||
52 | 148 | func TestCache_ModifyAfterClose(t *testing.T) { |
149 | t.Parallel() | |
53 | 150 | cache := NewCache() |
54 | 151 | |
55 | 152 | cache.SetTTL(time.Hour * 100) |
60 | 157 | cache.Set("2", 1) |
61 | 158 | cache.Set("3", 1) |
62 | 159 | |
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 | ||
63 | 168 | cache.Close() |
64 | 169 | |
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 | } | |
77 | 180 | |
78 | 181 | // Issue #23: Goroutine leak on closing. When adding a close method i would like to see |
79 | 182 | // that it can be called in a repeated way without problems. |
80 | 183 | func TestCache_MultipleCloseCalls(t *testing.T) { |
184 | t.Parallel() | |
81 | 185 | cache := NewCache() |
82 | 186 | |
83 | 187 | cache.SetTTL(time.Millisecond * 100) |
84 | 188 | |
85 | cache.SkipTtlExtensionOnHit(false) | |
189 | cache.SkipTTLExtensionOnHit(false) | |
86 | 190 | cache.Set("test", "!") |
87 | 191 | startTime := time.Now() |
88 | 192 | 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 { | |
90 | 194 | t.Errorf("Item was not found, even though it should not expire.") |
91 | 195 | } |
92 | 196 | |
93 | 197 | } |
94 | 198 | |
95 | 199 | cache.Close() |
96 | cache.Close() | |
97 | cache.Close() | |
98 | cache.Close() | |
200 | assert.Equal(t, ErrClosed, cache.Close()) | |
99 | 201 | } |
100 | 202 | |
101 | 203 | // test for Feature request in issue #12 |
102 | 204 | // |
103 | 205 | func TestCache_SkipTtlExtensionOnHit(t *testing.T) { |
206 | t.Parallel() | |
207 | ||
104 | 208 | cache := NewCache() |
105 | 209 | defer cache.Close() |
106 | 210 | |
107 | 211 | cache.SetTTL(time.Millisecond * 100) |
108 | 212 | |
109 | cache.SkipTtlExtensionOnHit(false) | |
213 | cache.SkipTTLExtensionOnHit(false) | |
110 | 214 | cache.Set("test", "!") |
111 | 215 | startTime := time.Now() |
112 | 216 | 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 { | |
114 | 218 | t.Errorf("Item was not found, even though it should not expire.") |
115 | 219 | } |
116 | 220 | |
117 | 221 | } |
118 | 222 | |
119 | cache.SkipTtlExtensionOnHit(true) | |
223 | cache.SkipTTLExtensionOnHit(true) | |
120 | 224 | cache.Set("expireTest", "!") |
121 | 225 | // 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") { | |
123 | 227 | } |
124 | 228 | } |
125 | 229 | |
126 | 230 | func TestCache_ForRacesAcrossGoroutines(t *testing.T) { |
231 | t.Parallel() | |
232 | ||
127 | 233 | cache := NewCache() |
128 | 234 | defer cache.Close() |
129 | 235 | |
130 | 236 | cache.SetTTL(time.Minute * 1) |
131 | cache.SkipTtlExtensionOnHit(false) | |
237 | cache.SkipTTLExtensionOnHit(false) | |
132 | 238 | |
133 | 239 | var wgSet sync.WaitGroup |
134 | 240 | var wgGet sync.WaitGroup |
174 | 280 | defer cache.Close() |
175 | 281 | |
176 | 282 | cache.SetTTL(time.Minute * 1) |
177 | cache.SkipTtlExtensionOnHit(true) | |
283 | cache.SkipTTLExtensionOnHit(true) | |
178 | 284 | |
179 | 285 | var wgSet sync.WaitGroup |
180 | 286 | var wgGet sync.WaitGroup |
218 | 324 | // test github issue #14 |
219 | 325 | // Testing expiration callback would continue with the next item in list, even when it exceeds list lengths |
220 | 326 | func TestCache_SetCheckExpirationCallback(t *testing.T) { |
327 | t.Parallel() | |
328 | ||
221 | 329 | iterated := 0 |
222 | 330 | ch := make(chan struct{}) |
223 | 331 | |
249 | 357 | // Which is not right when we become negative due to scheduling. |
250 | 358 | // This test could use improvement as it's not requiring a lot of time to trigger. |
251 | 359 | func TestCache_SetExpirationCallback(t *testing.T) { |
360 | t.Parallel() | |
252 | 361 | |
253 | 362 | type A struct { |
254 | 363 | } |
274 | 383 | |
275 | 384 | // test github issue #4 |
276 | 385 | func TestRemovalAndCountDoesNotPanic(t *testing.T) { |
386 | t.Parallel() | |
387 | ||
277 | 388 | cache := NewCache() |
278 | 389 | defer cache.Close() |
279 | 390 | |
285 | 396 | |
286 | 397 | // test github issue #3 |
287 | 398 | func TestRemovalWithTtlDoesNotPanic(t *testing.T) { |
399 | t.Parallel() | |
400 | ||
288 | 401 | cache := NewCache() |
289 | 402 | defer cache.Close() |
290 | 403 | |
296 | 409 | cache.Set("key", "value") |
297 | 410 | cache.Remove("key") |
298 | 411 | |
299 | value, exists := cache.Get("keyWithTTL") | |
300 | if exists { | |
412 | value, err := cache.Get("keyWithTTL") | |
413 | if err == nil { | |
301 | 414 | t.Logf("got %s for keyWithTTL\n", value) |
302 | 415 | } |
303 | 416 | count := cache.Count() |
305 | 418 | |
306 | 419 | <-time.After(3 * time.Second) |
307 | 420 | |
308 | value, exists = cache.Get("keyWithTTL") | |
309 | if exists { | |
421 | value, err = cache.Get("keyWithTTL") | |
422 | if err != nil { | |
310 | 423 | t.Logf("got %s for keyWithTTL\n", value) |
311 | 424 | } else { |
312 | 425 | t.Logf("keyWithTTL has gone") |
316 | 429 | } |
317 | 430 | |
318 | 431 | func TestCacheIndividualExpirationBiggerThanGlobal(t *testing.T) { |
432 | t.Parallel() | |
433 | ||
319 | 434 | cache := NewCache() |
320 | 435 | defer cache.Close() |
321 | 436 | |
323 | 438 | cache.SetWithTTL("key", "value", time.Duration(100*time.Millisecond)) |
324 | 439 | <-time.After(150 * time.Millisecond) |
325 | 440 | 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") | |
327 | 442 | assert.Nil(t, data, "Expected item to be nil") |
328 | 443 | } |
329 | 444 | |
330 | 445 | func TestCacheGlobalExpirationByGlobal(t *testing.T) { |
446 | t.Parallel() | |
447 | ||
331 | 448 | cache := NewCache() |
332 | 449 | defer cache.Close() |
333 | 450 | |
334 | 451 | cache.Set("key", "value") |
335 | 452 | <-time.After(50 * time.Millisecond) |
336 | 453 | 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") | |
338 | 455 | assert.Equal(t, data.(string), "value", "Expected item to have 'value' in value") |
339 | 456 | |
340 | 457 | cache.SetTTL(time.Duration(50 * time.Millisecond)) |
341 | 458 | 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") | |
343 | 460 | assert.Equal(t, data.(string), "value", "Expected item to have 'value' in value") |
344 | 461 | |
345 | 462 | <-time.After(100 * time.Millisecond) |
346 | 463 | 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") | |
348 | 465 | assert.Nil(t, data, "Expected item to be nil") |
349 | 466 | } |
350 | 467 | |
351 | 468 | func TestCacheGlobalExpiration(t *testing.T) { |
469 | t.Parallel() | |
470 | ||
352 | 471 | cache := NewCache() |
353 | 472 | defer cache.Close() |
354 | 473 | |
357 | 476 | cache.Set("key_2", "value") |
358 | 477 | <-time.After(200 * time.Millisecond) |
359 | 478 | assert.Equal(t, 0, cache.Count(), "Cache should be empty") |
360 | assert.Equal(t, 0, cache.priorityQueue.Len(), "PriorityQueue should be empty") | |
479 | ||
361 | 480 | } |
362 | 481 | |
363 | 482 | func TestCacheMixedExpirations(t *testing.T) { |
483 | t.Parallel() | |
484 | ||
364 | 485 | cache := NewCache() |
365 | 486 | defer cache.Close() |
366 | 487 | |
375 | 496 | } |
376 | 497 | |
377 | 498 | func TestCacheIndividualExpiration(t *testing.T) { |
499 | t.Parallel() | |
500 | ||
378 | 501 | cache := NewCache() |
379 | 502 | defer cache.Close() |
380 | 503 | |
393 | 516 | } |
394 | 517 | |
395 | 518 | func TestCacheGet(t *testing.T) { |
519 | t.Parallel() | |
520 | ||
396 | 521 | cache := NewCache() |
397 | 522 | defer cache.Close() |
398 | 523 | |
399 | 524 | 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") | |
401 | 526 | assert.Nil(t, data, "Expected data to be empty") |
402 | 527 | |
403 | 528 | cache.Set("hello", "world") |
404 | 529 | data, exists = cache.Get("hello") |
405 | 530 | 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") | |
407 | 532 | assert.Equal(t, "world", (data.(string)), "Expected data content to be 'world'") |
408 | 533 | } |
409 | 534 | |
410 | 535 | func TestCacheExpirationCallbackFunction(t *testing.T) { |
536 | t.Parallel() | |
537 | ||
411 | 538 | expiredCount := 0 |
412 | 539 | var lock sync.Mutex |
413 | 540 | |
432 | 559 | // TestCacheCheckExpirationCallbackFunction should consider that the next entry in the queue |
433 | 560 | // needs to be considered for eviction even if the callback returns no eviction for the current item |
434 | 561 | func TestCacheCheckExpirationCallbackFunction(t *testing.T) { |
562 | t.Parallel() | |
563 | ||
435 | 564 | expiredCount := 0 |
436 | 565 | var lock sync.Mutex |
437 | 566 | |
438 | 567 | cache := NewCache() |
439 | 568 | defer cache.Close() |
440 | 569 | |
441 | cache.SkipTtlExtensionOnHit(true) | |
570 | cache.SkipTTLExtensionOnHit(true) | |
442 | 571 | cache.SetTTL(time.Duration(50 * time.Millisecond)) |
443 | 572 | cache.SetCheckExpirationCallback(func(key string, value interface{}) bool { |
444 | 573 | if key == "key2" || key == "key4" { |
463 | 592 | } |
464 | 593 | |
465 | 594 | func TestCacheNewItemCallbackFunction(t *testing.T) { |
595 | t.Parallel() | |
596 | ||
466 | 597 | newItemCount := 0 |
467 | 598 | cache := NewCache() |
468 | 599 | defer cache.Close() |
479 | 610 | } |
480 | 611 | |
481 | 612 | func TestCacheRemove(t *testing.T) { |
613 | t.Parallel() | |
614 | ||
482 | 615 | cache := NewCache() |
483 | 616 | defer cache.Close() |
484 | 617 | |
488 | 621 | <-time.After(70 * time.Millisecond) |
489 | 622 | removeKey := cache.Remove("key") |
490 | 623 | 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") | |
493 | 626 | } |
494 | 627 | |
495 | 628 | func TestCacheSetWithTTLExistItem(t *testing.T) { |
629 | t.Parallel() | |
630 | ||
496 | 631 | cache := NewCache() |
497 | 632 | defer cache.Close() |
498 | 633 | |
501 | 636 | <-time.After(30 * time.Millisecond) |
502 | 637 | cache.SetWithTTL("key", "value2", time.Duration(50*time.Millisecond)) |
503 | 638 | 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") | |
505 | 640 | assert.Equal(t, "value2", data.(string), "Expected 'data' to have value 'value2'") |
506 | 641 | } |
507 | 642 | |
508 | 643 | func TestCache_Purge(t *testing.T) { |
644 | t.Parallel() | |
645 | ||
509 | 646 | cache := NewCache() |
510 | 647 | defer cache.Close() |
511 | 648 |
0 | module github.com/ReneKroon/ttlcache | |
0 | module github.com/ReneKroon/ttlcache/v2 | |
1 | 1 | |
2 | 2 | go 1.14 |
3 | 3 | |
4 | 4 | require ( |
5 | 5 | 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 | |
8 | 8 | ) |
0 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= | |
0 | 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
1 | 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
2 | 3 | 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= | |
3 | 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
4 | 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
5 | 11 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= |
6 | 12 | 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= |