11package storage
22
33import (
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
2223var 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
2538type 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
7892func 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