diff --git a/.travis.yml b/.travis.yml index 095be4f..c8f0d21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,9 @@ language: go go: - - "1.14" - - "1.13" + - "1.15.x" + - "1.14.x" + git: depth: 1 @@ -11,8 +12,12 @@ - go install golang.org/x/tools/cmd/cover - go install golang.org/x/lint/golint - export PATH=$HOME/gopath/bin:$PATH + - go get golang.org/x/tools/cmd/cover + - go get github.com/mattn/goveralls script: - golint . - go test -cover -race -count=1 -timeout=30s -run . - - cd bench; go test -run=Bench.* -bench=. -benchmem \ No newline at end of file + - go test -covermode=count -coverprofile=coverage.out -timeout=90s -run . + - '[ ! -z "$COVERALLS_TOKEN" ] && $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN' + - cd bench; go test -run=Bench.* -bench=. -benchmem; cd .. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 54d1d97..d502ac8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2.1.0 (October 2020) + +## API changes + +* `SetCacheSizeLimit(limit int)` a call was contributed to set a cache limit. #35 + # 2.0.0 (July 2020) ## Fixes #29, #30, #31 diff --git a/Readme.md b/Readme.md index 9d12301..42636e1 100644 --- a/Readme.md +++ b/Readme.md @@ -1,9 +1,12 @@ # TTLCache - an in-memory cache with expiration + +[![Documentation](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/ReneKroon/ttlcache/v2) +[![Release](https://img.shields.io/github/release/ReneKroon/ttlcache.svg?label=Release)](https://github.com/ReneKroon/ttlcache/releases) TTLCache is a simple key/value cache in golang with the following functions: 1. Expiration of items based on time, or custom function -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). +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). 3. Individual expiring time or global expiring time, you can choose 4. Auto-Extending expiration on `Get` -or- DNS style TTL, see `SkipTTLExtensionOnHit(bool)` 5. Can trigger callback on key expiration @@ -13,53 +16,77 @@ 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. [![Build Status](https://travis-ci.org/ReneKroon/ttlcache.svg?branch=master)](https://travis-ci.org/ReneKroon/ttlcache) +[![Go Report Card](https://goreportcard.com/badge/github.com/ReneKroon/ttlcache)](https://goreportcard.com/report/github.com/ReneKroon/ttlcache) +[![Coverage Status](https://coveralls.io/repos/github/ReneKroon/ttlcache/badge.svg?branch=master)](https://coveralls.io/github/ReneKroon/ttlcache?branch=master) +[![GitHub issues](https://img.shields.io/github/issues/ReneKroon/ttlcache.svg)](https://github.com/ReneKroon/ttlcache/issues) +[![license](https://img.shields.io/github/license/ReneKroon/ttlcache.svg?maxAge=2592000)](https://github.com/ReneKroon/ttlcache/LICENSE) ## Usage +You can copy it as a full standalone demo program. + ```go +package main + import ( - "time" - "fmt" + "fmt" + "time" - "github.com/ReneKroon/ttlcache" + "github.com/ReneKroon/ttlcache/v2" ) -func main () { - newItemCallback := func(key string, value interface{}) { - fmt.Printf("New key(%s) added\n", key) - } - checkExpirationCallback := func(key string, value interface{}) bool { - if key == "key1" { - // if the key equals "key1", the value - // will not be allowed to expire - return false - } - // all other values are allowed to expire - return true - } - expirationCallback := func(key string, value interface{}) { - fmt.Printf("This key(%s) has expired\n", key) - } +var ( + notFound = ttlcache.ErrNotFound + isClosed = ttlcache.ErrClosed +) - loaderFunction := func(key string) (data interface{}, ttl time.Duration, err error) { - ttl = time.Second * 300 - data, err = getFromNetwork(key) +func main() { + newItemCallback := func(key string, value interface{}) { + fmt.Printf("New key(%s) added\n", key) + } + checkExpirationCallback := func(key string, value interface{}) bool { + if key == "key1" { + // if the key equals "key1", the value + // will not be allowed to expire + return false + } + // all other values are allowed to expire + return true + } + expirationCallback := func(key string, value interface{}) { + fmt.Printf("This key(%s) has expired\n", key) + } - return data, ttl, err - } + loaderFunction := func(key string) (data interface{}, ttl time.Duration, err error) { + ttl = time.Second * 300 + data, err = getFromNetwork(key) - cache := ttlcache.NewCache() - defer cache.Close() - cache.SetTTL(time.Duration(10 * time.Second)) - cache.SetExpirationCallback(expirationCallback) - cache.SetLoaderFunction(loaderFunction) + return data, ttl, err + } - cache.Set("key", "value") - cache.SetWithTTL("keyWithTTL", "value", 10 * time.Second) + cache := ttlcache.NewCache() + defer cache.Close() + cache.SetTTL(time.Duration(10 * time.Second)) + cache.SetExpirationCallback(expirationCallback) + cache.SetLoaderFunction(loaderFunction) + cache.SetNewItemCallback(newItemCallback) + cache.SetCheckExpirationCallback(checkExpirationCallback) - value, exists := cache.Get("key") - count := cache.Count() - result := cache.Remove("key") + cache.Set("key", "value") + cache.SetWithTTL("keyWithTTL", "value", 10*time.Second) + + if value, exists := cache.Get("key"); exists == nil { + fmt.Printf("Got value: %v\n", value) + } + count := cache.Count() + if result := cache.Remove("keyNNN"); result == notFound { + fmt.Printf("Not found, %d items left\n", count) + } +} + +func getFromNetwork(key string) (string, error) { + time.Sleep(time.Millisecond * 30) + return "value", nil } ``` @@ -79,3 +106,4 @@ 3. The expiration can be either global or per item 4. Items can exist without expiration time (time.Zero) 5. Expirations and callbacks are realtime. Don't have a pooling time to check anymore, now it's done with a heap. +6. A cache count limiter diff --git a/cache.go b/cache.go index 28afa8a..29094cc 100644 --- a/cache.go +++ b/cache.go @@ -31,6 +31,7 @@ shutdownSignal chan (chan struct{}) isShutDown bool loaderFunction LoaderFunction + sizeLimit int } var ( @@ -166,7 +167,6 @@ // Close calls Purge after stopping the goroutine that does ttl checking, for a clean shutdown. // The cache is no longer cleaning up after the first call to Close, repeated calls are safe and return ErrClosed. func (cache *Cache) Close() error { - cache.mutex.Lock() if !cache.isShutDown { cache.isShutDown = true @@ -175,11 +175,11 @@ cache.shutdownSignal <- feedback <-feedback close(cache.shutdownSignal) + cache.Purge() } else { cache.mutex.Unlock() return ErrClosed } - cache.Purge() return nil } @@ -201,6 +201,9 @@ item.data = data item.ttl = ttl } else { + if cache.sizeLimit != 0 && len(cache.items) >= cache.sizeLimit { + cache.removeItem(cache.priorityQueue.items[0]) + } item = newItem(key, data, ttl) cache.items[key] = item } @@ -241,7 +244,7 @@ dataToReturn = item.data } - var err error = nil + var err error if !exists { err = ErrNotFound } @@ -381,6 +384,13 @@ cache.items = make(map[string]*item) cache.priorityQueue = newPriorityQueue() return nil +} + +// SetCacheSizeLimit sets a limit to the amount of cached items. +// If a new item is getting cached, the closes item to being timed out will be replaced +// Set to 0 to turn off +func (cache *Cache) SetCacheSizeLimit(limit int) { + cache.sizeLimit = limit } // NewCache is a helper to create instance of the Cache struct @@ -397,6 +407,7 @@ shutdownSignal: shutdownChan, isShutDown: false, loaderFunction: nil, + sizeLimit: 0, } go cache.startExpirationProcessing() return cache diff --git a/cache_test.go b/cache_test.go index ebd5f0d..a062cfc 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1,12 +1,12 @@ package ttlcache_test import ( + "go.uber.org/goleak" "math/rand" + "strconv" "sync/atomic" "testing" "time" - - "go.uber.org/goleak" "fmt" "sync" @@ -367,9 +367,11 @@ cache := NewCache() defer cache.Close() + ch := make(chan struct{}, 1024) cache.SetTTL(time.Second * 1) cache.SetExpirationCallback(func(key string, value interface{}) { t.Logf("This key(%s) has expired\n", key) + ch <- struct{}{} }) for i := 0; i < 1024; i++ { cache.Set(fmt.Sprintf("item_%d", i), A{}) @@ -379,6 +381,12 @@ if cache.Count() > 100 { t.Fatal("Cache should empty entries >1 second old") + } + + expired := 0 + for expired != 1024 { + <-ch + expired++ } } @@ -661,3 +669,23 @@ } } + +func TestCache_Limit(t *testing.T) { + t.Parallel() + + cache := NewCache() + defer cache.Close() + + cache.SetTTL(time.Duration(100 * time.Second)) + cache.SetCacheSizeLimit(10) + + for i := 0; i < 100; i++ { + cache.Set("key"+strconv.FormatInt(int64(i), 10), "value") + } + assert.Equal(t, 10, cache.Count(), "Cache should equal to limit") + for i := 90; i < 100; i++ { + key := "key" + strconv.FormatInt(int64(i), 10) + val, _ := cache.Get(key) + assert.Equal(t, "value", val, "Cache should be set [key90, key99]") + } +} diff --git a/go.mod b/go.mod index 888bce2..6e17f31 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,6 @@ github.com/davecgh/go-spew v1.1.1 // indirect github.com/stretchr/testify v1.6.1 go.uber.org/goleak v1.1.10 + golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect + golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d // indirect ) diff --git a/go.sum b/go.sum index 7940da4..fe55b5a 100644 --- a/go.sum +++ b/go.sum @@ -14,20 +14,40 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d h1:szSOL78iTCl0LF1AMjhSWJj8tIM0KixlUUnBtYXsmd8= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/priority_queue_test.go b/priority_queue_test.go index 0d66207..09a89c0 100644 --- a/priority_queue_test.go +++ b/priority_queue_test.go @@ -69,21 +69,21 @@ if item == nil { break } - assert.NotEqual(t, itemRemove.key, item.key, "This element was not supose to be in the queue") + assert.NotEqual(t, itemRemove.key, item.key, "This element was not supposed to be in the queue") } - assert.Equal(t, queue.Len(), 0, "The queue is supose to be with 0 items") + assert.Equal(t, queue.Len(), 0, "The queue is supposed to be with 0 items") } func TestPriorityQueueUpdate(t *testing.T) { queue := newPriorityQueue() item := newItem("key", "data", 1*time.Second) queue.push(item) - assert.Equal(t, queue.Len(), 1, "The queue is supose to be with 1 item") + assert.Equal(t, queue.Len(), 1, "The queue is supposed to be with 1 item") item.key = "newKey" queue.update(item) newItem := queue.pop() assert.Equal(t, newItem.key, "newKey", "The item key didn't change") - assert.Equal(t, queue.Len(), 0, "The queue is supose to be with 0 items") + assert.Equal(t, queue.Len(), 0, "The queue is supposed to be with 0 items") }