mirror of
				https://github.com/go-micro/go-micro.git
				synced 2025-10-30 23:27:41 +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:
		
							
								
								
									
										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/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" | ||||
| @@ -265,6 +266,8 @@ var ( | ||||
| 	} | ||||
|  | ||||
| 	DefaultConfigs = map[string]func(...config.Option) (config.Config, error){} | ||||
|  | ||||
| 	DefaultCaches = map[string]func(...cache.Option) cache.Cache{} | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| @@ -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, | ||||
| @@ -298,6 +302,7 @@ func newCmd(opts ...Option) Cmd { | ||||
| 		Auths:      DefaultAuths, | ||||
| 		Profiles:   DefaultProfiles, | ||||
| 		Configs:    DefaultConfigs, | ||||
| 		Caches:     DefaultCaches, | ||||
| 	} | ||||
|  | ||||
| 	for _, o := range opts { | ||||
|   | ||||
| @@ -5,6 +5,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" | ||||
| @@ -28,6 +29,7 @@ type Options struct { | ||||
| 	Registry  *registry.Registry | ||||
| 	Selector  *selector.Selector | ||||
| 	Transport *transport.Transport | ||||
| 	Cache     *cache.Cache | ||||
| 	Config    *config.Config | ||||
| 	Client    *client.Client | ||||
| 	Server    *server.Server | ||||
| @@ -38,6 +40,7 @@ type Options struct { | ||||
| 	Profile   *profile.Profile | ||||
|  | ||||
| 	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) | ||||
| 	Clients    map[string]func(...client.Option) client.Client | ||||
| 	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 { | ||||
| 	return func(o *Options) { | ||||
| 		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 | ||||
| func NewClient(name string, b func(...client.Option) client.Client) Option { | ||||
| 	return func(o *Options) { | ||||
|   | ||||
| @@ -6,6 +6,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/cmd" | ||||
| 	"github.com/asim/go-micro/v3/config" | ||||
| @@ -24,6 +25,7 @@ import ( | ||||
| type Options struct { | ||||
| 	Auth      auth.Auth | ||||
| 	Broker    broker.Broker | ||||
| 	Cache     cache.Cache | ||||
| 	Cmd       cmd.Cmd | ||||
| 	Config    config.Config | ||||
| 	Client    client.Client | ||||
| @@ -51,6 +53,7 @@ func newOptions(opts ...Option) Options { | ||||
| 	opt := Options{ | ||||
| 		Auth:      auth.DefaultAuth, | ||||
| 		Broker:    broker.DefaultBroker, | ||||
| 		Cache:     cache.DefaultCache, | ||||
| 		Cmd:       cmd.DefaultCmd, | ||||
| 		Config:    config.DefaultConfig, | ||||
| 		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 { | ||||
| 	return func(o *Options) { | ||||
| 		o.Cmd = c | ||||
|   | ||||
		Reference in New Issue
	
	Block a user