diff --git a/app/rest/cache/cache.go b/app/rest/cache/cache.go index c32ce04b..c335e4d6 100644 --- a/app/rest/cache/cache.go +++ b/app/rest/cache/cache.go @@ -2,14 +2,12 @@ package cache import ( "log" - "net/http" "strings" "sync" "time" "github.com/patrickmn/go-cache" "github.com/pkg/errors" - "github.com/umputun/remark/app/rest" ) // LoadingCache defines interface for caching @@ -50,7 +48,7 @@ type loadingCache struct { } // NewLoadingCache makes loadingCache implementation -func NewLoadingCache(options ...Option) LoadingCache { +func NewLoadingCache(options ...Option) *loadingCache { res := loadingCache{ defaultExpiration: time.Hour, cleanupInterval: 5 * time.Minute, @@ -104,6 +102,7 @@ func (lc *loadingCache) Flush(scopes ...string) { if len(scopes) == 0 { lc.bytesCache.Flush() + lc.withLock(func() { lc.activeKeys = map[string]struct{}{} }) go lc.postFlushFn() return } @@ -152,51 +151,3 @@ func (lc *loadingCache) allowed(data []byte) bool { } return true } - -// Option func type -type Option func(lc *loadingCache) 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 { - lc.maxValueSize = max - return nil - } -} - -// 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 { - lc.maxKeys = max - return nil - } -} - -// CleanupInterval functional option defines how often cleanup loop activated. -func CleanupInterval(interval time.Duration) Option { - return func(lc *loadingCache) error { - lc.cleanupInterval = interval - return nil - } -} - -// PostFlushFn functional option defines how callback function called after each Flush. -func PostFlushFn(postFlushFn func()) Option { - return func(lc *loadingCache) error { - lc.postFlushFn = postFlushFn - return nil - } -} - -// 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 { - adminPrefix := "admin!!" - key := strings.TrimPrefix(r.URL.String(), adminPrefix) // prevents attach with fake url to get admin view - if user, err := rest.GetUserInfo(r); err == nil && user.Admin { // make separate cache key for admins - key = adminPrefix + key - } - return key -} diff --git a/app/rest/cache/cache_test.go b/app/rest/cache/cache_test.go index 5c5e31aa..4352c237 100644 --- a/app/rest/cache/cache_test.go +++ b/app/rest/cache/cache_test.go @@ -188,7 +188,10 @@ func TestLoadingCache_Scopes(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "value2", string(res)) + assert.Equal(t, 2, len(lc.activeKeys)) lc.Flush("s1") + assert.Equal(t, 1, len(lc.activeKeys)) + lc.Get(Key("key2", "s2"), time.Minute, func() ([]byte, error) { assert.Fail(t, "should stay") return nil, nil @@ -200,6 +203,53 @@ func TestLoadingCache_Scopes(t *testing.T) { assert.Equal(t, "value-upd", string(res), "was deleted, update") } +func TestLoadingCache_Flush(t *testing.T) { + lc := NewLoadingCache(CleanupInterval(time.Second)) + + addToCache := func(key string, scopes ...string) { + res, err := lc.Get(key, time.Minute, 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, len(lc.activeKeys), "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, len(lc.activeKeys), "keys size, %s #%d", tt.msg, i) + assert.Equal(t, tt.left, len(lc.bytesCache.Items()), "items size, %s #%d", tt.msg, i) + + } +} + func TestLoadingCache_Keys(t *testing.T) { tbl := []struct { key string diff --git a/app/rest/cache/options.go b/app/rest/cache/options.go new file mode 100644 index 00000000..8fda85f6 --- /dev/null +++ b/app/rest/cache/options.go @@ -0,0 +1,57 @@ +package cache + +import ( + "net/http" + "strings" + "time" + + "github.com/umputun/remark/app/rest" +) + +// Option func type +type Option func(lc *loadingCache) 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 { + lc.maxValueSize = max + return nil + } +} + +// 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 { + lc.maxKeys = max + return nil + } +} + +// CleanupInterval functional option defines how often cleanup loop activated. +func CleanupInterval(interval time.Duration) Option { + return func(lc *loadingCache) error { + lc.cleanupInterval = interval + return nil + } +} + +// PostFlushFn functional option defines how callback function called after each Flush. +func PostFlushFn(postFlushFn func()) Option { + return func(lc *loadingCache) error { + lc.postFlushFn = postFlushFn + return nil + } +} + +// 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 { + adminPrefix := "admin!!" + key := strings.TrimPrefix(r.URL.String(), adminPrefix) // prevents attach with fake url to get admin view + if user, err := rest.GetUserInfo(r); err == nil && user.Admin { // make separate cache key for admins + key = adminPrefix + key + } + return key +}