1
0
mirror of https://github.com/go-micro/go-micro.git synced 2024-12-18 08:26:38 +02:00

Add simple in-memory cache (#2231)

* Add simple in-memory cache

* Support configuring cache expiration duration

* Support preinitializing cache with items

* Register cache
This commit is contained in:
Niek den Breeje 2021-08-31 16:31:16 +02:00 committed by GitHub
parent dd0a7746ff
commit 05a299b76c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 357 additions and 0 deletions

65
cache/cache.go vendored Normal file
View File

@ -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
cache/cache_test.go vendored Normal file
View File

@ -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
cache/default.go vendored Normal file
View File

@ -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
cache/options.go vendored Normal file
View File

@ -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
cache/options_test.go vendored Normal file
View File

@ -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)
}
}
})
}
}

View File

@ -9,6 +9,7 @@ import (
"github.com/asim/go-micro/v3/auth" "github.com/asim/go-micro/v3/auth"
"github.com/asim/go-micro/v3/broker" "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/client"
"github.com/asim/go-micro/v3/config" "github.com/asim/go-micro/v3/config"
"github.com/asim/go-micro/v3/debug/profile" "github.com/asim/go-micro/v3/debug/profile"
@ -265,6 +266,8 @@ var (
} }
DefaultConfigs = map[string]func(...config.Option) (config.Config, error){} DefaultConfigs = map[string]func(...config.Option) (config.Config, error){}
DefaultCaches = map[string]func(...cache.Option) cache.Cache{}
) )
func init() { func init() {
@ -285,6 +288,7 @@ func newCmd(opts ...Option) Cmd {
Tracer: &trace.DefaultTracer, Tracer: &trace.DefaultTracer,
Profile: &profile.DefaultProfile, Profile: &profile.DefaultProfile,
Config: &config.DefaultConfig, Config: &config.DefaultConfig,
Cache: &cache.DefaultCache,
Brokers: DefaultBrokers, Brokers: DefaultBrokers,
Clients: DefaultClients, Clients: DefaultClients,
@ -298,6 +302,7 @@ func newCmd(opts ...Option) Cmd {
Auths: DefaultAuths, Auths: DefaultAuths,
Profiles: DefaultProfiles, Profiles: DefaultProfiles,
Configs: DefaultConfigs, Configs: DefaultConfigs,
Caches: DefaultCaches,
} }
for _, o := range opts { for _, o := range opts {

View File

@ -5,6 +5,7 @@ import (
"github.com/asim/go-micro/v3/auth" "github.com/asim/go-micro/v3/auth"
"github.com/asim/go-micro/v3/broker" "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/client"
"github.com/asim/go-micro/v3/config" "github.com/asim/go-micro/v3/config"
"github.com/asim/go-micro/v3/debug/profile" "github.com/asim/go-micro/v3/debug/profile"
@ -28,6 +29,7 @@ type Options struct {
Registry *registry.Registry Registry *registry.Registry
Selector *selector.Selector Selector *selector.Selector
Transport *transport.Transport Transport *transport.Transport
Cache *cache.Cache
Config *config.Config Config *config.Config
Client *client.Client Client *client.Client
Server *server.Server Server *server.Server
@ -38,6 +40,7 @@ type Options struct {
Profile *profile.Profile Profile *profile.Profile
Brokers map[string]func(...broker.Option) broker.Broker Brokers map[string]func(...broker.Option) broker.Broker
Caches map[string]func(...cache.Option) cache.Cache
Configs map[string]func(...config.Option) (config.Config, error) Configs map[string]func(...config.Option) (config.Config, error)
Clients map[string]func(...client.Option) client.Client Clients map[string]func(...client.Option) client.Client
Registries map[string]func(...registry.Option) registry.Registry Registries map[string]func(...registry.Option) registry.Registry
@ -82,6 +85,12 @@ func Broker(b *broker.Broker) Option {
} }
} }
func Cache(c *cache.Cache) Option {
return func(o *Options) {
o.Cache = c
}
}
func Config(c *config.Config) Option { func Config(c *config.Config) Option {
return func(o *Options) { return func(o *Options) {
o.Config = c o.Config = c
@ -155,6 +164,13 @@ func NewBroker(name string, b func(...broker.Option) broker.Broker) Option {
} }
} }
// New cache func
func NewCache(name string, c func(...cache.Option) cache.Cache) Option {
return func(o *Options) {
o.Caches[name] = c
}
}
// New client func // New client func
func NewClient(name string, b func(...client.Option) client.Client) Option { func NewClient(name string, b func(...client.Option) client.Client) Option {
return func(o *Options) { return func(o *Options) {

View File

@ -6,6 +6,7 @@ import (
"github.com/asim/go-micro/v3/auth" "github.com/asim/go-micro/v3/auth"
"github.com/asim/go-micro/v3/broker" "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/client"
"github.com/asim/go-micro/v3/cmd" "github.com/asim/go-micro/v3/cmd"
"github.com/asim/go-micro/v3/config" "github.com/asim/go-micro/v3/config"
@ -24,6 +25,7 @@ import (
type Options struct { type Options struct {
Auth auth.Auth Auth auth.Auth
Broker broker.Broker Broker broker.Broker
Cache cache.Cache
Cmd cmd.Cmd Cmd cmd.Cmd
Config config.Config Config config.Config
Client client.Client Client client.Client
@ -51,6 +53,7 @@ func newOptions(opts ...Option) Options {
opt := Options{ opt := Options{
Auth: auth.DefaultAuth, Auth: auth.DefaultAuth,
Broker: broker.DefaultBroker, Broker: broker.DefaultBroker,
Cache: cache.DefaultCache,
Cmd: cmd.DefaultCmd, Cmd: cmd.DefaultCmd,
Config: config.DefaultConfig, Config: config.DefaultConfig,
Client: client.DefaultClient, Client: client.DefaultClient,
@ -80,6 +83,12 @@ func Broker(b broker.Broker) Option {
} }
} }
func Cache(c cache.Cache) Option {
return func(o *Options) {
o.Cache = c
}
}
func Cmd(c cmd.Cmd) Option { func Cmd(c cmd.Cmd) Option {
return func(o *Options) { return func(o *Options) {
o.Cmd = c o.Cmd = c