Added custom cache client for much easier cache operations.
This commit is contained in:
parent
d412e06dad
commit
bfbb9669aa
8 changed files with 451 additions and 64 deletions
97
README.md
97
README.md
|
|
@ -75,6 +75,10 @@
|
||||||
* [File configuration](#file-configuration)
|
* [File configuration](#file-configuration)
|
||||||
* [Funcmap](#funcmap)
|
* [Funcmap](#funcmap)
|
||||||
* [Cache](#cache)
|
* [Cache](#cache)
|
||||||
|
* [Set data](#set-data)
|
||||||
|
* [Get data](#get-data)
|
||||||
|
* [Flush data](#flush-data)
|
||||||
|
* [Flush tags](#flush-tags)
|
||||||
* [Static files](#static-files)
|
* [Static files](#static-files)
|
||||||
* [Cache control headers](#cache-control-headers)
|
* [Cache control headers](#cache-control-headers)
|
||||||
* [Cache-buster](#cache-buster)
|
* [Cache-buster](#cache-buster)
|
||||||
|
|
@ -568,13 +572,7 @@ By default, the cache expiration time will be set according to the configuration
|
||||||
|
|
||||||
You can optionally specify cache tags for the `Page` by setting a slice of strings on `Page.Cache.Tags`. This provides the ability to build in cache invalidation logic in your application driven by events such as entity operations, for example.
|
You can optionally specify cache tags for the `Page` by setting a slice of strings on `Page.Cache.Tags`. This provides the ability to build in cache invalidation logic in your application driven by events such as entity operations, for example.
|
||||||
|
|
||||||
The cache client on the `Container` is currently handled by [gocache](https://github.com/eko/gocache) which makes it easy to perform operations such as tag-invalidation, for example:
|
You can use the [cache client](#cache) on the `Container` to easily [flush cache tags](#flush-tags), if needed.
|
||||||
|
|
||||||
```go
|
|
||||||
c.Cache.Invalidate(ctx, store.InvalidateOptions{
|
|
||||||
Tags: []string{"my-tag"},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Cache middleware
|
#### Cache middleware
|
||||||
|
|
||||||
|
|
@ -866,9 +864,90 @@ To include additional custom functions, add to the slice in `GetFuncMap()` and d
|
||||||
|
|
||||||
## Cache
|
## Cache
|
||||||
|
|
||||||
As previously mentioned, [Redis](https://redis.io/) was chosen as the cache but it can be easily swapped out for something else. [go-redis](https://github.com/go-redis/redis) is used as the underlying client but the `Container` currently only exposes [gocache](https://github.com/eko/gocache) which was chosen because it makes interfacing with the cache client much easier, and it provides a consistent interface if you were to use a cache backend other than Redis.
|
As previously mentioned, [Redis](https://redis.io/) was chosen as the cache but it can be easily swapped out for something else. [go-redis](https://github.com/go-redis/redis) is used as the underlying client but the `Container` contains a custom client wrapper (`CacheClient`) that makes typical cache operations extremely simple. This wrapper does expose the [go-redis]() client however, at `CacheClient.Client`, in case you have a need for it.
|
||||||
|
|
||||||
The built-in usage of the cache is currently only for optional [page caching](#cached-responses) but it can be used for practically anything.
|
The cache functionality within the `CacheClient` is powered by [gocache](https://github.com/eko/gocache) which was chosen because it makes interfacing with the cache service much easier, and it provides a consistent interface if you were to use a cache backend other than Redis.
|
||||||
|
|
||||||
|
The built-in usage of the cache is currently only for optional [page caching](#cached-responses) but it can be used for practically anything. See examples below:
|
||||||
|
|
||||||
|
### Set data
|
||||||
|
|
||||||
|
**Set data with just a key:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := c.Cache.
|
||||||
|
Set().
|
||||||
|
Key("my-key").
|
||||||
|
Data(myData).
|
||||||
|
Save(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Set data within a group:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := c.Cache.
|
||||||
|
Set().
|
||||||
|
Group("my-group")
|
||||||
|
Key("my-key").
|
||||||
|
Data(myData).
|
||||||
|
Save(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Include cache tags:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := c.Cache.
|
||||||
|
Set().
|
||||||
|
Key("my-key").
|
||||||
|
Tags([]string{"tag1", "tag2"})
|
||||||
|
Data(myData).
|
||||||
|
Save(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Include an expiration:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := c.Cache.
|
||||||
|
Set().
|
||||||
|
Key("my-key").
|
||||||
|
Expiration(time.Hour * 2)
|
||||||
|
Data(myData).
|
||||||
|
Save(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get data
|
||||||
|
|
||||||
|
```go
|
||||||
|
data, err := c.Cache.
|
||||||
|
Get().
|
||||||
|
Group("my-group").
|
||||||
|
Key("my-key").
|
||||||
|
Type(myType).
|
||||||
|
Fetch(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Type` method tells the cache what type of data you stored so it can be cast afterwards with: `result, ok := data.(myType)`
|
||||||
|
|
||||||
|
### Flush data
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := c.Cache.
|
||||||
|
Flush().
|
||||||
|
Group("my-group").
|
||||||
|
Key("my-key").
|
||||||
|
Exec(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flush tags
|
||||||
|
|
||||||
|
This will flush all cache entries that were tagged with the given tags.
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := c.Cache.
|
||||||
|
Flush().
|
||||||
|
Tags([]string{"tag1"}).
|
||||||
|
Exec(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
## Static files
|
## Static files
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,6 @@ import (
|
||||||
"github.com/mikestefanello/pagoda/middleware"
|
"github.com/mikestefanello/pagoda/middleware"
|
||||||
"github.com/mikestefanello/pagoda/services"
|
"github.com/mikestefanello/pagoda/services"
|
||||||
|
|
||||||
"github.com/eko/gocache/v2/marshaler"
|
|
||||||
|
|
||||||
"github.com/eko/gocache/v2/store"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -131,10 +127,6 @@ func (c *Controller) cachePage(ctx echo.Context, page Page, html *bytes.Buffer)
|
||||||
// The request URL is used as the cache key so the middleware can serve the
|
// The request URL is used as the cache key so the middleware can serve the
|
||||||
// cached page on matching requests
|
// cached page on matching requests
|
||||||
key := ctx.Request().URL.String()
|
key := ctx.Request().URL.String()
|
||||||
opts := &store.Options{
|
|
||||||
Expiration: page.Cache.Expiration,
|
|
||||||
Tags: page.Cache.Tags,
|
|
||||||
}
|
|
||||||
cp := middleware.CachedPage{
|
cp := middleware.CachedPage{
|
||||||
URL: key,
|
URL: key,
|
||||||
HTML: html.Bytes(),
|
HTML: html.Bytes(),
|
||||||
|
|
@ -142,16 +134,21 @@ func (c *Controller) cachePage(ctx echo.Context, page Page, html *bytes.Buffer)
|
||||||
StatusCode: ctx.Response().Status,
|
StatusCode: ctx.Response().Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := marshaler.New(c.Container.Cache).Set(ctx.Request().Context(), key, cp, opts)
|
err := c.Container.Cache.
|
||||||
if err != nil {
|
Set().
|
||||||
if !context.IsCanceledError(err) {
|
Group(middleware.CachedPageGroup).
|
||||||
|
Key(key).
|
||||||
|
Tags(page.Cache.Tags).
|
||||||
|
Expiration(page.Cache.Expiration).
|
||||||
|
Data(cp).
|
||||||
|
Save(ctx.Request().Context())
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
ctx.Logger().Info("cached page")
|
||||||
|
case !context.IsCanceledError(err):
|
||||||
ctx.Logger().Errorf("failed to cache page: %v", err)
|
ctx.Logger().Errorf("failed to cache page: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Logger().Infof("cached page")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect redirects to a given route name with optional route parameters
|
// Redirect redirects to a given route name with optional route parameters
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,6 @@ import (
|
||||||
"github.com/mikestefanello/pagoda/services"
|
"github.com/mikestefanello/pagoda/services"
|
||||||
"github.com/mikestefanello/pagoda/tests"
|
"github.com/mikestefanello/pagoda/tests"
|
||||||
|
|
||||||
"github.com/eko/gocache/v2/store"
|
|
||||||
|
|
||||||
"github.com/eko/gocache/v2/marshaler"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
|
@ -151,8 +147,12 @@ func TestController_RenderPage(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Fetch from the cache
|
// Fetch from the cache
|
||||||
res, err := marshaler.New(c.Cache).
|
res, err := c.Cache.
|
||||||
Get(context.Background(), p.URL, new(middleware.CachedPage))
|
Get().
|
||||||
|
Group(middleware.CachedPageGroup).
|
||||||
|
Key(p.URL).
|
||||||
|
Type(new(middleware.CachedPage)).
|
||||||
|
Fetch(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Compare the cached page
|
// Compare the cached page
|
||||||
|
|
@ -164,14 +164,19 @@ func TestController_RenderPage(t *testing.T) {
|
||||||
assert.Equal(t, rec.Body.Bytes(), cp.HTML)
|
assert.Equal(t, rec.Body.Bytes(), cp.HTML)
|
||||||
|
|
||||||
// Clear the tag
|
// Clear the tag
|
||||||
err = c.Cache.Invalidate(context.Background(), store.InvalidateOptions{
|
err = c.Cache.
|
||||||
Tags: []string{p.Cache.Tags[0]},
|
Flush().
|
||||||
})
|
Tags([]string{p.Cache.Tags[0]}).
|
||||||
|
Exec(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Refetch from the cache and expect no results
|
// Refetch from the cache and expect no results
|
||||||
_, err = marshaler.New(c.Cache).
|
_, err = c.Cache.
|
||||||
Get(context.Background(), p.URL, new(middleware.CachedPage))
|
Get().
|
||||||
|
Group(middleware.CachedPageGroup).
|
||||||
|
Key(p.URL).
|
||||||
|
Type(new(middleware.CachedPage)).
|
||||||
|
Fetch(context.Background())
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,15 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mikestefanello/pagoda/context"
|
"github.com/mikestefanello/pagoda/context"
|
||||||
|
"github.com/mikestefanello/pagoda/services"
|
||||||
|
|
||||||
"github.com/eko/gocache/v2/cache"
|
|
||||||
"github.com/eko/gocache/v2/marshaler"
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CachedPageGroup stores the cache group for cached pages
|
||||||
|
const CachedPageGroup = "page"
|
||||||
|
|
||||||
// CachedPage is what is used to store a rendered Page in the cache
|
// CachedPage is what is used to store a rendered Page in the cache
|
||||||
type CachedPage struct {
|
type CachedPage struct {
|
||||||
// URL stores the URL of the requested page
|
// URL stores the URL of the requested page
|
||||||
|
|
@ -31,7 +33,7 @@ type CachedPage struct {
|
||||||
// ServeCachedPage attempts to load a page from the cache by matching on the complete request URL
|
// ServeCachedPage attempts to load a page from the cache by matching on the complete request URL
|
||||||
// If a page is cached for the requested URL, it will be served here and the request terminated.
|
// If a page is cached for the requested URL, it will be served here and the request terminated.
|
||||||
// Any request made by an authenticated user or that is not a GET will be skipped.
|
// Any request made by an authenticated user or that is not a GET will be skipped.
|
||||||
func ServeCachedPage(ch *cache.Cache) echo.MiddlewareFunc {
|
func ServeCachedPage(ch *services.CacheClient) echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
// Skip non GET requests
|
// Skip non GET requests
|
||||||
|
|
@ -45,11 +47,13 @@ func ServeCachedPage(ch *cache.Cache) echo.MiddlewareFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to load from cache
|
// Attempt to load from cache
|
||||||
res, err := marshaler.New(ch).Get(
|
res, err := ch.
|
||||||
c.Request().Context(),
|
Get().
|
||||||
c.Request().URL.String(),
|
Group(CachedPageGroup).
|
||||||
new(CachedPage),
|
Key(c.Request().URL.String()).
|
||||||
)
|
Type(new(CachedPage)).
|
||||||
|
Fetch(c.Request().Context())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case err == redis.Nil:
|
case err == redis.Nil:
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/eko/gocache/v2/marshaler"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -25,7 +23,13 @@ func TestServeCachedPage(t *testing.T) {
|
||||||
}
|
}
|
||||||
cp.Headers["a"] = "b"
|
cp.Headers["a"] = "b"
|
||||||
cp.Headers["c"] = "d"
|
cp.Headers["c"] = "d"
|
||||||
err := marshaler.New(c.Cache).Set(context.Background(), cp.URL, cp, nil)
|
|
||||||
|
err := c.Cache.
|
||||||
|
Set().
|
||||||
|
Group(CachedPageGroup).
|
||||||
|
Key(cp.URL).
|
||||||
|
Data(cp).
|
||||||
|
Save(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Request the URL of the cached page
|
// Request the URL of the cached page
|
||||||
|
|
|
||||||
204
services/cache.go
Normal file
204
services/cache.go
Normal 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
105
services/cache_test.go
Normal 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()
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,6 @@ import (
|
||||||
|
|
||||||
"entgo.io/ent/dialect"
|
"entgo.io/ent/dialect"
|
||||||
entsql "entgo.io/ent/dialect/sql"
|
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/jackc/pgx/v4/stdlib"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/gommon/log"
|
"github.com/labstack/gommon/log"
|
||||||
|
|
@ -31,11 +28,8 @@ type Container struct {
|
||||||
// Config stores the application configuration
|
// Config stores the application configuration
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
|
|
||||||
// Cache contains the cache interface
|
// Cache contains the cache client
|
||||||
Cache *cache.Cache
|
Cache *CacheClient
|
||||||
|
|
||||||
// cacheClient stores the client to the underlying cache service
|
|
||||||
cacheClient *redis.Client
|
|
||||||
|
|
||||||
// Database stores the connection to the database
|
// Database stores the connection to the database
|
||||||
Database *sql.DB
|
Database *sql.DB
|
||||||
|
|
@ -70,7 +64,7 @@ func NewContainer() *Container {
|
||||||
|
|
||||||
// Shutdown shuts the Container down and disconnects all connections
|
// Shutdown shuts the Container down and disconnects all connections
|
||||||
func (c *Container) Shutdown() error {
|
func (c *Container) Shutdown() error {
|
||||||
if err := c.cacheClient.Close(); err != nil {
|
if err := c.Cache.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := c.ORM.Close(); err != nil {
|
if err := c.ORM.Close(); err != nil {
|
||||||
|
|
@ -114,15 +108,10 @@ func (c *Container) initWeb() {
|
||||||
|
|
||||||
// initCache initializes the cache
|
// initCache initializes the cache
|
||||||
func (c *Container) initCache() {
|
func (c *Container) initCache() {
|
||||||
c.cacheClient = redis.NewClient(&redis.Options{
|
var err error
|
||||||
Addr: fmt.Sprintf("%s:%d", c.Config.Cache.Hostname, c.Config.Cache.Port),
|
if c.Cache, err = NewCacheClient(c.Config.Cache); err != nil {
|
||||||
Password: c.Config.Cache.Password,
|
panic(err)
|
||||||
})
|
|
||||||
if _, err := c.cacheClient.Ping(context.Background()).Result(); err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to connect to cache server: %v", err))
|
|
||||||
}
|
}
|
||||||
cacheStore := store.NewRedis(c.cacheClient, nil)
|
|
||||||
c.Cache = cache.New(cacheStore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initDatabase initializes the database
|
// initDatabase initializes the database
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue