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:
parent
dd0a7746ff
commit
05a299b76c
65
cache/cache.go
vendored
Normal file
65
cache/cache.go
vendored
Normal 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
111
cache/cache_test.go
vendored
Normal 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
68
cache/default.go
vendored
Normal 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
40
cache/options.go
vendored
Normal 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
43
cache/options_test.go
vendored
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user