Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
sgtsquiggs opened this issue Feb 7, 2024 · 0 comments
Open

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

sgtsquiggs opened this issue Feb 7, 2024 · 0 comments

Comments

@sgtsquiggs
Copy link
Contributor

sgtsquiggs commented Feb 7, 2024

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))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant