From 23439eaffa2e3f028480b57c2bb08dd30634741a Mon Sep 17 00:00:00 2001 From: Vincent Composieux Date: Mon, 14 Oct 2019 01:38:07 +0200 Subject: [PATCH 1/2] feat(cache): added options --- README.md | 51 +++++++++++++++++++++++++++++++++---------- cache/cache.go | 17 +++++++++------ cache/cache_test.go | 50 +++++++++++++++++++++++++++--------------- cache/options.go | 15 +++++++++++++ cache/options_test.go | 18 +++++++++++++++ 5 files changed, 115 insertions(+), 36 deletions(-) create mode 100644 cache/options.go create mode 100644 cache/options_test.go diff --git a/README.md b/README.md index 59d695c..ac231bf 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ Here is what it brings in detail: ## Built-in stores -* [Memory](https://github.com/dgraph-io/ristretto) (dgraph-io/ristretto) +* [Memory (bigcache)](https://github.com/allegro/bigcache) (allegro/bigcache) +* [Memory (ristretto)](https://github.com/dgraph-io/ristretto) (dgraph-io/ristretto) * [Memcache](https://github.com/bradfitz/gomemcache) (bradfitz/memcache) * [Redis](https://github.com/go-redis/redis/v7) (go-redis/redis) * More to come soon @@ -48,6 +49,23 @@ if err != nil { value := cacheManager.Get("my-key") ``` +#### Memory (using Bigcache) + +```go +bigcacheClient, _ := bigcache.NewBigCache(bigcache.DefaultConfig(5 * time.Minute)) +bigcacheStore := store.NewBigcache( + bigcacheClient, +) + +cacheManager := cache.New(bigcacheStore, nil) +err := cacheManager.Set("my-key", "my-value") +if err != nil { + panic(err) +} + +value := cacheManager.Get("my-key") +``` + #### Memory (using Ristretto) ```go @@ -57,8 +75,8 @@ if err != nil { } ristrettoStore := store.NewRistretto(ristrettoCache) -cacheManager := cache.New(ristrettoStore, 1*time.Second) -err := cacheManager.Set("my-key", "my-value) +cacheManager := cache.New(ristrettoStore, nil) +err := cacheManager.Set("my-key", "my-value") if err != nil { panic(err) } @@ -71,8 +89,8 @@ value := cacheManager.Get("my-key") ```go redisStore := store.NewRedis(redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})) -cacheManager := cache.New(redisStore, 15*time.Second) -err := cacheManager.Set("my-key", "my-value) +cacheManager := cache.New(redisStore, &cache.Options{Expiration: 15*time.Second}) +err := cacheManager.Set("my-key", "my-value") if err != nil { panic(err) } @@ -99,8 +117,8 @@ redisStore := store.NewRedis(redisClient) // Initialize chained cache cacheManager := cache.NewChain( - cache.New(ristrettoStore, 5*time.Second), - cache.New(redisStore, 15*time.Second), + cache.New(ristrettoStore, nil), + cache.New(redisStore, &cache.Options{Expiration: 15*time.Second}), ) // ... Then, do what you want with your cache @@ -124,7 +142,10 @@ loadFunction := func(key interface{}) (interface{}, error) { } // Initialize loadable cache -cacheManager := cache.NewLoadable(loadFunction, cache.New(redisStore, 15*time.Second)) +cacheManager := cache.NewLoadable( + loadFunction, + cache.New(redisStore, &cache.Options{Expiration: 15*time.Second}), +) // ... Then, you can get your data and your function will automatically put them in cache(s) ``` @@ -142,7 +163,10 @@ redisStore := store.NewRedis(redisClient) promMetrics := metrics.NewPrometheus("my-test-app") // Initialize metric cache -cacheManager := cache.NewMetric(promMetrics, cache.New(redisStore, 15*time.Second)) +cacheManager := cache.NewMetric( + promMetrics, + cache.New(redisStore, &cache.Options{Expiration: 15*time.Second}), +) // ... Then, you can get your data and metrics will be observed by Prometheus ``` @@ -159,7 +183,10 @@ redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"}) redisStore := store.NewRedis(redisClient) // Initialize chained cache -cacheManager := cache.NewMetric(promMetrics, cache.New(redisStore, 15*time.Second)) +cacheManager := cache.NewMetric( + promMetrics, + cache.New(redisStore, &cache.Options{Expiration: 15*time.Second}), +) // Initializes marshaler marshaller := marshaler.New(cacheManager) @@ -231,8 +258,8 @@ func main() { // and a load function that will put data back into caches if none has the value cacheManager := cache.NewMetric(promMetrics, cache.NewLoadable(loadFunction, cache.NewChain( - cache.New(ristrettoStore, 5*time.Second), - cache.New(redisStore, 15*time.Second), + cache.New(ristrettoStore, nil), + cache.New(redisStore, &cache.Options{Expiration: 15*time.Second}), ), )) diff --git a/cache/cache.go b/cache/cache.go index 6f34345..009ac5b 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -2,7 +2,6 @@ package cache import ( "strings" - "time" "github.com/eko/gache/codec" "github.com/eko/gache/store" @@ -14,15 +13,19 @@ const ( // Cache represents the configuration needed by a cache type Cache struct { - codec codec.CodecInterface - expiration time.Duration + codec codec.CodecInterface + options *Options } // New instanciates a new cache entry -func New(store store.StoreInterface, expiration time.Duration) *Cache { +func New(store store.StoreInterface, options *Options) *Cache { + if options == nil { + options = &Options{} + } + return &Cache{ - codec: codec.New(store), - expiration: expiration, + codec: codec.New(store), + options: options, } } @@ -35,7 +38,7 @@ func (c *Cache) Get(key interface{}) (interface{}, error) { // Set populates the cache item using the given key func (c *Cache) Set(key, object interface{}) error { cacheKey := c.getCacheKey(key) - return c.codec.Set(cacheKey, object, c.expiration) + return c.codec.Set(cacheKey, object, c.options.ExpirationValue()) } // GetCodec returns the current codec diff --git a/cache/cache_test.go b/cache/cache_test.go index e195caa..d3acdc8 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -14,22 +14,26 @@ import ( func TestNew(t *testing.T) { // Given store := &mocksStore.StoreInterface{} - expiration := 5 * time.Second + options := &Options{ + Expiration: 5 * time.Second, + } // When - cache := New(store, expiration) + cache := New(store, options) // Then assert.IsType(t, new(Cache), cache) assert.IsType(t, new(codec.Codec), cache.codec) assert.Equal(t, store, cache.codec.GetStore()) - assert.Equal(t, expiration, cache.expiration) + assert.Equal(t, options, cache.options) } func TestCacheSet(t *testing.T) { // Given - expiration := 5 * time.Second + options := &Options{ + Expiration: 5 * time.Second, + } value := &struct { Hello string @@ -38,9 +42,10 @@ func TestCacheSet(t *testing.T) { } store := &mocksStore.StoreInterface{} - store.On("Set", "9b1ac8a6e8ca8ca9477c0a252eb37756", value, expiration).Return(nil) + store.On("Set", "9b1ac8a6e8ca8ca9477c0a252eb37756", value, options.ExpirationValue()). + Return(nil) - cache := New(store, expiration) + cache := New(store, options) // When err := cache.Set("my-key", value) @@ -49,7 +54,9 @@ func TestCacheSet(t *testing.T) { func TestCacheSetWhenErrorOccurs(t *testing.T) { // Given - expiration := 5 * time.Second + options := &Options{ + Expiration: 5 * time.Second, + } value := &struct { Hello string @@ -60,9 +67,10 @@ func TestCacheSetWhenErrorOccurs(t *testing.T) { storeErr := errors.New("An error has occured while inserting data into store") store := &mocksStore.StoreInterface{} - store.On("Set", "9b1ac8a6e8ca8ca9477c0a252eb37756", value, expiration).Return(storeErr) + store.On("Set", "9b1ac8a6e8ca8ca9477c0a252eb37756", value, options.ExpirationValue()). + Return(storeErr) - cache := New(store, expiration) + cache := New(store, options) // When err := cache.Set("my-key", value) @@ -71,7 +79,9 @@ func TestCacheSetWhenErrorOccurs(t *testing.T) { func TestCacheGet(t *testing.T) { // Given - expiration := 5 * time.Second + options := &Options{ + Expiration: 5 * time.Second, + } cacheValue := &struct { Hello string @@ -82,7 +92,7 @@ func TestCacheGet(t *testing.T) { store := &mocksStore.StoreInterface{} store.On("Get", "9b1ac8a6e8ca8ca9477c0a252eb37756").Return(cacheValue, nil) - cache := New(store, expiration) + cache := New(store, options) // When value, err := cache.Get("my-key") @@ -94,14 +104,16 @@ func TestCacheGet(t *testing.T) { func TestCacheGetWhenNotFound(t *testing.T) { // Given - expiration := 5 * time.Second + options := &Options{ + Expiration: 5 * time.Second, + } returnedErr := errors.New("Unable to find item in store") store := &mocksStore.StoreInterface{} store.On("Get", "9b1ac8a6e8ca8ca9477c0a252eb37756").Return(nil, returnedErr) - cache := New(store, expiration) + cache := New(store, options) // When value, err := cache.Get("my-key") @@ -114,9 +126,11 @@ func TestCacheGetWhenNotFound(t *testing.T) { func TestCacheGetCodec(t *testing.T) { // Given store := &mocksStore.StoreInterface{} - expiration := 5 * time.Second + options := &Options{ + Expiration: 5 * time.Second, + } - cache := New(store, expiration) + cache := New(store, options) // When value := cache.GetCodec() @@ -129,9 +143,11 @@ func TestCacheGetCodec(t *testing.T) { func TestCacheGetType(t *testing.T) { // Given store := &mocksStore.StoreInterface{} - expiration := 5 * time.Second + options := &Options{ + Expiration: 5 * time.Second, + } - cache := New(store, expiration) + cache := New(store, options) // When - Then assert.Equal(t, CacheType, cache.GetType()) diff --git a/cache/options.go b/cache/options.go new file mode 100644 index 0000000..050d8a5 --- /dev/null +++ b/cache/options.go @@ -0,0 +1,15 @@ +package cache + +import ( + "time" +) + +// Options represents the cache available options +type Options struct { + Expiration time.Duration +} + +// ExpirationValue returns the expiration option value +func (o Options) ExpirationValue() time.Duration { + return o.Expiration +} diff --git a/cache/options_test.go b/cache/options_test.go new file mode 100644 index 0000000..dbcbfc8 --- /dev/null +++ b/cache/options_test.go @@ -0,0 +1,18 @@ +package cache + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestOptionsExpirationValue(t *testing.T) { + // Given + options := Options{ + Expiration: 25 * time.Second, + } + + // When - Then + assert.Equal(t, 25*time.Second, options.ExpirationValue()) +} From 4e751dc80c2678badcb99041c72bd55c18edd98b Mon Sep 17 00:00:00 2001 From: Vincent Composieux Date: Mon, 14 Oct 2019 01:38:34 +0200 Subject: [PATCH 2/2] feat(store): added bigcache --- .travis.yml | 2 +- Makefile | 1 + store/bigcache.go | 51 ++++++++++++++ store/bigcache_test.go | 67 +++++++++++++++++++ test/mocks/store/bigcache_client_interface.go | 47 +++++++++++++ 5 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 store/bigcache.go create mode 100644 store/bigcache_test.go create mode 100644 test/mocks/store/bigcache_client_interface.go diff --git a/.travis.yml b/.travis.yml index 14907ff..4261faf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,4 +15,4 @@ install: - go get -t -v ./... script: - - go test -v ./... + - go test -cover -v ./... diff --git a/Makefile b/Makefile index 1fc5d8f..48701ff 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ mocks: mockery -case=snake -name=SetterCacheInterface -dir=cache/ -output test/mocks/cache/ mockery -case=snake -name=MetricsInterface -dir=metrics/ -output test/mocks/metrics/ mockery -case=snake -name=StoreInterface -dir=store/ -output test/mocks/store/ + mockery -case=snake -name=BigcacheClientInterface -dir=store/ -output test/mocks/store/ mockery -case=snake -name=MemcacheClientInterface -dir=store/ -output test/mocks/store/ mockery -case=snake -name=RedisClientInterface -dir=store/ -output test/mocks/store/ mockery -case=snake -name=RistrettoClientInterface -dir=store/ -output test/mocks/store/ diff --git a/store/bigcache.go b/store/bigcache.go new file mode 100644 index 0000000..849820b --- /dev/null +++ b/store/bigcache.go @@ -0,0 +1,51 @@ +package store + +import ( + "errors" + "time" +) + +// BigcacheClientInterface represents a allegro/bigcache client +type BigcacheClientInterface interface { + Get(key string) ([]byte, error) + Set(key string, entry []byte) error +} + +const ( + BigcacheType = "bigcache" +) + +// BigcacheStore is a store for Redis +type BigcacheStore struct { + client BigcacheClientInterface +} + +// NewBigcache creates a new store to Bigcache instance(s) +func NewBigcache(client BigcacheClientInterface) *BigcacheStore { + return &BigcacheStore{ + client: client, + } +} + +// Get returns data stored from a given key +func (s *BigcacheStore) Get(key interface{}) (interface{}, error) { + item, err := s.client.Get(key.(string)) + if err != nil { + return nil, err + } + if item == nil { + return nil, errors.New("Unable to retrieve data from bigcache") + } + + return item, err +} + +// Set defines data in Redis for given key idntifier +func (s *BigcacheStore) Set(key interface{}, value interface{}, expiration time.Duration) error { + return s.client.Set(key.(string), value.([]byte)) +} + +// GetType returns the store type +func (s *BigcacheStore) GetType() string { + return BigcacheType +} diff --git a/store/bigcache_test.go b/store/bigcache_test.go new file mode 100644 index 0000000..63abfbe --- /dev/null +++ b/store/bigcache_test.go @@ -0,0 +1,67 @@ +package store + +import ( + "testing" + "time" + + mocksStore "github.com/eko/gache/test/mocks/store" + "github.com/stretchr/testify/assert" +) + +func TestNewBigcache(t *testing.T) { + // Given + client := &mocksStore.BigcacheClientInterface{} + + // When + store := NewBigcache(client) + + // Then + assert.IsType(t, new(BigcacheStore), store) + assert.Equal(t, client, store.client) +} + +func TestBigcacheGet(t *testing.T) { + // Given + cacheKey := "my-key" + cacheValue := []byte("my-cache-value") + + client := &mocksStore.BigcacheClientInterface{} + client.On("Get", cacheKey).Return(cacheValue, nil) + + store := NewBigcache(client) + + // When + value, err := store.Get(cacheKey) + + // Then + assert.Nil(t, err) + assert.Equal(t, cacheValue, value) +} + +func TestBigcacheSet(t *testing.T) { + // Given + cacheKey := "my-key" + cacheValue := []byte("my-cache-value") + expiration := 5 * time.Second + + client := &mocksStore.BigcacheClientInterface{} + client.On("Set", cacheKey, cacheValue).Return(nil) + + store := NewBigcache(client) + + // When + err := store.Set(cacheKey, cacheValue, expiration) + + // Then + assert.Nil(t, err) +} + +func TestBigcacheGetType(t *testing.T) { + // Given + client := &mocksStore.BigcacheClientInterface{} + + store := NewBigcache(client) + + // When - Then + assert.Equal(t, BigcacheType, store.GetType()) +} diff --git a/test/mocks/store/bigcache_client_interface.go b/test/mocks/store/bigcache_client_interface.go new file mode 100644 index 0000000..46eecf8 --- /dev/null +++ b/test/mocks/store/bigcache_client_interface.go @@ -0,0 +1,47 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// BigcacheClientInterface is an autogenerated mock type for the BigcacheClientInterface type +type BigcacheClientInterface struct { + mock.Mock +} + +// Get provides a mock function with given fields: key +func (_m *BigcacheClientInterface) Get(key string) ([]byte, error) { + ret := _m.Called(key) + + var r0 []byte + if rf, ok := ret.Get(0).(func(string) []byte); ok { + r0 = rf(key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(key) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Set provides a mock function with given fields: key, entry +func (_m *BigcacheClientInterface) Set(key string, entry []byte) error { + ret := _m.Called(key, entry) + + var r0 error + if rf, ok := ret.Get(0).(func(string, []byte) error); ok { + r0 = rf(key, entry) + } else { + r0 = ret.Error(0) + } + + return r0 +}