Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simple in-memory cache #2231

Merged
merged 4 commits into from
Aug 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
65 changes: 65 additions & 0 deletions cache/cache.go
@@ -0,0 +1,65 @@
package cache

import (
"context"
"errors"
"time"
)

var (
// DefaultCache is the default cache.
DefaultCache Cache = NewCache()
// DefaultExpiration is the default duration for items stored in
// the cache to expire.
DefaultExpiration time.Duration = 0

// ErrItemExpired is returned in Cache.Get when the item found in the cache
// has expired.
ErrItemExpired error = errors.New("item has expired")
// ErrKeyNotFound is returned in Cache.Get and Cache.Delete when the
// provided key could not be found in cache.
ErrKeyNotFound error = errors.New("key not found in cache")
)

// Cache is the interface that wraps the cache.
//
// Context specifies the context for the cache.
// Get gets a cached value by key.
// Put stores a key-value pair into cache.
// Delete removes a key from cache.
type Cache interface {
Context(ctx context.Context) Cache
Get(key string) (interface{}, time.Time, error)
Put(key string, val interface{}, d time.Duration) error
Delete(key string) error
}

// Item represents an item stored in the cache.
type Item struct {
Value interface{}
Expiration int64
}

// Expired returns true if the item has expired.
func (i *Item) Expired() bool {
if i.Expiration == 0 {
return false
}

return time.Now().UnixNano() > i.Expiration
}

// NewCache returns a new cache.
func NewCache(opts ...Option) Cache {
options := NewOptions(opts...)
items := make(map[string]Item)

if len(options.Items) > 0 {
items = options.Items
}

return &memCache{
opts: options,
items: items,
}
}
111 changes: 111 additions & 0 deletions cache/cache_test.go
@@ -0,0 +1,111 @@
package cache

import (
"context"
"testing"
"time"
)

var (
ctx context.Context = context.TODO()
key string = "test"
val interface{} = "hello go-micro"
)

// TestMemCache tests the in-memory cache implementation.
func TestCache(t *testing.T) {
t.Run("CacheGetMiss", func(t *testing.T) {
if _, _, err := NewCache().Context(ctx).Get(key); err == nil {
t.Error("expected to get no value from cache")
}
})

t.Run("CacheGetHit", func(t *testing.T) {
c := NewCache()

if err := c.Context(ctx).Put(key, val, 0); err != nil {
t.Error(err)
}

if a, _, err := c.Context(ctx).Get(key); err != nil {
t.Errorf("Expected a value, got err: %s", err)
} else if a != val {
t.Errorf("Expected '%v', got '%v'", val, a)
}
})

t.Run("CacheGetExpired", func(t *testing.T) {
c := NewCache()
e := 20 * time.Millisecond

if err := c.Context(ctx).Put(key, val, e); err != nil {
t.Error(err)
}

<-time.After(25 * time.Millisecond)
if _, _, err := c.Context(ctx).Get(key); err == nil {
t.Error("expected to get no value from cache")
}
})

t.Run("CacheGetValid", func(t *testing.T) {
c := NewCache()
e := 25 * time.Millisecond

if err := c.Context(ctx).Put(key, val, e); err != nil {
t.Error(err)
}

<-time.After(20 * time.Millisecond)
if _, _, err := c.Context(ctx).Get(key); err != nil {
t.Errorf("expected a value, got err: %s", err)
}
})

t.Run("CacheDeleteMiss", func(t *testing.T) {
if err := NewCache().Context(ctx).Delete(key); err == nil {
t.Error("expected to delete no value from cache")
}
})

t.Run("CacheDeleteHit", func(t *testing.T) {
c := NewCache()

if err := c.Context(ctx).Put(key, val, 0); err != nil {
t.Error(err)
}

if err := c.Context(ctx).Delete(key); err != nil {
t.Errorf("Expected to delete an item, got err: %s", err)
}

if _, _, err := c.Context(ctx).Get(key); err == nil {
t.Errorf("Expected error")
}
})
}

func TestCacheWithOptions(t *testing.T) {
t.Run("CacheWithExpiration", func(t *testing.T) {
c := NewCache(Expiration(20 * time.Millisecond))

if err := c.Context(ctx).Put(key, val, 0); err != nil {
t.Error(err)
}

<-time.After(25 * time.Millisecond)
if _, _, err := c.Context(ctx).Get(key); err == nil {
t.Error("expected to get no value from cache")
}
})

t.Run("CacheWithItems", func(t *testing.T) {
c := NewCache(Items(map[string]Item{key: {val, 0}}))

if a, _, err := c.Context(ctx).Get(key); err != nil {
t.Errorf("Expected a value, got err: %s", err)
} else if a != val {
t.Errorf("Expected '%v', got '%v'", val, a)
}
})
}
68 changes: 68 additions & 0 deletions cache/default.go
@@ -0,0 +1,68 @@
package cache

