Skip to content

Commit

Permalink
feat(storers): add otter storage
Browse files Browse the repository at this point in the history
  • Loading branch information
darkweak committed Mar 11, 2024
1 parent 647f9d8 commit 280e6ca
Show file tree
Hide file tree
Showing 10 changed files with 407 additions and 3 deletions.
7 changes: 7 additions & 0 deletions configurationtypes/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ type DefaultCache struct {
Mode string `json:"mode" yaml:"mode"`
Nuts CacheProvider `json:"nuts" yaml:"nuts"`
Olric CacheProvider `json:"olric" yaml:"olric"`
Otter CacheProvider `json:"otter" yaml:"otter"`
Redis CacheProvider `json:"redis" yaml:"redis"`
Port Port `json:"port" yaml:"port"`
Regex Regex `json:"regex" yaml:"regex"`
Expand Down Expand Up @@ -286,6 +287,11 @@ func (d *DefaultCache) GetNuts() CacheProvider {
return d.Nuts
}

// GetOtter returns otter configuration
func (d *DefaultCache) GetOtter() CacheProvider {
return d.Otter
}

// GetOlric returns olric configuration
func (d *DefaultCache) GetOlric() CacheProvider {
return d.Olric
Expand Down Expand Up @@ -340,6 +346,7 @@ type DefaultCacheInterface interface {
GetDistributed() bool
GetEtcd() CacheProvider
GetMode() string
GetOtter() CacheProvider
GetNuts() CacheProvider
GetOlric() CacheProvider
GetRedis() CacheProvider
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/fsnotify/fsnotify v1.6.0
github.com/google/uuid v1.3.1
github.com/imdario/mergo v0.3.13
github.com/maypok86/otter v1.1.1
github.com/nutsdb/nutsdb v0.14.3
github.com/pquerna/cachecontrol v0.2.0
github.com/prometheus/client_golang v1.16.0
Expand All @@ -29,6 +30,9 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dolthub/swiss v0.2.1 // indirect
github.com/gammazero/deque v0.2.1 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczC
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw=
github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
Expand All @@ -74,6 +78,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0=
github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
Expand Down Expand Up @@ -182,6 +188,8 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/maypok86/otter v1.1.1 h1:1WyOfBz2m8bjc4dAmJeUSuyUshoTP8ViiYmvRWO+53w=
github.com/maypok86/otter v1.1.1/go.mod h1:IuSnpxeUyjKPPjqGzhGKOO26tedMNl45vwGcdXsEi8U=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.45/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
Expand Down
198 changes: 198 additions & 0 deletions pkg/storage/otterProvider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package storage

import (
"bufio"
"bytes"
"fmt"
"net/http"
"regexp"
"strings"
"time"

t "github.com/darkweak/souin/configurationtypes"
"github.com/darkweak/souin/pkg/rfc"
"github.com/darkweak/souin/pkg/storage/types"
"github.com/maypok86/otter"
"go.uber.org/zap"
)

// Otter provider type
type Otter struct {
cache *otter.CacheWithVariableTTL[string, []byte]
stale time.Duration
logger *zap.Logger
}

// OtterConnectionFactory function create new Otter instance
func OtterConnectionFactory(c t.AbstractConfigurationInterface) (types.Storer, error) {
cache, e := otter.MustBuilder[string, []byte](10_000).
CollectStats().
Cost(func(key string, value []byte) uint32 {
return 1
}).
WithVariableTTL().
Build()

if e != nil {
c.GetLogger().Sugar().Error("Impossible to instanciate the Otter DB.", e)
}

dc := c.GetDefaultCache()

return &Otter{cache: &cache, logger: c.GetLogger(), stale: dc.GetStale()}, nil
}

// Name returns the storer name
func (provider *Otter) Name() string {
return "OTTER"
}

// MapKeys method returns a map with the key and value
func (provider *Otter) MapKeys(prefix string) map[string]string {
keys := map[string]string{}

provider.cache.Range(func(key string, val []byte) bool {
if !strings.HasPrefix(key, prefix) {
k, _ := strings.CutPrefix(string(key), prefix)
keys[k] = string(val)
}

return true
})

return keys
}

// ListKeys method returns the list of existing keys
func (provider *Otter) ListKeys() []string {
keys := []string{}

provider.cache.Range(func(key string, _ []byte) bool {
if !strings.Contains(key, surrogatePrefix) {
keys = append(keys, key)
}

return true
})

return keys
}

// Get method returns the populated response if exists, empty response then
func (provider *Otter) Get(key string) []byte {
result, found := provider.cache.Get(key)
if !found {
provider.logger.Sugar().Errorf("Impossible to get the key %s in Otter", key)
}

return result
}

// Prefix method returns the populated response if exists, empty response then
func (provider *Otter) Prefix(key string, req *http.Request, validator *rfc.Revalidator) *http.Response {
var result *http.Response
provider.cache.Range(func(k string, val []byte) bool {
if varyVoter(key, req, k) {
if res, err := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(val)), req); err == nil {
rfc.ValidateETag(res, validator)
if validator.Matched {
provider.logger.Sugar().Debugf("The stored key %s matched the current iteration key ETag %+v", k, validator)
result = res
return false
} else {
provider.logger.Sugar().Debugf("The stored key %s didn't match the current iteration key ETag %+v", k, validator)
}
} else {
provider.logger.Sugar().Errorf("An error occured while reading response for the key %s: %v", k, err)
}
}

return true
})

return result
}

// GetMultiLevel tries to load the key and check if one of linked keys is a fresh/stale candidate.
func (provider *Otter) GetMultiLevel(key string, req *http.Request, validator *rfc.Revalidator) (fresh *http.Response, stale *http.Response) {
val, found := provider.cache.Get(MappingKeyPrefix + key)
if !found {
provider.logger.Sugar().Errorf("Impossible to get the mapping key %s in Otter", MappingKeyPrefix+key)
return
}

fresh, stale, _ = mappingElection(provider, val, req, validator, provider.logger)
return
}

// SetMultiLevel tries to store the key with the given value and update the mapping key to store metadata.
func (provider *Otter) SetMultiLevel(baseKey, variedKey string, value []byte, variedHeaders http.Header, etag string, duration time.Duration) error {
now := time.Now()
inserted := provider.cache.Set(variedKey, value, duration)
if !inserted {
provider.logger.Sugar().Errorf("Impossible to set value into Otter, too large for the cost function")
return nil
}

mappingKey := MappingKeyPrefix + baseKey
item, found := provider.cache.Get(mappingKey)
if !found {
provider.logger.Sugar().Errorf("Impossible to get the base key %s in Otter", mappingKey)
return nil
}

val, e := mappingUpdater(variedKey, item, provider.logger, now, now.Add(duration), now.Add(duration+provider.stale), variedHeaders, etag)
if e != nil {
return e
}

provider.logger.Sugar().Debugf("Store the new mapping for the key %s in Badger, %v", variedKey, string(val))
// Used to calculate -(now * 2)
negativeNow, _ := time.ParseDuration(fmt.Sprintf("-%d", time.Now().Nanosecond()*2))
inserted = provider.cache.Set(mappingKey, val, negativeNow)
if !inserted {
provider.logger.Sugar().Errorf("Impossible to set value into Otter, too large for the cost function")
return nil
}

return nil
}

// Set method will store the response in Badger provider
func (provider *Otter) Set(key string, value []byte, url t.URL, duration time.Duration) error {
inserted := provider.cache.Set(key, value, duration)
if !inserted {
provider.logger.Sugar().Errorf("Impossible to set value into Otter, too large for the cost function")
}

return nil
}

// Delete method will delete the response in Badger provider if exists corresponding to key param
func (provider *Otter) Delete(key string) {
provider.cache.Delete(key)
}

// DeleteMany method will delete the responses in Badger provider if exists corresponding to the regex key param
func (provider *Otter) DeleteMany(key string) {
re, e := regexp.Compile(key)
if e != nil {
return
}

provider.cache.DeleteByFunc(func(k string, value []byte) bool {
return re.MatchString(k)
})
}

// Init method will
func (provider *Otter) Init() error {
return nil
}

// Reset method will reset or close provider
func (provider *Otter) Reset() error {
provider.cache.Clear()

return nil
}
Loading

0 comments on commit 280e6ca

Please sign in to comment.