├── errors.go ├── .travis.yml ├── memory_test.go ├── memory_nots_test.go ├── cache.go ├── sharded_cache.go ├── sharded_nots_test.go ├── lru_test.go ├── lru_nots_test.go ├── LICENCE ├── doc.go ├── memory.go ├── memory_nots.go ├── lfu.go ├── lru.go ├── memory_ttl_test.go ├── README.md ├── sharded_nots.go ├── lfu_test.go ├── mongo_model.go ├── sharded_ttl_test.go ├── lfu_nots_test.go ├── lru_nots.go ├── memory_ttl.go ├── sharded_ttl.go ├── helper_test.go ├── mongo_cache.go ├── lfu_nots.go └── mongo_cache_test.go /errors.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNotFound holds exported `not found error` for not found items 7 | ErrNotFound = errors.New("not found") 8 | ) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | services: 6 | - mongodb 7 | 8 | install: 9 | - go get -t -v ./... 10 | 11 | go: 12 | - 1.4.3 13 | - 1.5.4 14 | - 1.6.2 15 | 16 | script: 17 | - export GOMAXPROCS=$(nproc) # go1.4 18 | - go test -race ./... 19 | -------------------------------------------------------------------------------- /memory_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func TestMemoryGetSet(t *testing.T) { 6 | cache := NewMemory() 7 | testCacheGetSet(t, cache) 8 | } 9 | 10 | func TestMemoryDelete(t *testing.T) { 11 | cache := NewMemory() 12 | testCacheDelete(t, cache) 13 | } 14 | 15 | func TestMemoryNilValue(t *testing.T) { 16 | cache := NewMemory() 17 | testCacheNilValue(t, cache) 18 | } 19 | -------------------------------------------------------------------------------- /memory_nots_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func TestMemoryCacheNoTSGetSet(t *testing.T) { 6 | cache := NewMemoryNoTS() 7 | testCacheGetSet(t, cache) 8 | } 9 | 10 | func TestMemoryCacheNoTSDelete(t *testing.T) { 11 | cache := NewMemoryNoTS() 12 | testCacheDelete(t, cache) 13 | } 14 | 15 | func TestMemoryCacheNoTSNilValue(t *testing.T) { 16 | cache := NewMemoryNoTS() 17 | testCacheNilValue(t, cache) 18 | } 19 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | // Cache is the contract for all of the cache backends that are supported by 4 | // this package 5 | type Cache interface { 6 | // Get returns single item from the backend if the requested item is not 7 | // found, returns NotFound err 8 | Get(key string) (interface{}, error) 9 | 10 | // Set sets a single item to the backend 11 | Set(key string, value interface{}) error 12 | 13 | // Delete deletes single item from backend 14 | Delete(key string) error 15 | } 16 | -------------------------------------------------------------------------------- /sharded_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | // ShardedCache is the contract for all of the sharded cache backends that are supported by 4 | // this package 5 | type ShardedCache interface { 6 | // Get returns single item from the backend if the requested item is not 7 | // found, returns NotFound err 8 | Get(shardID, key string) (interface{}, error) 9 | 10 | // Set sets a single item to the backend 11 | Set(shardID, key string, value interface{}) error 12 | 13 | // Delete deletes single item from backend 14 | Delete(shardID, key string) error 15 | 16 | // Deletes all items in that shard 17 | DeleteShard(shardID string) error 18 | } 19 | -------------------------------------------------------------------------------- /sharded_nots_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func TestShardedCacheNoTSGetSet(t *testing.T) { 6 | cache := NewShardedNoTS(NewMemNoTSCache) 7 | testShardedCacheGetSet(t, cache) 8 | } 9 | 10 | func TestShardedCacheNoTSDelete(t *testing.T) { 11 | cache := NewShardedNoTS(NewMemNoTSCache) 12 | testShardedCacheDelete(t, cache) 13 | } 14 | 15 | func TestShardedCacheNoTSNilValue(t *testing.T) { 16 | cache := NewShardedNoTS(NewMemNoTSCache) 17 | testShardedCacheNilValue(t, cache) 18 | } 19 | 20 | func TestShardedCacheNoTSDeleteShard(t *testing.T) { 21 | cache := NewShardedNoTS(NewMemNoTSCache) 22 | testDeleteShard(t, cache) 23 | } 24 | -------------------------------------------------------------------------------- /lru_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func TestLRUGetSet(t *testing.T) { 6 | cache := NewLRU(2) 7 | testCacheGetSet(t, cache) 8 | } 9 | 10 | func TestLRUEviction(t *testing.T) { 11 | cache := NewLRU(2) 12 | testCacheGetSet(t, cache) 13 | 14 | err := cache.Set("test_key3", "test_data3") 15 | if err != nil { 16 | t.Fatal("should not give err while setting item") 17 | } 18 | 19 | _, err = cache.Get("test_key") 20 | if err == nil { 21 | t.Fatal("test_key should not be in the cache") 22 | } 23 | } 24 | 25 | func TestLRUDelete(t *testing.T) { 26 | cache := NewLRU(2) 27 | testCacheDelete(t, cache) 28 | } 29 | 30 | func TestLRUNilValue(t *testing.T) { 31 | cache := NewLRU(2) 32 | testCacheNilValue(t, cache) 33 | } 34 | -------------------------------------------------------------------------------- /lru_nots_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func TestLRUNoTSGetSet(t *testing.T) { 6 | cache := NewLRUNoTS(2) 7 | testCacheGetSet(t, cache) 8 | } 9 | 10 | func TestLRUNoTSEviction(t *testing.T) { 11 | cache := NewLRUNoTS(2) 12 | testCacheGetSet(t, cache) 13 | 14 | err := cache.Set("test_key3", "test_data3") 15 | if err != nil { 16 | t.Fatal("should not give err while setting item") 17 | } 18 | 19 | _, err = cache.Get("test_key") 20 | if err == nil { 21 | t.Fatal("test_key should not be in the cache") 22 | } 23 | } 24 | 25 | func TestLRUNoTSDelete(t *testing.T) { 26 | cache := NewLRUNoTS(2) 27 | testCacheDelete(t, cache) 28 | } 29 | 30 | func TestLRUNoTSNilValue(t *testing.T) { 31 | cache := NewLRUNoTS(2) 32 | testCacheNilValue(t, cache) 33 | } 34 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Koding, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package cache provides basic caching mechanisms for Go(lang) projects. 2 | // 3 | // Currently supported caching algorithms: 4 | // MemoryNoTS : provides a non-thread safe in-memory caching system 5 | // Memory : provides a thread safe in-memory caching system, built on top of MemoryNoTS cache 6 | // LRUNoTS : provides a non-thread safe, fixed size in-memory caching system, built on top of MemoryNoTS cache 7 | // LRU : provides a thread safe, fixed size in-memory caching system, built on top of LRUNoTS cache 8 | // MemoryTTL : provides a thread safe, expiring in-memory caching system, built on top of MemoryNoTS cache 9 | // ShardedNoTS : provides a non-thread safe sharded cache system, built on top of a cache interface 10 | // ShardedTTL : provides a thread safe, expiring in-memory sharded cache system, built on top of ShardedNoTS over MemoryNoTS 11 | // LFUNoTS : provides a non-thread safe, fixed size in-memory caching system, built on top of MemoryNoTS cache 12 | // LFU : provides a thread safe, fixed size in-memory caching system, built on top of LFUNoTS cache 13 | // 14 | package cache 15 | -------------------------------------------------------------------------------- /memory.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "sync" 4 | 5 | // Memory provides an inmemory caching mechanism 6 | type Memory struct { 7 | // Mutex is used for handling the concurrent 8 | // read/write requests for cache 9 | sync.Mutex 10 | 11 | // cache holds the cache data 12 | cache Cache 13 | } 14 | 15 | // NewMemory creates an inmemory cache system 16 | // Which everytime will return the true value about a cache hit 17 | func NewMemory() Cache { 18 | return &Memory{ 19 | cache: NewMemoryNoTS(), 20 | } 21 | } 22 | 23 | // Get returns the value of a given key if it exists 24 | func (r *Memory) Get(key string) (interface{}, error) { 25 | r.Lock() 26 | defer r.Unlock() 27 | 28 | return r.cache.Get(key) 29 | } 30 | 31 | // Set sets a value to the cache or overrides existing one with the given value 32 | func (r *Memory) Set(key string, value interface{}) error { 33 | r.Lock() 34 | defer r.Unlock() 35 | 36 | return r.cache.Set(key, value) 37 | } 38 | 39 | // Delete deletes the given key-value pair from cache, this function doesnt 40 | // return an error if item is not in the cache 41 | func (r *Memory) Delete(key string) error { 42 | r.Lock() 43 | defer r.Unlock() 44 | 45 | return r.cache.Delete(key) 46 | } 47 | -------------------------------------------------------------------------------- /memory_nots.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | // MemoryNoTS provides a non-thread safe caching mechanism 4 | type MemoryNoTS struct { 5 | // items holds the cache data 6 | items map[string]interface{} 7 | } 8 | 9 | // NewMemoryNoTS creates MemoryNoTS struct 10 | func NewMemoryNoTS() *MemoryNoTS { 11 | return &MemoryNoTS{ 12 | items: map[string]interface{}{}, 13 | } 14 | } 15 | 16 | // NewMemNoTSCache is a helper method to return a Cache interface, so callers 17 | // don't have to typecast 18 | func NewMemNoTSCache() Cache { 19 | return NewMemoryNoTS() 20 | } 21 | 22 | // Get returns a value of a given key if it exists 23 | // and valid for the time being 24 | func (r *MemoryNoTS) Get(key string) (interface{}, error) { 25 | value, ok := r.items[key] 26 | if !ok { 27 | return nil, ErrNotFound 28 | } 29 | 30 | return value, nil 31 | } 32 | 33 | // Set will persist a value to the cache or 34 | // override existing one with the new one 35 | func (r *MemoryNoTS) Set(key string, value interface{}) error { 36 | r.items[key] = value 37 | return nil 38 | } 39 | 40 | // Delete deletes a given key, it doesnt return error if the item is not in the 41 | // system 42 | func (r *MemoryNoTS) Delete(key string) error { 43 | delete(r.items, key) 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /lfu.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "sync" 4 | 5 | // LFU holds the Least frequently used cache values 6 | type LFU struct { 7 | // Mutex is used for handling the concurrent 8 | // read/write requests for cache 9 | sync.Mutex 10 | 11 | // cache holds the all cache values 12 | cache Cache 13 | } 14 | 15 | // NewLFU creates a thread-safe LFU cache 16 | func NewLFU(size int) Cache { 17 | return &LRU{ 18 | cache: NewLFUNoTS(size), 19 | } 20 | } 21 | 22 | // Get returns the value of a given key if it exists, every get item will be 23 | // increased for every usage 24 | func (l *LFU) Get(key string) (interface{}, error) { 25 | l.Lock() 26 | defer l.Unlock() 27 | 28 | return l.cache.Get(key) 29 | } 30 | 31 | // Set sets or overrides the given key with the given value, every set item will 32 | // be increased as usage. 33 | // when the cache is full, least frequently used items will be evicted from 34 | // linked list 35 | func (l *LFU) Set(key string, val interface{}) error { 36 | l.Lock() 37 | defer l.Unlock() 38 | 39 | return l.cache.Set(key, val) 40 | } 41 | 42 | // Delete deletes the given key-value pair from cache, this function doesnt 43 | // return an error if item is not in the cache 44 | func (l *LFU) Delete(key string) error { 45 | l.Lock() 46 | defer l.Unlock() 47 | 48 | return l.cache.Delete(key) 49 | } 50 | -------------------------------------------------------------------------------- /lru.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "sync" 4 | 5 | // LRU Discards the least recently used items first. This algorithm 6 | // requires keeping track of what was used when. 7 | type LRU struct { 8 | // Mutex is used for handling the concurrent 9 | // read/write requests for cache 10 | sync.Mutex 11 | 12 | // cache holds the all cache values 13 | cache Cache 14 | } 15 | 16 | // NewLRU creates a thread-safe LRU cache 17 | func NewLRU(size int) Cache { 18 | return &LRU{ 19 | cache: NewLRUNoTS(size), 20 | } 21 | } 22 | 23 | // Get returns the value of a given key if it exists, every get item will be 24 | // moved to the head of the linked list for keeping track of least recent used 25 | // item 26 | func (l *LRU) Get(key string) (interface{}, error) { 27 | l.Lock() 28 | defer l.Unlock() 29 | 30 | return l.cache.Get(key) 31 | } 32 | 33 | // Set sets or overrides the given key with the given value, every set item will 34 | // be moved or prepended to the head of the linked list for keeping track of 35 | // least recent used item. When the cache is full, last item of the linked list 36 | // will be evicted from the cache 37 | func (l *LRU) Set(key string, val interface{}) error { 38 | l.Lock() 39 | defer l.Unlock() 40 | 41 | return l.cache.Set(key, val) 42 | } 43 | 44 | // Delete deletes the given key-value pair from cache, this function doesnt 45 | // return an error if item is not in the cache 46 | func (l *LRU) Delete(key string) error { 47 | l.Lock() 48 | defer l.Unlock() 49 | 50 | return l.cache.Delete(key) 51 | } 52 | -------------------------------------------------------------------------------- /memory_ttl_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestMemoryCacheGetSet(t *testing.T) { 9 | cache := NewMemoryWithTTL(2 * time.Second) 10 | cache.StartGC(time.Millisecond * 10) 11 | defer cache.StopGC() 12 | cache.Set("test_key", "test_data") 13 | data, err := cache.Get("test_key") 14 | if err != nil { 15 | t.Fatal("data not found") 16 | } 17 | if data != "test_data" { 18 | t.Fatal("data is not \"test_data\"") 19 | } 20 | } 21 | 22 | func TestMemoryCacheTTL(t *testing.T) { 23 | cache := NewMemoryWithTTL(100 * time.Millisecond) 24 | cache.StartGC(time.Millisecond * 10) 25 | cache.Set("test_key", "test_data") 26 | time.Sleep(200 * time.Millisecond) 27 | _, err := cache.Get("test_key") 28 | if err == nil { 29 | t.Fatal("data found") 30 | } 31 | } 32 | 33 | func TestMemoryCacheTTLGetExpired(t *testing.T) { 34 | // Needs go test -race to catch problems 35 | cache := NewMemoryWithTTL(1 * time.Millisecond) 36 | cache.Set("test_key", "test_data") 37 | sig := make(chan struct{}) 38 | go func() { 39 | for { 40 | _, _ = cache.Get("test_key") 41 | select { 42 | case _, ok := <-sig: 43 | if !ok { 44 | break 45 | } 46 | default: 47 | } 48 | } 49 | 50 | }() 51 | time.Sleep(20 * time.Millisecond) 52 | _, err := cache.Get("test_key") 53 | if err == nil { 54 | t.Fatal("data found") 55 | } 56 | close(sig) 57 | } 58 | 59 | func TestMemoryCacheTTLNilValue(t *testing.T) { 60 | cache := NewMemoryWithTTL(100 * time.Millisecond) 61 | cache.StartGC(time.Millisecond * 10) 62 | cache.Set("test_key", nil) 63 | data, err := cache.Get("test_key") 64 | if err != nil { 65 | t.Fatal("data found") 66 | } 67 | if data != nil { 68 | t.Fatal("data is not null") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cache [![GoDoc](https://godoc.org/github.com/koding/cache?status.svg)](https://godoc.org/github.com/koding/cache) [![Build Status](https://travis-ci.org/koding/cache.svg?branch=master)](https://travis-ci.org/koding/cache) 2 | 3 | 4 | Cache is a backend provider for common use cases 5 | 6 | ## Install and Usage 7 | 8 | Install the package with: 9 | 10 | ```bash 11 | go get github.com/koding/cache 12 | ``` 13 | 14 | Import it with: 15 | 16 | ```go 17 | import "github.com/koding/cache" 18 | ``` 19 | 20 | 21 | Example 22 | ```go 23 | 24 | // create a cache with 2 second TTL 25 | cache := NewMemoryWithTTL(2 * time.Second) 26 | // start garbage collection for expired keys 27 | cache.StartGC(time.Millisecond * 10) 28 | // set item 29 | err := cache.Set("test_key", "test_data") 30 | // get item 31 | data, err := cache.Get("test_key") 32 | ``` 33 | 34 | 35 | ## Supported caching algorithms: 36 | 37 | - MemoryNoTS : provides a non-thread safe in-memory caching system 38 | - Memory : provides a thread safe in-memory caching system, built on top of MemoryNoTS cache 39 | - LRUNoTS : provides a non-thread safe, fixed size in-memory caching system, built on top of MemoryNoTS cache 40 | - LRU : provides a thread safe, fixed size in-memory caching system, built on top of LRUNoTS cache 41 | - MemoryTTL : provides a thread safe, expiring in-memory caching system, built on top of MemoryNoTS cache 42 | - ShardedNoTS : provides a non-thread safe sharded cache system, built on top of a cache interface 43 | - ShardedTTL : provides a thread safe, expiring in-memory sharded cache system, built on top of ShardedNoTS over MemoryNoTS 44 | - LFUNoTS : provides a non-thread safe, fixed size in-memory caching system, built on top of MemoryNoTS cache 45 | - LFU : provides a thread safe, fixed size in-memory caching system, built on top of LFUNoTS cache 46 | -------------------------------------------------------------------------------- /sharded_nots.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | // ShardedNoTS ; the concept behind this storage is that each cache entry is 4 | // associated with a tenantID and this enables fast purging for just that 5 | // tenantID 6 | type ShardedNoTS struct { 7 | cache map[string]Cache 8 | itemCount map[string]int 9 | constructor func() Cache 10 | } 11 | 12 | // NewShardedNoTS inits ShardedNoTS struct 13 | func NewShardedNoTS(c func() Cache) *ShardedNoTS { 14 | return &ShardedNoTS{ 15 | constructor: c, 16 | cache: make(map[string]Cache), 17 | itemCount: make(map[string]int), 18 | } 19 | } 20 | 21 | // Get returns a value of a given key if it exists 22 | // and valid for the time being 23 | func (l *ShardedNoTS) Get(tenantID, key string) (interface{}, error) { 24 | cache, ok := l.cache[tenantID] 25 | if !ok { 26 | return nil, ErrNotFound 27 | } 28 | 29 | return cache.Get(key) 30 | } 31 | 32 | // Set will persist a value to the cache or override existing one with the new 33 | // one 34 | func (l *ShardedNoTS) Set(tenantID, key string, val interface{}) error { 35 | _, ok := l.cache[tenantID] 36 | if !ok { 37 | l.cache[tenantID] = l.constructor() 38 | l.itemCount[tenantID] = 0 39 | } 40 | 41 | l.itemCount[tenantID]++ 42 | return l.cache[tenantID].Set(key, val) 43 | } 44 | 45 | // Delete deletes a given key 46 | func (l *ShardedNoTS) Delete(tenantID, key string) error { 47 | _, ok := l.cache[tenantID] 48 | if !ok { 49 | return nil 50 | } 51 | 52 | l.itemCount[tenantID]-- 53 | 54 | if l.itemCount[tenantID] == 0 { 55 | return l.DeleteShard(tenantID) 56 | } 57 | 58 | return l.cache[tenantID].Delete(key) 59 | } 60 | 61 | // DeleteShard deletes the keys inside from maps of cache & itemCount 62 | func (l *ShardedNoTS) DeleteShard(tenantID string) error { 63 | delete(l.cache, tenantID) 64 | delete(l.itemCount, tenantID) 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /lfu_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func TestLFUNGetSet(t *testing.T) { 6 | cache := NewLFU(2) 7 | testCacheGetSet(t, cache) 8 | } 9 | 10 | func TestLFUDelete(t *testing.T) { 11 | cache := NewLFU(2) 12 | testCacheDelete(t, cache) 13 | } 14 | 15 | func TestLFUNilValue(t *testing.T) { 16 | cache := NewLFU(2) 17 | testCacheNilValue(t, cache) 18 | } 19 | 20 | func TestLFUEviction(t *testing.T) { 21 | cache := NewLFU(2) 22 | testCacheGetSet(t, cache) 23 | 24 | _, err := cache.Get("test_key2") 25 | if err != nil { 26 | t.Fatal("test_key2 should be in the cache") 27 | } 28 | // get-> test_key should not be in cache after insert test_key3 29 | err = cache.Set("test_key3", "test_data3") 30 | if err != nil { 31 | t.Fatal("should not give err while setting item") 32 | } 33 | 34 | _, err = cache.Get("test_key") 35 | if err == nil { 36 | t.Fatal("test_key should not be in the cache") 37 | } 38 | } 39 | 40 | // 41 | // BENCHMARK OPERATIONS 42 | // 43 | 44 | func BenchmarkLFUSet1000(b *testing.B) { 45 | cache := NewLFU(5) 46 | for n := 0; n < b.N; n++ { 47 | cache.Set("keyBench", "test_data") 48 | } 49 | } 50 | func BenchmarkLFUGet1000(b *testing.B) { 51 | cache := NewLFU(5) 52 | cache.Set("keyBench", "test") 53 | for n := 0; n < b.N; n++ { 54 | cache.Get("keyBench") 55 | } 56 | } 57 | func BenchmarkLFUDelete1000(b *testing.B) { 58 | cache := NewLFU(5) 59 | cache.Set("keyBench", "test") 60 | for n := 0; n < b.N; n++ { 61 | cache.Delete("keyBench") 62 | } 63 | } 64 | func BenchmarkLFUSetDelete1000(b *testing.B) { 65 | cache := NewLFU(5) 66 | for n := 0; n < b.N; n++ { 67 | cache.Set("keyBench", "test_data") 68 | cache.Delete("keyBench") 69 | } 70 | } 71 | func BenchmarkLFUSetGet1000(b *testing.B) { 72 | cache := NewLFU(5) 73 | for n := 0; n < b.N; n++ { 74 | cache.Set("keyBench", "set_data") 75 | cache.Get("keyBench") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /mongo_model.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | mgo "gopkg.in/mgo.v2" 7 | "gopkg.in/mgo.v2/bson" 8 | ) 9 | 10 | // Document holds the key-value pair for mongo cache 11 | type Document struct { 12 | Key string `bson:"_id" json:"_id"` 13 | Value interface{} `bson:"value" json:"value"` 14 | ExpireAt time.Time `bson:"expireAt" json:"expireAt"` 15 | } 16 | 17 | // getKey fetches the key with its key 18 | func (m *MongoCache) get(key string) (*Document, error) { 19 | keyValue := new(Document) 20 | 21 | query := func(c *mgo.Collection) error { 22 | return c.Find(bson.M{ 23 | "_id": key, 24 | "expireAt": bson.M{ 25 | "$gt": time.Now().UTC(), 26 | }}).One(&keyValue) 27 | } 28 | 29 | err := m.run(m.CollectionName, query) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return keyValue, nil 35 | } 36 | 37 | func (m *MongoCache) set(key string, duration time.Duration, value interface{}) error { 38 | update := bson.M{ 39 | "_id": key, 40 | "value": value, 41 | "expireAt": time.Now().Add(duration), 42 | } 43 | 44 | query := func(c *mgo.Collection) error { 45 | _, err := c.UpsertId(key, update) 46 | return err 47 | } 48 | 49 | return m.run(m.CollectionName, query) 50 | } 51 | 52 | // deleteKey removes the key-value from mongoDB 53 | func (m *MongoCache) delete(key string) error { 54 | query := func(c *mgo.Collection) error { 55 | err := c.RemoveId(key) 56 | return err 57 | } 58 | 59 | return m.run(m.CollectionName, query) 60 | } 61 | 62 | func (m *MongoCache) deleteExpiredKeys() error { 63 | var selector = bson.M{"expireAt": bson.M{ 64 | "$lte": time.Now().UTC(), 65 | }} 66 | 67 | query := func(c *mgo.Collection) error { 68 | _, err := c.RemoveAll(selector) 69 | return err 70 | } 71 | 72 | return m.run(m.CollectionName, query) 73 | } 74 | 75 | func (m *MongoCache) run(collection string, s func(*mgo.Collection) error) error { 76 | session := m.mongeSession.Copy() 77 | defer session.Close() 78 | 79 | c := session.DB("").C(collection) 80 | return s(c) 81 | } 82 | -------------------------------------------------------------------------------- /sharded_ttl_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestShardedCacheGetSet(t *testing.T) { 9 | cache := NewShardedWithTTL(2 * time.Second) 10 | cache.StartGC(time.Millisecond * 10) 11 | cache.Set("user1", "test_key", "test_data") 12 | data, err := cache.Get("user1", "test_key") 13 | if err != nil { 14 | t.Fatal("data not found") 15 | } 16 | if data != "test_data" { 17 | t.Fatal("data is not \"test_data\"") 18 | } 19 | } 20 | 21 | func TestShardedCacheTTL(t *testing.T) { 22 | cache := NewShardedWithTTL(100 * time.Millisecond) 23 | cache.StartGC(time.Millisecond * 10) 24 | cache.Set("user1", "test_key", "test_data") 25 | time.Sleep(200 * time.Millisecond) 26 | _, err := cache.Get("user1", "test_key") 27 | if err == nil { 28 | t.Fatal("data found") 29 | } 30 | } 31 | 32 | func TestShardedCacheTTLGetExpired(t *testing.T) { 33 | // Needs go test -race to catch problems 34 | cache := NewShardedWithTTL(1 * time.Millisecond) 35 | cache.Set("user1", "test_key", "test_data") 36 | sig := make(chan struct{}) 37 | go func() { 38 | for { 39 | _, _ = cache.Get("user1", "test_key") 40 | select { 41 | case _, ok := <-sig: 42 | if !ok { 43 | break 44 | } 45 | default: 46 | } 47 | } 48 | 49 | }() 50 | time.Sleep(20 * time.Millisecond) 51 | _, err := cache.Get("user1", "test_key") 52 | if err == nil { 53 | t.Fatal("data found") 54 | } 55 | close(sig) 56 | } 57 | 58 | func TestShardedCacheTTLNilValue(t *testing.T) { 59 | cache := NewShardedWithTTL(100 * time.Millisecond) 60 | cache.StartGC(time.Millisecond * 10) 61 | cache.Set("user1", "test_key", nil) 62 | data, err := cache.Get("user1", "test_key") 63 | if err != nil { 64 | t.Fatal("data found") 65 | } 66 | if data != nil { 67 | t.Fatal("data is not null") 68 | } 69 | } 70 | 71 | func TestShardedCacheTTLDeleteShard(t *testing.T) { 72 | cache := NewShardedWithTTL(100 * time.Millisecond) 73 | cache.StartGC(time.Millisecond * 10) 74 | cache.Set("user1", "test_key", nil) 75 | cache.DeleteShard("user1") 76 | _, err := cache.Get("user1", "test_key") 77 | if err == nil { 78 | t.Fatal("data found") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lfu_nots_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func TestLFUNoTSGetSet(t *testing.T) { 6 | cache := NewLFUNoTS(2) 7 | testCacheGetSet(t, cache) 8 | } 9 | 10 | func TestLFUNoTSUsageWithSet(t *testing.T) { 11 | cache := NewLFUNoTS(2) 12 | cache.Set("test_key1", "test_data") 13 | cache.Set("test_key2", "test_data2") 14 | cache.Set("test_key2", "test_data2") 15 | cache.Set("test_key3", "test_data3") 16 | cache.Set("test_key3", "test_data3") 17 | cache.Set("test_key3", "test_data3") 18 | 19 | // test_key3 is used 3 times 20 | // test_key2 is used 2 times 21 | // test_key1 is used 1 times 22 | _, err := cache.Get("test_key1") 23 | if err != ErrNotFound { 24 | t.Fatal("test_key1 should not be in the cache") 25 | } 26 | data, err := cache.Get("test_key2") 27 | if err != nil { 28 | t.Fatal("test_key2 should be in the cache") 29 | } 30 | 31 | if data != "test_data2" { 32 | t.Fatal("data should be equal test_data2") 33 | } 34 | 35 | data, err = cache.Get("test_key3") 36 | if err != nil { 37 | t.Fatal("test_key3 should be in the cache") 38 | } 39 | 40 | if data != "test_data3" { 41 | t.Fatal("data should be equal test_data3") 42 | } 43 | } 44 | 45 | func TestLFUNoTSUsageWithSetAndGet(t *testing.T) { 46 | cache := NewLFUNoTS(3) 47 | cache.Set("test_key1", "test_data1") 48 | cache.Set("test_key2", "test_data2") 49 | cache.Set("test_key3", "test_data3") 50 | 51 | _, err := cache.Get("test_key1") 52 | if err != nil { 53 | t.Fatal("test_key1 should not in the cache") 54 | } 55 | _, err = cache.Get("test_key2") 56 | if err != nil { 57 | t.Fatal("test_key2 should not in the cache") 58 | } 59 | _, err = cache.Get("test_key2") 60 | if err != nil { 61 | t.Fatal("test_key2 should be in the cache") 62 | } 63 | _, err = cache.Get("test_key3") 64 | if err != nil { 65 | t.Fatal("test_key3 should be in the cache") 66 | } 67 | _, err = cache.Get("test_key3") 68 | if err != nil { 69 | t.Fatal("test_key3 should be in the cache") 70 | } 71 | _, err = cache.Get("test_key3") 72 | if err != nil { 73 | t.Fatal("test_key3 should be in the cache") 74 | } 75 | // set test_key4 into cache list 76 | // test_key1 should not be in cache list 77 | if err = cache.Set("test_key4", "test_data4"); err != nil { 78 | t.Fatal("test_key4 should be set") 79 | } 80 | 81 | _, err = cache.Get("test_key1") 82 | if err != ErrNotFound { 83 | t.Fatal("test_key1 should not be in the cache") 84 | } 85 | 86 | data, err := cache.Get("test_key4") 87 | if err != nil { 88 | t.Fatal("test_key4 should be in the cache") 89 | } 90 | 91 | if data != "test_data4" { 92 | t.Fatal("data should be equal to test_data4") 93 | } 94 | } 95 | 96 | func TestLFUNoTSDelete(t *testing.T) { 97 | cache := NewLFUNoTS(3) 98 | cache.Set("test_key1", "test_data1") 99 | cache.Set("test_key2", "test_data2") 100 | cache.Set("test_key3", "test_data3") 101 | _, err := cache.Get("test_key1") 102 | if err != nil { 103 | t.Fatal("test_key1 should be in the cache") 104 | } 105 | _, err = cache.Get("test_key2") 106 | if err != nil { 107 | t.Fatal("test_key2 should be in the cache") 108 | } 109 | _, err = cache.Get("test_key3") 110 | if err != nil { 111 | t.Fatal("test_key3 should be in the cache") 112 | } 113 | if err = cache.Delete("test_key1"); err != nil { 114 | t.Fatal("test_key1 should be deleted") 115 | } 116 | 117 | _, err = cache.Get("test_key1") 118 | if err != ErrNotFound { 119 | t.Fatal("test_key1 should not be in the cache") 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lru_nots.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "container/list" 5 | ) 6 | 7 | // LRUNoTS Discards the least recently used items first. This algorithm 8 | // requires keeping track of what was used when. 9 | type LRUNoTS struct { 10 | // list holds all items in a linked list, for finding the `tail` of the list 11 | list *list.List 12 | 13 | // cache holds the all cache values 14 | cache Cache 15 | 16 | // size holds the limit of the LRU cache 17 | size int 18 | } 19 | 20 | // kv is an helper struct for keeping track of the key for the list item. Only 21 | // place where we need the key of a value is while removing the last item from 22 | // linked list, for other cases, all operations alread have the key 23 | type kv struct { 24 | k string 25 | v interface{} 26 | } 27 | 28 | // NewLRUNoTS creates a new LRU cache struct for further cache operations. Size 29 | // is used for limiting the upper bound of the cache 30 | func NewLRUNoTS(size int) Cache { 31 | if size < 1 { 32 | panic("invalid cache size") 33 | } 34 | 35 | return &LRUNoTS{ 36 | list: list.New(), 37 | cache: NewMemoryNoTS(), 38 | size: size, 39 | } 40 | } 41 | 42 | // Get returns the value of a given key if it exists, every get item will be 43 | // moved to the head of the linked list for keeping track of least recent used 44 | // item 45 | func (l *LRUNoTS) Get(key string) (interface{}, error) { 46 | res, err := l.cache.Get(key) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | elem := res.(*list.Element) 52 | // move found item to the head 53 | l.list.MoveToFront(elem) 54 | 55 | return elem.Value.(*kv).v, nil 56 | } 57 | 58 | // Set sets or overrides the given key with the given value, every set item will 59 | // be moved or prepended to the head of the linked list for keeping track of 60 | // least recent used item. When the cache is full, last item of the linked list 61 | // will be evicted from the cache 62 | func (l *LRUNoTS) Set(key string, val interface{}) error { 63 | // try to get item 64 | res, err := l.cache.Get(key) 65 | if err != nil && err != ErrNotFound { 66 | return err 67 | } 68 | 69 | var elem *list.Element 70 | 71 | // if elem is not in the cache, push it to front of the list 72 | if err == ErrNotFound { 73 | elem = l.list.PushFront(&kv{k: key, v: val}) 74 | } else { 75 | // if elem is in the cache, update the data and move it the front 76 | elem = res.(*list.Element) 77 | 78 | // update the data 79 | elem.Value.(*kv).v = val 80 | 81 | // item already exists, so move it to the front of the list 82 | l.list.MoveToFront(elem) 83 | } 84 | 85 | // in any case, set the item to the cache 86 | err = l.cache.Set(key, elem) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | // if the cache is full, evict last entry 92 | if l.list.Len() > l.size { 93 | // remove last element from cache 94 | return l.removeElem(l.list.Back()) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | // Delete deletes the given key-value pair from cache, this function doesnt 101 | // return an error if item is not in the cache 102 | func (l *LRUNoTS) Delete(key string) error { 103 | res, err := l.cache.Get(key) 104 | if err != nil && err != ErrNotFound { 105 | return err 106 | } 107 | 108 | // item already deleted 109 | if err == ErrNotFound { 110 | // surpress not found errors 111 | return nil 112 | } 113 | 114 | elem := res.(*list.Element) 115 | 116 | return l.removeElem(elem) 117 | } 118 | 119 | func (l *LRUNoTS) removeElem(e *list.Element) error { 120 | l.list.Remove(e) 121 | return l.cache.Delete(e.Value.(*kv).k) 122 | } 123 | -------------------------------------------------------------------------------- /memory_ttl.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | var zeroTTL = time.Duration(0) 9 | 10 | // MemoryTTL holds the required variables to compose an in memory cache system 11 | // which also provides expiring key mechanism 12 | type MemoryTTL struct { 13 | // Mutex is used for handling the concurrent 14 | // read/write requests for cache 15 | sync.RWMutex 16 | 17 | // cache holds the cache data 18 | cache *MemoryNoTS 19 | 20 | // setAts holds the time that related item's set at 21 | setAts map[string]time.Time 22 | 23 | // ttl is a duration for a cache key to expire 24 | ttl time.Duration 25 | 26 | // gcTicker controls gc intervals 27 | gcTicker *time.Ticker 28 | 29 | // done controls sweeping goroutine lifetime 30 | done chan struct{} 31 | } 32 | 33 | // NewMemoryWithTTL creates an inmemory cache system 34 | // Which everytime will return the true values about a cache hit 35 | // and never will leak memory 36 | // ttl is used for expiration of a key from cache 37 | func NewMemoryWithTTL(ttl time.Duration) *MemoryTTL { 38 | return &MemoryTTL{ 39 | cache: NewMemoryNoTS(), 40 | setAts: map[string]time.Time{}, 41 | ttl: ttl, 42 | } 43 | } 44 | 45 | // StartGC starts the garbage collection process in a go routine 46 | func (r *MemoryTTL) StartGC(gcInterval time.Duration) { 47 | if gcInterval <= 0 { 48 | return 49 | } 50 | 51 | ticker := time.NewTicker(gcInterval) 52 | done := make(chan struct{}) 53 | 54 | r.Lock() 55 | r.gcTicker = ticker 56 | r.done = done 57 | r.Unlock() 58 | 59 | go func() { 60 | for { 61 | select { 62 | case <-ticker.C: 63 | now := time.Now() 64 | 65 | r.Lock() 66 | for key := range r.cache.items { 67 | if !r.isValidTime(key, now) { 68 | r.delete(key) 69 | } 70 | } 71 | r.Unlock() 72 | case <-done: 73 | return 74 | } 75 | } 76 | }() 77 | } 78 | 79 | // StopGC stops sweeping goroutine. 80 | func (r *MemoryTTL) StopGC() { 81 | if r.gcTicker != nil { 82 | r.Lock() 83 | r.gcTicker.Stop() 84 | r.gcTicker = nil 85 | close(r.done) 86 | r.done = nil 87 | r.Unlock() 88 | } 89 | } 90 | 91 | // Get returns a value of a given key if it exists 92 | // and valid for the time being 93 | func (r *MemoryTTL) Get(key string) (interface{}, error) { 94 | r.RLock() 95 | 96 | for !r.isValid(key) { 97 | r.RUnlock() 98 | // Need write lock to delete key, so need to unlock, relock and recheck 99 | r.Lock() 100 | if !r.isValid(key) { 101 | r.delete(key) 102 | r.Unlock() 103 | return nil, ErrNotFound 104 | } 105 | r.Unlock() 106 | // Could become invalid again in this window 107 | r.RLock() 108 | } 109 | 110 | defer r.RUnlock() 111 | 112 | value, err := r.cache.Get(key) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | return value, nil 118 | } 119 | 120 | // Set will persist a value to the cache or 121 | // override existing one with the new one 122 | func (r *MemoryTTL) Set(key string, value interface{}) error { 123 | r.Lock() 124 | defer r.Unlock() 125 | 126 | r.cache.Set(key, value) 127 | r.setAts[key] = time.Now() 128 | return nil 129 | } 130 | 131 | // Delete deletes a given key if exists 132 | func (r *MemoryTTL) Delete(key string) error { 133 | r.Lock() 134 | defer r.Unlock() 135 | 136 | r.delete(key) 137 | return nil 138 | } 139 | 140 | func (r *MemoryTTL) delete(key string) { 141 | r.cache.Delete(key) 142 | delete(r.setAts, key) 143 | } 144 | 145 | func (r *MemoryTTL) isValid(key string) bool { 146 | return r.isValidTime(key, time.Now()) 147 | } 148 | 149 | func (r *MemoryTTL) isValidTime(key string, t time.Time) bool { 150 | setAt, ok := r.setAts[key] 151 | if !ok { 152 | return false 153 | } 154 | 155 | if r.ttl == zeroTTL { 156 | return true 157 | } 158 | 159 | return setAt.Add(r.ttl).After(t) 160 | } 161 | -------------------------------------------------------------------------------- /sharded_ttl.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // ShardedTTL holds the required variables to compose an in memory sharded cache system 9 | // which also provides expiring key mechanism 10 | type ShardedTTL struct { 11 | // Mutex is used for handling the concurrent 12 | // read/write requests for cache 13 | sync.Mutex 14 | 15 | // cache holds the cache data 16 | cache ShardedCache 17 | 18 | // setAts holds the time that related item's set at, indexed by tenantID 19 | setAts map[string]map[string]time.Time 20 | 21 | // ttl is a duration for a cache key to expire 22 | ttl time.Duration 23 | 24 | // gcInterval is a duration for garbage collection 25 | gcInterval time.Duration 26 | } 27 | 28 | // NewShardedCacheWithTTL creates a sharded cache system with TTL based on specified Cache constructor 29 | // Which everytime will return the true values about a cache hit 30 | // and never will leak memory 31 | // ttl is used for expiration of a key from cache 32 | func NewShardedCacheWithTTL(ttl time.Duration, f func() Cache) *ShardedTTL { 33 | return &ShardedTTL{ 34 | cache: NewShardedNoTS(f), 35 | setAts: map[string]map[string]time.Time{}, 36 | ttl: ttl, 37 | } 38 | } 39 | 40 | // NewShardedWithTTL creates an in-memory sharded cache system 41 | // ttl is used for expiration of a key from cache 42 | func NewShardedWithTTL(ttl time.Duration) *ShardedTTL { 43 | return NewShardedCacheWithTTL(ttl, NewMemNoTSCache) 44 | } 45 | 46 | // StartGC starts the garbage collection process in a go routine 47 | func (r *ShardedTTL) StartGC(gcInterval time.Duration) { 48 | r.gcInterval = gcInterval 49 | go func() { 50 | for _ = range time.Tick(gcInterval) { 51 | r.Lock() 52 | for tenantID := range r.setAts { 53 | for key := range r.setAts[tenantID] { 54 | if !r.isValid(tenantID, key) { 55 | r.delete(tenantID, key) 56 | } 57 | } 58 | } 59 | r.Unlock() 60 | } 61 | }() 62 | } 63 | 64 | // Get returns a value of a given key if it exists 65 | // and valid for the time being 66 | func (r *ShardedTTL) Get(tenantID, key string) (interface{}, error) { 67 | r.Lock() 68 | defer r.Unlock() 69 | 70 | if !r.isValid(tenantID, key) { 71 | r.delete(tenantID, key) 72 | return nil, ErrNotFound 73 | } 74 | 75 | value, err := r.cache.Get(tenantID, key) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | return value, nil 81 | } 82 | 83 | // Set will persist a value to the cache or 84 | // override existing one with the new one 85 | func (r *ShardedTTL) Set(tenantID, key string, value interface{}) error { 86 | r.Lock() 87 | defer r.Unlock() 88 | 89 | r.cache.Set(tenantID, key, value) 90 | _, ok := r.setAts[tenantID] 91 | if !ok { 92 | r.setAts[tenantID] = make(map[string]time.Time) 93 | } 94 | r.setAts[tenantID][key] = time.Now() 95 | return nil 96 | } 97 | 98 | // Delete deletes a given key if exists 99 | func (r *ShardedTTL) Delete(tenantID, key string) error { 100 | r.Lock() 101 | defer r.Unlock() 102 | 103 | r.delete(tenantID, key) 104 | return nil 105 | } 106 | 107 | func (r *ShardedTTL) delete(tenantID, key string) { 108 | _, ok := r.setAts[tenantID] 109 | if !ok { 110 | return 111 | } 112 | r.cache.Delete(tenantID, key) 113 | delete(r.setAts[tenantID], key) 114 | if len(r.setAts[tenantID]) == 0 { 115 | delete(r.setAts, tenantID) 116 | } 117 | } 118 | 119 | func (r *ShardedTTL) isValid(tenantID, key string) bool { 120 | 121 | _, ok := r.setAts[tenantID] 122 | if !ok { 123 | return false 124 | } 125 | setAt, ok := r.setAts[tenantID][key] 126 | if !ok { 127 | return false 128 | } 129 | if r.ttl == zeroTTL { 130 | return true 131 | } 132 | 133 | return setAt.Add(r.ttl).After(time.Now()) 134 | } 135 | 136 | // DeleteShard deletes with given tenantID without key 137 | func (r *ShardedTTL) DeleteShard(tenantID string) error { 138 | r.Lock() 139 | defer r.Unlock() 140 | 141 | _, ok := r.setAts[tenantID] 142 | if ok { 143 | for key := range r.setAts[tenantID] { 144 | r.delete(tenantID, key) 145 | } 146 | } 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func testCacheGetSet(t *testing.T, cache Cache) { 6 | err := cache.Set("test_key", "test_data") 7 | if err != nil { 8 | t.Fatal("should not give err while setting item") 9 | } 10 | 11 | err = cache.Set("test_key2", "test_data2") 12 | if err != nil { 13 | t.Fatal("should not give err while setting item") 14 | } 15 | 16 | data, err := cache.Get("test_key") 17 | if err != nil { 18 | t.Fatal("test_key should be in the cache") 19 | } 20 | 21 | if data != "test_data" { 22 | t.Fatal("data is not \"test_data\"") 23 | } 24 | 25 | data, err = cache.Get("test_key2") 26 | if err != nil { 27 | t.Fatal("test_key2 should be in the cache") 28 | } 29 | 30 | if data != "test_data2" { 31 | t.Fatal("data is not \"test_data2\"") 32 | } 33 | } 34 | 35 | func testCacheNilValue(t *testing.T, cache Cache) { 36 | err := cache.Set("test_key", nil) 37 | if err != nil { 38 | t.Fatal("should not give err while setting item") 39 | } 40 | 41 | data, err := cache.Get("test_key") 42 | if err != nil { 43 | t.Fatal("test_key should be in the cache") 44 | } 45 | 46 | if data != nil { 47 | t.Fatal("data is not nil") 48 | } 49 | 50 | err = cache.Delete("test_key") 51 | if err != nil { 52 | t.Fatal("should not give err while deleting item") 53 | } 54 | 55 | _, err = cache.Get("test_key") 56 | if err == nil { 57 | t.Fatal("test_key should not be in the cache") 58 | } 59 | } 60 | 61 | func testCacheDelete(t *testing.T, cache Cache) { 62 | cache.Set("test_key", "test_data") 63 | cache.Set("test_key2", "test_data2") 64 | 65 | err := cache.Delete("test_key3") 66 | if err != nil { 67 | t.Fatal("non-exiting item should not give error") 68 | } 69 | 70 | err = cache.Delete("test_key") 71 | if err != nil { 72 | t.Fatal("exiting item should not give error") 73 | } 74 | 75 | data, err := cache.Get("test_key") 76 | if err != ErrNotFound { 77 | t.Fatal("test_key should not be in the cache") 78 | } 79 | 80 | if data != nil { 81 | t.Fatal("data should be nil") 82 | } 83 | } 84 | 85 | func testShardedCacheGetSet(t *testing.T, cache ShardedCache) { 86 | err := cache.Set("user1", "test_key", "test_data") 87 | if err != nil { 88 | t.Fatal("should not give err while setting item") 89 | } 90 | 91 | err = cache.Set("user1", "test_key2", "test_data2") 92 | if err != nil { 93 | t.Fatal("should not give err while setting item") 94 | } 95 | 96 | err = cache.Set("user2", "test_key3", "test_data3") 97 | if err != nil { 98 | t.Fatal("should not give err while setting item") 99 | } 100 | 101 | data, err := cache.Get("user1", "test_key") 102 | if err != nil { 103 | t.Fatal("test_key should be in the cache") 104 | } 105 | 106 | if data != "test_data" { 107 | t.Fatal("data is not \"test_data\"") 108 | } 109 | 110 | data, err = cache.Get("user1", "test_key2") 111 | if err != nil { 112 | t.Fatal("test_key2 should be in the cache") 113 | } 114 | 115 | if data != "test_data2" { 116 | t.Fatal("data is not \"test_data2\"") 117 | } 118 | 119 | data, err = cache.Get("user2", "test_key3") 120 | if err != nil { 121 | t.Fatal("test_key3 should be in the cache") 122 | } 123 | 124 | if data != "test_data3" { 125 | t.Fatal("data is not \"test_data3\"") 126 | } 127 | } 128 | 129 | func testShardedCacheNilValue(t *testing.T, cache ShardedCache) { 130 | err := cache.Set("user1", "test_key", nil) 131 | if err != nil { 132 | t.Fatal("should not give err while setting item") 133 | } 134 | 135 | data, err := cache.Get("user1", "test_key") 136 | if err != nil { 137 | t.Fatal("test_key should be in the cache") 138 | } 139 | 140 | if data != nil { 141 | t.Fatal("data is not nil") 142 | } 143 | 144 | err = cache.Delete("user1", "test_key") 145 | if err != nil { 146 | t.Fatal("should not give err while deleting item") 147 | } 148 | 149 | _, err = cache.Get("user1", "test_key") 150 | if err == nil { 151 | t.Fatal("test_key should not be in the cache") 152 | } 153 | } 154 | 155 | func testShardedCacheDelete(t *testing.T, cache ShardedCache) { 156 | cache.Set("user1", "test_key", "test_data") 157 | cache.Set("user1", "test_key2", "test_data2") 158 | 159 | err := cache.Delete("user1", "test_key3") 160 | if err != nil { 161 | t.Fatal("non-exiting item should not give error") 162 | } 163 | err = cache.Delete("user2", "test_key3") 164 | if err != nil { 165 | t.Fatal("non-exiting shard should not give error") 166 | } 167 | 168 | err = cache.Delete("user1", "test_key") 169 | if err != nil { 170 | t.Fatal("exiting item should not give error") 171 | } 172 | 173 | data, err := cache.Get("user1", "test_key") 174 | if err != ErrNotFound { 175 | t.Fatal("test_key should not be in the cache") 176 | } 177 | 178 | if data != nil { 179 | t.Fatal("data should be nil") 180 | } 181 | } 182 | 183 | func testDeleteShard(t *testing.T, cache ShardedCache) { 184 | cache.Set("user1", "test_key", "test_data") 185 | cache.Set("user1", "test_key2", "test_data2") 186 | cache.Set("user2", "test_key", "test_data") 187 | 188 | err := cache.DeleteShard("user1") 189 | if err != nil { 190 | t.Fatal("exiting shard should not give error") 191 | } 192 | 193 | err = cache.DeleteShard("user3") 194 | if err != nil { 195 | t.Fatal("non-exiting shard should not give error") 196 | } 197 | 198 | data, err := cache.Get("user1", "test_key") 199 | if err != ErrNotFound { 200 | t.Fatal("test_key should not be in the cache") 201 | } 202 | 203 | if data != nil { 204 | t.Fatal("data should be nil") 205 | } 206 | 207 | _, err = cache.Get("user2", "test_key") 208 | if err == ErrNotFound { 209 | t.Fatal("test_key for user2 should still be in cache") 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /mongo_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | mgo "gopkg.in/mgo.v2" 9 | ) 10 | 11 | const ( 12 | defaultExpireDuration = time.Minute 13 | defaultCollectionName = "jCache" 14 | defaultGCInterval = time.Minute 15 | indexExpireAt = "expireAt" 16 | ) 17 | 18 | // MongoCache holds the cache values that will be stored in mongoDB 19 | type MongoCache struct { 20 | // mongeSession specifies the mongoDB connection 21 | mongeSession *mgo.Session 22 | 23 | // CollectionName speficies the optional collection name for mongoDB 24 | // if CollectionName is not set, then default value will be set 25 | CollectionName string 26 | 27 | // ttl is a duration for a cache key to expire 28 | TTL time.Duration 29 | 30 | // GCInterval specifies the time duration for garbage collector time interval 31 | GCInterval time.Duration 32 | 33 | // GCStart starts the garbage collector and deletes the 34 | // expired keys from mongo with given time interval 35 | GCStart bool 36 | 37 | // gcTicker controls gc intervals 38 | gcTicker *time.Ticker 39 | 40 | // done controls sweeping goroutine lifetime 41 | done chan struct{} 42 | 43 | // Mutex is used for handling the concurrent 44 | // read/write requests for cache 45 | sync.RWMutex 46 | } 47 | 48 | // Option sets the options specified. 49 | type Option func(*MongoCache) 50 | 51 | // NewMongoCacheWithTTL creates a caching layer backed by mongo. TTL's are 52 | // managed either by a background cleaner or document is removed on the Get 53 | // operation. Mongo TTL indexes are not utilized since there can be multiple 54 | // systems using the same collection with different TTL values. 55 | // 56 | // The responsibility of stopping the GC process belongs to the user. 57 | // 58 | // Session is not closed while stopping the GC. 59 | // 60 | // This self-referential function satisfy you to avoid passing 61 | // nil value to the function as parameter 62 | // e.g (usage) : 63 | // configure with defaults, just call; 64 | // NewMongoCacheWithTTL(session) 65 | // 66 | // configure ttl duration with; 67 | // NewMongoCacheWithTTL(session, func(m *MongoCache) { 68 | // m.TTL = 2 * time.Minute 69 | // }) 70 | // or 71 | // NewMongoCacheWithTTL(session, SetTTL(time.Minute * 2)) 72 | // 73 | // configure collection name with; 74 | // NewMongoCacheWithTTL(session, func(m *MongoCache) { 75 | // m.CollectionName = "MongoCacheCollectionName" 76 | // }) 77 | func NewMongoCacheWithTTL(session *mgo.Session, configs ...Option) *MongoCache { 78 | if session == nil { 79 | panic("session must be set") 80 | } 81 | 82 | mc := &MongoCache{ 83 | mongeSession: session, 84 | TTL: defaultExpireDuration, 85 | CollectionName: defaultCollectionName, 86 | GCInterval: defaultGCInterval, 87 | GCStart: false, 88 | } 89 | 90 | for _, configFunc := range configs { 91 | configFunc(mc) 92 | } 93 | 94 | if mc.GCStart { 95 | mc.StartGC(mc.GCInterval) 96 | } 97 | 98 | return mc 99 | } 100 | 101 | // MustEnsureIndexExpireAt ensures the expireAt index 102 | // usage: 103 | // NewMongoCacheWithTTL(mongoSession, MustEnsureIndexExpireAt()) 104 | func MustEnsureIndexExpireAt() Option { 105 | return func(m *MongoCache) { 106 | if err := m.EnsureIndex(); err != nil { 107 | panic(fmt.Sprintf("index must ensure %q", err)) 108 | } 109 | } 110 | } 111 | 112 | // StartGC enables the garbage collector in MongoCache struct 113 | // usage: 114 | // NewMongoCacheWithTTL(mongoSession, StartGC()) 115 | func StartGC() Option { 116 | return func(m *MongoCache) { 117 | m.GCStart = true 118 | } 119 | } 120 | 121 | // SetTTL sets the ttl duration in MongoCache as option 122 | // usage: 123 | // NewMongoCacheWithTTL(mongoSession, SetTTL(time*Minute)) 124 | func SetTTL(duration time.Duration) Option { 125 | return func(m *MongoCache) { 126 | m.TTL = duration 127 | } 128 | } 129 | 130 | // SetGCInterval sets the garbage collector interval in MongoCache struct as option 131 | // usage: 132 | // NewMongoCacheWithTTL(mongoSession, SetGCInterval(time*Minute)) 133 | func SetGCInterval(duration time.Duration) Option { 134 | return func(m *MongoCache) { 135 | m.GCInterval = duration 136 | } 137 | } 138 | 139 | // SetCollectionName sets the collection name for mongoDB in MongoCache struct as option 140 | // usage: 141 | // NewMongoCacheWithTTL(mongoSession, SetCollectionName("mongoCollName")) 142 | func SetCollectionName(collName string) Option { 143 | return func(m *MongoCache) { 144 | m.CollectionName = collName 145 | } 146 | } 147 | 148 | // Get returns a value of a given key if it exists 149 | func (m *MongoCache) Get(key string) (interface{}, error) { 150 | data, err := m.get(key) 151 | if err == mgo.ErrNotFound { 152 | return nil, ErrNotFound 153 | } 154 | 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | return data.Value, nil 160 | } 161 | 162 | // Set will persist a value to the cache or override existing one with the new 163 | // one 164 | func (m *MongoCache) Set(key string, value interface{}) error { 165 | return m.set(key, m.TTL, value) 166 | } 167 | 168 | // SetEx will persist a value to the cache or override existing one with the new 169 | // one with ttl duration 170 | func (m *MongoCache) SetEx(key string, duration time.Duration, value interface{}) error { 171 | return m.set(key, duration, value) 172 | } 173 | 174 | // Delete deletes a given key if exists 175 | func (m *MongoCache) Delete(key string) error { 176 | return m.delete(key) 177 | } 178 | 179 | // EnsureIndex ensures the index with expireAt key 180 | func (m *MongoCache) EnsureIndex() error { 181 | query := func(c *mgo.Collection) error { 182 | return c.EnsureIndexKey(indexExpireAt) 183 | } 184 | 185 | return m.run(m.CollectionName, query) 186 | } 187 | 188 | // StartGC starts the garbage collector with given time interval The 189 | // expired data will be checked & deleted with given interval time 190 | func (m *MongoCache) StartGC(gcInterval time.Duration) { 191 | if gcInterval <= 0 { 192 | return 193 | } 194 | 195 | ticker := time.NewTicker(gcInterval) 196 | done := make(chan struct{}) 197 | 198 | m.Lock() 199 | m.gcTicker = ticker 200 | m.done = done 201 | m.Unlock() 202 | 203 | go func() { 204 | for { 205 | select { 206 | case <-ticker.C: 207 | m.Lock() 208 | m.deleteExpiredKeys() 209 | m.Unlock() 210 | case <-done: 211 | return 212 | } 213 | } 214 | }() 215 | } 216 | 217 | // StopGC stops sweeping goroutine. 218 | func (m *MongoCache) StopGC() { 219 | if m.gcTicker != nil { 220 | m.Lock() 221 | m.gcTicker.Stop() 222 | m.gcTicker = nil 223 | close(m.done) 224 | m.done = nil 225 | m.Unlock() 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /lfu_nots.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "container/list" 4 | 5 | // LFUNoTS holds the cache struct 6 | type LFUNoTS struct { 7 | // list holds all items in a linked list 8 | frequencyList *list.List 9 | 10 | // holds the all cache values 11 | cache Cache 12 | 13 | // size holds the limit of the LFU cache 14 | size int 15 | 16 | // currentSize holds the current item size in the list 17 | // after each adding of item, currentSize will be increased 18 | currentSize int 19 | } 20 | 21 | type cacheItem struct { 22 | // key of cache value 23 | k string 24 | 25 | // value of cache value 26 | v interface{} 27 | 28 | // holds the frequency elements 29 | // it holds the element's usage as count 30 | // if cacheItems is used 4 times (with set or get operations) 31 | // the freqElement's frequency counter will be 4 32 | // it holds entry struct inside Value of list.Element 33 | freqElement *list.Element 34 | } 35 | 36 | // NewLFUNoTS creates a new LFU cache struct for further cache operations. Size 37 | // is used for limiting the upper bound of the cache 38 | func NewLFUNoTS(size int) Cache { 39 | if size < 1 { 40 | panic("invalid cache size") 41 | } 42 | 43 | return &LFUNoTS{ 44 | frequencyList: list.New(), 45 | cache: NewMemoryNoTS(), 46 | size: size, 47 | currentSize: 0, 48 | } 49 | } 50 | 51 | // Get gets value of cache item 52 | // then increments the usage of the item 53 | func (l *LFUNoTS) Get(key string) (interface{}, error) { 54 | res, err := l.cache.Get(key) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | ci := res.(*cacheItem) 60 | 61 | // increase usage of cache item 62 | l.incr(ci) 63 | return ci.v, nil 64 | } 65 | 66 | // Set sets a new key-value pair 67 | // Set increments the key usage count too 68 | // 69 | // eg: 70 | // cache.Set("test_key","2") 71 | // cache.Set("test_key","1") 72 | // if you try to set a value into same key 73 | // its usage count will be increased 74 | // and usage count of "test_key" will be 2 in this example 75 | func (l *LFUNoTS) Set(key string, value interface{}) error { 76 | return l.set(key, value) 77 | } 78 | 79 | // Delete deletes the key and its dependencies 80 | func (l *LFUNoTS) Delete(key string) error { 81 | res, err := l.cache.Get(key) 82 | if err != nil && err != ErrNotFound { 83 | return err 84 | } 85 | 86 | // we dont need to delete if already doesn't exist 87 | if err == ErrNotFound { 88 | return nil 89 | } 90 | 91 | ci := res.(*cacheItem) 92 | 93 | l.remove(ci, ci.freqElement) 94 | l.currentSize-- 95 | return l.cache.Delete(key) 96 | } 97 | 98 | // set sets a new key-value pair 99 | func (l *LFUNoTS) set(key string, value interface{}) error { 100 | res, err := l.cache.Get(key) 101 | if err != nil && err != ErrNotFound { 102 | return err 103 | } 104 | 105 | if err == ErrNotFound { 106 | //create new cache item 107 | ci := newCacheItem(key, value) 108 | 109 | // if cache size si reached to max size 110 | // then first remove lfu item from the list 111 | if l.currentSize >= l.size { 112 | // then evict some data from head of linked list. 113 | l.evict(l.frequencyList.Front()) 114 | } 115 | 116 | l.cache.Set(key, ci) 117 | l.incr(ci) 118 | 119 | } else { 120 | //update existing one 121 | val := res.(*cacheItem) 122 | val.v = value 123 | l.cache.Set(key, val) 124 | l.incr(res.(*cacheItem)) 125 | } 126 | 127 | return nil 128 | } 129 | 130 | // entry holds the frequency node informations 131 | type entry struct { 132 | // freqCount holds the frequency number 133 | freqCount int 134 | 135 | // itemCount holds the items how many exist in list 136 | listEntry map[*cacheItem]struct{} 137 | } 138 | 139 | // incr increments the usage of cache items 140 | // incrementing will be used in 'Get' & 'Set' functions 141 | // whenever these functions are used, usage count of any key 142 | // will be increased 143 | func (l *LFUNoTS) incr(ci *cacheItem) { 144 | var nextValue int 145 | var nextPosition *list.Element 146 | // update existing one 147 | if ci.freqElement != nil { 148 | nextValue = ci.freqElement.Value.(*entry).freqCount + 1 149 | // replace the position of frequency element 150 | nextPosition = ci.freqElement.Next() 151 | } else { 152 | // create new frequency element for cache item 153 | // ci.freqElement is nil so next value of freq will be 1 154 | nextValue = 1 155 | // we created new element and its position will be head of linked list 156 | nextPosition = l.frequencyList.Front() 157 | l.currentSize++ 158 | } 159 | 160 | // we need to check position first, otherwise it will panic if we try to fetch value of entry 161 | if nextPosition == nil || nextPosition.Value.(*entry).freqCount != nextValue { 162 | // create new entry node for linked list 163 | entry := newEntry(nextValue) 164 | if ci.freqElement == nil { 165 | nextPosition = l.frequencyList.PushFront(entry) 166 | } else { 167 | nextPosition = l.frequencyList.InsertAfter(entry, ci.freqElement) 168 | } 169 | } 170 | 171 | nextPosition.Value.(*entry).listEntry[ci] = struct{}{} 172 | ci.freqElement = nextPosition 173 | 174 | // we have moved the cache item to the next position, 175 | // then we need to remove old position of the cacheItem from the list 176 | // then we deleted previous position of cacheItem 177 | if ci.freqElement.Prev() != nil { 178 | l.remove(ci, ci.freqElement.Prev()) 179 | } 180 | } 181 | 182 | // remove removes the cache item from the cache list 183 | // after deleting key from the list, if its linked list has no any item no longer 184 | // then that linked list elemnet will be removed from the list too 185 | func (l *LFUNoTS) remove(ci *cacheItem, position *list.Element) { 186 | entry := position.Value.(*entry).listEntry 187 | delete(entry, ci) 188 | if len(entry) == 0 { 189 | l.frequencyList.Remove(position) 190 | } 191 | } 192 | 193 | // evict deletes the element from list with given linked list element 194 | func (l *LFUNoTS) evict(e *list.Element) error { 195 | // ne need to return err if list element is already nil 196 | if e == nil { 197 | return nil 198 | } 199 | 200 | // remove the first item of the linked list 201 | for entry := range e.Value.(*entry).listEntry { 202 | l.cache.Delete(entry.k) 203 | l.remove(entry, e) 204 | l.currentSize-- 205 | break 206 | } 207 | 208 | return nil 209 | } 210 | 211 | // newEntry creates a new entry with frequency count 212 | func newEntry(freqCount int) *entry { 213 | return &entry{ 214 | freqCount: freqCount, 215 | listEntry: make(map[*cacheItem]struct{}), 216 | } 217 | } 218 | 219 | // newCacheItem creates a new cache item with key and value 220 | func newCacheItem(key string, value interface{}) *cacheItem { 221 | return &cacheItem{ 222 | k: key, 223 | v: value, 224 | } 225 | 226 | } 227 | -------------------------------------------------------------------------------- /mongo_cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | mgo "gopkg.in/mgo.v2" 9 | "gopkg.in/mgo.v2/bson" 10 | ) 11 | 12 | var ( 13 | // session is the default session with default options 14 | session = initMongo() 15 | ) 16 | 17 | func TestMongoCacheSetOptionFuncs(t *testing.T) { 18 | mgoCache := NewMongoCacheWithTTL(session) 19 | defer mgoCache.StopGC() 20 | if mgoCache == nil { 21 | t.Fatal("config should not be nil") 22 | } 23 | 24 | duration := time.Minute * 3 25 | cacheTTL := NewMongoCacheWithTTL(session, SetTTL(duration)) 26 | defer mgoCache.StopGC() 27 | if cacheTTL == nil { 28 | t.Fatal("ttl config should not be nil") 29 | } 30 | if cacheTTL.TTL != duration { 31 | t.Fatalf("config ttl time should equal to %v", duration) 32 | } 33 | 34 | // check multiple options 35 | collName := "testingCollectionName" 36 | cache := NewMongoCacheWithTTL(session, SetCollectionName(collName), SetGCInterval(duration), StartGC()) 37 | defer mgoCache.StopGC() 38 | if cache == nil { 39 | t.Fatal("cache should not be nil") 40 | } 41 | if cache.CollectionName != collName { 42 | t.Fatalf("cache collection name should equal %s", collName) 43 | } 44 | if cache.GCStart != true { 45 | t.Fatal("cache StartGC option should not be true") 46 | } 47 | 48 | if cache.GCInterval != duration { 49 | t.Fatalf("cache GCInterval option should equal %v", duration) 50 | } 51 | } 52 | 53 | func TestMongoCacheGet(t *testing.T) { 54 | mgoCache := NewMongoCacheWithTTL(session) 55 | defer mgoCache.StopGC() 56 | if mgoCache == nil { 57 | t.Fatal("config should not be nil") 58 | } 59 | if _, err := mgoCache.Get("test"); err != ErrNotFound { 60 | t.Fatalf("error is: %q", err) 61 | } 62 | } 63 | 64 | func TestMongoCacheSet(t *testing.T) { 65 | mgoCache := NewMongoCacheWithTTL(session) 66 | defer mgoCache.StopGC() 67 | if mgoCache == nil { 68 | t.Fatal("config should not be nil") 69 | } 70 | key := bson.NewObjectId().Hex() 71 | value := bson.NewObjectId().Hex() 72 | 73 | if err := mgoCache.Set(key, value); err != nil { 74 | t.Fatalf("error should be nil: %q", err) 75 | } 76 | 77 | data, err := mgoCache.Get(key) 78 | if err != nil { 79 | t.Fatal("error should be nil:", err) 80 | } 81 | if data == nil { 82 | t.Fatal("data should not be nil") 83 | } 84 | if data != value { 85 | t.Fatalf("data should equal: %v ,but got: %v", value, data) 86 | } 87 | } 88 | 89 | func TestMongoCacheSetEx(t *testing.T) { 90 | mgoCache := NewMongoCacheWithTTL(session) 91 | defer mgoCache.StopGC() 92 | if mgoCache == nil { 93 | t.Fatal("config should not be nil") 94 | } 95 | // defaultExpireDuration is 1 Minute as default 96 | if mgoCache.TTL != defaultExpireDuration { 97 | t.Fatalf("mongoCache TTL should equal %v", defaultExpireDuration) 98 | } 99 | 100 | key := bson.NewObjectId().Hex() 101 | value := bson.NewObjectId().Hex() 102 | 103 | duration := time.Second * 10 104 | err := mgoCache.SetEx(key, duration, value) 105 | if err != nil { 106 | t.Fatalf("error should be nil: %q", err) 107 | } 108 | 109 | document, err := mgoCache.get(key) 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | if !time.Now().Add(duration).After(document.ExpireAt) { 114 | t.Fatalf("expireAt should be greater than now + %v", duration) 115 | } 116 | 117 | } 118 | 119 | func TestMongoCacheDelete(t *testing.T) { 120 | mgoCache := NewMongoCacheWithTTL(session) 121 | defer mgoCache.StopGC() 122 | if mgoCache == nil { 123 | t.Fatal("config should not be nil") 124 | } 125 | key := bson.NewObjectId().Hex() 126 | value := bson.NewObjectId().Hex() 127 | 128 | err := mgoCache.Set(key, value) 129 | if err != nil { 130 | t.Fatalf("error should be nil: %q", err) 131 | } 132 | data, err := mgoCache.Get(key) 133 | if err != nil { 134 | t.Fatalf("error should be nil: %q", err) 135 | } 136 | if data != value { 137 | t.Fatalf("data should equal to %v, but got: %v", value, data) 138 | } 139 | 140 | if err = mgoCache.Delete(key); err != nil { 141 | t.Fatalf("err should be nil, but got %q", err) 142 | } 143 | 144 | if _, err := mgoCache.Get(key); err != ErrNotFound { 145 | t.Fatalf("error should equal to %q but got: %q", ErrNotFound, err) 146 | } 147 | } 148 | 149 | func TestMongoCacheTTL(t *testing.T) { 150 | // duration specifies the time duration to hold the data in mongo 151 | // after the duration interval, data will be deleted from mongoDB 152 | duration := time.Millisecond * 100 153 | 154 | mgoCache := NewMongoCacheWithTTL(session, SetTTL(duration)) 155 | defer mgoCache.StopGC() 156 | if mgoCache == nil { 157 | t.Fatal("config should not be nil") 158 | } 159 | defer mgoCache.StopGC() 160 | 161 | key, value := bson.NewObjectId().Hex(), bson.NewObjectId().Hex() 162 | 163 | if err := mgoCache.Set(key, value); err != nil { 164 | t.Fatalf("error should be nil: %q", err) 165 | } 166 | 167 | if data, err := mgoCache.Get(key); err != nil { 168 | t.Fatalf("error should be nil: %q", err) 169 | } else if data != value { 170 | t.Fatalf("data should equal: %v, but got: %v", value, data) 171 | } 172 | 173 | time.Sleep(duration) 174 | 175 | if _, err := mgoCache.Get(key); err != ErrNotFound { 176 | t.Fatalf("error should equal to %q but got: %q", ErrNotFound, err) 177 | } 178 | } 179 | 180 | // TestMongoCacheGC tests the garbage collector logic 181 | // Mainly tests the GCInterval & StartGC options 182 | func TestMongoCacheGC(t *testing.T) { 183 | // duration specifies the time duration to hold the data in mongo 184 | // after the duration interval, data will be deleted from mongoDB 185 | duration := time.Millisecond * 100 186 | 187 | mgoCache := NewMongoCacheWithTTL(session, SetTTL(duration/2), SetGCInterval(duration), StartGC()) 188 | defer mgoCache.StopGC() 189 | if mgoCache == nil { 190 | t.Fatal("config should not be nil") 191 | } 192 | 193 | defer mgoCache.StopGC() 194 | 195 | key, value := bson.NewObjectId().Hex(), bson.NewObjectId().Hex() 196 | key1, value1 := bson.NewObjectId().Hex(), bson.NewObjectId().Hex() 197 | 198 | if err := mgoCache.Set(key, value); err != nil { 199 | t.Fatalf("error should be nil: %q", err) 200 | } 201 | if err := mgoCache.Set(key1, value1); err != nil { 202 | t.Fatalf("error should be nil: %q", err) 203 | } 204 | 205 | if data, err := mgoCache.Get(key); err != nil { 206 | t.Fatalf("error should be nil: %q", err) 207 | } else if data != value { 208 | t.Fatalf("data should equal: %v, but got: %v", value, data) 209 | } 210 | 211 | if data1, err := mgoCache.Get(key1); err != nil { 212 | t.Fatalf("error should be nil: %q", err) 213 | } else if data1 != value1 { 214 | t.Fatalf("data should equal: %v, but got: %v", value1, data1) 215 | } 216 | 217 | time.Sleep(duration) 218 | 219 | docs, err := getAllDocuments(mgoCache, key1, key1) 220 | if err != nil { 221 | t.Fatal(err) 222 | } 223 | 224 | if len(docs) != 0 { 225 | t.Fatalf("len should equal to 0 but got: %d", len(docs)) 226 | } 227 | } 228 | 229 | func getAllDocuments(mgoCache *MongoCache, keys ...string) ([]Document, error) { 230 | var docs []Document 231 | query := func(c *mgo.Collection) error { 232 | return c.Find(bson.M{ 233 | "_id": bson.M{ 234 | "$in": keys, 235 | }, 236 | }).All(&docs) 237 | } 238 | 239 | err := mgoCache.run(mgoCache.CollectionName, query) 240 | if err != nil { 241 | return nil, err 242 | } 243 | 244 | return docs, nil 245 | } 246 | 247 | func initMongo() *mgo.Session { 248 | mongoURI := os.Getenv("MONGODB_URL") 249 | if mongoURI == "" { 250 | mongoURI = "127.0.0.1:27017/test" 251 | } 252 | 253 | ses, err := mgo.Dial(mongoURI) 254 | if err != nil { 255 | panic(err) 256 | } 257 | 258 | ses.SetSafe(&mgo.Safe{}) 259 | 260 | return ses 261 | } 262 | --------------------------------------------------------------------------------