Feature Request: Store for maypok86/otter cache (s3-fifo)
sgtsquiggs opened this issue · 0 comments
sgtsquiggs commented
I have preliminary work on this but it is not tested, vetted, and possibly is missing functionality from otter that would be useful. It is based on the ristretto store.
package cache
import (
"context"
"errors"
"fmt"
"strings"
"time"
lib_store "github.com/eko/gocache/lib/v4/store"
"github.com/maypok86/otter"
)
const (
// OtterType represents the storage type as a string value
OtterType = "otter"
// OtterTagPattern represents the tag pattern to be used as a key in specified storage
OtterTagPattern = "gocache_tag_%s"
)
// OtterClientInterface represents a maypok86/otter client
type OtterClientInterface interface {
Get(key string) (any, bool)
Set(key string, value any, ttl time.Duration) bool
Delete(key string)
Clear()
}
var _ OtterClientInterface = new(otter.CacheWithVariableTTL[string, any])
// OtterStore is a store for Otter (memory) library
type OtterStore struct {
client OtterClientInterface
options *lib_store.Options
}
// NewOtter creates a new store to Otter (memory) library instance
func NewOtter(client OtterClientInterface, options ...lib_store.Option) *OtterStore {
return &OtterStore{
client: client,
options: lib_store.ApplyOptions(options...),
}
}
// Get returns data stored from a given key
func (s *OtterStore) Get(_ context.Context, key any) (any, error) {
var err error
value, exists := s.client.Get(key.(string))
if !exists {
err = lib_store.NotFoundWithCause(errors.New("value not found in Otter store"))
}
return value, err
}
// GetWithTTL returns data stored from a given key and its corresponding TTL
func (s *OtterStore) GetWithTTL(ctx context.Context, key any) (any, time.Duration, error) {
value, err := s.Get(ctx, key)
return value, 0, err
}
// Set defines data in Otter memory cache for given key identifier
func (s *OtterStore) Set(ctx context.Context, key any, value any, options ...lib_store.Option) error {
opts := lib_store.ApplyOptionsWithDefault(s.options, options...)
var err error
if set := s.client.Set(key.(string), value, opts.Expiration); !set {
err = fmt.Errorf("error occurred while setting value '%v' on key '%v'", value, key)
}
if err != nil {
return err
}
if tags := opts.Tags; len(tags) > 0 {
s.setTags(ctx, key, tags)
}
return nil
}
func (s *OtterStore) setTags(ctx context.Context, key any, tags []string) {
for _, tag := range tags {
tagKey := fmt.Sprintf(OtterTagPattern, tag)
var cacheKeys []string
if result, err := s.Get(ctx, tagKey); err == nil {
if bytes, ok := result.([]byte); ok {
cacheKeys = strings.Split(string(bytes), ",")
}
}
alreadyInserted := false
for _, cacheKey := range cacheKeys {
if cacheKey == key.(string) {
alreadyInserted = true
break
}
}
if !alreadyInserted {
cacheKeys = append(cacheKeys, key.(string))
}
_ = s.Set(ctx, tagKey, []byte(strings.Join(cacheKeys, ",")), lib_store.WithExpiration(720*time.Hour))
}
}
// Delete removes data in Otter memory cache for given key identifier
func (s *OtterStore) Delete(_ context.Context, key any) error {
s.client.Delete(key.(string))
return nil
}
// Invalidate invalidates some cache data in Otter for given options
func (s *OtterStore) Invalidate(ctx context.Context, options ...lib_store.InvalidateOption) error {
opts := lib_store.ApplyInvalidateOptions(options...)
if tags := opts.Tags; len(tags) > 0 {
for _, tag := range tags {
tagKey := fmt.Sprintf(OtterTagPattern, tag)
result, err := s.Get(ctx, tagKey)
if err != nil {
return nil
}
var cacheKeys []string
if bytes, ok := result.([]byte); ok {
cacheKeys = strings.Split(string(bytes), ",")
}
for _, cacheKey := range cacheKeys {
_ = s.Delete(ctx, cacheKey)
}
}
}
return nil
}
// Clear resets all data in the store
func (s *OtterStore) Clear(_ context.Context) error {
s.client.Clear()
return nil
}
// GetType returns the store type
func (s *OtterStore) GetType() string {
return OtterType
}
I am using this as such:
otterCacheBuilder, err := otter.NewBuilder[string, any](defaultCapacity)
if err != nil {
return nil, fmt.Errorf("could not create cache: %w", err)
}
otterCache, err := otterCacheBuilder.WithVariableTTL().Build()
if err != nil {
return nil, fmt.Errorf("could not create cache: %w", err)
}
otterStore := NewOtter(otterCache, cachestore.WithExpiration(defaultTTL))