Skip to content

Commit

Permalink
Merge pull request #1 from eko/cache-memcached
Browse files Browse the repository at this point in the history
feat(store): added memcache support
  • Loading branch information
eko committed Oct 13, 2019
2 parents 41b785b + 7f5503c commit 52a29a1
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 13 deletions.
1 change: 1 addition & 0 deletions Makefile
Expand Up @@ -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/
61 changes: 48 additions & 13 deletions README.md
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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),
)
Expand All @@ -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)
```
Expand All @@ -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:
Expand All @@ -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"}
Expand Down Expand Up @@ -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"}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -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=
Expand Down
59 changes: 59 additions & 0 deletions 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
}
74 changes: 74 additions & 0 deletions 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())
}
48 changes: 48 additions & 0 deletions test/mocks/store/memcache_client_interface.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 52a29a1

Please sign in to comment.