From 1ac77dbf3786a17dfb4239a8b613af111aa81b15 Mon Sep 17 00:00:00 2001 From: igoradamenko Date: Mon, 11 Jun 2018 15:15:44 +0300 Subject: [PATCH 1/8] add verify / unverify actions to api --- web/app/common/api.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/app/common/api.js b/web/app/common/api.js index de15804c..bd1007eb 100644 --- a/web/app/common/api.js +++ b/web/app/common/api.js @@ -72,6 +72,16 @@ export const unpin = ({ id, url }) => fetcher.put({ withCredentials: true, }); +export const verify = ({ id }) => fetcher.put({ + url: `/admin/verify/${id}?verified=1`, + withCredentials: true, +}); + +export const unverify = ({ id }) => fetcher.put({ + url: `/admin/verify/${id}?verified=0`, + withCredentials: true, +}); + export const remove = ({ id }) => fetcher.delete({ url: `/admin/comment/${id}?url=${url}`, withCredentials: true, @@ -107,6 +117,8 @@ export default { pin, unpin, + verify, + unverify, remove, blockUser, unblockUser, From 4cf40b2f65c2084198edff018438bb7d38a3f724 Mon Sep 17 00:00:00 2001 From: igoradamenko Date: Mon, 11 Jun 2018 15:17:11 +0300 Subject: [PATCH 2/8] add verification check to comment block closes #60 --- .../_active/comment__verification_active.scss | 3 + .../_active/comment__verification_active.svg | 6 ++ .../comment__verification_clickable.scss | 3 + .../__verification/comment__verification.scss | 13 ++++ .../__verification/comment__verification.svg | 6 ++ web/app/components/comment/comment.jsx | 63 ++++++++++++++++++- web/app/components/comment/index.js | 4 ++ web/app/components/root/root.jsx | 2 + 8 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 web/app/components/comment/__verification/_active/comment__verification_active.scss create mode 100644 web/app/components/comment/__verification/_active/comment__verification_active.svg create mode 100644 web/app/components/comment/__verification/_clickable/comment__verification_clickable.scss create mode 100644 web/app/components/comment/__verification/comment__verification.scss create mode 100644 web/app/components/comment/__verification/comment__verification.svg diff --git a/web/app/components/comment/__verification/_active/comment__verification_active.scss b/web/app/components/comment/__verification/_active/comment__verification_active.scss new file mode 100644 index 00000000..e274b88a --- /dev/null +++ b/web/app/components/comment/__verification/_active/comment__verification_active.scss @@ -0,0 +1,3 @@ +.comment__verification_active { + background-image: url('comment__verification_active.svg'); +} diff --git a/web/app/components/comment/__verification/_active/comment__verification_active.svg b/web/app/components/comment/__verification/_active/comment__verification_active.svg new file mode 100644 index 00000000..117e912e --- /dev/null +++ b/web/app/components/comment/__verification/_active/comment__verification_active.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/app/components/comment/__verification/_clickable/comment__verification_clickable.scss b/web/app/components/comment/__verification/_clickable/comment__verification_clickable.scss new file mode 100644 index 00000000..c406abfc --- /dev/null +++ b/web/app/components/comment/__verification/_clickable/comment__verification_clickable.scss @@ -0,0 +1,3 @@ +.comment__verification_clickable { + cursor: pointer; +} diff --git a/web/app/components/comment/__verification/comment__verification.scss b/web/app/components/comment/__verification/comment__verification.scss new file mode 100644 index 00000000..79b65086 --- /dev/null +++ b/web/app/components/comment/__verification/comment__verification.scss @@ -0,0 +1,13 @@ +.comment__verification { + display: inline-block; + width: 12px; + height: 12px; + margin-left: 4px; + vertical-align: middle; + background: url('comment__verification.svg') center no-repeat; + background-size: cover; + + &:hover { + opacity: .75; + } +} diff --git a/web/app/components/comment/__verification/comment__verification.svg b/web/app/components/comment/__verification/comment__verification.svg new file mode 100644 index 00000000..aafbd798 --- /dev/null +++ b/web/app/components/comment/__verification/comment__verification.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/app/components/comment/comment.jsx b/web/app/components/comment/comment.jsx index 65f4a043..d36a2eab 100644 --- a/web/app/components/comment/comment.jsx +++ b/web/app/components/comment/comment.jsx @@ -15,6 +15,7 @@ export default class Comment extends Component { isReplying: false, isEditing: false, isUserIdVisible: false, + isUserVerified: false, editTimeLeft: null, }; @@ -33,6 +34,8 @@ export default class Comment extends Component { this.onReply = this.onReply.bind(this); this.onPinClick = this.onPinClick.bind(this); this.onUnpinClick = this.onUnpinClick.bind(this); + this.onVerifyClick = this.onVerifyClick.bind(this); + this.onUnverifyClick = this.onUnverifyClick.bind(this); this.onBlockClick = this.onBlockClick.bind(this); this.onUnblockClick = this.onUnblockClick.bind(this); this.onDeleteClick = this.onDeleteClick.bind(this); @@ -151,6 +154,30 @@ export default class Comment extends Component { } } + onVerifyClick() { + const { id, user: { id: userId } } = this.props.data; + + if (confirm('Do you want to verify this user?')) { + this.setState({ isUserVerified: true }); + + api.verify({ id: userId }).then(() => { + api.getComment({ id }).then(comment => store.replaceComment(comment)); + }); + } + } + + onUnverifyClick() { + const { id, user: { id: userId } } = this.props.data; + + if (confirm('Do you want to unverify this user?')) { + this.setState({ isUserVerified: false }); + + api.unverify({ id: userId }).then(() => { + api.getComment({ id }).then(comment => store.replaceComment(comment)); + }); + } + } + onBlockClick() { const { id, user: { id: userId } } = this.props.data; @@ -272,7 +299,20 @@ export default class Comment extends Component { } } - render(props, { guest, isUserIdVisible, userBlocked, pinned, score, scoreIncreased, scoreDecreased, deleted, isReplying, isEditing, editTimeLeft }) { + render(props, { + guest, + isUserIdVisible, + userBlocked, + pinned, + score, + scoreIncreased, + scoreDecreased, + deleted, + isReplying, + isEditing, + isUserVerified, + editTimeLeft, + }) { const { data, mods = {} } = props; const isAdmin = !guest && store.get('user').admin; const isGuest = guest || !Object.keys(store.get('user')).length; @@ -314,6 +354,7 @@ export default class Comment extends Component { ...data.user, picture: data.user.picture.indexOf(API_BASE) === 0 ? `${BASE_URL}${data.user.picture}` : data.user.picture, isDefaultPicture: !data.user.picture.length, + verified: data.user.verified || isUserVerified, }, }; @@ -364,6 +405,26 @@ export default class Comment extends Component { isUserIdVisible && ({o.user.id}) } + { + isAdmin && ( + + ) + } + + { + !isAdmin && !!o.user.verified && ( + + ) + } + {o.time} { diff --git a/web/app/components/comment/index.js b/web/app/components/comment/index.js index 96b755ae..c2b83915 100644 --- a/web/app/components/comment/index.js +++ b/web/app/components/comment/index.js @@ -28,6 +28,10 @@ require('./__time/comment__time.scss'); require('./__user-id/comment__user-id.scss'); require('./__username/comment__username.scss'); +require('./__verification/comment__verification.scss'); +require('./__verification/_active/comment__verification_active.scss'); +require('./__verification/_clickable/comment__verification_clickable.scss'); + require('./__vote/comment__vote.scss'); require('./__vote/_disabled/comment__vote_disabled.scss'); require('./__vote/_selected/comment__vote_selected.scss'); diff --git a/web/app/components/root/root.jsx b/web/app/components/root/root.jsx index 175b37ef..927bab70 100644 --- a/web/app/components/root/root.jsx +++ b/web/app/components/root/root.jsx @@ -1,3 +1,5 @@ +import 'mimic'; + import { h, Component } from 'preact'; import api from 'common/api'; From 3f807b2e3f877980dd5b9009fa28287e86d02410 Mon Sep 17 00:00:00 2001 From: igoradamenko Date: Mon, 11 Jun 2018 15:21:58 +0300 Subject: [PATCH 3/8] oops; remove mimic from root component --- web/app/components/root/root.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/app/components/root/root.jsx b/web/app/components/root/root.jsx index 927bab70..175b37ef 100644 --- a/web/app/components/root/root.jsx +++ b/web/app/components/root/root.jsx @@ -1,5 +1,3 @@ -import 'mimic'; - import { h, Component } from 'preact'; import api from 'common/api'; From 3e60e57b754739d74f9823eddddf1ea3667291a4 Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 11 Jun 2018 09:24:32 -0500 Subject: [PATCH 4/8] set verified status retroactively --- README.md | 4 ++++ app/rest/api/admin.go | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7810333..3c55a92e 100644 --- a/README.md +++ b/README.md @@ -477,3 +477,7 @@ _all admin calls require auth and admin privilege_ * User ID hashed and prefixed by oauth provider name to avoid collisions and potential abuse. * All avatars cached locally to prevent rate limiters from google/github/facebook/yandex. * Docker build uses [publicly available](https://github.com/umputun/baseimage) base images. + +## Current status + +Remark42 is still under development and the first stable v1.0 is about to be released. API (v1) is stable and won't have any breaking changes. diff --git a/app/rest/api/admin.go b/app/rest/api/admin.go index 565c1f34..db752891 100644 --- a/app/rest/api/admin.go +++ b/app/rest/api/admin.go @@ -204,8 +204,9 @@ func (a *admin) alterComments(comments []store.Comment, r *http.Request) (res [] for i, c := range comments { + blocked := a.dataService.IsBlocked(c.Locator.SiteID, c.User.ID) // process blocked users - if a.dataService.IsBlocked(c.Locator.SiteID, c.User.ID) { + if blocked { if !isAdmin { // reset comment to deleted for non-admins c.SetDeleted(store.SoftDelete) } @@ -213,6 +214,11 @@ func (a *admin) alterComments(comments []store.Comment, r *http.Request) (res [] c.Deleted = true } + // set verified status retroactively + if !blocked { + c.User.Verified = a.dataService.IsVerified(c.Locator.SiteID, c.User.ID) + } + // hide info from non-admins if !isAdmin { c.User.IP = "" From 50c9b4c6bd1f228dd1991a389cb5116d9fb4e928 Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 11 Jun 2018 09:53:53 -0500 Subject: [PATCH 5/8] set verified status to userInfo to allow it before user re-login --- app/rest/api/rest_private.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/rest/api/rest_private.go b/app/rest/api/rest_private.go index dff8667c..c0fd9ce8 100644 --- a/app/rest/api/rest_private.go +++ b/app/rest/api/rest_private.go @@ -132,13 +132,18 @@ func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, res) } -// GET /user - returns user info +// GET /user?site=siteID - returns user info func (s *Rest) userInfoCtrl(w http.ResponseWriter, r *http.Request) { user, err := rest.GetUserInfo(r) if err != nil { rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") return } + + if siteID := r.URL.Query().Get("site"); siteID != "" { + user.Verified = s.DataService.IsVerified(siteID, user.ID) + } + render.JSON(w, r, user) } From 363579969136720c7b40b7a7211b0bde3a26ab2f Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 11 Jun 2018 10:48:32 -0500 Subject: [PATCH 6/8] add test to verified status retroactively populated --- app/rest/api/admin_test.go | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/rest/api/admin_test.go b/app/rest/api/admin_test.go index f43ebe25..5a38c431 100644 --- a/app/rest/api/admin_test.go +++ b/app/rest/api/admin_test.go @@ -265,7 +265,7 @@ func TestAdmin_ReadOnly(t *testing.T) { assert.Nil(t, err) assert.True(t, info.ReadOnly) - // resset post's read-only + // reset post's read-only req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=0", ts.URL), nil) assert.Nil(t, err) @@ -305,6 +305,15 @@ func TestAdmin_Verify(t *testing.T) { verified = srv.DataService.IsVerified("radio-t", "user1") assert.True(t, verified) + res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah&sort=+time") + assert.Equal(t, 200, code) + comments := []store.Comment{} + err = json.Unmarshal([]byte(res), &comments) + assert.Nil(t, err) + assert.Equal(t, 2, len(comments), "should have 2 comments") + assert.Equal(t, "test test #1", comments[0].Text) + assert.True(t, comments[0].User.Verified) + req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=0", ts.URL), nil) assert.Nil(t, err) @@ -313,6 +322,16 @@ func TestAdmin_Verify(t *testing.T) { require.Nil(t, err) verified = srv.DataService.IsVerified("radio-t", "user1") assert.False(t, verified) + + res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah&sort=+time") + assert.Equal(t, 200, code) + comments = []store.Comment{} + err = json.Unmarshal([]byte(res), &comments) + assert.Nil(t, err) + assert.Equal(t, 2, len(comments), "should have 2 comments") + assert.Equal(t, "test test #1", comments[0].Text) + assert.False(t, comments[0].User.Verified) + } func TestAdmin_ExportStream(t *testing.T) { From 3772f8364e1c42fe12b61b92fa6aacd2c2df2116 Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 11 Jun 2018 13:20:56 -0500 Subject: [PATCH 7/8] add EOL to robots --- app/rest/api/rest.go | 2 +- app/rest/api/rest_public_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/rest/api/rest.go b/app/rest/api/rest.go index 7e69c36a..848e2c99 100644 --- a/app/rest/api/rest.go +++ b/app/rest/api/rest.go @@ -172,7 +172,7 @@ func (s *Rest) routes() chi.Router { for i := range allowed { allowed[i] = "Allow: /api/v1" + allowed[i] } - render.PlainText(w, r, "User-agent: *\nDisallow: /auth/\nDisallow: /api/\n"+strings.Join(allowed, "\n")) + render.PlainText(w, r, "User-agent: *\nDisallow: /auth/\nDisallow: /api/\n"+strings.Join(allowed, "\n")+"\n") }) // file server for static content from /web diff --git a/app/rest/api/rest_public_test.go b/app/rest/api/rest_public_test.go index 27654b31..3e68c712 100644 --- a/app/rest/api/rest_public_test.go +++ b/app/rest/api/rest_public_test.go @@ -436,5 +436,5 @@ func TestRest_Robots(t *testing.T) { assert.Equal(t, 200, code) assert.Equal(t, "User-agent: *\nDisallow: /auth/\nDisallow: /api/\nAllow: /api/v1/find\n"+ "Allow: /api/v1/last\nAllow: /api/v1/id\nAllow: /api/v1/count\nAllow: /api/v1/counts\n"+ - "Allow: /api/v1/list\nAllow: /api/v1/config\nAllow: /api/v1/img\nAllow: /api/v1/avatar", string(body)) + "Allow: /api/v1/list\nAllow: /api/v1/config\nAllow: /api/v1/img\nAllow: /api/v1/avatar\n", string(body)) } From 3000cd0b6c436b4ad53abc08ec4580d2f1e7b202 Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 11 Jun 2018 14:18:24 -0500 Subject: [PATCH 8/8] separate memory cache --- app/main.go | 2 +- app/rest/cache/cache.go | 116 +----------- app/rest/cache/cache_test.go | 335 +++------------------------------- app/rest/cache/memory.go | 119 ++++++++++++ app/rest/cache/memory_test.go | 299 ++++++++++++++++++++++++++++++ app/rest/cache/options.go | 10 +- 6 files changed, 447 insertions(+), 434 deletions(-) create mode 100644 app/rest/cache/memory.go create mode 100644 app/rest/cache/memory_test.go diff --git a/app/main.go b/app/main.go index 86184ff8..b7d802dc 100644 --- a/app/main.go +++ b/app/main.go @@ -130,7 +130,7 @@ func New(opts Opts) (*Application, error) { MaxCommentSize: opts.MaxCommentSize, } - loadingCache, err := cache.NewLoadingCache(cache.MaxValSize(opts.MaxCachedValue), cache.MaxKeys(opts.MaxCachedItems), + loadingCache, err := cache.NewMemoryCache(cache.MaxValSize(opts.MaxCachedValue), cache.MaxKeys(opts.MaxCachedItems), cache.PostFlushFn(postFlushFn(opts.Sites, opts.Port))) if err != nil { return nil, err diff --git a/app/rest/cache/cache.go b/app/rest/cache/cache.go index 8ac138c1..f734f42f 100644 --- a/app/rest/cache/cache.go +++ b/app/rest/cache/cache.go @@ -1,12 +1,9 @@ package cache import ( - "log" "net/http" "strings" - "sync/atomic" - "github.com/hashicorp/golang-lru" "github.com/pkg/errors" "github.com/umputun/remark/app/rest" ) @@ -22,7 +19,8 @@ func Key(key string, scopes ...string) string { return strings.Join(scopes, "$$") + "@@" + key } -func parseKey(fullKey string) (key string, scopes []string, err error) { +// ParseKey gets compound key created by Key func and split it to the actual key and scopes +func ParseKey(fullKey string) (key string, scopes []string, err error) { elems := strings.Split(fullKey, "@@") if len(elems) != 2 { return "", nil, errors.Errorf("can't parse cache key %s", key) @@ -35,116 +33,6 @@ func parseKey(fullKey string) (key string, scopes []string, err error) { return key, scopes, nil } -// loadingCache implements LoadingCache interface on top of cache.Cache (go-cache) -type loadingCache struct { - bytesCache *lru.Cache - postFlushFn func() - maxKeys int - maxValueSize int - maxCacheSize int64 - currentSize int64 -} - -// NewLoadingCache makes loadingCache implementation -func NewLoadingCache(options ...Option) (LoadingCache, error) { - res := loadingCache{ - postFlushFn: func() {}, - maxKeys: 1000, - maxValueSize: 0, - } - for _, opt := range options { - if err := opt(&res); err != nil { - log.Printf("[WARN] failed to set cache option, %v", err) - } - } - - onEvicted := func(key interface{}, value interface{}) { - size := len(value.([]byte)) - atomic.AddInt64(&res.currentSize, -1*int64(size)) - } - - var err error - // OnEvicted called automatically for expired and manually deleted - if res.bytesCache, err = lru.NewWithEvict(res.maxKeys, onEvicted); err != nil { - return nil, errors.Wrap(err, "failed to make cache") - } - - log.Printf("[DEBUG] create lru cache, maxKeys=%d, maxValueSize=%d", res.maxKeys, res.maxValueSize) - return &res, nil -} - -// Get is loading cache method to get value by key or load via fn if not found -func (lc *loadingCache) Get(key string, fn func() ([]byte, error)) (data []byte, err error) { - if b, ok := lc.bytesCache.Get(key); ok { - return b.([]byte), nil - } - - if data, err = fn(); err != nil { - return data, err - } - if lc.allowed(data) { - lc.bytesCache.Add(key, data) - atomic.AddInt64(&lc.currentSize, int64(len(data))) - - if lc.maxCacheSize > 0 && atomic.LoadInt64(&lc.currentSize) > lc.maxCacheSize { - for atomic.LoadInt64(&lc.currentSize) > lc.maxCacheSize { - lc.bytesCache.RemoveOldest() - } - } - } - return data, nil -} - -// Flush clears cache and calls postFlushFn async -func (lc *loadingCache) Flush(scopes ...string) { - - if len(scopes) == 0 { - lc.bytesCache.Purge() - go lc.postFlushFn() - return - } - - // check if fullKey has matching scopes - inScope := func(fullKey string) bool { - for _, s := range scopes { - _, keyScopes, err := parseKey(fullKey) - if err != nil { - return false - } - for _, ks := range keyScopes { - if ks == s { - return true - } - } - } - return false - } - - // all matchedKeys should be collected first - // we can't delete it from locked section, it will lock on eviction callback - matchedKeys := []string{} - for _, k := range lc.bytesCache.Keys() { - key := k.(string) - if inScope(key) { - matchedKeys = append(matchedKeys, key) - } - } - for _, mkey := range matchedKeys { - lc.bytesCache.Remove(mkey) - } - - if lc.postFlushFn != nil { - go lc.postFlushFn() - } -} - -func (lc *loadingCache) allowed(data []byte) bool { - if lc.maxValueSize > 0 && len(data) >= lc.maxValueSize { - return false - } - return true -} - // URLKey gets url from request to use it as cache key // admins will have different keys in order to prevent leak of admin-only data to regular users func URLKey(r *http.Request) string { diff --git a/app/rest/cache/cache_test.go b/app/rest/cache/cache_test.go index 3f1f443f..22cabca0 100644 --- a/app/rest/cache/cache_test.go +++ b/app/rest/cache/cache_test.go @@ -1,187 +1,42 @@ package cache import ( - "errors" - "fmt" - "math/rand" "net/http" - "sync" - "sync/atomic" "testing" - "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/umputun/remark/app/rest" "github.com/umputun/remark/app/store" ) -func TestLoadingCache_Get(t *testing.T) { - var postFnCall, coldCalls int32 - lc, err := NewLoadingCache(PostFlushFn(func() { atomic.AddInt32(&postFnCall, 1) })) - require.Nil(t, err) - res, err := lc.Get("key", func() ([]byte, error) { - atomic.AddInt32(&coldCalls, 1) - return []byte("result"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "result", string(res)) - assert.Equal(t, int32(1), atomic.LoadInt32(&coldCalls)) - assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall)) +func TestCache_Keys(t *testing.T) { + tbl := []struct { + key string + scopes []string + full string + }{ + {"key1", []string{"s1"}, "s1@@key1"}, + {"key2", []string{"s11", "s2"}, "s11$$s2@@key2"}, + {"key3", []string{}, "@@key3"}, + } - res, err = lc.Get("key", func() ([]byte, error) { - atomic.AddInt32(&coldCalls, 1) - return []byte("result"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "result", string(res)) - assert.Equal(t, int32(1), atomic.LoadInt32(&coldCalls)) - assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall)) + for n, tt := range tbl { + full := Key(tt.key, tt.scopes...) + assert.Equal(t, tt.full, full, "making key, #%d", n) - lc.Flush() - time.Sleep(100 * time.Millisecond) // let postFn to do its thing - assert.Equal(t, int32(1), atomic.LoadInt32(&postFnCall)) - - _, err = lc.Get("key", func() ([]byte, error) { - return nil, errors.New("err") - }) - assert.NotNil(t, err) -} - -func TestLoadingCache_MaxKeys(t *testing.T) { - var postFnCall, coldCalls int32 - lc, err := NewLoadingCache(PostFlushFn(func() { atomic.AddInt32(&postFnCall, 1) }), - MaxKeys(5), MaxValSize(10)) - require.Nil(t, err) - - // put 5 keys to cache - for i := 0; i < 5; i++ { - res, e := lc.Get(fmt.Sprintf("key-%d", i), func() ([]byte, error) { - atomic.AddInt32(&coldCalls, 1) - return []byte(fmt.Sprintf("result-%d", i)), nil - }) + k, s, e := ParseKey(full) assert.Nil(t, e) - assert.Equal(t, fmt.Sprintf("result-%d", i), string(res)) - assert.Equal(t, int32(i+1), atomic.LoadInt32(&coldCalls)) - assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall)) + assert.Equal(t, tt.scopes, s) + assert.Equal(t, tt.key, k) } - // check if really cached - res, err := lc.Get("key-3", func() ([]byte, error) { - return []byte("result-blah"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "result-3", string(res), "should be cached") - - // try to cache after maxKeys reached - res, err = lc.Get("key-X", func() ([]byte, error) { - return []byte("result-X"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "result-X", string(res)) - - assert.Equal(t, 5, lc.(*loadingCache).bytesCache.Len()) - - // put to cache and make sure it cached - res, err = lc.Get("key-Z", func() ([]byte, error) { - return []byte("result-Z"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "result-Z", string(res)) - - res, err = lc.Get("key-Z", func() ([]byte, error) { - return []byte("result-Zzzz"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "result-Z", string(res), "got cached value") - assert.Equal(t, 5, lc.(*loadingCache).bytesCache.Len()) + _, _, err := ParseKey("abc") + assert.Error(t, err) + _, _, err = ParseKey("") + assert.Error(t, err) } -func TestLoadingCache_MaxValueSize(t *testing.T) { - lc, err := NewLoadingCache(MaxKeys(5), MaxValSize(10)) - require.Nil(t, err) - // put good size value to cache and make sure it cached - res, err := lc.Get("key-Z", func() ([]byte, error) { - return []byte("result-Z"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "result-Z", string(res)) - - res, err = lc.Get("key-Z", func() ([]byte, error) { - return []byte("result-Zzzz"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "result-Z", string(res), "got cached value") - - // put too big value to cache and make sure it is not cached - res, err = lc.Get("key-Big", func() ([]byte, error) { - return []byte("1234567890"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "1234567890", string(res)) - - res, err = lc.Get("key-Big", func() ([]byte, error) { - return []byte("result-big"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "result-big", string(res), "got not cached value") -} - -func TestLoadingCache_MaxCacheSize(t *testing.T) { - lc, err := NewLoadingCache(MaxKeys(50), MaxCacheSize(20)) - require.Nil(t, err) - - // put good size value to cache and make sure it cached - res, err := lc.Get("key-Z", func() ([]byte, error) { - return []byte("result-Z"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "result-Z", string(res)) - assert.Equal(t, int64(8), lc.(*loadingCache).currentSize) - - _, err = lc.Get("key-Z2", func() ([]byte, error) { - return []byte("result-Z"), nil - }) - assert.Nil(t, err) - assert.Equal(t, int64(16), lc.(*loadingCache).currentSize) - - // this will cause removal - _, err = lc.Get("key-Z3", func() ([]byte, error) { - return []byte("result-Z"), nil - }) - assert.Nil(t, err) - assert.Equal(t, int64(16), lc.(*loadingCache).currentSize) - - assert.Equal(t, 2, lc.(*loadingCache).bytesCache.Len()) -} - -func TestLoadingCache_MaxCacheSizeParallel(t *testing.T) { - lc, err := NewLoadingCache(MaxCacheSize(123), MaxKeys(10000)) - require.Nil(t, err) - - wg := sync.WaitGroup{} - for i := 0; i < 1000; i++ { - wg.Add(1) - i := i - go func() { - time.Sleep(time.Duration(rand.Intn(100)) * time.Nanosecond) - defer wg.Done() - res, err := lc.Get(fmt.Sprintf("key-%d", i), func() ([]byte, error) { - return []byte(fmt.Sprintf("result-%d", i)), nil - }) - require.Nil(t, err) - require.Equal(t, fmt.Sprintf("result-%d", i), string(res)) - size := atomic.LoadInt64(&lc.(*loadingCache).currentSize) - require.True(t, size < 200 && size >= 0, "unexpected size=%d", size) // won't be exactly 123 due parallel - }() - } - wg.Wait() - assert.True(t, lc.(*loadingCache).currentSize < 123 && lc.(*loadingCache).currentSize >= 0) - t.Log("size=", lc.(*loadingCache).currentSize) -} - -func TestLoadingCache_URLKey(t *testing.T) { +func TestCache_URLKey(t *testing.T) { r, err := http.NewRequest("GET", "http://blah/123", nil) assert.Nil(t, err) key := URLKey(r) @@ -197,151 +52,3 @@ func TestLoadingCache_URLKey(t *testing.T) { key = URLKey(r) assert.Equal(t, "admin!!http://blah/123?key=v&k2=v2", key) } - -func TestLoadingCache_Parallel(t *testing.T) { - var coldCalls int32 - lc, err := NewLoadingCache() - require.Nil(t, err) - - res, err := lc.Get("key", func() ([]byte, error) { - return []byte("value"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "value", string(res)) - - wg := sync.WaitGroup{} - for i := 0; i < 1000; i++ { - wg.Add(1) - i := i - go func() { - defer wg.Done() - res, err := lc.Get("key", func() ([]byte, error) { - atomic.AddInt32(&coldCalls, 1) - return []byte(fmt.Sprintf("result-%d", i)), nil - }) - require.Nil(t, err) - require.Equal(t, "value", string(res)) - }() - } - wg.Wait() - assert.Equal(t, int32(0), atomic.LoadInt32(&coldCalls)) -} - -func TestLoadingCache_Scopes(t *testing.T) { - lc, err := NewLoadingCache() - require.Nil(t, err) - - res, err := lc.Get(Key("key", "s1", "s2"), func() ([]byte, error) { - return []byte("value"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "value", string(res)) - - res, err = lc.Get(Key("key2", "s2"), func() ([]byte, error) { - return []byte("value2"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "value2", string(res)) - - assert.Equal(t, 2, lc.(*loadingCache).bytesCache.Len()) - lc.Flush("s1") - assert.Equal(t, 1, lc.(*loadingCache).bytesCache.Len()) - - lc.Get(Key("key2", "s2"), func() ([]byte, error) { - assert.Fail(t, "should stay") - return nil, nil - }) - - res, err = lc.Get(Key("key", "s1", "s2"), func() ([]byte, error) { - return []byte("value-upd"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "value-upd", string(res), "was deleted, update") -} - -func TestLoadingCache_Flush(t *testing.T) { - lc, err := NewLoadingCache() - require.Nil(t, err) - - addToCache := func(key string, scopes ...string) { - res, err := lc.Get(key, func() ([]byte, error) { - return []byte("value" + key), nil - }) - require.Nil(t, err) - require.Equal(t, "value"+key, string(res)) - } - - init := func() { - lc.Flush() - addToCache(Key("key1", "s1", "s2")) - addToCache(Key("key2", "s1", "s2", "s3")) - addToCache(Key("key3", "s1", "s2", "s3")) - addToCache(Key("key4", "s2", "s3")) - addToCache(Key("key5", "s2")) - addToCache(Key("key6")) - addToCache(Key("key7", "s4", "s3")) - require.Equal(t, 7, lc.(*loadingCache).bytesCache.Len(), "cache init") - } - - tbl := []struct { - scopes []string - left int - msg string - }{ - {[]string{}, 0, "full flush, no scopes"}, - {[]string{"s0"}, 7, "flush wrong scope"}, - {[]string{"s1"}, 4, "flush s1 scope"}, - {[]string{"s2", "s1"}, 2, "flush s2+s1 scope"}, - {[]string{"s1", "s2"}, 2, "flush s1+s2 scope"}, - {[]string{"s1", "s2", "s4"}, 1, "flush s1+s2+s4 scope"}, - {[]string{"s1", "s2", "s3"}, 1, "flush s1+s2+s3 scope"}, - {[]string{"s1", "s2", "ss"}, 2, "flush s1+s2+wrong scope"}, - } - - for i, tt := range tbl { - init() - lc.Flush(tt.scopes...) - assert.Equal(t, tt.left, lc.(*loadingCache).bytesCache.Len(), "keys size, %s #%d", tt.msg, i) - } -} - -func TestLoadingCache_FlushFailed(t *testing.T) { - lc, err := NewLoadingCache() - require.Nil(t, err) - val, err := lc.Get("invalid-composite", func() ([]byte, error) { - return []byte("value"), nil - }) - assert.Nil(t, err) - assert.Equal(t, "value", string(val)) - assert.Equal(t, 1, lc.(*loadingCache).bytesCache.Len()) - - lc.Flush("invalid-composite") - assert.Equal(t, 1, lc.(*loadingCache).bytesCache.Len()) -} - -func TestLoadingCache_Keys(t *testing.T) { - tbl := []struct { - key string - scopes []string - full string - }{ - {"key1", []string{"s1"}, "s1@@key1"}, - {"key2", []string{"s11", "s2"}, "s11$$s2@@key2"}, - {"key3", []string{}, "@@key3"}, - } - - for n, tt := range tbl { - full := Key(tt.key, tt.scopes...) - assert.Equal(t, tt.full, full, "making key, #%d", n) - - k, s, e := parseKey(full) - assert.Nil(t, e) - assert.Equal(t, tt.scopes, s) - assert.Equal(t, tt.key, k) - } - - _, _, err := parseKey("abc") - assert.Error(t, err) - _, _, err = parseKey("") - assert.Error(t, err) -} diff --git a/app/rest/cache/memory.go b/app/rest/cache/memory.go new file mode 100644 index 00000000..56511ae3 --- /dev/null +++ b/app/rest/cache/memory.go @@ -0,0 +1,119 @@ +package cache + +import ( + "log" + "sync/atomic" + + "github.com/hashicorp/golang-lru" + "github.com/pkg/errors" +) + +// memoryCache implements LoadingCache interface on top of cache.Cache (go-cache) +type memoryCache struct { + bytesCache *lru.Cache + postFlushFn func() + maxKeys int + maxValueSize int + maxCacheSize int64 + currentSize int64 +} + +// NewMemoryCache makes memoryCache implementation +func NewMemoryCache(options ...Option) (LoadingCache, error) { + res := memoryCache{ + postFlushFn: func() {}, + maxKeys: 1000, + maxValueSize: 0, + } + for _, opt := range options { + if err := opt(&res); err != nil { + log.Printf("[WARN] failed to set cache option, %v", err) + } + } + + onEvicted := func(key interface{}, value interface{}) { + size := len(value.([]byte)) + atomic.AddInt64(&res.currentSize, -1*int64(size)) + } + + var err error + // OnEvicted called automatically for expired and manually deleted + if res.bytesCache, err = lru.NewWithEvict(res.maxKeys, onEvicted); err != nil { + return nil, errors.Wrap(err, "failed to make cache") + } + + log.Printf("[DEBUG] create lru cache, maxKeys=%d, maxValueSize=%d", res.maxKeys, res.maxValueSize) + return &res, nil +} + +// Get is loading cache method to get value by key or load via fn if not found +func (m *memoryCache) Get(key string, fn func() ([]byte, error)) (data []byte, err error) { + if b, ok := m.bytesCache.Get(key); ok { + return b.([]byte), nil + } + + if data, err = fn(); err != nil { + return data, err + } + if m.allowed(data) { + m.bytesCache.Add(key, data) + atomic.AddInt64(&m.currentSize, int64(len(data))) + + if m.maxCacheSize > 0 && atomic.LoadInt64(&m.currentSize) > m.maxCacheSize { + for atomic.LoadInt64(&m.currentSize) > m.maxCacheSize { + m.bytesCache.RemoveOldest() + } + } + } + return data, nil +} + +// Flush clears cache and calls postFlushFn async +func (m *memoryCache) Flush(scopes ...string) { + + if len(scopes) == 0 { + m.bytesCache.Purge() + go m.postFlushFn() + return + } + + // check if fullKey has matching scopes + inScope := func(fullKey string) bool { + for _, s := range scopes { + _, keyScopes, err := ParseKey(fullKey) + if err != nil { + return false + } + for _, ks := range keyScopes { + if ks == s { + return true + } + } + } + return false + } + + // all matchedKeys should be collected first + // we can't delete it from locked section, it will lock on eviction callback + matchedKeys := []string{} + for _, k := range m.bytesCache.Keys() { + key := k.(string) + if inScope(key) { + matchedKeys = append(matchedKeys, key) + } + } + for _, mkey := range matchedKeys { + m.bytesCache.Remove(mkey) + } + + if m.postFlushFn != nil { + go m.postFlushFn() + } +} + +func (m *memoryCache) allowed(data []byte) bool { + if m.maxValueSize > 0 && len(data) >= m.maxValueSize { + return false + } + return true +} diff --git a/app/rest/cache/memory_test.go b/app/rest/cache/memory_test.go new file mode 100644 index 00000000..b4b5f99b --- /dev/null +++ b/app/rest/cache/memory_test.go @@ -0,0 +1,299 @@ +package cache + +import ( + "fmt" + "math/rand" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMemoryCache_Get(t *testing.T) { + var postFnCall, coldCalls int32 + lc, err := NewMemoryCache(PostFlushFn(func() { atomic.AddInt32(&postFnCall, 1) })) + require.Nil(t, err) + res, err := lc.Get("key", func() ([]byte, error) { + atomic.AddInt32(&coldCalls, 1) + return []byte("result"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "result", string(res)) + assert.Equal(t, int32(1), atomic.LoadInt32(&coldCalls)) + assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall)) + + res, err = lc.Get("key", func() ([]byte, error) { + atomic.AddInt32(&coldCalls, 1) + return []byte("result"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "result", string(res)) + assert.Equal(t, int32(1), atomic.LoadInt32(&coldCalls)) + assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall)) + + lc.Flush() + time.Sleep(100 * time.Millisecond) // let postFn to do its thing + assert.Equal(t, int32(1), atomic.LoadInt32(&postFnCall)) + + _, err = lc.Get("key", func() ([]byte, error) { + return nil, errors.New("err") + }) + assert.NotNil(t, err) +} + +func TestMemoryCache_MaxKeys(t *testing.T) { + var postFnCall, coldCalls int32 + lc, err := NewMemoryCache(PostFlushFn(func() { atomic.AddInt32(&postFnCall, 1) }), + MaxKeys(5), MaxValSize(10)) + require.Nil(t, err) + + // put 5 keys to cache + for i := 0; i < 5; i++ { + res, e := lc.Get(fmt.Sprintf("key-%d", i), func() ([]byte, error) { + atomic.AddInt32(&coldCalls, 1) + return []byte(fmt.Sprintf("result-%d", i)), nil + }) + assert.Nil(t, e) + assert.Equal(t, fmt.Sprintf("result-%d", i), string(res)) + assert.Equal(t, int32(i+1), atomic.LoadInt32(&coldCalls)) + assert.Equal(t, int32(0), atomic.LoadInt32(&postFnCall)) + } + + // check if really cached + res, err := lc.Get("key-3", func() ([]byte, error) { + return []byte("result-blah"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "result-3", string(res), "should be cached") + + // try to cache after maxKeys reached + res, err = lc.Get("key-X", func() ([]byte, error) { + return []byte("result-X"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "result-X", string(res)) + + assert.Equal(t, 5, lc.(*memoryCache).bytesCache.Len()) + + // put to cache and make sure it cached + res, err = lc.Get("key-Z", func() ([]byte, error) { + return []byte("result-Z"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "result-Z", string(res)) + + res, err = lc.Get("key-Z", func() ([]byte, error) { + return []byte("result-Zzzz"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "result-Z", string(res), "got cached value") + assert.Equal(t, 5, lc.(*memoryCache).bytesCache.Len()) +} + +func TestMemoryCache_MaxValueSize(t *testing.T) { + lc, err := NewMemoryCache(MaxKeys(5), MaxValSize(10)) + require.Nil(t, err) + // put good size value to cache and make sure it cached + res, err := lc.Get("key-Z", func() ([]byte, error) { + return []byte("result-Z"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "result-Z", string(res)) + + res, err = lc.Get("key-Z", func() ([]byte, error) { + return []byte("result-Zzzz"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "result-Z", string(res), "got cached value") + + // put too big value to cache and make sure it is not cached + res, err = lc.Get("key-Big", func() ([]byte, error) { + return []byte("1234567890"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "1234567890", string(res)) + + res, err = lc.Get("key-Big", func() ([]byte, error) { + return []byte("result-big"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "result-big", string(res), "got not cached value") +} + +func TestMemoryCache_MaxCacheSize(t *testing.T) { + lc, err := NewMemoryCache(MaxKeys(50), MaxCacheSize(20)) + require.Nil(t, err) + + // put good size value to cache and make sure it cached + res, err := lc.Get("key-Z", func() ([]byte, error) { + return []byte("result-Z"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "result-Z", string(res)) + assert.Equal(t, int64(8), lc.(*memoryCache).currentSize) + + _, err = lc.Get("key-Z2", func() ([]byte, error) { + return []byte("result-Z"), nil + }) + assert.Nil(t, err) + assert.Equal(t, int64(16), lc.(*memoryCache).currentSize) + + // this will cause removal + _, err = lc.Get("key-Z3", func() ([]byte, error) { + return []byte("result-Z"), nil + }) + assert.Nil(t, err) + assert.Equal(t, int64(16), lc.(*memoryCache).currentSize) + + assert.Equal(t, 2, lc.(*memoryCache).bytesCache.Len()) +} + +func TestMemoryCache_MaxCacheSizeParallel(t *testing.T) { + lc, err := NewMemoryCache(MaxCacheSize(123), MaxKeys(10000)) + require.Nil(t, err) + + wg := sync.WaitGroup{} + for i := 0; i < 1000; i++ { + wg.Add(1) + i := i + go func() { + time.Sleep(time.Duration(rand.Intn(100)) * time.Nanosecond) + defer wg.Done() + res, err := lc.Get(fmt.Sprintf("key-%d", i), func() ([]byte, error) { + return []byte(fmt.Sprintf("result-%d", i)), nil + }) + require.Nil(t, err) + require.Equal(t, fmt.Sprintf("result-%d", i), string(res)) + size := atomic.LoadInt64(&lc.(*memoryCache).currentSize) + require.True(t, size < 200 && size >= 0, "unexpected size=%d", size) // won't be exactly 123 due parallel + }() + } + wg.Wait() + assert.True(t, lc.(*memoryCache).currentSize < 123 && lc.(*memoryCache).currentSize >= 0) + t.Log("size=", lc.(*memoryCache).currentSize) +} + +func TestMemoryCache_Parallel(t *testing.T) { + var coldCalls int32 + lc, err := NewMemoryCache() + require.Nil(t, err) + + res, err := lc.Get("key", func() ([]byte, error) { + return []byte("value"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "value", string(res)) + + wg := sync.WaitGroup{} + for i := 0; i < 1000; i++ { + wg.Add(1) + i := i + go func() { + defer wg.Done() + res, err := lc.Get("key", func() ([]byte, error) { + atomic.AddInt32(&coldCalls, 1) + return []byte(fmt.Sprintf("result-%d", i)), nil + }) + require.Nil(t, err) + require.Equal(t, "value", string(res)) + }() + } + wg.Wait() + assert.Equal(t, int32(0), atomic.LoadInt32(&coldCalls)) +} + +func TestMemoryCache_Scopes(t *testing.T) { + lc, err := NewMemoryCache() + require.Nil(t, err) + + res, err := lc.Get(Key("key", "s1", "s2"), func() ([]byte, error) { + return []byte("value"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "value", string(res)) + + res, err = lc.Get(Key("key2", "s2"), func() ([]byte, error) { + return []byte("value2"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "value2", string(res)) + + assert.Equal(t, 2, lc.(*memoryCache).bytesCache.Len()) + lc.Flush("s1") + assert.Equal(t, 1, lc.(*memoryCache).bytesCache.Len()) + + lc.Get(Key("key2", "s2"), func() ([]byte, error) { + assert.Fail(t, "should stay") + return nil, nil + }) + + res, err = lc.Get(Key("key", "s1", "s2"), func() ([]byte, error) { + return []byte("value-upd"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "value-upd", string(res), "was deleted, update") +} + +func TestMemoryCache_Flush(t *testing.T) { + lc, err := NewMemoryCache() + require.Nil(t, err) + + addToCache := func(key string, scopes ...string) { + res, err := lc.Get(key, func() ([]byte, error) { + return []byte("value" + key), nil + }) + require.Nil(t, err) + require.Equal(t, "value"+key, string(res)) + } + + init := func() { + lc.Flush() + addToCache(Key("key1", "s1", "s2")) + addToCache(Key("key2", "s1", "s2", "s3")) + addToCache(Key("key3", "s1", "s2", "s3")) + addToCache(Key("key4", "s2", "s3")) + addToCache(Key("key5", "s2")) + addToCache(Key("key6")) + addToCache(Key("key7", "s4", "s3")) + require.Equal(t, 7, lc.(*memoryCache).bytesCache.Len(), "cache init") + } + + tbl := []struct { + scopes []string + left int + msg string + }{ + {[]string{}, 0, "full flush, no scopes"}, + {[]string{"s0"}, 7, "flush wrong scope"}, + {[]string{"s1"}, 4, "flush s1 scope"}, + {[]string{"s2", "s1"}, 2, "flush s2+s1 scope"}, + {[]string{"s1", "s2"}, 2, "flush s1+s2 scope"}, + {[]string{"s1", "s2", "s4"}, 1, "flush s1+s2+s4 scope"}, + {[]string{"s1", "s2", "s3"}, 1, "flush s1+s2+s3 scope"}, + {[]string{"s1", "s2", "ss"}, 2, "flush s1+s2+wrong scope"}, + } + + for i, tt := range tbl { + init() + lc.Flush(tt.scopes...) + assert.Equal(t, tt.left, lc.(*memoryCache).bytesCache.Len(), "keys size, %s #%d", tt.msg, i) + } +} + +func TestMemoryCache_FlushFailed(t *testing.T) { + lc, err := NewMemoryCache() + require.Nil(t, err) + val, err := lc.Get("invalid-composite", func() ([]byte, error) { + return []byte("value"), nil + }) + assert.Nil(t, err) + assert.Equal(t, "value", string(val)) + assert.Equal(t, 1, lc.(*memoryCache).bytesCache.Len()) + + lc.Flush("invalid-composite") + assert.Equal(t, 1, lc.(*memoryCache).bytesCache.Len()) +} diff --git a/app/rest/cache/options.go b/app/rest/cache/options.go index e077cf8f..2902b0fa 100644 --- a/app/rest/cache/options.go +++ b/app/rest/cache/options.go @@ -1,12 +1,12 @@ package cache // Option func type -type Option func(lc *loadingCache) error +type Option func(lc *memoryCache) error // MaxValSize functional option defines the largest value's size allowed to be cached // By default it is 0, which means unlimited. func MaxValSize(max int) Option { - return func(lc *loadingCache) error { + return func(lc *memoryCache) error { lc.maxValueSize = max return nil } @@ -15,7 +15,7 @@ func MaxValSize(max int) Option { // MaxKeys functional option defines how many keys to keep. // By default it is 0, which means unlimited. func MaxKeys(max int) Option { - return func(lc *loadingCache) error { + return func(lc *memoryCache) error { lc.maxKeys = max return nil } @@ -24,7 +24,7 @@ func MaxKeys(max int) Option { // MaxCacheSize functional option defines the total size of cached data. // By default it is 0, which means unlimited. func MaxCacheSize(max int64) Option { - return func(lc *loadingCache) error { + return func(lc *memoryCache) error { lc.maxCacheSize = max return nil } @@ -32,7 +32,7 @@ func MaxCacheSize(max int64) Option { // PostFlushFn functional option defines how callback function called after each Flush. func PostFlushFn(postFlushFn func()) Option { - return func(lc *loadingCache) error { + return func(lc *memoryCache) error { lc.postFlushFn = postFlushFn return nil }