eko/gocache

Feature Request: Store for maypok86/otter cache (s3-fifo)

sgtsquiggs opened this issue · 0 comments

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))