New upstream version 1.4.0+git20190429.e7780e9
Sascha Steinbiss
3 years ago
0 | language: go | |
1 | ||
2 | go: | |
3 | - 1.12 | |
4 | - 1.11 | |
5 | git: | |
6 | depth: 1 | |
7 | ||
8 | install: | |
9 | - go install -race std | |
10 | - go get golang.org/x/tools/cmd/cover | |
11 | - go get golang.org/x/lint/golint | |
12 | - go get github.com/tools/godep | |
13 | - export PATH=$HOME/gopath/bin:$PATH | |
14 | - godep restore | |
15 | ||
16 | script: | |
17 | - golint . | |
18 | - go test ./... -race -count=1 -timeout=1m -run . | |
19 | - go test -cover ./... | |
20 | - go test -run=Bench.* -bench=. -benchmem⏎ |
0 | MIT License | |
1 | ||
2 | Copyright (c) 2018 Rene Kroon | |
3 | ||
4 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
5 | of this software and associated documentation files (the "Software"), to deal | |
6 | in the Software without restriction, including without limitation the rights | |
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
8 | copies of the Software, and to permit persons to whom the Software is | |
9 | furnished to do so, subject to the following conditions: | |
10 | ||
11 | The above copyright notice and this permission notice shall be included in all | |
12 | copies or substantial portions of the Software. | |
13 | ||
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
20 | SOFTWARE. |
0 | ## TTLCache - an in-memory cache with expiration | |
1 | ||
2 | TTLCache is a simple key/value cache in golang with the following functions: | |
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 | |
8 | 5. Can trigger callback on key expiration | |
9 | ||
10 | [](https://travis-ci.org/ReneKroon/ttlcache) | |
11 | ||
12 | #### Usage | |
13 | ```go | |
14 | import ( | |
15 | "time" | |
16 | "fmt" | |
17 | ||
18 | "github.com/ReneKroon/ttlcache" | |
19 | ) | |
20 | ||
21 | func main () { | |
22 | newItemCallback := func(key string, value interface{}) { | |
23 | fmt.Printf("New key(%s) added\n", key) | |
24 | } | |
25 | checkExpirationCallback := func(key string, value interface{}) bool { | |
26 | if key == "key1" { | |
27 | // if the key equals "key1", the value | |
28 | // will not be allowed to expire | |
29 | return false | |
30 | } | |
31 | // all other values are allowed to expire | |
32 | return true | |
33 | } | |
34 | expirationCallback := func(key string, value interface{}) { | |
35 | fmt.Printf("This key(%s) has expired\n", key) | |
36 | } | |
37 | ||
38 | cache := ttlcache.NewCache() | |
39 | cache.SetTTL(time.Duration(10 * time.Second)) | |
40 | cache.SetExpirationCallback(expirationCallback) | |
41 | ||
42 | cache.Set("key", "value") | |
43 | cache.SetWithTTL("keyWithTTL", "value", 10 * time.Second) | |
44 | ||
45 | value, exists := cache.Get("key") | |
46 | count := cache.Count() | |
47 | result := cache.Remove("key") | |
48 | } | |
49 | ``` | |
50 | ||
51 | #### Original Project | |
52 | ||
53 | TTLCache was forked from [wunderlist/ttlcache](https://github.com/wunderlist/ttlcache) to add extra functions not avaiable in the original scope. | |
54 | The main differences are: | |
55 | ||
56 | 1. A item can store any kind of object, previously, only strings could be saved | |
57 | 2. Optionally, you can add callbacks to: check if a value should expire, be notified if a value expires, and be notified when new values are added to the cache | |
58 | 3. The expiration can be either global or per item | |
59 | 4. Can exist items without expiration time | |
60 | 5. Expirations and callbacks are realtime. Don't have a pooling time to check anymore, now it's done with a heap. |
0 | package ttlcache | |
1 | ||
2 | import ( | |
3 | "sync" | |
4 | "time" | |
5 | ) | |
6 | ||
7 | // CheckExpireCallback is used as a callback for an external check on item expiration | |
8 | type checkExpireCallback func(key string, value interface{}) bool | |
9 | ||
10 | // 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 | ||
13 | // Cache is a synchronized map of items that can auto-expire once stale | |
14 | type Cache struct { | |
15 | mutex sync.RWMutex | |
16 | ttl time.Duration | |
17 | items map[string]*item | |
18 | expireCallback expireCallback | |
19 | checkExpireCallback checkExpireCallback | |
20 | newItemCallback expireCallback | |
21 | priorityQueue *priorityQueue | |
22 | expirationNotification chan bool | |
23 | expirationTime time.Time | |
24 | skipTTLExtension bool | |
25 | } | |
26 | ||
27 | func (cache *Cache) getItem(key string) (*item, bool) { | |
28 | cache.mutex.RLock() | |
29 | ||
30 | item, exists := cache.items[key] | |
31 | if !exists || item.expired() { | |
32 | cache.mutex.RUnlock() | |
33 | return nil, false | |
34 | } | |
35 | ||
36 | if item.ttl >= 0 && (item.ttl > 0 || cache.ttl > 0) { | |
37 | if cache.ttl > 0 && item.ttl == 0 { | |
38 | item.ttl = cache.ttl | |
39 | } | |
40 | ||
41 | if !cache.skipTTLExtension { | |
42 | item.touch() | |
43 | } | |
44 | cache.priorityQueue.update(item) | |
45 | } | |
46 | ||
47 | cache.mutex.RUnlock() | |
48 | cache.expirationNotificationTrigger(item) | |
49 | return item, exists | |
50 | } | |
51 | ||
52 | func (cache *Cache) startExpirationProcessing() { | |
53 | for { | |
54 | var sleepTime time.Duration | |
55 | cache.mutex.Lock() | |
56 | if cache.priorityQueue.Len() > 0 { | |
57 | sleepTime = time.Until(cache.priorityQueue.items[0].expireAt) | |
58 | if sleepTime < 0 && cache.priorityQueue.items[0].expireAt.IsZero() { | |
59 | sleepTime = time.Hour | |
60 | } else if sleepTime < 0 { | |
61 | sleepTime = time.Microsecond | |
62 | } | |
63 | if cache.ttl > 0 { | |
64 | sleepTime = min(sleepTime, cache.ttl) | |
65 | } | |
66 | ||
67 | } else if cache.ttl > 0 { | |
68 | sleepTime = cache.ttl | |
69 | } else { | |
70 | sleepTime = time.Hour | |
71 | } | |
72 | ||
73 | cache.expirationTime = time.Now().Add(sleepTime) | |
74 | cache.mutex.Unlock() | |
75 | ||
76 | timer := time.NewTimer(sleepTime) | |
77 | select { | |
78 | case <-timer.C: | |
79 | timer.Stop() | |
80 | cache.mutex.Lock() | |
81 | if cache.priorityQueue.Len() == 0 { | |
82 | cache.mutex.Unlock() | |
83 | continue | |
84 | } | |
85 | ||
86 | // index will only be advanced if the current entry will not be evicted | |
87 | i := 0 | |
88 | for item := cache.priorityQueue.items[i]; item.expired(); item = cache.priorityQueue.items[i] { | |
89 | ||
90 | if cache.checkExpireCallback != nil { | |
91 | if !cache.checkExpireCallback(item.key, item.data) { | |
92 | item.touch() | |
93 | cache.priorityQueue.update(item) | |
94 | i++ | |
95 | if i == cache.priorityQueue.Len() { | |
96 | break | |
97 | } | |
98 | continue | |
99 | } | |
100 | } | |
101 | ||
102 | cache.priorityQueue.remove(item) | |
103 | delete(cache.items, item.key) | |
104 | if cache.expireCallback != nil { | |
105 | go cache.expireCallback(item.key, item.data) | |
106 | } | |
107 | if cache.priorityQueue.Len() == 0 { | |
108 | goto done | |
109 | } | |
110 | } | |
111 | done: | |
112 | cache.mutex.Unlock() | |
113 | ||
114 | case <-cache.expirationNotification: | |
115 | timer.Stop() | |
116 | continue | |
117 | } | |
118 | } | |
119 | } | |
120 | ||
121 | func (cache *Cache) expirationNotificationTrigger(item *item) { | |
122 | cache.mutex.Lock() | |
123 | if cache.expirationTime.After(time.Now().Add(item.ttl)) { | |
124 | cache.mutex.Unlock() | |
125 | cache.expirationNotification <- true | |
126 | } else { | |
127 | cache.mutex.Unlock() | |
128 | } | |
129 | } | |
130 | ||
131 | // Set is a thread-safe way to add new items to the map | |
132 | func (cache *Cache) Set(key string, data interface{}) { | |
133 | cache.SetWithTTL(key, data, ItemExpireWithGlobalTTL) | |
134 | } | |
135 | ||
136 | // SetWithTTL is a thread-safe way to add new items to the map with individual ttl | |
137 | func (cache *Cache) SetWithTTL(key string, data interface{}, ttl time.Duration) { | |
138 | item, exists := cache.getItem(key) | |
139 | cache.mutex.Lock() | |
140 | ||
141 | if exists { | |
142 | item.data = data | |
143 | item.ttl = ttl | |
144 | } else { | |
145 | item = newItem(key, data, ttl) | |
146 | cache.items[key] = item | |
147 | } | |
148 | ||
149 | if item.ttl >= 0 && (item.ttl > 0 || cache.ttl > 0) { | |
150 | if cache.ttl > 0 && item.ttl == 0 { | |
151 | item.ttl = cache.ttl | |
152 | } | |
153 | item.touch() | |
154 | } | |
155 | ||
156 | if exists { | |
157 | cache.priorityQueue.update(item) | |
158 | } else { | |
159 | cache.priorityQueue.push(item) | |
160 | } | |
161 | ||
162 | cache.mutex.Unlock() | |
163 | if !exists && cache.newItemCallback != nil { | |
164 | cache.newItemCallback(key, data) | |
165 | } | |
166 | cache.expirationNotification <- true | |
167 | } | |
168 | ||
169 | // Get is a thread-safe way to lookup items | |
170 | // Every lookup, also touches the item, hence extending it's life | |
171 | func (cache *Cache) Get(key string) (interface{}, bool) { | |
172 | item, exists := cache.getItem(key) | |
173 | if exists { | |
174 | cache.mutex.RLock() | |
175 | defer cache.mutex.RUnlock() | |
176 | return item.data, true | |
177 | } | |
178 | return nil, false | |
179 | } | |
180 | ||
181 | func (cache *Cache) Remove(key string) bool { | |
182 | cache.mutex.Lock() | |
183 | object, exists := cache.items[key] | |
184 | if !exists { | |
185 | cache.mutex.Unlock() | |
186 | return false | |
187 | } | |
188 | delete(cache.items, object.key) | |
189 | cache.priorityQueue.remove(object) | |
190 | cache.mutex.Unlock() | |
191 | ||
192 | return true | |
193 | } | |
194 | ||
195 | // Count returns the number of items in the cache | |
196 | func (cache *Cache) Count() int { | |
197 | cache.mutex.RLock() | |
198 | length := len(cache.items) | |
199 | cache.mutex.RUnlock() | |
200 | return length | |
201 | } | |
202 | ||
203 | func (cache *Cache) SetTTL(ttl time.Duration) { | |
204 | cache.mutex.Lock() | |
205 | cache.ttl = ttl | |
206 | cache.mutex.Unlock() | |
207 | cache.expirationNotification <- true | |
208 | } | |
209 | ||
210 | // SetExpirationCallback sets a callback that will be called when an item expires | |
211 | func (cache *Cache) SetExpirationCallback(callback expireCallback) { | |
212 | cache.expireCallback = callback | |
213 | } | |
214 | ||
215 | // SetCheckExpirationCallback sets a callback that will be called when an item is about to expire | |
216 | // in order to allow external code to decide whether the item expires or remains for another TTL cycle | |
217 | func (cache *Cache) SetCheckExpirationCallback(callback checkExpireCallback) { | |
218 | cache.checkExpireCallback = callback | |
219 | } | |
220 | ||
221 | // SetNewItemCallback sets a callback that will be called when a new item is added to the cache | |
222 | func (cache *Cache) SetNewItemCallback(callback expireCallback) { | |
223 | cache.newItemCallback = callback | |
224 | } | |
225 | ||
226 | // SkipTtlExtensionOnHit allows the user to change the cache behaviour. When this flag is set to true it will | |
227 | // no longer extend TTL of items when they are retrieved using Get, or when their expiration condition is evaluated | |
228 | // using SetCheckExpirationCallback. | |
229 | func (cache *Cache) SkipTtlExtensionOnHit(value bool) { | |
230 | cache.skipTTLExtension = value | |
231 | } | |
232 | ||
233 | // Purge will remove all entries | |
234 | func (cache *Cache) Purge() { | |
235 | cache.mutex.Lock() | |
236 | cache.items = make(map[string]*item) | |
237 | cache.priorityQueue = newPriorityQueue() | |
238 | cache.mutex.Unlock() | |
239 | } | |
240 | ||
241 | // NewCache is a helper to create instance of the Cache struct | |
242 | func NewCache() *Cache { | |
243 | cache := &Cache{ | |
244 | items: make(map[string]*item), | |
245 | priorityQueue: newPriorityQueue(), | |
246 | expirationNotification: make(chan bool), | |
247 | expirationTime: time.Now(), | |
248 | } | |
249 | go cache.startExpirationProcessing() | |
250 | return cache | |
251 | } | |
252 | ||
253 | func min(duration time.Duration, second time.Duration) time.Duration { | |
254 | if duration < second { | |
255 | return duration | |
256 | } | |
257 | return second | |
258 | } |
0 | package ttlcache | |
1 | ||
2 | import ( | |
3 | "testing" | |
4 | "time" | |
5 | ||
6 | "fmt" | |
7 | "log" | |
8 | "sync" | |
9 | ||
10 | "github.com/stretchr/testify/assert" | |
11 | ) | |
12 | ||
13 | // test for Feature request in issue #12 | |
14 | // | |
15 | func TestCache_SkipTtlExtensionOnHit(t *testing.T) { | |
16 | cache := NewCache() | |
17 | cache.SetTTL(time.Millisecond * 100) | |
18 | ||
19 | cache.SkipTtlExtensionOnHit(false) | |
20 | cache.Set("test", "!") | |
21 | startTime := time.Now() | |
22 | for now := time.Now(); now.Before(startTime.Add(time.Second * 3)); now = time.Now() { | |
23 | if _, found := cache.Get("test"); !found { | |
24 | t.Errorf("Item was not found, even though it should not expire.") | |
25 | } | |
26 | ||
27 | } | |
28 | ||
29 | cache.SkipTtlExtensionOnHit(true) | |
30 | cache.Set("expireTest", "!") | |
31 | // will loop if item does not expire | |
32 | for _, found := cache.Get("expireTest"); found; _, found = cache.Get("expireTest") { | |
33 | } | |
34 | } | |
35 | ||
36 | func TestCache_SkipTtlExtensionOnHit_ForRacesAcrossGoroutines(t *testing.T) { | |
37 | cache := NewCache() | |
38 | cache.SetTTL(time.Minute * 1) | |
39 | cache.SkipTtlExtensionOnHit(true) | |
40 | ||
41 | var wgSet sync.WaitGroup | |
42 | var wgGet sync.WaitGroup | |
43 | ||
44 | n := 500 | |
45 | wgSet.Add(1) | |
46 | go func() { | |
47 | for i := 0; i < n; i++ { | |
48 | wgSet.Add(1) | |
49 | ||
50 | go func() { | |
51 | cache.Set("test", false) | |
52 | wgSet.Done() | |
53 | }() | |
54 | } | |
55 | wgSet.Done() | |
56 | }() | |
57 | wgGet.Add(1) | |
58 | go func() { | |
59 | for i := 0; i < n; i++ { | |
60 | wgGet.Add(1) | |
61 | ||
62 | go func() { | |
63 | cache.Get("test") | |
64 | wgGet.Done() | |
65 | }() | |
66 | } | |
67 | wgGet.Done() | |
68 | }() | |
69 | ||
70 | wgGet.Wait() | |
71 | wgSet.Wait() | |
72 | } | |
73 | ||
74 | // test github issue #14 | |
75 | // Testing expiration callback would continue with the next item in list, even when it exceeds list lengths | |
76 | func TestCache_SetCheckExpirationCallback(t *testing.T) { | |
77 | iterated := 0 | |
78 | ch := make(chan struct{}) | |
79 | ||
80 | cacheAD := NewCache() | |
81 | cacheAD.SetTTL(time.Millisecond) | |
82 | cacheAD.SetCheckExpirationCallback(func(key string, value interface{}) bool { | |
83 | v := value.(*int) | |
84 | log.Printf("key=%v, value=%d\n", key, *v) | |
85 | iterated++ | |
86 | if iterated == 1 { | |
87 | // this is the breaking test case for issue #14 | |
88 | return false | |
89 | } | |
90 | ch <- struct{}{} | |
91 | return true | |
92 | }) | |
93 | ||
94 | i := 2 | |
95 | cacheAD.Set("a", &i) | |
96 | ||
97 | <-ch | |
98 | } | |
99 | ||
100 | // test github issue #9 | |
101 | // Due to scheduling the expected TTL of the top entry can become negative (already expired) | |
102 | // This is an issue because negative TTL at the item level was interpreted as 'use global TTL' | |
103 | // Which is not right when we become negative due to scheduling. | |
104 | // This test could use improvement as it's not requiring a lot of time to trigger. | |
105 | func TestCache_SetExpirationCallback(t *testing.T) { | |
106 | ||
107 | type A struct { | |
108 | } | |
109 | ||
110 | // Setup the TTL cache | |
111 | cache := NewCache() | |
112 | cache.SetTTL(time.Second * 1) | |
113 | cache.SetExpirationCallback(func(key string, value interface{}) { | |
114 | fmt.Printf("This key(%s) has expired\n", key) | |
115 | }) | |
116 | for i := 0; i < 1024; i++ { | |
117 | cache.Set(fmt.Sprintf("item_%d", i), A{}) | |
118 | time.Sleep(time.Millisecond * 10) | |
119 | fmt.Printf("Cache size: %d\n", cache.Count()) | |
120 | } | |
121 | ||
122 | if cache.Count() > 100 { | |
123 | t.Fatal("Cache should empty entries >1 second old") | |
124 | } | |
125 | } | |
126 | ||
127 | // test github issue #4 | |
128 | func TestRemovalAndCountDoesNotPanic(t *testing.T) { | |
129 | cache := NewCache() | |
130 | cache.Set("key", "value") | |
131 | cache.Remove("key") | |
132 | count := cache.Count() | |
133 | t.Logf("cache has %d keys\n", count) | |
134 | } | |
135 | ||
136 | // test github issue #3 | |
137 | func TestRemovalWithTtlDoesNotPanic(t *testing.T) { | |
138 | cache := NewCache() | |
139 | cache.SetExpirationCallback(func(key string, value interface{}) { | |
140 | t.Logf("This key(%s) has expired\n", key) | |
141 | }) | |
142 | ||
143 | cache.SetWithTTL("keyWithTTL", "value", time.Duration(2*time.Second)) | |
144 | cache.Set("key", "value") | |
145 | cache.Remove("key") | |
146 | ||
147 | value, exists := cache.Get("keyWithTTL") | |
148 | if exists { | |
149 | t.Logf("got %s for keyWithTTL\n", value) | |
150 | } | |
151 | count := cache.Count() | |
152 | t.Logf("cache has %d keys\n", count) | |
153 | ||
154 | <-time.After(3 * time.Second) | |
155 | ||
156 | value, exists = cache.Get("keyWithTTL") | |
157 | if exists { | |
158 | t.Logf("got %s for keyWithTTL\n", value) | |
159 | } else { | |
160 | t.Logf("keyWithTTL has gone") | |
161 | } | |
162 | count = cache.Count() | |
163 | t.Logf("cache has %d keys\n", count) | |
164 | } | |
165 | ||
166 | func TestCacheIndividualExpirationBiggerThanGlobal(t *testing.T) { | |
167 | cache := NewCache() | |
168 | cache.SetTTL(time.Duration(50 * time.Millisecond)) | |
169 | cache.SetWithTTL("key", "value", time.Duration(100*time.Millisecond)) | |
170 | <-time.After(150 * time.Millisecond) | |
171 | data, exists := cache.Get("key") | |
172 | assert.Equal(t, exists, false, "Expected item to not exist") | |
173 | assert.Nil(t, data, "Expected item to be nil") | |
174 | } | |
175 | ||
176 | func TestCacheGlobalExpirationByGlobal(t *testing.T) { | |
177 | cache := NewCache() | |
178 | cache.Set("key", "value") | |
179 | <-time.After(50 * time.Millisecond) | |
180 | data, exists := cache.Get("key") | |
181 | assert.Equal(t, exists, true, "Expected item to exist in cache") | |
182 | assert.Equal(t, data.(string), "value", "Expected item to have 'value' in value") | |
183 | ||
184 | cache.SetTTL(time.Duration(50 * time.Millisecond)) | |
185 | data, exists = cache.Get("key") | |
186 | assert.Equal(t, exists, true, "Expected item to exist in cache") | |
187 | assert.Equal(t, data.(string), "value", "Expected item to have 'value' in value") | |
188 | ||
189 | <-time.After(100 * time.Millisecond) | |
190 | data, exists = cache.Get("key") | |
191 | assert.Equal(t, exists, false, "Expected item to not exist") | |
192 | assert.Nil(t, data, "Expected item to be nil") | |
193 | } | |
194 | ||
195 | func TestCacheGlobalExpiration(t *testing.T) { | |
196 | cache := NewCache() | |
197 | cache.SetTTL(time.Duration(100 * time.Millisecond)) | |
198 | cache.Set("key_1", "value") | |
199 | cache.Set("key_2", "value") | |
200 | <-time.After(200 * time.Millisecond) | |
201 | assert.Equal(t, 0, cache.Count(), "Cache should be empty") | |
202 | assert.Equal(t, 0, cache.priorityQueue.Len(), "PriorityQueue should be empty") | |
203 | } | |
204 | ||
205 | func TestCacheMixedExpirations(t *testing.T) { | |
206 | cache := NewCache() | |
207 | cache.SetExpirationCallback(func(key string, value interface{}) { | |
208 | t.Logf("expired: %s", key) | |
209 | }) | |
210 | cache.Set("key_1", "value") | |
211 | cache.SetTTL(time.Duration(100 * time.Millisecond)) | |
212 | cache.Set("key_2", "value") | |
213 | <-time.After(150 * time.Millisecond) | |
214 | assert.Equal(t, 1, cache.Count(), "Cache should have only 1 item") | |
215 | } | |
216 | ||
217 | func TestCacheIndividualExpiration(t *testing.T) { | |
218 | cache := NewCache() | |
219 | cache.SetWithTTL("key", "value", time.Duration(100*time.Millisecond)) | |
220 | cache.SetWithTTL("key2", "value", time.Duration(100*time.Millisecond)) | |
221 | cache.SetWithTTL("key3", "value", time.Duration(100*time.Millisecond)) | |
222 | <-time.After(50 * time.Millisecond) | |
223 | assert.Equal(t, cache.Count(), 3, "Should have 3 elements in cache") | |
224 | <-time.After(160 * time.Millisecond) | |
225 | assert.Equal(t, cache.Count(), 0, "Cache should be empty") | |
226 | ||
227 | cache.SetWithTTL("key4", "value", time.Duration(50*time.Millisecond)) | |
228 | <-time.After(100 * time.Millisecond) | |
229 | <-time.After(100 * time.Millisecond) | |
230 | assert.Equal(t, 0, cache.Count(), "Cache should be empty") | |
231 | } | |
232 | ||
233 | func TestCacheGet(t *testing.T) { | |
234 | cache := NewCache() | |
235 | data, exists := cache.Get("hello") | |
236 | assert.Equal(t, exists, false, "Expected empty cache to return no data") | |
237 | assert.Nil(t, data, "Expected data to be empty") | |
238 | ||
239 | cache.Set("hello", "world") | |
240 | data, exists = cache.Get("hello") | |
241 | assert.NotNil(t, data, "Expected data to be not nil") | |
242 | assert.Equal(t, true, exists, "Expected data to exist") | |
243 | assert.Equal(t, "world", (data.(string)), "Expected data content to be 'world'") | |
244 | } | |
245 | ||
246 | func TestCacheExpirationCallbackFunction(t *testing.T) { | |
247 | expiredCount := 0 | |
248 | var lock sync.Mutex | |
249 | ||
250 | cache := NewCache() | |
251 | cache.SetTTL(time.Duration(500 * time.Millisecond)) | |
252 | cache.SetExpirationCallback(func(key string, value interface{}) { | |
253 | lock.Lock() | |
254 | defer lock.Unlock() | |
255 | expiredCount = expiredCount + 1 | |
256 | }) | |
257 | cache.SetWithTTL("key", "value", time.Duration(1000*time.Millisecond)) | |
258 | cache.Set("key_2", "value") | |
259 | <-time.After(1100 * time.Millisecond) | |
260 | ||
261 | lock.Lock() | |
262 | defer lock.Unlock() | |
263 | assert.Equal(t, 2, expiredCount, "Expected 2 items to be expired") | |
264 | } | |
265 | ||
266 | // TestCacheCheckExpirationCallbackFunction should consider that the next entry in the queue | |
267 | // needs to be considered for eviction even if the callback returns no eviction for the current item | |
268 | func TestCacheCheckExpirationCallbackFunction(t *testing.T) { | |
269 | expiredCount := 0 | |
270 | var lock sync.Mutex | |
271 | ||
272 | cache := NewCache() | |
273 | cache.SkipTtlExtensionOnHit(true) | |
274 | cache.SetTTL(time.Duration(50 * time.Millisecond)) | |
275 | cache.SetCheckExpirationCallback(func(key string, value interface{}) bool { | |
276 | if key == "key2" || key == "key4" { | |
277 | return true | |
278 | } | |
279 | return false | |
280 | }) | |
281 | cache.SetExpirationCallback(func(key string, value interface{}) { | |
282 | lock.Lock() | |
283 | expiredCount = expiredCount + 1 | |
284 | lock.Unlock() | |
285 | }) | |
286 | cache.Set("key", "value") | |
287 | cache.Set("key3", "value") | |
288 | cache.Set("key2", "value") | |
289 | cache.Set("key4", "value") | |
290 | ||
291 | <-time.After(110 * time.Millisecond) | |
292 | lock.Lock() | |
293 | assert.Equal(t, 2, expiredCount, "Expected 2 items to be expired") | |
294 | lock.Unlock() | |
295 | } | |
296 | ||
297 | func TestCacheNewItemCallbackFunction(t *testing.T) { | |
298 | newItemCount := 0 | |
299 | cache := NewCache() | |
300 | cache.SetTTL(time.Duration(50 * time.Millisecond)) | |
301 | cache.SetNewItemCallback(func(key string, value interface{}) { | |
302 | newItemCount = newItemCount + 1 | |
303 | }) | |
304 | cache.Set("key", "value") | |
305 | cache.Set("key2", "value") | |
306 | cache.Set("key", "value") | |
307 | <-time.After(110 * time.Millisecond) | |
308 | assert.Equal(t, 2, newItemCount, "Expected only 2 new items") | |
309 | } | |
310 | ||
311 | func TestCacheRemove(t *testing.T) { | |
312 | cache := NewCache() | |
313 | cache.SetTTL(time.Duration(50 * time.Millisecond)) | |
314 | cache.SetWithTTL("key", "value", time.Duration(100*time.Millisecond)) | |
315 | cache.Set("key_2", "value") | |
316 | <-time.After(70 * time.Millisecond) | |
317 | removeKey := cache.Remove("key") | |
318 | removeKey2 := cache.Remove("key_2") | |
319 | assert.Equal(t, true, removeKey, "Expected 'key' to be removed from cache") | |
320 | assert.Equal(t, false, removeKey2, "Expected 'key_2' to already be expired from cache") | |
321 | } | |
322 | ||
323 | func TestCacheSetWithTTLExistItem(t *testing.T) { | |
324 | cache := NewCache() | |
325 | cache.SetTTL(time.Duration(100 * time.Millisecond)) | |
326 | cache.SetWithTTL("key", "value", time.Duration(50*time.Millisecond)) | |
327 | <-time.After(30 * time.Millisecond) | |
328 | cache.SetWithTTL("key", "value2", time.Duration(50*time.Millisecond)) | |
329 | data, exists := cache.Get("key") | |
330 | assert.Equal(t, true, exists, "Expected 'key' to exist") | |
331 | assert.Equal(t, "value2", data.(string), "Expected 'data' to have value 'value2'") | |
332 | } | |
333 | ||
334 | func TestCache_Purge(t *testing.T) { | |
335 | cache := NewCache() | |
336 | cache.SetTTL(time.Duration(100 * time.Millisecond)) | |
337 | ||
338 | for i := 0; i < 5; i++ { | |
339 | ||
340 | cache.SetWithTTL("key", "value", time.Duration(50*time.Millisecond)) | |
341 | <-time.After(30 * time.Millisecond) | |
342 | cache.SetWithTTL("key", "value2", time.Duration(50*time.Millisecond)) | |
343 | cache.Get("key") | |
344 | ||
345 | cache.Purge() | |
346 | assert.Equal(t, 0, cache.Count(), "Cache should be empty") | |
347 | } | |
348 | ||
349 | } | |
350 | ||
351 | func BenchmarkCacheSetWithoutTTL(b *testing.B) { | |
352 | cache := NewCache() | |
353 | for n := 0; n < b.N; n++ { | |
354 | cache.Set(string(n), "value") | |
355 | } | |
356 | } | |
357 | ||
358 | func BenchmarkCacheSetWithGlobalTTL(b *testing.B) { | |
359 | cache := NewCache() | |
360 | cache.SetTTL(time.Duration(50 * time.Millisecond)) | |
361 | for n := 0; n < b.N; n++ { | |
362 | cache.Set(string(n), "value") | |
363 | } | |
364 | } | |
365 | ||
366 | func BenchmarkCacheSetWithTTL(b *testing.B) { | |
367 | cache := NewCache() | |
368 | for n := 0; n < b.N; n++ { | |
369 | cache.SetWithTTL(string(n), "value", time.Duration(50*time.Millisecond)) | |
370 | } | |
371 | } |
0 | package ttlcache | |
1 | ||
2 | import ( | |
3 | "time" | |
4 | ) | |
5 | ||
6 | const ( | |
7 | // ItemNotExpire Will avoid the item being expired by TTL, but can still be exired by callback etc. | |
8 | ItemNotExpire time.Duration = -1 | |
9 | // ItemExpireWithGlobalTTL will use the global TTL when set. | |
10 | ItemExpireWithGlobalTTL time.Duration = 0 | |
11 | ) | |
12 | ||
13 | func newItem(key string, data interface{}, ttl time.Duration) *item { | |
14 | item := &item{ | |
15 | data: data, | |
16 | ttl: ttl, | |
17 | key: key, | |
18 | } | |
19 | // since nobody is aware yet of this item, it's safe to touch without lock here | |
20 | item.touch() | |
21 | return item | |
22 | } | |
23 | ||
24 | type item struct { | |
25 | key string | |
26 | data interface{} | |
27 | ttl time.Duration | |
28 | expireAt time.Time | |
29 | queueIndex int | |
30 | } | |
31 | ||
32 | // Reset the item expiration time | |
33 | func (item *item) touch() { | |
34 | if item.ttl > 0 { | |
35 | item.expireAt = time.Now().Add(item.ttl) | |
36 | } | |
37 | } | |
38 | ||
39 | // Verify if the item is expired | |
40 | func (item *item) expired() bool { | |
41 | if item.ttl <= 0 { | |
42 | return false | |
43 | } | |
44 | return item.expireAt.Before(time.Now()) | |
45 | } |
0 | package ttlcache | |
1 | ||
2 | import ( | |
3 | "testing" | |
4 | "time" | |
5 | ||
6 | "github.com/stretchr/testify/assert" | |
7 | ) | |
8 | ||
9 | func TestItemExpired(t *testing.T) { | |
10 | item := newItem("key", "value", (time.Duration(100) * time.Millisecond)) | |
11 | assert.Equal(t, item.expired(), false, "Expected item to not be expired") | |
12 | <-time.After(200 * time.Millisecond) | |
13 | assert.Equal(t, item.expired(), true, "Expected item to be expired once time has passed") | |
14 | } | |
15 | ||
16 | func TestItemTouch(t *testing.T) { | |
17 | item := newItem("key", "value", (time.Duration(100) * time.Millisecond)) | |
18 | oldExpireAt := item.expireAt | |
19 | <-time.After(50 * time.Millisecond) | |
20 | item.touch() | |
21 | assert.NotEqual(t, oldExpireAt, item.expireAt, "Expected dates to be different") | |
22 | <-time.After(150 * time.Millisecond) | |
23 | assert.Equal(t, item.expired(), true, "Expected item to be expired") | |
24 | item.touch() | |
25 | <-time.After(50 * time.Millisecond) | |
26 | assert.Equal(t, item.expired(), false, "Expected item to not be expired") | |
27 | } | |
28 | ||
29 | func TestItemWithoutExpiration(t *testing.T) { | |
30 | item := newItem("key", "value", ItemNotExpire) | |
31 | <-time.After(50 * time.Millisecond) | |
32 | assert.Equal(t, item.expired(), false, "Expected item to not be expired") | |
33 | } |
0 | package ttlcache | |
1 | ||
2 | import ( | |
3 | "container/heap" | |
4 | ) | |
5 | ||
6 | func newPriorityQueue() *priorityQueue { | |
7 | queue := &priorityQueue{} | |
8 | heap.Init(queue) | |
9 | return queue | |
10 | } | |
11 | ||
12 | type priorityQueue struct { | |
13 | items []*item | |
14 | } | |
15 | ||
16 | func (pq *priorityQueue) update(item *item) { | |
17 | heap.Fix(pq, item.queueIndex) | |
18 | } | |
19 | ||
20 | func (pq *priorityQueue) push(item *item) { | |
21 | heap.Push(pq, item) | |
22 | } | |
23 | ||
24 | func (pq *priorityQueue) pop() *item { | |
25 | if pq.Len() == 0 { | |
26 | return nil | |
27 | } | |
28 | return heap.Pop(pq).(*item) | |
29 | } | |
30 | ||
31 | func (pq *priorityQueue) remove(item *item) { | |
32 | heap.Remove(pq, item.queueIndex) | |
33 | } | |
34 | ||
35 | func (pq priorityQueue) Len() int { | |
36 | length := len(pq.items) | |
37 | return length | |
38 | } | |
39 | ||
40 | // Less will consider items with time.Time default value (epoch start) as more than set items. | |
41 | func (pq priorityQueue) Less(i, j int) bool { | |
42 | if pq.items[i].expireAt.IsZero() { | |
43 | return false | |
44 | } | |
45 | if pq.items[j].expireAt.IsZero() { | |
46 | return true | |
47 | } | |
48 | return pq.items[i].expireAt.Before(pq.items[j].expireAt) | |
49 | } | |
50 | ||
51 | func (pq priorityQueue) Swap(i, j int) { | |
52 | pq.items[i], pq.items[j] = pq.items[j], pq.items[i] | |
53 | pq.items[i].queueIndex = i | |
54 | pq.items[j].queueIndex = j | |
55 | } | |
56 | ||
57 | func (pq *priorityQueue) Push(x interface{}) { | |
58 | item := x.(*item) | |
59 | item.queueIndex = len(pq.items) | |
60 | pq.items = append(pq.items, item) | |
61 | } | |
62 | ||
63 | func (pq *priorityQueue) Pop() interface{} { | |
64 | old := pq.items | |
65 | n := len(old) | |
66 | item := old[n-1] | |
67 | item.queueIndex = -1 | |
68 | pq.items = old[0 : n-1] | |
69 | return item | |
70 | } |
0 | package ttlcache | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | "testing" | |
5 | "time" | |
6 | ||
7 | "github.com/stretchr/testify/assert" | |
8 | ) | |
9 | ||
10 | func TestPriorityQueuePush(t *testing.T) { | |
11 | queue := newPriorityQueue() | |
12 | for i := 0; i < 10; i++ { | |
13 | queue.push(newItem(fmt.Sprintf("key_%d", i), "data", -1)) | |
14 | } | |
15 | assert.Equal(t, queue.Len(), 10, "Expected queue to have 10 elements") | |
16 | } | |
17 | ||
18 | func TestPriorityQueuePop(t *testing.T) { | |
19 | queue := newPriorityQueue() | |
20 | for i := 0; i < 10; i++ { | |
21 | queue.push(newItem(fmt.Sprintf("key_%d", i), "data", -1)) | |
22 | } | |
23 | for i := 0; i < 5; i++ { | |
24 | item := queue.pop() | |
25 | assert.Equal(t, fmt.Sprintf("%T", item), "*ttlcache.item", "Expected 'item' to be a '*ttlcache.item'") | |
26 | } | |
27 | assert.Equal(t, queue.Len(), 5, "Expected queue to have 5 elements") | |
28 | for i := 0; i < 5; i++ { | |
29 | item := queue.pop() | |
30 | assert.Equal(t, fmt.Sprintf("%T", item), "*ttlcache.item", "Expected 'item' to be a '*ttlcache.item'") | |
31 | } | |
32 | assert.Equal(t, queue.Len(), 0, "Expected queue to have 0 elements") | |
33 | ||
34 | item := queue.pop() | |
35 | assert.Nil(t, item, "*ttlcache.item", "Expected 'item' to be nil") | |
36 | } | |
37 | ||
38 | func TestPriorityQueueCheckOrder(t *testing.T) { | |
39 | queue := newPriorityQueue() | |
40 | for i := 10; i > 0; i-- { | |
41 | queue.push(newItem(fmt.Sprintf("key_%d", i), "data", time.Duration(i)*time.Second)) | |
42 | } | |
43 | for i := 1; i <= 10; i++ { | |
44 | item := queue.pop() | |
45 | assert.Equal(t, item.key, fmt.Sprintf("key_%d", i), "error") | |
46 | } | |
47 | } | |
48 | ||
49 | func TestPriorityQueueRemove(t *testing.T) { | |
50 | queue := newPriorityQueue() | |
51 | items := make(map[string]*item) | |
52 | var itemRemove *item | |
53 | for i := 0; i < 5; i++ { | |
54 | key := fmt.Sprintf("key_%d", i) | |
55 | items[key] = newItem(key, "data", time.Duration(i)*time.Second) | |
56 | queue.push(items[key]) | |
57 | ||
58 | if i == 2 { | |
59 | itemRemove = items[key] | |
60 | } | |
61 | } | |
62 | assert.Equal(t, queue.Len(), 5, "Expected queue to have 5 elements") | |
63 | queue.remove(itemRemove) | |
64 | assert.Equal(t, queue.Len(), 4, "Expected queue to have 4 elements") | |
65 | ||
66 | for { | |
67 | item := queue.pop() | |
68 | if item == nil { | |
69 | break | |
70 | } | |
71 | assert.NotEqual(t, itemRemove.key, item.key, "This element was not supose to be in the queue") | |
72 | } | |
73 | ||
74 | assert.Equal(t, queue.Len(), 0, "The queue is supose to be with 0 items") | |
75 | } | |
76 | ||
77 | func TestPriorityQueueUpdate(t *testing.T) { | |
78 | queue := newPriorityQueue() | |
79 | item := newItem("key", "data", 1*time.Second) | |
80 | queue.push(item) | |
81 | assert.Equal(t, queue.Len(), 1, "The queue is supose to be with 1 item") | |
82 | ||
83 | item.key = "newKey" | |
84 | queue.update(item) | |
85 | newItem := queue.pop() | |
86 | assert.Equal(t, newItem.key, "newKey", "The item key didn't change") | |
87 | assert.Equal(t, queue.Len(), 0, "The queue is supose to be with 0 items") | |
88 | } |