Added custom cache client for much easier cache operations.

This commit is contained in:
mikestefanello 2022-01-13 21:13:41 -05:00
parent d412e06dad
commit bfbb9669aa
8 changed files with 451 additions and 64 deletions

204
services/cache.go Normal file
View file

@ -0,0 +1,204 @@
package services
import (
"context"
"fmt"
"time"
"github.com/eko/gocache/v2/cache"
"github.com/eko/gocache/v2/marshaler"
"github.com/eko/gocache/v2/store"
"github.com/go-redis/redis/v8"
"github.com/mikestefanello/pagoda/config"
)
type (
// CacheClient is the client that allows you to interact with the cache
CacheClient struct {
// Client stores the client to the underlying cache service
Client *redis.Client
// cache stores the cache interface
cache *cache.Cache
}
// cacheSet handles chaining a set operation
cacheSet struct {
client *CacheClient
key string
group string
data interface{}
expiration time.Duration
tags []string
}
// cacheGet handles chaining a get operation
cacheGet struct {
client *CacheClient
key string
group string
dataType interface{}
}
// cacheFlush handles chaining a flush operation
cacheFlush struct {
client *CacheClient
key string
group string
tags []string
}
)
// NewCacheClient creates a new cache client
func NewCacheClient(cfg config.CacheConfig) (*CacheClient, error) {
c := &CacheClient{}
c.Client = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Hostname, cfg.Port),
Password: cfg.Password,
})
if _, err := c.Client.Ping(context.Background()).Result(); err != nil {
return c, err
}
cacheStore := store.NewRedis(c.Client, nil)
c.cache = cache.New(cacheStore)
return c, nil
}
// Close closes the connection to the cache
func (c *CacheClient) Close() error {
return c.Client.Close()
}
// Set creates a cache set operation
func (c *CacheClient) Set() *cacheSet {
return &cacheSet{
client: c,
}
}
// Get creates a cache get operation
func (c *CacheClient) Get() *cacheGet {
return &cacheGet{
client: c,
}
}
// Flush creates a cache flush operation
func (c *CacheClient) Flush() *cacheFlush {
return &cacheFlush{
client: c,
}
}
// cacheKey formats a cache key with an optional group
func (c *CacheClient) cacheKey(group, key string) string {
if group != "" {
return fmt.Sprintf("%s::%s", group, key)
}
return key
}
// Key sets the cache key
func (c *cacheSet) Key(key string) *cacheSet {
c.key = key
return c
}
// Group sets the cache group
func (c *cacheSet) Group(group string) *cacheSet {
c.group = group
return c
}
// Data sets the data to cache
func (c *cacheSet) Data(data interface{}) *cacheSet {
c.data = data
return c
}
// Expiration sets the expiration duration of the cached data
func (c *cacheSet) Expiration(expiration time.Duration) *cacheSet {
c.expiration = expiration
return c
}
// Tags sets the cache tags
func (c *cacheSet) Tags(tags []string) *cacheSet {
c.tags = tags
return c
}
// Save saves the data in the cache
func (c *cacheSet) Save(ctx context.Context) error {
opts := &store.Options{
Expiration: c.expiration,
Tags: c.tags,
}
return marshaler.
New(c.client.cache).
Set(ctx, c.client.cacheKey(c.group, c.key), c.data, opts)
}
// Key sets the cache key
func (c *cacheGet) Key(key string) *cacheGet {
c.key = key
return c
}
// Group sets the cache group
func (c *cacheGet) Group(group string) *cacheGet {
c.group = group
return c
}
// Type sets the expected Go type of the data being retrieved from the cache
func (c *cacheGet) Type(expectedType interface{}) *cacheGet {
c.dataType = expectedType
return c
}
// Fetch fetches the data from the cache
func (c *cacheGet) Fetch(ctx context.Context) (interface{}, error) {
return marshaler.New(c.client.cache).Get(
ctx,
c.client.cacheKey(c.group, c.key),
c.dataType,
)
}
// Key sets the cache key
func (c *cacheFlush) Key(key string) *cacheFlush {
c.key = key
return c
}
// Group sets the cache group
func (c *cacheFlush) Group(group string) *cacheFlush {
c.group = group
return c
}
// Tags sets the cache tags
func (c *cacheFlush) Tags(tags []string) *cacheFlush {
c.tags = tags
return c
}
// Exec flushes the data from the cache
func (c *cacheFlush) Exec(ctx context.Context) error {
if len(c.tags) > 0 {
if err := c.client.cache.Invalidate(ctx, store.InvalidateOptions{
Tags: c.tags,
}); err != nil {
return err
}
}
if c.key != "" {
return c.client.cache.Delete(ctx, c.client.cacheKey(c.group, c.key))
}
return nil
}