import (
"context"
"sync"
"time"
)

type memCache struct {
opts Options
sync.RWMutex
ctx context.Context

items map[string]Item
}

func (c *memCache) Context(ctx context.Context) Cache {
c.ctx = ctx
return c
}

func (c *memCache) Get(key string) (interface{}, time.Time, error) {
c.RWMutex.Lock()
defer c.RWMutex.Unlock()

item, found := c.items[key]
if !found {
return nil, time.Time{}, ErrKeyNotFound
}
if item.Expired() {
return nil, time.Time{}, ErrItemExpired
}

return item.Value, time.Unix(0, item.Expiration), nil
}

func (c *memCache) Put(key string, val interface{}, d time.Duration) error {
var e int64
if d == DefaultExpiration {
d = c.opts.Expiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano()
}

c.RWMutex.Lock()
defer c.RWMutex.Unlock()

c.items[key] = Item{
Value: val,
Expiration: e,
}

return nil
}

func (c *memCache) Delete(key string) error {
c.RWMutex.Lock()
defer c.RWMutex.Unlock()

_, found := c.items[key]
if !found {
return ErrKeyNotFound
}

delete(c.items, key)
return nil
}
40 changes: 40 additions & 0 deletions cache/options.go
@@ -0,0 +1,40 @@
package cache

import "time"

// Options represents the options for the cache.
type Options struct {
Expiration time.Duration
Items map[string]Item
}

// Option manipulates the Options passed.
type Option func(o *Options)

// Expiration sets the duration for items stored in the cache to expire.
func Expiration(d time.Duration) Option {
return func(o *Options) {
o.Expiration = d
}
}

// Items initializes the cache with preconfigured items.
func Items(i map[string]Item) Option {
return func(o *Options) {
o.Items = i
}
}

// NewOptions returns a new options struct.
func NewOptions(opts ...Option) Options {
options := Options{
Expiration: DefaultExpiration,
Items: make(map[string]Item),
}

for _, o := range opts {
o(&options)
}

return options
}
43 changes: 43 additions & 0 deletions cache/options_test.go
@@ -0,0 +1,43 @@
package cache

import (
"testing"
"time"
)

func TestOptions(t *testing.T) {
testData := map[string]struct {
set bool
expiration time.Duration
items map[string]Item
}{
"DefaultOptions": {false, DefaultExpiration, map[string]Item{}},
"ModifiedOptions": {true, time.Second, map[string]Item{"test": {"hello go-micro", 0}}},
}

for k, d := range testData {
t.Run(k, func(t *testing.T) {
var opts Options

if d.set {
opts = NewOptions(
Expiration(d.expiration),
Items(d.items),
)
} else {
opts = NewOptions()
}

// test options
for _, o := range []Options{opts} {
if o.Expiration != d.expiration {
t.Fatalf("Expected expiration '%v', got '%v'", d.expiration, o.Expiration)
}

if o.Items["test"] != d.items["test"] {
t.Fatalf("Expected items %#v, got %#v", d.items, o.Items)
}
}
})
}
}
5 changes: 5 additions & 0 deletions cmd/cmd.go
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/asim/go-micro/v3/auth"
"github.com/asim/go-micro/v3/broker"
"github.com/asim/go-micro/v3/cache"
"github.com/asim/go-micro/v3/client"
"github.com/asim/go-micro/v3/config"
"github.com/asim/go-micro/v3/debug/profile"
Expand Down Expand Up @@ -265,6 +266,8 @@ var (
}

DefaultConfigs = map[string]func(...config.Option) (config.Config, error){}

DefaultCaches = map[string]func(...cache.Option) cache.Cache{}
)

func init() {
Expand All @@ -285,6 +288,7 @@ func newCmd(opts ...Option) Cmd {
Tracer: &trace.DefaultTracer,
Profile: &profile.DefaultProfile,
Config: &config.DefaultConfig,
Cache: &cache.DefaultCache,

Brokers: DefaultBrokers,
Clients: DefaultClients,
Expand All @@ -298,6 +302,7 @@ func newCmd(opts ...Option) Cmd {
Auths: DefaultAuths,
Profiles: DefaultProfiles,
Configs: DefaultConfigs,
Caches: DefaultCaches,
}

for _, o := range opts {
Expand Down