diff --git a/.travis.yml b/.travis.yml index b35391a..616e9de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,18 @@ language: go go: - - 1.2 - - release - - tip + - 1.7 git: depth: 1 install: - go install -race std - - go get code.google.com/p/go.tools/cmd/cover + - go get golang.org/x/tools/cmd/cover - go get github.com/golang/lint/golint - - go get github.com/tools/godep - export PATH=$HOME/gopath/bin:$PATH script: - golint . - - godep go test -race ./... - - godep go test -cover ./... + - go test -race ./... + - go test -cover ./... diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json deleted file mode 100644 index 08af3e4..0000000 --- a/Godeps/Godeps.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "ImportPath": "github.com/wunderlist/ttlcache", - "GoVersion": "go1.2.2", - "Deps": [] -} diff --git a/Godeps/_workspace/.gitignore b/Godeps/_workspace/.gitignore deleted file mode 100644 index f037d68..0000000 --- a/Godeps/_workspace/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/pkg -/bin diff --git a/Readme.md b/Readme.md index 25b88ad..b510bf4 100644 --- a/Readme.md +++ b/Readme.md @@ -1,24 +1,26 @@ -## TTLCache - an in-memory LRU cache with expiration +## TTLCache - an in-memory cache with expiration -TTLCache is a minimal wrapper over a string map in golang, entries of which are +TTLCache is a minimal wrapper over a map of custom in golang, entries of which are 1. Thread-safe 2. Auto-Expiring after a certain time -3. Auto-Extending expiration on `Get`s +3. Managed auto-extending expiration on `Get`s -[![Build Status](https://travis-ci.org/wunderlist/ttlcache.svg)](https://travis-ci.org/wunderlist/ttlcache) +[![Build Status](https://travis-ci.org/ikoroteev/ttlcache.svg)](https://travis-ci.org/ikoroteev/ttlcache) #### Usage ```go import ( "time" - "github.com/wunderlist/ttlcache" + "github.com/ikoroteev/ttlcache" ) func main () { - cache := ttlcache.NewCache(time.Second) - cache.Set("key", "value") - value, exists := cache.Get("key") + cache := ttlcache.NewCache() + cache.Set("key", "value", time.Second) + cache.Set("key1", 24, time.Duration(500) * time.Millisecond) + cache.Set("key3", time.Second, time.Second) + value, exists := cache.Get("key", true) // true - extend cache ttl, otherwise false count := cache.Count() } ``` \ No newline at end of file diff --git a/cache.go b/cache.go index dcee7fd..784751c 100644 --- a/cache.go +++ b/cache.go @@ -7,37 +7,45 @@ import ( // Cache is a synchronised map of items that auto-expire once stale type Cache struct { - mutex sync.RWMutex - ttl time.Duration - items map[string]*Item + mutex sync.RWMutex + items map[string]*Item + counter uint64 } // Set is a thread-safe way to add new items to the map -func (cache *Cache) Set(key string, data string) { +func (cache *Cache) Set(key string, data interface{}, ttl time.Duration) { cache.mutex.Lock() - item := &Item{data: data} - item.touch(cache.ttl) + defer cache.mutex.Unlock() + item := &Item{data: data, ttl: ttl} + item.touch() cache.items[key] = item - cache.mutex.Unlock() } // Get is a thread-safe way to lookup items -// Every lookup, also touches the item, hence extending it's life -func (cache *Cache) Get(key string) (data string, found bool) { +// Every lookup, if touch set to true, touches the item, hence extending it's life +func (cache *Cache) Get(key string, touch bool) (data interface{}, found bool) { cache.mutex.Lock() + defer cache.mutex.Unlock() item, exists := cache.items[key] if !exists || item.expired() { data = "" found = false } else { - item.touch(cache.ttl) + if touch { + item.touch() + } + cache.counter++ data = item.data found = true } - cache.mutex.Unlock() return } +//GetCounter return cache hit count +func (cache *Cache) GetCounter() uint64 { + return cache.counter +} + // Count returns the number of items in the cache // (helpful for tracking memory leaks) func (cache *Cache) Count() int { @@ -49,18 +57,20 @@ func (cache *Cache) Count() int { func (cache *Cache) cleanup() { cache.mutex.Lock() + defer cache.mutex.Unlock() for key, item := range cache.items { if item.expired() { delete(cache.items, key) } } - cache.mutex.Unlock() } func (cache *Cache) startCleanupTimer() { - duration := cache.ttl - if duration < time.Second { - duration = time.Second + duration := time.Second + for i := range cache.items { + if cache.items[i].ttl < time.Second { + duration = cache.items[i].ttl + } } ticker := time.Tick(duration) go (func() { @@ -73,10 +83,25 @@ func (cache *Cache) startCleanupTimer() { })() } +//CleanAll delete all data from cache +func (cache *Cache) CleanAll() { + cache.mutex.Lock() + defer cache.mutex.Unlock() + for key := range cache.items { + delete(cache.items, key) + } +} + +//Delete cache item by key +func (cache *Cache) Delete(key string) { + cache.mutex.Lock() + delete(cache.items, key) + cache.mutex.Unlock() +} + // NewCache is a helper to create instance of the Cache struct -func NewCache(duration time.Duration) *Cache { +func NewCache() *Cache { cache := &Cache{ - ttl: duration, items: map[string]*Item{}, } cache.startCleanupTimer() diff --git a/cache_test.go b/cache_test.go index a49a906..e533873 100644 --- a/cache_test.go +++ b/cache_test.go @@ -7,34 +7,52 @@ import ( func TestGet(t *testing.T) { cache := &Cache{ - ttl: time.Second, items: map[string]*Item{}, } - data, exists := cache.Get("hello") + data, exists := cache.Get("hello", true) if exists || data != "" { t.Errorf("Expected empty cache to return no data") } - cache.Set("hello", "world") - data, exists = cache.Get("hello") + cache.Set("hello", "world", time.Second) + data, exists = cache.Get("hello", true) if !exists { t.Errorf("Expected cache to return data for `hello`") } if data != "world" { t.Errorf("Expected cache to return `world` for `hello`") } + if cache.GetCounter() != 1 { + t.Errorf("Expected cache get counter is equal to 1") + } +} + +func TestDelete(t *testing.T) { + cache := &Cache{ + items: map[string]*Item{}, + } + cache.Set("Test", "Delete", time.Second) + _, exists := cache.Get("Test", true) + if !exists { + t.Errorf("Expected cache to return data for `Test`") + } + cache.Delete("Test") + _, exists = cache.Get("Test", true) + if exists { + t.Errorf("Expected cache to delete data for `Test`") + } + } func TestExpiration(t *testing.T) { cache := &Cache{ - ttl: time.Second, items: map[string]*Item{}, } - cache.Set("x", "1") - cache.Set("y", "z") - cache.Set("z", "3") + cache.Set("x", "1", time.Second) + cache.Set("y", 123, time.Second) + cache.Set("z", time.Second, time.Second) cache.startCleanupTimer() count := cache.Count() @@ -44,7 +62,7 @@ func TestExpiration(t *testing.T) { <-time.After(500 * time.Millisecond) cache.mutex.Lock() - cache.items["y"].touch(time.Second) + cache.items["y"].touch() item, exists := cache.items["x"] cache.mutex.Unlock() if !exists || item.data != "1" || item.expired() { diff --git a/item.go b/item.go index 875ecaa..d44ac2c 100644 --- a/item.go +++ b/item.go @@ -8,13 +8,14 @@ import ( // Item represents a record in the cache map type Item struct { sync.RWMutex - data string + data interface{} expires *time.Time + ttl time.Duration } -func (item *Item) touch(duration time.Duration) { +func (item *Item) touch() { item.Lock() - expiration := time.Now().Add(duration) + expiration := time.Now().Add(item.ttl) item.expires = &expiration item.Unlock() } diff --git a/item_test.go b/item_test.go index ac2323d..61c316d 100644 --- a/item_test.go +++ b/item_test.go @@ -25,8 +25,8 @@ func TestExpired(t *testing.T) { } func TestTouch(t *testing.T) { - item := &Item{data: "blahblah"} - item.touch(time.Second) + item := &Item{data: "blahblah", ttl: time.Second} + item.touch() if item.expired() { t.Errorf("Expected item to not be expired once touched") }