105
services/cache_test.go Normal file
View file

@ -0,0 +1,105 @@
package services
import (
"context"
"testing"
"time"
"github.com/go-redis/redis/v8"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCacheClient(t *testing.T) {
type cacheTest struct {
Value string
}
// Cache some data
data := cacheTest{Value: "abcdef"}
group := "testgroup"
key := "testkey"
err := c.Cache.
Set().
Group(group).
Key(key).
Data(data).
Save(context.Background())
require.NoError(t, err)
// Get the data
fromCache, err := c.Cache.
Get().
Group(group).
Key(key).
Type(new(cacheTest)).
Fetch(context.Background())
require.NoError(t, err)
cast, ok := fromCache.(*cacheTest)
require.True(t, ok)
assert.Equal(t, data, *cast)
// The same key with the wrong group should fail
_, err = c.Cache.
Get().
Key(key).
Type(new(cacheTest)).
Fetch(context.Background())
assert.Error(t, err)
// Flush the data
err = c.Cache.
Flush().
Group(group).
Key(key).
Exec(context.Background())
require.NoError(t, err)
// The data should be gone
assertFlushed := func() {
// The data should be gone
_, err = c.Cache.
Get().
Group(group).
Key(key).
Type(new(cacheTest)).
Fetch(context.Background())
assert.Equal(t, redis.Nil, err)
}
assertFlushed()
// Set with tags
err = c.Cache.
Set().
Group(group).
Key(key).
Data(data).
Tags([]string{"tag1"}).
Save(context.Background())
require.NoError(t, err)
// Flush the tag
err = c.Cache.
Flush().
Tags([]string{"tag1"}).
Exec(context.Background())
require.NoError(t, err)
// The data should be gone
assertFlushed()
// Set with expiration
err = c.Cache.
Set().
Group(group).
Key(key).
Data(data).
Expiration(time.Millisecond).
Save(context.Background())
require.NoError(t, err)
// Wait for expiration
time.Sleep(time.Millisecond * 2)
// The data should be gone
assertFlushed()
}

View file

@ -7,9 +7,6 @@ import (
"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"
"github.com/eko/gocache/v2/cache"
"github.com/eko/gocache/v2/store"
"github.com/go-redis/redis/v8"
_ "github.com/jackc/pgx/v4/stdlib"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
@ -31,11 +28,8 @@ type Container struct {
// Config stores the application configuration
Config *config.Config
// Cache contains the cache interface
Cache *cache.Cache
// cacheClient stores the client to the underlying cache service
cacheClient *redis.Client
// Cache contains the cache client
Cache *CacheClient
// Database stores the connection to the database
Database *sql.DB
@ -70,7 +64,7 @@ func NewContainer() *Container {
// Shutdown shuts the Container down and disconnects all connections
func (c *Container) Shutdown() error {
if err := c.cacheClient.Close(); err != nil {
if err := c.Cache.Close(); err != nil {
return err
}
if err := c.ORM.Close(); err != nil {
@ -114,15 +108,10 @@ func (c *Container) initWeb() {
// initCache initializes the cache
func (c *Container) initCache() {
c.cacheClient = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", c.Config.Cache.Hostname, c.Config.Cache.Port),
Password: c.Config.Cache.Password,
})
if _, err := c.cacheClient.Ping(context.Background()).Result(); err != nil {
panic(fmt.Sprintf("failed to connect to cache server: %v", err))
var err error
if c.Cache, err = NewCacheClient(c.Config.Cache); err != nil {
panic(err)
}
cacheStore := store.NewRedis(c.cacheClient, nil)
c.Cache = cache.New(cacheStore)
}
// initDatabase initializes the database