diff --git a/Makefile b/Makefile index dcdcb6a..1fc5d8f 100644 --- a/Makefile +++ b/Makefile @@ -6,5 +6,6 @@ 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=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/README.md b/README.md index 4dea6e9..59d695c 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,9 @@ Here is what it brings in detail: ## Built-in stores -* [Ristretto](https://github.com/dgraph-io/ristretto) (in memory) -* [Go-Redis](github.com/go-redis/redis/v7) (redis) +* [Memory](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 ## Built-in metrics providers @@ -33,16 +34,50 @@ Here is what it brings in detail: Here is a simple cache instanciation with Redis but you can also look at other available stores: +#### Memcache + +```go +memcacheStore := store.NewMemcache(memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212")) + +cacheManager := cache.New(memcacheStore, 15*time.Second) +err := cacheManager.Set("my-key", []byte("my-value)) +if err != nil { + panic(err) +} + +value := cacheManager.Get("my-key") +``` + +#### Memory (using Ristretto) + +```go +ristrettoCache, err := ristretto.NewCache(&ristretto.Config{NumCounters: 1000, MaxCost: 100, BufferItems: 64}) +if err != nil { + panic(err) +} +ristrettoStore := store.NewRistretto(ristrettoCache) + +cacheManager := cache.New(ristrettoStore, 1*time.Second) +err := cacheManager.Set("my-key", "my-value) +if err != nil { + panic(err) +} + +value := cacheManager.Get("my-key") +``` + +#### Redis + ```go redisStore := store.NewRedis(redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})) -cache.New(redisStore, 15*time.Second) -err := cache.Set("my-key", "my-value) +cacheManager := cache.New(redisStore, 15*time.Second) +err := cacheManager.Set("my-key", "my-value) if err != nil { panic(err) } -value := cache.Get("my-key") +value := cacheManager.Get("my-key") ``` ### A chained cache @@ -63,7 +98,7 @@ ristrettoStore := store.NewRistretto(ristrettoCache) redisStore := store.NewRedis(redisClient) // Initialize chained cache -cache := cache.NewChain( +cacheManager := cache.NewChain( cache.New(ristrettoStore, 5*time.Second), cache.New(redisStore, 15*time.Second), ) @@ -89,7 +124,7 @@ loadFunction := func(key interface{}) (interface{}, error) { } // Initialize loadable cache -cache := cache.NewLoadable(loadFunction, cache.New(redisStore, 15*time.Second)) +cacheManager := cache.NewLoadable(loadFunction, cache.New(redisStore, 15*time.Second)) // ... Then, you can get your data and your function will automatically put them in cache(s) ``` @@ -107,14 +142,14 @@ redisStore := store.NewRedis(redisClient) promMetrics := metrics.NewPrometheus("my-test-app") // Initialize metric cache -cache := cache.NewMetric(promMetrics, cache.New(redisStore, 15*time.Second)) +cacheManager := cache.NewMetric(promMetrics, cache.New(redisStore, 15*time.Second)) // ... Then, you can get your data and metrics will be observed by Prometheus ``` Of course, you can pass a `Chain` cache into the `Loadable` one so if your data is not available in all caches, it will bring it back in all caches. -### A marshaler wraper +### A marshaler wrapper Some caches like Redis stores and returns the value as a string so you have to marshal/unmarshal your structs if you want to cache an object. That's why we bring a marshaler service that wraps your cache and make the work for you: @@ -124,10 +159,10 @@ redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"}) redisStore := store.NewRedis(redisClient) // Initialize chained cache -cache := cache.NewMetric(promMetrics, cache.New(redisStore, 15*time.Second)) +cacheManager := cache.NewMetric(promMetrics, cache.New(redisStore, 15*time.Second)) // Initializes marshaler -marshaller := marshaler.New(cache) +marshaller := marshaler.New(cacheManager) key := BookQuery{Slug: "my-test-amazing-book"} value := Book{ID: 1, Name: "My test amazing book", Slug: "my-test-amazing-book"} @@ -194,14 +229,14 @@ func main() { // Initialize a chained cache (memory with Ristretto then Redis) with Prometheus metrics // and a load function that will put data back into caches if none has the value - cache := cache.NewMetric(promMetrics, cache.NewLoadable(loadFunction, + cacheManager := cache.NewMetric(promMetrics, cache.NewLoadable(loadFunction, cache.NewChain( cache.New(ristrettoStore, 5*time.Second), cache.New(redisStore, 15*time.Second), ), )) - marshaller := marshaler.New(cache) + marshaller := marshaler.New(cacheManager) key := Book{Slug: "my-test-amazing-book"} value := Book{ID: 1, Name: "My test amazing book", Slug: "my-test-amazing-book"} diff --git a/go.mod b/go.mod index 7a2dbd2..b4c5cd6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/eko/gache go 1.13 require ( + github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b github.com/go-redis/redis/v7 v7.0.0-beta.4 github.com/kr/pretty v0.1.0 // indirect github.com/prometheus/client_golang v1.1.0 diff --git a/go.sum b/go.sum index eb31be4..7980089 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/store/memcache.go b/store/memcache.go new file mode 100644 index 0000000..42d1769 --- /dev/null +++ b/store/memcache.go @@ -0,0 +1,59 @@ +package store + +import ( + "errors" + "time" + + "github.com/bradfitz/gomemcache/memcache" +) + +// MemcacheClientInterface represents a bradfitz/gomemcache client +type MemcacheClientInterface interface { + Get(key string) (item *memcache.Item, err error) + Set(item *memcache.Item) error +} + +const ( + MemcacheType = "memcache" +) + +// MemcacheStore is a store for Redis +type MemcacheStore struct { + client MemcacheClientInterface +} + +// NewMemcache creates a new store to Memcache instance(s) +func NewMemcache(client MemcacheClientInterface) *MemcacheStore { + return &MemcacheStore{ + client: client, + } +} + +// Get returns data stored from a given key +func (s *MemcacheStore) 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 memcache") + } + + return item.Value, err +} + +// Set defines data in Redis for given key idntifier +func (s *MemcacheStore) Set(key interface{}, value interface{}, expiration time.Duration) error { + item := &memcache.Item{ + Key: key.(string), + Value: value.([]byte), + Expiration: int32(expiration.Seconds()), + } + + return s.client.Set(item) +} + +// GetType returns the store type +func (s *MemcacheStore) GetType() string { + return MemcacheType +} diff --git a/store/memcache_test.go b/store/memcache_test.go new file mode 100644 index 0000000..ac831f5 --- /dev/null +++ b/store/memcache_test.go @@ -0,0 +1,74 @@ +package store + +import ( + "testing" + "time" + + "github.com/bradfitz/gomemcache/memcache" + mocksStore "github.com/eko/gache/test/mocks/store" + "github.com/stretchr/testify/assert" +) + +func TestNewMemcache(t *testing.T) { + // Given + client := &mocksStore.MemcacheClientInterface{} + + // When + store := NewMemcache(client) + + // Then + assert.IsType(t, new(MemcacheStore), store) + assert.Equal(t, client, store.client) +} + +func TestMemcacheGet(t *testing.T) { + // Given + cacheKey := "my-key" + cacheValue := []byte("my-cache-value") + + client := &mocksStore.MemcacheClientInterface{} + client.On("Get", cacheKey).Return(&memcache.Item{ + Value: cacheValue, + }, nil) + + store := NewMemcache(client) + + // When + value, err := store.Get(cacheKey) + + // Then + assert.Nil(t, err) + assert.Equal(t, cacheValue, value) +} + +func TestMemcacheSet(t *testing.T) { + // Given + cacheKey := "my-key" + cacheValue := []byte("my-cache-value") + expiration := 5 * time.Second + + client := &mocksStore.MemcacheClientInterface{} + client.On("Set", &memcache.Item{ + Key: cacheKey, + Value: cacheValue, + Expiration: int32(expiration.Seconds()), + }).Return(nil) + + store := NewMemcache(client) + + // When + err := store.Set(cacheKey, cacheValue, expiration) + + // Then + assert.Nil(t, err) +} + +func TestMemcacheGetType(t *testing.T) { + // Given + client := &mocksStore.MemcacheClientInterface{} + + store := NewMemcache(client) + + // When - Then + assert.Equal(t, MemcacheType, store.GetType()) +} diff --git a/test/mocks/store/memcache_client_interface.go b/test/mocks/store/memcache_client_interface.go new file mode 100644 index 0000000..96a7471 --- /dev/null +++ b/test/mocks/store/memcache_client_interface.go @@ -0,0 +1,48 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import memcache "github.com/bradfitz/gomemcache/memcache" +import mock "github.com/stretchr/testify/mock" + +// MemcacheClientInterface is an autogenerated mock type for the MemcacheClientInterface type +type MemcacheClientInterface struct { + mock.Mock +} + +// Get provides a mock function with given fields: key +func (_m *MemcacheClientInterface) Get(key string) (*memcache.Item, error) { + ret := _m.Called(key) + + var r0 *memcache.Item + if rf, ok := ret.Get(0).(func(string) *memcache.Item); ok { + r0 = rf(key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*memcache.Item) + } + } + + 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: item +func (_m *MemcacheClientInterface) Set(item *memcache.Item) error { + ret := _m.Called(item) + + var r0 error + if rf, ok := ret.Get(0).(func(*memcache.Item) error); ok { + r0 = rf(item) + } else { + r0 = ret.Error(0) + } + + return r0 +}