Skip to content

Commit 92c02be

Browse files
committed
feat(storers): Add get/set multilevel support and rebase
1 parent 43bab16 commit 92c02be

File tree

3 files changed

+169
-107
lines changed

3 files changed

+169
-107
lines changed

pkg/api/souin.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,13 @@ func (s *SouinAPI) listKeys(search string) []string {
135135
}
136136

137137
var storageToInfiniteTTLMap = map[string]time.Duration{
138-
"BADGER": 365 * 24 * time.Hour,
139-
"ETCD": 365 * 24 * time.Hour,
140-
"NUTS": 0,
141-
"OLRIC": 365 * 24 * time.Hour,
142-
"OTTER": 365 * 24 * time.Hour,
143-
"REDIS": 0,
138+
"BADGER": 365 * 24 * time.Hour,
139+
"ETCD": 365 * 24 * time.Hour,
140+
"NUTS": 0,
141+
"NUTS_MEMCACHED": 0,
142+
"OLRIC": 365 * 24 * time.Hour,
143+
"OTTER": 365 * 24 * time.Hour,
144+
"REDIS": 0,
144145
}
145146

146147
func (s *SouinAPI) purgeMapping() {

pkg/storage/nutsMemcachedProvider.go

Lines changed: 156 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package storage
22

33
import (
4-
"bufio"
54
"bytes"
65
"encoding/json"
76
"errors"
@@ -10,17 +9,31 @@ import (
109
"strings"
1110
"time"
1211

12+
"github.com/darkweak/souin/configurationtypes"
1313
t "github.com/darkweak/souin/configurationtypes"
1414
"github.com/darkweak/souin/pkg/rfc"
1515
"github.com/darkweak/souin/pkg/storage/types"
1616
"github.com/dgraph-io/ristretto"
1717
"github.com/imdario/mergo"
1818
"github.com/nutsdb/nutsdb"
19+
lz4 "github.com/pierrec/lz4/v4"
1920
"go.uber.org/zap"
2021
)
2122

2223
var nutsMemcachedInstanceMap = map[string]*nutsdb.DB{}
2324

25+
// Why NutsMemcached?
26+
// ---
27+
// The NutsMemcached storage backend is composed of two different storage backends:
28+
// 1. NutsDB: for the cache key index (i.e., IDX_ keys).
29+
// 2. Memcached: for the cache content.
30+
// There are two storage backends because:
31+
// 1. is a "non forgetting" storage backend (NutsDB, for the index). Keys will be kept until their TTL expires.
32+
// → if it was handled by a storage backend that can preemptively evict, you might evict IDX_ keys, which you wouldn't want.
33+
// You need to make sure index and content stays in sync.
34+
// 2. is "forgetting" storage backend (Memcached, for the data). Cache data will be pre-emptively evicted (i.e., before TTL is reached).
35+
// → it makes it possible to put limits on total RAM/disk usage.
36+
2437
// NutsMemcached provider type
2538
type NutsMemcached struct {
2639
*nutsdb.DB
@@ -30,51 +43,52 @@ type NutsMemcached struct {
3043
ristrettoCache *ristretto.Cache
3144
}
3245

33-
// const (
34-
// bucket = "souin-bucket"
35-
// nutsLimit = 1 << 16
36-
// )
37-
38-
// func sanitizeProperties(m map[string]interface{}) map[string]interface{} {
39-
// iotas := []string{"RWMode", "StartFileLoadingMode"}
40-
// for _, i := range iotas {
41-
// if v := m[i]; v != nil {
42-
// currentMode := nutsdb.FileIO
43-
// switch v {
44-
// case 1:
45-
// currentMode = nutsdb.MMap
46-
// }
47-
// m[i] = currentMode
48-
// }
49-
// }
50-
51-
// for _, i := range []string{"SegmentSize", "NodeNum", "MaxFdNumsInCache"} {
52-
// if v := m[i]; v != nil {
53-
// m[i], _ = v.(int64)
54-
// }
55-
// }
56-
57-
// if v := m["EntryIdxMode"]; v != nil {
58-
// m["EntryIdxMode"] = nutsdb.HintKeyValAndRAMIdxMode
59-
// switch v {
60-
// case 1:
61-
// m["EntryIdxMode"] = nutsdb.HintKeyAndRAMIdxMode
62-
// }
63-
// }
64-
65-
// if v := m["SyncEnable"]; v != nil {
66-
// m["SyncEnable"] = true
67-
// if b, ok := v.(bool); ok {
68-
// m["SyncEnable"] = b
69-
// } else if s, ok := v.(string); ok {
70-
// m["SyncEnable"], _ = strconv.ParseBool(s)
71-
// }
72-
// }
73-
74-
// return m
75-
// }
76-
77-
// NutsConnectionFactory function create new Nuts instance
46+
// Below is already defined in the original Nuts provider.
47+
/* const (
48+
bucket = "souin-bucket"
49+
nutsLimit = 1 << 16
50+
)
51+
52+
func sanitizeProperties(m map[string]interface{}) map[string]interface{} {
53+
iotas := []string{"RWMode", "StartFileLoadingMode"}
54+
for _, i := range iotas {
55+
if v := m[i]; v != nil {
56+
currentMode := nutsdb.FileIO
57+
switch v {
58+
case 1:
59+
currentMode = nutsdb.MMap
60+
}
61+
m[i] = currentMode
62+
}
63+
}
64+
65+
for _, i := range []string{"SegmentSize", "NodeNum", "MaxFdNumsInCache"} {
66+
if v := m[i]; v != nil {
67+
m[i], _ = v.(int64)
68+
}
69+
}
70+
71+
if v := m["EntryIdxMode"]; v != nil {
72+
m["EntryIdxMode"] = nutsdb.HintKeyValAndRAMIdxMode
73+
switch v {
74+
case 1:
75+
m["EntryIdxMode"] = nutsdb.HintKeyAndRAMIdxMode
76+
}
77+
}
78+
79+
if v := m["SyncEnable"]; v != nil {
80+
m["SyncEnable"] = true
81+
if b, ok := v.(bool); ok {
82+
m["SyncEnable"] = b
83+
} else if s, ok := v.(string); ok {
84+
m["SyncEnable"], _ = strconv.ParseBool(s)
85+
}
86+
}
87+
88+
return m
89+
} */
90+
91+
// NutsConnectionFactory function create new NutsMemcached instance
7892
func NutsMemcachedConnectionFactory(c t.AbstractConfigurationInterface) (types.Storer, error) {
7993
dc := c.GetDefaultCache()
8094
nutsConfiguration := dc.GetNutsMemcached()
@@ -88,6 +102,10 @@ func NutsMemcachedConnectionFactory(c t.AbstractConfigurationInterface) (types.S
88102
// Use: github.com/nutsdb/nutsdb v0.14.0
89103
//nutsOptions.EntryIdxMode = nutsdb.HintBPTSparseIdxMode
90104

105+
// EntryIdxMode will affect the size of the key index in memory.
106+
// → since this storage backend has no limit on memory usage, it has to be chosen depending on
107+
// the max number of cache keys that will be kept in flight.
108+
91109
if nutsConfiguration.Configuration != nil {
92110
var parsedNuts nutsdb.Options
93111
nutsConfiguration.Configuration = sanitizeProperties(nutsConfiguration.Configuration.(map[string]interface{}))
@@ -122,6 +140,7 @@ func NutsMemcachedConnectionFactory(c t.AbstractConfigurationInterface) (types.S
122140
return nil, e
123141
}
124142

143+
// Ristretto config
125144
var numCounters int64 = 1e7 // number of keys to track frequency of (10M).
126145
var maxCost int64 = 1 << 30 // maximum cost of cache (1GB).
127146
if nutsConfiguration.Configuration != nil {
@@ -142,7 +161,8 @@ func NutsMemcachedConnectionFactory(c t.AbstractConfigurationInterface) (types.S
142161
BufferItems: 64, // number of keys per Get buffer.
143162
})
144163
if err != nil {
145-
panic(err)
164+
c.GetLogger().Sugar().Error("Impossible to make new Ristretto cache.", err)
165+
return nil, e
146166
}
147167

148168
instance := &NutsMemcached{
@@ -167,10 +187,13 @@ func (provider *NutsMemcached) ListKeys() []string {
167187
keys := []string{}
168188

169189
e := provider.DB.View(func(tx *nutsdb.Tx) error {
170-
e, _ := tx.GetAll(bucket)
190+
e, _ := tx.PrefixScan(bucket, []byte(MappingKeyPrefix), 0, 100)
171191
for _, k := range e {
172-
if !strings.Contains(string(k.Key), surrogatePrefix) {
173-
keys = append(keys, string(k.Key))
192+
mapping, err := decodeMapping(k.Value)
193+
if err == nil {
194+
for _, v := range mapping.Mapping {
195+
keys = append(keys, v.RealKey)
196+
}
174197
}
175198
}
176199
return nil
@@ -217,8 +240,8 @@ func (provider *NutsMemcached) Get(key string) (item []byte) {
217240
}
218241

219242
// Prefix method returns the populated response if exists, empty response then
220-
func (provider *NutsMemcached) Prefix(key string, req *http.Request, validator *rfc.Revalidator) *http.Response {
221-
var result *http.Response
243+
func (provider *NutsMemcached) Prefix(key string) []string {
244+
result := []string{}
222245

223246
_ = provider.DB.View(func(tx *nutsdb.Tx) error {
224247
prefix := []byte(key)
@@ -227,32 +250,7 @@ func (provider *NutsMemcached) Prefix(key string, req *http.Request, validator *
227250
return err
228251
} else {
229252
for _, entry := range entries {
230-
if varyVoter(key, req, string(entry.Key)) {
231-
// TODO: improve this
232-
// Store only response header in nuts and avoid query to memcached on each vary
233-
// E.g, rfc.ValidateETag on NutsDB header value, retrieve response body later from memcached.
234-
235-
// Reminder: the key must be at most 250 bytes in length
236-
//fmt.Println("memcached PREFIX", key, "GET", string(entry.Key))
237-
i, e := provider.getFromMemcached(string(entry.Value))
238-
if e == nil {
239-
res, err := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(i)), req)
240-
if err == nil {
241-
rfc.ValidateETag(res, validator)
242-
if validator.Matched {
243-
provider.logger.Sugar().Debugf("The stored key %s matched the current iteration key ETag %+v", string(entry.Key), validator)
244-
result = res
245-
return nil
246-
}
247-
248-
provider.logger.Sugar().Debugf("The stored key %s didn't match the current iteration key ETag %+v", string(entry.Key), validator)
249-
} else {
250-
provider.logger.Sugar().Errorf("An error occured while reading response for the key %s: %v", string(entry.Key), err)
251-
}
252-
} else {
253-
provider.logger.Sugar().Errorf("An error occured while reading memcached for the key %s: %v", string(entry.Key), err)
254-
}
255-
}
253+
result = append(result, string(entry.Key))
256254
}
257255
}
258256
return nil
@@ -261,45 +259,102 @@ func (provider *NutsMemcached) Prefix(key string, req *http.Request, validator *
261259
return result
262260
}
263261

264-
// Set method will store the response in Nuts provider
265-
func (provider *NutsMemcached) Set(key string, value []byte, url t.URL, ttl time.Duration) error {
266-
if ttl == 0 {
267-
ttl = url.TTL.Duration
268-
}
269-
// Only for memcached (to overcome 250 bytes key limit)
270-
//memcachedKey := uuid.New().String()
271-
memcachedKey := key
262+
// GetMultiLevel tries to load the key and check if one of linked keys is a fresh/stale candidate.
263+
func (provider *NutsMemcached) GetMultiLevel(key string, req *http.Request, validator *rfc.Revalidator) (fresh *http.Response, stale *http.Response) {
264+
_ = provider.DB.View(func(tx *nutsdb.Tx) error {
265+
i, e := tx.Get(bucket, []byte(MappingKeyPrefix+key))
266+
if e != nil && !errors.Is(e, nutsdb.ErrKeyNotFound) {
267+
return e
268+
}
272269

273-
// set to nuts (normal TTL)
274-
{
275-
err := provider.DB.Update(func(tx *nutsdb.Tx) error {
270+
var val []byte
271+
if i != nil {
272+
val = i.Value
273+
}
274+
fresh, stale, e = mappingElection(provider, val, req, validator, provider.logger)
276275

277-
// key: cache-key, value: memcached-key
278-
return tx.Put(bucket, []byte(key), []byte(memcachedKey), uint32(ttl.Seconds()))
279-
})
276+
return e
277+
})
280278

279+
return
280+
}
281+
282+
// SetMultiLevel tries to store the key with the given value and update the mapping key to store metadata.
283+
func (provider *NutsMemcached) SetMultiLevel(baseKey, variedKey string, value []byte, variedHeaders http.Header, etag string, duration time.Duration, realKey string) error {
284+
now := time.Now()
285+
286+
compressed := new(bytes.Buffer)
287+
if _, err := lz4.NewWriter(compressed).ReadFrom(bytes.NewReader(value)); err != nil {
288+
provider.logger.Sugar().Errorf("Impossible to compress the key %s into Nuts, %v", variedKey, err)
289+
return err
290+
}
291+
{
292+
// matchedURL is only use when ttl == 0
293+
ttl := duration + provider.stale
294+
url := t.URL{
295+
TTL: configurationtypes.Duration{Duration: ttl},
296+
}
297+
err := provider.Set(variedKey, compressed.Bytes(), url, ttl)
281298
if err != nil {
282-
provider.logger.Sugar().Errorf("Impossible to set value into Nuts, %v", err)
283299
return err
284300
}
285301
}
286302

287-
// set to nuts (stale TTL)
288-
staleTtl := int32((provider.stale + ttl).Seconds())
303+
err := provider.DB.Update(func(tx *nutsdb.Tx) error {
304+
mappingKey := MappingKeyPrefix + baseKey
305+
item, e := tx.Get(bucket, []byte(mappingKey))
306+
if e != nil && !errors.Is(e, nutsdb.ErrKeyNotFound) {
307+
provider.logger.Sugar().Errorf("Impossible to get the base key %s in Nuts, %v", baseKey, e)
308+
return e
309+
}
310+
311+
var val []byte
312+
if item != nil {
313+
val = item.Value
314+
}
315+
316+
val, e = mappingUpdater(variedKey, val, provider.logger, now, now.Add(duration), now.Add(duration+provider.stale), variedHeaders, etag, realKey)
317+
if e != nil {
318+
return e
319+
}
320+
321+
provider.logger.Sugar().Debugf("Store the new mapping for the key %s in Nuts", variedKey)
322+
323+
return tx.Put(bucket, []byte(mappingKey), val, nutsdb.Persistent)
324+
})
325+
326+
if err != nil {
327+
provider.logger.Sugar().Errorf("Impossible to set value into Nuts, %v", err)
328+
}
329+
330+
return err
331+
}
332+
333+
// Set method will store the response in Nuts provider
334+
func (provider *NutsMemcached) Set(key string, value []byte, url t.URL, duration time.Duration) error {
335+
if duration == 0 {
336+
duration = url.TTL.Duration
337+
}
338+
// Only for memcached (to overcome 250 bytes key limit)
339+
//memcachedKey := uuid.New().String()
340+
// Disabled for ristretto to improve performances
341+
memcachedKey := key
342+
343+
// set to nuts
289344
{
290345
err := provider.DB.Update(func(tx *nutsdb.Tx) error {
291-
// key: "STALE_" + cache-key, value: memcached-key
292-
return tx.Put(bucket, []byte(StalePrefix+key), []byte(memcachedKey), uint32(staleTtl))
346+
// key: cache-key, value: memcached-key
347+
return tx.Put(bucket, []byte(key), []byte(memcachedKey), uint32(duration.Seconds()))
293348
})
294349

295350
if err != nil {
296351
provider.logger.Sugar().Errorf("Impossible to set value into Nuts, %v", err)
352+
return err
297353
}
298354
}
299355

300-
// set to memcached with stale TTL
301-
_ = provider.setToMemcached(memcachedKey, value, staleTtl)
302-
356+
// set to memcached
357+
_ = provider.setToMemcached(memcachedKey, value, int32(duration.Seconds()))
303358
return nil
304359
}
305360

@@ -373,7 +428,7 @@ func (provider *NutsMemcached) setToMemcached(memcachedKey string, value []byte,
373428
// }
374429
ok := provider.ristrettoCache.Set(memcachedKey, value, int64(len(value)))
375430
if !ok {
376-
provider.logger.Sugar().Debugf("Value not set to cache, key=%v", memcachedKey)
431+
provider.logger.Sugar().Debugf("Value not set to ristretto cache, key=%v", memcachedKey)
377432
}
378433
return
379434
}

0 commit comments

Comments
 (0)