Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: added cache invalidation by tags
  • Loading branch information
eko committed Oct 19, 2019
1 parent 8de72e9 commit ff78752
Show file tree
Hide file tree
Showing 36 changed files with 1,018 additions and 28 deletions.
47 changes: 47 additions & 0 deletions README.md
Expand Up @@ -18,6 +18,7 @@ Here is what it brings in detail:
* ✅ A metric cache to let you store metrics about your caches usage (hits, miss, set success, set error, ...)
* ✅ A marshaler to automatically marshal/unmarshal your cache values as a struct
* ✅ Define default values in stores and override them when setting data
* ✅ Cache invalidation by expiration time and/or using tags

## Built-in stores

Expand Down Expand Up @@ -228,6 +229,52 @@ marshal.Delete("my-key")
The only thing you have to do is to specify the struct in which you want your value to be unmarshalled as a second argument when calling the `.Get()` method.
### Cache invalidation using tags
You can attach some tags to items you create so you can easily invalidate some of them later.
Tags are stored using the same storage you choose for your cache.
Here is an example on how to use it:
```go
// Initialize Redis client and store
redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
redisStore := store.NewRedis(redisClient, nil)
// Initialize chained cache
cacheManager := cache.NewMetric(
promMetrics,
cache.New(redisStore),
)
// Initializes marshaler
marshal := marshaler.New(cacheManager)
key := BookQuery{Slug: "my-test-amazing-book"}
value := Book{ID: 1, Name: "My test amazing book", Slug: "my-test-amazing-book"}
// Set an item in the cache and attach it a "book" tag
err = marshal.Set(key, value, store.Options{Tags: []string{"book"}})
if err != nil {
panic(err)
}
// Remove all items that have the "book" tag
err := marshal.Invalidate(store.InvalidateOptions{Tags: []string{"book"}})
if err != nil {
panic(err)
}
returnedValue, err := marshal.Get(key, new(Book))
if err != nil {
// Should be triggered because item has been deleted so it cannot be found.
panic(err)
}
```
Mix this with expiration times on your caches to have a fine tuned control on how your data are cached.
### All together!
Finally, you can mix all of these available caches or bring them together to build the cache you want to.
Expand Down
5 changes: 5 additions & 0 deletions cache/cache.go
Expand Up @@ -41,6 +41,11 @@ func (c *Cache) Delete(key interface{}) error {
return c.codec.Delete(cacheKey)
}

// Invalidate invalidates cache item from given options
func (c *Cache) Invalidate(options store.InvalidateOptions) error {
return c.codec.Invalidate(options)
}

