eko / gocache

☔️ A complete Go cache library that brings you multiple ways of managing your caches
https://vincent.composieux.fr/article/i-wrote-gocache-a-complete-and-extensible-go-cache-library/
MIT License
2.4k stars 193 forks source link

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

Open sgtsquiggs opened 6 months ago

sgtsquiggs commented 6 months ago

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