2022-01-13 18:13:41 -08:00
|
|
|
package services
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-01-14 10:07:19 -08:00
|
|
|
"errors"
|
2022-01-13 18:13:41 -08:00
|
|
|
"fmt"
|
2024-06-22 07:34:26 -07:00
|
|
|
"sync"
|
2022-01-13 18:13:41 -08:00
|
|
|
"time"
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
"github.com/maypok86/otter"
|
2022-01-13 18:13:41 -08:00
|
|
|
)
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
// ErrCacheMiss indicates that the requested key does not exist in the cache
|
|
|
|
var ErrCacheMiss = errors.New("cache miss")
|
|
|
|
|
2022-01-13 18:13:41 -08:00
|
|
|
type (
|
2024-06-22 07:34:26 -07:00
|
|
|
// CacheStore provides an interface for cache storage
|
|
|
|
CacheStore interface {
|
|
|
|
// get attempts to get a cached value
|
|
|
|
get(context.Context, *CacheGetOp) (any, error)
|
|
|
|
|
|
|
|
// set attempts to set an entry in the cache
|
|
|
|
set(context.Context, *CacheSetOp) error
|
|
|
|
|
|
|
|
// flush removes a given key and/or tags from the cache
|
|
|
|
flush(context.Context, *CacheFlushOp) error
|
|
|
|
|
|
|
|
// close shuts down the cache storage
|
|
|
|
close()
|
|
|
|
}
|
|
|
|
|
2022-01-13 18:13:41 -08:00
|
|
|
// CacheClient is the client that allows you to interact with the cache
|
|
|
|
CacheClient struct {
|
2024-06-22 07:34:26 -07:00
|
|
|
// store holds the Cache storage
|
|
|
|
store CacheStore
|
2022-01-13 18:13:41 -08:00
|
|
|
}
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
// CacheSetOp handles chaining a set operation
|
|
|
|
CacheSetOp struct {
|
2022-01-13 18:13:41 -08:00
|
|
|
client *CacheClient
|
|
|
|
key string
|
|
|
|
group string
|
2023-12-16 08:07:20 -08:00
|
|
|
data any
|
2022-01-13 18:13:41 -08:00
|
|
|
expiration time.Duration
|
|
|
|
tags []string
|
|
|
|
}
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
// CacheGetOp handles chaining a get operation
|
|
|
|
CacheGetOp struct {
|
|
|
|
client *CacheClient
|
|
|
|
key string
|
|
|
|
group string
|
2022-01-13 18:13:41 -08:00
|
|
|
}
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
// CacheFlushOp handles chaining a flush operation
|
|
|
|
CacheFlushOp struct {
|
2022-01-13 18:13:41 -08:00
|
|
|
client *CacheClient
|
|
|
|
key string
|
|
|
|
group string
|
|
|
|
tags []string
|
|
|
|
}
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
// inMemoryCacheStore is a cache store implementation in memory
|
|
|
|
inMemoryCacheStore struct {
|
|
|
|
store *otter.CacheWithVariableTTL[string, any]
|
|
|
|
tagIndex *tagIndex
|
2022-02-06 07:07:25 -08:00
|
|
|
}
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
// tagIndex maintains an index to support cache tags for in-memory cache stores.
|
|
|
|
// There is a performance and memory impact to using cache tags since set and get operations using tags will require
|
|
|
|
// locking, and we need to keep track of this index in order to keep everything in sync.
|
|
|
|
// If using something like Redis for caching, you can leverage sets to store the index.
|
|
|
|
// Cache tags can be useful and convenient, so you should decide if your app benefits enough from this.
|
|
|
|
// As it stands here, there is no limiting how much memory this will consume and it will track all keys
|
|
|
|
// and tags added and removed from the cache. You could store these in the cache itself but allowing these to
|
|
|
|
// be evicted poses challenges.
|
|
|
|
tagIndex struct {
|
|
|
|
sync.Mutex
|
|
|
|
tags map[string]map[string]struct{} // tag->keys
|
|
|
|
keys map[string]map[string]struct{} // key->tags
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
// NewCacheClient creates a new cache client
|
|
|
|
func NewCacheClient(store CacheStore) *CacheClient {
|
|
|
|
return &CacheClient{store: store}
|
2022-01-13 18:13:41 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Close closes the connection to the cache
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheClient) Close() {
|
|
|
|
c.store.close()
|
2022-01-13 18:13:41 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set creates a cache set operation
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheClient) Set() *CacheSetOp {
|
|
|
|
return &CacheSetOp{
|
2022-01-13 18:13:41 -08:00
|
|
|
client: c,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get creates a cache get operation
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheClient) Get() *CacheGetOp {
|
|
|
|
return &CacheGetOp{
|
2022-01-13 18:13:41 -08:00
|
|
|
client: c,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Flush creates a cache flush operation
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheClient) Flush() *CacheFlushOp {
|
|
|
|
return &CacheFlushOp{
|
2022-01-13 18:13:41 -08:00
|
|
|
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
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheSetOp) Key(key string) *CacheSetOp {
|
2022-01-13 18:13:41 -08:00
|
|
|
c.key = key
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Group sets the cache group
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheSetOp) Group(group string) *CacheSetOp {
|
2022-01-13 18:13:41 -08:00
|
|
|
c.group = group
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Data sets the data to cache
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheSetOp) Data(data any) *CacheSetOp {
|
2022-01-13 18:13:41 -08:00
|
|
|
c.data = data
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Expiration sets the expiration duration of the cached data
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheSetOp) Expiration(expiration time.Duration) *CacheSetOp {
|
2022-01-13 18:13:41 -08:00
|
|
|
c.expiration = expiration
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tags sets the cache tags
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheSetOp) Tags(tags ...string) *CacheSetOp {
|
2022-01-13 18:13:41 -08:00
|
|
|
c.tags = tags
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save saves the data in the cache
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheSetOp) Save(ctx context.Context) error {
|
|
|
|
switch {
|
|
|
|
case c.key == "":
|
2022-01-14 10:07:19 -08:00
|
|
|
return errors.New("no cache key specified")
|
2024-06-22 07:34:26 -07:00
|
|
|
case c.data == nil:
|
|
|
|
return errors.New("no cache data specified")
|
|
|
|
case c.expiration == 0:
|
|
|
|
return errors.New("no cache expiration specified")
|
2022-01-14 10:07:19 -08:00
|
|
|
}
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
return c.client.store.set(ctx, c)
|
2022-01-13 18:13:41 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Key sets the cache key
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheGetOp) Key(key string) *CacheGetOp {
|
2022-01-13 18:13:41 -08:00
|
|
|
c.key = key
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Group sets the cache group
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheGetOp) Group(group string) *CacheGetOp {
|
2022-01-13 18:13:41 -08:00
|
|
|
c.group = group
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch fetches the data from the cache
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheGetOp) Fetch(ctx context.Context) (any, error) {
|
2022-01-14 10:07:19 -08:00
|
|
|
if c.key == "" {
|
|
|
|
return nil, errors.New("no cache key specified")
|
|
|
|
}
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
return c.client.store.get(ctx, c)
|
2022-01-13 18:13:41 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Key sets the cache key
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheFlushOp) Key(key string) *CacheFlushOp {
|
2022-01-13 18:13:41 -08:00
|
|
|
c.key = key
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Group sets the cache group
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheFlushOp) Group(group string) *CacheFlushOp {
|
2022-01-13 18:13:41 -08:00
|
|
|
c.group = group
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tags sets the cache tags
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheFlushOp) Tags(tags ...string) *CacheFlushOp {
|
2022-01-13 18:13:41 -08:00
|
|
|
c.tags = tags
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
2022-01-19 06:14:18 -08:00
|
|
|
// Execute flushes the data from the cache
|
2024-06-22 07:34:26 -07:00
|
|
|
func (c *CacheFlushOp) Execute(ctx context.Context) error {
|
|
|
|
return c.client.store.flush(ctx, c)
|
|
|
|
}
|
|
|
|
|
|
|
|
// newInMemoryCache creates a new in-memory CacheStore
|
|
|
|
func newInMemoryCache(capacity int) (CacheStore, error) {
|
|
|
|
s := &inMemoryCacheStore{
|
|
|
|
tagIndex: newTagIndex(),
|
|
|
|
}
|
|
|
|
|
|
|
|
store, err := otter.MustBuilder[string, any](capacity).
|
|
|
|
WithVariableTTL().
|
|
|
|
DeletionListener(func(key string, value any, cause otter.DeletionCause) {
|
|
|
|
s.tagIndex.purgeKeys(key)
|
|
|
|
}).
|
|
|
|
Build()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
s.store = &store
|
|
|
|
|
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *inMemoryCacheStore) get(_ context.Context, op *CacheGetOp) (any, error) {
|
|
|
|
v, exists := s.store.Get(op.client.cacheKey(op.group, op.key))
|
|
|
|
|
|
|
|
if !exists {
|
|
|
|
return nil, ErrCacheMiss
|
2022-01-13 18:13:41 -08:00
|
|
|
}
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *inMemoryCacheStore) set(_ context.Context, op *CacheSetOp) error {
|
|
|
|
key := op.client.cacheKey(op.group, op.key)
|
|
|
|
|
|
|
|
added := s.store.Set(
|
|
|
|
key,
|
|
|
|
op.data,
|
|
|
|
op.expiration,
|
|
|
|
)
|
|
|
|
|
|
|
|
if len(op.tags) > 0 {
|
|
|
|
s.tagIndex.setTags(key, op.tags...)
|
2022-01-13 18:13:41 -08:00
|
|
|
}
|
|
|
|
|
2024-06-22 07:34:26 -07:00
|
|
|
if !added {
|
|
|
|
return errors.New("cache set failed")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *inMemoryCacheStore) flush(_ context.Context, op *CacheFlushOp) error {
|
|
|
|
keys := make([]string, 0)
|
|
|
|
|
|
|
|
if key := op.client.cacheKey(op.group, op.key); key != "" {
|
|
|
|
keys = append(keys, key)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(op.tags) > 0 {
|
|
|
|
keys = append(keys, s.tagIndex.purgeTags(op.tags...)...)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, key := range keys {
|
|
|
|
s.store.Delete(key)
|
|
|
|
}
|
|
|
|
|
|
|
|
s.tagIndex.purgeKeys(keys...)
|
|
|
|
|
2022-01-13 18:13:41 -08:00
|
|
|
return nil
|
|
|
|
}
|
2024-06-22 07:34:26 -07:00
|
|
|
|
|
|
|
func (s *inMemoryCacheStore) close() {
|
|
|
|
s.store.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
func newTagIndex() *tagIndex {
|
|
|
|
return &tagIndex{
|
|
|
|
tags: make(map[string]map[string]struct{}),
|
|
|
|
keys: make(map[string]map[string]struct{}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *tagIndex) setTags(key string, tags ...string) {
|
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
if _, exists := i.keys[key]; !exists {
|
|
|
|
i.keys[key] = make(map[string]struct{})
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tag := range tags {
|
|
|
|
if _, exists := i.tags[tag]; !exists {
|
|
|
|
i.tags[tag] = make(map[string]struct{})
|
|
|
|
}
|
|
|
|
i.tags[tag][key] = struct{}{}
|
|
|
|
i.keys[key][tag] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *tagIndex) purgeTags(tags ...string) []string {
|
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
keys := make([]string, 0)
|
|
|
|
|
|
|
|
for _, tag := range tags {
|
|
|
|
if tagKeys, exists := i.tags[tag]; exists {
|
|
|
|
delete(i.tags, tag)
|
|
|
|
|
|
|
|
for key := range tagKeys {
|
|
|
|
delete(i.keys[key], tag)
|
|
|
|
if len(i.keys[key]) == 0 {
|
|
|
|
delete(i.keys, key)
|
|
|
|
}
|
|
|
|
|
|
|
|
keys = append(keys, key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return keys
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *tagIndex) purgeKeys(keys ...string) {
|
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
for _, key := range keys {
|
|
|
|
if keyTags, exists := i.keys[key]; exists {
|
|
|
|
delete(i.keys, key)
|
|
|
|
|
|
|
|
for tag := range keyTags {
|
|
|
|
delete(i.tags[tag], key)
|
|
|
|
if len(i.tags[tag]) == 0 {
|
|
|
|
delete(i.tags, tag)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|