// GetCodec returns the current codec
func (c *Cache) GetCodec() codec.CodecInterface {
return c.codec
Expand Down
38 changes: 38 additions & 0 deletions cache/cache_test.go
Expand Up @@ -150,6 +150,44 @@ func TestCacheDelete(t *testing.T) {
assert.Nil(t, err)
}

func TestCacheInvalidate(t *testing.T) {
// Given
options := store.InvalidateOptions{
Tags: []string{"tag1"},
}

store := &mocksStore.StoreInterface{}
store.On("Invalidate", options).Return(nil)

cache := New(store)

// When
err := cache.Invalidate(options)

// Then
assert.Nil(t, err)
}

func TestCacheInvalidateWhenError(t *testing.T) {
// Given
options := store.InvalidateOptions{
Tags: []string{"tag1"},
}

expectedErr := errors.New("Unexpected error during invalidation")

store := &mocksStore.StoreInterface{}
store.On("Invalidate", options).Return(expectedErr)

cache := New(store)

// When
err := cache.Invalidate(options)

// Then
assert.Equal(t, expectedErr, err)
}

func TestCacheDeleteWhenError(t *testing.T) {
// Given
expectedErr := errors.New("Unable to delete key")
Expand Down
9 changes: 9 additions & 0 deletions cache/chain.go
Expand Up @@ -65,6 +65,15 @@ func (c *ChainCache) Delete(key interface{}) error {
return nil
}

// Invalidate invalidates cache item from given options
func (c *ChainCache) Invalidate(options store.InvalidateOptions) error {
for _, cache := range c.caches {
cache.Invalidate(options)
}

return nil
}

// setUntil sets a value in available caches, eventually until a given cache layer
func (c *ChainCache) setUntil(key, object interface{}, until *string) error {
for _, cache := range c.caches {
Expand Down
46 changes: 46 additions & 0 deletions cache/chain_test.go
Expand Up @@ -156,6 +156,52 @@ func TestChainDeleteWhenError(t *testing.T) {
assert.Nil(t, err)
}

func TestChainInvalidate(t *testing.T) {
// Given
options := store.InvalidateOptions{
Tags: []string{"tag1"},
}

// Cache 1
cache1 := &mocksCache.SetterCacheInterface{}
cache1.On("Invalidate", options).Return(nil)

// Cache 2
cache2 := &mocksCache.SetterCacheInterface{}
cache2.On("Invalidate", options).Return(nil)

cache := NewChain(cache1, cache2)

// When
err := cache.Invalidate(options)

// Then
assert.Nil(t, err)
}

func TestChainInvalidateWhenError(t *testing.T) {
// Given
options := store.InvalidateOptions{
Tags: []string{"tag1"},
}

// Cache 1
cache1 := &mocksCache.SetterCacheInterface{}
cache1.On("Invalidate", options).Return(errors.New("An unexpected error has occured while invalidation data"))

// Cache 2
cache2 := &mocksCache.SetterCacheInterface{}
cache2.On("Invalidate", options).Return(nil)

cache := NewChain(cache1, cache2)

// When
err := cache.Invalidate(options)

// Then
assert.Nil(t, err)
}

func TestChainGetType(t *testing.T) {
// Given
cache1 := &mocksCache.SetterCacheInterface{}
Expand Down
1 change: 1 addition & 0 deletions cache/interface.go
Expand Up @@ -10,6 +10,7 @@ type CacheInterface interface {
Get(key interface{}) (interface{}, error)
Set(key, object interface{}, options *store.Options) error
Delete(key interface{}) error
Invalidate(options store.InvalidateOptions) error
GetType() string
}

Expand Down
5 changes: 5 additions & 0 deletions cache/loadable.go
Expand Up @@ -58,6 +58,11 @@ func (c *LoadableCache) Delete(key interface{}) error {
return c.cache.Delete(key)
}

// Invalidate invalidates cache item from given options
func (c *LoadableCache) Invalidate(options store.InvalidateOptions) error {
return c.cache.Invalidate(options)
}

// GetType returns the cache type
func (c *LoadableCache) GetType() string {
return LoadableType
Expand Down
46 changes: 46 additions & 0 deletions cache/loadable_test.go
Expand Up @@ -128,6 +128,52 @@ func TestLoadableDeleteWhenError(t *testing.T) {
assert.Equal(t, expectedErr, err)
}

func TestLoadableInvalidate(t *testing.T) {
// Given
options := store.InvalidateOptions{
Tags: []string{"tag1"},
}

cache1 := &mocksCache.SetterCacheInterface{}
cache1.On("Invalidate", options).Return(nil)

loadFunc := func(key interface{}) (interface{}, error) {
return "a value", nil
}

cache := NewLoadable(loadFunc, cache1)

// When
err := cache.Invalidate(options)

// Then
assert.Nil(t, err)
}

func TestLoadableInvalidateWhenError(t *testing.T) {
// Given
options := store.InvalidateOptions{
Tags: []string{"tag1"},
}

expectedErr := errors.New("Unexpected error when invalidating data")

cache1 := &mocksCache.SetterCacheInterface{}
cache1.On("Invalidate", options).Return(expectedErr)

loadFunc := func(key interface{}) (interface{}, error) {
return "a value", nil
}

cache := NewLoadable(loadFunc, cache1)

// When
err := cache.Invalidate(options)

// Then
assert.Equal(t, expectedErr, err)
}

func TestLoadableGetType(t *testing.T) {
// Given
cache1 := &mocksCache.SetterCacheInterface{}
Expand Down
5 changes: 5 additions & 0 deletions cache/metric.go
Expand Up @@ -42,6 +42,11 @@ func (c *MetricCache) Delete(key interface{}) error {
return c.cache.Delete(key)
}

// Invalidate invalidates cache item from given options
func (c *MetricCache) Invalidate(options store.InvalidateOptions) error {
return c.cache.Invalidate(options)
}

// Get obtains a value from cache and also records metrics
func (c *MetricCache) updateMetrics(cache CacheInterface) {
switch current := cache.(type) {
Expand Down
43 changes: 43 additions & 0 deletions cache/metric_test.go
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"testing"

"github.com/eko/gocache/store"
mocksCache "github.com/eko/gocache/test/mocks/cache"
mocksCodec "github.com/eko/gocache/test/mocks/codec"
mocksMetrics "github.com/eko/gocache/test/mocks/metrics"
Expand Down Expand Up @@ -119,6 +120,48 @@ func TestMetricDeleteWhenError(t *testing.T) {
assert.Equal(t, expectedErr, err)
}

func TestMetricInvalidate(t *testing.T) {
// Given
options := store.InvalidateOptions{
Tags: []string{"tag1"},
}

cache1 := &mocksCache.SetterCacheInterface{}
cache1.On("Invalidate", options).Return(nil)

metrics := &mocksMetrics.MetricsInterface{}

cache := NewMetric(metrics, cache1)

// When
err := cache.Invalidate(options)

// Then
assert.Nil(t, err)
}

func TestMetricInvalidateWhenError(t *testing.T) {
// Given
options := store.InvalidateOptions{
Tags: []string{"tag1"},
}

expectedErr := errors.New("Unexpected error while invalidating data")

cache1 := &mocksCache.SetterCacheInterface{}
cache1.On("Invalidate", options).Return(expectedErr)

metrics := &mocksMetrics.MetricsInterface{}

cache := NewMetric(metrics, cache1)

// When
err := cache.Invalidate(options)

// Then
assert.Equal(t, expectedErr, err)
}

func TestMetricGetType(t *testing.T) {
// Given
cache1 := &mocksCache.SetterCacheInterface{}
Expand Down
27 changes: 21 additions & 6 deletions codec/codec.go
Expand Up @@ -6,12 +6,14 @@ import (

// Stats allows to returns some statistics of codec usage
type Stats struct {
Hits int
Miss int
SetSuccess int
SetError int
DeleteSuccess int
DeleteError int
Hits int
Miss int
SetSuccess int
SetError int
DeleteSuccess int
DeleteError int
InvalidateSuccess int
InvalidateError int
}

// Codec represents an instance of a cache store
Expand Down Expand Up @@ -68,6 +70,19 @@ func (c *Codec) Delete(key interface{}) error {
return err
}

// Invalidate invalidates some cach items from given options
func (c *Codec) Invalidate(options store.InvalidateOptions) error {
err := c.store.Invalidate(options)

if err == nil {
c.stats.InvalidateSuccess++
} else {
c.stats.InvalidateError++
}

return err
}

// GetStore returns the store associated to this codec
func (c *Codec) GetStore() store.StoreInterface {
return c.store
Expand Down

0 comments on commit ff78752

Please sign in to comment.