diff --git a/configurationtypes/types.go b/configurationtypes/types.go index 884df845f..3bf6e52f5 100644 --- a/configurationtypes/types.go +++ b/configurationtypes/types.go @@ -232,6 +232,7 @@ type DefaultCache struct { Etcd CacheProvider `json:"etcd" yaml:"etcd"` Mode string `json:"mode" yaml:"mode"` Nuts CacheProvider `json:"nuts" yaml:"nuts"` + NutsMemcached CacheProvider `json:"nuts_memcached" yaml:"nuts_memcached"` Olric CacheProvider `json:"olric" yaml:"olric"` Otter CacheProvider `json:"otter" yaml:"otter"` Redis CacheProvider `json:"redis" yaml:"redis"` @@ -300,6 +301,11 @@ func (d *DefaultCache) GetOtter() CacheProvider { return d.Otter } +// GetNutsMemcached returns nuts_memcached configuration +func (d *DefaultCache) GetNutsMemcached() CacheProvider { + return d.NutsMemcached +} + // GetOlric returns olric configuration func (d *DefaultCache) GetOlric() CacheProvider { return d.Olric @@ -356,6 +362,7 @@ type DefaultCacheInterface interface { GetMode() string GetOtter() CacheProvider GetNuts() CacheProvider + GetNutsMemcached() CacheProvider GetOlric() CacheProvider GetRedis() CacheProvider GetHeaders() []string diff --git a/go.mod b/go.mod index 268430fd1..b0eddaf78 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/darkweak/souin go 1.21 require ( + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 github.com/buraksezer/olric v0.5.4 github.com/cespare/xxhash/v2 v2.2.0 github.com/dgraph-io/badger/v3 v3.2103.5 diff --git a/go.sum b/go.sum index d1f5edd0b..b651fce13 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bits-and-blooms/bitset v1.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8= github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/buraksezer/consistent v0.10.0 h1:hqBgz1PvNLC5rkWcEBVAL9dFMBWz6I0VgUCW25rrZlU= github.com/buraksezer/consistent v0.10.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw= github.com/buraksezer/olric v0.5.4 h1:LDgLIfVoyol4qzdNirrrDUKqzFw0yDsa7ukvLrpP4cU= diff --git a/pkg/api/souin.go b/pkg/api/souin.go index 4d415a4d3..0e430b6de 100644 --- a/pkg/api/souin.go +++ b/pkg/api/souin.go @@ -135,12 +135,13 @@ func (s *SouinAPI) listKeys(search string) []string { } var storageToInfiniteTTLMap = map[string]time.Duration{ - "BADGER": 365 * 24 * time.Hour, - "ETCD": 365 * 24 * time.Hour, - "NUTS": 0, - "OLRIC": 365 * 24 * time.Hour, - "OTTER": 365 * 24 * time.Hour, - "REDIS": 0, + "BADGER": 365 * 24 * time.Hour, + "ETCD": 365 * 24 * time.Hour, + "NUTS": 0, + "NUTS_MEMCACHED": 0, + "OLRIC": 365 * 24 * time.Hour, + "OTTER": 365 * 24 * time.Hour, + "REDIS": 0, } func (s *SouinAPI) purgeMapping() { diff --git a/pkg/storage/nutsMemcachedProvider.go b/pkg/storage/nutsMemcachedProvider.go new file mode 100644 index 000000000..470776c46 --- /dev/null +++ b/pkg/storage/nutsMemcachedProvider.go @@ -0,0 +1,473 @@ +package storage + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "strconv" + "strings" + "time" + + "github.com/darkweak/souin/configurationtypes" + t "github.com/darkweak/souin/configurationtypes" + "github.com/darkweak/souin/pkg/rfc" + "github.com/darkweak/souin/pkg/storage/types" + "github.com/dgraph-io/ristretto" + "github.com/imdario/mergo" + "github.com/nutsdb/nutsdb" + lz4 "github.com/pierrec/lz4/v4" + "go.uber.org/zap" +) + +var nutsMemcachedInstanceMap = map[string]*nutsdb.DB{} + +// Why NutsMemcached? +// --- +// The NutsMemcached storage backend is composed of two different storage backends: +// 1. NutsDB: for the cache key index (i.e., IDX_ keys). +// 2. Memcached: for the cache content. +// There are two storage backends because: +// 1. is a "non forgetting" storage backend (NutsDB, for the index). Keys will be kept until their TTL expires. +// → if it was handled by a storage backend that can preemptively evict, you might evict IDX_ keys, which you wouldn't want. +// You need to make sure index and content stays in sync. +// 2. is "forgetting" storage backend (Memcached, for the data). Cache data will be pre-emptively evicted (i.e., before TTL is reached). +// → it makes it possible to put limits on total RAM/disk usage. + +// NutsMemcached provider type +type NutsMemcached struct { + *nutsdb.DB + bucketName string + stale time.Duration + logger *zap.Logger + //memcacheClient *memcache.Client + ristrettoCache *ristretto.Cache +} + +// Below is already defined in the original Nuts provider. +/* const ( + bucket = "souin-bucket" + nutsLimit = 1 << 16 +) + +func sanitizeProperties(m map[string]interface{}) map[string]interface{} { + iotas := []string{"RWMode", "StartFileLoadingMode"} + for _, i := range iotas { + if v := m[i]; v != nil { + currentMode := nutsdb.FileIO + switch v { + case 1: + currentMode = nutsdb.MMap + } + m[i] = currentMode + } + } + + for _, i := range []string{"SegmentSize", "NodeNum", "MaxFdNumsInCache"} { + if v := m[i]; v != nil { + m[i], _ = v.(int64) + } + } + + if v := m["EntryIdxMode"]; v != nil { + m["EntryIdxMode"] = nutsdb.HintKeyValAndRAMIdxMode + switch v { + case 1: + m["EntryIdxMode"] = nutsdb.HintKeyAndRAMIdxMode + } + } + + if v := m["SyncEnable"]; v != nil { + m["SyncEnable"] = true + if b, ok := v.(bool); ok { + m["SyncEnable"] = b + } else if s, ok := v.(string); ok { + m["SyncEnable"], _ = strconv.ParseBool(s) + } + } + + return m +} */ + +// NutsConnectionFactory function create new NutsMemcached instance +func NutsMemcachedConnectionFactory(c t.AbstractConfigurationInterface) (types.Storer, error) { + dc := c.GetDefaultCache() + cacheName := dc.GetCacheName() + nutsConfiguration := dc.GetNutsMemcached() + nutsOptions := nutsdb.DefaultOptions + nutsOptions.Dir = "/tmp/souin-nuts-memcached" + + // `HintKeyAndRAMIdxMode` represents ram index (only key) mode. + nutsOptions.EntryIdxMode = nutsdb.HintKeyAndRAMIdxMode + // `HintBPTSparseIdxMode` represents b+ tree sparse index mode. + // Note: this mode was removed after v0.14.0 + // Use: github.com/nutsdb/nutsdb v0.14.0 + //nutsOptions.EntryIdxMode = nutsdb.HintBPTSparseIdxMode + + // EntryIdxMode will affect the size of the key index in memory. + // → since this storage backend has no limit on memory usage, it has to be chosen depending on + // the max number of cache keys that will be kept in flight. + + if nutsConfiguration.Configuration != nil { + var parsedNuts nutsdb.Options + nutsConfiguration.Configuration = sanitizeProperties(nutsConfiguration.Configuration.(map[string]interface{})) + if b, e := json.Marshal(nutsConfiguration.Configuration); e == nil { + if e = json.Unmarshal(b, &parsedNuts); e != nil { + c.GetLogger().Sugar().Error("Impossible to parse the configuration for the Nuts provider", e) + } + } + + if err := mergo.Merge(&nutsOptions, parsedNuts, mergo.WithOverride); err != nil { + c.GetLogger().Sugar().Error("An error occurred during the nutsOptions merge from the default options with your configuration.") + } + } else { + nutsOptions.RWMode = nutsdb.MMap + if nutsConfiguration.Path != "" { + nutsOptions.Dir = nutsConfiguration.Path + } + } + + // Ristretto config + var numCounters int64 = 1e7 // number of keys to track frequency of (10M). + var maxCost int64 = 1 << 30 // maximum cost of cache (1GB). + if nutsConfiguration.Configuration != nil { + rawNumCounters, ok := nutsConfiguration.Configuration.(map[string]interface{})["NumCounters"] + if ok { + numCounters, _ = strconv.ParseInt(rawNumCounters.(string), 10, 64) + } + + rawMaxCost, ok := nutsConfiguration.Configuration.(map[string]interface{})["MaxCost"] + if ok { + maxCost, _ = strconv.ParseInt(rawMaxCost.(string), 10, 64) + } + } + // See https://github.com/dgraph-io/ristretto?tab=readme-ov-file#example + ristrettoCache, err := ristretto.NewCache(&ristretto.Config{ + NumCounters: numCounters, + MaxCost: maxCost, + BufferItems: 64, // number of keys per Get buffer. + }) + if err != nil { + c.GetLogger().Sugar().Error("Impossible to make new Ristretto cache.", err) + return nil, err + } + //c.GetLogger().Sugar().Debugf("Ristretto cache was created with NumCounters=%d, MaxCost=%d", numCounters, maxCost) + + // If multiple caches are created on the same directory, reuse the same NutsDB instance. + // E.g., in automated tests. + if instance, ok := nutsMemcachedInstanceMap[nutsOptions.Dir]; ok && instance != nil { + return &NutsMemcached{ + DB: instance, + bucketName: cacheName, + stale: dc.GetStale(), + logger: c.GetLogger(), + //memcacheClient: memcache.New("127.0.0.1:11211"), // hardcoded for now + ristrettoCache: ristrettoCache, + }, nil + } + + db, e := nutsdb.Open(nutsOptions) + if e != nil { + c.GetLogger().Sugar().Error("Impossible to open the Nuts DB.", e) + return nil, e + } + + instance := &NutsMemcached{ + DB: db, + bucketName: cacheName, + stale: dc.GetStale(), + logger: c.GetLogger(), + //memcacheClient: memcache.New("127.0.0.1:11211"), // hardcoded for now + ristrettoCache: ristrettoCache, + } + nutsMemcachedInstanceMap[nutsOptions.Dir] = instance.DB + + return instance, nil +} + +// Name returns the storer name +func (provider *NutsMemcached) Name() string { + return "NUTS_MEMCACHED" +} + +// ListKeys method returns the list of existing keys +func (provider *NutsMemcached) ListKeys() []string { + keys := []string{} + + e := provider.DB.View(func(tx *nutsdb.Tx) error { + e, _ := tx.PrefixScan(provider.bucketName, []byte(MappingKeyPrefix), 0, 100) + for _, k := range e { + mapping, err := decodeMapping(k.Value) + if err == nil { + for _, v := range mapping.Mapping { + keys = append(keys, v.RealKey) + } + } + } + return nil + }) + + if e != nil { + return []string{} + } + + return keys +} + +// MapKeys method returns the map of existing keys +func (provider *NutsMemcached) MapKeys(prefix string) map[string]string { + keys := map[string]string{} + + e := provider.DB.View(func(tx *nutsdb.Tx) error { + e, _ := tx.GetAll(provider.bucketName) + for _, k := range e { + if strings.HasPrefix(string(k.Key), prefix) { + nk, _ := strings.CutPrefix(string(k.Key), prefix) + keys[nk] = string(k.Value) + } + } + return nil + }) + + if e != nil { + return map[string]string{} + } + + return keys +} + +// Get method returns the populated response if exists, empty response then +func (provider *NutsMemcached) Get(key string) (item []byte) { + memcachedKey, _ := provider.getFromNuts(key) + + if memcachedKey != "" { + item, _ = provider.getFromMemcached(memcachedKey) + } + + return +} + +// Prefix method returns the populated response if exists, empty response then +func (provider *NutsMemcached) Prefix(key string) []string { + result := []string{} + + _ = provider.DB.View(func(tx *nutsdb.Tx) error { + prefix := []byte(key) + + if entries, err := tx.PrefixSearchScan(provider.bucketName, prefix, "^({|$)", 0, 50); err != nil { + return err + } else { + for _, entry := range entries { + result = append(result, string(entry.Key)) + } + } + return nil + }) + + return result +} + +// GetMultiLevel tries to load the key and check if one of linked keys is a fresh/stale candidate. +func (provider *NutsMemcached) GetMultiLevel(key string, req *http.Request, validator *rfc.Revalidator) (fresh *http.Response, stale *http.Response) { + _ = provider.DB.View(func(tx *nutsdb.Tx) error { + i, e := tx.Get(provider.bucketName, []byte(MappingKeyPrefix+key)) + if e != nil && !errors.Is(e, nutsdb.ErrKeyNotFound) { + return e + } + + var val []byte + if i != nil { + val = i.Value + } + fresh, stale, e = mappingElection(provider, val, req, validator, provider.logger) + + return e + }) + + return +} + +// SetMultiLevel tries to store the key with the given value and update the mapping key to store metadata. +func (provider *NutsMemcached) SetMultiLevel(baseKey, variedKey string, value []byte, variedHeaders http.Header, etag string, duration time.Duration, realKey string) error { + now := time.Now() + + compressed := new(bytes.Buffer) + if _, err := lz4.NewWriter(compressed).ReadFrom(bytes.NewReader(value)); err != nil { + provider.logger.Sugar().Errorf("Impossible to compress the key %s into Nuts, %v", variedKey, err) + return err + } + { + // matchedURL is only use when ttl == 0 + ttl := duration + provider.stale + url := t.URL{ + TTL: configurationtypes.Duration{Duration: ttl}, + } + err := provider.Set(variedKey, compressed.Bytes(), url, ttl) + if err != nil { + return err + } + } + + err := provider.DB.Update(func(tx *nutsdb.Tx) error { + mappingKey := MappingKeyPrefix + baseKey + item, e := tx.Get(provider.bucketName, []byte(mappingKey)) + if e != nil && !errors.Is(e, nutsdb.ErrKeyNotFound) { + provider.logger.Sugar().Errorf("Impossible to get the base key %s in Nuts, %v", baseKey, e) + return e + } + + var val []byte + if item != nil { + val = item.Value + } + + val, e = mappingUpdater(variedKey, val, provider.logger, now, now.Add(duration), now.Add(duration+provider.stale), variedHeaders, etag, realKey) + if e != nil { + return e + } + + provider.logger.Sugar().Debugf("Store the new mapping for the key %s in Nuts", variedKey) + + return tx.Put(provider.bucketName, []byte(mappingKey), val, nutsdb.Persistent) + }) + + if err != nil { + provider.logger.Sugar().Errorf("Impossible to set value into Nuts, %v", err) + } + + return err +} + +// Set method will store the response in Nuts provider +func (provider *NutsMemcached) Set(key string, value []byte, url t.URL, duration time.Duration) error { + if duration == 0 { + duration = url.TTL.Duration + } + // Only for memcached (to overcome 250 bytes key limit) + //memcachedKey := uuid.New().String() + // Disabled for ristretto to improve performances + memcachedKey := key + + // set to nuts + { + err := provider.DB.Update(func(tx *nutsdb.Tx) error { + // key: cache-key, value: memcached-key + return tx.Put(provider.bucketName, []byte(key), []byte(memcachedKey), uint32(duration.Seconds())) + }) + + if err != nil { + provider.logger.Sugar().Errorf("Impossible to set value into Nuts, %v", err) + return err + } + } + + // set to memcached + _ = provider.setToMemcached(memcachedKey, value, duration) + return nil +} + +// Delete method will delete the response in Nuts provider if exists corresponding to key param +func (provider *NutsMemcached) Delete(key string) { + memcachedKey, _ := provider.getFromNuts(key) + + // delete from memcached + if memcachedKey != "" { + _ = provider.delFromMemcached(memcachedKey) + } + + // delete from nuts + _ = provider.DB.Update(func(tx *nutsdb.Tx) error { + return tx.Delete(provider.bucketName, []byte(key)) + }) +} + +// DeleteMany method will delete the responses in Nuts provider if exists corresponding to the regex key param +func (provider *NutsMemcached) DeleteMany(keyReg string) { + _ = provider.DB.Update(func(tx *nutsdb.Tx) error { + if entries, err := tx.PrefixSearchScan(provider.bucketName, []byte(""), keyReg, 0, nutsLimit); err != nil { + return err + } else { + for _, entry := range entries { + // delete from memcached + _ = provider.delFromMemcached(string(entry.Value)) + // delete from nuts + _ = tx.Delete(provider.bucketName, entry.Key) + } + } + return nil + }) +} + +// Init method will +func (provider *NutsMemcached) Init() error { + return nil +} + +// Reset method will reset or close provider +func (provider *NutsMemcached) Reset() error { + return provider.DB.Update(func(tx *nutsdb.Tx) error { + return tx.DeleteBucket(1, provider.bucketName) + }) +} + +func (provider *NutsMemcached) getFromNuts(nutsKey string) (memcachedKey string, err error) { + err = provider.DB.View(func(tx *nutsdb.Tx) error { + i, e := tx.Get(provider.bucketName, []byte(nutsKey)) + if i != nil { + memcachedKey = string(i.Value) + } + return e + }) + return +} + +// Reminder: the memcachedKey must be at most 250 bytes in length +func (provider *NutsMemcached) setToMemcached(memcachedKey string, value []byte, ttl time.Duration) (err error) { + //fmt.Println("memcached SET", key) + // err = provider.memcacheClient.Set( + // &memcache.Item{ + // Key: memcachedKey, + // Value: value, + // Expiration: ttl, + // }, + // ) + //if err != nil { + // provider.logger.Sugar().Errorf("Failed to set into memcached, %v", err) + // } + + ok := provider.ristrettoCache.SetWithTTL(memcachedKey, value, int64(len(value)), ttl) + if !ok { + provider.logger.Sugar().Debugf( + "Value not set to ristretto cache, key=%v ttl=%.2fs len=%d", + memcachedKey, ttl.Seconds(), len(value), + ) + // Note: failed to store is not considered an error because Ristretto doesn't guarantee + // a value is set or not. + // See https://pkg.go.dev/github.com/dgraph-io/ristretto@v0.1.1#Cache.Set + } + return +} + +// Reminder: the memcachedKey must be at most 250 bytes in length +func (provider *NutsMemcached) getFromMemcached(memcachedKey string) (value []byte, err error) { + //fmt.Println("memcached GET", key) + // i, err := provider.memcacheClient.Get(memcachedKey) + // if err == nil && i != nil { + // value = i.Value + // } else { + // provider.logger.Sugar().Errorf("Failed to get from memcached, %v", err) + // } + rawValue, found := provider.ristrettoCache.Get(memcachedKey) + if !found { + provider.logger.Sugar().Debugf("Failed to get from cache, key=%v", memcachedKey) + return nil, errors.New("failed to get from cache") + } + value = rawValue.([]byte) + return +} + +func (provider *NutsMemcached) delFromMemcached(memcachedKey string) (err error) { + //err = provider.memcacheClient.Delete(memcachedKey) + provider.ristrettoCache.Del(memcachedKey) + return +} diff --git a/pkg/storage/nutsMemcachedProvider_test.go b/pkg/storage/nutsMemcachedProvider_test.go new file mode 100644 index 000000000..d18d10840 --- /dev/null +++ b/pkg/storage/nutsMemcachedProvider_test.go @@ -0,0 +1,110 @@ +package storage + +import ( + "fmt" + "testing" + + "github.com/darkweak/souin/pkg/storage/types" + "github.com/darkweak/souin/tests" + + "time" + + "github.com/darkweak/souin/configurationtypes" + "github.com/darkweak/souin/errors" +) + +func getNutsMemcachedClientAndMatchedURL(key string) (types.Storer, configurationtypes.URL) { + return GetCacheProviderClientAndMatchedURL( + key, + func() configurationtypes.AbstractConfigurationInterface { + return tests.MockConfiguration(tests.NutsConfiguration) + }, + func(config configurationtypes.AbstractConfigurationInterface) (types.Storer, error) { + provider, _ := NutsMemcachedConnectionFactory(config) + _ = provider.Init() + + return provider, nil + }, + ) +} + +func TestNutsMemcached_ConnectionFactory(t *testing.T) { + c := tests.MockConfiguration(tests.NutsConfiguration) + r, err := NutsMemcachedConnectionFactory(c) + + if nil != err { + errors.GenerateError(t, "Shouldn't have panic") + } + + if nil == r { + errors.GenerateError(t, "Nuts should be instanciated") + } + + if nil == r.(*NutsMemcached).DB { + errors.GenerateError(t, "Nuts database should be accesible") + } +} + +func TestNutsMemcached_IShouldBeAbleToReadAndWriteData(t *testing.T) { + client, matchedURL := getNutsMemcachedClientAndMatchedURL("Test") + + _ = client.Set("Test", []byte(BASE_VALUE), matchedURL, time.Duration(20)*time.Second) + time.Sleep(1 * time.Second) + + res := client.Get("Test") + if res == nil || len(res) <= 0 { + errors.GenerateError(t, fmt.Sprintf("Key '%s' should exist", BASE_VALUE)) + } + if BASE_VALUE != string(res) { + errors.GenerateError(t, fmt.Sprintf("'%s' not corresponding to '%s'", string(res), BASE_VALUE)) + } +} + +func TestNutsMemcached_GetRequestInCache(t *testing.T) { + c := tests.MockConfiguration(tests.NutsConfiguration) + client, _ := NutsMemcachedConnectionFactory(c) + res := client.Get(NONEXISTENTKEY) + if 0 < len(res) { + errors.GenerateError(t, fmt.Sprintf("Key %s should not exist", NONEXISTENTKEY)) + } +} + +func TestNutsMemcached_GetSetRequestInCache_OneByte(t *testing.T) { + client, matchedURL := getNutsMemcachedClientAndMatchedURL(BYTEKEY) + _ = client.Set(BYTEKEY, []byte("A"), matchedURL, time.Duration(20)*time.Second) + time.Sleep(1 * time.Second) + + res := client.Get(BYTEKEY) + if len(res) == 0 { + errors.GenerateError(t, fmt.Sprintf("Key %s should exist", BYTEKEY)) + } + + if string(res) != "A" { + errors.GenerateError(t, fmt.Sprintf("%s not corresponding to %v", res, 65)) + } +} + +func TestNutsMemcached_SetRequestInCache_TTL(t *testing.T) { + key := "MyEmptyKey" + client, matchedURL := getNutsMemcachedClientAndMatchedURL(key) + nv := []byte("Hello world") + setValueThenVerify(client, key, nv, matchedURL, time.Duration(20)*time.Second, t) +} + +func TestNutsMemcached_DeleteRequestInCache(t *testing.T) { + client, _ := NutsMemcachedConnectionFactory(tests.MockConfiguration(tests.NutsConfiguration)) + client.Delete(BYTEKEY) + time.Sleep(1 * time.Second) + if 0 < len(client.Get(BYTEKEY)) { + errors.GenerateError(t, fmt.Sprintf("Key %s should not exist", BYTEKEY)) + } +} + +func TestNutsMemcached_Init(t *testing.T) { + client, _ := NutsMemcachedConnectionFactory(tests.MockConfiguration(tests.NutsConfiguration)) + err := client.Init() + + if nil != err { + errors.GenerateError(t, "Impossible to init NutsMemcached provider") + } +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index d056385aa..8c90cb4f8 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -34,6 +34,7 @@ var storageMap = map[string]StorerInstanciator{ "otter": OtterConnectionFactory, "embedded_olric": EmbeddedOlricConnectionFactory, "nuts": NutsConnectionFactory, + "nuts_memcached": NutsMemcachedConnectionFactory, "badger": BadgerConnectionFactory, } @@ -54,6 +55,8 @@ func getStorageNameFromConfiguration(configuration configurationtypes.AbstractCo return "otter" } else if configuration.GetDefaultCache().GetNuts().Configuration != nil || configuration.GetDefaultCache().GetNuts().Path != "" { return "nuts" + } else if configuration.GetDefaultCache().GetNutsMemcached().Configuration != nil || configuration.GetDefaultCache().GetNutsMemcached().Path != "" { + return "nuts_memcached" } return "badger" diff --git a/plugins/caddy/configuration.go b/plugins/caddy/configuration.go index 2d9c5fc24..726306ac1 100644 --- a/plugins/caddy/configuration.go +++ b/plugins/caddy/configuration.go @@ -42,6 +42,8 @@ type DefaultCache struct { Nuts configurationtypes.CacheProvider `json:"nuts"` // Otter provider configuration. Otter configurationtypes.CacheProvider `json:"otter"` + // NutsMemcached provider configuration. + NutsMemcached configurationtypes.CacheProvider `json:"nuts_memcached"` // Regex to exclude cache. Regex configurationtypes.Regex `json:"regex"` // Storage providers chaining and order. @@ -109,6 +111,11 @@ func (d *DefaultCache) GetOtter() configurationtypes.CacheProvider { return d.Otter } +// GetNutsMemcached returns nuts_memcached configuration +func (d *DefaultCache) GetNutsMemcached() configurationtypes.CacheProvider { + return d.NutsMemcached +} + // GetOlric returns olric configuration func (d *DefaultCache) GetOlric() configurationtypes.CacheProvider { return d.Olric @@ -518,6 +525,24 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isGlobal boo } } cfg.DefaultCache.Otter = provider + case "nuts_memcached": + provider := configurationtypes.CacheProvider{} + for nesting := h.Nesting(); h.NextBlock(nesting); { + directive := h.Val() + switch directive { + case "url": + urlArgs := h.RemainingArgs() + provider.URL = urlArgs[0] + case "path": + urlArgs := h.RemainingArgs() + provider.Path = urlArgs[0] + case "configuration": + provider.Configuration = parseCaddyfileRecursively(h) + default: + return h.Errf("unsupported nuts directive: %s", directive) + } + } + cfg.DefaultCache.NutsMemcached = provider case "olric": cfg.DefaultCache.Distributed = true provider := configurationtypes.CacheProvider{} diff --git a/plugins/caddy/httpcache.go b/plugins/caddy/httpcache.go index 498f66a66..7152d8718 100644 --- a/plugins/caddy/httpcache.go +++ b/plugins/caddy/httpcache.go @@ -54,6 +54,8 @@ type SouinCaddyMiddleware struct { Nuts configurationtypes.CacheProvider `json:"nuts,omitempty"` // Configure the Otter cache storage. Otter configurationtypes.CacheProvider `json:"otter,omitempty"` + // Configure the Otter cache storage. + NutsMemcached configurationtypes.CacheProvider `json:"nuts_memcached,omitempty"` // Enable the Etcd distributed cache storage. Etcd configurationtypes.CacheProvider `json:"etcd,omitempty"` // Enable the Redis distributed cache storage. @@ -95,6 +97,7 @@ func (s *SouinCaddyMiddleware) configurationPropertyMapper() error { Badger: s.Badger, Nuts: s.Nuts, Otter: s.Otter, + NutsMemcached: s.NutsMemcached, Key: s.Key, DefaultCacheControl: s.DefaultCacheControl, CacheName: s.CacheName, @@ -222,6 +225,9 @@ func (s *SouinCaddyMiddleware) FromApp(app *SouinApp) error { if dc.Otter.Path == "" && dc.Otter.Configuration == nil { s.Configuration.DefaultCache.Otter = appDc.Otter } + if dc.NutsMemcached.Path == "" && dc.NutsMemcached.Configuration == nil { + s.Configuration.DefaultCache.NutsMemcached = appDc.NutsMemcached + } if dc.Regex.Exclude == "" { s.Configuration.DefaultCache.Regex.Exclude = appDc.Regex.Exclude }