New upstream version 2.1.0+ds
Sascha Steinbiss
2 years ago
0 | 0 | language: go |
1 | 1 | |
2 | 2 | go: |
3 | - "1.14" | |
4 | - "1.13" | |
3 | - "1.15.x" | |
4 | - "1.14.x" | |
5 | ||
5 | 6 | git: |
6 | 7 | depth: 1 |
7 | 8 | |
10 | 11 | - go install golang.org/x/tools/cmd/cover |
11 | 12 | - go install golang.org/x/lint/golint |
12 | 13 | - export PATH=$HOME/gopath/bin:$PATH |
14 | - go get golang.org/x/tools/cmd/cover | |
15 | - go get github.com/mattn/goveralls | |
13 | 16 | |
14 | 17 | script: |
15 | 18 | - golint . |
16 | 19 | - go test -cover -race -count=1 -timeout=30s -run . |
17 | - cd bench; go test -run=Bench.* -bench=. -benchmem⏎ | |
20 | - go test -covermode=count -coverprofile=coverage.out -timeout=90s -run . | |
21 | - '[ ! -z "$COVERALLS_TOKEN" ] && $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN' | |
22 | - cd bench; go test -run=Bench.* -bench=. -benchmem; cd ..⏎ |
0 | # 2.1.0 (October 2020) | |
1 | ||
2 | ## API changes | |
3 | ||
4 | * `SetCacheSizeLimit(limit int)` a call was contributed to set a cache limit. #35 | |
5 | ||
0 | 6 | # 2.0.0 (July 2020) |
1 | 7 | |
2 | 8 | ## Fixes #29, #30, #31 |
0 | 0 | # TTLCache - an in-memory cache with expiration |
1 | ||
2 | [](https://pkg.go.dev/github.com/ReneKroon/ttlcache/v2) | |
3 | [](https://github.com/ReneKroon/ttlcache/releases) | |
1 | 4 | |
2 | 5 | TTLCache is a simple key/value cache in golang with the following functions: |
3 | 6 | |
4 | 7 | 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). | |
8 | 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 | 9 | 3. Individual expiring time or global expiring time, you can choose |
7 | 10 | 4. Auto-Extending expiration on `Get` -or- DNS style TTL, see `SkipTTLExtensionOnHit(bool)` |
8 | 11 | 5. Can trigger callback on key expiration |
12 | 15 | 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. |
13 | 16 | |
14 | 17 | [](https://travis-ci.org/ReneKroon/ttlcache) |
18 | [](https://goreportcard.com/report/github.com/ReneKroon/ttlcache) | |
19 | [](https://coveralls.io/github/ReneKroon/ttlcache?branch=master) | |
20 | [](https://github.com/ReneKroon/ttlcache/issues) | |
21 | [](https://github.com/ReneKroon/ttlcache/LICENSE) | |
15 | 22 | |
16 | 23 | ## Usage |
17 | 24 | |
25 | You can copy it as a full standalone demo program. | |
26 | ||
18 | 27 | ```go |
28 | package main | |
29 | ||
19 | 30 | import ( |
20 | "time" | |
21 | "fmt" | |
31 | "fmt" | |
32 | "time" | |
22 | 33 | |
23 | "github.com/ReneKroon/ttlcache" | |
34 | "github.com/ReneKroon/ttlcache/v2" | |
24 | 35 | ) |
25 | 36 | |
26 | func main () { | |
27 | newItemCallback := func(key string, value interface{}) { | |
28 | fmt.Printf("New key(%s) added\n", key) | |
29 | } | |
30 | checkExpirationCallback := func(key string, value interface{}) bool { | |
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 | } | |
39 | expirationCallback := func(key string, value interface{}) { | |
40 | fmt.Printf("This key(%s) has expired\n", key) | |
41 | } | |
37 | var ( | |
38 | notFound = ttlcache.ErrNotFound | |
39 | isClosed = ttlcache.ErrClosed | |
40 | ) | |
42 | 41 | |
43 | loaderFunction := func(key string) (data interface{}, ttl time.Duration, err error) { | |
44 | ttl = time.Second * 300 | |
45 | data, err = getFromNetwork(key) | |
42 | func main() { | |
43 | newItemCallback := func(key string, value interface{}) { | |
44 | fmt.Printf("New key(%s) added\n", key) | |
45 | } | |
46 | checkExpirationCallback := func(key string, value interface{}) bool { | |
47 | if key == "key1" { | |
48 | // if the key equals "key1", the value | |
49 | // will not be allowed to expire | |
50 | return false | |
51 | } | |
52 | // all other values are allowed to expire | |
53 | return true | |
54 | } | |
55 | expirationCallback := func(key string, value interface{}) { | |
56 | fmt.Printf("This key(%s) has expired\n", key) | |
57 | } | |
46 | 58 | |
47 | return data, ttl, err | |
48 | } | |
59 | loaderFunction := func(key string) (data interface{}, ttl time.Duration, err error) { | |
60 | ttl = time.Second * 300 | |
61 | data, err = getFromNetwork(key) | |
49 | 62 | |
50 | cache := ttlcache.NewCache() | |
51 | defer cache.Close() | |
52 | cache.SetTTL(time.Duration(10 * time.Second)) | |
53 | cache.SetExpirationCallback(expirationCallback) | |
54 | cache.SetLoaderFunction(loaderFunction) | |
63 | return data, ttl, err | |
64 | } | |
55 | 65 | |
56 | cache.Set("key", "value") | |
57 | cache.SetWithTTL("keyWithTTL", "value", 10 * time.Second) | |
66 | cache := ttlcache.NewCache() | |
67 | defer cache.Close() | |
68 | cache.SetTTL(time.Duration(10 * time.Second)) | |
69 | cache.SetExpirationCallback(expirationCallback) | |
70 | cache.SetLoaderFunction(loaderFunction) | |
71 | cache.SetNewItemCallback(newItemCallback) | |
72 | cache.SetCheckExpirationCallback(checkExpirationCallback) | |
58 | 73 | |
59 | value, exists := cache.Get("key") | |
60 | count := cache.Count() | |
61 | result := cache.Remove("key") | |
74 | cache.Set("key", "value") | |
75 | cache.SetWithTTL("keyWithTTL", "value", 10*time.Second) | |
76 | ||
77 | if value, exists := cache.Get("key"); exists == nil { | |
78 | fmt.Printf("Got value: %v\n", value) | |
79 | } | |
80 | count := cache.Count() | |
81 | if result := cache.Remove("keyNNN"); result == notFound { | |
82 | fmt.Printf("Not found, %d items left\n", count) | |
83 | } | |
84 | } | |
85 | ||
86 | func getFromNetwork(key string) (string, error) { | |
87 | time.Sleep(time.Millisecond * 30) | |
88 | return "value", nil | |
62 | 89 | } |
63 | 90 | ``` |
64 | 91 | |
78 | 105 | 3. The expiration can be either global or per item |
79 | 106 | 4. Items can exist without expiration time (time.Zero) |
80 | 107 | 5. Expirations and callbacks are realtime. Don't have a pooling time to check anymore, now it's done with a heap. |
108 | 6. A cache count limiter |
30 | 30 | shutdownSignal chan (chan struct{}) |
31 | 31 | isShutDown bool |
32 | 32 | loaderFunction LoaderFunction |
33 | sizeLimit int | |
33 | 34 | } |
34 | 35 | |
35 | 36 | var ( |
165 | 166 | // Close calls Purge after stopping the goroutine that does ttl checking, for a clean shutdown. |
166 | 167 | // The cache is no longer cleaning up after the first call to Close, repeated calls are safe and return ErrClosed. |
167 | 168 | func (cache *Cache) Close() error { |
168 | ||
169 | 169 | cache.mutex.Lock() |
170 | 170 | if !cache.isShutDown { |
171 | 171 | cache.isShutDown = true |
174 | 174 | cache.shutdownSignal <- feedback |
175 | 175 | <-feedback |
176 | 176 | close(cache.shutdownSignal) |
177 | cache.Purge() | |
177 | 178 | } else { |
178 | 179 | cache.mutex.Unlock() |
179 | 180 | return ErrClosed |
180 | 181 | } |
181 | cache.Purge() | |
182 | 182 | return nil |
183 | 183 | } |
184 | 184 | |
200 | 200 | item.data = data |
201 | 201 | item.ttl = ttl |
202 | 202 | } else { |
203 | if cache.sizeLimit != 0 && len(cache.items) >= cache.sizeLimit { | |
204 | cache.removeItem(cache.priorityQueue.items[0]) | |
205 | } | |
203 | 206 | item = newItem(key, data, ttl) |
204 | 207 | cache.items[key] = item |
205 | 208 | } |
240 | 243 | dataToReturn = item.data |
241 | 244 | } |
242 | 245 | |
243 | var err error = nil | |
246 | var err error | |
244 | 247 | if !exists { |
245 | 248 | err = ErrNotFound |
246 | 249 | } |
380 | 383 | cache.items = make(map[string]*item) |
381 | 384 | cache.priorityQueue = newPriorityQueue() |
382 | 385 | return nil |
386 | } | |
387 | ||
388 | // SetCacheSizeLimit sets a limit to the amount of cached items. | |
389 | // If a new item is getting cached, the closes item to being timed out will be replaced | |
390 | // Set to 0 to turn off | |
391 | func (cache *Cache) SetCacheSizeLimit(limit int) { | |
392 | cache.sizeLimit = limit | |
383 | 393 | } |
384 | 394 | |
385 | 395 | // NewCache is a helper to create instance of the Cache struct |
396 | 406 | shutdownSignal: shutdownChan, |
397 | 407 | isShutDown: false, |
398 | 408 | loaderFunction: nil, |
409 | sizeLimit: 0, | |
399 | 410 | } |
400 | 411 | go cache.startExpirationProcessing() |
401 | 412 | return cache |
0 | 0 | package ttlcache_test |
1 | 1 | |
2 | 2 | import ( |
3 | "go.uber.org/goleak" | |
3 | 4 | "math/rand" |
5 | "strconv" | |
4 | 6 | "sync/atomic" |
5 | 7 | "testing" |
6 | 8 | "time" |
7 | ||
8 | "go.uber.org/goleak" | |
9 | 9 | |
10 | 10 | "fmt" |
11 | 11 | "sync" |
366 | 366 | cache := NewCache() |
367 | 367 | defer cache.Close() |
368 | 368 | |
369 | ch := make(chan struct{}, 1024) | |
369 | 370 | cache.SetTTL(time.Second * 1) |
370 | 371 | cache.SetExpirationCallback(func(key string, value interface{}) { |
371 | 372 | t.Logf("This key(%s) has expired\n", key) |
373 | ch <- struct{}{} | |
372 | 374 | }) |
373 | 375 | for i := 0; i < 1024; i++ { |
374 | 376 | cache.Set(fmt.Sprintf("item_%d", i), A{}) |
378 | 380 | |
379 | 381 | if cache.Count() > 100 { |
380 | 382 | t.Fatal("Cache should empty entries >1 second old") |
383 | } | |
384 | ||
385 | expired := 0 | |
386 | for expired != 1024 { | |
387 | <-ch | |
388 | expired++ | |
381 | 389 | } |
382 | 390 | } |
383 | 391 | |
660 | 668 | } |
661 | 669 | |
662 | 670 | } |
671 | ||
672 | func TestCache_Limit(t *testing.T) { | |
673 | t.Parallel() | |
674 | ||
675 | cache := NewCache() | |
676 | defer cache.Close() | |
677 | ||
678 | cache.SetTTL(time.Duration(100 * time.Second)) | |
679 | cache.SetCacheSizeLimit(10) | |
680 | ||
681 | for i := 0; i < 100; i++ { | |
682 | cache.Set("key"+strconv.FormatInt(int64(i), 10), "value") | |
683 | } | |
684 | assert.Equal(t, 10, cache.Count(), "Cache should equal to limit") | |
685 | for i := 90; i < 100; i++ { | |
686 | key := "key" + strconv.FormatInt(int64(i), 10) | |
687 | val, _ := cache.Get(key) | |
688 | assert.Equal(t, "value", val, "Cache should be set [key90, key99]") | |
689 | } | |
690 | } |
5 | 5 | github.com/davecgh/go-spew v1.1.1 // indirect |
6 | 6 | github.com/stretchr/testify v1.6.1 |
7 | 7 | go.uber.org/goleak v1.1.10 |
8 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect | |
9 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d // indirect | |
8 | 10 | ) |
13 | 13 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
14 | 14 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= |
15 | 15 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
16 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | |
16 | 17 | go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= |
17 | 18 | go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= |
18 | 19 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
20 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | |
21 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | |
19 | 22 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= |
20 | 23 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= |
24 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= | |
25 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= | |
26 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= | |
27 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= | |
28 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | |
21 | 29 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
30 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |
22 | 31 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
32 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | |
23 | 33 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
34 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
24 | 35 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
36 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
37 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |
25 | 38 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
26 | 39 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
27 | 40 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E= |
28 | 41 | golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
42 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | |
43 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= | |
44 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d h1:szSOL78iTCl0LF1AMjhSWJj8tIM0KixlUUnBtYXsmd8= | |
45 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= | |
29 | 46 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
47 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
48 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | |
49 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |
30 | 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
31 | 51 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= |
32 | 52 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
68 | 68 | if item == nil { |
69 | 69 | break |
70 | 70 | } |
71 | assert.NotEqual(t, itemRemove.key, item.key, "This element was not supose to be in the queue") | |
71 | assert.NotEqual(t, itemRemove.key, item.key, "This element was not supposed to be in the queue") | |
72 | 72 | } |
73 | 73 | |
74 | assert.Equal(t, queue.Len(), 0, "The queue is supose to be with 0 items") | |
74 | assert.Equal(t, queue.Len(), 0, "The queue is supposed to be with 0 items") | |
75 | 75 | } |
76 | 76 | |
77 | 77 | func TestPriorityQueueUpdate(t *testing.T) { |
78 | 78 | queue := newPriorityQueue() |
79 | 79 | item := newItem("key", "data", 1*time.Second) |
80 | 80 | queue.push(item) |
81 | assert.Equal(t, queue.Len(), 1, "The queue is supose to be with 1 item") | |
81 | assert.Equal(t, queue.Len(), 1, "The queue is supposed to be with 1 item") | |
82 | 82 | |
83 | 83 | item.key = "newKey" |
84 | 84 | queue.update(item) |
85 | 85 | newItem := queue.pop() |
86 | 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") | |
87 | assert.Equal(t, queue.Len(), 0, "The queue is supposed to be with 0 items") | |
88 | 88 | } |